函数式编程
一、Lambda表达式
一)如何辨别Lambda表达式
1 Runnable noArguments = () -> System.out.println("Hello World"); 2 3 ActionListener oneArgument = event -> System.out.println("button clicked"); 4 Runnable multiStatement = () -> { 5 System.out.print("Hello"); 6 System.out.println(" World"); 7 }; 8 9 BinaryOperator<Long> add = (x, y) -> x + y; 10 11 BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
1)没有参数用空“()”表示。
2)只有一个参数,括号可以省略,只写参数名。
3)Lambda 表达式的主体不仅可以是一个表达式, 而且也可以是一段代码块, 使用大括号
( {}) 将代码块括起来。 该代码块和普通方法遵循的规则别无二致, 可以用返
回或抛出异常来退出。 只有一行代码的 Lambda 表达式也可使用大括号, 用以明确 Lambda
表达式从何处开始、 到哪里结束。
4)Lambda 表达式也可以表示包含多个参数的方法。 这时就有必要思考怎样去阅
读该 Lambda 表达式。 这行代码并不是将两个数字相加, 而是创建了一个函数, 用来计算
两个数字相加的结果。 变量 add 的类型是 BinaryOperator<Long>, 它不是两个数字的和,
而是将两个数字相加的那行代码。
5)到目前为止, 所有 Lambda 表达式中的参数类型都是由编译器推断得出的。 这当然不错,
但有时最好也可以显式声明参数类型, 此时就需要使用小括号将参数括起来, 多个参数的
情况也是如此。
6)方法引用:Lambda表达式调用参数的一种简便写法。
如:
artist -> artist.getName() //可以写成 Artist :: getName
构造函数有同样的写法:
(name,nationality)-> new Artist(name,nationlity) //可以写为 Artist :: new
目标类型是指 Lambda 表达式所在上下文环境的类型。 比如, 将 Lambda 表
达式赋值给一个局部变量, 或传递给一个方法作为参数, 局部变量或方法参
数的类型就是 Lambda 表达式的目标类型。
二)引用值,而不是变量
匿名内部类需要引用它所在方法里的变量时,需要将变量声明为final。
Java 8中放松了这一限制,可以引用非final变量,但该变量在既成事实上必须是final(只能给该变量赋值一次,
如果试图给该变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错)。
这种行为也解释了为什么lamdba表达式为闭包-------含有自由变量(不是传入参数,且没有在方法块中定义的变量)的代码块。
三)函数接口
函数接口是只有一个抽象方法的接口,用作lambda表达式的类型。
四)类型推断
可省略Lambda表达式中所有参数的类型。
无法推断出类型的报错信息:
1 Operator ‘& #x002B;‘ cannot be applied to java.lang.Object, java.lang.Object.
二、流(Stream)
流使得程序员在更高的层次上对集合进行操作。
一)从外部迭代到内部迭代
使用 for 循环计算来自伦敦的艺术家人数 :
1 int count = 0; 2 for (Artist artist : allArtists) { 3 if (artist.isFrom("London")) { 4 count++; 5 } 6 }
此类代码的问题:
1.每次迭代集合类时, 都需要写很多样板代码
2.for 循环改造成并行方式运行也很麻烦, 需要修改每个 for 循环才能实现。
3.for 循环的样板代码模糊了代码的本意, 程
序员必须阅读整个循环体才能理解。 若是单一的 for 循环, 倒也问题不大, 但面对一个满
是循环( 尤其是嵌套循环) 的庞大代码库时, 负担就重了。
就其背后的原理来看, for 循环其实是一个封装了迭代的语法糖, 我们在这里多花点时间,
看看它的工作原理。 首先调用 iterator 方法, 产生一个新的 Iterator 对象, 进而控制整
个迭代过程, 这就是外部迭代。 迭代过程通过显式调用 Iterator 对象的 hasNext 和 next
方法完成迭代。 展开后的代码如例 3-2 所示, 图 3-1 展示了迭代过程中的方法调用。
1 int count = 0; 2 Iterator<Artist> iterator = allArtists.iterator(); 3 while(iterator.hasNext()) { 4 Artist artist = iterator.next(); 5 if (artist.isFrom("London")) { 6 count++; 7 } 8 }
内部迭代:
1 long count = allArtists.stream() 2 .filter(artist -> artist.isFrom("London")) 3 .count();
Stream 是用函数式编程方式在集合类上进行复杂操作的工具。
二)常用的流操作
惰性求值方法:只描述Steam,最终不产生新集合的方法。
及早求值方法:最终会从Steam产生值的方法。
1.collect(toList())
该方法由Steam的值生成一个列表,是一个及早求值的操作。
List<String> collected = Stream.of("a", "b", "c") //Stream.of方法使用一组初始值生成Stream。 .collect(Collectors.toList()); assertEquals(Arrays.asList("a", "b", "c"), collected);
2.map
map可以将一个流中的值转换成一个新的流。
1 List<String> collected = Stream.of("a", "b", "hello") 2 .map(string -> string.toUpperCase()) //Lambda 表达式必须是 Function 接口的一实例 3 .collect(toList()); 4 assertEquals(asList("A", "B", "HELLO"), collected);
3.filter
遍历数据并检查其中的元素。
1 List<String> beginningWithNumbers 2 = Stream.of("a", "1abc", "abc1") 3 .filter(value -> isDigit(value.charAt(0))) //必须是Predicate 4 .collect(toList());
4.flatmap
flatMap 方法可用 Stream 替换值, 然后将多个 Stream 连接成一个 Stream
1 List<Integer> together = Stream.of(asList(1, 2), asList(3, 4)) 2 .flatMap(numbers -> numbers.stream()) //必须是Function 3 .collect(toList()); 4 assertEquals(asList(1, 2, 3, 4), together);
5.max和min方法
1 List<Track> tracks = asList(new Track("Bakai", 524), 2 new Track("Violets for Your Furs", 378), 3 new Track("Time Was", 451)); 4 Track shortestTrack = tracks.stream() 5 .min(Comparator.comparing(track -> track.getLength())) 6 .get(); 7 assertEquals(tracks.get(1), shortestTrack);
6.调用List或者Set的stream方法就可以得到Steam对象。
三)元素顺序
1)出现顺序
测试永远通过:
1 List<Integer> numbers = asList(1, 2, 3, 4); 2 List<Integer> sameOrder = numbers.stream() 3 .collect(toList()); 4 assertEquals(numbers, sameOrder);
无法保证每次都通过:
1 Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1)); 2 List<Integer> sameOrder = numbers.stream() 3 .collect(toList()); 4 // 该断言有时会失败 5 assertEquals(asList(4, 3, 2, 1), sameOrder);
这会带来一些意想不到的结果, 比如使用并行流时, forEach 方法不能保证元素是
按顺序处理的。 如果需要保证按顺序处理, 应该使用
forEachOrdered 方法
四)收集器
一种通用的、 从流生成复杂值的结构。 只要将它传给 collect 方法, 所有
的流就都可以使用它了 。
1)转换为其他集合
如:toList, toSet, toCollection
2)转换成值
利用收集器让流生成一个值 ,如:
1 public Optional<Artist> biggestGroup(Stream<Artist> artists) { 2 Function<Artist,Long> getCount = artist -> artist.getMembers().count(); 3 return artists.collect(maxBy(comparing(getCount))); 4 }
3)数据分块
分解成两个集合 ,收集器 partitioningBy, 它接受一个流, 并将其分成两部分 ,它使用 Predicate 对象判断一个元素应该属于哪个部分, 并根据布尔值返回一
个 Map 到列表。
1 public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) { 2 return artists.collect(partitioningBy(artist -> artist.isSolo())); 3 }
4)数据分组
1 public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) { 2 return albums.collect(groupingBy(album -> album.getMainMusician())); 3 }
5)字符串
以前的写法:
1 StringBuilder builder = new StringBuilder("["); 2 for (Artist artist : artists) { 3 if (builder.length() > 1) 4 builder.append(", "); 5 String name = artist.getName(); 6 builder.append(name); 7 } b 8 uilder.append("]"); 9 String result = builder.toString();
现在的写法:
1 String result = 2 artists.stream() 3 .map(Artist::getName) 4 .collect(Collectors.joining(", ", "[", "]"));
6)定制收集器
未完待续
三、重载和继承
一)重载
1)Lambda 表达式作为参数时, 其类型由它的目标类型推导得出, 推导过程遵循
如下规则:
1.如果只有一个可能的目标类型, 由相应函数接口里的参数类型推导得出;
2.如果有多个可能的目标类型, 由最具体的类型推导得出;
3.如果有多个可能的目标类型且最具体的类型不明确, 则需人为指定类型。
2)每个用作函数接口的接口都应该添加@FunctionalInterface注释,该注释会检查被注释接口是否符合函数接口的标准。
三)继承
1)默认方法
引入默认方法目的:接口的向后兼容(如果没有默认方法,我们在接口中定义一个新方法时......)。
1.无论函数接口,还是非函数接口都可以使用默认方法。
2.任何时候,默认方法与类中的方法产生冲突,优先选择类中的方法。
3.多重继承,类实现的多个接口中有方法签名相同的默认方法,此时编译器会报错:
1 class Musical Carriage 2 inherits unrelated defaults for rock() from types Carriage and Jukebox。
解决方法:重写:
1 public class MusicalCarriage 2 implements Carriage, Jukebox { 3 @Override 4 public String rock() { 5 return Carriage.super.rock(); 6 } 7 }
三)与抽象类的区别
接口和抽象类之间还是存在明显的区别。 接口允许多重继承, 却没有成员变量; 抽象类可
以继承成员变量, 却不能多重继承。
四、接口的静态方法
如果一个方法有充分的语义原因和某个概念相关, 那么就应该将该方法和相关的类
或接口放在一起, 而不是放到另一个工具类中。 这有助于更好地组织代码, 阅读代码的人
也更容易找到相关方法。