Java8新特性
本文简单介绍了Java 8中比较重要的新特性,仅作参考入门的概览,若要深入了解熟练使用,还需多看多练。
Java 8新特性的核心就是简化代码,让代码操作更加优雅。
Java 8新特性:
- Lambda表达式:简化部分匿名内部类的操作,让代码简洁。
- 函数式接口:Lambda表达式的使用前提。
- 方法引用:Lambda表达式的得力帮手,让代码简洁。
- Stream流:重中之重,前面三个内容可以视为Stream流的铺垫,让集合操作更简洁。
- Optional:对判断null值的方便处理。
- 新日期时间API:摒弃旧的日期时间API,更加合理的日期时间API设计。
1. Lambda表达式
- Java为了简化代码,会使用匿名内部类,然而匿名内部类语法冗余,为了更加简化代码,Java 8引入了Lambda表达式。
- Lambda表达式是函数式编程思想的一种体现。
函数式编程:数学中的函数:2x+1,Java中的函数(Lambda表达式):(x) -> 2x+1。函数式编程是一种编程范式,它将函数的整体(如(x) -> 2x+1)视为对象,可以将其作为参数进行传递,或者作为返回值,甚至存储到变量中。
1.1 Lambda标准格式
Lambda表达式的标准格式如下:
1 | (参数类型 参数名称) -> { |
说明:
- (参数类型 参数名称) :参数列表
- (代码体):方法体
>
箭头:分隔参数列表和方法体,可以视为赋值标志
无参无返回值
1 | // 定义只含一个抽象方法的接口 |
1 | public static void main(String[] args) { |
含参含返回值
1 | // 定义只含一个抽象方法的接口 |
1 | public static void main(String[] args) { |
1.2 Lambda省略格式
Lambda表达式省略的规则
- 小括号内参数类型可以省略
- 若小括号内有且仅有一个参数,小括号可以省略
- 若大括号内有且仅有一条语句,可以同时省略大括号、return和结束分号
;
省略写法:
1 | public static void main(String[] args) { |
1.3 Lambda实现原理
匿名内部类在编译时会形成以一个.class文件,Lambda表达式则会在运行时生成一个私有方法,Lambda在程序编译执行期间做如下操作:
- 会在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码;
- 会生成一个匿名内部类,并实现接口,重写方法;
- 在接口的重写方法中会调用第一步中新生成的方法。
因此本质上,Lambda表达式就是对接口的实现和对接口中抽象方法的重写。
1.4 Lambda使用前置条件
使用Lambda表达式有两个前置条件:
- 方法的参数或局部变量类型必须为接口类型,才能使用Lambda表达式;
- 该接口中有且仅有一个抽象方法。
函数式接口
而对于只含有一个抽象方法的接口,我们称为函数式接口
。
函数式接口,即适用于函数式编程场景的接口。而Java中函数式编程的表现就是Lambda表达式,因此函数式接口可以适用于Lambda表达式中。只有确保接口中有且仅有一个抽象方法,Lambda表达式才能顺利进行推导,将方法体作为接口中抽象方法的重写。
@FunctionInterface注解
与@Oveerride注解的作用类似,Java 8中引入了一个新的注解@FunctionInterface,用于检测接口是否为函数式接口,若添加注解的接口不是函数式接口,则会报错提醒。
1.5 Lambda表达式与匿名内部类的比较
Lambda表达式虽然可以用于简化匿名内部类代码,但并非所有匿名内部类都可以用Lambda表达式代替,下面来看看二者的比较:
适用类型不一样
Lambda表达式所需的类型必须是接口,匿名内部类所需类型可以是类、抽象类、接口。
抽象方法数量不一样
Lambda表达式要求接口中有且仅有一个抽象方法,匿名内部类并无此要求。
实现原理不一样
Lambda表达式是程序运行时动态生成.class文件,匿名内部类是在编译后形成.clss文件。
2. 函数式接口
前面提到,有且仅有一个抽象方法的接口称为函数式接口
,而函数式接口是使用Lambda表达式的前置条件之一,只有在方法的参数类型是函数式接口时,才能将Lambda表达式作为参数传入,并重写接口中唯一的抽象方法。
在Java 8中,有很多内置的函数式接口,以供Lambda表达式使用,Java 8在接口中引入了默认方法
和静态方法
,来提高内置函数式接口的扩展性和适用性。
Java 8之前的接口:
1 | interface 接口名 { |
Java 8新增默认方法和静态方法,对接口进行增强后:
1 | interface 接口名 { |
2.1 默认方法
接口的默认方法不需要被实现类重写即可调用,有利于接口的扩展。
在Java 8之前,当我们需要对接口添加一个抽象方法进行扩展时,要对其所有实现类或子接口都进行方法重写,这样的工程量是十分浩大的,例如Java 8中
1 | Map<K, V> |
接口新增了一个
1 | forEach方法 |
,那么要对每一个实现类或子接口的Map都进行重写,显然是很不聪明的做法。
!https://img-blog.csdnimg.cn/direct/21c6043b9e92466283e2068d66797642.png
因此,Java 8引入了默认方法,它在接口中默认被所有子接口和实现类拥有,可以直接调用,而不需要进行重写,当然,也可以根据具体的需求将默认方法重写。
我们可以看看Map<K, V>接口对于forEach方法实现的源码:
1 | public interface Map<K,V> { |
可以看到,Map<K, V>接口使用default
对forEach方法进行修饰,这就是接口中默认方法的格式:
1 | interface 接口名 { |
综上,默认方法就是接口的子接口及实现类默认拥有的方法,不需要重写也可以调用,有利于对接口进行扩展,并且默认方法也可以根据实际需要被重写。
2.2 静态方法
接口的静态方法只能由接口名以接口名.静态方法名()
的形式调用,不需要通过实现类实例化出对象再进行调用,与一般类的静态方法类似,有利于接口的扩展。
静态方法的格式:
1 | interface 接口名 { |
同样的,我们可以在Map<K, V>的内部接口Entry<K, V>
中看到静态方法的身影:
1 | public interface Map<K,V> { |
上面的静态方法comparingByKey
在调用时直接以Map.Entry.comparingByKey(入参)
的形式调用即可,也不需要通过实现类来重写调用。
2.3 常用函数式接口
Java 8中为了方便使用Lambda表达式,提供了一系列内置的函数式接口,这些接口中只包含一个抽象方法。
由于使用Lambda表达式时,我们不关注接口的名称和其中抽象方法的名称,只关注抽象方法的入参和返回值
,因此这些内置的函数式接口我们只需要根据其抽象方法的入参和返回值传入Lambda表达式即可。
例如下面这个之前写的Lambda表达式,重写含参含返回值的抽象方法,在Lambda表达式的书写过程中,没有出现过接口名和抽象方法名,只需要参数列表与返回值与抽象方法一致即可,Java会默认将Lambda表达式推导到相应的接口抽象方法,进行重写。
1 | public static void main(String[] args) { |
内置函数式接口在rt包的java.util.function
包中可以看到,常用的内置函数式接口如下:
Suppiler:无参一个返回值,称为“供给型”接口。
1 |
|
Consumer:一个参数无返回值,称为“消费型”接口。
1 |
|
Function:一个参数一个返回值,参数称为前置条件,返回值称为后置条件。
1 |
|
Predicate:一个参数一个Boolean类型返回值,可以用来做判断。
1 |
|
3. 方法引用
方法引用的格式:::
双冒号是方法引用运算符,它所在的表达式称为方法引用
,如果Lambda表达式所要实现的方法体已经由现成的方法,可以通过方法引用直接丢给函数式接口作为抽象方法的重写,可以进一步简化Lambda表达式。
常见的引用方式包括:
- 对象引用成员方法:instanceName::methodName
- 类名引用静态方法:ClassName::staticMethodName
- 类名引用实例方法:ClassName::methodName,这种调用方式是将Lambda表达式的第一个参数作为实例方法的调用者,而非真的直接通过类名调用成员方法。
- 引用类的构造器:ClassName::new
- 引用数组的构造器:TypeName[]::new
注意:以方法引用
的方式引用的方法,其参数列表与返回值必须与函数式接口的抽象方法一致,因为最后引用的方法还是要重写函数式接口的抽象方法的。
4. Stream流
4.1 Stream流的作用
对于集合进行操作很重,一般都要通过for循环进行,并在for循环中进行逻辑处理,十分繁琐。
为了简化集合的操作,Java 8引入了Stream流
,它就像流水线,可以对集合进行流水线式的加工处理。下面是一般集合使用for循环与Stream流的对比:
使用for循环:
1 | public static void main(String[] args) { |
使用Stream流:
1 | public static void main(String[] args) { |
上面代码中,filter()
是Stream流的过滤方法,用来筛选符合条件的元素,用Lambda表达式
进行重写;forEach()
是Stream流的遍历方法,同样可以用Lambda表达式重写,而此处因为打印方法已经存在,可以用方法引用
来引用现有的打印方法。
可以看到,集合使用Stream流可以大大减少集合操作的代码量,并且在Stream流中经常使用到Lambda表达式与方法引用,可以帮助我们进一步简化代码。
4.2 获取Stream流
常用的获取Stream流有三种方式:
- Collection接口的默认方法:Collection.stream()
因为Collection中以默认方法的形式定义了stream()方法,因此Collection接口的所有实现类,如ArrayList、HashSet都可以用实例直接获取Stream流;而对于Map接口的实现类,可以通过Map实现类的keySet()、values()、entrySet()来获取流,进而实现对Map实现类的Stream流操作。
1 | List<String> list = new ArrayList<>(); |
- Stream接口的静态方法of():Stream.of(…T)
这种方法一般用作引用类型数组获取Stream流的场景。
1 | String[] strings = {"Superman", "Batman", "Wonder Woman"}; |
- Arrays类的静态方法stream():Arrays.stream(T[] array)
Arrays.stream(T[] array)
可以获取引用类型数组的Stream流,也可以获取基本数据类型数组的Stream流。
1 | String[] strings = {"Superman", "Batman", "Wonder Woman"}; |
4.3 Stream流的常用方法
Stream流常用的方法分为两种:
- 非终结方法:返回值为Stream类型,支持使用链式调用,后面可以接方法,一般作为Stream流中间处理的方法。
- 终结方法:返回值为非Stream类型,不支持链式调用,一般作为Stream流处理结束的标志。
Stream流常用方法如下表(可以作为粗略理解,要熟练使用还需要多写多练)
方法 | 作用 | 返回值 | 是否为终结方法 |
---|---|---|---|
filter(Predicate<? super T> predicate) | 过滤出流中符合条件的元素 | Stream |
非终结方法 |
map(Function<? super T,? extends R> mapper) | 将流中元素映射为另一个类型 | 非终结方法 | |
sorted() | 将流中元素按自然顺序排序 | Stream |
非终结方法 |
concat(Stream<? extends T> a, Stream<? extends T> b) | 将两个流归为一个流 | Stream |
非终结方法 |
limit(long maxSize) | 获取流中前maxSize个元素 | Stream |
非终结方法 |
distinct() | 对流中元素去重 | Stream |
非终结方法 |
skip(long n) | 跳过流中前n个元素 | Stream |
非终结方法 |
forEach(Consumer<? super T> action) | 对流中元素进行遍历操作 | void | 终结方法 |
collect(Collector<? super T,A,R> collector) | 将流中元素收集到指定的集合类型 | 终结方法 |
其他流中的常用方法可以参考Java 8的在线帮助文档:Java 8 中文版 - 在线API手册 - 码工具
下面通过一个综合案例,演示Stream流中的常用方法:
有正义联盟(Justice League)和复仇者联盟(The Avengers)两支超级英雄队伍,下面对这两支队伍的成员进行如下操作:
- 正义联盟只要名字以“n”结尾的成员;
- 正义联盟筛选后只要前3个人;
- 复仇者联盟只要名字含“空格”的成员;
- 复仇者联盟筛选后不要前2个人;
- 将筛选后的两支队伍合并成一支队伍;
- 根据名字创建
Hero
对象; - 打印整个队伍的
Hero
对象信息。
综合案例代码:
1 | public static void main(String[] args) { |
Hero类:
1 | public class Hero { |
以上只是部分Stream流常用方法的一个简单案例,其他诸如对Stream流的分组、聚合计算、数据拼接这里不做展开,还是那句话:“菜就多练练”。
5. Optional 类
5.1 Optional类的作用
在Java 8之前,我们经常需要对数据是否为空进行判断,以避免空指针异常
,如下代码:
1 | public static void main(String[] args) { |
而在Java 8引入了Optional类
,它是一个没有子类的工具类,是一个可以存放null值的容器对象,其主要作用就是避免null检查,避免空指针异常NullPointerException
的报错。
5.2 Optional类的使用
创建Optional实例:
1 | Optional.of(T t):创建一个Optional实例,入参不能为null。 |
Optional类常用方法
1 | isPresent():判断值是否含有值,含值返回true,不含值返回false。 |
Optional类常用方法示例
1 | public static void main(String[] args) { |
Optional类方法进阶使用
1 | public static void main(String[] args) { |
6. Date Time API
6.1 Java 8之前日期时间API存在的问题
在Java 8之前,日期时间API存在以下问题:
- 设计差:在
java.util
和java.sql
的包中都含有日期Date类,前者的Date包含时间日期,后者的Date只包含日期。 - 非线程安全:
java.util.Date
是非线程安全的,所有日期类都是可变的,这是Java日期类最大的问题之一。 - 时区处理麻烦:Date类并不提供国际化,没有时区支持,因此Java引入了
java.util.Calenda
和java.util.TimeZone
类,单它们同样存在上述问题。
6.2 Java 8新的日期时间类
Java 8中新增了一套全新的日期时间API,设计合理,线程安全,使用方便。新的日期时间API存在于java.time
包下,以下是一些关键类:
- LocalDate:表示日期,包含
年月日
,格式如2024-05-19
。 - LocalTime:表示时间,包含
时分秒
,格式如03:59:29.415
。 - LocalDateTime:表示日期时间,包含
年月日时分秒
,格式为2024-05-19T04:00:36.508
- DateTimeFormatter:日期时间格式化类,用于将日期时间转换为指定的格式,如
yyyy-MM-dd HH:mm:ss
。 - Instant:时间戳类。
- Duration:用于计算两个时间(LocalTime,时分秒)的距离。
- Period:用于计算两个日期(LocalDate,年月日)的距离。
- ZoneDateTime:包含时区的时间。
Java 8新的日期时间类使用起来还是比较简单的,这里也不做赘述,多敲点demo就能快速上手,下面贴上日期时间类常用的方法:
ps:由于LocalDate、LocalTime、LocalDateTime这三个类有很多方法是相似的,只是返回值或操作的入参,分别对应年月日、时分秒、年月日时分秒进行操作,因此这里偷懒贴下三者的共名方法。
1 | now():获取当前年月日/时分秒。 |
如果对新日期时间API有疑惑,可以参考Java在线帮助文档:Java 8 中文版 - 在线API手册 - 码工具