将会来完整认识Lambda,如何善用Lambda。
1 Lambda语法概述
1.1 初识Lambda
在Java 8之前,经常使用的Comparator
创建匿名内部类来对集合进行排序。
String[] names = { "Justin", "caterpillar", "Bush" };
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
System.out.println(Arrays.toString(names));
不过,现在的可读性不是很强。
我们可以修改如下
String[] names = { "Justin", "caterpillar", "Bush" };
// JDK 8 之前
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
Arrays.sort(names, byLength);
System.out.println(Arrays.toString(names));
在 JDK 8 之后,我们可以使用lambda表达式。
Comparator<String> byLength2 = (String name1, String name2) -> name1.length() - name2.length();
因为从Comparator
Comparator<String> byLength2 = (name1, name2) -> name1.length() - name2.length();
最后我们是使用Lambda实现刚才那个匿名内部类的例子
String[] names = { "Justin", "caterpillar", "Bush" };
Arrays.sort(names, (name1, name2) -> name1.length() - name2.length());
System.out.println(Arrays.toString(names));
那现在问题又 来了。
假如我们有很多地方否需要比较字符串的长度,我们怎么做比较好呢?
我们先把比较长度的代码封装成一个静态 方法。
public class StringOrder {
public static int byLength(String s1, String s2) {
return s1.length() - s2.length();
}
public static int byLexicography(String s1, String s2) {
return s1.compareTo(s2);
}
public static int byLexicographyIgnoreCase(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
}
我们要怎么在Lambda表达式中引用某个方法呢。
在JDK 8 中为我们提供方法引用的 特性。我们可以使用双冒号运算符,来指定引用的方法。
比如
public class StringOrderDemo {
public static void main(String[] args) {
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLength);
System.out.println(Arrays.toString(names));
}
}
如果我们现在需要按照字典的顺序进行排序。
我们可以直接使用byLexicography方法
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, StringOrder::byLexicography);
System.out.println(Arrays.toString(names));
如果你仔细观察的话,你会发现byLexicography使用的是compareTo方法。
那我可不可直接使用 String的compare方法呢?
String[] names = {"Justin", "caterpillar", "Bush"};
Arrays.sort(names, String::compareTo);
System.out.println(Arrays.toString(names));
方法引用不仅可以避免重复的写Lambda表达式,也可以让代码更为清楚。
下面我们来看一下更多的细节。
1.2 Lambda 表达式的结构
让我们了解一下 Lambda 表达式的结构。
- 一个 Lambda 表达式可以有零个或多个参数
- 参数的类型既可以明确声明,也可以根据上下文来推断。例如:
(int a)
与(a)
效果相同 - 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:
(a, b)
或(int a, int b)
或(String a, int b, float c)
- 空圆括号代表参数集为空。例如:
() -> 42
- 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:
a -> return a*a
- Lambda 表达式的主体可包含零条或多条语句
- 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致
- 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空
2 函数式接口
2.1 首选标准函数式接口
在java.util.function包中的函数接口满足了大多数开发人员为lambda表达式和方法引用提供目标类型的需求。 这些接口中的每一个都是通用的和抽象的,使它们很容易适应几乎任何lambda表达式。 开发人员应在创建新的功能接口之前查看此包。
比如一个接口Foo:
@FunctionalInterface
public interface Foo {
String method(String string);
}
假如我们在一个方法中使用这个接口
public String add(String string, Foo foo) {
return foo.method(string);
}
我们可以这样使用
Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);
仔细观察,你会发现Foo只不过是一个接受一个参数并产生结果的函数。 Java 8已经在java.util.function
包中的Function <T,R>
中提供了这样的接口。
现在我们可以完全删除接口Foo
并将我们的代码更改为:
public String add2(String string, Function<String, String> fn) {
return fn.apply(string);
}
然后我们可以这样使用
Function<String, String> fn = parameter -> parameter + " from lambda";
String result = useFoo.add2("Message ", fn);
2.2 使用@FunctionalInterface注释
使用@FunctionalInterface
注释函数式接口并不是必须的。 即使没有它,只要它是一个抽象方法,你的接口也可以被视为函数式接口。但是要求必须只有一个抽象方法。
可以想象一个有很多接口的大项目 - 很难手动控制一切。 设计为函数式接口可能会通过添加其他抽象方法/默认方法而意外的更改,从而使其无法用作函数式接口。
但是使用@FunctionalInterface
注释,编译器将触发错误以响应任何破坏函数式接口的预定义结构的尝试。 它也是一个非常方便的工具,使您的应用程序架构更易于理解为其他开发人员。
所以,推荐使用这个:
@FunctionalInterface
public interface Foo {
String method();
}
而不是这个
public interface Foo {
String method();
}
2.3 不要在函数式接口中过度使用默认方法
我们可以将默认方法添加到函数式接口。 只要只有一个抽象方法声明,这对于函数式接口是没有问题的:
@FunctionalInterface
public interface Foo {
String method();
default void defaultMethod() {}
}
如果功能接口的抽象方法具有相同的签名,则可以通过其他功能接口进行扩展。 例如:
@FunctionalInterface
public interface FooExtended extends Baz, Bar {}
@FunctionalInterface
public interface Baz {
String method();
default void defaultBaz() {}
}
@FunctionalInterface
public interface Bar {
String method();
default void defaultBar() {}
}
与常规接口一样,使用相同的默认方法扩展不同的功能接口可能会有问题。 例如,假设接口Bar和Baz都有默认方法defaultCommon()
。 在这种情况下,您将收到编译时错误:
Error:(3, 8) java: 类型 com.ripjava.java.core.function.Baz 和 com.ripjava.java.core.function.Bar 不兼容;
接口 com.ripjava.java.core.function.FooExtended从类型 com.ripjava.java.core.function.Baz 和 com.ripjava.java.core.function.Bar 中继承了defaultCommon() 的不相关默认值
要解决此问题, 可以在FooExtended接口中重写defaultCommon()
方法。 来提供此方法的自定义实现。 但是如果你想使用其中一个父接口的实现(例如,从Baz接口),请将以下代码行添加到defaultCommon()
方法的主体:
@FunctionalInterface
public interface FooExtended extends Baz, Bar {
default void defaultCommon() {
Baz.super.defaultCommon();
}
}
但是向接口中添加太多默认方法不是一个很好的架构设计。 应将其视为折衷方案,仅在需要时使用,以便在不破坏向后兼容性的情况下升级现有接口。
2.4 使用Lambda表达式实例化函数式接口
编译器允许我们使用内部类来实例化函数式接口。 但是,这可能会导致代码非常冗长。 我们应该使用lambda表达式:
- lambda表达式
Foo foo = parameter -> parameter + " from Foo";
- 内部类
Foo fooByIC = new Foo() {
@Override
public String method(String string) {
return string + " from Foo";
}
};
lambda表达式方法可用于Java 8 之前的任何合适的接口。比如Runnable,Comparator等接口。 但是,这并不意味着您应该检查整个旧代码库并更改所有内容。
2.6 避免使用功能接口作为参数的重载方法
使用具有不同名称的方法以避免冲突; 让我们看一个例子:
public interface Processor {
String process(Callable<String> c) throws Exception;
String process(Supplier<String> s);
}
public class ProcessorImpl implements Processor {
@Override
public String process(Callable<String> c) throws Exception {
// 具体实现
return null;
}
@Override
public String process(Supplier<String> s) {
// 具体实现
return null;
}
}
乍一看,这似乎是合理的。 但尝试执行任何ProcessorImpl的方法:
processor.process(()-> "test");
编译器就会向我们提示:
Error:(18, 18) java: 对process的引用不明确
com.ripjava.java.core.function.Processor 中的方法 process(java.util.concurrent.Callable<java.lang.String>) 和 com.ripjava.java.core.function.Processor 中的方法 process(java.util.function.Supplier<java.lang.String>) 都匹配
要解决这个问题,我们有两个选择。
- 第一种是使用具有不同名称的方法:
String processWithCallable(Callable<String> c) throws Exception;
String processWithSupplier(Supplier<String> s);
- 第二种是手动转型。 这不是一个好的选择。
processor.process((Callable<String>) ()-> "test");
2.7 不要将Lambda表达式当成内部类
尽管我们之前的示例,我们基本上用lambda表达式替换内部类,但这两个概念在一个重要方面是不同的:范围。
使用内部类时,它会创建一个新范围。 您可以通过实例化具有相同名称的新局部变量来隐藏封闭范围中的局部变量。 您还可以在内部类中使用关键字this作为其实例的引用。
但是,lambda表达式适用于封闭范围。 无法隐藏lambda体内封闭范围内的变量。 在这种情况下,关键字this是对封闭实例的引用。
例如,在UseFoo类中,有一个实例变量值:
private String value = "Enclosing scope value";
然后在此类的某些方法中放置以下代码并执行此方法。
public String scopeExperiment() {
Foo fooIC = new Foo() {
String value = "Inner class value";
@Override
public String method(String string) {
return this.value;
}
};
String resultIC = fooIC.method("");
Foo fooLambda = parameter -> {
String value = "Lambda value";
return this.value;
};
String resultLambda = fooLambda.method("");
return "Results: resultIC = " + resultIC +
", resultLambda = " + resultLambda;
}
执行的结果如下
Results: resultIC = Inner class value, resultLambda = Enclosing scope value
通过在IC中调用this.value
,您可以从其实例访问本地变量。 但是在lambda的情况下,this.value
调用使您可以访问在UseFoo
类中定义的变量值而不能访问在lambda体内定义的变量值。
2.8 保持Lambda表达式简短和意义明了
如果可能,请使用一行结构而不是大块代码。 记住lambda应该是一种表达,而不是一种叙述。 尽管语法简洁,但lambdas应该精确地表达它们提供的功能。
这主要是风格建议,因为性能不会发生剧烈变化。 但是,一般而言,理解和使用此类代码要容易得多。
这可以通过多种方式实现 。
2.8.1 避免Lambda体内的代码块
在理想情况下,lambda应该用一行代码编写。 使用这种方法,lambda是一个不言自明的结构,它声明应该用什么数据执行什么操作(在带有参数的lambdas的情况下)。
如果你有一大块代码,那么lambda的功能就不会立即清楚了。
考虑到这一点,我们来比较一下下面两种写法
- 通过定义方法,保持lambda表达式简洁
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
- 全部写在lambda表达式中
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};
但是,请不要使用这个“单行lambda”规则作为教条。 如果lambda的定义中有两行或三行,那么将该代码提取到另一个方法中可能没有用。
2.8.2 避免指定参数类型
在大多数情况下,编译器能够借助类型推断来解析lambda参数的类型。 因此,向参数添加类型是可选的,可以省略。
我们不用去指定参数的类型 。
推荐这个做
(a, b) -> a.toLowerCase() + b.toLowerCase();
而不是
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
2.8.3 避免在单个参数时使用括号
Lambda语法仅在多个参数周围或者根本没有参数时需要括号。 这就是为什么可以安全地使代码更短,并在只有一个参数时排除括号。
所以,当只有一个参数时,推荐不使用括号
a -> a.toLowerCase();
而不是
(a) -> a.toLowerCase();
2.8.4 避免返回语句和花括号
在一行lambda体中,花括号和返回语句是可选的。 这意味着,为了清晰和简洁,可以省略它们。
我们应该这样做
a -> a.toLowerCase();
而不是
a -> {return a.toLowerCase()};
2.8.5 使用方法
通常,即使在我们之前的示例中,lambda表达式也只调用已在其他地方实现的方法。 在这种情况下,使用另一个Java 8功能非常有用:方法引用。
所以,lambda表达式
a -> a.toLowerCase();
可以替换为
String::toLowerCase;
上面的原则并不总是为了代码短,而是使代码更具可读性。
2.9 使用final变量
在lambda表达式中访问非final变量将导致编译时错误。 但这并不意味着您应该将每个目标变量标记为final变量。
根据“有效最终”概念,编译器将每个变量视为final,只要它只被赋值一次即可。
在lambdas中使用这些变量是安全的,因为编译器将控制它们的状态并在任何尝试更改它们之后立即触发编译时错误。
例如,以下代码将无法编译(可能不太恰当):
public void method() {
String localVariable = "Local";
Foo foo = parameter -> {
String localVariable = parameter;
return localVariable;
};
}
会提示如下的错误
Error:(40, 20) java: 已在方法 method()中定义了变量 localVariable
这种方法应该简化使lambda执行线程安全的过程。
2.10 保护对象变量不被修改
lambdas的主要目的之一是用于并行计算 - 这意味着它们在线程安全方面非常有用。
“有效最终”范式在这里有很大帮助,但并非在每种情况下都有。 Lambdas无法从封闭范围更改对象的值。 但是在可变对象变量的情况下,可以在lambda表达式中更改状态。
请考虑以下代码:
int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();
这段代码是合法的,因为总变量仍然是“有效的最终”。
但它执行lambda后它引用的对象是否具有相同的状态? 不相同!
此示例仅作为提醒,以避免可能导致意外突变的代码。
3. Streams
3.1 Stream API
Java 8的一个主要新功能是引入了Stream功能 - java.util.stream--它包含用于处理元素序列的类。
最主要的API类是Stream <T>
。
以下部分将演示如何使用现有数据创建Stream。
3.2 创建Stream
3.2.1 Stream Creation
可以从不同的元素源创建Stream,例如 使用stream()
和of()
方法的从集合或数组创建Stream:
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("a", "b", "c");
stream()
默认方法被添加到Collection接口,允许使用任何集合作为元素源创建Stream <T>
:
Stream<String> stream = list.stream();
3.2.2. 使用Stream进行多线程处理
Stream API还通过提供parallelStream()
方法简化多线程处理,该方法在并行模式下对Stream的元素进行操作。
下面的代码允许为Stream的每个元素并行运行方法doWork()
:
list.parallelStream().forEach(element -> doWork(element));
在下一节中,我们将介绍一些基本的Stream API操作。
3.3. Stream操作
可以在流上执行许多有用的操作。
它们分为
- 中间操作(返回Stream ) 中间操作允许链接。
- 终端操作(返回确定类型的结果)
值得注意的是,对Stream的操作不会改变元素源。
这是一个简单的例子:
long count = list.stream().distinct().count();
distinct()
方法表示一个中间操作,它创建前一个Stream的唯一元素的新Stream。 count()
方法是一个终端操作,它返回流的大小。
3.3.1 迭代
Stream API有助于替换for-each
和while
循环。
它帮助我们专注于操作的逻辑,而不是集中在元素序列上的迭代。
例如:
for (String string : list) {
if (string.contains("a")) {
return true;
}
}
使用Stream之后只需要一行:
boolean isExist = list.stream().anyMatch(element -> element.contains("a"));
3.3.2 过滤
filter()
方法允许我们选择满足谓词的Stream。
例如,请考虑以下列表:
ArrayList<String> list = new ArrayList<>();
list.add("One");
list.add("OneAndOnly");
list.add("Derek");
list.add("Change");
list.add("china");
list.add("justBefore");
list.add("Italy");
list.add("Italy");
list.add("Thursday");
list.add("");
list.add("");
下面的代码创建List <String>
的Stream <String>
,查找包含“d”
的所有元素,并创建仅包含已过滤元素的新流
Stream<String> stream = list.stream().filter(element -> element.contains("d"));
3.3.3 映射(Mappings)
如果我们需要通过使用应用特殊函数来转换Stream的元素并将这些新元素收集到Stream中,我们可以使用map()
方法:
List<String> uris = new ArrayList<>();
uris.add("C:\\tet\My.txt");
Stream<Path> stream = uris.stream().map(uri -> Paths.get(uri));
上面的代码通过将特定的lambda表达式应用于Stream的每个元素,将Stream <String>
转换为Stream <Path>
。
如果你有一个流,其中每个元素都包含自己的元素序列,并且你想创建这些内部元素的Stream,你应该使用flatMap()
方法:
List<Detail> details = new ArrayList<>();
details.add(new Detail());
Stream<String> stream
= details.stream().flatMap(detail -> detail.getParts().stream());
在这个示例中,我们有一个Detail
类型的元素列表。 Detail类包含字段PARTS
,它是List 。 在flatMap()
方法的帮助下,将提取字段PARTS
中的每个元素并将其添加到新的结果Stream中。 然后,初始的Stream 就会被丢弃。
3.4 匹配
Stream API提供了一组便捷的工具,可根据某些谓词验证Stream中的元素。
为此,可以使用以下方法之一:
- anyMatch()
- allMatch()
- noneMatch()
意思和他们的名字是一直的。 都是返回布尔值的终端操作。
boolean isValid = list.stream().anyMatch(element -> element.contains("h")); // true
boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); // false
boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); // false
3.5 Reduction
Stream API允许借助Stream类型的reduce()
方法根据指定的函数将元素序列累积到某个值。 此方法有两个参数:
- 第一个 - 起始值
- 第二个 - 累加器函数。
想象一下,你有一个List <Integer>
,你希望得到所有这些元素和一些初始Integer的总和。
因此,您可以运行以下代码,结果将是26(23 + 1 + 1 + 1)。
List<Integer> integers = Arrays.asList(1, 1, 1);
Integer reduced = integers.stream().reduce(23, (a, b) -> a + b);
3.6 Collecting
我们还可以通过Stream类型的collect()
方法来提供。 将Stream转换为Collection或Map并以单个字符串的形式表示Stream的情况下,此操作非常方便。
Java为我们提供了一个收集器工具类(Collectors
),它为几乎所有典型的收集操作提供解决方案。
对于某些,而不是简单的任务,可以创建自定义收集器。
List<String> resultList
= list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());
这段代码使用终端collect()
操作将Stream <String>
转换为List <String>
。