函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。
为什么Java需要Lambda表达式进行函数式编程呢?在我看来,有以下好处:
不多啰嗦,下面开始函数式编程之Stream流处理的方法和案例讲解。
Streams API已在Java 8中引入,并且已经是Java语言规范的一部分多年了。尽管如此,平时的工作中,还是有很多项目还停留在java1.7中的使用中,而且java8的很多新特性都是革命性的,尤其是stream流处理。因此,这篇文章里,将介绍Streams的基本概念,并通过一些示例对其进行解释。
本文参考自官方Java文档java-stream流处理。与集合相比,Streams的一些特征(取自官方文档)如下:
在接下来的部分中,我们将介绍如何创建 Streams,介绍一些中间操作,最后我们将介绍一些终止操作。这些源代码可以在结尾的github源码里提供,不需要自己复制粘贴。
有几种方法可以创建流。它们可以从集合,数组,文件等创建。在本节中,我们将创建一些测试,向您展示如何以不同的方式创建流。我们将创建并使用终止操作,以便将 消费到 .我们不对 执行任何其他操作,我们将它留给其他部分。最后,我们断言是否与我们期望的相同。测试位于单元测试中。
使用
java.util.Collection.stream() 方法
private static final List<String> stringsList = Arrays.asList("a", "b", "c");@Testpublic void createStreamsFromCollection() { List<String> streamedStrings = stringsList.stream().collect(Collectors.toList()); assertLinesMatch(stringsList, streamedStrings);}
使用 java.util.Arrays.stream(T[]array)方法
@Testpublic void createStreamsFromArrays() { List<String> streamedStrings = Arrays.stream(new String[]{"a", "b", "c"}).collect(Collectors.toList()); assertLinesMatch(stringsList, streamedStrings);}
使用 Stream的静态方法:of()、iterate()、generate()
@Testpublic void createStreamsFromStreamOf() { List streamedStrings = Stream.of("a", "b", "c").collect(Collectors.toList()); assertLinesMatch(stringsList, streamedStrings);}
使用将int值作为参数来创建。
@Testpublic void createStreamsFromIntStream() { int[] streamedInts = IntStream.of(1, 2, 3).toArray(); assertArrayEquals(new int[]{1, 2, 3}, streamedInts);}
从文件中读取进行创建。
@Test public void createStreamsFromFile() { try { List<String> expectedLines = Arrays.asList("file1", "file2", "file3","file4"); BufferedReader reader = new BufferedReader(new FileReader(new File(System.getProperty("user.dir")+"/src/main/resources/file.txt"))); List<String> streamedLines = reader.lines().collect(Collectors.toList()); assertLinesMatch(expectedLines, streamedLines); } catch (FileNotFoundException e) { e.printStackTrace(); } }
中间操作(intermediate operations)指的是将一个stream转换为另一个stream的操作,譬如filter和map操作。这些操作返回新的,但不返回最终结果。中间操作可以分为无状态操作(不保留有关先前处理的元素的信息)和有状态操作(可能需要在生成中间结果之前处理所有元素)。
可以在Streams API 中找到可调用的操作的完整列表。
public class Shoe { private int id; private String brand; private String size; private String color; public Shoe(int id, String brand, String size, String color) { this.id = id; this.brand = brand; this.size = size; this.color = color; } ...}
在单元测试中,我们定义了四个对象,我们将在下一个示例中使用:shoe
private static final Shoe volkswagenGolf = new Shoe(0, "Volkswagen", "L", "blue"); private static final Shoe skodaOctavia = new Shoe(1, "Skoda", "XL", "green"); private static final Shoe renaultKadjar = new Shoe(2, "Renault", "XXL", "red"); private static final Shoe volkswagenTiguan = new Shoe(3, "Volkswagen", "M", "red");
该操作允许我们基于给定的.在示例中,我们首先创建4双鞋子中的一双,然后创建一双仅包含Volkswagen鞋子的新鞋子。
@Testpublic void filterStream() { List expectedShoes = Arrays.asList(volkswagenGolf, volkswagenTiguan); List filteredShoes = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .filter(shoe -> shoe.getBrand().equals("Volkswagen")) .collect(Collectors.toList()); assertIterableEquals(expectedShoes, filteredShoes);}
在前面的所有示例中,源中的类型和结果始终相同。通常,您希望对每个元素应用一个函数。例如,如果我们希望结果仅包含品牌而不是对象,我们可以使用该操作并将该方法应用于每个元素,从而产生仅具有品牌名称的新元素。
@Testpublic void mapStream() { List expectedBrands = Arrays.asList("Volkswagen", "Skoda", "Renault", "Volkswagen"); List brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getBrand) .collect(Collectors.toList()); assertIterableEquals(expectedBrands, brands);}
当然,将操作组合并将它们连接在管道中是完全有效的。在下一个示例中,我们进行筛选以检索Volkswagen鞋子,然后使用该操作仅检索颜色。这样,我们最终会得到一个包含Volkswagen鞋子颜色的列表。
@Testpublic void filterMapStream() { List expectedColors = Arrays.asList("blue", "red"); List volkswagenColors = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .filter(shoe -> shoe.getBrand().equals("Volkswagen")) .map(Shoe::getColor) .collect(Collectors.toList()); assertIterableEquals(expectedColors, volkswagenColors);}
不同操作将返回仅包含基于元素的方法实现的不同元素的。在下一个示例中,我们首先检索一个品牌,然后对其执行操作。这就产生了我们使用的三个不同品牌的列表。
@Testpublic void distinctStream() { List expectedBrands = Arrays.asList("Volkswagen", "Skoda", "Renault"); List brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getBrand) .distinct() .collect(Collectors.toList()); assertIterableEquals(expectedBrands, brands);}
该操作将根据其自然顺序对元素进行排序。也可以使用 a 作为参数,以便进行更自定义的排序。下一个示例将从 中检索品牌并按字母顺序对其进行排序。
@Testpublic void sortedStream() { List expectedSortedBrands = Arrays.asList("Renault", "Skoda", "Volkswagen", "Volkswagen"); List brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getBrand) .sorted() .collect(Collectors.toList()); assertIterableEquals(expectedSortedBrands, brands);}
我们将讨论的最后一个中间操作是操作。这是一个特殊的,主要用于调试目的。 将在使用元素时对其执行操作。让我们以组合的过滤器和地图为例,并向其添加一些操作,以便打印正在使用的元素。
@Testpublic void peekStream() { List expectedColors = Arrays.asList("blue", "red"); List volkswagenColors = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .filter(shoe -> shoe.getBrand().equals("Volkswagen")) .peek(e -> System.out.println("Filtered value: " + e)) .map(Shoe::getColor) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList()); assertIterableEquals(expectedColors, volkswagenColors);}
此测试的输出为:
Filtered value: Shoe{id=0, brand='Volkswagen', type='Golf', color='blue'}Mapped value: blueFiltered value: Shoe{id=3, brand='Volkswagen', type='Tiguan', color='red'}Mapped value: red
终止操作(terminal operations)则指的是那些会产生一个新值或副作用(side-effect)的操作,例如count 和 forEach 操作。
我们已经在前面的所有示例中使用了终止操作。我们总是在操作中使用参数。不过还有更多选项可以使用。我们将在下一个示例中介绍一些内容。完整的列表可以在JavaDoc中找到。
方法 collect 里使用 ,我们可以使用方法Collectors.joining进行分隔符分隔。
@Testpublic void collectJoinStream() { String expectedBrands = "Volkswagen;Skoda;Renault;Volkswagen"; String joinedBrands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getBrand) .collect(Collectors.joining(";")); assertEquals(expectedBrands, joinedBrands);}
Collectors.summingInt 使用 ,我们可以计算元素的属性之和。在我们的示例中,我们只计算鞋子 id 的总和。
@Testpublic void collectSummingIntStream() { int sumIds = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .collect(Collectors.summingInt(Shoe::getId)); assertEquals(6, sumIds);}
Collectors.groupingBy 使用 ,我们可以将元素分组到 .在我们的示例中,我们根据品牌对元素进行分组。
@Testpublic void collectGroupingByStream() { Map> expectedShoes = new HashMap<>(); expectedShoes.put("Skoda", Arrays.asList(skodaOctavia)); expectedShoes.put("Renault", Arrays.asList(renaultKadjar)); expectedShoes.put("Volkswagen", Arrays.asList(volkswagenGolf, volkswagenTiguan)); Map> groupedShoes = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .collect(Collectors.groupingBy(Shoe::getBrand)); assertTrue(expectedShoes.equals(groupedShoes));}
reduce,该操作对 的元素执行减量。它使用恒等式(即起始值)和执行归约的累积函数。在我们的示例中,我们对元素的 id 执行求和,就像我们使用该操作时所做的那样。
@Testpublic void reduceStream() { int sumIds = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getId) .reduce(0, Integer::sum); assertEquals(6, sumIds);}
该操作对每个元素执行一个函数。它与中间操作非常相似。
@Testpublic void forEachStream() { Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .forEach(System.out::println);}
该测试的结果如下:
Shoe{id=0, brand='Volkswagen', type='Golf', color='blue'}Shoe{id=1, brand='Skoda', type='Octavia', color='green'}Shoe{id=2, brand='Renault', type='Kadjar', color='red'}Shoe{id=3, brand='Volkswagen', type='Tiguan', color='red'}
该操作对流中的元素数进行计数。
@Testpublic void countStream() { long count = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan).count(); assertEquals(4, count);}
该操作返回基于给定的 的最大元素。在我们的示例中,我们创建一个 id 并检索具有最高 id 的元素。请注意,这将返回一个 .当无法确定最高 id 时,我们只是提供了一个替代值。
@Testpublic void maxStream() { int maxId = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan) .map(Shoe::getId) .max((o1, o2) -> o1.compareTo(o2)) .orElse(-1); assertEquals(3, maxId);}
Java Streams API非常强大,学习起来并不难。stream流处理可读性还是很不错的,想让自己代码变得更加简洁美观,那就来跟着本文学习吧,还有源码跟着调试。
如果您还不熟悉 Streams API,可以安装IDEA的插件工具,调试 Java 流的出色 IntelliJ 功能。
本文案例的源码地址: beierzsj/javaStreamDemo (github.com)