Stream流
由来
package demo01; import java.util.ArrayList; import java.util.List; public class Demo01ForEach { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); for (String name : list) { System.out.println(name); } } }
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
- 将集合A根据条件一过滤为子集B;
- 然后再根据条件二过滤为子集C。
package demo01; import java.util.ArrayList; import java.util.List; /* 使用传统的方式,遍历集合,对集合中的数据进行过滤 */ public class Demo01List { public static void main(String[] args) { //创建一个List集合,存储姓名 List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); //对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中 List<String> listA = new ArrayList<>(); for(String s : list){ if(s.startsWith("张")){ listA.add(s); } } //对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中 List<String> listB = new ArrayList<>(); for (String s : listA) { if(s.length()==3){ listB.add(s); } } //遍历listB集合 for (String s : listB) { System.out.println(s); } } }
- 首先筛选所有姓张的人;
- 然后筛选名字有三个字的人;
- 最后进行对结果进行打印输出。
Stream的更优写法
下面来看一下借助Java 8的Stream API,什么才叫优雅:
package demo01; import java.util.ArrayList; import java.util.List; /* 使用Stream流的方式,遍历集合,对集合中的数据进行过滤 Stream流是JDK1.8之后出现的 关注的是做什么,而不是怎么做 */ public class Demo02Stream { public static void main(String[] args) { //创建一个List集合,存储姓名 List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张强"); list.add("张三丰"); //对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中 //对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中 //遍历listB集合 list.stream() .filter(name -> name.startsWith("张")) .filter(name -> name.length() == 3) .forEach(name -> System.out.println(name)); } }
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
流式思想概述
整体来看,流式思想类似于工厂车间的“生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。
Stream(流)是一个来自数据源的元素队列
- 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
- 数据源 流的来源。 可以是集合,数组 等等。
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluentstyle)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。
获取流
- 所有的 Collection 集合都可以通过 stream 默认方法获取流;
- Stream 接口的静态方法 of 可以获取数组对应的流。
根据Collection获取流
package demo02; import java.util.*; import java.util.stream.Stream; /* - 所有的Collection集合都可以通过stream默认方法获取流; default Stream<E> stream() */ public class Demo04GetStream { public static void main(String[] args) { //把集合转换为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(); //等等 } }
根据Map获取流
java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:
package demo02; import java.util.*; import java.util.stream.Stream; /* - 所有的Collection集合都可以通过stream默认方法获取流; default Stream<E> stream() */ public class Demo04GetStream { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); //获取键,存储到一个Set集合中 Set<String> keySet = map.keySet(); Stream<String> stream1 = keySet.stream(); //获取值,存储到一个Collection集合中 Collection<String> values = map.values(); Stream<String> stream2 = values.stream(); //获取键值对(键与值的映射关系 entrySet) Set<Map.Entry<String, String>> entries = map.entrySet(); Stream<Map.Entry<String, String>> stream3 = entries.stream(); } }
根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of ,使用很简单
package demo02; import java.util.*; import java.util.stream.Stream; /* java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。) - Stream接口的静态方法of可以获取数组对应的流。 static <T> Stream<T> of(T... values) 参数是一个可变参数,那么我们就可以传递一个数组 */ public class Demo04GetStream { public static void main(String[] args) { //把数组转换为Stream流 Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5); //可变参数可以传递数组 Integer[] arr = {1, 2, 3, 4, 5}; Stream<Integer> stream7 = Stream.of(arr); String[] arr2 = {"a", "bb", "ccc"}; Stream<String> stream2 = Stream.of(arr2); } }
常用方法
- 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
- 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 count 和 forEach 方法。
逐一处理:forEach
void forEach(Consumer<? super T> action);
- java.util.function.Consumer<T>接口是一个消费型接口。
- Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_forEach void forEach(Consumer<? super T> action); 该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。 Consumer接口是一个消费型的函数式接口,可以传递Lambda表达式,消费数据 简单记: forEach方法,用来遍历流中的数据 是一个终结方法,遍历之后就不能继续调用Stream流中的其他方法 */ public class Demo02Stream_forEach { public static void main(String[] args) { //获取一个Stream流 Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六", "田七"); //使用Stream流中的方法forEach对Stream流中的数据进行遍历 /*stream.forEach((String name)->{ System.out.println(name); });*/ stream.forEach(name -> System.out.println(name)); } }
过滤:filter
基本使用
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_filter:用于对Stream流中的数据进行过滤 Stream<T> filter(Predicate<? super T> predicate); filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤 Predicate中的抽象方法: boolean test(T t); */ public class Demo03Stream_filter { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌"); //对Stream流中的元素进行过滤,只要姓张的人 Stream<String> stream2 = stream.filter((String name) -> { return name.startsWith("张"); }); //遍历stream2流 stream2.forEach(name -> System.out.println(name)); /* Stream流属于管道流,只能被消费(使用)一次 第一个Stream流调用完毕方法,数据就会流转到下一个Stream上 而这时第一个Stream流已经使用完毕,就会关闭了 所以第一个Stream流就不能再调用方法了 IllegalStateException: stream has already been operated upon or closed */ //遍历stream流 stream.forEach(name -> System.out.println(name)); } }
映射:map
基本使用
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_map:用于类型转换 如果需要将流中的元素映射到另一个流中,可以使用map方法. <R> Stream<R> map(Function<? super T, ? extends R> mapper); 该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。 Function中的抽象方法: R apply(T t); */ public class Demo04Stream_map { public static void main(String[] args) { //获取一个String类型的Stream流 Stream<String> stream = Stream.of("1", "2", "3", "4"); //使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数 Stream<Integer> stream2 = stream.map((String s) -> { return Integer.parseInt(s); }); //遍历Stream2流 stream2.forEach(i -> System.out.println(i)); } }
统计个数:count
package demo02; import java.util.ArrayList; import java.util.stream.Stream; /* Stream流中的常用方法_count:用于统计Stream流中元素的个数 long count(); count方法是一个终结方法,返回值是一个long类型的整数 所以不能再继续调用Stream流中的其他方法了 */ public class Demo05Stream_count { public static void main(String[] args) { //获取一个Stream流 ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); list.add(5); list.add(6); list.add(7); Stream<Integer> stream = list.stream(); long count = stream.count(); System.out.println(count);//7 } }
取用前几个:limit
limit 方法可以对流进行截取,只取用前n个。参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_limit:用于截取流中的元素 limit方法可以对流进行截取,只取用前n个。方法签名: Stream<T> limit(long maxSize); 参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作 limit方法是一个延迟方法,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用Stream流中的其他方法 */ public class Demo06Stream_limit { public static void main(String[] args) { //获取一个Stream流 String[] arr = {"美羊羊", "喜洋洋", "懒洋洋", "灰太狼", "红太狼"}; Stream<String> stream = Stream.of(arr); //使用limit对Stream流中的元素进行截取,只要前3个元素 Stream<String> stream2 = stream.limit(3); //遍历stream2流 stream2.forEach(name -> System.out.println(name)); } }
跳过前几个:skip
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_skip:用于跳过元素 如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流: Stream<T> skip(long n); 如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。 */ public class Demo07Stream_skip { public static void main(String[] args) { //获取一个Stream流 String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"}; Stream<String> stream = Stream.of(arr); //使用skip方法跳过前3个元素 Stream<String> stream2 = stream.skip(3); //遍历stream2流 stream2.forEach(name-> System.out.println(name)); } }
组合:concat
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。基本使用
package demo02; import java.util.stream.Stream; /* Stream流中的常用方法_concat:用于把流组合到一起 如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) */ public class Demo08Stream_concat { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream1 = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌"); //获取一个Stream流 String[] arr = {"美羊羊", "喜洋洋", "懒洋洋", "灰太狼", "红太狼"}; Stream<String> stream2 = Stream.of(arr); //把以上两个流组合为一个流 Stream<String> concat = Stream.concat(stream1, stream2); //遍历concat流 concat.forEach(name -> System.out.println(name)); } }
练习:集合元素处理(传统方式)
定义Person类
package demo03; public class Person { private String name; public Person() { } public Person(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
使用传统的for循环写法
package demo03; import java.util.ArrayList; /* 练习:集合元素处理(传统方式) 现在有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤: 1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。 2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。 3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。 4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。 5. 将两个队伍合并为一个队伍;存储到一个新集合中。 6. 根据姓名创建Person对象;存储到一个新集合中。 7. 打印整个队伍的Person对象信息。 */ public class Demo01StreamTest { public static void main(String[] args) { //第一支队伍 ArrayList<String> one = new ArrayList<>(); one.add("迪丽热巴"); one.add("宋远桥"); one.add("苏星河"); one.add("石破天"); one.add("石中玉"); one.add("老子"); one.add("庄子"); one.add("洪七公"); //1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。 ArrayList<String> one1 = new ArrayList<>(); for (String name : one) { if (name.length() == 3) { one1.add(name); } } //2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。 ArrayList<String> one2 = new ArrayList<>(); for (int i = 0; i < 3; i++) { one2.add(one1.get(i));//i = 0,1,2 } //第二支队伍 ArrayList<String> two = new ArrayList<>(); two.add("古力娜扎"); two.add("张无忌"); two.add("赵丽颖"); two.add("张三丰"); two.add("尼古拉斯赵四"); two.add("张天爱"); two.add("张二狗"); //3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。 ArrayList<String> two1 = new ArrayList<>(); for (String name : two) { if (name.startsWith("张")) { two1.add(name); } } //4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。 ArrayList<String> two2 = new ArrayList<>(); for (int i = 2; i < two1.size(); i++) { two2.add(two1.get(i)); //i 不包含0 1 } //5. 将两个队伍合并为一个队伍;存储到一个新集合中。 ArrayList<String> all = new ArrayList<>(); all.addAll(one2); all.addAll(two2); //6. 根据姓名创建Person对象;存储到一个新集合中。 ArrayList<Person> list = new ArrayList<>(); for (String name : all) { list.add(new Person(name)); } //7. 打印整个队伍的Person对象信息。 for (Person person : list) { System.out.println(person); } } }
等效的Stream流式处理代码为:
package demo03; import java.util.ArrayList; import java.util.stream.Stream; /* 练习:集合元素处理(Stream方式) 将上一题当中的传统for循环写法更换为Stream流式处理方式。 两个集合的初始内容不变,Person类的定义也不变。 */ public class Demo02StreamTest { public static void main(String[] args) { //第一支队伍 ArrayList<String> one = new ArrayList<>(); one.add("迪丽热巴"); one.add("宋远桥"); one.add("苏星河"); one.add("石破天"); one.add("石中玉"); one.add("老子"); one.add("庄子"); one.add("洪七公"); //1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。 //2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。 Stream<String> oneStream = one.stream().filter(name -> name.length() == 3).limit(3); //第二支队伍 ArrayList<String> two = new ArrayList<>(); two.add("古力娜扎"); two.add("张无忌"); two.add("赵丽颖"); two.add("张三丰"); two.add("尼古拉斯赵四"); two.add("张天爱"); two.add("张二狗"); //3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。 //4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。 Stream<String> twoStream = two.stream().filter(name -> name.startsWith("张")).skip(2); //5. 将两个队伍合并为一个队伍;存储到一个新集合中。 //6. 根据姓名创建Person对象;存储到一个新集合中。 //7. 打印整个队伍的Person对象信息。 Stream.concat(oneStream, twoStream).map(name -> new Person(name)).forEach(p -> System.out.println(p)); } }
方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
冗余的Lambda场景
来看一个简单的函数式接口以应用Lambda表达式
package demo04; /* 定义一个打印的函数式接口 */ @FunctionalInterface public interface Printable { //定义字符串的抽象方法 void print(String s); }
在 Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单
package demo04; public class Demo01PrintSimple { private static void printString(Printable data) { data.print("Hello, World!"); } public static void main(String[] args) { printString(s -> System.out.println(s)); } }
用方法引用改进代码
package demo04; public class Demo01Printable { //定义一个方法,参数传递Printable接口,对字符串进行打印 public static void printString(Printable p) { p.print("HelloWorld"); } public static void main(String[] args) { //调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda printString((s) -> { System.out.println(s); }); /* 分析: Lambda表达式的目的,打印参数传递的字符串 把参数s,传递给了System.out对象,调用out对象中的方法println对字符串进行了输出 注意: 1.System.out对象是已经存在的 2.println方法也是已经存在的 所以我们可以使用方法引用来优化Lambda表达式 可以使用System.out方法直接引用(调用)println方法 */ printString(System.out::println); } }
方法引用符
语义分析
- Lambda表达式写法: s -> System.out.println(s);
- 方法引用写法: System.out::println
- Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常
推导与省略
通过对象名引用成员方法
/* 通过对象名引用成员方法 使用前提是对象名是已经存在的,成员方法也是已经存在 就可以使用对象名来引用成员方法 */ public class Demo01ObjectMethodReference { //定义一个方法,方法的参数传递Printable接口 public static void printString(Printable p){ p.print("Hello"); } public static void main(String[] args) { //调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式 printString((s)->{ //创建MethodRerObject对象 MethodRerObject obj = new MethodRerObject(); //调用MethodRerObject对象中的成员方法printUpperCaseString,把字符串按照大写输出 obj.printUpperCaseString(s); }); /* 使用方法引用优化Lambda 对象是已经存在的MethodRerObject 成员方法也是已经存在的printUpperCaseString 所以我们可以使用对象名引用成员方法 */ //创建MethodRerObject对象 MethodRerObject obj = new MethodRerObject(); printString(obj::printUpperCaseString); } }
通过类名称引用静态方法
/* 通过类名引用静态成员方法 类已经存在,静态成员方法也已经存在 就可以通过类名直接引用静态成员方法 */ public class Demo01StaticMethodReference { //定义一个方法,方法的参数传递要计算绝对值的整数,和函数式接口Calcable public static int method(int number,Calcable c){ return c.calsAbs(number); } public static void main(String[] args) { //调用method方法,传递计算绝对值得整数,和Lambda表达式 int number = method(-10,(n)->{ //对参数进行绝对值得计算并返回结果 return Math.abs(n); }); System.out.println(number); /* 使用方法引用优化Lambda表达式 Math类是存在的 abs计算绝对值的静态方法也是已经存在的 所以我们可以直接通过类名引用静态方法 */ int number2 = method(-10,Math::abs); System.out.println(number2); } }
- Lambda表达式: n -> Math.abs(n)
- 方法引用: Math::abs
通过super引用成员方法
/* 定义子类 */ public class Man extends Human{ //子类重写父类sayHello的方法 @Override public void sayHello() { System.out.println("Hello 我是Man!"); } //定义一个方法参数传递Greetable接口 public void method(Greetable g){ g.greet(); } public void show(){ //调用method方法,方法的参数Greetable是一个函数式接口,所以可以传递Lambda /*method(()->{ //创建父类Human对象 Human h = new Human(); //调用父类的sayHello方法 h.sayHello(); });*/ //因为有子父类关系,所以存在的一个关键字super,代表父类,所以我们可以直接使用super调用父类的成员方法 /* method(()->{ super.sayHello(); });*/ /* 使用super引用类的成员方法 super是已经存在的 父类的成员方法sayHello也是已经存在的 所以我们可以直接使用super引用父类的成员方法 */ method(super::sayHello); } public static void main(String[] args) { new Man().show(); } }
- Lambda表达式: () -> super.sayHello()
- 方法引用: super::sayHello
通过this引用成员方法
/* 使用this引用本类的成员方法 */ public class Husband { //定义一个买房子的方法 public void buyHouse(){ System.out.println("北京二环内买一套四合院!"); } //定义一个结婚的方法,参数传递Richable接口 public void marry(Richable r){ r.buy(); } //定义一个非常高兴的方法 public void soHappy(){ //调用结婚的方法,方法的参数Richable是一个函数式接口,传递Lambda表达式 /* marry(()->{ //使用this.成员方法,调用本类买房子的方法 this.buyHouse(); });*/ /* 使用方法引用优化Lambda表达式 this是已经存在的 本类的成员方法buyHouse也是已经存在的 所以我们可以直接使用this引用本类的成员方法buyHouse */ marry(this::buyHouse); } public static void main(String[] args) { new Husband().soHappy(); } }
- Lambda表达式: () -> this.buyHouse()
- 方法引用: this::buyHouse
类的构造器引用
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。
/* 类的构造器(构造方法)引用 */ public class Demo { //定义一个方法,参数传递姓名和PersonBuilder接口,方法中通过姓名创建Person对象 public static void printName(String name,PersonBuilder pb){ Person person = pb.builderPerson(name); System.out.println(person.getName()); } public static void main(String[] args) { //调用printName方法,方法的参数PersonBuilder接口是一个函数式接口,可以传递Lambda printName("迪丽热巴",(String name)->{ return new Person(name); }); /* 使用方法引用优化Lambda表达式 构造方法new Person(String name) 已知 创建对象已知 new 就可以使用Person引用new创建对象 */ printName("古力娜扎",Person::new);//使用Person类的带参构造方法,通过传递的姓名创建对象 } }
- Lambda表达式: name -> new Person(name)
- 方法引用: Person::new
数组的构造器引用
import java.util.Arrays; /* 数组的构造器引用 */ public class Demo { /* 定义一个方法 方法的参数传递创建数组的长度和ArrayBuilder接口 方法内部根据传递的长度使用ArrayBuilder中的方法创建数组并返回 */ public static int[] createArray(int length, ArrayBuilder ab){ return ab.builderArray(length); } public static void main(String[] args) { //调用createArray方法,传递数组的长度和Lambda表达式 int[] arr1 = createArray(10,(len)->{ //根据数组的长度,创建数组并返回 return new int[len]; }); System.out.println(arr1.length);//10 /* 使用方法引用优化Lambda表达式 已知创建的就是int[]数组 数组的长度也是已知的 就可以使用方法引用 int[]引用new,根据参数传递的长度来创建数组 */ int[] arr2 =createArray(10,int[]::new); System.out.println(Arrays.toString(arr2)); System.out.println(arr2.length);//10 } }
- Lambda表达式: length -> new int[length]
- 方法引用: int[]::new