初步认识Java8
需求: 给定一个字符串列表:
1["1","2","bilibili","of","xiaoming","5","at","BILIBILI","xiaoming","23","CHEERS","6"]
找出所有长度>=5的字符串,并且忽略大小写、去除重复字符串,然后按字母排序,最后用“❤”连接成一个字符串输出!
思路: 1、首先判断输入字符是字母还是数字 2、遍历字符串存入Set集合去重,同时进行大小写转换、长度判断 3、遍历用“❤”拼接结果字符串
方法:使用Java8的Stream流式操作:
1public class Test1 { 2 public static void main(String[] args) { 3 List<String> list = new ArrayList<String>(); 4 list.add("bilibili"); 5 list.add("of"); 6 list.add("xiaoming"); 7 list.add("5"); 8 list.add("at"); 9 list.add("BILIBILI"); 10 list.add("23"); 11 list.add("CHEERS"); 12 13 String result = list.stream() 14 .filter(i -> !isNum(i)) 15 .filter(i -> i.length() >= 5) 16 .map(i -> i.toLowerCase()) 17 .distinct() 18 .sorted(Comparator.naturalOrder()) 19 .collect(Collectors.joining("❤")); 20 System.out.println(result); 21 } 22 23 private static boolean isNum(String str) { 24 for (int i = 0; i < str.length(); i++) { 25 if (!Character.isDigit(str.charAt(i))){ 26 return false; 27 } 28 } 29 return true; 30 } 31}
阅读书籍《Java8函数式编程》
1. Lambda表达式
1.1 辨别Lambda表达式
1public class Test01 { 2 public static void main(String[] args) { 3 Button button = new Button(); 4 // 给Button注册一个事件监听器 5 button.addActionListener(new ActionListener() { 6 @Override 7 public void actionPerformed(ActionEvent e) { 8 System.out.println("button clicked"); 9 } 10 }); 11 12 // 1、实现了actionPerformed方法 13 button.addActionListener(e -> System.out.println("button clicked!")); 14 15 // 2、无参,实现了Runnable接口,重写了内部run()方法 16 Runnable noArguments = () -> System.out.println("Hello World"); 17 18 // 3、一个参数,和1一样 19 ActionListener oneArguments = e -> System.out.println("button clicked"); 20 21 // 4、Lambda表达式的主体还可以是一段代码块 22 Runnable multiStatement = () -> { 23 System.out.println("Hello"); 24 System.out.println("World"); 25 }; 26 27 // 5、变量 add 的类型是 BinaryOperator<Long>, 它不是两个数字的和,而是将两个数字相加的那行代码。 28 BinaryOperator<Long> add = (x, y) -> x + y; 29 30 // 6、上述所有Lambda表达式中的参数类型都是依赖于上下文环境由编译器推断得出的,也可以使用()显式声明参数类型 31 BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; 32 } 33}
1.2 引用值,而不是变量
Lambda表达式中引用的局部变量必须是final或既成事实上的final变量,否则编译器会报错,其中既成事实上的final是指只能给该变量赋值一次。
1// 正确引用: 2String name = getUserName(); 3button.addActionListener(e -> {System.out.println("hi " + name)}); 4 5// 试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错 6// 并显示出错信息: local variables referenced from a Lambda expression must be final or effectively final 7String name = getUserName(); 8name = formatUserName(name); 9button.addActionListener(event -> System.out.println("hi " + name));
1.3 函数接口
定义:函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。
例如:
1public interface ActionListener extends EventListener { 2 /** 3 * Invoked when an action occurs. 4 */ 5 // 由于actionPerformed定义在一个接口里,因此 abstract 关键字不是必需的 6 public void actionPerformed(ActionEvent e); 7}
Java中重要的函数接口:
| 接口 | 参数 | 返回类型 | 示例 |
|---|---|---|---|
| Predicate<T> | T | boolean | 这张唱片是否以及发行 |
| Consumer<T> | T | void | 输出一个值 |
| Function<T,R> | T | R | 获得Artist对象的名字 |
| Supplier<T> | None | T | 工厂方法 |
| UnaryOperator<T> | T | T | 逻辑非(!) |
| BinaryOperator<T> | (T,T) | T | 求两个数的乘积(*) |
1.4 类型推断
Lambda表达式中的类型推断,实际上是Java 7中就引入的目标类型推断的扩展。
Java 7中的菱形操作符:
1Map<String, Integer> map1 = new HashMap<String, Integer>(); 2Map<String, Integer> map2 = new HashMap<>();
Java 8中的类型推断:
1Predicate<Integer> atLeast5 = x -> x > 5; 2 3// 源码: 4public interface Predicate<T> { 5 boolean test(T t); 6} 7 8BinaryOperator<Long> addLongs = (x,y) -> x + y; 9 10BinaryOperator add = (x, y) -> x + y; // 编译错误,无法判定类型
2. Stream流
2.1 从外部迭代到内部迭代
例子:使用for循环计算来自伦敦的艺术家人数
1int count = 0; 2for(Artist artist : allArtists){ 3 if(artist.isFrom("London")){ 4 count++; 5 } 6}
for循环本质上是一个封装了迭代的语法糖,其工作原理如下:首先调用iterator方法,产生一个新的Iterator对象,进而控制整个迭代过程,即外部迭代。迭代过程通过显式调用 Iterator 对象的 hasNext 和 next方法完成迭代。
1int count = 0; 2Iterator<Artist> iterator = allArtists.iterator(); 3while(iterator.hasNext()){ 4 Artist artist = iterator.next(); 5 if(artist.isFrom("London")){ 6 count++; 7 } 8}

内部迭代: 首先调用stream()方法,返回内部迭代中的相应接口:Stream
1long count = allArtists.stream() 2 .filter(artist -> artist.isFrom("London")) 3 .count();

::: warning Stream是用函数式编程方式在集合类上进行复杂操作的工具。 :::
2.2 实现机制
惰性求值:返回值是Stream 及早求值:返回值是另一个值或空
1// 该行码中并未做什么实际性的工作, filter只刻画出了Stream,但没有产生新的集合 2allArtists.stream() 3 .filter(artist -> artist.isFrom("London"));
2.3 常用的流操作
| 流操作 | 解释 |
|---|---|
| collect(toList()) | 由 Stream 里的值生成一个列表, 是一个及早求值操作。 |
| map | 将一个流中的值转换成一个新的流 |
| filter | 遍历数据并检查其中的元素时使用 |
| flatMap | 用Stream替换值,然后将多个Stream连接成一个Stream |
| max & min | 求流中的最大值和最小值 |
| 通用模式 | |
| reduce | 可以实现从一组值中生成一个值 |
| 整合操作 | 结合多个流操作 |
1public class Test01 { 2 public static void main(String[] args) { 3 // of:将一组初始值生成新的Stream 4 // collect:将Stream中的值生成一个列表 5 List<String> list1 = Stream.of("a", "b", "c").collect(toList()); 6 Assert.assertEquals(Arrays.asList("a", "b", "c"), list1); 7 8 // map:将字符转换为大写形式 9 List<String> list2 = Stream.of("a", "b", "helloWorld") 10 .map(str -> str.toUpperCase()) 11 .collect(toList()); 12 Assert.assertEquals(Arrays.asList("A","B","HELLOWORLD"), list2); 13 14 // filter:找出以数字开头的字符串 15 List<String> list3 = Stream.of("1abc", "abc") 16 .filter(str -> Character.isDigit(str.charAt(0))) 17 .collect(toList()); 18 Assert.assertEquals(Arrays.asList("1abc"), list3); 19 20 // flatMap:将多个Stream流连接成一个Stream流 21 List<Integer> list4 = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4)) 22 .flatMap(numbers -> numbers.stream()) 23 .collect(toList()); 24 Assert.assertEquals(Arrays.asList(1,2,3,4), list4); 25 26 // min:找出最短的字符串 27 String res1 = Stream.of("a", "ab", "abc") 28 .min(Comparator.comparing(str -> str.length())) 29 .get(); 30 Assert.assertEquals("a" , res1); 31 32 // max:找出最长的字符串 33 String res2 = Stream.of("a", "ab", "abc") 34 .max(Comparator.comparing(str -> str.length())) 35 .get(); 36 Assert.assertEquals("abc" , res2); 37 38 // reduce:实现累加求和 39 // 0 :初始值 40 // acc :累加器 41 // element:当前元素 42 int count1 = Stream.of(1,2,3) 43 .reduce(0, (acc, element) -> acc + element); 44 Assert.assertEquals(6, count1); 45 46 // 展开reduce操作 47 BinaryOperator<Integer> accumulator = (acc, element) -> acc + element; 48 int count2 = accumulator.apply(accumulator.apply(accumulator.apply(0, 1), 2), 3); 49 Assert.assertEquals(6, count2); 50 51 } 52 53 // 计算字符串中小写字母的个数 54 public static int countLowerCaseLetters(String string){ 55 return (int) string.chars() 56 .filter(Character::isLowerCase) 57 .count(); 58 } 59 60 // 在一个字符串列表中,找出包含最多小写字母的字符串。对于空列表,返回Optional<String>对象。 61 public static Optional<String> mostLowerCaseString(List<String> strings){ 62 return strings.stream() 63 .max(Comparator.comparingInt(Test01::countLowerCaseLetters)); 64 } 65}
2.4 链式调用
例:找出专辑上所有演出乐队的国籍
1Set<String> origins = album.getMusicians() 2 .filter(artist -> artist.getName().startsWith("The")) 3 .map(artist -> artist.getNationality()) 4 .collect(toSet());
2.5 高阶函数
定义:如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。 例如:map 是一个高阶函数, 因为它的 mapper 参数是一个函数。
2.6 总结
- 内部迭代将更多控制权交给了集合类。
- 和 Iterator 类似, Stream 是一种内部迭代方式。
- 将 Lambda 表达式和 Stream 上的方法结合起来, 可以完成很多常见的集合操作。
3. 类库
...
4. 高级集合类和收集器
4.1 方法引用
Lambda表达式经常调用参数,例如:artist -> artist.getName() Java 8为其提供了一个简写语法,叫作方法引用,例如:Artist::getName 标准语法为 Classname::methodName 凡是使用 Lambda 表达式的地方, 就可以使用方法引用
4.2 元素顺序
直观上看, 流是有序的, 因为流中的元素都是按顺序处理的。 这种顺序称为出现顺序。
1List<Integer> numbers = asList(1, 2, 3, 4); 2List<Integer> sameOrder = numbers.stream() 3 .collect(toList()); 4Assert.assertEquals(numbers, sameOrder);
如果集合本身就是无序的, 由此生成的流也是无序的。 HashSet 就是一种无序的集合,下面程序不一定每次都通过
1Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1)); 2List<Integer> sameOrder = numbers.stream() 3 .collect(toList()); 4// 该断言有时会失败 5Assert.assertEquals(asList(4, 3, 2, 1), sameOrder);
一些中间操作会产生顺序, 比如对值做映射时, 映射后的值是有序的
1Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1)); 2List<Integer> sameOrder = numbers.stream() 3 .sorted() 4 .collect(toList()); 5assertEquals(asList(1, 2, 3, 4), sameOrder);
一些操作在有序的流上开销更大, 调用 unordered 方法消除这种顺序就能解决该问题。 大多数操作都是在有序流上效率更高, 比如 filter、 map 和 reduce 等。
4.3 收集器
定义:一种通用的、 从流生成复杂值的结构。 只要将它传给 collect 方法, 所有的流就都可以使用它了。
4.3.1 转换成其他集合
使用toCollection,用定制的集合收集元素
1stream.collect(toCollection(TreeSet::new));
4.3.2 转换成值
maxBy 和 minBy 允许用户按某种特定的顺序生成一个值。
1// 找出成员最多的乐队 2public Optional<Artist> biggestGroup(Stream<Artist> artists){ 3 Function<Artists, Long> getCount = artist -> artist.getMembers().count(); 4 return artists.collect(maxBy(comparing(getCount))); 5}
有些收集器实现了一些常用的数值运算
1// 找出一组专辑上曲目的平均值 2public double averageNumberOfTracks(List<Alum> albums){ 3 return albums.stream() 4 .collect(averagingInt(album -> album.getTrackList().size())); 5}
4.3.3 数据分块
收集器 partitioningBy, 它接受一个流, 并将其分成两部分。

1// 假设有一个艺术家组成的流, 你可能希望将其分成两个部分, 2// 一部分是独唱歌手, 另一部分是由多人组成的乐队。 3public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists){ 4 rerurn artists.collect(partitioningBy(artist -> artist.isSolo())); 5 6 // 方法引用 7 // rerurn artists.collect(partitioningBy(Artist::isSolo)); 8}
4.3.4 数据分组
groupingBy 收集器,接受一个分类函数,用来对数据分组,就像 partitioningBy一样,接受一个Predicate 对象将数据分成 ture 和 false 两部分。

1// 现在有一个由专辑组成的流, 可以按专辑当中的主唱对专辑分组 2public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums){ 3 return albums.collect(groupingBy(album -> album.getMainMusician())); 4}
4.3.5 字符串
Collectors.joining 收集流中的值,该方法可以方便地从一个流得到一个字符串,允许用户提供分隔符( 用以分隔元素)、前缀和后缀。
1// 格式化艺术家姓名 2String result = artists.stream() 3 .map(Artist::getName) 4 .collect(Collectors.joining(",","[","]"));
4.3.6 组合收集器
groupingBy + countin收集器
1// 计算每个艺术家的专辑数量: 2// 1、groupingBy先将元素分组,每块都与分类函数 getMainMusician 提供的键值相关联 3// 2、然后使用下游的另一个收集器收集每块中的元素 4// 3、最后将结果映射为一个 Map。 5public Map<Artist, Long> numberOfAlbums(Stream<Album> albums){ 6 return albums.collect(groupingBy(album -> album.getMainMusician(), counting())); 7}
groupingBy + mapping收集器
1// 计算每个艺术家的专辑名 2public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums){ 3 return albums.collect(groupingBy(Album::getMainMusician, 4 mapping(Album::getName, toList()))); 5}
4.3.7 重构和定制收集器
...
4.4 其他细节
Lambda 表达式的引入也推动了一些新方法被加入集合类,例如Map:
构建 Map 时, 为给定值计算键值是常用的操作之一,一个经典的例子就是实现一个缓存。 传统的处理方式是先试着从 Map 中取值, 如果没有取到, 创建一个新值并返回。
假设使用 Map<String, Artist> artistCache 定义缓存, 我们需要使用费时的数据库操作查 询艺术家信息:
1// 使用显式判断空值的方式缓存 2public Artist getArtist(String name){ 3 Artist artist = artistCache.get(name); 4 if(artist == null){ 5 artist = readArtistFromDB(name); 6 artistCache.put(name, artist); 7 } 8 return artist; 9} 10 11// Java 8新方法:computIfAbsent 12// 该方法接受一个 Lambda 表达式, 值不存在时使用该 Lambda 表达式计算新值。 13public Artist getArtist(String name){ 14 return artistCache.computeIfAbsent(name, this::readArtistFromDB(name)); 15}
迭代Map
1// 普通迭代遍历Map,代码冗余 2Map<Artist, Integer> countOfAlbums = new HashMap<>(); 3for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()){ 4 Artist artist = entry.getKey(); 5 List<Album> albums = entry.getValue(); 6 countOfAlbums.put(artist, albums.size()); 7} 8 9// Java 8内部迭代遍历Map 10Map<Artist, Integer> countOfAlbums = new HashMap<>(); 11albumsByArtist.forEach((artist, albums) -> { 12 countOfAlbums .put(artist, albums.size()); 13});
5. 数据并行化
5.1 并行和并发
并行:两(多)个任务在同一时间发生
并发:两(多)个任务共享时间段

5.2 并行化流操作
- 如果已经有一个Stream对 象,调用它的parallel方法就能让其拥有并行操作的能力。
- 如果想从一个集合类创建一个流,调用parallelStream就能立即获得一个拥有并行能力的流。
1// 串行化计算专辑曲目长度 2public int serialArraySum(){ 3 return albums.stream() 4 .flatMap(Album::getTracks) 5 .mapToInt(Track::getLength) 6 .sum(); 7 8// 并行化计算专辑曲目长度 9public int serialArraySum(){ 10 return albums.parallelStream() 11 .flatMap(Album::getTracks) 12 .mapToInt(Track::getLength) 13 .sum(); 14}
5.3 限制
为了发挥并行流框架的优势, 写代码时必须遵守一些规则和限制。
限制一: 调用 reduce 方法,初始值可以为任意值,为了让其在并行化时能工作正常,初值必须为组合函数的恒等值。 reduce 操作求和,组合函数为(acc, element) -> acc + element,则其初值必须为 0,因为任何数字加 0,值不变。 reduce 操作求积,组合函数为(acc, element) -> acc * element,则其初值必须为 1,因为任何数字乘 1,值不变。
限制二: reduce 操作的另一个限制是组合操作必须符合结合律。 这意味着只要序列的值不变, 组合操作的顺序不重要。
与 parallel 对应的是 sequential
5.4 性能
使用串行流还是并行化,取决于以下5个主要因素:
- 数据大小:分解数据并行处理后合并会带来额外开销
- 源数据结构:
- 装箱:处理基本类型比处理装箱类型要快
- 核的数量:指运行时机器能使用多少核
- 单元处理开销:花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显
1// 并行求和 2private int addIntegers(List<Integer> values) { 3 return values.parallelStream() 4 .mapToInt(i -> i) 5 .sum(); 6}
在底层,并行流还是沿用了fork/join框架。 fork递归式地分解问题,然后每段并行执行,最终由 join 合并结果,返回最后的值。
根据问题的分解方式, 初始的数据源的特性变得尤其重要, 它影响了分解的性能。
根据性能的好坏, 将核心类库提供的通用数据结构分成以下 3 组:
- 性能好
- 类似ArrayList、 数组或 IntStream.range数据结构支持随机读取,能轻而易举地被任意分解。
- 性能一般
- HashSet、TreeSet,这些数据结构不易公平地被分解
- 性能差
- LinkedList、Streams.iterate 和 BufferedReader.lines难于分解
在讨论流中单独操作每一块的种类时, 可以分成两种不同的操作: 无状态的和有状态的。
- 无状态操作整个过程中不必维护状态,
- map、filter 和 flatMap
- 有状态操作则有维护状态所需的开销和限制。
- sorted、 distinct 和 limit
5.5 并行化数组操作
|方法名|操作| |-|-|-| |parallelPrefix|任意给定一个函数, 计算数组的和| |parallelSetAll|使用 Lambda 表达式更新数组元素| |parallelSort|并行化对数组元素排序|
parallelPrefix操作擅长对时间序列数据做累加,它会更新一个数组,将每一个元素替换为当前元素和其前驱元素的和,这里的“ 和” 是一个宽泛的概念,它不必是加法,可以是任意一个 BinaryOperator
1// 计算简单滑动平均数(n为滑动窗口的大小) 2public static double[] simpleMovingAverage(double[] values, int n){ 3 // 并行操作会改变原有数组内容,为不修改原有数据,复制一份 4 double[] sums= Arrays.copyOf(values, values.length); 5 Arrays.parallelPrefix(sums, Double::sum); 6 int start = n - 1; 7 return IntStream.range(start, sums.length) 8 .mapToDouble(i -> { 9 double prefix = i == start ? 0 : sums[i-n]; 10 return (sums[i] - prefix) / n; 11 }) 12 .toArray(); 13}
1// 使用并行化数组操作初始化数组(改变了传入的数组,没有创建一个新的数组) 2public static double[] parallelInitialize(int size){ 3 double[] values = new double[size]; 4 Arrays.parallelSetAll(values, i -> i); 5 return values; 6}
1Double[] values = new Double[]{3.0,1.0,2.0}; 2Arrays.parallelSort(values, ((o1, o2) -> (int) (o2 - o1)));
6. 测试、调式和重构
6.1 Lambda表达式的单元测试
通常,在编写单元测试时,怎么在应用中调用该方法,就怎么在测试中调用。给定一些输入或测试替身,调用这些方法,然后验证结果是否和预期的行为一致。
局限性:因为Lambda 表达式没有名字,无法直接在测试代码中调用。 解决: 1、将Lambda表达式放入一个方法测试,这种方式要测那个方法,而不是Lambda表达式本身
1// 将字符串转换为大写形式 2public static List<String> allToUpperCase(List<String> words){ 3 return words.stream() 4 .map(string -> string.toUpperCase()) 5 .collect(Collectors.<String>toList()); 6} 7// 测试大写转换 8@Test 9public void multiWordsToUppercase(){ 10 List<String> input = Arrays.toList("a","b","hello"); 11 List<String> result = Testing.allToUpperCase(input); 12 Assert.assertEquals(asList("A", "B","HELLO"), result); 13}
1// 将列表中元素的第一个字母转换成大写 2public static List<String> elementFirstToUpperCaseLambdas(List<String> words){ 3 return words.stream() 4 .map(word -> { 5 char firstChar = Character.toUpperCase(word.charAt(0)); 6 return firstChar + word.substring(1); 7 }) 8 .collect(Collector.<String>toList()); 9} 10 11// 测试,这样测试必须创建一个列表,将所有可能的边界情况考虑到,太繁琐了! 12@Test 13public void twoLetterStringConvertedToUppercaseLambdas() { 14 List<String> input = Arrays.asList("ab"); 15 List<String> result = Testing.elementFirstToUpperCaseLambdas(input); 16 assertEquals(asList("Ab"), result); 17} 18 19// 解决:将Lambda表达式改写成普通方法,在流操作中使用引用 20public static List<String> elementFirstToUppercase(List<String> words) { 21 return words.stream() 22 .map(Testing::firstToUppercase) 23 .collect(Collectors.<String>toList()); 24} 25public static String firstToUppercase(String value) { 26 char firstChar = Character.toUpperCase(value.charAt(0)); 27 return firstChar + value.substring(1); 28} 29 30// 测试单独的方法 31@Test 32public void twoLetterStringConvertedToUppercase() { 33 String input = "ab"; 34 String result = Testing.firstToUppercase(input); 35 Assert.assertEquals("Ab", result); 36}
6.2 在测试替身时使用Lambda表达式
测试代码时, 使用 Lambda 表达式的最简单方式是实现轻量级的测试存根。 对于countFeature方法的期望行为是为传入的专辑返回某个数值。这里传入 4 张专辑, 测试存根中为每张专辑返回 2,然后断言该方法返回 8,即 2× 4。如果要向代码传入一个Lambda 表达式,最好确保 Lambda 表达式也通过测试。
1// 使用 Lambda 表达式编写测试替身, 传给 countFeature 方法 2@Test 3public void canCountFeatures() { 4 OrderDomain order = new OrderDomain(asList( 5 newAlbum("Exile on Main St."), 6 newAlbum("Beggars Banquet"), 7 newAlbum("Aftermath"), 8 newAlbum("Let it Bleed"))); 9 Assert.assertEquals(8, order.countFeature(album -> 2)); 10}
多数的测试替身都很复杂,使用Mockito这样的框架有助于更容易地产生测试替身。 让我们考虑一种简单情形,为List生成测试替身。我们不想返回List本上的长度,而是返回另一个 List 的长度,为了模拟 List 的 size 方法 我们不想只给出答案, 还想做一些操作, 因此传入一个 Lambda 表达式:
1// 结合 Mockito 框架使用 Lambda 表达式 2List<String> list = mock(List.class); 3when(list.size()).thenAnswer(inv -> otherList.size()); 4Assert.assertEquals(3, list.size());
6.3 日志和打印消息
以“找出专辑上每位艺术家来自哪个国家”为例
for循环打印中间值
1Set<String> nationalities = new HashSet<>(); 2for(Artist artist : album.getMusicianList()){ 3 if(artist.getName().startWith("The")){ 4 String nationality = artist.getNationality(); 5 System.out.println("Found nationality :" + nationality); 6 nationalities.add(nationality); 7 } 8}
可以使用 forEach 方法打印出流中的值,这同时会触发求值过程。但是这样的操作有个缺点:我们无法再继续操作流了,流只能使用一次。如果我们还想继续,必须重新创建流。
1album.getMusicians() 2 .filter(artist -> artist.getName().startsWith("The")) 3 .map(artist -> artist.getNationality()) 4 .forEach(nationality -> System.out.println("Found: " + nationality)); 5 6Set<String> nationalities 7 album.getMusicians() 8 .filter(artist -> artist.getName().startsWith("The")) 9 .map(artist -> artist.getNationality()) 10 .collect(Collectors.<String>toSet());
解决办法:peek能查看每个值,同时能继续操作流
1// 使用 peek 方法记录中间值 2Set<String> nationalities 3 album.getMusicians() 4 .filter(artist -> artist.getName().startsWith("The")) 5 .map(artist -> artist.getNationality()) 6 .peek(nation -> System.out.println("Found nationality: " + nation)) 7 .collect(Collectors.<String>toSet());
使用 peek 方法还能以同样的方式,将输出定向到现有的日志系统中,比如 log4j、java.util.logging 或者 slf4j。
7. 设计和架构的原则
7.1 Lambda表达式改变了设计模式
7.2 使用Lambda表达式的SOLID原则
SOLID原则:
- Single responsibility
- Open/closed
- Liskov substitution
- Interface segregation
- Dependency inversion
7.2.1 单一功能原则
程序中的类或方法只能有一个改变的理由。
1// 计算质数个数, 一个方法里塞进了多重职责 2public long countPrimes(int upTo){ 3 long tally = 0; 4 for(int i = 1; i < upTo; i++){ 5 boolean isPrime = true; 6 for(int j = 2; j < i; j++){ 7 if(i % j == 0){ 8 isPrime == false; 9 } 10 } 11 if(isPrime){ 12 tally++; 13 } 14 } 15 return tally; 16}
1// 将 isPrime 重构成另外一个方法后, 计算质数个数的方法 2public long countPrimes(int upTo) { 3 long tally = 0; 4 for (int i = 1; i < upTo; i++) { 5 if (isPrime(i)) { 6 tally++; 7 } 8 } 9 return tally; 10} 11 12private boolean isPrime(int number) { 13 for (int i = 2; i < number; i++) { 14 if (number % i == 0) { 15 return false; 16 } 17 } 18return true; 19}
1// 使用 Java 8 的集合流重构上述代码 2public long countPrimes(int upTo){ 3 return IntStream.range(1, upTo) 4 .filter(this::isPrime) 5 .count(); 6} 7 8public boolean isPrime(int number){ 9 return IntStream.range(2, number) 10 .allMatch(x -> (number % x) != 0); 11}
1// 并行流处理 2public long countPrimes(int upTo){ 3 return IntStream.range(1, upTo) 4 .parallel() 5 .filter(this::isPrime) 6 .count(); 7} 8 9public boolean isPrime(int number){ 10 return IntStream.range(2, number) 11 .allMatch(x -> (number % x) != 0); 12}
7.2.2 开闭原则
软件应该对扩展开放,对修改闭合。 借助于抽象实现!
例:我们有描述计算机花在用户空间、 内核空间和输入输出上的时间散点图。 我将负责显示这些指标的类叫作 MetricDataGraph
1class MetricDataGraph { 2 public void updateUserTime(int value); 3 public void updateSystemTime(int value); 4 public void updateIoTime(int value); 5}
如果添加新的时间点,需要修改MetricDataGraph类,这里新建一个“时间点”的抽象类TimeSeries接口
1public interface TimeSeries{ 2 3}
每种时间点都实现这个抽象类
1public class UserTimeSeries implments TimeSeries{ 2 ... 3} 4public class SystemTimeSeries implments TimeSeries{ 5 ... 6} 7public class IoTimeSeries implments TimeSeries{ 8 ... 9} 10// 新增时间点 11public class StealTimeSeries implments TimeSeries{ 12 ... 13}
1// 重构 2class MetricDataGraph { 3 public void addTimeSeries(TimeSeries values); 4}
对于高阶函数,比如 ThreadLocal 有一个特殊的变量, 每个线程都有一个该变量的副本并与之交互。 该类的静态方法 withInitial 是一个高阶函数, 传入一个负责生成初始值的Lambda 表达式。
1// ThreadLocal 日期格式化器 2 3// 实现 4ThreadLocal<DateFormat> localFormatter 5 = ThradLocal.withInitial(() -> new SimpleDateFormat()); 6 7// 使用 8DateFomat formatter = localFormatter.get();
通过传入不同的 Lambda 表达式, 可以得到完全不同的行为。
1// ThreadLocal 标识符 2// 实现 3AtomicInteger threadId = new AtomicInteger(); 4ThreadLocal<Integer> localId 5 = ThreadLocal.withInitial(() - > threadId.getAndIncrement()); 6 7// 使用 8int idForThisThread = localId.get();
对开闭原则的另外一种理解和传统的思维不同, 那就是使用 不可变对象 实现开闭原则
不可变性:
- 观测不可变:指在其他对象看来, 该类是不可变的
- 实现不可变:指对象本身不可变
java.lang.String 宣称是不可变的,但事实上只是观测不可变,因为它在第一次调用hashCode 方法时缓存了生成的散列值。在其他类看来,这是完全安全的,它们看不出散列值是每次在构造函数中计算出来的,还是从缓存中返回的。
7.2.3 依赖反转原则
抽象不应依赖细节, 细节应该依赖抽象。 ...
8. 使用Lambda表达式编写并发程序
待更新...