前言

对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在常量池中有专属结构
lambda结构
其中bootstrap_method_attr_index指向bootstrap_methods的一个有效索引值,其结构在属性表的 bootstrap method 结构中,位置对应上面的#0索引为从0开始。

可以通过idea插件jclasslib bytecode viewer看到:
jclasslib结构1
jclasslib结构2
对应javap查看的字节码部分:
0: invokedynamic #2, 0 // InvokeDynamic #0🉑()Ljava/util/function/Consumer;
jclasslib结构3

debug:
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(流)

用于操作数据源(集合、数组等)生成元素序列的数据渠道。
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 f);
生成:Stream.generate(Supplier s);

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

streamFilter

终止操作

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 reduce(BinaryOperator accumulator);

// 来自源码注释
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 accumulator);

// 来自源码注释
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()

有状态:元素之间有依赖,才能拿到结果
无状态:元素之间互不依赖

并行使用方式:
stream并行

示例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问题