03-lambda-basic

  • Java8新引入的语法糖《Lambda表达式》(关于lambda是否属于语法糖存在很多争议,不要纠结于字面表述)
  • Lambda表达式是一种用于取代匿名类,把函数行为表述为函数式编程风格的一种匿名函数
  • 再重申一下:Lambda表达式是实现函数式接口的一个匿名类的对象
image-20240803083453024
image-20240803083545027
image-20240803083632113

1. basic

1. 示例代码

  • 需求:遍历List集合

1. Lambda表达式

List<String> strList = Arrays.asList("a", "b", "c");
strList.forEach(s -> {
    System.out.println(s);
});

2. 匿名内部类

  • 使用匿名内部类来实现上述lambda的功能
List<String> strList = Arrays.asList("a", "b", "c");
// 通过匿名内部类来代替lambda
strList.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

2. 示例代码分析

  • forEach()Iterable接口的一个默认方法,需要一个Consumer类型参数
  • 方法体中是一个for循环,对迭代器的每个Obj进行遍历,调用参数对象的accept()
public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}






 







  • Consumeraccept(T)方法。Consumer是一个函数式接口(只有一个抽象方法的接口)
@FunctionalInterface
public interface Consumer<T> {
    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    // ...
}







 


  • s -> {System.out.println(s);}相当于是实现了Consumer接口的一个匿名(内部类)对象
  • System.out.println(s);相当于重写了accept()的方法体

3. 反编译lambda代码

image-20231228174802939
  • jad系列的反编译工具不支持jdk1.8,使用CFR进行反编译
  • cfr下载地址open in new window
  • 语法:java -jar cfr-0.145.jar LambdaTest.class --decodelambdas false
public class LambdaTest {

    public static void main(String ... args) {
        List<String> strList = Arrays.asList("a", "b", "c");
        strList.forEach(
            (Consumer<String>)LambdaMetafactory.metafactory(
                null,
                null,
                null,
                (Ljava/lang/Object;)V,
                lambda$main$0(java.lang.String ),
                (Ljava/lang/String;)V)()
        );
    }

    private static /* synthetic */ void lambda$main$0(String s) {
        System.out.println(s);
    }
}










 





 


  • 调用了java.lang.invoke.LambdaMetafactory#metafactory(),该方法的第5个参数implMethod指定了方法实现
  • 调用lambda$main$0()方法进行输出。跟踪metafactory()(参数较多,可以跳过)
    public static CallSite metafactory(
        // 调用者(LambdaTest)可访问权限的上下文对象,JVM自动填充
        MethodHandles.Lookup caller,
        // 要执行的方法名,即Consumer.accept(),JVM自动填充
        String invokedName,
        // 调用点预期的签名(包含目标方法参数类型String和Lambda返回类型Consumer),JVM自动填充
        MethodType invokedType,
        // 函数式接口抽象方法的签名, (Object)void,泛型String被擦出,所以是Object
        MethodType samMethodType,
        // 直接方法句柄,真正被调用的方法,即lambda$main$0,签名为MethodHandle(String)void
        MethodHandle implMethod,
        // 实例化的方法签名,即调用时动态执行的方法签名,
        // 可能与samMethodType相同,也可能包含了泛型的具体类型,比如(String)void
        MethodType instantiatedMethodType) throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }










 









 

  • 其中new InnerClassLambdaMetafactory创建了一个Lambda相关的内部类
public InnerClassLambdaMetafactory(...)
            throws LambdaConversionException {
        // ...
        lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // ...
}




 


  • lambdaClassName——Lambda表达式对应的类名,而ClassWriter对象cw,暴露了Lambda表达式的底层实现机制:ASM技术(Assembly,Java字节码操作和分析框架open in new window,程序运行时动态生成和操作字节码文件)。在这个构造方法里,初始化了大量的ASM技术需要的成员变量,为后续生成字节码的相关操作完成了一系列的初始化
  • Lambda表达式底层是通过一个匿名内部类来实现的,这个类由ASM技术在程序运行时动态生成,它实现了函数式接口,并重写了对应的抽象方法

4. 验证猜想

  • metafactory()方法中,跟踪方法结尾的返回语句mf.buildCallSite(); —— 创建调用点
    /**
     * 创建调用点。定义一个实现了函数式接口的类并生成它的类文件
     * Build the CallSite. Generate a class file which implements the functional
     * interface, define the class, if there are no parameters create an instance
     * of the class which the CallSite will return, otherwise, generate handles
     * which will call the class' constructor.
     *
     * 返回一个调用点,执行的时候,将会返回一个函数式接口(Consumer)的实例
     * @return a CallSite, which, when invoked, will return an instance of the
     * functional interface
     */
    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        // 省略部分代码...

        try {
                Object inst = ctrs[0].newInstance();
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
        }
        // ...
    }













 



 
 



  • 方法的注释非常清晰的告诉我们,这个方法在运行期会返回一个函数式接口的实例,也就是Consumer接口的匿名对象
  • 第一行spinInnerClass(),使用ASM技术生成了一个Class文件,然后使用sun.misc.Unsafe将该类加载到JVM(创建并返回该类的Class对象)
   	private final ClassWriter cw;                    // ASM class writer

	/**
     * 生成一个实现函数式接口的类文件,定义并返回该类的Class实例
     * Generate a class file which implements the functional
     * interface, define and return the class.
     *
     * 返回一个实现函数式接口的Class实例
     * @return a Class which implements the functional interface
     */
    private Class<?> spinInnerClass() throws LambdaConversionException {
        // ...
        // ClassWriter通过visit方法动态构造类的字节码
        cw.visit(, , lambdaClassName, null, , interfaces);  // 生成接口字节码
        // ...
        for ( ; ; ) {
            cw.visitField( , , , null, null);               // 生成域的字节码
        }
        generateConstructor();                              // 生成构造器字节码
        // ...
        cw.visitMethod( ,  , , null, null);                 // 生成普通方法字节码
        // ...
        cw.visitEnd();                                      // end

        // Define the generated class in this VM.

        final byte[] classBytes = cw.toByteArray();

        // If requested, dump out to a file for debugging purposes
        if (dumper != null) {                               // 转储对象
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    dumper.dumpClass(lambdaClassName, classBytes);
                    return null;
                }
            }, null,
            new FilePermission("<<ALL FILES>>", "read, write"),
            // createDirectories may need it
            new PropertyPermission("user.dir", "read"));
        }
		// 使用Unsafe对象定义并返回,该内部类字节码文件对象(Class)
        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
    }













 
 
 
 
 
 
 
 
 
 



 















 

  • if (dumper != null) 代码块提供了将该内部类转储到本地磁盘,以下代码,将Lambda表达式对应的内部类转储到指定目录:
System.setProperty("jdk.internal.lambda.dumpProxyClasses", "out/production/");
  • 程序运行之后,Lambda表达式对应的内部类文件生成出来com.boxuegu.intermediate.language.sugar.lambda.LambdaTest$$Lambda$1
import java.lang.invoke.LambdaForm;
import java.util.function.Consumer;

final class LambdaTest$$Lambda$1 implements Consumer {    // 实现函数式接口

    private LambdaTest$$Lambda$1() {
    }

    @LambdaForm.Hidden
    public void accept(Object object) {                   // 重写抽象方法
        LambdaTest.lambda$main$0((String)object);
    }

}



 






 



  1. Lambda表达式底层是用内部类来实现的
  2. 该内部类实现了 (根据Lambda所属的代码指定) 函数式接口,并重写了该接口的抽象方法
  3. 该内部类是在程序运行时使用ASM技术动态生成的,所以编译期没有对应的.class文件,但是可以通过设置系统属性将该内部类文件转储出来

5. Lambda编译、运行过程

  1. Java7在 JSR(Java Specification Requests,Java 规范提案)292open in new window 中增加了对动态类型语言的支持,使得Java也可以像C语言那样将方法作为参数传递,其实现在java.lang.invoke包中。核心就是invokedynamic指令,为后续函数式编程响应式编程提供了前置支持
  2. invokedynamic指令对应的执行方法会关联到一个动态调用点对象(java.lang.invoke.CallSite),一个调用点(call site)是一个方法句柄(method handle,调用点的目标)的持有者,这个调用点对象会指向一个具体的引导方法(bootstrap method,eg:metafactory()),引导方法成功调用之后,调用点的目标将会与它持有的方法句柄的引用永久绑定,最终得到一个实现了函数式接口(eg:Consumer)的对象
  3. Lambda在编译期进行脱糖(desugar),它的主体部分会被转换成一个脱糖方法(desugared method,即lambda$main$0),这是一个合成方法,如果Lambda没有用到外部变量,则是一个私有的静态方法,否则将是个私有的实例方法——synthetic,表示不在源码中显示,并在Lambda所属的方法(eg:main方法)中生成invokedynamic指令
  4. 进入运行期invokedynamic指令会调用引导方法metafactory()初始化ASM生成内部类所需的各项属性,然后由spinInnerClass()方法组装内部类并用Unsafe加载到JVM,通过构造方法实例化内部类的实例(Lambda的实现内部类的构造是私有的,需要手动设置可访问属性为true),最后绑定到方法句柄,完成调用点的创建
  5. 可以把调用点看成是函数式接口(eg:Consumer等)的匿名对象,当然,内部类是确实存在的——eg:final class LambdaTest$$Lambda$1 implements Consumer。值得注意的是,内部类的实现方法里并没有Lambda的任何操作,它不过是调用了脱糖后定义在调用点目标类(targetClass,即LambdaTest类)中的合成方法(即lambda$main$0)而已,这样做使得内部类的代码量尽可能的减少,降低内存占用,对效率的提升更加稳定和可控

6. Lambda语法糖结论

  • Lambda在编译期脱去糖衣语法,生成了一个“合成方法“,在运行期,invokedynamic指令通过引导方法创建调用点,过程中生成一个实现了函数式接口的内部类并返回它的对象,最终通过调用点所持有的方法句柄完成对合成方法的调用,实现具体的功能
  • Lambda是一个语法糖,但远远不止是一个语法糖
  • 语法糖(Syntactic Sugar),也称糖衣语法,对语言本身的功能并没有影响,但是更方便程序员使用
    • 特点
      • 是一个语法结构,对原生语法的封装
      • 不被JVM支持
      • 解语法糖发生在编译期
      • 不一定能提高性能
    • 目的
      • 简化代码,提高编码效率
      • 增加可读性
      • 减少错误率

2. 方法引用

  • 使用Lambda,实际上传递进去的代码是一种解决方案:拿什么参数做什么操作
  • 在Lambda中所指定的操作方案,已经有地方存在相同方案,没必要再写重复逻辑
image-20231228190740143
image-20231228190815555
image-20231228190636046

1. 冗余的Lambda场景

@FunctionalInterface
public interface Printable {
    void print(String str);
}
// 只管调用`Printable`接口的`print()`,而并不管`print()`的具体实现逻辑,将字符串打印到什么地方
private static void printString(Printable printable) {
    printable.print("Hello, World!");
}

public static void main(String[] args) {
    // 指定`Printable`的具体操作方案:拿到`String`(类型可推导,所以可省略)数据后,在控制台中输出它
    printString(s ‐> System.out.println(s));
}

2. 问题分析

  • 对字符串进行控制台打印输出的操作方案,已经有了现成的实现,System.out对象中的println(String)。既然Lambda希望做的事情就是调用println(String),那何必手动调用呢?

3. 方法引用改进

  • 双冒号 ::
printString(System.out::println);

4. 方法引用符

  • 双冒号::为引用运算符,它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者

1. 语义分析

  • Lambda写法:s -> System.out.println(s);
  • 方法引用写法:System.out::println
  1. 第一种语义:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理
  2. 第二种等效写法的语义指:直接让 System.out 中的 println 方法来取代Lambda。复用了已有方案,更加简洁

2. 推导、省略

  • 无需指定参数类型,也无需指定的重载形式,都将被自动推导
@FunctionalInterface
public interface PrintableInteger {
	void print(int str);
}
  • 方法引用将会自动匹配到 println(int) 的重载形式
private static void printInteger(PrintableInteger data) {
    data.print(1024);
}

public static void main(String[] args) {
    printInteger(System.out::println);
}

 





5. 对象名

  • 最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:
public class MethodRefObject {
    // String大写转化逻辑
    public void printUpperCase(String str) {
    	System.out.println(str.toUpperCase());
    }
}
@FunctionalInterface
public interface Printable {
	void print(String str);
}
  • 使用这个printUpperCase()成员方法来替代Printable接口的Lambda时,通过对象名引用成员方法
private static void printString(Printable lambda) {
    lambda.print("Hello");
}

public static void main(String[] args) {
    MethodRefObject obj = new MethodRefObject();
    printString(obj::printUpperCase);
}






 

6. 类名

  • java.lang.Math类中已经存在了静态方法abs
@FunctionalInterface
public interface Calcable {
	int calc(int num);
}
  1. 使用Lambda表达式:
private static void method(int num, Calcable lambda) {
    System.out.println(lambda.calc(num));
}

public static void main(String[] args) {
    method(10, n ‐> Math.abs(n));
}





 

  1. 使用方法引用
private static void method(int num, Calcable lambda) {
    System.out.println(lambda.calc(num));
}

public static void main(String[] args) {
    method(10, Math::abs);
}





 

7. super

  • 如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:
@FunctionalInterface
public interface Greetable {
	void greet();
}
public class Human {

    public void sayHello() {
    	System.out.println("Hello!");
    }
}
public class Man extends Human {

    @Override
    public void sayHello() {
    	System.out.println("hello, 我是Man!");
    }

     // 定义方法method,参数传递Greetable
    public void method(Greetable g){
    	g.greet();
    }

    public void show(){
        // 1. Lambda表达式
        method(()> {
            new Human().sayHello();
        });

        // 2. 简化Lambda
        method(()> new Human().sayHello());

        // 3. super关键字代替父类对象
        method(()> super.sayHello());

        // 4. 方法引用
        method(super::sayHello);
    }
}



















 


 


 


8. this

  • this代表当前对象,如果需要引用的方法是当前类中的成员方法,this::成员方法
@FunctionalInterface
public interface Richable {
	void buy();
}
public class Husband {

    private void buyHouse() {
    	System.out.println("买套房子");
    }

    private void marry(Richable lambda) {
    	lambda.buy();
    }

    public void beHappy() {
        // lambda
    	marry(()> this.buyHouse());

    	// 方法引用
    	marry(this::buyHouse);
    }
}












 


 


9. 构造器

  • 构造器引用使用:类名称::new格式
public class Person {

    private String name;

    public Person(String name) {
    	this.name = name;
    }
}
public interface PersonBuilder {

	Person buildPerson(String name);
}
public static void printName(String name, PersonBuilder builder) {
    System.out.println(builder.buildPerson(name).getName());
}

public static void main(String[] args) {

    // lambda
    printName("hello", name ‐> new Person(name));

    // 方法引用
    printName("hello", Person::new);
}







 


 

10. 数组构造器

  • 数组也是Object的子类对象,所以同样具有构造器,只是语法不同
@FunctionalInterface
public interface ArrayBuilder {
	int[] buildArray(int length);
}
private static int[] initArray(int length, ArrayBuilder builder) {
    return builder.buildArray(length);
}

public static void main(String[] args) {
    // lambda
    int[] array = initArray(10, length ‐> new int[length]);

    // 方法引用
    int[] array = initArray(10, int[]::new);
}






 


 

3. Stream流

image-20231228190914235
image-20231228190936424

1. 引言

  • I/O Stream。Java8中,得益于Lambda带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端
  • 几乎所有的集合(eg:Collection接口、Map接口等)都支持直接或间接的遍历操作
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
for (String name : list) {
    System.out.println(name);
}

1. 循环遍历弊端

Java8的Lambda更加专注于做什么(What),而不是怎么做(How)

  • for循环的语法就是“怎么做
  • for循环的循环体才是“做什么

循环是遍历的唯一方式吗?

  • 遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环
  • 遍历是目的,循环只是一种方式

对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B
  2. 然后再根据条件二过滤为子集C
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
    // 1. 首先筛选所有姓张的人
    if (name.startsWith("张")) {
        zhangList.add(name);
    }
}

List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
    // 2. 然后筛选名字有三个字的人
    if (name.length() == 3) {
        shortList.add(name);
    }
}

for (String name : shortList) {
    // 3. 最后进行对结果进行打印输出
    System.out.println(name);
}




 







 






 

2. Stream优雅写法

  • 完美展示无关逻辑方式的语义:获取流 -> 过滤姓张 -> 过滤长度为3 -> 逐一打印
  • 代码中并没有体现使用线性循环或是其他任何算法进行遍历
@Test
public void lambda() {
    List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");

    list.stream()
          .filter(s -> s.startsWith("张"))
          .filter(s -> s.length() == 3)
          .forEach(System.out::println);
}





 
 
 

2. 流式思想概述

  • 整体来看,流式思想类似于工厂车间的“生产流水线”
  • 需要对多个元素进行操作(特别是多步操作)时,考虑到性能及便利性。首先拼好一个“模型”步骤方案,这种方案就是一种“函数模型”
  • filter(), map(), skip()都是对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行时,整个模型才会按照指定策略执行。而这得益于Lambda的延迟执行特性

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)

  • Stream(流)是一个来自数据源的元素队列
    • 元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算
    • 数据源流的来源。可以是集合、数组等
  • 和以前的Collection操作不同,Stream操作还有两个基础的特征
    1. Pipelining:中间操作都会返回流对象本身。多个操作可以串联成一个管道,流式风格(fluent style)。可以对操作进行优化
      • eg:延迟执行(laziness)短路(short-circuiting)
    2. 内部迭代IteratorforEach,显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,流可以直接调用遍历方法

  • 当使用一个流时,通常包括三个基本步骤
    1. 获取一个数据源(source)
    2. 数据转换
    3. 执行操作获取想要的结果
  • 每次转换原有Stream对象不改变,返回一个新的Stream对象(可以多次转换),这就允许对其操作可以像链条一样排列,变成一个管道

3. 获取流

  • java.util.stream.Stream<T> Java8新加入的最常用的流接口(并不是一个函数式接口)

1. Collection

  • java.util.Collection接口中加入了default方法stream()用来获取流
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();

Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();

Vector<String> vector = new Vector<>();
Stream<String> stream3 = vector.stream();

 


 


 

2. Map

  • java.util.Map接口不是Collection的子接口,且其K-V数据结构不符合流元素的单一特征,获取对应的流需要分key、value、entry
Map<String, String> map = new HashMap<>();

Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();


 
 
 

3. Array

  • 如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream.of()
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
// `of()`参数其实是一个可变参数,支持数组
Stream<String> stream = Stream.of(array);


 

4. 常用方法

  • 延迟方法:返回值类型仍然是Stream,因此支持链式调用(除了终结方法外,其余方法均为延迟方法)
  • 终结方法:返回值类型不再是Stream,因此不再支持链式调用。count()forEach()

1. 逐一处理(forEach)

  • 与for循环中的“for-each”昵称不同
Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach(name -> System.out.println(name));
  • 接收一个Consumer函数接口,会将每一个流元素交给该函数进行处理
void forEach(Consumer<? super T> action);
1. Consumer接口
@FunctionalInterface
public interface Consumer<T> {
    // 消费一个指定泛型的数据
    void accept(T var1);
}



 

2. 过滤(filter)

Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
// Lambda表达式指定筛选条件
Stream<String> result = original.filter(s -> s.startsWith("张"));
  • 接收一个Predicate函数接口(Lambda、方法引用)作为筛选条件
Stream<T> filter(Predicate<? super T> predicate);
1. Predicate接口
@FunctionalInterface
public interface Predicate<T> {
    // 指定的条件是否满足。true,留用元素;false,舍弃元素
    boolean test(T var1);
}



 

3. 映射(map)

  • 将流中的元素映射到另一个流中
Stream<String> original = Stream.of("10", "12", "18");
// String转换为Integer
Stream<Integer> result = original.map(str -> Integer.parseInt(str));
  • Function函数式接口,可以将当前流中的T类型数据转换为另一种R类型的流
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
1. Function接口
@FunctionalInterface
public interface Function<T, R> {
    // 一种T类型转换成为R类型,这种转换的动作,称为“映射”
    R apply(T var1);
}



 

4. 统计个数(count)

  • Collection当中的size()一样,Stream提供count()数一数其中的元素个数
  • 返回一个long值代表元素个数(集合是int值)
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count());


 

5. 取用前几个(limit)

// 如果集合当前长度大于参数则进行截取;否则不进行操作
Stream<T> limit(long maxSize);
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2

 

6. 跳过前几个(skip)

// 流的当前长度大于n,则跳过前n个;否则得到一个长度为0的空流
Stream<T> skip(long n);
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1

 

7. 组合(concat)

  • 两个流合并成为一个流
  • 是一个静态方法,与java.lang.String当中的concat()不同
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
Stream<String> streamA = Stream.of("张无忌");
Stream<String> streamB = Stream.of("张翠山");
Stream<String> result = Stream.concat(streamA, streamB);