一文读懂:Java函数式编程

168 阅读9分钟

基本定义

Java中重要的函数接口

接口参数类型返回类型示例
PredicateTboolean接受一个对象, 返回一个布尔值s -> s.isEmpty()
ConsumerTvoids -> System.out.pring(s)
Function<T,R>TUs -> new Integer(s)
SupplierNoneT() -> new String();
UnaryOperatorTT
BinaryOperator(T, T)T

Java中的lambda表达式包含一个参数列表和一个lambda体,二者之间通过一个函数箭头“->”分隔。

单个参数:

p ->p.translate(1, 1)
i -> new Point(i, i + 1)

不过与方法声明类似,lambda 可以接收任意数量的参数。除了像之前那样接收单个参数的lambda外,参数列表必须使用圆括号包围起来:

(x, y) ->x+y
()->23

到目前为止,我们在声明参数时并没有显式指定类型,因为不指定类型时lambda的可读性通常会更好一些。 不过,我们总是可以提供参数类型,有时这也是必要的,因为编译器可能无法从上下文中推断出其类型。如果显式提供了类型,那就必须为所有参数都提供类型,而且参数列表必须包围在圆括号中:

(int x,int y) ->x+y

可以像方法参数那样修改这种显式类型的参数,例如,可以将其声明为final, 也可以添加注解。

函数箭头右侧的lambda体可以是表达式,到目前为止所有示例都是这样的(注意,方法调用是表达式,包括那些返回void 的方法)。诸如此类的lambda有时也称为“表达式lambda"。更为一般的形式则是“语句lambda",其中的lambda体是-一个块,也就是说,是由花括号包围的一系列语句:

(Thread t) ->{t.start();}
() ->{System.gc(); return 0;}

表达式lambda:

args -> expr

可以看成相应的语句lambda的简写形式:

args -> { return expr; }

在块体中到底使用还是省略return 关键字的原则与普通的方法体是一致的,也就是说,如果lambda体中的表达式有返回值,那就需要使用return, 也可以后面跟一个参数来立刻终止lambda 体的执行。如果lambda返回void,那就可以省略returm,也可以使用它,但后面不带参数。

方法引用类型:

方法引用

artist -> artist.getName() 等价于 Artist::getName

lambda与匿名内部类

内部类的声明会创建出一个新的命名作用域,在这个作用域中,this 与super指的是内部类本身的当前实例:相反,lambda表达式并不会引入任何新的命名环境。这样就避免了内部类名称查找的复杂性,名称查找会导致很多小错误,例如想要调用外围实例的方法时却错误地调用了内部类实例的Object方法。

由于lambda声明就像简单的块一样,因此关键字this与super与外围环境的含义一样:也就是说,它们分别指的是外围对象及其父类对象。

惰性求值与及早求值

像filter 这样只描述 Stream, 最终不产生新集合的方法叫作惰性求值方法; 而像 count 这样最终会从 Stream 产生值的方法叫作及早求值方法。

判断一个操作是惰性求值还是及早求值很简单: 只需看它的返回值。 如果返回值是 Stream, 那么是惰性求值; 如果返回值是另一个值或为空, 那么就是及早求值。

Tip:高阶函数是指接受另外一个函数作为参数, 或返回一个函数的函数。 高阶函数不难辨认: 看函数签名就够了。 如果函数的参数列表里包含函数接口, 或该函数返回一个函数接口, 那么该函数就是高阶函数。

常用的流操作

Stream API

Stream 的 of 方法使用一组初始值生成新的 Stream。

List collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

map 映射

可以将一种类型的值转换成另外一种类型,方法map会通过提供的Function<T, R>转换每个流元素。

其输出是一个流,包含了对输入流中每个元素应用了Function后的结果。

List collected = Stream.of("a", "b", "hello") .map(string -> string.toUpperCase()) .collect(toList()); 
assertEquals(asList("A", "B", "HELLO"), collected);

传给 map 的 Lambda 表达式只接受一个 String 类型的参数, 返回一个新的 String。 参数 和返回值不必属于同一种类型, 但是 Lambda 表达式必须是 Function 接口的一个实例( 如 图 3-4 所示), Function 接口是只包含一个参数的普通函数接口。

filter 过滤

经过过滤,输入流中符合提供的Predicate的那些元素, 即 Lambda 表达式值为 true 的元素被保留下来。

flatMap

flatMap 方法可用 Stream 替换值, 然后将多个 Stream 连接成一个 Stream。

假定选定一组专辑(album), 找出其中所有长度大于 1 分钟的曲目(track)名称

Set trackNames = albums.stream().flatMap(album -> album.getTracks()).filter(track -> track.getLength() > 60).map(track -> track.getName()).collect(toSet());

排序与去重

如果集合本身就是无序的, 由此生成的流也是无序的。sorted使得输出流包含输入流中的元素,并且是有序的。

  • 第1个sorted重载方法会使用自然顺序对对象进行排序。

Stream sortedTitles = library.stream().map(Book: :getTitle).sorted();

  • 第2个重载的sorted会接受一个Comparator;例如,静态方法Comparator.comparing会根据一个键抽取器创建一个Comparator:

Stream booksSortedByTitle = library.stream().sorted(Comparator.comparing(Book: :getTitle));

通过从一个键创建一个Comparator,它提供了除了对流元素

搜索操作

可以划分为“搜索"操作的Stream方法分为两组:

第1组:包含了匹配操作,它们会测试是否有某个流元素或全部流元素满足给定的Predicate:

anyMatch在找到与predicate匹配的元素时会返回true;allMatch在找到不满足predicate的任意一个元素时会返回false,否则返回true; 

noneMatch与之类似,如果找到任意一个满足predicate的元素时会返回false,否则返回true。

boolean withinShelfHeight = library.stream().filter(b ->b.getTopic() == HISTORY).allMatch(b ->b.getHeight() < 19) ;

第2组:搜索操作由两个“find"方法构成: findFirst与findAny:

findFirst在有序流中找到第1个匹配的元素并返回。而如果有序流中的任何一个匹配的元素都可以接受,那么你就应该使用findAny;

optional anyBook = library.stream().filter(b ->b.getAuthors().contains ("Herman Melville")).findAny();
BufferedReader br = new BufferedReader (new FileReader ("Mastering.tex")) ;
Optional line = br.lines().filter(s ->s.contains ("findFirst") ).findFirst() ;

类Optional

  • get: 如果存在则返回一个值;否则,该方法会抛出NoSuchElementException异常。这是个“不安全的”访问一个Optional内容的操作,通常情况下应该避免使用,转而使用如下安全的方法。
  • ifPresent:如果值存在,那么将其提供给Consumer; 否则,什么都不做。
  • isPresent: 如果值存在,那么返回true;否则返回false。
  • orElse: 如果值存在,那么将其返回,否则返回参数。它与orElseGet是访问内容的安全操作。对于Optional的一般使用场景来说,即便存在空值的可能,这些操作也要比get用处更大。
  • orElseGet: 如果值存在,那么将其返回;否则,调用Supplier并返回其结果。

reduce

reduce 操作可以实现从一组值中生成一个值。

Comparator titleComparator = Comparator.comparing(Book::getTitle) ;
Optional first = library.stream().reduce(BinaryOperator.minBy(titleComparator));

Stream biStream = LongStream.of(1,2,3).mapTo0bj(BigInteger: :value0f) ;

Optional bigIntegerSum = biStream.reduce(BigInteger: :add) ;

流处理的示例

  • 只包含计算机图书的流:

Stream computingBooks = library.stream ().filter(b ->b.getTopic() == COMPUTING);

  • 图书标题的流:

Stream  bookTitles = library.stream().map(Book: :getTitle);

  • 根据标题排序:

Stream booksSortedByTitle = library.stream().sorted(Comparator.comparing(Book: :getTitle));

  • 使用这个排序流创建一个作者流,根据图书标题排序,并且去除重复的:
Stream authorsInBookTitleorder = library.stream()
.sorted(Comparator.comparing(Book: :getTitle))
.flatMap(book ->book.getAuthors().stream())
.distinct();
  • 以标题的字母顺序生成前100个图书的流:

Stream readingList = library.stream().sorted(Comparator.comparing(Book: :getTitle)).limit (100);

  • 除去前100个图书的流:

Stream remainderList = library.stream().sorted(Comparator.comparing(Book: :getTitle)).skip(100);

  • 图书馆中最早出版的图书:

Optional oldest = library.stream().min(Comparator.comparing(Book: :getPubDate)) ;

  • 图书馆中图书的标题集合:

Set titles = library.stream().map(Book: :getTitle).collect(Collectors.toSet()) ;

  • Accumulate names into a List

List list = people.stream().map(Person::getName).collect(Collectors.toList());

  • Accumulate names into a TreeSet

Set set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));

  • Convert elements to strings and concatenate them, separated by commas

String joined = things.stream().map(Object::toString).collect(Collectors.joining(", "));

  • Compute sum of salaries of employee

int total = employees.stream().collect(Collectors.summingInt(Employee::getSalary)));

  • Group employees by department

Map<Department, List> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));

  • Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,Collectors.summingInt(Employee::getSalary)));
  • Partition students into passing and failing

Map<Boolean, List> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

使用收集器

转换成其他集合

Set titles = library.stream().map(Book: :getTitle).collect(Collectors.toSet());

例如,可以通过toMap的第1个重载收集器将集合中的每本图书的标题映射到其出版日期上:

Map<String,Year> titleToPubDate = library.stream().collect(toMap(Book::getTitle, Book::getPubDate));

如果需要重复键,那么就可以使用第2个重载方法,这样就可以指定具体行为了,这是通过类型为BinaryOperator的merge函数形式指定的,它会从两个已有值(一个位于Map中,另一个是要添加到Map中的)生成一个新值。 有多种方式可以通过两个值来生成相同类型的第3个值;例如,两个String值可以拼接。这里,我决定只在Map中包含每本书的最新版本:

Map<String, Year> titleToPubDate = library.stream().collect(toMap(Book::getTitle,Book::get PubDate,(x,y) -> x.isAfter(y) ? x : y));

数据分块

partitioningBy, 它接受一个流, 并将其分成两部分。它使用 Predicate 对象判断一个元素应该属于哪个部分,并根据布尔值返回一个 Map到列表。因此,对于 true List 中的元素, Predicate 返回 true; 对其他 List 中的元素, Predicate 返回 false。

数据分组

数据分组是一种更自然的分割数据操作, 与数据分块将数据分成 ture 和 false 两部分不同, 可以使用任意值对数据分组。groupingBy 收集器接受一个分类函数, 用来对数据分组, 使用的分类器是一个 Function 对象,和 map 操作用到的一样。

  • 根据主题对图书进行分类的Map:

Map<Topic,List> booksByTopic = library.stream().collect (groupingBy (Book: :getTopic));

  • 从图书标题映射到最新版发布日期的有序Map:

Map<String, Year> titleToPubDate = library.stream () .collect (toMap (Book: :getTitle,Book: :getPubDate, BinaryOperator.maxBy (natural0rder()),TreeMap: :new)); 

  • 将图书划分为小说(对应true)-与非小说(对应false)的Map:

Map<Boolean, List> fictionOrNon = library. stream().collect(partitioningBy(b -> b.getTopic() == FICTION));

  • 将每个主题关联到该主题下拥有最多作者的图书上:

Map<Topic, optional > mostAuthorsByTopic =library.stream () .collect (groupingBy (Book: :getTopic, maxBy (comparing(b -> b.getAuthors() .size()))));

  • 将每个主题关联到该主题总的卷数上:

Map<Topic, Integer> volumeCountByTopic = library.stream() .collect (groupingBy(Book::getTopic, summingInt(b -> b.getPageCounts().length)));

  • 拥有最多图书的主题:
Optional mostPopularTopic = library.stream()
.collect(groupingBy (Book: :getTopic, counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
  • 将每个主题关联到该主题下所有图书标题拼接成的字符串上:

Map<Topic, String> concatenatedTitlesByTopic = library.stream().collect(groupingBy(Book: :getTopic,mapping(Book: :getTitle, joining(";"))));

  • given a stream of Person, to accumulate the set of last names in each city:

Map<City, Set> lastNamesByCity = people.stream().collect(groupingBy(Person::getCity, mapping(Person::getLastName, toSet())));

Map<String, Year> titleToPubDate = library.stream() .collect (toMap(Book::getTitle, Book: :getPubDate, (x,y) -> x.isAfter(y) ? x : y,TreeMap: :new)) ;

NavigableSet sortedTitles = library.stream().map (Book::getTitle).collect(toCollection(TreeSet::new)) ;