Java 8函数式编程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 8函数式编程相关的知识,希望对你有一定的参考价值。

函数式编程

一、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 }

三)与抽象类的区别

接口和抽象类之间还是存在明显的区别。 接口允许多重继承, 却没有成员变量; 抽象类可
以继承成员变量, 却不能多重继承。

 

四、接口的静态方法

如果一个方法有充分的语义原因和某个概念相关, 那么就应该将该方法和相关的类
或接口放在一起, 而不是放到另一个工具类中。 这有助于更好地组织代码, 阅读代码的人
也更容易找到相关方法。

 








































以上是关于Java 8函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

深度探秘 Java 8 函数式编程(下)

Java 8 新语法习惯 (更轻松的函数式编程)

深度探秘 Java 8 函数式编程(上)

Java 8 函数式编程中“减少”函数的第三个参数的目的

浅谈Java 8的函数式编程

跟上 Java 8 : 函数式编程