Loading... [Java8.pdf][1] 《Java 8 in Action》 [1]: http://www.tangsong.fun/usr/uploads/2021/01/568626069.pdf <!--more--> 老坑未补,新坑又来。 这次开坑是因为在开发过程中频繁使用到了Stream流、Lambda表达式,以及一些函数式数据处理, java.util.Optional 类的使用等等。打算学了之后连同样例一起放在这里。 ## 一、为什么要关心java8 java从一出生就占据了许多的优势,如:有了集成的线程和锁的支持,且支持小规模并发。此外,将Java编译成JVM字节码意味着它成为了互联网applet(小应用)的首选。 在1990+年代面向对象就已经兴起了,因为这种“**一切都是对象**”;“**单击鼠标就能给处理程序发送一个事件消息**”的思维模型受到广大程序员的喜爱。而Java的“**一次编写,随处运行**”模式,刚好符合了时代浪潮。 Java 8中的主要变化反映了它开始远离常侧重改变现有值的经典**面向对象思想**,而向**函数式编程**领域转变,在大面上考虑**做什么**(例如,创建一个值代表所有从A到B低于给定价格的交通线路)被认为是头等大事,并和**如何实现**(例如,扫描一个数据结构并修改某些元素)区分开来。请注意,如果极端点儿来说,传统的面向对象编程和函数式可能看起来是冲突的。但是我们的理念是获得两种编程范式中最好的东西,这样你就有更大的机会为任务找到理想的工具了。 ### (1)流处理 **流**:是一系列数据项,一次只生成一项。程序可以从输入流(System.in)中一个一个读取数据项,然后以同样的方式将数据项写入输出流(System.out)。 举个栗子:`cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3` 通过Unix的 cat 命令会把两个文件连接起来创建一个流, tr 会转换流中的字符, sort 会对流中的行进行排序,而 tail -3 则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起。 假设 file1 和 file2 中每行都只有一个词,那么这句话的实际含义就是:先把字母转换成小写字母,然后打印出按照词典排序出现在最后的三个单词。 但重点在于,在Unix中,这些命令是同时进行的,就像加工厂的流水线一样,尽管每条流水线是一个序列,但是不同加工厂之间是并行的。也正是基于这种思想,`Stream API`诞生了。 这就好比我不需要通过,把查出的List<Object>中筛选出符合条件的数据到新的List中,再转成Map对象后,再添加到List<Map>对象中返回,而是在stream.map()里面定义完后一步到位。 ### (2)用行为参数化把代码传递给方法 上面的举例其实就是用行为参数化把代码传递给方法的体现。再举个栗子: 在Java8之前,如果你需要自定义排序sort,你无法直接将 `compare()`方法作为参数传递给sort,而是需要创建一个Compare对象,再通过实例化的对象作为参数进行传递。 ```java Collections.sort(students, Comparator<? super T> c) //转变为 Collections.sort(students, Comparator.comparing(Student::getName)); ``` ### (3)并行与共享的可变数据 首先,这是个理念。流处理的并行需要你保证对不同的输入**安全的执行**。这就需要你写代码时不能访问共享的可变数据。**在这种命令式编程范式中,你写的程序则是一系列改变状态的指令。** 这种“不能有共享的可变数据”就意味着你写的方法就像是一个数学函数,其内部是不可见的。 **PS:**既然Steam需要并行处理,那么在高并发场景下就需要保证其使用的数据结构的数据一致性。可以使用 `ConcurrentHashMap`、`CopyOnWriteArrayList`等。 但是IDEA已经支持对Stream的Debug调试,而且是真的香~ 参考指路:[stream流:idea调试小技巧](https://www.cnblogs.com/wwjj4811/p/13734382.html) --- ## 二、行为参数化传递代码的演变过程 通过行为参数化传递代码,可以理解为: 举个栗子,如果一开始对一个对象数组 `appleList`进行两种标准 `color`、`weight`的筛选操作: (1)第一层,就是写两个方法 `method(appleList, color/weight)`去筛选。 这些代码不仅复用高,而且把所有逻辑写在同一模块,很难维护。 (2)第二层,你可以写一个标准接口,在里面生定义一个标准筛选方法,让不同的标准去实现它。 需要哪个标准,就只需要调用该标准的实现类。这也正是**策略模式**。 其中,这个**标准接口就是算法族,而不同的标准实现类就是策略**。 当我们增加或修改一个需求时,只需要新增一个策略就可以了。 但是这样很啰嗦,因为你需要声明很多只要实例化一次的类(策略) ```java @Data @AllArgsConstructor public class Apple { private String color; private int weight; } ``` ```java public interface ApplePredicate { boolean test(Apple apple); } ``` ```java public class AppleGreenColorPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return "green".equals(apple.getColor()); } } ``` ```java public class AppleHeavyWeightPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.getWeight() > 150; } } ``` ```java public class FilterApples { public static void main(String[] args) { Apple apple1 = new Apple("green", 160); Apple apple2 = new Apple("green", 140); Apple apple3 = new Apple("red", 160); Apple apple4 = new Apple("red", 140); List<Apple> list = new ArrayList<>(); list.add(apple1); list.add(apple2); list.add(apple3); list.add(apple4); // 策略模式 ApplePredicate greenColorPredicate = new AppleGreenColorPredicate(); System.out.println(filterApples(list, greenColorPredicate).toString()); ApplePredicate heavyWeightPredicate = new AppleHeavyWeightPredicate(); System.out.println(filterApples(list, heavyWeightPredicate).toString()); } public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) { List<Apple> result = new ArrayList<>(); for (Apple apple : inventory) { if (p.test(apple)) { result.add(apple); } } return result; } } ``` (3)第三层,在调用如上的 `filterApples()`方法时,传递的是匿名类。也就是在方法中实现了策略。 缺点是匿名类的代码维护性和可读性很差。 注意,这里已经演变到不是策略模式的范畴了。因为它把策略(筛选标准)和业务逻辑(该标准下的操作)杂糅在一起了。 准确来说,它是用Lambda表达式实现行为参数化的过渡期。 (4)第四层,在调用 `filterApples()`方法时,将匿名类替换成Lamdba表达式。 ```java ApplePredicate greenColorPredicate = new AppleGreenColorPredicate(); filterApples(list, greenColorPredicate).toString(); ↓ filterApples(list, (Apple apple) -> "green".equals(apple.getColor())) ``` (5)第五层,抽象化filter。对所有对象都可以进行自定义过滤。 ```java @Data @AllArgsConstructor public class Apple { private String color; private int weight; } ``` ```java @Data @AllArgsConstructor public class Pen { private String color; private int size; } ``` ```java public interface Predicate<T> { boolean test(T t); } ``` ```java public class FiterT { public static void main(String[] args) { Apple apple1 = new Apple("green", 160); Apple apple2 = new Apple("green", 140); Apple apple3 = new Apple("red", 160); Apple apple4 = new Apple("red", 140); List<Apple> appleList = new ArrayList<>(); appleList.add(apple1); appleList.add(apple2); appleList.add(apple3); appleList.add(apple4); Pen pen1 = new Pen("black", 1); Pen pen2 = new Pen("black", 2); Pen pen3 = new Pen("red", 1); Pen pen4 = new Pen("red", 2); List<Pen> penList = new ArrayList<>(); penList.add(pen1); penList.add(pen2); penList.add(pen3); penList.add(pen4); System.out.println(filter(appleList, (Apple apple) -> "green".equals(apple.getColor()))); System.out.println(filter(penList, (Pen pen) -> pen.getSize() > 1)); } public static <T> List<T> filter(List<T> inventory, Predicate<T> p) { List<T> result = new ArrayList<>(); for (T e : inventory) { if (p.test(e)) { result.add(e); } } return result; } } ``` ### 总结 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。并且可让代码更好地适应不断变化的要求,减轻未来的工作量。 --- ## 三、Lambda表达式 ### 1.基本语法 `(parameters) -> expression` `(parameters) -> { statements; }` eg: `(int a, int b) -> a * b` `(Integer i) -> { return "the" + i; }` ### 2.函数式接口 `函数式接口`就是只定义**一个抽象方法**的**接口**。 具体来说,Lambda表达式就是函数式接口的一个具体实现的实例。 在 java.util.function 包中引入了几个新的函数式接口。   注意,**任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个 try/catch 块中。** ### 3.类型检查 Lambda的类型是从上下文推断的,大致流程如下: `List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);`  #### 对局部变量的限制 局部变量必须显式声明为 `final` ,或事实上是 final 。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。 (注:捕获实例变量可以被看作捕获最终局部变量 this 。) 这种限制存在的原因在于**局部变量保存在栈上,并且隐式表示它们仅限于其所在线程**。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(**实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的**)。 同时这一限制也是因为 不鼓励你使用改变外部变量的典型命令式编程模式,因为这种模式会阻碍很容易做到的并行处理。 ### 4.方法引用 方法应用就是让你根据已有的方法来创建Lambda表达式,是通过名称去调用它,而不是描述如何调用它。这种显式的指明方法名称会让代码的可读性更好。 eg: `Apple::getWeight`   #### 示例 可以从上图回顾: Supplier<T>是 `()->T`,用于创建对象 Function<T,R>是 `T->R`,可以用于部分参数的构造 ```java // 利用默认构造函数创建 Apple 的Lambda表达式(面向过程) Supplier<Apple> c1 = () -> new Apple(); // 调用 Supplier 的 get 方法将产生一个新的 Apple Apple a1 = c1.get(); ↓ // 构造函数引用指向默认的 Apple() 构造函数(面向名称) Supplier<Apple> c1 = Apple::new; Apple a1 = c1.get(); // 用要求的重量创建一个 Apple 的Lambda表达式 Function<Integer, Apple> c2 = (weight) -> new Apple(weight); // 调用该 Function 函数的 apply 方法,并给出要求的重量,将产生一个新的 Apple 对象(T->R) Apple a2 = c2.apply(110); ↓ // 指向 Apple(Integer weight)的构造函数引用 Function<Integer, Apple> c2 = Apple::new; Apple a2 = c2.apply(110); // 同理,Apple的全参构造可以用BiFunction<T,U,R>:`(T,U)->R` BiFunction<String,Integer,Apple> c3 = Apple::new; Apple a3 = c3.apply("green", 150); ``` 方法引用要比“过程”引用难一点,不过IDEA会自动帮我们优化。但是有时候也会出现人工智障的毛病,并且这块对后来维护者来说,也是需要一点功底的。 真香。 更高级的还有Lambda表达式的各种比较器复合、函数复合,现在学了也容易忘,后续有碰到再补充吧。 --- ## 四、Stream流理念 流的定义为:**从支持数据处理操作的源生成的元素序列**。 实在让初学者晦涩难懂,还是直接通过实战来解释吧。 ```java @Data @AllArgsConstructor public class Dish { private final String name; private final boolean vegetarian; private final int calories; private final Type type; public enum Type { MEAT, FISH, OTHER } } ``` ```java public class Test { public static void main(String[] args) { List<Dish> menu = Arrays.asList( new Dish("pork", false, 800, Dish.Type.MEAT), new Dish("beef", false, 700, Dish.Type.MEAT), new Dish("chicken", false, 400, Dish.Type.MEAT), new Dish("french fries", true, 530, Dish.Type.OTHER), new Dish("rice", true, 350, Dish.Type.OTHER), new Dish("season fruit", true, 120, Dish.Type.OTHER), new Dish("pizza", true, 550, Dish.Type.OTHER), new Dish("prawns", false, 300, Dish.Type.FISH), new Dish("salmon", false, 450, Dish.Type.FISH)); getThreeHighCaloricDishNames(menu); } public static void getThreeHighCaloricDishNames(List<Dish> menu) { List<String> threeHighCaloricDishNames = menu.stream() .filter(dish -> dish.getCalories() > 300) .map(Dish::getName) .limit(3) .collect(Collectors.toList()); System.out.println(threeHighCaloricDishNames); } } ``` 菜单menu是**数据源**,为流提供**元素序列**。`filter`、`map`、`limit`、`collect`成为称为**数据处理**操作,并且前三个都是返回流,它们连接成一条**流水线**,这些成为**中间操作**。而最后一个则是处理流水线,称为**终端操作**。 在调用 collect 之前,没有任何结果产生,实际上根本就没有从 menu 里选择元素,链中的方法调用都在排队等待,直到调用collect 。 也就是说,即使在 `filter`、`map`的过程中输出当前值,也只会是前三个。因为它们和后面的 `limit`是一个流水线。  * `filter`:接收Lambda,从流中筛选出符合条件的元素。 * `map`:接收Lambda,将元素转换成其他形式或者提取信息。 * `limit`:截断流,使元素不超过给定的数量。 * `collect`:接受各种方案,将流转换成为其他形式。 ### 流与集合 集合与流之间的差异就在于什么时候进行计算。 集合是一个**内存**中的数据结构,它包含数据结构中目前所有的值。也就是,集合中的每个元素都得先算出来才能添加到集合中。 (你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。) 流则是在**概念**上固定的数据结构(你不能添加或删除元素),其元素则是**按需计算**的。 这个思想就是用户仅仅从流中提取需要的值,而这些值——在用户看不见的地方——只会按需生成。这是一种**生产者-消费者**的关系。从另一个角度来说,**流就是一个延迟创建的集合**:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)。 #### 流只能遍历一次 集合和流的另一个区别在于它们遍历数据的方式。 流就像迭代器一样,遍历完一遍之后这个流就被消费掉了。只能从原数据那边重新获得流 `xxx.stream()`。 ```java public static void one() { List<String> title = Arrays.asList("1", "2", "3"); Stream<String> s = title.stream(); s.forEach(System.out::println); // s = title.stream(); 注释掉就会报流已被消费的异常 s.forEach(System.out::println); } ``` `java.lang.IllegalStateException: stream has already been operated upon or closed` #### 流的内部迭代与集合的外部迭代 如上文所说,到 `collect()`方法时,只是执行了操作的流水线,并没有迭代。 内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理。 或者说,Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。 ```java List<String> names = menu.stream() .map(Dish::getName) .collect(toList()); ``` 而对集合的操作,如“优雅”的 `for-each`结构其实也只是一个语法糖,它的背后也是通过Iterator进行迭代。 (虽然c/c++竞赛中常用的迭代器能加快运行速率,但是对于java来说也就那样) 一旦选择引用外部迭代,你就得考虑好并行的问题了。也就是那一堆并行化、synchronized的头疼问题。 ```java List<String> names = new ArrayList<>(); Iterator<String> it = menu.iteartor(); while(it.hasNext()) { Dish d = it.next(); names.add(d.getName()); } ``` --- ## 五、Stream流的基础使用 ### (1)筛选 #### `filter` 用法很简单,就是筛选出来符合条件的元素的流。 #### `distinct` 跟数据库用的一样,就是去重。 ### (2)切片 #### `limit` 截断流。 #### `skip` (在limit的结果后)返回扔掉的前n个元素的流,如果不够则返回空流。 ```java public static void limitAndSkip() { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7); List<Integer> result1 = list.stream() .filter(i -> i > 1) .limit(5) .collect(Collectors.toList()); // [2, 3, 4, 5, 6] System.out.println(result1); List<Integer> result2 = list.stream() .filter(i -> i > 1) .limit(5) .skip(2) .collect(Collectors.toList()); // [4, 5, 6] System.out.println(result2); } ``` ### (3)映射 #### `map` 类型转换,根据传入的方法引用或者对应的Lambda函数,返回对应的类型的流。 ```java List<Integer> dishNameLengths = menu.stream() // Stream<Dish> .map(Dish::getName) // Stream<String> .map(String::length) // Stream<Integer> .collect(Collectors.toList()); ``` #### `flatMap`流的扁平化 想要把List<String>类型的:`["hello", "world"]`,变成同类型的:`["h", "e", "l", "o", "w", "r", "d"]` 错误1: ```java public static void error1() { List<String> words = Arrays.asList("hello", "world"); List<String> resWord = words.stream() // split之后会把List<String>(Stream<String>)转成String[](Stream<String[]>),出现类型转换错误 .map(word -> word.split("")) .distinct() .collect(Collectors.toList()); } ``` 错误2: ```java public static void error2() { List<String> words = Arrays.asList("hello", "world"); List<String> resWord = words.stream() // Stream<String> .map(word -> word.split("")) // Stream<String[]> .map(Arrays::stream) // Stream<Stream<string>> .distinct() .collect(Collectors.toList()); } ``` 这时候就可以使用flatMap了,它的作用就是将 `Arrays::stream`映射成的**流的内容**,合并(或者说是连接)起来,即扁平化为一个流。 ```java public static void flatMap() { List<String> words = Arrays.asList("hello", "world"); List<String> resWord = words.stream() // Stream<String> .map(word -> word.split("")) // Stream<String[]> .flatMap(Arrays::stream) // Stream<String> .distinct() .collect(Collectors.toList()); } ``` ### (4)查找和匹配 #### `allMatch`、`anyMatch`、`noneMatch`、`findFirst`、`findAny` 它们返回的都是一个boolean,因此是一个终端操作。 并且它们具有java中的短路功能。 需要引入下文的Optional避免find操作中出现null的情况。 【BUG】 在测试的时候发现,`findAny()`每次都只返回匹配的第一个元素,也就是等同于 `findFirst()` ### (5)归约 用 `reduce()`操作来做更复杂的查询,如“计算总卡路里”、“拿到最高卡路里”等复杂操作,就称为**规约操作**或者**折叠(fold)**。 reduce的优势在于为后文的 `paralleStream()`并行化提供一种优雅的方案。 `T reduce(T identity, BinaryOperator<T> accumulator);` 其中 `identity`代表初始值,accumulator代表定义的Lambda函数操作。 #### 求和 ```java public static void sum() { int sum = 0; List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); for (int x : numbers) { sum += x; } System.out.println(sum); int streamSum = numbers.stream().reduce(0, Integer::sum); System.out.println(streamSum); } ``` ```java public static void multiply() { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int streamSum = numbers.stream().reduce(1, (a, b) -> a * b); System.out.println(streamSum); } ``` **计算总卡路里:** ```java public static void getSumCalories(List<Dish> menu) { int sumCalories = menu.stream() .map(Dish::getCalories) .reduce(0, Integer::sum); System.out.println(sumCalories); } ``` 注意: 此方法有一个暗含的装箱成本,它会先把每个Integer都拆成原始数据类型再求和,当数据量多的时候开销就会很大。 具体在后面的**数值流**中会提及。 **计算菜品数1:** ```java public static void getSumMenus1(List<Dish> menu) { int sumMenus = menu.stream() .map(dish -> 1) .reduce(0, Integer::sum); System.out.println(sumMenus); } ``` **计算菜品数2:** ```java public static void getSumMenus2(List<Dish> menu) { // int sumMenus = menu.size(); long sumMenus = menu.stream() .count(); System.out.println(sumMenus); } ``` #### 最大最小值 **最大卡路里:** ```java public static void getMaxCaloric(List<Dish> menu) { int maxCaloric = menu.stream() .map(Dish::getCalories) .reduce(0, Integer::max); System.out.println(maxCaloric); } ``` 注意: 这里有一个坑:在其他场景中,你无法保证0是最大值还是没有元素默认为0。 这个坑留给后面数值流来填。 **最大卡路里的菜品:** ```java public static void getMaxCaloricDish(List<Dish> menu) { Optional<Dish> dish = menu.stream() .reduce((dish1, dish2) -> dish1.getCalories() > dish2.getCalories() ? dish1 : dish2); System.out.println(dish); } ``` ### 操作总表  ### 流的基础实操 ```java @Data @AllArgsConstructor public class Trader { private String name; private String city; } ``` ```java @Data @AllArgsConstructor public class Transaction { private Trader trader; private int year; private int value; } ``` ```java public class Test { public static void main(String[] args) { // test1(); // test2(); // test3(); // test4(); // test5(); // test6(); // test7(); test8(); } public static List<Transaction> init() { Trader raoul = new Trader("Raoul", "Cambridge"); Trader mario = new Trader("Mario", "Milan"); Trader alan = new Trader("Alan", "Cambridge"); Trader brian = new Trader("Brian", "Cambridge"); return Arrays.asList( new Transaction(brian, 2011, 300), new Transaction(raoul, 2012, 1000), new Transaction(raoul, 2011, 400), new Transaction(mario, 2012, 710), new Transaction(mario, 2012, 700), new Transaction(alan, 2012, 950) ); } /** * 找出2011年发生的所有交易,并按交易额排序(从高到低) */ public static void test1() { List<Transaction> transactions = init(); List<Transaction> tr2011 = transactions.stream() .filter(transaction -> transaction.getYear() == 2011) .sorted(Comparator.comparing(Transaction::getValue).reversed()) .collect(Collectors.toList()); System.out.println(tr2011); } /** * 交易员都在哪些不同的城市工作过? */ public static void test2() { List<Transaction> transactions = init(); // 常规解法 List<String> cities = transactions.stream() .map(transaction -> transaction.getTrader().getCity()) .distinct() .collect(Collectors.toList()); System.out.println(cities); // 转为set Set<String> cities2 = transactions.stream() .map(transaction -> transaction.getTrader().getCity()) .collect(Collectors.toSet()); System.out.println(cities2); } /** * 查找所有来自于剑桥的交易员,并按姓名排序 */ public static void test3() { List<Transaction> transactions = init(); List<Trader> traders = transactions.stream() .map(Transaction::getTrader) .filter(trader -> "Cambridge".equals(trader.getCity())) .distinct() .sorted(Comparator.comparing(Trader::getName)) .collect(Collectors.toList()); System.out.println(traders); } /** * 返回所有交易员的姓名字符串,按字母顺序排序 */ public static void test4() { List<Transaction> transactions = init(); // 常规 String反复创建,且结尾有多出一个 "、" String traderStr = transactions.stream() .map(transaction -> transaction.getTrader().getName()) .distinct() .sorted() .reduce("", (n1, n2) -> n1 + n2 + "、"); System.out.println(traderStr); // 优化:joining内部用的是StringBuilder,且结尾没有多余的 "、" String traderStr2 = transactions.stream() .map(transaction -> transaction.getTrader().getName()) .distinct() .sorted() .collect(Collectors.joining("、")); System.out.println(traderStr2); } /** * 有没有交易员是在米兰工作的 */ public static void test5() { List<Transaction> transactions = init(); boolean isMilan = transactions.stream() .anyMatch(transaction -> "Milan".equals(transaction.getTrader().getCity())); System.out.println(isMilan); } /** * 打印生活在剑桥的交易员的所有交易额 */ public static void test6() { List<Transaction> transactions = init(); transactions.stream() .filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity())) .map(Transaction::getValue) .forEach(System.out::println); } /** * 所有交易中,最高的交易额是多少 */ public static void test7() { List<Transaction> transactions = init(); Optional<Integer> maxValue = transactions.stream() .map(Transaction::getValue) .reduce(Integer::max); System.out.println(maxValue); } /** * 找到交易额最小的交易 */ public static void test8() { List<Transaction> transactions = init(); Optional<Transaction> transaction = transactions.stream() .min(Comparator.comparing(Transaction::getValue)); System.out.println(transaction); } } ``` ### 数值流和构建流 (这块重点不多,但又有很多细节,实在是不好写。) 流有三种基本的原始类型特化: `IntStream` 、 `DoubleStream` 和 `LongStream` 。 上文说到的装箱问题,就可以用IntStream来解决。 **补坑1:卡路里总和** ```java public static void getSumCalories(List<Dish> menu) { int sumCalories = menu.stream() .map(Dish::getCalories) //Stream<Integer> .reduce(0, Integer::sum); System.out.println(sumCalories); int sumCalories2 = menu.stream() .mapToInt(Dish::getCalories) //IntStream // sum为IntStream中独有的 .sum(); System.out.println(sumCalories2); } ``` **补坑2:最大卡路里** ```java public static void getMaxCaloric(List<Dish> menu) { int maxCaloric = menu.stream() .map(Dish::getCalories) // Stream<integer> .reduce(0, Integer::max); System.out.println(maxCaloric); int maxCaloric2 = menu.stream() .mapToInt(Dish::getCalories) // IntStream .max() // OptionalInt // 如果是因为没有数据就返回 -1 .orElse(-1); System.out.println(maxCaloric2); } ``` 如果要把三种**特化/原始流**转回一般流,只需要: `intStream.boxed()` **实战:勾股数** ```java /** * 生成[1,100]以内的勾股数 */ public static void getPythagoreanTriples() { Stream<int[]> pythagoreanTriples = // IntStream生成[1-100]随机数 IntStream.rangeClosed(1, 100) .boxed() // 将多个Stream<int[]>连接起来,如果是map()则会变成Stream<Stream<int[]>> .flatMap(a -> IntStream.rangeClosed(a, 100) .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0) // 这里不用boxed()是因为mapToObj会将IntStream转成Stream<int[]> .mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}) ); pythagoreanTriples.forEach(t -> System.out.println(t[0] + "," + t[1] + "," + t[2])); } /** * 生成[1,100]以内的勾股数 优化版 */ public static void getPythagoreanTriples2() { Stream<double[]> pythagoreanTriples2 = IntStream.rangeClosed(1, 100) .boxed() .flatMap(a -> IntStream.rangeClosed(1, 100) .mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)}) .filter(t -> t[2] % 1 == 0) ); pythagoreanTriples2.forEach(t -> System.out.println((int)t[0] + "," + (int)t[1] + "," + (int)t[2])); } ``` --- ## 六/七、用流收集数据 和 并行数据处理与性能 这两章节算是对stream的高阶操作(例如自定义方法),平时开发中比较少用到,就只是留个印象先。 主要是讲了collect收集器的用法 和 Collectors.reducing()方法等汇总结果的各种收集方式。但是最小值最大值等之前用map就够了,更多的是用来做分级分组处理。 从IDEA的智能优化上来看,它不是很好用,还是推荐直接用最初的stream.map()/reduce()方法。 并行流只适用于处理的元素数据量特别大或者筛选的单个元素特别耗时,否则效率可能反而降低。 这章的重点在于对分支/合并框架的运用,通过递归的方式去拆分成更小的任务,在不同线程上执行,最终再合并。(相当于MapReduce方法了) 同时,Spliterator 定义了并行流如何拆分它要遍历的数据。 --- ## 八、重构、测试和调试 ### 为改善可读性和灵活性重构代码 Lamdba的出现本就是为了在不失可读性的情况下写出更简洁更灵活的代码。在上面的篇幅中我们也一直在做这几件事去提高**可读性**: (1)用Lambda表达式代替匿名类。 (2)用方法引用代替Lambda表达式。 (3)用Stream API代替命令式的数据处理。 而**灵活性**则体现在与可以有条件的延迟执行以及环绕执行方法。 ### 测试和调试 IDEA现在已经支持对Lambda的调试了,所以这块内容可以不用去了解。 --- ## 九、默认方法 主要讲述了两个很有意思的概念。 (1)默认方法的开头以关键字 default 修饰,方法体与常规的类方法相同。默认方法的出现能帮助库的设计者以后向兼容的方式演进API。 即: 发布了接口A后,B实现了接口A。现在A新增了一个方法,只要B不调用就不会出错(二进制级的兼容方式下,如果新添加的方法不被调用,接口已经实现的方法可以继续运行,不会出现错误)。但是只要B调用到了新增的方法就直接报错了。而且越是父级的接口A,如果有BCDEF实现它的话修改起来非常麻烦。 这时候可以**把新增的方法定义成默认方法并在A接口中实现它。**(一般来说,接口中是不能有实现方法的,只有抽象类中可以)这样BCDEF都会继承A新增方法的实现。如下: ```java piblic interface Sized { int size(); // 默认方法 default boolean isEmpty() { return size() == 0; } } ``` `Collection.stream()`、`List.sort()`等接口用的也是默认方法。 (2)解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突: 1.类或者父类中声明的方法的优先级高于任何默认方法。 2.如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。 3.两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。 --- ## 十、用 Optional 取代 null 无聊的六七八九过渡之后,迎来了本书第二个重点:**`Optional`** 取代的好处很明显。当你引用一个null的时候,必定会引发NPE异常。但是如果是Optional.empty()就没事,因为它是该类的一个有效对象。而null本身就没有任何语义,它仅仅只是代表在静态类型语言中以一种错误的方式对缺失变量值的建模。 Optional就是为缺失的值(null)建模。  用法上,map、flatMap、filter等跟之前学到的Stream方法大致相同。 记住上面这几个常用的就够了,Optional是重点但不是难点,用法都是很简单的。 --- ## 十一、CompletableFuture:组合式 主要是学习了非阻塞式调用的异步API。 **Demo1:简单样例** ```java package com.example.demo.future; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; public class Shop { public static void main(String[] args) { Shop shop = new Shop(); long start = System.nanoTime(); // 查询商店,试图获取价格(实际在计算中) Future<Double> futurePrice = shop.getPriceAsync("my favorite product"); long invocationTime = ((System.nanoTime() - start) / 1_000_000); System.out.println("Invocation returned after " + invocationTime + " msecs"); // 在计算价格的同时,执行更多任务 doSomethingElse(); try { // 从 Future 对象中读取价格,如果价格未知,会发生阻塞 double price = futurePrice.get(); System.out.printf("Price is %.2f%n", price); } catch (Exception e) { throw new RuntimeException(e); } long retrievalTime = ((System.nanoTime() - start) / 1_000_000); System.out.println("Price returned after " + retrievalTime + " msecs"); } private static void doSomethingElse() { System.out.println("Do something else during getPriceAsync() method"); } /** * 得到价格 * 阻塞式的同步API调用方法 * * @param product 产品 * @return double */ public double getPrice(String product) { return calculatePrice(product); } /** * 非阻塞式的异步API调用方法 * 创建一个代表异步计算的CompletableFuture对象实例,它会在计算完成的时候包含计算计算的结果 * 开辟另一个线程去执行实际上的计算操作,但是该方法不会等到计算结束,而是直接返回一个实例 * 当请求的产品价格最终计算得出时,可以使用它的 complete 方法,结束 completableFuture 对象的运行,并设置变量的值。 * * @param product 产品 * @return {@link Future<Double>} */ public Future<Double> getPriceAsync(String product) { // 创建CompletableFuture对象,它会包含计算的结果 CompletableFuture<Double> futurePrice = new CompletableFuture<>(); // 在另一个线程中以异步方式执行计算 new Thread( () -> { // 如果操作正常结束,则完成Future操作并设置商品价格 try { double price = calculatePrice(product); // 需长时间计算的任务结束并得出结果时,设置Future 的返回值 futurePrice.complete(price); // 否则抛出失败异常,完成这次的Future操作 } catch (Exception ex) { futurePrice.completeExceptionally(ex); } }).start(); // 无需等待还没结束的计算,直接返回 Future 对象 return futurePrice; } /** * 计算价格 * * @param product 产品 * @return double */ private double calculatePrice(String product) { delay(); Random random = new Random(); return random.nextDouble() * product.charAt(0) + product.charAt(1); } /** * 延迟 */ public static void delay() { try { Thread.sleep(2000L); } catch (InterruptedException e) { throw new RuntimeException(e); } } } ``` ```console Invocation returned after 39 msecs Do something else during getPriceAsync() method Price is 162.29 Price returned after 2048 msecs ``` **Demo2:与Stream结合的批处理** ```java public static List<String> findPrices(String product) { List<Shop> shops = Arrays.asList(new Shop("BestPrice"), new Shop("LetsSaveBig"), new Shop("MyFavoriteShop"), new Shop("BuyItAll")); List<CompletableFuture<String>> priceFutures = shops.stream() // 使用 CompletableFuture 以异步方式计算每种商品的价格 .map(shop -> CompletableFuture.supplyAsync( () -> shop.getName() + " price is " + shop.getPrice(product))) .collect(Collectors.toList()); return priceFutures.stream() // 等待所有异步操作结束 .map(CompletableFuture::join) .collect(Collectors.toList()); } ``` 再往上优化差不多就是合理的制定线程池的大小,创建专有的执行器。 **Demo3:实际项目中的使用** ```java /** * @Description: 【异步】查询账户画像列表 v1.0.0 **/ public IPage<AccountActionVo> findDataGridAsynchronously(AccountActionParam search) { ... Future<Optional<Map<String, String>>> muteUuidMap = null; // 异步查找 muteUuidMap = userInfoService.queryMuteStatusAsync(accountUuidList); // 异步执行的其它操作 ... Future<Optional<Map<String, String>>> finalMuteUuidMap = muteUuidMap; //填充禁言类型,账户风险值,账户昵称,是否是hook StreamSupport.stream(list.spliterator(), false).forEach(item -> { try { String typeValue = null; typeValue = Optional.ofNullable(finalMuteUuidMap.get().orElse(new HashMap<>()).get(item.getAccountUuid())).orElse(MuteTypeEnum.NONE.value()); item.setMuteType(String.valueOf(MuteTypeEnum.getEnumByValue(typeValue))); } catch (InterruptedException | ExecutionException e) { ... } }); return all; } ``` ```java /** * 查询禁言状态 * * @return {@link Future<Optional<Map<String, String>>>} */ @Async("customPoolExecutor") public Future<Optional<Map<String, String>>> queryMuteStatusAsync(Collection<String> accountUuidList) { return forumRpcCall.queryMuteStatusAsync(accountUuidList); } ``` ```java /** * @return: java.util.concurrent.Future<java.util.Optional < java.util.Map < java.lang.String, java.lang.String>>> **/ @Async("customPoolExecutor") public Future<Optional<Map<String, String>>> queryMuteStatusAsync(Collection<String> accountUuidList) { if (CollectionUtils.isEmpty(accountUuidList)) { return new AsyncResult(Optional.of(new HashMap<>())); } // SOA调用获取data ... return new AsyncResult<Optional<Map<String, String>>>(Optional.ofNullable(JSON.parseObject(data, new com.alibaba.fastjson.TypeReference<Map<String, String>>() { }))); } ``` ```java /** * 自定义线程池 - 账户画像异步查询使用 */ @Configuration public class CustomThreadPoolExecutor { private static final Log LOG = LogFactory.getLog(CustomThreadPoolExecutor.class); // 核心线程数 private int corePoolSize; // 队列大小 private int workQueueSize; // 最大线程数 private int maxPoolSize; // 存活时间 private long keepAliveTime; @Bean(name = "customPoolExecutor") public ThreadPoolExecutor init() { workQueueSize = 8; corePoolSize = 4 ; maxPoolSize = 4; keepAliveTime = 3000; BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(workQueueSize); ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("customPoolExecutor-Thread"); return t; } }, new ThreadPoolExecutor.CallerRunsPolicy()); LOG.info("customPoolExecutor Info : " + " corePoolSize = " + corePoolSize + " | maxPoolSize = " + maxPoolSize + " | workQueueSize = " + workQueueSize); return executor; } } ``` 在实际项目中也只是引用最简单的单异步执行,甚至没有规范的引用As油腻吃Result的异常处理,而是抛到了上游业务中。 CompletableFuture本身还有一些多异步处理流水线操作、响应式编程等等对现在来说有点吃力。放到以后有需求再看。 另,近期抽空看看这篇文章[@Async、CompletableFuture、异步非阻塞、响应式编程](https://blog.csdn.net/u014203449/article/details/88830287) --- ## 十二、新的日期和时间API 这块在[Java面试宝典Beta5.0](http://www.tangsong.fun/index.php/JavaTest2-1.html)的第4.3中就有提及大部分知识。 时期和时间本就是当做工具类来使用的,这边只需要了解到大致有哪些去求,具体用到的时候再去查就够了。 --- ## 十三、Scala 本书在文末介绍了一款类Java的语言 `Scala`,它是一种混合了面向对象和函数式编程的语言。 它更像是C/C++与JavaScript的结合体,下面例子展示了它与Java的差异: ```Java public class Foo { public static void main(String[] args) { IntStream.rangeClosed(2, 6) .forEach(n -> System.out.println("Hello " + n + " bottles of beer")); } } ``` ```Scala object Test { def main(args: Array[String]){ 2 to 6 foreach { n => println(s"Hello ${n} bottles of beer") } } } ``` ### 一、基本数据结构 (1)**Map** `Map("Raoul" -> 23, "Mario" -> 40)` (2)**List** `List("Raoul", "Mario")` (3)**Set** ```Scala val numbers = Set(23, 40) val newNumbers = numbers + 8 ``` (4)**Tuple** (5)**元组** ```Scala val book = (2014, "Java 8", "Mainning") println(book._1) //下标从1开始,输出:2014 ``` (6)**Stream** 相比Java,能过回溯之前的元素,支持索引访问。 (7)**Option** ```java public String getCarInsuranceName(Optional<Person> person, int minAge) { return person.filter(p -> p.getAge() >= minAge) .flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); } ``` ```Scala def getCarInsuranceName(person: Option[Person], minAge: Int) = person.filter(_.getAge() >= minAge) .flatMap(_.getCar) .flatMap(_.getInsurance) .map(_.getName).getOrElse("Unknown") ``` 注意:在前面的代码中,你使用的是 _.getCar (并未使用圆括号),而不是 _.getCar() (带 圆括号)。 **Scala的集合在默认情况下是只读的,无法修改。** ### 二、函数、类、trait Scala为 `函数`提供了更加丰富的特性,这方面比Java 8做得好,Scala支持:函数类型、可以不受限制地访问本地变量的闭包,以及内置的科里化表单。 Scala中的 `类`可以提供隐式的构造器、getter方法以及setter方法。 Scala还支持 `trait`,它是一种同时包含了字段和默认方法的接口。可多继承。 Last modification:August 22, 2022 © Allow specification reprint Like 0 喵ฅฅ