1.行为参数化
1.1 简单说明
你想要写两个只有几行代码不同的方法,那现在你只需要把不同的那部分代码作为参数传递进去就可以了。让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
行为参数化把代码传递给方法,Java 8里面将代码传递给方法的功能(同时也能够返回代码并将其包含在数据结构中)还让我们能够使用一整套新技巧,通常称为函数式编程。
1.2 实例:
比如现在有一些苹果库存,农民需要根据不同的条件查询出对应的苹果库存。
1.2.1 初始实现
对于不同的条件,刚开始我们可能会这样写
条件1 根据颜色过滤:
java
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
条件2 根据重量过滤:
java
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
上面的代码,打破了DRY(Don't Repeat Yourself)的软件工程原则。 我们发现除了过滤条件外,其他的代码几乎一模一样。于是考虑把条件代码作为行为传给方法。
1.2.2 第一次优化:行为参数化
为了解决上面代码重复的问题,开始对我们的选择标准进行建模,考虑的是苹果,需要根据Apple的某些属性(是否是绿色的?重量是否超过150g?)来返回一个boolean的值。我们把它称之为谓词(即一个返回boolean值的函数)
定义一个接口对选择标准建模
java
public interface ApplePredicate {
boolean test (Apple apple);
}
行为1
java
public class AppleRedPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
}
行为2
java
public class AppleHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
过滤方法改写后
java
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (predicate.test(apple)) {
result.add(apple);
}
}
return result;
}
调用过滤方法,将行为作为方法传递进去
java
List<Apple> filterApples = filterApples(inventory,new AppleRedPredicate());
1.2.3 第二次优化:匿名内部类
java
List<Apple> filterApplesColor = filterApples(inventory,new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return apple.getColor().equals("red");
}
});
List<Apple> filterApplesWeight = filterApples(inventory,new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
});
1.2.4 第三次优化:Lambda 表达式
java
List<Apple> filterApples = filterApples(inventory, apple -> apple.getColor().equals("red"));
List<Apple> filterApplesColor = filterApples(inventory, apple -> apple.getWeight() > 150);
2. Lambda表达式
2.1 Lambda表达式简介
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda表达式 由 参数,箭头,主体组成
Lambda 的基本语法
java
(parameters)-> expression
java
(parameters)-> { statements; }
2.2 函数式接口
函数式接口就是只定义一个抽象方法的接口。
接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
下面是一个简单的函数式接口
java
@FunctionalInterface
public interface Predicate<T>f
boolean test (T t);
}
用函数式接口可以干什么呢?
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙: 需要提供一个实现,然后再直接内联将它实例化。具体例子可见1.2.3 和1.2.4
2.3 函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法。
java 8中常见的函数式接口
2.4 Lambda 实践
继续以1.2 的例子来说,比如现在有一些苹果库存,农民需要根据不同的条件查询出对应的苹果库存。
2.4.1 第1步:行为参数化
可见公共部分的代码是:对苹果列表进行遍历,如果符合条件就添加到新的列表,后续返回新的列表。 不同的是,每次对每个苹果的查询条件不一样。即
java
if (apple.getColor().equals(color))
if (apple.getWeight() > weight)
于是需要把判断条件行为参数化,给filterApples方法传递不同的行为(不同的判断条件),以便对苹果进行不同条件的筛选。
传递行为正是Lambda的拿手好戏。那要是查询颜色为红色的苹果,这个新的filterApples方法看起来又该是什么样的呢?基本上,你需要一个接收Apple并返回boolean的Lambda。
例如,下面就是查询颜色为红色的苹果的写法
java
filterApples(inventory, apple -> apple.getColor().equals("red"));
2.4.2 第2步:使用函数式接口来传递行为
Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配 Apple -> boolean。我们发现java中已经有的一个函数式接口可以直接使用
java
@FunctionalInterface
public interface Predicate<T>f
boolean test (T t);
}
现在你就可以把这个接口作为新的filterApples方法的参数了
java
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> predicate) {
......
}
2.4.3 第3步:执行一个行为
任何 Apple -> boolean 形式的Lambda 都可以作为参数来传递,因为它们符合Predicate接口中定义的test方法的签名。现在你只需要一种方法在filterApples主体内执行Lambda所代表的代码。请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在filterApples主体内,对得到的Predicate对象调用test方法执行处理。
2.4.4 第4步:传递Lambda
java事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple
表3-4 Lambda及其等效方法引用的例子

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖
List<Apple> filterApples = filterApples(inventory, apple -> apple.getColor().equals("red"));
List<Apple> filterApplesColor = filterApples(inventory, apple -> apple.getWeight() > 150);
2.5 方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。
Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是"直接调用这个方法",那最好还是用名称来调用它,而不是去描述如何调用它。你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖。
当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。
3. Stream流
3.1 流简介
Java 8中的集合支持一个新的stream方法,它会返回一个流,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。
比如java7对一个集合处理过滤+排序+获取排序后的某个字段
引入流后的写法
3.2 流使用
流的使用一般包括三件事:
❑ 一个数据源(如集合)来执行一个查询;
❑ 一个中间操作链,形成一条流的流水线;
❑ 一个终端操作,执行流水线,并能生成结果。
一些开始操作
操作 | 类型 | 返回类型 | 目的 |
---|---|---|---|
stream() | 开始 | Stream | 创建出一个新的stream串行流对象 |
parallelStream() | 开始 | Stream | 创建出一个可并行执行的stream流对象 |
Stream.of() | 开始 | Stream | 通过给定的一系列元素创建一个新的Stream串行流对象 |
一些中间操作
一些终端操作
3.2.1 map与flatMap
3.2.1.1 map
map
是对每个元素进行转换的操作。它接受一个函数作为参数,该函数将每个输入元素映射到一个输出元素。map
的操作是一对一的,即每个输入元素都对应一个输出元素。- 结果是一个新的集合,其中包含了经过函数转换的每个元素。
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
.map(x -> x * x)
.collect(Collectors.toList());
System.out.println(squaredNumbers);
如上面的例子中,map
操作将每个数的平方映射到新的集合中。输出结果为
[1, 4, 9, 16, 25]
3.2.1.2 flatMap
:
flatMap
也对每个元素进行转换,但是它的转换函数返回的是一个包含零个或多个元素的流。flatMap
的操作是一对多的,即每个输入元素可以对应零个或多个输出元素。- 结果是一个扁平化的集合,其中包含了所有转换后的元素。
flatMap
操作的时候其实是先每个元素处理并返回一个新的Stream,然后将多个Stream展开合并为了一个完整的新的Stream
java
List<List<Integer>> nestedNumbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
List<Integer> flattenedNumbers = nestedNumbers.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flattenedNumbers);
在上面的例子中,flatMap
操作将嵌套的列表转换成了一个扁平的列表。输出结果为
[1, 2, 3, 4, 5, 6, 7, 8, 9]
3.2.2 filter、sorted、distinct、limit
3.2.2.1 filter
:
filter
用于根据指定的条件筛选流中的元素。它接受一个Predicate
函数式接口作为参数,该函数决定了哪些元素会被保留下来。
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers);
上面的例子输出结果为 [2, 4, 6, 8]
3.2.2.2 sorted
:
sorted
用于对流中的元素进行排序。- 它可以接受一个比较器,或者直接使用元素的自然顺序进行排序。
java
List<String> words = Arrays.asList("banana", "apple", "orange", "grape");
List<String> sortedWords = words.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedWords)
上面的例子输出结果为 [apple, banana, grape, orange]
3.2.2.3 distinct
:
distinct
用于去除流中重复的元素,得到不重复的元素集合。- 它依赖元素的
equals
方法来判断是否相同。
java
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(distinctNumbers);
上面的例子输出结果为 [1, 2, 3, 4, 5]
3.2.2.4 limit
:
limit
用于截取流中的前 N 个元素,返回一个新的流。- 它接受一个参数,表示保留的元素个数。
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> limitedNumbers = numbers.stream()
.limit(5)
.collect(Collectors.toList());
System.out.println(limitedNumbers);
上面的例子输出结果为 [1, 2, 3, 4, 5]
这些操作可以组合使用,以构建复杂的流处理管道。例如,你可以先使用 filter
进行条件筛选,然后使用 sorted
进行排序,接着使用 distinct
去除重复元素,最后使用 limit
截取前几个元素。
3.2.3 peek
和 forEach
3.2.3.1 peek
方法:
-
peek
方法用于在流的每个元素被消费的同时执行一些操作,这些操作可以是一些输出语句、记录日志、或者对元素进行修改等。 -
它返回的是一个新的流,因此可以链式调用多个
peek
操作。 -
peek
不会触发流的终结操作,因此可以在peek
之后继续进行其他的中间操作。
java
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
List<String> modifiedWords = words.stream()
.peek(word -> System.out.println("Original: " + word))
.map(String::toUpperCase)
.peek(word -> System.out.println("Uppercase: " + word))
.collect(Collectors.toList());
System.out.println(modifiedWords);
上面的例子输出结果为
makefile
Original: apple
Uppercase: APPLE
Original: banana
Uppercase: BANANA
Original: orange
Uppercase: ORANGE
Original: grape
Uppercase: GRAPE
[APPLE, BANANA, ORANGE, GRAPE]
3.2.3.2 forEach
方法:
forEach
方法用于对流中的每个元素执行指定的操作。与peek
不同,forEach
是一个终结操作,它不返回新的流。forEach
接受一个Consumer
函数式接口作为参数,该接口定义了对每个元素执行的操作。
arduino
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
words.stream()
.forEach(word -> System.out.println("Word: " + word));
上面的例子输出结果为
makefile
Word: apple
Word: banana
Word: orange
Word: grape
- 注意:
forEach
是一个终结操作,它会遍历整个流,执行操作。因此,在使用forEach
之后,不能再继续进行其他中间操作。
总体而言,如果你需要在流的每个元素上执行一些操作,但仍然想保持流的状态,可以使用 peek
。如果你只是希望对每个元素执行一些操作而不关心返回结果,并且不再进行其他操作,可以使用 forEach
。
3.2.4 collect 将流中的元素收集到不同的集合类型
3.2.4.1 Collectors.toSet()
:
Collectors.toSet()
用于将流中的元素收集到一个Set
集合中,去除重复元素。
java
List<String> words = Arrays.asList("apple", "banana", "orange", "apple", "grape");
Set<String> uniqueWords = words.stream()
.collect(Collectors.toSet());
System.out.println(uniqueWords);
上面的例子输出结果为
csharp
[banana, orange, apple, grape]
3.2.4.2 Collectors.toMap()
:
Collectors.toMap()
用于将流中的元素收集到一个Map
集合中,需要指定 key 和 value 的提取方式。
java
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
Map<Integer, String> wordLengthMap = words.stream()
.collect(Collectors.toMap(String::length, Function.identity(), (e, o) -> e));
System.out.println(wordLengthMap);
上面的例子输出结果为
ini
{5=apple, 6=banana}
- 上面的例子中,
String::length
是作为 key 提取方式,Function.identity()
是作为 value 提取方式,生成的Map
包含了单词长度和单词本身的映射。(e, o) -> e 用于处理当key 冲突的情况。
3.2.4.3 Collectors.toList()
:
Collectors.toList()
用于将流中的元素收集到一个List
集合中。
java
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
List<String> wordList = words.stream()
.collect(Collectors.toList());
这些收集器方法是常用的,根据具体需求选择适合的收集器。注意,toMap
中需要处理 key 冲突的情况,可以通过提供合并函数来解决。如果不提供合并函数,当遇到重复的 key 时会抛出 IllegalStateException
。