本文介绍Java8新特性。
1. 概述
Java8是最大的一个版本改动。具有以下新特性:
- 速度更快
- 代码更少(增加了新的语法Lambda表达式)
- 强大的Stream API
- 便于并行(fork join)
- 最大化减少空指针异常(Optional容器类)
其中最为核心的为Lambda表达式与StreamAPI。除此之外,还有以下几种新特性。
2. Lambda表达式
2.1 概述
Lambda是一个匿名函数,可以把Lambda表达式理解为是一段可以传递的代码(将代码和数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
匿名函数,即函数没有名字。和接口的匿名实现类类似,没有类名,直接实现接口,并创建对象。
以前的Comparator的匿名实现类,需要实现该接口,实现compare方法。但是通过Lambda表达式,可直接将compar方法的实现代码作为匿名实现类。案例如下所示:
1 | // 传统方式 |
通过lambda表达式,可以很简单地匿名实现接口类。这样的话,对于一个接口,如果实现类的具体实现方法都不一样,显然就需要创建多个类,即使这个类就使用一次,也要实现这个类,并且实现方法。
显然这是不合理的,因此可匿名实现接口。但是传统的匿名实现接口,比较繁琐,那些接口名、返回值类型等等,都是比较麻烦的,因此简化之后,就是lambda表达式了。
以下是自己的错误思维:
其实,如果究其本质,lambda其实就是一个函数,并不是什么接口,上面只是从接口的例子来引出lambda的方便。
这个函数是匿名的,只需要将函数需要的参数,和函数体写出来即可。
本质上,Lambda其实就是匿名接口实现类的简化版。但是这个接口必须只有一个抽象方法,这样,才能默认就是实现的这个抽象方法。只有一个抽象方法的接口称为函数式接口。
针对函数体内,匿名内部类的一些特点,比如无法修改函数体内局部变量的值,其实对于Lambda表达式也是同样适用的。
2.2 基础语法
Lambda表达式的操作符为:”->”,这个符号称为箭头操作符或者Lambda操作符。该操作符将Lambda表达式分成了两部分:
- 左侧:lambda表达式的参数列表,即Lambda参数列表。即函数式接口中抽象方法的形参列表。
- 右侧:lambda表达式所需要执行的功能,即Lambda体。即函数式接口中抽象方法的实现体。
既然是抽象方法的简化版,显然抽象方法可以有返回值,也可没有返回值,返回值的类型也是多样的。另外,抽象方法的形参列表,可以没有参数,也可以有多个,并且参数的数据类型也是多样的。
那么对应于Lambda表达式,该有哪些具体的类型呢?
这里需要说明一点,对于返回值类型,以及参数列表的类型,其实无需自己规定,Lambda底层会通过类型推断自己识别出来。
语法格式有以下几种:
无参无返回值:
这里不再自己写接口,线程的接口就是Runnable接口,而且该接口中只有一个抽象方法,显然Runnable接口是函数式接口。
1
2
3
4
5
6
7
8
9
10// 传统方式
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello World");
}
};
// Lambda表达式
Runnable r2 = () -> System.out.println("Hello World!");因此语法格式为:
() -> Lambda体
有一个参数,无返回值:
语法格式为:
(参数) -> Lambda体
如果只有一个参数,左侧小括号可以不写。
1
2
3
4
5
6// 有参,只有一个参数,左侧小括号可以不写。
Consumer<String> consumer1 = (s) -> System.out.println(s);
consumer1.accept("hianian");
Consumer<String> consumer2 = s -> System.out.println(s);
consumer2.accept("hianian");有多个参数,有返回值,并且有多条语句:
1
2
3
4
5
6Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
int result = com.compare(1, 2);语法和上面的类似,只不过返回值需要return关键字返回,多条语句则需要用大括号代码括起来。语法:
(参数列表) -> {Lambda体}
。有多个参数,有返回值,只有一条语句:
这种形式,return关键字和大括号都可以省略不写。
这里有个疑问,如果省略return关键字,那么和没有返回值有什么区别呢?它怎么知道是否有无返回值呢?
我认为,本质上还是要回归到该接口中原始的方法上,似乎因为是函数式接口,可以唯一定位到那个抽象方法,也就可以知道是否有返回值。
Lambda的参数列表的数据类型可以省略不写,疑问JVM编译器通过上下文推断出数据类型,称为“类型推断”。实际上仍然是函数式接口,可以找到唯一的抽象方法,找到其参数列表类型。
其他的类型推断有:
1
2
3
4
5
6
7
8
9
10// 类型推断成功,
String[] strs = {"asdf", "qwer"};
// 类型推断失败,因为无法知道推断出strs的类型。
Strign strs;
strs = {"qwer"};
// 类型推断成功,钻石表达式
// 后面的尖括号中无需写泛型。
List<String> list = new ArrayList<>();
可以看到,Lambda表达式就是匿名实现类的简化版本,缺点就是代码不能复用。优点就是对于代码量少的实现类,无需新建一个类来显示实现该接口。因此Lambda一般情况下用于代码量少的情况,比如一行代码或者两行代码,或者这段代码出现的概率较低,无需代码复用。
3. 函数式接口
3.1 概述
若接口中只有一个抽象方法,那么这个接口就是函数式接口。可以使用注解@FunctionalInterface
修饰,该注解可以检查该接口是否是函数式接口。
注意,这里的接口中的方法指的是自定义的方法,不是继承自Object类的(比如equals),也不是default修饰的。换句话说,继承自Object类中的方法不是抽象方法,default修饰的也不是抽象方法。
注意,接口实际上就是一种特殊的类,因此也是继承自Object。
3.2 四大常用函数式接口
其实上面的例子中可以看出,如果采用Lambda表达式,那么就必须新建一个函数式接口来支持Lambda表达式,显然这对开发者来说是不友好的。因此Java内置了四大常用函数式接口。
函数式接口 | 参数类型 | 返回类型 | 用途 |
---|---|---|---|
Consumer<T> | T | void | 消费型接口,对类型为T的对象应用操作,包含方法void accept(T t) 。 |
Supplier<T> | 无 | T | 供给型接口,返回类型为T的对象,包含方法T get() 。 |
Function<T, R> | T | R | 函数型接口,对类型为T的对象应用操作,并返回结果。结果是R类型的对象,包含方法:R apply(T t) 。 |
Predicate<T> | T | boolean | 断定型接口,确定类型为T的对象是否满足某约束,并返回boolean值。包含方法boolean test(T t) 。 |
这几个常用接口均在java.util.function
包下。
- Consumer<T>接口,里面的accept方法,没有返回值,有参数T对象,其实就是对T对象参数进行操作,消费这个对象,并不会产出。
- Supplier<T>接口,里面的get方法,返回值类型为T,没有参数,其实就是获取T类型的对象。提供这个对象,没有消费支出,但是有产出。
- Function<T, R>,里面的apply方法,返回值类型为R,参数为T对象,这个就相当于上面两者的融合。相当于函数,既有输入也有输出、
- Predicate<T>,里面的test方法,返回值类型为boolean,参数类型为T,其实就是相当于一个判断函数,对T进行操作,并返回true或者false。相当于对输入的参数条件进行判断。
4. 方法引用与构造器引用
4.1 方法引用
Lambda体中的代码就是普通的Java代码,如果这段代码的功能已经由其他方法实现了,可以直接调用该方法。换句话说,Lambda体就是一行方法调用,显然本质上,直接调用该方法就能完成任务。
这里引用其他的方法称为方法引用。方法引用是Lambda表达式的另一种表现形式。
具体的语法格式主要有以下三种:
对象::实例方法名
注意,方法名无需加括号,案例如下所示,两种方式是等价的。注意,这里因为是直接省略了左侧形参和右侧方法调用时实参,所以,函数式接口中的抽象方法和方法引用的方法的形参类型、返回值类型必须要保持一致。
1
2
3
4
5Consumer<String> con = (x) -> System.out.println(x);
PrintStream ps = System.out;
Consumer<String> con2 = ps::println;
Consumer<String> con3 = System.out::println;这其实就相当于将直接将参数传给了方法引用。
类::静态方法名
这种方式和上面的类似,案例如下所示:
1
2
3
4
5Comparator<Integer> com1 = (x, y) -> Integer.compare(x, y);
// 这里的比较器的具体实现代码就是Integer类中的静态方法,
// 所以可以直接方法引用。
Comparator<Integer> com2 = Integer::compare;类::实例方法名
1
2
3
4
5
6
7// 情景:传入两个参数,返回一个结果
// 这里是test(x,y)方法的代码实现,因为只有一行,所以可直接写。右侧是Lambda体。
BiPredicate<String, String> bp = (x, y) -> x.equals(y);
// 注意,这里是简化后的,采用:类名::实例方法 来简化
// 这种简化方式有要求:即实例方法的调用是:第一个参数.实例方法(第二个参数),此时就可以采用如下方式简化。
BiPredicate<String, String> bp2 = String::equals;
4.2 构造器引用
和方法引用类似,构造器引用的返回值以及参数列表要和函数式接口方法的返回值、参数列表一一对应。
构造器引用的语法如下所示:
类名::new
代码如下所示:
1
2
3
4
5
6
7
8
9
10
11Supplier<String> sup = () -> new String();
// 因为Supplier接口中的方法是无参的,所以调用的就是String中无参构造方法
Supplier<String> sup2 = String::new;
System.out.println(sup.get());
System.out.println(sup2.get());
Function<String, String> func = (x) -> new String(x);
// 因为Function接口中的方法是有参的,所以调用的就是String中有参构造方法
Function<String, String> func2 = String::new;
System.out.println(func.apply("asdf"));
System.out.println(func2.apply("qwer"));如果是数组的话,则类名后面加中括号即可。
5. Stream API
5.1 Stream概述
Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似使用SQL执行的数据库查询。也可以使用StreamAPI来执行并行操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。
注意,这里的Stream流和IO流是不一样的。但核心目的仍然是传输数据。
换句话说,这里的Stream流其实就是:
- 对某个数据转换成流,【不影响原始数据源】
- 以流的形式展开一系列操作,
- 对上面的操作结果,产生一个新的流。
流(Stream)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
集合讲的是数据,而流讲的是计算。
注意:
- Stream自己不会存储元素。
- Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream
- Stream操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。
整个Stream流的操作有三个步骤:
创建Stream流
一个数据源(如:集合、数组),获取一个流
中间操作
一个中间操作链,对数据源的数据进行处理
终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果
5.2 创建Stream
Stream是一个接口,全限定类名为:java.util.stream.Stream
通过Collection系列集合提供的stream()或parallelStream()获取到Stream流对象
1
2
3
4List<Integer> list = new ArrayList<>();
Stream<Integer> stream = list.stream();
Stream<Integer> parallel = list.parallelStream();通过Arrays中的静态方法stream方法将数组转换成stream流对象。
1
2Integer[] array = new Integer[6];
Stream<Integer> stream1 = Arrays.stream(array);通过Stream类中的静态方法of()方法
1
Stream<String> aa = Stream.of("aa", "bvv", "qwer");
创建无限流
以迭代器形式创建无限流,即迭代器可以无限产生元素,本质上是从迭代器中产生流。
1
2
3Stream<Integer> iterate = Stream.iterate(0, (x) -> x + 2);
// 中间操作(限制10个)加终止操作
iterate.limit(10).forEach(System.out::println);以生成器形式创建无限流,生成器也可无限产生元素,本质上是从生成器中产生流。
1
2
3Stream<Double> generate = Stream.generate(Math::random);
// 中间操作(限制10个)加终止操作
generate.limit(10).forEach(System.out::println);
5.3 中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。换句话说,这些中间操作,并不会马上执行,而是在终止操作时一次性全部执行。因此说,如果单纯的中间操作,是不会有任何外观上的结果的。
对于Stream,中间操作有如下几种:
注意,这些操作都是实例方法,并不是静态方法。
5.3.1 筛选与切片
方法 | 描述 |
---|---|
filter(Predicate p) | 接收Lambda,从流中排除某些元素【保留filter条件元素】。返回一个新的流。 |
distinct() | 筛选,通过流所生成元素的hashCode()和equals()去除重复元素。返回一个新的流。 |
limit(long maxSize) | 截断流,使其元素不超过给定数量。返回一个新的流。 |
skip(long n) | 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)是互补的。 |
案例如下所示:
1 | // 按照某种形式来迭代产生数据 |
上面的filter和limit就是一系列的中间操作,先过滤出整除5的元素,然后取前10个,之后,再跳过这10个中的前8个,最后输出终止操作。
5.3.2 映射
方法 | 描述 |
---|---|
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream。 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。 |
感觉map映射就是遍历数组or集合中的每个元素,对其进行相同操作。
Function f,这个方法是返回一个流对象,而flatMap则是将若干个流对象,串联整合成一个流对象。
5.3.3 排序
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序(按照字典排序内置的Comparable) |
sorted(Comparator comp) | 产生一个新流,其中按比较器顺序排序(自定义规则Comparator) |
5.4 终止操作
注意,一个流,只要终止操作了,那么就会被关闭,后续不能再被使用该流。
5.4.1 查找与匹配
方法 | 描述 |
---|---|
allMatch | 检查是否匹配所有元素 |
anyMatch | 检查是否至少匹配一个元素 |
noneMatch | 检查是否没有匹配所有元素 |
findFirst | 返回第一个元素 |
findAny | 返回当前流中的任意元素 |
count | 返回流中元素的总个数 |
max | 返回流中的最大值 |
min | 返回流中的最小值 |
上述的匹配方法,返回结果都是布尔值。下面的5个方法返回值类型均为Optional容器类型,可避免空指针异常。
5.4.2 规约
方法 | 描述 |
---|---|
reduce(T identity, BinaryOperator) | 将流中元素按照BinaryOperator规则进行规约运算 |
reduce(BinaryOperator) | 将流中元素按照BinaryOperator规则进行规约运算 |
规约就是对流中所有元素进行计算,比如累加累减等等。identity是起始值,即将这个值开始规约,后面依次是流中的元素,所以结果必不为空。如果没有identity,那么结果就可能为空,所以为Optional类型。
备注:map和reduce操作非常注明,二者的连接通常被称为map-reduce模式,因Google用它来进行网格搜索而出名。
5.4.3 收集
方法 | 描述 |
---|---|
collect(Collector collector) | 将流转换为其他方式,接收一个Collector接口实现,用于流汇总 |
collect(Supplier supplier, BiConsumer consumer) | 将流按照指定的收集器方式进行收集 |
Collectors提供了很多静态方法,返回Collector接口实现类。
1 | List<Integer> collect = iterate.limit(10).collect(Collectors.toList()); |
注意,Collectors中提供了很多收集工具方法。除了上面的转换成list的收集器,也可以分组、多级分组、分区、最大值、最小值等等。
5.5 并行流与顺序流
在这里先了解一下Java7中的Fork/Join框架:就是在必要的情况下,将一个大人物,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总。重点在于分割成若干个小任务,然后将这些小任务压入到每个线程的队列中,但是为了避免,某些线程效率较高,执行速度较快,导致很快空闲;而有些线程则效率低,处于繁忙状态,即实时资源分配不均。所以采用“工作窃取模式”,速度较快的线程会从速度较慢的线程中的任务队列中偷取任务,自己执行。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程就会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了线程的等待时间,提高了性能。
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。Java8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API可以声明性地通过parallel()与sequential()在并行流与顺序流之间进行切换。换句话说,这里的并行流实际上也就是fork/join的实现。因为fork/join框架手动开发比较困难。
顺序流就是stream,可以直接获取parallelStream(),或者parallel()将顺序流转换成并行流。这样,后续针对该流的操作,底层默认使用多线程了。但是一定是大数量下,如果是小数据量,因为多线程分配任务实际上还会有额外开销的,这里的开销还不足以弥补小数量下多线程的提升效果。
5.6 Optional容器类(了解)
Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在,原来用null表示一个值不存在,现在Optional可以更好的表达这个概念。并且可以避免空指针异常。常用方法如下:
方法 | 描述 |
---|---|
Optional.of(T t) | 创建一个Optional实例 |
Optional.empty() | 创建一个空的Optional实例 |
Optional.ofNullable(T t) | 若t不为null,则创建Optional实例,否则创建空实例 |
isPresent() | 判断是否包含值 |
orElse(T t) | 如果调用对象包含之,返回该值,否则返回t |
orElseGet(Supplier s) | 如果调用对象包含值,返回该值,否则返回s获取的值 |
map(Function f) | 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty() |
flatMap(Function mapper) | 与map类似,要求返回值必须是Optional |
本质上说,Optional类就是一个容器,这样,即使对象为空,这样就相当于Optional容器里面是空的,但是还是有这个容器对象的。
6. 接口中的默认方法与静态方法
Java8开始允许接口中有默认实现的方法。即default关键字修饰的,称为默认方法。注意,该方法不是抽象方法。那么此时,如果某个类有和接口重名的方法(默认方法),并且测试类即继承了这个类,也实现了这个接口,此时有如下原则:
- 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
- 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须子类必须实现(重写)该方法来解决冲突。
另外,也允许有静态方法。
7. 新时间日期API
- Date
- Calendar
- TimeZone
- SimpleDateFormat(java.text)
上面四个时间相关类都是线程不安全的,而且也都不在一个包下,而且用法非常不友好。Java8新增的时间类如下:
- java.time
- LocalDate
- LocalTime
- LocalDateTime
- Instant
- ZoneOffset
- java.time.chrono
- java.time.format
- DateTimeFormat
- java.time.temporal
- TemporalAdjuster
- TemporalAdjusters
- java.time.zone
上面的一套API解决了线程安全问题。
8. 其他新特性
Java8对注解处理提供了两点改进:可重复的注解以及可用于类型的注解。
可重复注解就是可重复添加同名的注解,只不过value值不一样。【注意,注解定义的时候要加repeatable】
类型注解指的是注解修饰的类型,以前只有TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE。Java8新增了TYPE_PARAMETER,表示类型,比如修饰String等。