Lambda Expressions and Stream API
Java 一直以拥有大量样板代码而闻名。随着 Java 8 的发布,这种说法变得不那么有效了。流 API 和 lambda 表达式是使我们更接近函数式编程的新特性。
在我们的示例中,我们将看到如何在不同场景中使用 lambda 和流。
Lambda 表达式之前的世界
我们拥有一家汽车经销商业务。为了放弃所有的文书工作,我们想创建一个软件来查找所有当前可用的行驶不到 50,000 公里的汽车。
让我们看看我们如何以一种天真的方式实现这样的功能:
public class LambdaExpressions {
public static List<Car> findCarsOldWay(List<Car> cars) {
List<Car> selectedCars = new ArrayList<>();
for (Car car : cars) {
if (car.kilometers < 50000) {
selectedCars.add(car);
}
}
return selectedCars;
}
}
为了实现这一点,我们正在创建一个接受汽车列表的静态函数。它应该根据指定的条件返回一个过滤列表。
Using a Stream and a Lambda Expression
我们遇到了与上一个示例相同的问题。 我们的客户希望找到所有具有相同标准的汽车。
Let us see a solution where we used the stream API and lambda expression:
public class LambdaExpressions {
public static List<Car> findCarsUsingLambda(List<Car> cars) {
return cars.stream().filter(car -> car.kilometers < 50000)
.collect(Collectors.toList());
}
}
我们需要通过调用 stream() 方法将汽车列表传输到流中。在 filter() 方法中,我们正在设置我们的条件。我们正在根据所需条件评估每个条目。我们只保留那些少于 50,000 公里的条目。我们需要做的最后一件事是将其包装成一个列表。
Method Reference方法参考
Without Method Reference
我们仍然拥有一家汽车经销店,我们想打印出店里所有的汽车。为此,我们将使用方法引用。
方法引用允许我们使用一种特殊的语法:: 来调用类中的函数。有四种方法引用:
- Reference to a static method 引用静态方法
- Reference to an instance method on a object 引用对象上的实例方法
- Reference to an instance method on a type. 引用类型上的实例方法
- Reference to a constructor. 对构造函数的引用
让我们看看如何使用标准方法调用来做到这一点:
public class MethodReference {
List<String> withoutMethodReference =
cars.stream().map(car -> car.toString())
.collect(Collectors.toList());
}
我们使用 lambda 表达式在每辆车上调用 toString() 方法。
Using a Method Reference
Now, let us see how to use a method reference in the same situation:
public class MethodReference {
List<String> methodReference = cars.stream().map(Car::toString)
.collect(Collectors.toList());
}
我们再次使用 lambda 表达式,但现在我们通过方法引用调用 toString() 方法。我们可以看到它是如何更简洁、更易于阅读的。
Default Methods默认方法
让我们想象一下,我们有一个简单的方法 log(String message),它在调用时打印日志消息。我们意识到我们希望为消息提供时间戳,以便轻松搜索日志。我们不希望我们的客户在我们引入此更改后中断。我们将使用接口上的默认方法实现来做到这一点。
默认方法实现是允许我们创建接口方法的后备实现的功能。
Use Case
Let us see how our contract looks: 让我们看看我们的合约是怎样的:
public class DefaultMethods {
public interface Logging {
void log(String message);
}
public class LoggingImplementation implements Logging {
@Override
public void log(String message) {
System.out.println(message);
}
}
}
我们正在创建一个只有一个方法的简单接口,并在 LoggingImplementation 类中实现它。
Adding New Method
我们将在接口内添加新方法。该方法接受称为日期的第二个参数,它表示时间戳。
public class DefaultMethods {
public interface Logging {
void log(String message);
void log(String message, Date date);
}
}
我们正在添加一个新方法,但并未在所有客户端类中实现它。编译器将失败并出现异常:
Class 'LoggingImplementation' must either be declared abstract
or implement abstract method 'log(String, Date)' in 'Logging'`.
Using Default Methods
在接口内添加新方法后,我们的编译器抛出异常。我们将使用新方法的默认方法实现来解决这个问题。
Let us look at how to create a default method implementation:
public class DefaultMethods {
public interface Logging {
void log(String message);
default void log(String message, Date date) {
System.out.println(date.toString() + ": " + message);
}
}
}
放置 default 关键字允许我们在接口内添加方法的实现。现在,我们的 LoggingImplementation 类不会因编译器错误而失败,即使我们没有在其中实现这个新方法。
Type Annotations类型注释
类型注解是 Java 8 中引入的又一个特性。即使我们之前有可用的注解,现在我们可以在使用类型的任何地方使用它们。这意味着我们可以将它们用于:
- a local variable definition. 局部变量定义
- constructor calls. 构造函数调用
- type casting.
- generics. 通用类型
- throw clauses and more. throw 子句等等
然后像 IDE 这样的工具可以读取这些注释并根据注释显示警告或错误。
Local Variable Definition局部变量定义
让我们看看如何确保我们的局部变量不会以空值结束:
public class TypeAnnotations {
public static void main(String[] args) {
@NotNull String userName = args[0];
}
}
我们在这里对局部变量定义使用注释。编译时注释处理器现在可以读取 @NotNull 注释并在字符串为空时抛出错误。
Constructor Call构造函数调用
我们要确保我们不能创建一个空的 ArrayList:
public class TypeAnnotations {
public static void main(String[] args) {
List<String> request =
new @NotEmpty ArrayList<>(Arrays.stream(args).collect(
Collectors.toList()));
}
}
这是如何在构造函数上使用类型注释的完美示例。同样,注释处理器可以评估注释并检查数组列表是否不为空。
Generic Type通用类型
我们的要求之一是每封电子邮件的格式必须为 < name>@< company>.com。如果我们使用类型注解,我们可以很容易地做到这一点:
public class TypeAnnotations {
public static void main(String[] args) {
List<@Email String> emails;
}
}
这是电子邮件地址列表的定义。我们使用@Email 注释来确保此列表中的每条记录都采用所需的格式。
工具可以使用反射来评估注释并检查列表中的每个元素是否是有效的电子邮件地址。
Repeating Annotations重复注释
让我们想象一下,我们有一个完全实现了安全性的应用程序。它具有不同级别的授权。尽管我们仔细地实施了一切,但我们希望确保记录每一个未经授权的操作。对于每项未经授权的操作,我们都会向公司所有者和我们的安全管理员组电子邮件发送一封电子邮件。重复注释是我们继续这个例子的方式。
重复注释允许我们在同一个类上放置多个注释。
Creating a Repeating Annotation
例如,我们将创建一个名为 @Notify 的重复注解:
public class RepeatingAnnotations {
@Repeatable(Notifications.class)
public @interface Notify {
String email();
}
public @interface Notifications {
Notify[] value();
}
}
我们将@Notify 创建为常规注释,但我们将@Repeatable(元)注释添加到它。此外,我们必须创建一个包含 Notify 对象数组的“容器”注解 Notifications。注释处理器现在可以通过容器注释通知访问所有重复的通知注释。
请注意,这是一个模拟注释,仅用于演示目的。如果没有注释处理器读取它然后发送电子邮件,此注释将不会发送电子邮件。
Using Repeating Annotations
我们可以多次向同一个构造添加重复注释:
@Notify(email = "admin@company.com")
@Notify(email = "owner@company.com")
public class UserNotAllowedForThisActionException
extends RuntimeException {
final String user;
public UserNotAllowedForThisActionException(String user) {
this.user = user;
}
}
我们有我们的自定义异常类,每当用户尝试执行不允许用户执行的操作时,我们都会抛出该异常类。我们对这个类的注释说我们想在代码抛出这个异常时通知两封电子邮件。