# share_java8_stream **Repository Path**: j2eelyh/share_java8_stream ## Basic Information - **Project Name**: share_java8_stream - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-10-08 - **Last Updated**: 2021-11-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ***java8 stream*** ## 1.java8知识补充 #### 1.1 lambda表达式 ##### 匿名函数、lambda、closure区别 >  从功能性上说lambda和closure(或是OC中的blocks)是一个东西,只是不同语言的不同称呼罢了,它们都是匿名函数。 > > 若匿名函数捕获了一个外部变量,那么它就是一个closure。 > > Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中) **(行为参数化)** *接下来以redis旁路缓存演示匿名内部类和lambda的不同写法* 代码执行逻辑: ![image.png](https://i.loli.net/2021/10/11/PQguXldnxfWYpN3.png) >环境准备:spring-boot 2.3.0.RELEASE 同时引入 web,test,redis 对应start包,并设置redis序列化方式为string RedisUtil类: ```java package com.smile.component; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/11 09:18 */ @Component public class RedisUtil { private final RedisTemplate redisTemplate; public RedisUtil(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 获取缓存 * @param key * @return */ public Object getCacheByKey(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * @param key 键 * @param value 值 */ public void setCache(String key, Object value,long time) { redisTemplate.opsForValue().set(key, value,time,TimeUnit.SECONDS); } /** * 旁路缓存 闭包函数形式 * @param key * @param closure * @param * @param * @return */ public Object getCacheByKeyInCacheAside1(K key,long expireTime, Closure closure) { Object ret = getCacheByKey(key.toString()); if (ret == null) { Object r = closure.execute(key); setCache(key.toString(), r.toString(),expireTime); return r; } return ret; } /** * lambda 形式 * @param key * @param expireTime * @param supplier * @param * @param * @return */ public Object getCacheByKeyInCacheAside2(K key,long expireTime,Supplier supplier) { Object ret = getCacheByKey(key.toString()); if (ret == null) { Object res = supplier.get(); setCache(key.toString(), res.toString(),expireTime); return res; } return ret; } } ``` Closure类 ```java package com.smile.component; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/11 09:16 */ public interface Closure { /** * 闭包函数 * @param input * @return */ public O execute(I input); } ``` 测试类: ```java package com.smile.lambda.test; import com.smile.App; import com.smile.component.Closure; import com.smile.component.RedisUtil; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.Random; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/11 09:39 */ @SpringBootTest(classes = App.class) @RunWith(SpringRunner.class) public class TestRedisCache { public static Integer id = null; static { id = new Random().nextInt(2); } @Autowired RedisUtil redisUtil; @Test public void test00() { String key = "test_00_" + id; Object value = redisUtil.getCacheByKey(key); if (value == null) { value = getValueFromDb(id); redisUtil.setCache(key,value,100); } System.out.println("value = " + value); } @Test public void test01() { String key = "test_01_" + id; Object value = redisUtil.getCacheByKeyInCacheAside1(key,100,new Closure() { @Override public String execute(String input) { return getValueFromDb(id); } }); System.out.println("value = " + value); } @Test public void test02() { String key = "test_02_" + id; Object value = redisUtil.getCacheByKeyInCacheAside2(key,100,() -> { return getValueFromDb(id); }); System.out.println("value = " + value); } private String getValueFromDb(Integer id) { return id + "_" + System.currentTimeMillis(); } } ``` ##### 引入它们的作用 * 简洁 * 捕获变量 ``` java private static final Logger log = LoggerFactory.getLogger(LambdaTest1.class); //内部类写法 String name = "myName"; Thread thread = new Thread() { @Override public void run() { log.info("匿名内部类 run ....:{}",name); } }; thread.start(); // lambda 写法 Thread lambdaThread = new Thread(() -> log.info("lambda run ....:{}",name)); lambdaThread.start(); ``` #### 1.2 lambda语法 ![image-20210930105245498.png](https://i.loli.net/2021/10/08/CuOxXZQBIDl13gs.png) *有**两种风格**,分别是:* 1. 表达式-风格 (parameters) -> expression 2. 块-风格 (parameters) -> { statements; } 其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to) 例: 1. `() -> {}` 2. `() -> "hello"` 3. `() -> { return "hello"; }` 代码实例: ```text // 1. 不需要参数,返回值为 5 () -> 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s) ``` #### 1.3 方法引用 ##### 什么是方法引用 > 方法引用是java8的新特性之一, 可以直接引用已有Java类或对象的方法或构造器。 > >方法引用与lambda表达式结合使用,可以进一步简化代码 > >方法引用通过方法的名字来指向一个方法。 ##### 方法引用具体使用 * 静态方法引用       :   ClassName :: staticMethodName * 构造器引用        :   ClassName :: new * 类的任意对象的实例方法引用:   ClassName :: instanceMethodName * 特定对象的实例方法引用  :   object :: instanceMethodName ###### 静态方法引用 ```java public class Test { public static void main(String[] args) { //lambda表达式使用: Arrays.asList(new String[] {"a", "c", "b"}).stream().forEach(s -> Test.println(s)); //静态方法引用: Arrays.asList(new String[] {"a", "c", "b"}).stream().forEach(Test::println); } public static void println(String s) { System.out.println(s); } } ``` ###### 构造器引用 ```java Supplier> supplier1= () -> new ArrayList(); //等价于 Supplier> supplier = ArrayList::new; ``` ###### 类的任意对象的实例方法引用 ```java Arrays.sort(strs,(s1,s2)->s1.compareToIgnoreCase(s2)); //等价于 Arrays.sort(strs, String::compareToIgnoreCase); ``` ###### 特定对象的实例方法引用 ```java public class Test { public static void main(String[] args) { Test test = new Test(); // lambda表达式使用: Arrays.asList(new String[] {"a", "c", "b"}).stream().forEach(s -> test.println(s)); // 特定对象的实例方法引用: Arrays.asList(new String[] {"a", "c", "b"}).stream().forEach(test::println); } public void println(String s) { System.out.println(s); } } ``` >[java8新特性 - 方法引用](https://www.jianshu.com/p/62465b26818f ) #### 1.4 函数式接口 ###### 定义 > 1. 只能有一个抽象方法 > 2. 可以有静态方法和默认方法,因为这两种方法都是已经实现的了 > 3. 可以包含Object里所有能重写的方法,因为即使接口包含像String toString()这样的抽象方法,它的实现类也会因继承了Object类,而再次对接口中的toString()方法进行实现。 > > 函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。 ###### 作用 > 方便直接用Lambda表达式构造出实例,让代码更加简洁。 ###### 注解 > @FunctionalInterface与@Override注解作用相似,用于在编译期间检查接口是否符合函数式接口的语法。 ###### java8内置函数接口 为了减少函数式接口的编写,JDK已经帮我们抽出几种常用的函数式接口 所在包java.util.function ![image-20210930121309569.png](https://i.loli.net/2021/10/08/JeW9Z47Lj3MPKzX.png) ![image.png](https://i.loli.net/2021/10/08/xU4dlCJTBHILmt1.png) ![image.png](https://i.loli.net/2021/10/08/lAweCbhf5MJtgKD.png) > [菜鸟教程 函数式接口](https://www.runoob.com/java/java8-functional-interfaces.html) ## 2.什么是 Stream > Stream 是用函数式编程方式在集合(Collection)类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作。 > > 开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找、遍历、过滤以及常见计算等。 > > > > **Stream是一种用来计算数据的流,它本身并没有存储数据。你可以认为它是对数据源的一个映射或者视图。** > > **它的工作流程是:获取数据源->进行一次或多次逻辑转换操作->进行归约操作形成新的流(最后可以将流转换成集合)** > > **IPO (input -> process -> output)** 下面两段代码都是用来返回低热量的菜肴名称的, 并按照卡路里排序,一个是用Java 7写的,另一个是用Java 8的流写的。 ```java //region java 7 List menu = StreamUtil.getMenu(); List lowCaloricDishes = new ArrayList<>(); for (Dish d : menu) { if (d.getCalories() < 400) { lowCaloricDishes.add(d); } } //用累加器筛 选元素 Collections.sort(lowCaloricDishes,new Comparator() { @Override public int compare(Dish d1,Dish d2) { return Integer.compare(d1.getCalories(),d2.getCalories()); } }); List lowCaloricDishesName = new ArrayList<>(); for (Dish d : lowCaloricDishes) { lowCaloricDishesName.add(d.getName()); } System.out.println(lowCaloricDishesName); //endregion ``` 在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次 性的中间容器。在Java 8中,实现的细节被放在它本该归属的库里了。 ```java //region java8 List collect = menu.stream() .filter(d -> d.getCalories() < 400) .sorted(Comparator.comparing(Dish::getCalories)) .map(Dish::getName) .collect(Collectors.toList()); System.out.println(collect); //endregion ``` 现在,你可以看出,从软件工程师的角度来看,新 的方法有几个显而易见的好处。 * 代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。这种方法 加上行为参数化让你可以轻松应对变化的需求:你很容易再创建一个代码版本,利用 Lambda表达式来筛选高卡路里的菜肴,而用不着去复制粘贴代码。 * 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上 sorted、map和collect操作),同时保持代码清晰可读。filter的结果 被传给了sorted方法,再传给map方法,最后传给collect方法。 ##### 2.1 Stream内置方法纵览 ![Stream.png](https://i.loli.net/2021/10/08/bOBPkNMJHUGnhSe.png) ##### 2.1 Stream流的操作 java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。我们再来看一下前面的例子: ![image-20211007160839864](https://i.loli.net/2021/10/08/rgGJioTwplVkXqU.png) 你可以看到两类操作: * filter、map和limit可以连成一条流水线; * collect触发流水线执行并关闭它。 可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。 图中展示了这两类 操作。这种区分有什么意义呢? ![image-20211007160929247.png](https://i.loli.net/2021/10/08/PEvD4QxXmIw7Yug.png) ###### 2.1.1中间操作 诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查 询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。 这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。 为了搞清楚流水线中到底发生了什么,我们把代码改一改 ```java public static void main(String[] args) { List menu = StreamUtil.getMenu(); System.out.println(menu); List names = menu .stream() .filter(v -> { //打印当前筛选的菜肴 System.out.println("filtering >>" + v.getName()); return v.getCalories() > 300; }).map(v -> { //提取菜名时打印出来 System.out.println("mapping >>" + v.getName()); return v.getName(); }).limit(3).collect(Collectors.toList()); System.out.println(names); } ``` 执行结果: ![image-20211007162013261.png](https://i.loli.net/2021/10/08/RNVoZxsYGwuh25m.png) 你会发现,有好几种优化利用了流的延迟性质。 第一,尽管很多菜的热量都高于300卡路里, 但只选出了前三个!这是因为limit操作和一种称为短路的技巧。 第二, 尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环 合并)。 ###### 2.1.2终端操作 终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Map、Integer,甚至void。例如,在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道 菜应用一个Lambda。把System.out.println传递给forEach,并要求它打印出由menu生成的 流中的每一个Dish: ```java StreamUtil.getMenu().stream().forEach(System.out::println); ``` ## 3.使用流 流的使用一般包括三件事: * 一个数据源(如集合)来执行一个查询; * 一个中间操作链,形成一条流的流水线; * 一个终端操作,执行流水线,并能生成结果。 流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配 置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。 ### 3.1 创建流的方式 #### 3.1.1由值创建流 你可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例 如,以下代码直接使用Stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再 一个个打印出来: ```java Stream stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); stream.map(String::toUpperCase).forEach(System.out::println); ``` 你可以使用empty得到一个空流,如下所示: ```java Stream emptyStream = Stream.empty(); ``` 你可以使用build方法构造一个流流,如下所示: ```java Stream build = Stream .builder() .add(4) .add(5) .build(); System.out.println("build := "); build.forEach(System.out::println); ``` #### 3.1.2 由文件生成流 Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容, 你可以用这个方法看看一个文件中有多少各不相同的词: ![image-20211007165430137.png](https://i.loli.net/2021/10/08/qmWx2kdEG4FLTlN.png) #### 3.1.3集合创建流 Java 集合内部提供了创建流的方法 ```java ArrayList arrayList = new ArrayList<>(); arrayList.add(7); arrayList.add(8); arrayList.add(9); Stream stream1 = arrayList.stream(); Stream stream2 = new HashMap<>().values().stream(); Stream stream3 = new HashSet<>().stream(); ``` #### 3.1.4使用数值范围的数值流 ```java Stream limit = Stream.iterate(0,n -> n + 1).limit(5); ``` #### 3.1.5从多个源创建流 ```java Stream concat1 = Stream.concat( Stream.of(1,2,3),Stream.builder().add(4).add(5).build()); concat1.forEach(System.out::println); ``` #### 3.1.6由数组生成流 你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如, 你可以将一个原始类型int的数组转换成一个IntStream,如下所示: int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = **Arrays.stream(numbers)**.sum(); ### 3.2 流操作 #### 3.2.1 筛选和切片 * **用谓词筛选 filter** ​ Streams接口支持filter方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回 boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如,筛选出所有素菜,创建一张素食菜单 ```java //筛选素菜 List collect = StreamUtil .getMenu() .stream() //此处两个filter等价 .filter(Dish::isVegetarian) .filter(v-> v.isVegetarian()) .collect(Collectors.toList()); System.out.println(collect); ``` 输出结果: [french fries, rice, season fruit, pizza] * **去重 distinct** ```java Stream.of("张无忌","金毛狮王","赵敏","小昭","周芷若","赵敏") .distinct() .forEach(System.out::println); ``` 输出结果: 张无忌 金毛狮王 赵敏 小昭 周芷若 * **截短流 limit** ```java Stream.of("张无忌","金毛狮王","赵敏","小昭","周芷若","赵敏") .limit(3) .forEach(System.out::println); ``` 输出结果: 张无忌 金毛狮王 赵敏 * **跳过元素 skipt** ```java Stream.of("张无忌","金毛狮王","赵敏","小昭","周芷若","赵敏") .skip(3) .forEach(System.out::println); ``` 输出结果: 小昭 周芷若 赵敏 #### 3.2.2 映射 * **对流中每一个元素应用函数** 流支持map方法,它会接受一个函数作为参数。 这个函数会被应用到每个元素上,并将其映 射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一 个新版本”而不是去“修改”)。 例如,下面的代码把方法引用Dish::getName传给了map方法, 来提取流中菜肴的名称: ```java List dishNames = StreamUtil.getMenu().stream() .map(Dish::getName) .collect(Collectors.toList()); System.out.println(dishNames); ``` 输出结果: [pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon] 因为getName方法返回一个String,所以map方法输出的流的类型就是Stream。 让我们看一个稍微不同的例子来巩固一下对map的理解。 给定一个单词列表,你想要返回另 一个列表,显示每个单词中有几个字母。怎么做呢? 你需要对列表中的每个元素应用一个函数。 这听起来正好该用map方法去做!应用的函数应该接受一个单词,并返回其长度。你可以像下面 这样,给map传递一个方法引用String::length来解决这个问题: ```java List words = Arrays.asList("Java 8", "Lambdas", "In", "Action"); List wordLengths = words.stream() .map(String::length) .collect(Collectors.toList()); System.out.println(wordLengths); ``` 输出结果: [6, 7, 2, 6] 现在让我们回到提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做? 你可以像下 面这样,再链接上一个map ``` List collect = StreamUtil.getMenu().stream() .map(Dish::getName) .map(String::length) .collect(Collectors.toList()); System.out.println(collect); ``` 输出结果: [4, 4, 7, 12, 4, 12, 5, 6, 6] * **流的扁平化** 你已经看到如何使用map方法返回列表中每个单词的长度了。让我们拓展一下:对于一张单 词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表 ["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]。 ```java String[] arrayOfWords = {"Hello", "World"}; List collect = Arrays.asList(arrayOfWords) .stream() .map(w -> w.split("")) .flatMap(Arrays::stream) .distinct() .collect(Collectors.toList()); System.out.println(collect); ``` 输出结果: [H, e, l, o, W, r, d] **使用flatMap方法的效果是:** **各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。** ![image-20211007175042873.png](https://i.loli.net/2021/10/08/Lh4DR7pvXlTMEIU.png) #### 3.2.3 查找和匹配 * **检查谓词是否至少匹配一个元素** anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看 看菜单里面是否有素食可选择: ```java if(menu.stream().anyMatch(Dish::isVegetarian)){ System.out.println("The menu is (somewhat) vegetarian friendly!!"); } ``` anyMatch方法返回一个boolean,因此是一个终端操作。 * **检查谓词是否匹配所有元素** allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里): ```java boolean isHealthy = menu.stream() .allMatch(d -> d.getCalories() < 1000); ``` **noneMatch** 和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如, 你可以用noneMatch重写前面的例子: ```java boolean isHealthy = menu.stream() .noneMatch(d -> d.getCalories() >= 1000); ``` anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路, 这就是大家熟悉 的Java中&&和||运算符短路在流中的版本。 * **查找元素** findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如, 你可能想找到一道素食菜肴。你可以结合使用filter和findAny方法来实现这个查询: ```java Optional dish = menu.stream() .filter(Dish::isVegetarian) .findAny(); ``` 流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。不过慢着, 代码里面的Optional是个什么玩意儿? >Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在 上面的代码中,findAny可能什么元素都没找到。Java 8的库设计人员引入了Optional,这 样就不用返回众所周知容易出问题的null了。 Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。 * isPresent()将在Optional包含值的时候返回true, 否则返回false。 * ifPresent(Consumer block)会在值存在的时候执行给定的代码块。 * T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。 * T orElse(T other)会在值存在时返回值,否则返回一个默认值。 例如,在前面的代码中你需要显式地检查Optional对象中是否存在一道菜可以访问其名称: ```java StreamUtil.getMenu() .stream() .filter(Dish::isVegetarian) .findAny() .ifPresent(d -> System.out.println(d.getName())); ``` * **查找第一个元素** 有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或 排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst 方法,它的工作方式类似于findany。例如,给定一个数字列表,下面的代码能找出第一个平方 能被3整除的数: ```java List someNumbers = Arrays.asList(1, 2, 3, 4, 5); Optional firstSquareDivisibleByThree = someNumbers.stream() .map(x -> x * x) .filter(x -> x % 3 == 0) .findFirst(); System.out.println(firstSquareDivisibleByThree); ``` #### 3.2.4 归约 到目前为止,你见到过的终端操作都是返回一个boolean(allMatch之类的)、void (forEach)或Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List。 * **元素求和** ```java System.out.println("##############################################################################"); Optional accResult = Stream.of(1,2,3,4).reduce((acc,item) -> { System.out.println("acc : " + acc); acc += item; System.out.println("item: " + item); System.out.println("acc+ : " + acc); System.out.println("--------"); return acc; }); System.out.println(accResult.get()); ``` 输出结果: ![image-20211007180357023.png](https://i.loli.net/2021/10/08/fcygSb4LMVaZRDh.png) * **最大值和最小值** 原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的reduce 来计算流中最大或最小的元素。正如你前面看到的,reduce接受两个参数: * 一个初始值 * 一个Lambda来把两个流元素结合起来并产生一个新值 Lambda是一步步用加法运算符应用到流中每个元素上的因此,你需要一个给定两个元素能够返回最大值的Lambda。 reduce操作会考虑新值和流中下一个元素,并产生一 个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce来计算流中的最大值,如图所示 ![image-20211007180831776.png](https://i.loli.net/2021/10/08/uUfYLa2dPB8gcpV.png) 最大值最小值代码: ```java private static void min() { Optional min = Stream.of(1,2,3,4).reduce(Integer::min); System.out.println(min); } private static void max() { Optional max = Stream.of(1,2,3,4).reduce(Integer::max); System.out.println(max); } ``` #### 3.2.5 小结 * 你可以使用filter、distinct、skip和limit对流做筛选和切片。 * 你可以使用map和flatMap提取或转换流中的元素。 * 你可以使用findFirst和findAny方法查找流中的元素。 * 你可以用allMatch、noneMatch和anyMatch方法让流匹配给定的谓词。 - 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。 - 你可以利用reduce方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大 元素。 - filter和map等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才 能计算出一个值。sorted和distinct等操作也要存储状态,因为它们需要把流中的所 有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。 - 流有三种基本的原始类型特化:IntStream、DoubleStream和LongStream。它们的操 作也有相应的特化。 - 流不仅可以从集合创建,也可从值、数组、文件以及iterate与generate等特定方法 创建。 - 无限流是没有固定大小的流。 ### 3.3用流收集数据 想象一下,你有一个由Transaction 构成的List,并且想按照级别进行分组。 在没有Lambda的Java里,哪怕像这种简单的用例 实现起来都很啰嗦,就像下面这样。 ```java private static void groupInJava7() { List transactions=getListByCOndition(); Map> transactionsByCurrencies = new HashMap<>(); for (Transaction transaction : transactions) { Integer level = transaction.getLevel(); List transactionList = transactionsByCurrencies.getOrDefault(level,new ArrayList<>()); transactionList.add(transaction); transactionsByCurrencies.putIfAbsent(level,transactionList); } transactionsByCurrencies.entrySet().forEach(System.out::println); } ``` 如果你是一位经验丰富的Java程序员,写这种东西可能挺顺手的,不过你必须承认,做这么 简单的一件事就得写很多代码。更糟糕的是,读起来比写起来更费劲!代码的目的并不容易看出 来,尽管换作白话的话是很直截了当的:“把列表中的交易按货币分组。”你在本章中会学到,用 Stream中collect方法的一个更通用的Collector参数,你就可以用一句话实现完全相同的结 果,而用不着使用上一章中那个toList的特殊情况了: ```java private static void groupInJava8() { List transactions=getListByCOndition(); Map> collect = transactions .stream() .collect(Collectors.groupingBy(Transaction::getLevel)); collect.entrySet().forEach(System.out::println); } ``` 这一比差得还真多,对吧? ##### 3.3.1 **收集器简介** 前一个例子清楚地展示了函数式编程相对于指令式编程的一个主要优势:你只需指出希望的 结果——“做什么”,而不用操心执行的步骤——“如何做”。在上一个例子里,传递给collect 方法的参数是Collector接口的一个实现,也就是给Stream中元素做汇总的方法。上一章里的 toList只是说“按顺序给每个元素生成一个列表”;在本例中,groupingBy说的是“生成一个 Map,它的键是(等级)桶,值则是桶中那些元素的列表”。 要是做多级分组,指令式和函数式之间的区别就会更加明显:由于需要好多层嵌套循环和条 件,指令式代码很快就变得更难阅读、更难维护、更难修改。相比之下,函数式版本只要再加上 一个收集器就可以轻松地增强功能了 ##### 3.3.2 **归约和汇总** 以下代码导入了Collectors类的所有静态工厂方法: **import static java.util.stream.Collectors.*;** * 查找流中的最大值和最小值 ```java //region 最大值 Comparator dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional mostCalorieDish = StreamUtil.getMenu() .stream() .collect(maxBy(dishCaloriesComparator)); System.out.println(mostCalorieDish); //endregion //region 最小值 Optional minCalorieDish = StreamUtil.getMenu() .stream() .collect(minBy(dishCaloriesComparator)); System.out.println(minCalorieDish); //endregion ``` * **汇总** Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一 个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方 法后即执行我们需要的汇总操作。 举个例子来说,你可以这样求出菜单列表的总热量: ```java int totalCalories = StreamUtil.getMenu().stream().collect(summingInt(Dish::getCalories)); ``` Collectors.summingLong和Collectors.summingDouble方法的作用完全一样,可以用 于求和字段为long或double的情况。 **但汇总不仅仅是求和;** 还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数: ```java double avgCalories =StreamUtil.getMenu().stream().collect(averagingInt(Dish::getCalories)); ``` ```java IntSummaryStatistics menuStatistics =StreamUtil.getMenu().stream().collect(summarizingInt(Dish::getCalories)); ``` 这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了 方便的取值(getter)方法来访问结果。打印menuStatisticobject会得到以下输出: IntSummaryStatistics{count=9, sum=4300, min=120,average=477.777778, max=800} 同样,相应的summarizingLong和summarizingDouble工厂方法有相关的LongSummary- Statistics和DoubleSummaryStatistics类型,适用于收集的属性是原始类型long或 double的情况。 * **连接字符串** joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。这意味着你把菜单中所有菜肴的名称连接起来,如下所示: ```java //region 拼接字符串 String shortMenu = StreamUtil.getMenu().stream().map(Dish::getName).collect(joining()); System.out.println(shortMenu); //porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon String shortMenu2 = StreamUtil.getMenu().stream().map(Dish::getName).collect(joining(", ")); System.out.println(shortMenu2); //pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon //endregion ``` * **广义的归约汇总** 事实上,我们已经讨论的所有收集器,都是一个可以用reducing工厂方法定义的归约过程 的特殊情况而已。 Collectors.reducing工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。(但是,请记得方便程序员和可读性是头等大事!) 例如,可以用reducing方法创建的收集器来计算你菜单的总热量,如下所示: ```java int totalCalories = StreamUtil.getMenu().stream().collect(reducing( 0, Dish::getCalories, (i, j) -> i + j)); ``` 它需要三个参数。 * 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值 和而言0是一个合适的值。 - 第二个参数就是你在6.2.2节中使用的函数,将菜肴转换成一个表示其所含热量的int。 - 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是 8 对两个int求和。 同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示: ```java Optional mostCalorieDish2 = StreamUtil.getMenu() .stream() .collect(reducing( (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)); ``` ##### 3.3.3 分组 假设你要把菜单中的菜按照类型进行分类, 有肉的放一组,有鱼的放一组,其他的都放另一组。用Collectors.groupingBy工厂方法返回 的收集器就可以轻松地完成这项任务,如下所示: ```java Map> collect = StreamUtil .getMenu() .stream() .collect(Collectors.groupingBy(Dish::getType)); collect.entrySet().forEach(System.out::println); /** * 输出结果 * FISH=[prawns, salmon] * MEAT=[pork, beef, chicken] * OTHER=[french fries, rice, season fruit, pizza] */ ``` ![image-20211007212439826.png](https://i.loli.net/2021/10/08/HBtiwbS2Jnkc4DU.png) 但是,**分类函数不一定像方法引用那样可用**,因为你想用以分类的条件可能比简单的属性访 问器要复杂。 例如,你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700 卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。 由于Dish类的作者 没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式: ```java Map> dishesByCaloricLevel = menu .stream() .collect(Collectors.groupingBy(dish -> { if (dish.getCalories() <= 400) { return CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return CaloricLevel.NORMAL; } else { return CaloricLevel.FAT; } })); dishesByCaloricLevel.entrySet().forEach(System.out::println); /** * 输出结果 * NORMAL=[beef, french fries, pizza, salmon] * DIET=[chicken, rice, season fruit, prawns] * FAT=[pork] */ ``` * 多级分组 要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创 建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。 那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流 中项目分类的二级标准,如代码清单所示。 ```java package com.smile.stream; import com.smile.stream.pojo.Dish; import com.smile.stream.pojo.Type; import java.util.List; import java.util.Map; import static java.util.stream.Collectors.*; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/7 21:29 */ public class GroupDemo2 { public static void main(String[] args) { List menu = StreamUtil.getMenu(); Map>> dishesByTypeCaloricLevel = menu .stream() .collect(groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() <= 400) { return CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return CaloricLevel.NORMAL; } else { return CaloricLevel.FAT; } }))); dishesByTypeCaloricLevel.entrySet().forEach(System.out::println); /** * 输出结果 * FISH={NORMAL=[salmon], DIET=[prawns]} * MEAT={NORMAL=[beef], DIET=[chicken], FAT=[pork]} * OTHER={NORMAL=[french fries, pizza], DIET=[rice, season fruit]} */ } public enum CaloricLevel {DIET,NORMAL,FAT} } ``` * 按子组收集数据 我们看到可以把第二个groupingBy收集器传递给外层收集器来实现多级分 组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数: ```java Map typesCount = StreamUtil.getMenu() .stream() .collect(groupingBy(Dish::getType, counting())); System.out.println("typesCount = " + typesCount); /** * 输出结果 * typesCount = {MEAT=3, OTHER=4, FISH=2} */ ``` 还要注意,普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f, toList())的简便写法。 再举一个例子,可以把前面用于查找菜单中热量最高的菜肴的收集器改一改,按照菜的类型分类 ```java Map> collect = StreamUtil.getMenu() .stream() .collect(groupingBy(Dish::getType,maxBy(Comparator.comparingInt(Dish::getCalories)))); System.out.println("collect = " + collect); /** * 输出结果 * collect = {MEAT=Optional[pork], OTHER=Optional[pizza], FISH=Optional[salmon]} */ ``` 因为分组操作的Map结果中的每个值上包装的Optional没什么用,所以你可能想要把它们 去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用 Collectors.collectingAndThen工厂方法返回的收集器 ```java Map collect1 = StreamUtil.getMenu() .stream() .collect(groupingBy(Dish::getType, collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)),Optional::get))); System.out.println("collect1 = " + collect1); /** * 输出结果 * collect1 = {MEAT=pork, OTHER=pizza, FISH=salmon} */ ``` ##### 3.3.4 分区 * 分区的优势 分区的好处在于保留了分区函数返回true或false的两套流元素列表。在上一个例子中,要 得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false 键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy 工厂方法有一个重载版本,可以像下面这样传递第二个收集器: ```java package com.smile.stream.group; import com.smile.stream.StreamUtil; import com.smile.stream.pojo.Dish; import com.smile.stream.pojo.Type; import java.util.List; import java.util.Map; import static java.util.stream.Collectors.*; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/7 21:50 */ public class Group4 { public static void main(String[] args) { Map>> collect = StreamUtil .getMenu() .stream() .collect(partitioningBy(Dish::isVegetarian,groupingBy(Dish::getType))); System.out.println("collect = " + collect); //collect = {false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, // true={OTHER=[french fries, rice, season fruit, pizza]}} } } ``` 这里,对于分区产生的素食和非素食子流,分别按类型对菜肴分组,得到了一个二级Map ##### 3.3.5 收集器接口 * Collector接口 ```java public interface Collector { Supplier supplier(); BiConsumer accumulator(); Function finisher(); BinaryOperator combiner(); Set characteristics(); } ``` 本列表适用以下定义。 * T是流中要收集的项目的泛型。 * A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。  R是收集操作得到的对象(通常但并不一定是集合)的类型。 例如,你可以实现一个ToListCollector类,将Stream中的所有元素收集到一个 List里,它的签名如下: public class ToListCollector implements Collector, List> * 理解**Collector**接口声明的方法 * 建立新的结果容器:**supplier**方法 >supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会 > >创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收 集器,比如我们的ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的ToListCollector中,supplier返回一个空的List,如下所示: > >```java > public Supplier> supplier() { > return () -> new ArrayList(); > } >``` > >请注意你也可以只传递一个构造函数引用: > >```java >public Supplier> supplier() { > return ArrayList::new; >} >``` * 将元素添加到结果容器:**accumulator**方法 >accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n1 个项目),还有第n个元素本身。 5 该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的 > >元素的效果。对于ToListCollector,这个函数仅仅会把当前项目添加至已经遍历过的项目的 列表: > >```java >public BiConsumer, T> accumulator() { > return (list, item) -> list.add(item); >} >``` > >你也可以使用方法引用,这会更为简洁: > >```java >public BiConsumer, T> accumulator() { > return List::add; >} >``` * 对结果容器应用最终转换:**finisher**方法 在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加 器对象转换为整个集合操作的最终结果。通常,就像ToListCollector的情况一样,累加器对 象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数: ```java public Function, List> finisher() { return Function.identity(); } ``` ![image-20211007220325860.png](https://i.loli.net/2021/10/08/pPdfAoacB2siwCJ.png) * 合并两个结果容器:**combiner**方法 >四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对 流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对于toList而言, 这个方法的实现非常简单,只要把从流的第二个部分收集到的项目列表加到遍历第一部分时得到 的列表后面就行了: > >```java > public BinaryOperator> combiner() { > return (list1, list2) -> { > list1.addAll(list2); > return list1; } > } >``` * **characteristics**方法 >最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义 12 了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。 > >Characteristics是一个包含三个项目的枚举。 > >* UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。 >* CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归 > >约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。  IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种 > >情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检 15 查地转换为结果R是安全的。 > >我们迄今开发的ToListCollector是IDENTITY_FINISH的,因为用来累积流中元素的 > >List已经是我们要的最终结果,用不着进一步转换了,但它并不是UNORDERED,因为用在有序 流上的时候,我们还是希望顺序能够保留在得到的List中。最后,它是CONCURRENT的,但我们 刚才说过了,仅仅在背后的数据源无序时才会并行处理。 * 全部融合到一起 ##### 3.3.6 小结 - collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。 - 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。 - 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。 - 收集器可以高效地复合起来,进行多级分组、分区和归约。 - 你可以实现Collector接口中定义的方法来开发你自己的收集器。 ### 3.4 并行流 Stream接口可以让你非常方便地处理它的元素:可以通过对 收集源调用parallelStream方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷 分配给多核处理器的所有内核,让它们都忙起来。 请注意,在现实中,对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并 10 行执行。类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。请注意,你 可能以为把这两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要 顺序执行。 ``` stream.parallel() .filter(...) .sequential() .map(...) .parallel() .reduce(); ``` >看看流的parallel方法,你可能会想,并行流用的线程是从哪儿来的?有多少个?怎么 自定义这个过程呢? > >并行流内部使用了默认的ForkJoinPool,它默认的 线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().available- Processors()得到的。 > >但 是 你 可 以 通 过 系 统 属 性 java.util.concurrent.ForkJoinPool.common. parallelism来改变线程池大小,如下所示: > >System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12"); > >这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个 并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值, 除非你有很好的理由,否则我们强烈建议你不要修改它。 **注意:** 并行流默认使用的全局公用的ForkJoinPool,**仅适用于CPU密集型任务**, IO密集型操作请使用java.util.concurrent.CompletableFuture执行多线程任务 ```java public static CompletableFuture runAsync(Runnable runnable, Executor executor) { return asyncRunStage(screenExecutor(executor), runnable); } public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) { return asyncSupplyStage(screenExecutor(executor), supplier); } ``` ### 3.5附录 ![image-20211007182311118.png](https://i.loli.net/2021/10/08/Sw8xiEp3vn2Jk1f.png) ### 3.6 相关示例代码&练习 * 针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个集合 * 针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个线程安全的集合(Vector) * 针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个set * 针对字符串 转小写并去除首尾空格,并过滤空字符串,且去重 把结果收集成一个以“,”拼接的字符串 ```java package com.smile.stream.exercises; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Vector; import java.util.stream.Collectors; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/8 16:24 */ public class Demo1 { public static final List stringList= Arrays.asList("Java"," Python "," go" ,"","shell","go"); /** * 1针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个集合 * 2针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个线程安全的集合(Vector) * 3针对字符串 转小写并去除首尾空格,并过滤空字符串,把结果收集成一个set * 4针对字符串 转小写并去除首尾空格,并过滤空字符串,且去重 把结果收集成一个以“,”拼接的字符串 */ public static void main(String[] args) { function1(); function2(); function3(); function4(); } private static void function4() { System.out.println("function4"); String collect = stringList .stream() .map(String::trim) .map(String::toLowerCase) .filter(StringUtils::isNotBlank) .distinct() .collect(Collectors.joining(",")); System.out.println(collect); } private static void function3() { System.out.println("function3"); HashSet collect = stringList .stream() .map(String::trim) .map(String::toLowerCase) .filter(StringUtils::isNotBlank) .collect(Collectors.toCollection(HashSet::new)); System.out.println(collect); } private static void function2() { System.out.println("function2"); Vector collect = stringList .stream() .map(String::trim) .map(String::toLowerCase) .filter(StringUtils::isNotBlank) .collect(Collectors.toCollection(Vector::new)); System.out.println(collect); } private static void function1() { System.out.println("function1"); List collect = stringList .stream() .map(String::trim) .map(String::toLowerCase) .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); System.out.println(collect); } } ``` ```java package com.smile.stream.pojo; import lombok.AllArgsConstructor; import lombok.Data; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/8 16:40 */ @Data @AllArgsConstructor public class Person { /** * id */ private Integer id; /** * 账号 */ private String account; /** * 名称 */ private String name; /** * 年龄 */ private Integer age; /** *性别,0男 1 女 */ private Integer sex; } ``` * 把集合 转换成一个map。key为账号,value为对象本身(后来者居上) * 把集合 转换成一个map。key为账号,value为name (key冲突设置value为“,”拼接字符串) * 把集合按照sex 分组 * 把集合按照age分组,value为 name字段的集合 * 把集合按照age分组,value为 name字段 以“,”拼接的字符串 * 筛选集合中的女性,并按照年龄排序 * 求出集合中最大的年龄 * 求出集合中最小的年龄 * 求出集合中平均年龄 * 找出集合中 任意一个18岁的女生 * 集合根据sex分组,并过滤年级最小的person ```java package com.smile.stream.exercises; import com.smile.stream.pojo.Person; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import static java.util.stream.Collectors.maxBy; /** * @author LiYuhang * @version 0.1 * @application * @Date 2021/10/8 16:41 */ public class Demo2 { public static final List personList=new ArrayList<>(10); static{ initData(); } private static void initData() { personList.add(new Person(1,"10001","小明",23,0)); personList.add(new Person(2,"10002","小光",26,0)); personList.add(new Person(3,"10003","小华",25,0)); personList.add(new Person(4,"10003","花花",24,1)); personList.add(new Person(5,"10006","小红",18,1)); personList.add(new Person(6,"10007","小兰",17,1)); } /** * 把集合 转换成一个map。key为账号,value为对象本身(后来者居上) * 把集合 转换成一个map。key为账号,value为name (key冲突设置value为“,”拼接字符串) * 把集合按照sex 分组 * 把集合按照age分组,value为 name字段的集合 * 把集合按照age分组,value为 name字段 以“,”拼接的字符串 * 筛选集合中的女性,并按照年龄排序 * 求出集合中最大的年龄 * 求出集合中最小的年龄 * 求出集合中平均年龄 * 找出集合中 任意一个18岁的女生 * 集合根据sex分组,并过滤年级最小的person */ public static void main(String[] args) { function1(); function2(); function3(); function4(); function5(); function6(); function7(); function8(); function9(); function10(); function11(); } private static void function1() { System.out.println("function1"); Map collect = personList .stream() .collect(Collectors.toMap(Person::getAccount,Function.identity(),(v1,v2) -> v2)); collect.entrySet().forEach(System.out::println); } private static void function2() { System.out.println("function2"); Map collect = personList .stream() .collect(Collectors.toMap(Person::getAccount,Person::getName,(v1,v2) -> v1+","+v2)); collect.entrySet().forEach(System.out::println); } private static void function3() { System.out.println("function3"); Map> collect = personList .stream() .collect(Collectors.groupingBy(Person::getSex)); collect.entrySet().forEach(System.out::println); } private static void function4() { System.out.println("function4"); Map> collect = personList .stream() .collect( Collectors.groupingBy( Person::getSex,Collectors.mapping(Person::getName,Collectors.toList()) ) ); collect.entrySet().forEach(System.out::println); } private static void function5() { System.out.println("function5"); Map collect = personList .stream() .collect( Collectors.groupingBy( Person::getSex,Collectors.mapping(Person::getName,Collectors.joining(",")) ) ); collect.entrySet().forEach(System.out::println); } private static void function6(){ System.out.println("function6"); List collect = personList .stream() .filter(v -> Objects.equals(1,v.getSex())) .sorted(Comparator.comparing(Person::getAge)) .collect(Collectors.toList()); collect.forEach(System.out::println); } private static void function7(){ System.out.println("function7"); Optional max = personList .stream() .max(Comparator.comparing(Person::getAge)); System.out.println("max = " + max); } private static void function8(){ System.out.println("function8"); Optional min = personList .stream() .min(Comparator.comparing(Person::getAge)); System.out.println("min = " + min); } private static void function9(){ System.out.println("function9"); Double average = personList .stream() .collect(Collectors.averagingInt(Person::getAge)); System.out.println("average = " + average); } private static void function10(){ System.out.println("function10"); Optional personOptional = personList .stream() .filter(v -> Objects.equals(18,v.getAge())) .findAny(); System.out.println("personOptional = " + personOptional); } private static void function11(){ System.out.println("function11_1"); Map collect = personList .stream() .collect( Collectors.groupingBy(Person::getSex, Collectors.collectingAndThen( maxBy(Comparator.comparingInt(Person::getAge)),Optional::get) ) ); collect.entrySet().forEach(System.out::println); } } ``` **思考:** Distinct() 方法是根据object中的hash 和equals方法判断是否重复,如果有个二放/三方类,我们无法修改原始代码的equals和hash方法,请思考: * 如何根据某个字段,做唯一性校验? * 如何根据多个字段组合值,做唯一性校验? ### 3.7 其他 *代码仓库:* **推荐阅读 Java8 in action** ![image-20211008173136629.png](https://i.loli.net/2021/10/08/WePr6m3JSjYbphT.png)