java8新特性
前言
对java8的“新”特性有个比较全面的了解,在开发过程中,如果使用函数式编程+lambda表达式时能够得心应手,同时规避一些风险点。
内容包括:Lambda表达式、函数式接口、方法引用、Stream Api、默认方法、Optional 类。
Lambda表达式
一个小示例,无参:
@Test
public void test () {
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Thread run()");
}
}).start();
}
@Test
public void testLambda () {
// 接口名
new Thread(() -> {
System.out.println("Thread run()");
}).start();
}
使用lambda:
1、简化了匿名内部类的写法;
2、通过字节码可以看出,lambda表达式是通过invokedynamic指令实现的。
匿名内部类
内部类的一种,使用时不定义对象名,常用于接口回调,只需要实现对应的方法。
问题:如果没有匿名类,上面的线程该如何实现?
invokedynamic指令
编译后,查看class文件,lambda表达式的字节码会生成invokedynamic的指令(匿名内部类会生成单独的.class类文件)。
package com.mainto;
import java.util.function.Consumer;
public class LambdaCodeTest {
public static void main(String[] args) {
Consumer<String> consumer = (str) -> System.out.println(str);
consumer.accept("kz");
}
}
对应字节码(二进制 -> 助计符):
/* 类摘要 */
// 文件路径
Classfile /Users/xudj/Downloads/LambdaCodeTest.class
// 修改时间
Last modified 2022-10-31; size 1078 bytes
// MD5校验和
MD5 checksum a02205b49eb8c4f2c1610a525cd56e49
// 编译来源
Compiled from "LambdaCodeTest.java"
// 类名
public class com.mainto.LambdaCodeTest
// 主次版本号,52表示jdk8
minor version: 0
major version: 52
// 访问权限(ACC_SUPER 修正 invokespecial 指令调用父类)
flags: ACC_PUBLIC, ACC_SUPER
/* class文件常量池,包括基本类型常量和符号引用 */
Constant pool:
// 编号 = 描述
#1 = Methodref #8.#19 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#25 // #0:accept:()Ljava/util/function/Consumer;
#3 = String #26 // kz
#4 = InterfaceMethodref #27.#28 // java/util/function/Consumer.accept:(Ljava/lang/Object;)V
#5 = Fieldref #29.#30 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #33 // com/mainto/LambdaCodeTest
#8 = Class #34 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 lambda$main$0
#16 = Utf8 (Ljava/lang/String;)V
#17 = Utf8 SourceFile
#18 = Utf8 LambdaCodeTest.java
#19 = NameAndType #9:#10 // "<init>":()V
#20 = Utf8 BootstrapMethods
#21 = MethodHandle #6:#35 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#22 = MethodType #36 // (Ljava/lang/Object;)V
#23 = MethodHandle #6:#37 // invokestatic com/mainto/LambdaCodeTest.lambda$main$0:(Ljava/lang/String;)V
#24 = MethodType #16 // (Ljava/lang/String;)V
#25 = NameAndType #38:#39 // accept:()Ljava/util/function/Consumer;
#26 = Utf8 kz
#27 = Class #40 // java/util/function/Consumer
#28 = NameAndType #38:#36 // accept:(Ljava/lang/Object;)V
#29 = Class #41 // java/lang/System
#30 = NameAndType #42:#43 // out:Ljava/io/PrintStream;
#31 = Class #44 // java/io/PrintStream
#32 = NameAndType #45:#16 // println:(Ljava/lang/String;)V
#33 = Utf8 com/mainto/LambdaCodeTest
#34 = Utf8 java/lang/Object
#35 = Methodref #46.#47 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#36 = Utf8 (Ljava/lang/Object;)V
#37 = Methodref #7.#48 // com/mainto/LambdaCodeTest.lambda$main$0:(Ljava/lang/String;)V
#38 = Utf8 accept
#39 = Utf8 ()Ljava/util/function/Consumer;
#40 = Utf8 java/util/function/Consumer
#41 = Utf8 java/lang/System
#42 = Utf8 out
#43 = Utf8 Ljava/io/PrintStream;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Class #49 // java/lang/invoke/LambdaMetafactory
#47 = NameAndType #50:#54 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#48 = NameAndType #15:#16 // lambda$main$0:(Ljava/lang/String;)V
#49 = Utf8 java/lang/invoke/LambdaMetafactory
#50 = Utf8 metafactory
#51 = Class #56 // java/lang/invoke/MethodHandles$Lookup
#52 = Utf8 Lookup
#53 = Utf8 InnerClasses
#54 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#55 = Class #57 // java/lang/invoke/MethodHandles
#56 = Utf8 java/lang/invoke/MethodHandles$Lookup
#57 = Utf8 java/lang/invoke/MethodHandles
/* 方法信息 */
{
public com.mainto.LambdaCodeTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
//labmda表达式操作invokedynamic
0: invokedynamic #2, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
5: astore_1
6: aload_1
7: ldc #3 // String kz
9: invokeinterface #4, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
14: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
private static void lambda$main$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 8: 0
}
SourceFile: "LambdaCodeTest.java"
InnerClasses:
public static final #52= #51 of #55; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #21 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#22 (Ljava/lang/Object;)V
#23 invokestatic com/mainto/LambdaCodeTest.lambda$main$0:(Ljava/lang/String;)V
#24 (Ljava/lang/String;)V
查看字节码方式:
1、javac 编译 *.java文件;javap -verbose -p *.class 查看字节码
2、另一种方式:安装idea插件 jclasslib bytecode viewer
0: invokedynamic #2, 0 // InvokeDynamic #0🉑()Ljava/util/function/Consumer;
其中0:表示方法中该操作码偏移量
invokedynamic:字节码指令
#2 0:为指令的操作数,#2表示常量池的引用;后面0是固定的(JSR某版本规范中定义的)
// 注释中#0,指向BootstrapMethods的索引为0的位置
常量池包括基本类型常量和符号引用,其中符号引用有一定的结构,如invokedynamic在常量池中有专属结构
其中bootstrap_method_attr_index指向bootstrap_methods的一个有效索引值,其结构在属性表的 bootstrap method 结构中,位置对应上面的#0索引为从0开始。
可以通过idea插件jclasslib bytecode viewer看到:
对应javap查看的字节码部分:
0: invokedynamic #2, 0 // InvokeDynamic #0🉑()Ljava/util/function/Consumer;
debug:
生成的内部类:
增加vm参数:-Djdk.internal.lambda.dumpProxyClasses
结论:当invokedynamic或lambda执行时
1、有个引导方法调用LambdaMetafactory.metafactory 通过jdk ASM生产接口(lambda表达式)的内部类对象
2、并将对象绑定到MethodHandle,MethodHandle再绑定到CallSite上
3、最后invokedynamic通过CallSite动态调用生成内部类对象的方法,该方法会直接调用编译时lambda对应的类静态方法
CallSite只会生成1次,后面会直接调用
问题:所有的匿名内部类都能用lambda表达式吗?
函数式接口
定义:由@FunctionalInterface注解声明,标识并检测仅有一个抽象方法的接口定义。
java8新增包,java.util.function
常见的几种:Supplier(生产)、Consumer(消费)、Predicate(判断)、Function(输入输出)
举例1:Consumer(消费)
// 1、 定义
Consumer<String> consumer = str -> sout(str);
// 2、 循环打印
static void foreach(List<String> list, Consumer<String> consumer) {
list.forEach (consumer.accept);
}
举例2:List的forEach方法
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
结论:
1、凡是函数式接口都可以使用lambda表达式。
2、lambda体中调用方法的参数列表和返回值类型,要和函数式接口的抽象方法的参数列表和返回值类型一致。
lambda表达式足够简单吗?
方法引用
如果Lambda表达式体中的方法已经实现了,可以用方法引用代替lambda表达式,诸如String::Length。
因为是代替lambda表达式,所以编译时还是invokedDynamic指令。
语法可以分为4类:
类别 | 示例 |
---|---|
对象::实例方法名 | list::add |
类::静态方法 | Integer::compare |
类::实例方法 | String::length |
构造方法 | String::new |
代码示例:
// 对象::实例方法名
List<String> list = new ArrayList<>();
// Consumer<String> con = (s) -> list.add(s);
Consumer<String> con = list::add;
Arrays.asList("a", "b", "c")
.forEach(con);
// 类::静态方法
// Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y);
Comparator<Integer> comparator = Integer::compare;
List<Integer> sorted = Stream.of(1, 2, 3)
.sorted(comparator)
.collect(Collectors.toList());
// 类::实例方法
// Function<String, Integer> function = (s) -> s.length();
Function<String, Integer> toLength = String::length;
Arrays.asList("a", "bb", "ccc").stream()
.map(toLength)
.collect(Collectors.toList());
// 类::new
// Supplier<List> supplier = () -> new ArrayList();
Supplier<List> supplier = ArrayList::new;
List getList = supplier.get();
结论:
方法引用的方法的参数列表和返回值类型,要和函数式接口的抽象方法的参数列表和返回值类型一致。
Stream api
什么是Stream(流)
用于操作数据源(集合、数组等)生成元素序列的数据渠道。
可以理解成生产车间的流水线思想,先进行“模型”或视图的定义(一系列的中间操作),形成一个流。最后再真正执行操作,得到新的数据结果。
结合示例理解:
public void test1 () {
Stream<Integer> stream = Stream.of(1, 2, 3)
.map(i -> {
System.out.println("中间操作");
return ++i;
})
.filter(i -> i > 1);
System.out.println("准备计算");
List<Integer> collect = stream.collect(Collectors.toList());
System.out.println("计算完成:" + collect);
}
输出结果:
准备计算
中间操作
中间操作
中间操作
计算完成:[2, 3, 4]
注意:
1、Stream不会改变原对象
2、Stream自己不会存储元素
3、Stream操作是延迟执行的(上面示例)
4、只能被“消费”一次,一旦遍历过后就会失效
Stream操作的三步骤
1、创建Stream
一个数据源(数组、集合),形成一个流
2、中间操作
对数据源进行一些列的中间操作,不会真正操作数据
3、终止操作
一个终止操作,执行中间操作链,产生结果
Stream创建的方式
1、集合系列提供的stream方法
new ArrayList().stream();
2、通过Arrays中的静态方法
Arrays.stream();
3、通过Stream类中的静态方法
Stream.of(obj…);
4、无限流
迭代:Stream.iterate(T seed, UnaryOperator
生成:Stream.generate(Supplier
Stream常见方法
操作类型 | 接口方法 |
---|---|
中间操作 | concat() distinct() filter() flatMap() limit() map() peek() |
skip() sorted() parallel() sequential() unordered() | |
结束操作 | allMatch() anyMatch() collect() count() findAny() findFirst() |
forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
区分方式,看返回值,返回值为Stream的大多数为中间操作
Stream使用
使用时,Stream的使用基本上都会依赖函数式接口(@FunctionInterface),进而用到lambda表达式。
中间操作
filter()
Stream<T> filter(Predicate< ? super T> predicate);
入参是一个表示断言的函数式接口,输入t参数,返回boolean类型。
断言的函数式接口:对应lambda表达式:(t) -> boolean。
示例:
public void test2 () {
// 保留长度大于3的字符串
Stream<String> stream= Stream.of("do", "you", "understand", "?");
stream.filter(str -> str.length() > 3)
.forEach(str -> System.out.println(str));
}
结果:
understand
终止操作
forEach()
public void forEach(Consumer< ? super P_OUT> action) {}
入参是个Consumer,消费型接口,执行action指定的动作。
对应lambda表达式:(x) -> out();
示例
public void test2 () {
// 保留长度大于3的字符串
Stream<String> stream= Stream.of("do", "you", "understand", "?");
Stream<String> stringStream = stream.filter(str -> str.length() > 3);
// 终止操作
stringStream.forEach(str -> System.out.println(str));
}
Stream使用升级
Stream Api的归约操作、收集操作、并行流
归约操作
1、Optional<T> reduce(BinaryOperator<T> accumulator);
2、T reduce(T identity, BinaryOperator<T> accumulator);
3、<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
第三个并行流用的到,先忽略
对于参数BiFunction,函数式接口对应的抽象方法:R apply(T t, U u);
BinaryOperator继承于BiFunction,只是方法范型:T apply(T t, T u);
示例:
public void testReduce () {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Integer reduce = list.stream().reduce(0, (x, y) -> {
System.out.println(x + "+" + y);
return x + y;
});
System.out.println(reduce); // 15
}
输出:
0+1
1+2
3+3
6+4
10+5
15
api:Optional
// 来自源码注释
boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element;
}
else
result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();
api:T reduce(T identity, BinaryOperator
// 来自源码注释
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
收集操作
使用:Stream.collect(Collector)
对应方法:<R, A> R collect(Collector< ? super T, A, R> collector);
具体实现:Collectors提供了很多的静态收集方法,返回工具接口Collector的实现类对象CollectorImpl。
使用注释:
// 1、获取String集合
List<String> asList = stringStream.collect(Collectors.toList());
// 2、按用户所在城市分组
Map<String, List<Person>> peopleByCity
= personStream.collect(Collectors.groupingBy(Person::getCity));
// 3、按用户状态分组的情况下,再按用户城市分组
Map<String, Map<String, List<Person>>> peopleByStateAndCity
= personStream.collect(Collectors.groupingBy(Person::getState,
Collectors.groupingBy(Person::getCity)));
并行流
并行处理,利用cpu的多核进行处理;但是如果处理不当,会出现安全问题。
先看下Stream api状态:
操作 | 状态 | api |
---|---|---|
中间操作 | 无状态 | filter() map() mapToInt() mapToLong() mapToDouble() unordered() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek() |
有状态 | distinct() sorted() limit() skip() | |
结束操作 | 非短路操作 | forEach() forEachOrdered() toArray() reduce() collect() max() min() count() |
短路操作 | anyMatch() allMatch() noneMatch() findFirst() findAny() |
有状态:元素之间有依赖,才能拿到结果
无状态:元素之间互不依赖
并行使用方式:
示例1:
public void testParallel () {
Long count = Stream.of(1, 2, 3, 4, 5)
.parallel()
.filter(x -> {
System.out.println("x:" + x);
return x > 1;
})
.collect(Collectors.counting());
System.out.println("count:" + count);
}
结果:
x:3
x:5
x:1
x:4
x:2
count:4
示例2:
public void testParallelBug () {
List<Integer> newList = new ArrayList<>();
List<Integer> list = Arrays.asList(5, 7, 1, 3, 2, 10, 20, 11, 12, 23, 15);
Stream<Integer> sorted = list.stream().parallel()
.filter(x -> x > 1).sorted();
sorted.forEach(newList::add);
System.out.println(newList);
}
结果:
[12, 11, 20, 23, 15, 5, 10, 7, 3, 2]
sorted后,forEach不保证顺序,单纯的处理并行流的数据
示例3:
public void testParallelFixBug () {
List<Integer> list = Arrays.asList(5, 7, 1, 3, 2, 10, 20, 11, 12, 23, 15);
Stream<Integer> sorted = list.stream().parallel()
.filter(x -> x > 1).sorted();
List<Integer> newList = sorted.collect(Collectors.toList());
System.out.println(newList);
}
结果:
[2, 3, 5, 7, 10, 11, 12, 15, 20, 23]
sorted后,通过收集器内部保障顺序
结论:
1、并行流情况下,慎用forEach,使用时内部数据结果需要保证线程安全
2、并行流情况下,慎用有状态的中间操作
3、并行流情况下,获取集合数据使用collect收集方式
默认方法
java8 引用了默认方法
背景:针对一个接口多实现的情况,想要给一个接口再加一个抽象方法比较困难,因为所有方法都要进行实现。为了避免这种情况,引入默认方法。
// List中定义的默认排序方法,所有子类都可以用,不用实现了。
default void sort(Comparator< ? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
java8支持了默认方法,同时多支持了static方法,就不用单独定义util了,而且static不能被实现类重写,如函数式接口Function的identity()
static <T> Function<T, T> identity() {
return t -> t;
}
示例:
Map<Integer, Person> peopleById
= personStream.collect(Collectors.toMap(p -> p.getId(), Function.identity());
Optional类
Optional 类的引入很好的解决空指针异常。
Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。
类定义:
public final class Optional<T> {
private final T value;
}
示例:
public void test1() {
Integer value1 = null;
Integer value2 = new Integer(10);
// Optional.ofNullable - 允许传递为 null 参数
Optional<Integer> a = Optional.ofNullable(value1);
// Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
Optional<Integer> b = Optional.of(value2);
// 获取
System.out.println(a.isPresent() ? a.get() : null);
System.out.println(a.orElse(1));
System.out.println(b.get());
}
扩展
java8“新”特性不止上面提到的几点,感兴趣可以自行学习,还包括:
时间类:LocalDateTime(线程安全);Collection的调整;等!
总结
1、lambda的如何使用,原理了解
2、函数式接口概念,熟练掌握4大类
3、Stream api理解,如何结合lambda表达式,使用时触类旁通
4、Optional类规避NPE问题