Java17 教程·续(三)
原文:More Java 17
三、泛型
在本章中,您将学习:
-
什么是泛型
-
如何定义泛型类型、方法和构造函数
-
如何定义类型参数的界限
-
如何使用通配符作为实际的类型参数
-
编译器如何推断泛型类型使用的实际类型参数
-
泛型及其在数组创建中的局限性
-
泛型的不正确使用如何导致堆污染
本章中的所有示例程序都是清单 3-1 中声明的jdojo.generics模块的成员。
// module-info.java
module jdojo.generics {
exports com.jdojo.generics;
}
Listing 3-1The Declaration of a jdojo.generics Module
什么是泛型?
泛型让你可以编写真正的多态代码,可以处理任何类型的代码。
在我定义什么是泛型以及它们能为你做什么之前,让我们讨论一个简单的例子。假设您想要创建一个新类,它的唯一工作是存储对任何类型的引用,其中“任何类型”意味着任何引用类型。让我们称这个类为ObjectWrapper,如清单 3-2 所示。
// ObjectWrapper.java
package com.jdojo.generics;
public class ObjectWrapper {
private Object ref;
public ObjectWrapper(Object ref) {
this.ref = ref;
}
public Object get() {
return ref;
}
public void set(Object ref) {
this.ref = ref;
}
}
Listing 3-2A Wrapper Class to Store a Reference of Any Type
作为一名 Java 开发人员,您会同意在不知道必须处理的对象类型时编写这种代码。ObjectWrapper类可以存储 Java 中任何类型的引用,比如String、Integer、Person等。你如何使用ObjectWrapper类?以下是使用它与String型配合工作的方法之一:
ObjectWrapper stringWrapper = new ObjectWrapper("Hello");
stringWrapper.set("Another string");
String myString = (String) stringWrapper.get();
这段代码中有一个问题。即使您知道您在stringWrapper对象中存储了一个字符串,您也必须将get()方法的返回值转换为(String) stringWrapper.get()中的String类型。考虑编写以下代码片段:
ObjectWrapper stringWrapper = new ObjectWrapper("Hello");
stringWrapper.set(new Integer(101));
String myString =(String) stringWrapper.get();
这段代码可以很好地编译。但是,第三条语句在运行时抛出了一个ClassCastException,因为您在第二条语句中存储了一个Integer,并试图在第三条语句中将一个Integer转换为一个String。首先,它允许您在stringWrapper中存储一个Integer。其次,它没有抱怨第三条语句中的代码,因为它不知道您的意图,即您只想将String与stringWrapper一起使用。
Java 在帮助开发人员编写类型安全程序方面取得了一些进展。如果ObjectWrapper类允许你指定只对一个特定的类型使用这个类,比如这次使用String而下一次使用Integer,这不是很好吗?Java 中的泛型实现了你的愿望。它们允许您用类型(类或接口)指定类型参数。这种类型称为泛型类型(更确切地说是泛型类或泛型接口)。当您声明泛型类型的变量并创建泛型类型的对象时,可以指定类型参数值。您已经看到了为方法指定参数。这一次,我讨论的是为类或接口等类型指定参数。
Note
声明中带有类型参数的类型称为泛型类型。
让我们重写ObjectWrapper类来使用泛型,简单地命名新类为Wrapper。泛型类型的形参在泛型类型的声明中指定。参数名是有效的 Java 标识符,在参数化类型名后面的尖括号(< >)中指定。您将使用T作为Wrapper类的类型参数名称:
public class Wrapper<T> {
}
类型参数名是一个字符,用T表示参数是类型,E表示参数是元素,K表示参数是键,N表示参数是数字,V表示参数是值,这是一个不成文的约定。在前面的示例中,您可以为类型参数使用任何名称,如下所示:
public class Wrapper<Hello> {
}
public class Wrapper<MyType> {
}
多个类型参数由逗号分隔。下面的MyClass声明采用了四个类型参数,分别为T、U、V和W:
public class MyClass<T, U, V, W> {
}
您将在实例变量声明、构造函数、get()方法和set()方法的类代码中使用名为T的类型参数。现在,T对你来说意味着任何类型,当你使用这个类的时候就知道了。清单 3-3 包含了Wrapper类的完整代码。
// Wrapper.java
package com.jdojo.generics;
public class Wrapper<T> {
private T ref;
public Wrapper(T ref) {
this.ref = ref;
}
public T get() {
return ref;
}
public void set(T ref) {
this.ref = ref;
}
}
Listing 3-3Using a Type Parameter to Define a Generic Class
你对在清单 3-3 中使用T感到困惑吗?这里,T表示任何类类型或接口类型。可能是String、Object、com.jdojo.generics.Person等。如果您在这个程序中处处用Object替换T,并从类名中删除<T>,它就是您拥有的ObjectWrapper class的相同代码。
如何使用Wrapper类?由于它的类名不仅仅是Wrapper,而是Wrapper<T>,您可以指定(但不是必须)为T赋值。要在Wrapper对象中存储一个String引用,您需要如下创建它:
Wrapper<String> greetingWrapper =
new Wrapper<String>("Hello");
你如何使用Wrapper类的set()和get()方法?因为您已经将类Wrapper<T>的类型参数指定为String,所以set()和get()方法将只适用于String类型。这是因为您在set()方法中使用了T作为参数类型,在get()方法声明中使用了T作为返回类型。想象一下用String替换类定义中的T,理解下面的代码应该没有问题:
greetingWrapper.set("Hi");
// <- OK to pass a String
String greeting = greetingWrapper.get();
// <- No need to cast
这一次,您不必强制转换get()方法的返回值。编译器知道greetingWrapper已经被声明为类型Wrapper<String>,所以它的get()方法返回一个String。让我们试着在greetingWrapper中存储一个Integer对象:
// A compile-time error. You can use greetingWrapper
// only to store a String.
greetingWrapper.set(new Integer(101));
该语句将生成以下编译时错误:
error: incompatible types: Integer cannot be converted to
String
greetingWrapper.set(new Integer(101));
您不能将Integer传递给set()方法。编译器将生成一个错误。如果您想使用Wrapper类来存储一个Integer,您的代码将如下所示:
Wrapper<Integer> idWrapper =
new Wrapper<Integer>(new Integer(101));
idWrapper.set(new Integer(897));
// <- OK to pass an Integer
Integer id = idWrapper.get();
// A compile-time error. You can use idWrapper only
// with an Integer.
idWrapper.set("hello");
假设存在一个包含带两个参数的构造函数的Person类,您将一个Person对象存储在Wrapper中,如下所示:
Wrapper<Person> personWrapper = new Wrapper<Person>(
new Person(1, "Chris"));
personWrapper.set(new Person(2, "Laynie"));
Person laynie = personWrapper.get();
在类型声明中指定的参数称为形参;例如,T是Wrapper<T>类声明中的一个形式类型参数。当你用实际类型替换形式类型参数时(例如,在Wrapper<String>中,你用String替换形式类型参数T,它被称为参数化类型。Java 中的引用类型接受一个或多个类型参数,称为泛型类型。泛型类型主要是在编译器中实现的。JVM 不了解泛型类型。所有实际的类型参数都在编译时通过一个称为擦除的过程被擦除。编译时类型安全是您在代码中使用参数化泛型类型而无需使用强制转换时获得的好处。
多态性是指根据一种类型编写代码,这种类型也适用于许多其他类型。在任何一本关于 Java 的入门书籍中,您都会学到如何使用继承和接口编写多态代码。Java 中的继承提供了包含多态性,您可以根据基本类型编写代码,代码也可以处理该基本类型的所有子类型。在这种情况下,您必须将所有其他类型归入一个继承层次结构中。也就是说,多态代码工作的所有类型都必须从单个基类型继承。Java 中的接口解除了这一限制,允许您根据接口编写代码。代码适用于实现接口的所有类型。这一次,代码适用的所有类型不必都属于一个类型层次结构。但是,您有一个约束,即所有这些类型必须实现相同的接口。Java 中的泛型让你离编写“真正的”多态代码更近了一步。使用泛型编写的代码适用于任何类型。Java 中的泛型对于在代码中如何处理泛型确实有一些限制。本章讨论的主题是向您展示在 Java 中使用泛型可以做些什么,以及详细说明这些限制。
超类型-子类型关系
让我们玩一个把戏。下面的代码创建了Wrapper<T>类的两个参数化实例,一个用于String类型,一个用于Object类型:
Wrapper<String> stringWrapper =
new Wrapper<String>("Hello");
stringWrapper.set("a string");
Wrapper<Object> objectWrapper =
new Wrapper<Object>(new Object());
objectWrapper.set(new Object());
// Use a String object with objectWrapper
objectWrapper.set("a string"); // OK
在objectWrapper中存储一个String对象就可以了。毕竟,如果你打算在objectWrapper中存储一个Object,一个String也是一个Object。允许以下作业吗?
objectWrapper = stringWrapper;
不,这个作业是不允许的。也就是说,Wrapper<String>与Wrapper<Object>的赋值不兼容。为了理解为什么不允许这种赋值,让我们假设它是允许的,您可以编写如下代码:
// Now objectWrapper points to stringWrapper
objectWrapper = stringWrapper;
// We could store an Object in stringWrapper using
// objectWrapper
objectWrapper.set(new Object());
// The following statement will throw a runtime
// ClassCastException
String s = stringWrapper.get();
你看到允许像objectWrapper= stringWrapper这样的任务的危险了吗?如果允许这种赋值,编译器不能确保stringWrapper只存储一个String类型的引用。
记住,a String是一个Object,因为String是Object的子类。然而,Wrapper<String>并不是Wrapper<Object>。普通的超类型/子类型规则不适用于参数化类型。如果你不理解,不要担心记住这条规则。如果你尝试这样的赋值,编译器会告诉你不可以。
原始类型
Java 中泛型类型的实现是向后兼容的。如果现有的非泛型类被重写以利用泛型,使用该类的非泛型版本的现有代码应该继续工作。代码可以通过省略对泛型类型参数的引用来使用(尽管不推荐)泛型类的非泛型版本。泛型类型的非泛型版本称为原始类型。不鼓励使用原始类型。如果在代码中使用原始类型,编译器将生成未检查的警告,如下面的代码片段所示:
// Use the Wrapper<T> generic type as a raw type Wrapper
Wrapper rawType = new Wrapper("Hello"); // An unchecked
// warning
// Using the Wrapper<T> generic type as a parameterized
// type Wrapper<String>
Wrapper<String> genericType = new Wrapper<String>("Hello");
// Assigning the raw type to the parameterized type
genericType = rawType; // An unchecked warning
// Assigning the parameterized type to the raw type
rawType = genericType;
编译这段代码时,编译器会生成以下警告:
warning: [unchecked] unchecked call to Wrapper(T) as a
member of the raw type Wrapper
Wrapper rawType = new Wrapper("Hello");
^
where T is a type-variable:
T extends Object declared in class Wrapper
warning: [unchecked] unchecked conversion
genericType = rawType;
^
required: Wrapper<String>
found: Wrapper
2 warnings
无限通配符
先说个例子。它将帮助您理解在泛型类型中使用通配符的必要性。让我们为Wrapper类构建一个实用程序类,并将其命名为WrapperUtil。向该类添加一个名为printDetails()的静态实用方法,它将接受一个Wrapper<T>类的对象。你应该如何定义这个方法的参数?以下是第一次尝试:
public class WrapperUtil {
public static
void printDetails(Wrapper<Object> wrapper){
// More code goes here
}
}
既然你的printDetails()方法应该打印任何类型的Wrapper<T>的细节,那么Object作为类型参数似乎更合适。让我们使用你的新printDetails()方法,如图所示:
Wrapper<Object> objectWrapper =
new Wrapper<Object>(new Object());
WrapperUtil.printDetails(objectWrapper); // OK
Wrapper<String> stringWrapper =
new Wrapper<String>("Hello");
WrapperUtil.printDetails(stringWrapper); // A compile-time
// error
编译时错误如下:
error: method printDetails in class WrapperUtil cannot be
applied to given types;
WrapperUtil.printDetails(stringWrapper);
^
required: Wrapper<Object>
found: Wrapper<String>
reason: argument mismatch; Wrapper<String> cannot be
converted to Wrapper<Object>
1 error
你可以用Wrapper<Object>类型调用printDetails()方法,但不能用Wrapper<String>类型,因为它们不是赋值兼容的,这与你的直觉相矛盾。要完全理解它,您需要了解泛型中的通配符类型。通配符类型用问号表示,如在<?>中。对于泛型类型,通配符类型就像Object类型对于原始类型一样。您可以将已知类型的类属指定给通配符类型的类属。下面是示例代码:
// Wrapper of String type
Wrapper<String> stringWrapper = new Wrapper<String>("Hi");
// You can assign a Wrapper<String> to Wrapper<?> type
Wrapper<?> wildCardWrapper = stringWrapper;
通配符泛型类型中的问号(例如,<?>)表示未知类型。当您使用通配符(表示未知)作为参数类型来声明参数化类型时,这意味着它不知道自己的类型:
// wildCardWrapper has unknown type
Wrapper<?> wildCardWrapper;
// Better to name it as an unknownWrapper
Wrapper<?> unknownWrapper;
可以创建一个未知类型的Wrapper<T>对象吗?让我们假设约翰为你做饭。他把食物打包,然后交给你。你把包交给唐娜。唐娜问你包里是什么。你的答案是你不知道。约翰能像你一样回答吗?不。他一定知道他做了什么,因为他是做饭的人。即使你不知道包里装的是什么,你也可以毫不费力地拿着它交给唐娜。如果唐娜让你给她包装里的蔬菜,你会怎么回答?你会说你不知道蔬菜是否在包装里。
以下是使用通配符(未知)泛型类型的规则。因为它不知道自己的类型,所以不能用它来创建未知类型的对象。以下代码是非法的:
// Cannot use <?> with new operator. It is a compile-time
// error.
new Wrapper<?>("");
error: unexpected type
new Wrapper<?>("");
^
required: class or interface without bounds
found: ?
1 error
当您拿着未知食物类型的包时(John 在烹饪食物时知道食物的类型),通配符泛型类型可以引用已知的泛型类型对象,如下所示:
Wrapper<?> unknownWrapper = new Wrapper<String>("Hello");
关于通配符泛型类型引用可以对对象做什么,有一个复杂的规则列表。然而,有一个简单的经验法则需要记住。使用泛型的目的是获得编译时类型安全。只要编译器确信该操作在运行时不会产生任何意外的结果,它就允许对通配符泛型类型引用执行该操作。
让我们将经验法则应用于您的unknownWrapper参考变量。这个unknownWrapper变量可以确定的一件事是,它引用了一个已知类型的Wrapper<T>类的对象。但是,它不知道已知的类型是什么。可以用下面的get()方法吗?以下语句会生成编译时错误:
String str = unknownWrapper.get();
error: incompatible types: CAP#1 cannot be converted
to String
String str = unknownWrapper.get();
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error
编译器知道Wrapper<T>类的get()方法返回一个T类型的对象。然而,对于unknownWrapper变量,类型T未知。因此,编译器不能确保方法调用unknownWrapper.get()将返回一个String,并且在运行时它对str变量的赋值是正确的。你所要做的就是让编译器相信这个赋值不会在运行时抛出一个ClassCastException。下面一行代码会编译吗?
Object obj = unknownWrapper.get(); // OK
这段代码会编译,因为编译器确信这条语句不会在运行时抛出ClassCastException。它知道get()方法返回一个类型的对象,而这个类型对于unknownWrapper变量来说是未知的。无论get()方法返回什么类型的对象,它总是与Object类型赋值兼容。毕竟,Java 中的所有引用类型都是Object类型的子类型。下面的代码片段可以编译吗?
unknownWrapper.set("Hello"); // A compile-time error
unknownWrapper.set(new Integer()); // A compile-time error
unknownWrapper.set(new Object()); // A compile-time error
unknownWrapper.set(null); // OK
您对这段代码中的错误感到惊讶吗?你会发现这并不像看起来那么令人惊讶。set(T a)方法接受泛型类型参数。这个类型T对于unknownWrapper来说是未知的,因此编译器不能确定这个未知类型是String类型、Integer类型还是Object类型。这就是为什么对set()的前三次调用会被编译器拒绝。为什么第四次调用set()方法是正确的?在 Java 中,null对任何引用类型都是赋值兼容的。编译器认为,无论set(T a)方法中的T是什么类型,对于unknownWrapper引用变量所指向的对象,使用null总是安全的。下面是你的printDetails()方法的代码。如果您将一个null Wrapper对象传递给这个方法,它将抛出一个NullPointerException:
public class WrapperUtil {
public static void printDetails(Wrapper<?> wrapper) {
// Can assign get() return value to an Object
Object value = wrapper.get();
String className = null;
if (value != null) {
className = value.getClass().getName();
}
System.out.println("Class: " + className);
System.out.println("Value: " + value);
}
}
Note
仅使用问号作为参数类型(<?>)被称为无界通配符。它没有限制可以引用什么类型。您还可以使用通配符设置上限或下限。我将在接下来的两节中讨论有界通配符。
上限通配符
假设您想向您的WrapperUtil类添加一个方法。该方法应该接受包装在您的Wrapper对象中的两个数字,并将返回它们的总和。被包裹的物体可以是Integer、Long、Byte、Short、Double或Float。您的第一个尝试是编写如下所示的sum()方法:
public static double sum(Wrapper<?> n1, Wrapper<?> n2) {
//Code goes here
}
这个方法签名有一些明显的问题。参数n1和n2可以是Wrapper<T>类的任何参数化类型。例如,下面的调用将是对sum()方法的有效调用:
// Try adding an Integer and a String
sum(new Wrapper<Integer>(new Integer(125)),
new Wrapper<String>("Hello"));
计算一个Integer和一个String的和是没有意义的。然而,代码会编译,你应该准备好根据sum()方法的实现得到一些运行时异常。您必须限制这种代码的编译。它应该接受两个Number类型的Wrapper对象或者它的子类,而不仅仅是任何东西。因此,您知道了Wrapper对象应该具有的实际参数类型的上限。上限是Number型。如果您传递任何其他类型,它是Number类型的一个子类,那就没问题。然而,任何不是Number类型或其子类类型的东西都应该在编译时被拒绝。您可以将通配符的上限表示为
<? extends T>
这里,T是一个类型。<?扩展T>意味着任何类型T或其子类是可接受的。使用你的上限作为Number,你可以定义你的方法为
public static double sum(Wrapper<? extends Number> n1,
Wrapper<? extends Number> n2) {
Number num1 = n1.get();
Number num2 = n2.get();
double sum = num1.doubleValue() + num2.doubleValue();
return sum;
}
方法中的以下代码片段编译正常:
Number num1 = n1.get();
Number num2 = n2.get();
无论你为n1和n2传递什么,它们总是与Number的赋值兼容,因为编译器会确保传递给sum()方法的参数遵循其<? extends Number>声明中指定的规则。试图计算一个Integer和一个String的和将被编译器拒绝。考虑以下代码片段:
Wrapper<Integer> intWrapper =
new Wrapper<Integer>(new Integer(10));
Wrapper<? extends Number> numberWrapper = intWrapper;
// <- OK
numberWrapper.set(new Integer(1220));
// <- A compile-time error
numberWrapper.set(new Double(12.20));
// <- A compile-time error
你能找出这段代码的问题吗?numberWrapper的类型是<? extends Number>,这意味着它可以引用(或者是赋值兼容的)任何属于Number类的子类型的东西。由于Integer是Number的子类,所以允许将intWrapper赋值给numberWrapper。当你试图在numberWrapper上使用set()方法时,编译器开始抱怨,因为它不能在编译时确定numberWrapper是Integer或Double的类型,它们是Number的子类型。使用泛型时,要小心这种编译时错误。从表面上看,这对您来说可能是显而易见的,您会认为代码应该编译和运行良好。除非编译器确保该操作是类型安全的,否则它不会允许您继续操作。毕竟,编译时和运行时类型安全是泛型的主要目标!
下界通配符
指定下限通配符与指定上限通配符相反。使用下界通配符的语法是<? super T>,这意味着“任何是 t 的超类型的东西”。您将调用新方法copy(),它将把值从源包装器对象复制到目标包装器对象。这是第一次尝试。<T>是copy()方法的形式类型参数。它指定 source 和 dest 参数必须是同一类型。我将在下一节详细解释泛型方法。
public class WrapperUtil {
public static <T> void
copy(Wrapper<T> source, Wrapper<T> dest) {
T value = source.get();
dest.set(value);
}
}
使用您的copy()方法将Wrapper<String>的内容复制到Wrapper<Object>将不起作用:
Wrapper<Object> objectWrapper =
new Wrapper<Object>(new Object());
Wrapper<String> stringWrapper =
new Wrapper<String>("Hello");
WrapperUtil.copy(stringWrapper, objectWrapper);
// <- A compile-time error
这段代码将生成一个编译时错误,因为copy()方法要求source和dest参数是同一类型。然而,实际上,String永远是Object。这里,您需要使用下界通配符,如下所示:
public class WrapperUtil {
// New definition of the copy() method
public static <T> void
copy(Wrapper<T> source, Wrapper<? super T> dest){
T value = source.get();
dest.set(value);
}
}
现在你说copy()方法的dest参数可以是T,与source相同,或者是它的任何超类型。您可以使用copy()方法将Wrapper<String>的内容复制到Wrapper<Object>中,如下所示:
Wrapper<Object> objectWrapper =
new Wrapper<Object>(new Object());
Wrapper<String> stringWrapper =
new Wrapper<String>("Hello");
WrapperUtil.copy(stringWrapper, objectWrapper);
// <- OK with the new copy() method
由于Object是String的超类型,新的copy()方法将会工作。然而,你不能用它从一个Object类型的包装器复制到一个String类型的包装器,因为“一个对象是一个字符串”并不总是正确的。清单 3-4 显示了WrapperUtil类的完整代码。
// WrapperUtil.java
package com.jdojo.generics;
public class WrapperUtil {
public static void printDetails(Wrapper<?> wrapper) {
// Can assign get() return value to Object
Object value = wrapper.get();
String className = null;
if (value != null) {
className = value.getClass().getName();
}
System.out.println("Class: " + className);
System.out.println("Value: " + value);
}
public static double sum(Wrapper<? extends Number> n1,
Wrapper<? extends Number> n2) {
Number num1 = n1.get();
Number num2 = n2.get();
double sum = num1.doubleValue() +
num2.doubleValue();
return sum;
}
public static <T> void copy(Wrapper<T> source,
Wrapper<? super T> dest) {
T value = source.get();
dest.set(value);
}
}
Listing 3-4A WrapperUtil Utility Class That Works with Wrapper Objects
清单 3-5 向您展示了如何使用Wrapper和WrapperUtil类。
// WrapperUtilTest.java
package com.jdojo.generics;
public class WrapperUtilTest {
public static void main(String[] args) {
Wrapper<Integer> n1 = new Wrapper<>(10);
Wrapper<Double> n2 = new Wrapper<>(15.75);
// Print the details
WrapperUtil.printDetails(n1);
WrapperUtil.printDetails(n2);
// Add numeric values in two WrapperUtil
double sum = WrapperUtil.sum(n1, n2);
System.out.println("sum: " + sum);
// Copy the value of a Wrapper<Double> to a
// Wrapper<Number>
Wrapper<Number> holder = new Wrapper<>(45);
System.out.println("Original holder: " +
holder.get());
WrapperUtil.copy(n2, holder);
System.out.println("After copy holder: " +
holder.get());
}
}
Class: java.lang.Integer
Value: 10
Class: java.lang.Double
Value: 15.75
sum: 25.75
Original holder: 45
After copy holder: 15.75
Listing 3-5Using the WrapperUtil Class
泛型方法和构造函数
您可以在方法声明中定义类型参数。它们在方法的返回类型前的尖括号中指定。包含泛型方法声明的类型不一定是泛型类型,因此可以在非泛型类型中包含泛型方法。类型及其方法也可以定义不同的类型参数。
Note
为泛型类型定义的类型参数在该类型的静态方法中不可用。因此,如果静态方法需要是泛型的,它必须定义自己的类型参数。如果一个方法需要是泛型的,只将该方法定义为泛型,而不是将整个类型定义为泛型。
下面的代码片段定义了一个泛型类型Test,它的类型参数名为T。它还定义了一个通用实例方法m1(),该方法定义了自己的通用类型参数V。该方法还使用类型参数T,该参数由其类定义。注意在m1()方法的返回类型void之前<V>的使用。它为该方法定义了一个名为V的新泛型类型。
public class Test<T> {
public <V> void m1(Wrapper<V> a, Wrapper<V> b, T c) {
// Do something
}
}
您能想到为m1()方法定义和使用泛型类型参数V的含义吗?看看它在定义方法的第一个和第二个参数为Wrapper<V>时的用法。它强制第一个和第二个参数为同一类型。第三个参数必须是相同的类型T,这是类实例化的类型。
当你想调用一个方法时,你如何指定这个方法的泛型类型?通常,在调用方法时,不需要指定实际的类型参数。编译器会使用您传递给方法的值为您计算出来。但是,如果您需要为方法的形式类型参数传递实际类型参数,您必须在方法调用中的点和方法名称之间的尖括号(< >)中指定它,如下所示:
Test<String> t = new Test<String>();
Wrapper<Integer> iw1 =
new Wrapper<Integer>(new Integer(201));
Wrapper<Integer> iw2 =
new Wrapper<Integer>(new Integer(202));
// Specify that Integer is the actual type for the type
// parameter for m1()
t.<Integer>m1(iw1, iw2, "hello");
// Let the compiler figure out the actual type parameters
// using types for iw1 and iw2
t.m1(iw1, iw2, "hello"); // OK
清单 3-4 展示了如何声明一个通用的静态方法。不能在静态方法中引用包含类的类型参数。静态方法只能引用它自己声明的类型参数。
这是来自WrapperUtil类的copy()静态方法的副本。它定义了一个类型参数T,用于约束参数source和dest的类型:
public static <T> void copy(Wrapper<T> source,
Wrapper<? super T> dest) {
T value = source.get();
dest.set(value);
}
编译器会计算出一个方法的实际类型参数,不管这个方法是非静态的还是静态的。但是,如果要为静态方法调用指定实际的类型参数,可以按如下方式进行:
WrapperUtil.<Integer>copy(iw1, iw2);
也可以像定义方法一样定义构造函数的类型参数。下面的代码片段为类Test的构造函数定义了一个类型参数U。它设置了一个约束,即构造函数的类型参数U必须与其类类型参数T的实际类型相同或者是其实际类型的子类型:
public class Test<T> {
public <U extends T> Test(U k) {
// Do something
}
}
编译器将通过检查您传递给构造函数的参数来计算出传递给构造函数的实际类型参数。如果要为构造函数指定实际的类型参数值,可以在 new 运算符和构造函数名称之间的尖括号中指定,如下面的代码片段所示:
// Specify the actual type parameter for the constructor
// as Double
Test<Number> t1 = new <Double>Test<Number>(
new Double(12.89));
// Let the compiler figure out that we are using Integer
// as the actual type parameter for the constructor
Test<Number> t2 = new Test<Number>(new Integer(123));
通用对象创建中的类型推理
在许多情况下,当您创建泛型类型的对象时,编译器可以推断出对象创建表达式中类型参数的值。注意,对象创建表达式中的类型推断支持仅限于类型明显的情况。请考虑以下陈述:
List<String> list = new ArrayList<String>();
list 声明为List<String>,很明显你想创建一个类型参数为<String>的ArrayList。在这种情况下,您可以指定空尖括号<>(称为菱形运算符或简称为菱形),作为ArrayList的类型参数。您可以重写该语句,如下所示:
List<String> list = new ArrayList<>();
请注意,如果在对象创建表达式中没有为泛型类型指定类型参数,则该类型是原始类型,编译器会生成未检查的警告。例如,下面的语句将编译为带有未检查的警告:
// Using ArrayList as a raw type, not a generic type
List<String> list = new ArrayList(); // Generates an
// unchecked warning
warning: [unchecked] unchecked conversion
List<String> list = new ArrayList();
^
required: List<String>
found: ArrayList
1 warning
有时,编译器无法正确推断对象创建表达式中类型的参数类型。在这些情况下,您需要指定参数类型,而不是使用菱形运算符(<>)。否则,编译器会推断出错误的类型,从而产生错误。
当在对象创建表达式中使用菱形运算符时,编译器使用四步过程来推断参数化类型的参数类型。让我们考虑一个典型的对象创建表达式:
-
首先,它试图从构造函数参数的静态类型中推断出类型参数。请注意,构造函数参数可能为空,例如,
new ArrayList<>()。如果在此步骤中推断出类型参数,则该过程继续到下一步骤。 -
它使用赋值运算符的左侧来推断类型。在前面的语句中,如果构造函数参数为空,它会将
T2推断为类型。请注意,对象创建表达式可能不是赋值语句的一部分。在这种情况下,它将使用下一步。 -
如果对象创建表达式用作方法调用的实际参数,编译器会通过查看被调用方法的形参类型来推断类型。
-
如果所有其他的都失败了,并且它不能使用这些步骤推断类型,它推断
Object作为类型参数。
T1<T2> var = new T3<>(constructor-arguments);
让我们讨论几个涉及类型推断过程中所有步骤的例子。创建两个列表,List<String>类型的list1和List<Integer>类型的list2:
import java.util.Arrays;
import java.util.List;
// More code goes here...
List<String> list1 = Arrays.asList("A", "B");
List<Integer> list2 = Arrays.asList(9, 19, 1969);
考虑以下使用菱形运算符的语句:
List<String> list3 = new ArrayList<>(list1);
// <- Inferred type is String
编译器使用构造函数参数list1来推断类型。list1的静态类型是List<String>,所以类型String是由编译器推断出来的。前面的语句编译正常。在推理过程中,编译器没有使用赋值操作符List<String> list3的左侧。你可能不相信这个论点。考虑下面的陈述来证明这一点:
List<String> list4 = new ArrayList<>(list2);
// <- A compile-time error
required: List<String>
found: ArrayList<Integer>
1 error
你现在相信了吗?构造函数参数是list2,它的静态类型是List<Integer>。编译器推断类型为Integer,并将ArrayList<>替换为ArrayList<Integer>。变量list4的类型是List<String>,它与ArrayList<Integer>的赋值不兼容,这导致了编译时错误。
考虑以下语句:
List<String> list5 = new ArrayList<>();
// <- Inferred type is String
这一次,没有构造函数参数。编译器使用第二步查看赋值操作符的左侧来推断类型。在左侧,它找到了List<String>,并正确地推断出类型为String。考虑一个如下声明的process()方法:
public static void process(List<String> list) {
// Code goes here
}
下面的语句调用了process()方法,推断出的类型参数是String:
// The inferred type is String
process(new ArrayList<>());
编译器查看process()方法的形参类型,找到List<String>,并将类型推断为String。
Note
使用菱形操作符可以节省一些输入。当类型推断很明显时使用它。但是,为了提高可读性,最好在复杂的对象创建表达式中指定类型,而不是菱形运算符。总是选择可读性而不是简洁。
JDK9 增加了对匿名类中 diamond 操作符的支持,如果推断的类型是可拒绝的。如果推断的类型是不可指定的,则不能对匿名类使用 diamond 运算符,即使在 JDK9 或更高版本中也是如此。Java 编译器使用不能用 Java 程序编写的类型。可以用 Java 程序编写的类型称为可命名类型。编译器知道但不能用 Java 程序编写的类型称为不可命名类型。例如,String是一个可命名的类型,因为你可以在程序中用它来表示一个类型;然而,Serializable & CharSequence不是一个可命名的类型,即使它对于编译器是一个有效的类型。它是一个交集类型,表示实现两个接口Serializable和CharSequence的类型。泛型类型定义中允许交集类型,但不能使用此交集类型声明变量:
// Not allowed in Java code. Cannot declare a variable
// of an intersection type.
Serializable & CharSequence var;
// Allowed in Java code
class Magic<T extends Serializable & CharSequence> {
// More code goes here
}
Java 在java.util.concurrent package中包含一个通用的Callable<V>接口。兹声明如下:
public interface Callable<V> {
V call() throws Exception;
}
在 JDK9 和更高版本中,编译器将在下面的代码片段中推断匿名类的类型参数为Integer:
// A compile-time error in JDK8, but allowed in JDK9.
Callable<Integer> c = new Callable<>() {
@Override
public Integer call() {
return 100;
}
};
没有通用异常类
异常在运行时抛出。如果在 catch 子句中使用泛型异常类,编译器无法确保异常在运行时的类型安全,因为擦除过程会在编译期间擦除任何类型参数。这就是试图定义泛型类是编译时错误的原因,泛型类是java.lang.Throwable的直接或间接子类。
没有通用匿名类
匿名类是一次性类。您需要一个类名来指定实际的类型参数。匿名类没有名字。因此,不能有泛型匿名类。但是,匿名类中可以有泛型方法。匿名类可以继承泛型类。匿名类可以实现通用接口。除了异常类型、枚举和匿名内部类之外,任何类都可以有类型参数。
泛型和数组
让我们看看下面这个名为GenericArrayTest的类的代码:
public class GenericArrayTest<T> {
private T[] elements;
public GenericArrayTest(int howMany) {
elements = new T[howMany]; // A compile-time error
}
// More code goes here
}
GenericArrayTest类声明一个类型参数T。在构造函数中,它试图创建一个泛型类型的数组。您不能编译前面的代码。编译器会抱怨以下语句:
elements = new T[howMany]; // A compile-time error
回想一下,当编译泛型类或使用泛型类型参数的代码时,所有对泛型类型参数的引用都将从代码中删除。数组在创建时需要知道它的类型,这样当元素存储在数组中时,它可以在运行时执行检查,以确保元素与数组类型的赋值兼容。如果使用类型参数创建数组,则数组的类型信息在运行时将不可用。这就是不允许使用该语句的原因。
您不能创建泛型类型的数组,因为编译器不能确保数组元素赋值的类型安全。您不能编写以下代码:
Wrapper<String>[] gsArray = null;
// Cannot create an array of generic type
gsArray = new Wrapper<String>[10]; // A compile-time error
允许创建一个无界通配符泛型类型数组,如下所示:
Wrapper<?>[] anotherArray = new Wrapper<?>[10]; // Ok
假设你想使用一个泛型类型的数组。您可以通过使用java.lang.reflect.Array类的newInstance()方法来这样做,如下所示。由于数组创建语句中使用的强制转换,您必须在编译时处理未检查的警告。下面的代码片段显示,当您试图将一个Object偷偷放入一个Wrapper<String>数组时,您仍然可以绕过编译时类型安全检查。然而,这是您在使用泛型时必须忍受的后果,泛型在运行时不携带其类型信息。Java 泛型就像你能想象的那样肤浅。
Wrapper<String>[] a = (Wrapper<String>[]) Array.
newInstance(Wrapper.class, 10);
Object[] objArray = (Object[]) a;
objArray[0] = new Object();
// <- Will throw a java.lang.
// ArrayStoreExceptionxception
a[0] = new Wrapper<String>("Hello");
// <- OK. Checked by compiler
通用对象的运行时类类型
参数化类型的对象的类类型是什么?考虑清单 3-6 中的程序。
// GenericsRuntimeClassTest.java
package com.jdojo.generics;
public class GenericsRuntimeClassTest {
public static void main(String[] args) {
Wrapper<String> a =
new Wrapper<String>("Hello");
Wrapper<Integer> b =
new Wrapper<Integer>(new Integer(123));
Class aClass = a.getClass();
Class bClass = b.getClass();
System.out.println("Class for a: " +
aClass.getName());
System.out.println("Class for b: " +
bClass.getName());
System.out.println("aClass == bClass: " +
(aClass == bClass));
}
}
Class for a: com.jdojo.generics.Wrapper
Class for b: com.jdojo.generics.Wrapper
aClass == bClass: true
Listing 3-6All Objects of a Parameterized Type Share the Same Class at Runtime
程序创建Wrapper<String>和Wrapper<Integer>的对象。它打印两个对象的类名,它们是相同的。输出显示相同泛型类型的所有参数化对象在运行时共享相同的类对象。如前所述,您提供给泛型类型的类型信息在编译期间会从代码中移除。编译器将Wrapper<String> a;语句改为Wrapper a;。对于 JVM,一切照常(在泛型出现之前)!
堆积污染
在运行时表示一个类型被称为具体化。可以在运行时表示的类型被称为可重用类型。在运行时没有完全表示的类型被称为不可再具体化类型。大多数泛型类型是不可再具体化的,因为泛型是使用擦除实现的,擦除在编译时移除类型的参数信息。比如你写Wrapper<String>的时候,编译器去掉了类型参数<String>,运行时看到的只是Wrapper而不是Wrapper<String>。
堆污染是一种当参数化类型的变量引用不属于相同参数化类型的对象时发生的情况。如果编译器检测到可能的堆污染,它会发出未经检查的警告。如果您的程序编译时没有任何未检查的警告,堆污染就不会发生。考虑以下代码片段:
Wrapper nWrapper = new Wrapper<Integer>(101); // #1
// Unchecked warning at compile-time and heap pollution
// at runtime
Wrapper<String> sWrapper = nWrapper; // #2
String str = sWrapper.get(); // #3
// ClassCastException
第一条语句(标记为#1)编译正常。第二条语句(标记为#2)生成一个未检查的警告,因为编译器无法确定nWrapper是否属于类型Wrapper<String>。由于参数类型信息在编译时被删除,运行时无法检测这种类型不匹配。第二条语句中的堆污染使得在运行时在第三条语句(标记为#3)中获得一个ClassCastException成为可能。如果第二条语句不被允许,第三条语句将不会引起ClassCastException。
堆污染也可能因为未检查的强制转换操作而发生。考虑以下代码片段:
Wrapper<? extends Number> nW = new Wrapper<Long>(1L); // #1
// Unchecked cast and unchecked warning occurs when the
// following statement #2 is compiled. Heap pollution
// occurs, when it is executed.
Wrapper<Short> sw = (Wrapper<Short>) nW; // #2
short s = sw.get(); // #3
// ClassCastException
标记为#2 的语句使用了未检查的强制转换。编译器发出未经检查的警告。在运行时,它会导致堆污染。因此,标记为#3 的语句生成一个运行时ClassCastException。
Varargs 方法和堆污染警告
Java 通过将 varargs 参数转换为数组来实现 varargs 方法的 varargs 参数。如果 varargs 方法使用泛型类型 varargs 参数,Java 不能保证类型安全。不可重新验证的泛型类型 varargs 参数可能会导致堆污染。考虑下面的代码片段,它声明了一个带有参数化类型参数的process()方法。方法体中的注释指出了堆污染和其他类型的问题:
public static void process(Wrapper<Long>...nums) {
Object[] obj = nums; // Heap pollution
obj[0] = new Wrapper<>("Hello"); // An array
// corruption
Long lv = nums[0].get(); // A ClassCastException
// Other code goes here
}
Note
您需要在 javac 编译器中使用-Xlint:unchecked,varargs选项来查看 unchecked 和 varargs 警告。
当process()方法被编译时,编译器从其参数化类型参数中移除类型信息<Long>,并将其签名改为process(Wrapper[] nums)。当您编译process()方法的声明时,您会得到以下未检查的警告:
warning: [unchecked] Possible heap pollution from
parameterized vararg type Wrapper<Long>
public static void process(Wrapper<Long>...nums) {
^
1 warning
考虑下面调用process()方法的代码片段:
Wrapper<Long> v1 = new Wrapper<>(10L);
Wrapper<Long> v2 = new Wrapper<>(11L);
process(v1, v2); // An unchecked warning
编译这段代码时,它会生成以下编译器未检查警告:
warning: [unchecked] unchecked generic array creation for
varargs parameter of type
Wrapper<Long>[]
process(v1, v2);
^
1 warning
警告是在方法声明和方法调用位置生成的。如果您创建了这样一个方法,那么您有责任确保在您的方法体中不会发生堆污染。
如果您创建了一个带有不可重用类型参数的 varargs 方法,那么您可以通过使用@SafeVarargs注释来隐藏方法声明位置以及方法调用位置的未检查警告。通过使用@SafeVarargs,您断言您的 varargs 方法和不可重证的类型参数可以安全使用。下面的代码片段使用了带有process()方法的@SafeVarargs注释:
@SafeVarargs
public static void process(Wrapper<Long>...nums) {
Object[] obj = nums;
// <- Heap pollution
obj[0] = new Wrapper<String>("Hello");
// <- An array corruption
Long lv = nums[0].get();
// <- A ClassCastException
// Other code goes here
}
当您编译这个process()方法的声明时,您不会得到一个未检查的警告。但是,您会得到下面的 varargs 警告,因为当 varargs 参数nums被分配给Object数组 obj 时,编译器会发现可能的堆污染:
warning: [varargs] Varargs method could cause heap
pollution from non-reifiable varargs
parameter nums
Object[] obj = nums;
^
1 warning
通过使用如下的@SuppressWarnings注释,可以取消带有不可重证类型参数的 varargs 方法的未检查和 varargs 警告:
@SuppressWarnings({"unchecked", "varargs"})
public static void process(Wrapper<Long>...nums) {
// Code goes here
}
请注意,在 varargs 方法中使用@SuppressWarnings注释时,它只在方法声明的位置取消警告,而不在调用方法的位置取消警告。
摘要
泛型是 Java 语言的特性,允许您声明使用类型参数的类型(类和接口)。当使用泛型类型时,指定类型参数。与实际类型参数一起使用的类型称为参数化类型。当使用泛型类型而没有指定其类型参数时,它被称为原始类型。例如,如果Wrapper<T>是泛型类,Wrapper<String>是参数化类型,String是实际类型参数,Wrapper是原始类型。也可以为构造函数和方法指定类型参数。泛型允许您使用适用于所有类型的类型参数在 Java 代码中编写真正的多态代码。
默认情况下,类型参数是无界的,这意味着您可以为类型参数指定任何类型。例如,如果用类型参数<T>声明一个类,那么可以指定 Java 中可用的任何类型,比如<String>、<Object>、<Person>、<Employee>、<Integer>等。,实际类型为T。类型声明中的类型参数也可以指定为有上限或下限。声明Wrapper<U extends Person>是为类型参数U指定上限的一个例子,它指定U可以是Person的类型或Person的子类型。声明Wrapper<?super Person>是指定下限的一个例子;它指定类型参数是类型Person还是超类型Person。
Java 还允许您将通配符(一个问号)指定为实际的类型参数。通配符作为实际参数意味着实际类型参数未知;例如,Wrapper<?>意味着泛型类型Wrapper<T>的类型参数T未知。
编译器尝试使用泛型来推断表达式的类型,这取决于使用表达式的上下文。如果编译器无法推断类型,它会生成编译时错误,您需要显式指定类型。
参数化类型不存在超类型-子类型关系。比如Wrapper<Long>不是Wrapper<Number>的子类型。
编译器使用名为类型删除的过程删除泛型类型参数。因此,泛型类型参数在运行时不可用。比如Wrapper<Long>和Wrapper<String>的运行时类型是一样的,都是Wrapper。
练习
练习 1
什么是泛型(或泛型类型)、参数化类型和原始类型?给出一个泛型类型及其参数化类型的例子。
练习 2
Number类是Long类的超类。以下代码片段无法编译。解释一下。
List<Number> list1= new ArrayList<>();
List<Long> list2= new ArrayList<>();
list1 = list2; // A compile-time error
运动 3
运行下面的ClassNamePrinter类时,写下输出。编译器在编译过程中删除类型参数T后,重写该类的print()方法的代码:
// ClassNamePrinter.java
package com.jdojo.generics.exercises;
public class ClassNamePrinter {
public static void main(String[] args) {
ClassNamePrinter.print(10);
ClassNamePrinter.print(10L);
ClassNamePrinter.print(10.2);
}
public static <T extends Number> void
print(T obj) {
String className = obj.getClass().
getSimpleName();
System.out.println(className);
}
}
演习 4
什么是无界通配符?为什么下面的代码片段无法编译?
List<?> list = new ArrayList<>();
list.add("Hello"); // A compile-time error
锻炼 5
考虑下面的Util类的不完整声明:
// Util.java
package com.jdojo.generics.exercises;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Util {
public static void main(String[] args) {
Integer[] n1 = {1, 2};
Integer[] n2 = {3, 4};
Integer[] m = merge(n1, n2);
System.out.println(Arrays.toString(m));
String[] s1 = {"one", "two"};
String[] s2 = {"three", "four"};
String[] t = merge(s1, s2);
System.out.println(Arrays.toString(t));
List<Number> list = new ArrayList<>();
add(list, 10, 20, 30L, 40.5F, 50.9);
System.out.println(list);
}
public static <T> T[] merge(T[] a, T[] b) {
}
public static /* Add type parameters here */ void
add(List<T> list, U... elems) {
/* Your code to add elems to list goes here */
}
}
完成merge()方法的主体,这样它可以连接作为参数传入的两个数组,并返回连接的数组。通过指定类型参数并在其主体中添加代码来完成add()方法。该方法的第一个参数是参数化的List<T>,第二个参数是类型为T或其后代的 varargs 参数。也就是说,第二个参数类型是其对象可以添加到List<T>的任何类型。运行Util类应该产生以下输出:
[1, 2, 3, 4]
[one, two, three, four]
[10, 20, 30, 40.5, 50.9]
锻炼 6
创建一个通用的Stack<E>类。它的对象表示一个堆栈,可以存储其类型参数E的元素。以下是该类的模板。您需要为它的所有方法提供实现。编写测试代码来测试所有方法。方法名是堆栈的标准方法名。任何对堆栈的非法访问都会引发运行时异常。
// Stack.java
package com.jdojo.generics.exercises;
import java.util.LinkedList;
import java.util.List;
public class Stack<E> {
// Use LinkedList instead of ArrayList
private final List<E> stack = new LinkedList<>();
public void push(E e) {}
public E pop() { }
public E peek() { }
public boolean isEmpty() { }
public int size() { }
}
锻炼 7
什么是堆污染?当编译器检测到堆污染的可能性时,它会生成什么类型的警告?如何在编译期间打印这样的警告?你如何抑制这种警告?
运动 8
描述下面的Test类声明不能编译的原因:
public class Test {
public <T> void test(T t) {
// More code goes here
}
public <U> void test(U u) {
// More code goes here
}
}public class Test {
public <T> void test(T t) {
// More code goes here
}
public <U> void test(U u) {
// More code goes here
}
}
四、Lambda 表达式
在本章中,您将学习:
-
什么是 lambda 表达式
-
为什么我们需要 lambda 表达式
-
定义 lambda 表达式的语法
-
lambda 表达式的目标类型
-
常用的内置功能接口
-
方法和构造函数引用
-
lambda 表达式的词法范围
本章中的所有示例程序都是清单 4-1 中声明的jdojo.lambda模块的成员。
// module-info.java
module jdojo.lambda {
exports com.jdojo.lambda;
}
Listing 4-1The Declaration of a jdojo.lambda Module
什么是 Lambda 表达式?
lambda 表达式是一个未命名的代码块(或未命名的函数),带有一个形参列表和一个主体。有时,一个 lambda 表达式被简称为 lambda。lambda 表达式的主体可以是块语句或表达式。箭头(->)用于分隔参数列表和主体。术语“lambda”源于 Lambda calculus,它使用希腊字母 lambda ( lambda)来表示函数抽象。以下是 Java 中 lambda 表达式的一些示例:
// Takes an int parameter and returns the parameter value
// incremented by 1
(int x) -> x + 1
// Takes two int parameters and returns their sum
(int x, int y) -> x + y
// Takes two int parameters and returns the maximum of
// the two
(int x, int y) -> { int max = x > y ? x : y;
return max;
}
// Takes no parameters and returns void
() -> { }
// Takes no parameters and returns a string "OK"
() -> "OK"
// Takes a String parameter and prints it on the standard
// output
(String msg) -> { System.out.println(msg); }
// Takes a parameter and prints it on the standard output
msg -> System.out.println(msg)
// Takes a String parameter and returns its length
(String str) -> str.length()
此时,您将无法完全理解 lambda 表达式的语法。我很快会详细介绍语法。现在,只要有感觉就行了,记住 lambda 表达式的语法类似于声明方法的语法。
Note
lambda 表达式不是方法,尽管它的声明看起来像方法。顾名思义,lambda 表达式是表示函数接口实例的表达式。
Java 中的每个表达式都有一个类型,lambda 表达式也是如此。lambda 表达式的类型是函数接口类型。当函数接口的抽象方法被调用时,lambda 表达式的主体被执行。考虑采用String参数并返回其长度的 lambda 表达式:
(String str) -> str.length()
这个 lambda 表达式的类型是什么?答案是我们不知道。通过查看 lambda 表达式,你只能说它接受一个String参数并返回一个int,这是String参数的长度。它的类型可以是任何带有抽象方法的函数接口类型,该方法将一个String作为参数并返回一个int。以下是这种功能界面的一个示例:
@FunctionalInterface
interface StringToIntMapper {
int map(String str);
}
lambda 表达式表示出现在赋值语句中的StringToIntMapper函数接口的一个实例,如下所示:
StringToIntMapper mapper =
(String str) -> str.length();
在这个语句中,编译器发现赋值操作符的右边是一个 lambda 表达式。为了推断它的类型,它查看赋值操作符的左侧,该操作符需要一个StringToIntMapper接口的实例;它验证 lambda 表达式符合StringToIntMapper接口中map()方法的声明;最后,它推断 lambda 表达式的类型是StringToIntMapper接口类型。当您调用映射器变量上的map()方法并传递一个String时,lambda 表达式的主体被执行,如下面的代码片段所示:
StringToIntMapper mapper = (String str) -> str.length();
String name = "Kristy";
int mappedValue = mapper.map(name);
System.out.println("name=" + name +
", mapped value=" + mappedValue);
name=Kristy, mapped value=6
到目前为止,您还没有看到任何不使用 lambda 表达式就无法在 Java 中完成的事情。以下代码片段使用匿名类来实现与上一示例中使用的 lambda 表达式相同的结果:
StringToIntMapper mapper = new StringToIntMapper() {
@Override
public int map(String str) {
return str.length();
}
};
String name = "Kristy";
int mappedValue = mapper.map(name);
System.out.println("name=" + name +
", mapped value=" + mappedValue);
name=Kristy, mapped value=6
此时,lambda 表达式似乎是编写匿名类的一种简洁方式,就语法而言确实如此。两者在语义上有一些微妙的区别。我将在稍后讨论更多细节时讨论这些差异。
Note
Java 是一种强类型语言,这意味着编译器必须知道 Java 程序中使用的所有表达式的类型。lambda 表达式本身没有类型,因此不能用作独立的表达式。lambda 表达式的类型总是由编译器根据使用它的上下文来推断。
为什么我们需要 Lambda 表达式?
Java 从一开始就支持面向对象编程。在面向对象编程中,程序逻辑是基于可变对象的。类的方法包含逻辑。在对象上调用方法,这通常会修改对象的状态。在面向对象编程中,方法调用的顺序很重要,因为每个方法调用都可能修改对象的状态,从而产生副作用。程序逻辑的静态分析是困难的,因为程序状态取决于代码执行的顺序。用变化的对象编程也对并发编程提出了挑战,在并发编程中,程序的多个部分可能试图同时修改同一对象的状态。
随着近年来计算机处理能力的增加,要处理的数据量也增加了。如今,处理兆兆字节大小的数据是很常见的,这就需要并行编程。现在,电脑普遍拥有多核处理器,让用户有机会更快地运行软件程序;与此同时,这给程序员提出了一个挑战,即利用处理器中所有可用的内核来编写更多的并行程序。Java 从一开始就支持并发编程。它通过 fork/join 框架增加了对 Java 7 中并行编程的支持,这个框架不太好用。
基于 Lambda 演算的函数式编程早在面向对象编程之前就存在了。它基于函数的概念,函数是接受值(称为参数)的代码块,执行代码块来计算结果。功能代表一种功能或操作。函数不修改数据,包括它的输入,因此不会产生副作用;因此,函数的执行顺序在函数式编程中并不重要。在函数式编程中,高阶函数是一个匿名函数,可以被视为数据对象。也就是说,它可以存储在变量中,并从一个上下文传递到另一个上下文。它可能在不一定定义它的上下文中被调用。注意,高阶函数是匿名函数,所以调用上下文不需要知道它的名字。闭包是与其定义环境打包在一起的高阶函数。定义闭包时,它会携带作用域中的变量,即使在定义这些变量的上下文之外的上下文中调用闭包,它也可以访问这些变量。
近年来,函数式编程因其在并发、并行和事件驱动编程中的适用性而变得流行起来。C#、Groovy、Python、Scala 等现代编程语言都支持函数式编程。Java 不想落后,因此,它引入了 lambda 表达式来支持函数式编程,这可以与它已经流行的面向对象特性相结合来开发健壮的、并发的、并行的程序。Java 采用的 lambda 表达式的语法与其他编程语言中使用的语法非常相似,比如 C# 和 Scala。
在面向对象编程中,函数被称为方法,它总是类的一部分。如果您想在 Java 中传递功能,您需要创建一个类,向该类添加一个方法来表示功能,创建该类的一个对象,并传递该对象。Java 中的 lambda 表达式就像函数式编程中的高阶函数,它是一个未命名的代码块,表示可以像数据一样传递的功能。lambda 表达式可以捕获其定义范围内的变量,并且它可以稍后在没有定义所捕获变量的上下文中访问这些变量。这些特性允许您使用 lambda 表达式在 Java 中实现闭包。
那么我们为什么需要 lambda 表达式,在哪里需要呢?匿名类使用庞大的语法。Lambda 表达式使用非常简洁的语法来实现相同的结果。Lambda 表达式不能完全替代匿名类。在一些情况下,您仍然需要使用匿名类。为了理解 lambda 表达式的简洁性,请比较前一节中创建了一个StringToIntMapper接口实例的以下两条语句;一个使用匿名类,有 6 行代码,另一个使用 lambda 表达式,只有一行代码:
// Using an anonymous class
StringToIntMapper mapper = new StringToIntMapper() {
@Override
public int map(String str) {
return str.length();
}
};
// Using a lambda expression
StringToIntMapper mapper = (String str) -> str.length();
Lambda 表达式的语法
lambda 表达式描述了一个匿名函数。使用 lambda 表达式的一般语法与声明方法非常相似。一般语法是
(<LambdaParametersList>) -> { <LambdaBody> }
lambda 表达式由一个参数列表和一个由箭头(->))分隔的主体组成。参数列表的声明方式与方法的参数列表相同。参数列表用括号括起来,方法也是如此。lambda 表达式的主体是用大括号括起来的代码块。像方法的主体一样,lambda 表达式的主体可以声明局部变量;使用语句包括break、continue、return;抛出异常;等等。与方法不同,lambda 表达式没有以下四个部分:
-
lambda 表达式没有名称。
-
lambda 表达式没有返回类型。它由编译器从它的使用上下文和它的主体中推断出来。
-
lambda 表达式没有
throws子句。从它的使用上下文和它的主体来推断。 -
lambda 表达式不能声明类型参数。也就是说,lambda 表达式不能是泛型的。
表 4-1 包含一些 lambda 表达式和等效方法的例子。我给方法取了一个合适的名字,因为在 Java 中,方法不能没有名字。编译器推断 lambda 表达式的返回类型。
表 4-1
Lambda 表达式和等效方法的示例
|Lambda 表达式
|
等效方法
|
| --- | --- |
| (int x, int y) -> {``return x + y;``} | int sum(int x, int y) {``return x + y;``} |
| (Object x) -> {``return x;``} | Object identity(Object x)``return x;``} |
| (int x, int y) -> {``if (x > y)``return x;``} else {``return y;``}``} | int getMax(int x, int y) {``if (x > y)``return x;``} else {``return y;``}``} |
| (String msg) -> {``System.out.println(msg);``} | void print(String msg) {``System.out.println(msg);``} |
| () -> {``System.out.println(LocalDate.now());``} | void printCurrentDate() {``System.out.println(LocalDate.now());``} |
| () -> {``// No code goes here``} | void doNothing() {``// No code goes here``} |
lambda 表达式的目标之一是保持其语法简洁,并让编译器推断细节。下面几节讨论声明 lambda 表达式的简写语法。
省略参数类型
您可以省略参数的声明类型。编译器将从使用 lambda 表达式的上下文中推断参数的类型:
// Types of parameters are declared
(int x, int y) -> { return x + y; }
// Types of parameters are omitted
(x, y) -> { return x + y; }
如果省略参数类型,则必须为所有参数省略或不省略。你不能忽略一些人而忽略另一些人。以下 lambda 表达式将不会编译,因为它声明了一个参数的类型,而省略了另一个参数的类型:
// A compile-time error
(int x, y) -> { return x + y; }
Note
不声明其参数类型的 lambda 表达式称为隐式 lambda 表达式或隐式类型化 lambda 表达式。声明其参数类型的 lambda 表达式称为显式 lambda 表达式或显式类型化 lambda 表达式。
对参数使用局部变量语法
您可以对 lambda 表达式中的参数使用局部变量语法:
// A compile-time error
(var x, var y) -> { return x + y; }
编译器将从使用 lambda 表达式的上下文中推断参数的类型,并记住每个变量的类型。在 JDK11 中,lambda 表达式参数的局部变量语法被添加到 Java 中。
声明单个参数
有时,lambda 表达式只接受一个参数。可以省略单参数 lambda 表达式的参数类型,就像可以省略具有多个参数的 lambda 表达式一样。如果在单参数 lambda 表达式中省略了参数类型,也可以省略括号。以下是用单个参数声明 lambda 表达式的三种方法:
// Declares the parameter type
(String msg) -> { System.out.println(msg); }
// Omits the parameter type
(msg) -> { System.out.println(msg); }
// Omits the parameter type and parentheses
msg -> { System.out.println(msg); }
只有当单个参数也省略其类型时,才能省略括号。以下 lambda 表达式将不会编译:
// Omits parentheses, but not the parameter type, which is not allowed.
String msg -> { System.out.println(msg); }
未声明任何参数
如果 lambda 表达式不带任何参数,则需要使用空括号:
// Takes no parameters
() -> { System.out.println("Hello"); }
当 lambda 表达式不带参数时,不允许省略括号。以下声明不会编译:
-> { System.out.println("Hello"); }
带修饰符的参数
您可以在显式 lambda 表达式的参数声明中使用修饰符,如final。以下两个 lambda 表达式是有效的:
(final int x, final int y) -> { return x + y; }
(int x, final int y) -> { return x + y; }
以下 lambda 表达式将不会编译,因为它在参数声明中使用了 final 修饰符,但省略了参数类型:
(final x, final y) -> { return x + y; }
声明 Lambda 表达式的主体
lambda 表达式的主体可以是块语句或单个表达式。block 语句用大括号括起来;单个表达式没有用大括号括起来。
lambda 表达式的主体以与方法主体相同的方式执行。一个return语句或主体的结尾将控制返回给 lambda 表达式的调用者。
当一个表达式被用作主体时,它被求值并返回给调用者。如果表达式的计算结果为void,则不会向调用者返回任何内容。以下两个 lambda 表达式是相同的;一个使用块语句,另一个使用表达式:
/ Uses a block statement. Takes two int parameters and
// returns their sum.
(int x, int y) -> { return x + y; }
// Uses an expression. Takes two int parameters and
// returns their sum.
(int x, int y) -> x + y
以下两个 lambda 表达式是相同的;一个使用 block 语句作为主体,另一个使用计算结果为void的表达式:
// Uses a block statement
(String msg) -> { System.out.println(msg); }
// Uses an expression
(String msg) -> System.out.println(msg)
目标分类
每个 lambda 表达式都有一个类型,这是一个函数接口类型。换句话说,lambda 表达式代表一个函数接口的实例。考虑以下 lambda 表达式:
(x, y) -> x + y
这个 lambda 表达式的类型是什么?换句话说,这个 lambda 表达式代表哪个函数接口的实例?此时,我们不知道这个 lambda 表达式的类型。关于这个 lambda 表达式,我们所能自信地说的是,它有两个名为x和y的参数。我们无法判断它的返回类型,因为表达式x + y根据x和y的类型,可能会计算出一个数字(int、long、float或double)或一个String。这是一个隐式 lambda 表达式,因此,编译器必须使用使用该表达式的上下文来推断两个参数的类型。这个 lambda 表达式可以是不同的函数接口类型,这取决于使用它的上下文。
Java 中有两种类型的表达式:
-
独立表达式
-
多边形表达式
独立表达式是一种无需知道其使用上下文即可确定其类型的表达式。以下是独立表达式的示例:
// The type of expression is String
new String("Hello")
// The type of expression is String (a String literal
// is also an expression)
"Hello"
// The type of expression is ArrayList<String>
new ArrayList<String>()
聚合表达式是在不同上下文中具有不同类型的表达式。编译器确定类型。允许使用多边形表达式的上下文称为多边形上下文。Java 中所有的 lambda 表达式都是 poly 表达式。您必须在上下文中使用它才能知道它的类型。例如,表达式new ArrayList<>()是一个多边形表达式。除非提供其使用的上下文,否则无法判断其类型。此表达式在以下两种上下文中用于表示两种不同的类型:
// The type of new ArrayList<>() is ArrayList<Long>
ArrayList<Long> idList = new ArrayList<>();
// The type of new ArrayList<>() is ArrayList<String>
ArrayList<String> nameList = new ArrayList<>();
编译器推断 lambda 表达式的类型。使用 lambda 表达式的上下文需要一个称为目标类型的类型。从上下文推断 lambda 表达式类型的过程称为目标类型化。考虑以下赋值语句的伪代码,其中类型为T的变量被赋予一个 lambda 表达式:
T t = <LambdaExpression>;
这个上下文中 lambda 表达式的目标类型是T。编译器使用以下规则来确定<LambdaExpression>是否与其目标类型T的赋值兼容:
-
T必须是功能接口类型。 -
lambda 表达式的参数数量和类型与
T的抽象方法相同。对于隐式 lambda 表达式,编译器会从T的抽象方法中推断出参数的类型。 -
lambda 表达式主体返回值的类型与抽象方法
T的返回类型赋值兼容。 -
如果 lambda 表达式的主体抛出任何检查过的异常,这些异常必须与抽象方法
T的声明 throws 子句兼容。如果 lambda 表达式的目标类型的方法不包含throws子句,则从 lambda 表达式的主体中抛出检查过的异常是一个编译时错误。
让我们看几个目标类型的例子。考虑两个功能接口Adder和Joiner,分别如清单 4-2 和 4-3 所示。
// Joiner.java
package com.jdojo.lambda;
@FunctionalInterface
public interface Joiner {
String join(String s1, String s2);
}
Listing 4-3A Functional Interface Named Joiner
// Adder.java
package com.jdojo.lambda;
@FunctionalInterface
public interface Adder {
double add(double n1, double n2);
}
Listing 4-2A Functional Interface Named Adder
Adder接口的add()方法将两个数相加。Joiner接口的join()方法连接两个字符串。这两个接口都用于琐碎的目的;然而,它们将很好地用于演示 lambda 表达式的目标类型。考虑下面的赋值语句:
Adder adder = (x, y) -> x + y;
加法器变量的类型是Adder。lambda 表达式被赋值给变量adder,因此,lambda 表达式的目标类型是Adder。编译器验证Adder是一个函数接口。lambda 表达式是隐式 lambda 表达式。编译器发现Adder接口包含一个double add(double, double)抽象方法。它将x和y参数的类型分别推断为double和double。此时,编译器将该语句视为如下所示:
Adder adder = (double x, double y) -> x + y;
如果你写
Adder adder = (var x, var y) -> x + y;
编译器将再次从上下文中知道x和y是double s。因此我们再次拥有一个隐式的 lambda 表达式。与完全省略类型相比,var name语法更好地表达了为 lambda 表达式创建局部变量,尽管我们对实际声明类型并不感兴趣。
编译器现在验证 lambda 表达式返回值和add()方法返回类型的兼容性。add()方法的返回类型是double。lambda 表达式返回x + y,这将是一个double,因为编译器已经知道x和y的类型是double。lambda 表达式不抛出任何检查过的异常。因此,编译器不需要为此进行任何验证。此时,编译器推断 lambda 表达式的类型是类型Adder。
对以下赋值语句应用目标类型规则:
Joiner joiner = (x, y) -> x + y;
这一次,编译器推断 lambda 表达式的类型为Joiner。你是否看到了一个聚合表达式的例子,其中同一个 lambda 表达式(x, y) -> x + y在一个上下文中属于类型Adder,而在另一个上下文中属于类型Joiner?
清单 4-4 展示了如何在程序中使用这些 lambda 表达式。
// TargetTypeTest.java
package com.jdojo.lambda;
public class TargetTypeTest {
public static void main(String[] args) {
// Creates an Adder using a lambda expression
Adder adder = (x, y) -> x + y;
// Creates a Joiner using a lambda expression
Joiner joiner = (x, y) -> x + y;
// Adds two doubles
double sum1 = adder.add(10.34, 89.11);
// Adds two ints
double sum2 = adder.add(10, 89);
// Joins two strings
String str = joiner.join("Hello", " lambda");
System.out.println("sum1 = " + sum1);
System.out.println("sum2 = " + sum2);
System.out.println("str = " + str);
}
}
sum1 = 99.45
sum2 = 99.0
str = Hello lambda
Listing 4-4Examples of Using Lambda Expressions
我现在在方法调用的上下文中讨论目标类型。您可以将 lambda 表达式作为参数传递给方法。考虑清单 4-5 中显示的LambdaUtil类的代码。
// LambdaUtil.java
package com.jdojo.lambda;
public class LambdaUtil {
public void testAdder(Adder adder) {
double x = 190.90;
double y = 8.50;
double sum = adder.add(x, y);
System.out.print("Using an Adder:");
System.out.println(x + " + " + y + " = " + sum);
}
public void testJoiner(Joiner joiner) {
String s1 = "Hello";
String s2 = "World";
String s3 = joiner.join(s1,s2);
System.out.print("Using a Joiner:");
System.out.println("\"" + s1 + "\" + \"" + s2 +
"\" = \"" + s3 + "\"");
}
}
Listing 4-5A LambdaUtil Class That Uses Functional Interfaces As an Argument in Methods
LambdaUtil类包含两个方法:testAdder()和testJoiner()。一个方法将一个Adder作为参数,另一个方法将一个Joiner作为参数。这两种方法都有简单的实现。考虑以下代码片段:
LambdaUtil util = new LambdaUtil();
util.testAdder((x, y) -> x + y);
第一条语句创建了一个LambdaUtil类的对象。第二条语句调用对象上的testAdder()方法,传递一个(x, y) -> x + y的 lambda 表达式。编译器必须推断 lambda 表达式的类型。lambda 表达式的目标类型是类型Adder,因为testAdder(Adder adder)的参数类型是Adder。目标键入过程的其余部分与您在前面的赋值语句中看到的一样。最后,编译器推断 lambda 表达式的类型是Adder。
清单 4-6 中的程序创建了一个LambdaUtil类的对象,并调用了testAdder()和testJoiner()方法。
// LambdaUtilTest.java
package com.jdojo.lambda;
public class LambdaUtilTest {
public static void main(String[] args) {
LambdaUtil util = new LambdaUtil();
// Call the testAdder() method
util.testAdder((x, y) -> x + y);
// Call the testJoiner() method
util.testJoiner((x, y) -> x + y);
// Call the testJoiner() method. The Joiner will
// add a space between the two strings
util.testJoiner((x, y) -> x + " " + y);
// Call the testJoiner() method. The Joiner will
// reverse the strings and join resulting
// strings in reverse order adding a comma in
//between
util.testJoiner((x, y) -> {
StringBuilder sbx = new StringBuilder(x);
StringBuilder sby = new StringBuilder(y);
sby.reverse().append(",").
append(sbx.reverse());
return sby.toString();
});
}
}
Using an Adder:190.9 + 8.5 = 199.4
Using a Joiner:"Hello" + "World" = "HelloWorld"
Using a Joiner:"Hello" + "World" = "Hello World"
Using a Joiner:"Hello" + "World" = "dlroW,olleH"
Listing 4-6Using Lambda Expressions As Method Arguments
注意LambdaUtilTest类的输出。testJoiner()方法被调用了三次,每次都打印出不同的连接两个字符串“Hello”和“World”的结果。这是可能的,因为不同的 lambda 表达式被传递给了该方法。此时,你可以说你已经参数化了testJoiner()方法的行为。也就是说,testJoiner()方法的行为取决于它的参数。通过方法的参数来改变方法的行为被称为行为参数化。这也被称为将代码作为数据传递,因为您将封装在 lambda 表达式中的代码(逻辑、功能或行为)传递给方法,就好像它是数据一样。
编译器并不总是能够推断出 lambda 表达式的类型。在某些情况下,编译器无法推断 lambda 表达式的类型;这些上下文不允许使用 lambda 表达式。一些上下文可能允许使用 lambda 表达式,但是使用本身对于编译器来说可能是不明确的;其中一种情况是将 lambda 表达式传递给重载方法。
考虑清单 4-7 中显示的LambdaUtil2类的代码。这个类的代码与清单 4-5 中的LambdaUtil类的代码相同,除了这个类将两个方法的名字改成了同一个名字test(),使其成为一个重载方法。
// LambdaUtil2.java
package com.jdojo.lambda;
public class LambdaUtil2 {
public void test(Adder adder) {
double x = 190.90;
double y = 8.50;
double sum = adder.add(x, y);
System.out.print("Using an Adder:");
System.out.println(x + " + " + y + " = " + sum);
}
public void test(Joiner joiner) {
String s1 = "Hello";
String s2 = "World";
String s3 = joiner.join(s1,s2);
System.out.print("Using a Joiner:");
System.out.println("\"" + s1 + "\" + \"" + s2 +
"\" = \"" + s3 + "\"");
}
}
Listing 4-7A LambdaUtil2 Class That Uses Functional Interfaces As an Argument in Methods
考虑以下代码片段:
LambdaUtil2 util = new LambdaUtil2();
util.test((x, y) -> x + y); // A compile-time error
第二条语句导致以下编译时错误:
Reference to test is ambiguous. Both method test(Adder) in
LambdaUtil2 and method test(Joiner) in LambdaUtil2 match.
对test()方法的调用失败,因为 lambda 表达式是隐式的,并且它匹配两个版本的test()方法。编译器不知道使用哪种方法:test(Adder adder)还是test(Joiner joiner)。在这种情况下,您需要通过提供更多信息来帮助编译器。以下是帮助编译器解决歧义的一些方法:
-
如果 lambda 表达式是隐式的,则通过指定参数的类型使其显式。
-
使用石膏。
-
不要直接使用 lambda 表达式作为方法参数。首先,将它赋给所需类型的变量,然后将变量传递给方法。
让我们讨论解决编译时错误的所有三种方法。以下代码片段将 lambda 表达式更改为显式 lambda 表达式:
LambdaUtil2 util = new LambdaUtil2();
util.test((double x, double y) -> x + y);
// <- OK. Will call test(Adder adder)
在 lambda 表达式中指定参数类型解决了这个问题。编译器有两个候选方法:test(Adder adder)和test(Joiner joiner)。有了(double x, double y)参数信息,只有test(Adder adder)方法匹配。
以下代码片段使用强制转换将 lambda 表达式转换为类型Adder:
LambdaUtil2 util = new LambdaUtil2();
util.test((Adder)(x, y) -> x + y);
// <- OK. Will call test(Adder adder)
使用强制转换告诉编译器 lambda 表达式的类型是Adder,从而帮助它选择test(Adder adder)方法。
考虑下面的代码片段,它将方法调用分解为两条语句:
LambdaUtil2 util = new LambdaUtil2();
Adder adder = (x, y) -> x + y;
util.test(adder);
// <- OK. Will call test(Adder adder)
lambda 表达式被分配给一个类型为Adder的变量,该变量被传递给test()方法。同样,它帮助编译器根据adder变量的编译时类型选择test(Adder adder)方法。
清单 4-8 中的程序类似于清单 4-6 中的程序,除了它使用了LambdaUtil2类。它使用显式 lambda 表达式和强制转换来解决 lambda 表达式的不明确匹配。
// LambdaUtil2Test.java
package com.jdojo.lambda;
public class LambdaUtil2Test {
public static void main(String[] args) {
LambdaUtil2 util = new LambdaUtil2();
// Calls the testAdder() method
util.test((double x, double y) -> x + y);
// Calls the testJoiner() method
util.test((String x, String y) -> x + y);
// Calls the testJoiner() method. The Joiner will
// add a space between the two strings
util.test((Joiner) (x, y) -> x + " " + y);
// Calls the testJoiner() method. The Joiner will
// reverse the strings and join resulting strings
// in reverse order adding a comma in between
util.test((Joiner) (x, y) -> {
StringBuilder sbx = new StringBuilder(x);
StringBuilder sby = new StringBuilder(y);
sby.reverse().append(",").
append(sbx.reverse());
return sby.toString();
});
}
}
Using an Adder:190.9 + 8.5 = 199.4
Using a Joiner:"Hello" + "World" = "HelloWorld"
Using a Joiner:"Hello" + "World" = "Hello World"
Using a Joiner:"Hello" + "World" = "dlroW,olleH"
Listing 4-8Resolving Ambiguity During Target Typing
Lambda 表达式只能在以下上下文中使用:
-
赋值上下文:lambda 表达式可能出现在赋值语句中赋值操作符的右边。例如:
-
方法调用上下文:lambda 表达式可能作为方法或构造函数调用的参数出现。例如:
ReferenceType variable1 = LambdaExpression;
- 返回上下文:lambda 表达式可能出现在方法内部的
return语句中,因为它的目标类型是该方法声明的返回类型。例如:
util.testJoiner(LambdaExpression);
- 强制转换上下文:如果 lambda 表达式前面有强制转换,则可以使用该表达式。强制转换中指定的类型是其目标类型。例如:
return LambdaExpression;
(Joiner) LambdaExpression;
功能界面
函数接口就是一个只有一个抽象方法的接口。接口中的以下类型的方法不包括在定义函数接口中:
-
默认方法
-
静态方法
-
从
Object类继承的公共方法
注意,一个接口可能有不止一个抽象方法,如果除了其中一个之外的所有方法都是对Object类中方法的重新声明,那么它仍然可以是一个函数接口。考虑在java.util包中的Comparator类的声明,如下所示:
package java.util;
@FunctionalInterface
public interface Comparator<T> {
// An abstract method declared in the interface
int compare(T o1, T o2);
// Re-declaration of the equals() method in the
// Object class
boolean equals(Object obj);
// Many more static and default methods that are
// not shown here.
}
Comparator接口包含两个抽象方法:compare()和equals()。在Comparator接口中的equals()方法是对Object类的equals()方法的重新声明,因此它不违背作为一个函数接口的抽象方法需求。Comparator接口包含几个默认的静态方法,这里没有显示。
lambda 表达式用于表示函数式编程中使用的未命名函数。一个函数接口用它唯一的抽象方法来表示一种类型的功能/操作。这种共性是 lambda 表达式的目标类型总是函数接口的原因。
使用@FunctionalInterface注释
一个函数接口的声明可以选择用注释@FunctionalInterface进行注释,它在java.lang包中。至此,本章声明的所有功能接口,如Adder、Joiner,均已标注@FunctionalInterface。这个注释的存在告诉编译器要确保声明的类型是一个函数接口。如果注释@FunctionalInterface用在非功能接口或其他类型(如类)上,就会出现编译时错误。如果你没有在一个具有抽象方法的接口上使用注释@FunctionalInterface,这个接口仍然是一个函数接口,并且它可以是 lambda 表达式的目标类型。使用这个注释可以从编译器那里获得额外的保证。注释的存在还可以防止您不小心将一个函数接口变成了一个非函数接口,因为编译器会发现它。
下面对一个Operations接口的声明将不会被编译,因为接口声明使用了@FunctionalInterface注释,并且它不是一个函数接口(定义了两个抽象方法):
@FunctionalInterface
public interface Operations {
double add(double n1, double n2);
double mult(double n1, double n2);
}
要编译Operations接口,要么移除两个抽象方法中的一个,要么移除@FunctionalInterface注释。下面对一个Test类的声明将不会被编译,因为@FunctionalInterface不能用在除了函数接口之外的类型上:
@FunctionalInterface
public class Test {
// Code goes here
}
通用功能接口
函数接口可以有类型参数。也就是说,功能接口可以是通用的。通用函数参数的一个例子是带有一个类型参数T的Comparator接口:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
一个函数接口可以有一个通用的抽象方法。也就是说,抽象方法可以声明类型参数。下面是一个名为Processor的非泛型函数接口的例子,它的抽象方法process()是泛型的:
@FunctionalInterface
public interface Processor {
<T> void process(T[] list);
}
lambda 表达式不能声明类型参数,因此,它不能有抽象方法为泛型的目标类型。例如,您不能使用 lambda 表达式来表示Processor接口。在这种情况下,您需要使用方法引用(我将在下一节讨论)或匿名类。
让我们看一个通用函数接口的简短例子,并用 lambda 表达式实例化它。清单 4-9 显示了名为Mapper的功能接口的代码。
// Mapper.java
package com.jdojo.lambda;
@FunctionalInterface
public interface Mapper<T> {
// An abstract method
int map(T source);
// A generic static method
public static <U> int[] mapToInt(U[] list,
Mapper<? super U> mapper) {
int[] mappedValues = new int[list.length];
for (int i = 0; i < list.length; i++) {
// Map the object to an int
mappedValues[i] = mapper.map(list[i]);
}
return mappedValues;
}
}
Listing 4-9A Mapper Functional Interface
Mapper是带有类型参数T的通用函数接口。它的抽象方法map()将类型为T的对象作为参数,并返回一个int。mapToInt()方法是一个通用的静态方法,它接受一个类型为U的数组和一个类型为U本身或U的超类型的Mapper。该方法返回一个int数组,其元素包含作为数组传递的对应元素的映射值。
清单 4-10 中的程序展示了如何使用 lambda 表达式实例化Mapper<T>接口。该程序将一个String数组和一个Integer数组映射到int数组。
// MapperTest.java
package com.jdojo.lambda;
public class MapperTest {
public static void main(String[] args) {
// Map names using their length
System.out.println(
"Mapping names to their lengths:");
String[] names = {"David", "Li", "Doug"};
int[] lengthMapping = Mapper.mapToInt(names,
(String name) -> name.length());
printMapping(names, lengthMapping);
System.out.println("\nMapping integers to " +
"their squares:");
Integer[] numbers = {7, 3, 67};
int[] countMapping = Mapper.mapToInt(numbers,
(Integer n) -> n * n);
printMapping(numbers, countMapping);
}
public static void printMapping(Object[] from,
int[] to) {
for (int i = 0; i < from.length; i++) {
System.out.println(from[i] + " mapped to " +
to[i]);
}
}
}
Mapping names to their lengths:
David mapped to 5
Li mapped to 2
Doug mapped to 4
Mapping integers to their squares:
7 mapped to 49
3 mapped to 9
67 mapped to 4489
Listing 4-10Using the Mapper Functional Interface
交集类型和 Lambda 表达式
可以声明一个交集类型,它是多个类型的交集(或子类型)(从 Java 8 开始)。在强制转换中,交集类型可能作为目标类型出现。在两种类型之间使用了一个与号(&),比如(Type1 & Type2 & Type3),它代表的是一个新类型,是Type1、Type2和Type3的交集。考虑一个名为Sensitive的标记接口,如清单 4-11 所示。
// Sensitive.java
package com.jdojo.lambda;
public interface Sensitive {
// It is a marker interface. So, no methods exist.
}
Listing 4-11A Marker Interface Named Sensitive
假设有一个 lambda 表达式分配给了一个Sensitive类型的变量:
Sensitive sen = (x, y) -> x + y;
// <- A compile-time error
这条语句不能编译。lambda 表达式的目标类型必须是函数接口;Sensitive不是功能接口。但是,您应该能够进行这样的赋值,因为标记接口不包含任何方法。在这种情况下,您需要使用交集类型的强制转换来创建一个新的合成类型,该类型是所有类型的子类型。下面的语句将被编译:
Sensitive sen = (Sensitive & Adder) (x, y) -> x + y;
// <- OK
交集类型Sensitive & Adder仍然是一个函数接口,因此,lambda 表达式的目标类型是一个函数接口,具有来自Adder接口的一个方法。
在 Java 中,你可以把一个对象转换成一个字节流,然后再把它还原。这就是所谓的序列化。一个类必须为其要序列化的对象实现java.io.Serializable标记接口。如果你想要一个 lambda 表达式被序列化,你需要使用一个交集类型的转换。以下语句将 lambda 表达式赋给Serializable接口的变量:
Serializable ser = (Serializable & Adder) (x, y) -> x + y;
常用功能界面
java.util.function包包含许多有用的功能接口。它们在表 4-2 中列出。
表 4-2
在java.util.function包中声明的功能接口
接口名称
|
方法
|
描述
|
| --- | --- | --- |
| Function<T,R> | R apply(T t) | 表示采用类型为T的参数并返回类型为R的结果的函数。 |
| BiFunction<T,U,R> | R apply(T t, U u) | 表示一个函数,它接受两个类型为T和U的参数,并返回类型为R的结果。 |
| Predicate<T> | boolean test(T t) | 在数学中,谓词是一个布尔值函数,它接受一个参数并返回true或false。该函数表示为指定参数返回true或false的条件。 |
| BiPredicate<T,U> | boolean test(T t, U u) | 表示具有两个参数的谓词。 |
| Consumer<T> | void accept(T t) | 表示一种操作,它接受一个参数,对其进行操作以产生一些副作用,并且不返回任何结果。 |
| BiConsumer<T,U> | void accept(T t, U u) | 表示采用两个参数的操作,对它们进行操作以产生一些副作用,并且不返回任何结果。 |
| Supplier<T> | T get() | 表示返回值的供应商。 |
| UnaryOperator<T> | T apply(T t) | 继承自Function<T,T>。表示接受参数并返回相同类型结果的函数。 |
| BinaryOperator<T> | T apply(T t1, T t2) | 继承自BiFunction<T,T,T>。表示采用两个相同类型的参数并返回相同结果的函数。 |
该表仅显示了功能接口的通用版本。这些接口有几种专门的版本。它们专门用于经常使用的原始数据类型;比如IntConsumer就是Consumer<T>的专门版本。表中的一些接口包含方便的默认和静态方法。该表只列出了抽象方法,没有列出默认方法和静态方法。
使用Function<T,R>界面
Function<T,R>接口有六种专门化:
-
IntFunction<R> -
LongFunction<R> -
DoubleFunction<R> -
ToIntFunction<T> -
ToLongFunction<T> -
ToDoubleFunction<T>
IntFunction<R>、LongFunction<R>、DoubleFunction<R>分别以一个int、long和double为自变量,返回值类型为R。ToIntFunction<T>、ToLongFunction<T>和ToDoubleFunction<T>接受类型为T的参数,并分别返回一个int、一个long和一个double。表中列出的其他类型的通用函数也有类似的专用函数。
Note
您的com.jdojo.lambda.Mapper<T>接口表示与java.util.function包中的ToIntFunction<T>相同的函数类型。您创建了Mapper<T>接口来学习如何创建和使用通用功能接口。从现在开始,先看看内置的功能接口,再创建自己的;如果它们满足你的需求,就使用它们。
以下代码片段显示了如何使用相同的 lambda 表达式来表示接受 int 并返回其 square 的函数,使用了四种不同的Function<T, R>函数类型:
// Takes an int and returns its square
Function<Integer, Integer> square1 = x -> x * x;
IntFunction<Integer> square2 = x -> x * x;
ToIntFunction<Integer> square3 = x -> x * x;
UnaryOperator<Integer> square4 = x -> x * x;
System.out.println(square1.apply(5));
System.out.println(square2.apply(5));
System.out.println(square3.applyAsInt(5));
System.out.println(square4.apply(5));
25
25
25
25
Function接口包含以下默认和静态方法:
-
default <V> Function<T,V> andThen(Function<? super R,? extends V> after) -
default <V> Function<V,R> compose(Function<? super V,? extends T> before) -
static <T> Function<T,T> identity()
andThen()方法返回一个复合的Function,它将这个函数应用于参数,然后将指定的 after 函数应用于结果。compose()函数返回一个复合函数,该复合函数将指定的before函数应用于参数,然后将该函数应用于结果。identify()方法返回一个总是返回其参数的函数。
以下代码片段演示了如何使用Function接口的默认和静态方法来组合新函数:
// Create two functions
Function<Long, Long> square = x -> x * x;
Function<Long, Long> addOne = x -> x + 1;
// Compose functions from the two functions
Function<Long, Long> squareAddOne = square.andThen(addOne);
Function<Long, Long> addOneSquare = square.compose(addOne);
// Get an identity function
Function<Long, Long> identity = Function.<Long>identity();
// Test the functions
long num = 5L;
System.out.println("Number: " + num);
System.out.println("Square and then add one: " +
squareAddOne.apply(num));
System.out.println("Add one and then square: " +
addOneSquare.apply(num));
System.out.println("Identity: " + identity.apply(num));
Number: 5
Square and then add one: 26
Add one and then square: 36
Identity: 5
您并不局限于构建一个由两个以特定顺序执行的函数组成的函数。一个函数可以由任意多的函数组成。您可以链接 lambda 表达式,在一个表达式中创建一个组合函数。请注意,当您链接 lambda 表达式时,您可能需要向编译器提供提示,以解决可能出现的目标类型歧义。下面是一个通过链接三个函数组成的函数的示例。提供强制转换来帮助编译器。没有强制转换,编译器将无法推断目标类型:
// Square the input, add one to the result, and square
// the result
Function<Long, Long> chainedFunction =
((Function<Long, Long>)(x -> x * x))
.andThen(x -> x + 1)
.andThen(x -> x * x);
System.out.println(chainedFunction.apply(3L));
100
使用Predicate<T>界面
一个谓词表示一个条件,对于给定的输入,它要么是true要么是false。Predicate接口包含以下默认和静态方法,这些方法允许您使用逻辑 NOT、and 和 OR 基于其他谓词来组合一个谓词:
-
default Predicate<T> negate() -
default Predicate<T> and(Predicate<? super T> other) -
default Predicate<T> or(Predicate<? super T> other) -
static <T> Predicate<T> isEqual(Object targetRef)
negate()方法返回一个Predicate,它是原始谓词的逻辑否定。and()方法返回这个谓词和指定谓词的一个短路逻辑 AND 谓词。or()方法返回该谓词和指定谓词的短路逻辑 OR 谓词。isEqual()方法返回一个谓词,该谓词根据Objects.equals(Object o1, Object o2)测试指定的targetRef是否等于谓词的指定参数;如果两个输入是null,这个谓词的计算结果是true。您可以链接对这些方法的调用来创建复杂的谓词。下面的代码片段展示了一些创建和使用谓词的示例:
// Create some predicates
Predicate<Integer> greaterThanTen = x -> x > 10;
Predicate<Integer> divisibleByThree = x -> x % 3 == 0;
Predicate<Integer> divisibleByFive = x -> x % 5 == 0;
Predicate<Integer> equalToTen = Predicate.isEqual(null);
// Create predicates using NOT, AND, and OR on other
// predicates
Predicate<Integer> lessThanOrEqualToTen =
greaterThanTen.negate();
Predicate<Integer> divisibleByThreeAndFive =
divisibleByThree.and(divisibleByFive);
Predicate<Integer> divisibleByThreeOrFive =
divisibleByThree.or(divisibleByFive);
// Test the predicates
int num = 10;
System.out.println("Number: " + num);
System.out.println("greaterThanTen: " +
greaterThanTen.test(num));
System.out.println("divisibleByThree: " +
divisibleByThree.test(num));
System.out.println("divisibleByFive: " +
divisibleByFive.test(num));
System.out.println("lessThanOrEqualToTen: " +
lessThanOrEqualToTen.test(num));
System.out.println("divisibleByThreeAndFive: " +
divisibleByThreeAndFive.test(num));
System.out.println("divisibleByThreeOrFive: " +
divisibleByThreeOrFive.test(num));
System.out.println("equalsToTen: " +
equalToTen.test(num));
Number: 10
greaterThanTen: false
divisibleByThree: false
divisibleByFive: true
lessThanOrEqualToTen: true
divisibleByThreeAndFive: false
divisibleByThreeOrFive: true
equalsToTen: false
使用功能接口
两种不同类型的用户在两种环境中使用功能界面:
-
由库设计者设计 API
-
由库用户使用 API
功能接口被库设计者用来设计 API。它们用于在方法声明中声明参数的类型和返回类型。它们的使用方式与非功能性接口的使用方式相同(功能性接口从一开始就存在于 Java 中)。
库用户使用函数接口作为 lambda 表达式的目标类型。也就是说,当 API 中的方法将函数接口作为参数时,API 的用户应该使用 lambda 表达式来传递参数。使用 lambda 表达式的好处是使代码简洁,可读性更强。
在这一节中,我将向您展示如何使用函数接口设计 API,以及如何使用 lambda 表达式来使用 API。在为集合和流 API 设计 Java 库时,大量使用了函数接口。
在后面的例子中,我使用了一个枚举和两个类。清单 4-12 中显示的Gender枚举包含两个常量来表示一个人的性别。清单 4-13 中所示的Person类表示一个人;除了其他方法之外,它还包含一个返回人员列表的getPersons()方法。
// Person.java
package com.jdojo.lambda;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import static com.jdojo.lambda.Gender.MALE;
import static com.jdojo.lambda.Gender.FEMALE;
public class Person {
private String firstName;
private String lastName;
private LocalDate dob;
private Gender gender;
public Person(String firstName, String lastName,
LocalDate dob, Gender gender) {
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.gender = gender;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public LocalDate getDob() {
return dob;
}
public void setDob(LocalDate dob) {
this.dob = dob;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return firstName + " " + lastName + ", " +
gender + ", " + dob;
}
// A convenience method
public static List<Person> getPersons() {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person("John", "Jacobs",
LocalDate.of(1975, 1, 20), MALE));
list.add(new Person("Wally", "Inman",
LocalDate.of(1965, 9, 12), MALE));
list.add(new Person("Donna", "Jacobs",
LocalDate.of(1970, 9, 12), FEMALE));
return list;
}
}
Listing 4-13A Person Class
// Gender.java
package com.jdojo.lambda;
public enum Gender {
MALE, FEMALE
}
Listing 4-12A Gender enum
清单 4-14 中的FunctionUtil类是一个实用程序类。它的方法对一个List应用一个函数。List是一个由ArrayList类实现的接口。forEach()方法对列表中的每一项应用一个动作,通常会产生副作用;该动作由一个Consumer表示。filter()方法根据指定的Predicate过滤列表。map()方法使用Function将列表中的每一项映射到一个值。作为库设计者,您将使用函数接口设计这些方法。
// FunctionUtil.java
package com.jdojo.lambda;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class FunctionUtil {
// Applies an action on each item in a list
public static <T> void forEach(List<T> list,
Consumer<? super T> action) {
for (T item : list) {
action.accept(item);
}
}
// Applies a filter to a list and returns the
// filtered list items
public static <T> List<T> filter(List<T> list,
Predicate<? super T> predicate) {
List<T> filteredList = new ArrayList<>();
for (T item : list) {
if (predicate.test(item)) {
filteredList.add(item);
}
}
return filteredList;
}
// Maps each item in a list to a value
public static <T, R> List<R> map(List<T> list,
Function<? super T, R> mapper) {
List<R> mappedList = new ArrayList<>();
for (T item : list) {
mappedList.add(mapper.apply(item));
}
return mappedList;
}
}
Listing 4-14A FunctionUtil Class
现在,您将使用FunctionUtil类作为库用户,并使用函数接口作为 lambda 表达式的目标类型。清单 4-15 展示了如何使用FunctionUtil类。
// FunctionUtilTest.java
package com.jdojo.lambda;
import static com.jdojo.lambda.Gender.MALE;
import java.util.List;
public class FunctionUtilTest {
public static void main(String[] args) {
List<Person> list = Person.getPersons();
// Use the forEach() method to print each person
// in the list
System.out.println("Original list of persons:");
FunctionUtil.forEach(list, p ->
System.out.println(p));
// Filter only males
List<Person> maleList = FunctionUtil.filter(list,
p -> p.getGender() == MALE);
System.out.println("\nMales only:");
FunctionUtil.forEach(maleList,
p -> System.out.println(p));
// Map each person to his/her year of birth
List<Integer> dobYearList = FunctionUtil.map(list,
p -> p.getDob().getYear());
System.out.println("\nPersons mapped to year of " +
"their birth:");
FunctionUtil.forEach(dobYearList,
year -> System.out.println(year));
// Apply an action to each person in the list.
// Add one year to each male's dob
FunctionUtil.forEach(maleList,
p -> p.setDob(p.getDob().plusYears(1)));
System.out.println("\nMales only after adding " +
"1 year to DOB:");
FunctionUtil.forEach(maleList,
p -> System.out.println(p));
}
}
Original list of persons:
John Jacobs, MALE, 1975-01-20
Wally Inman, MALE, 1965-09-12
Donna Jacobs, FEMALE, 1970-09-12
Males only:
John Jacobs, MALE, 1975-01-20
Wally Inman, MALE, 1965-09-12
Persons mapped to year of their birth:
1975
1965
1970
Males only after adding 1 year to DOB:
John Jacobs, MALE, 1976-01-20
Wally Inman, MALE, 1966-09-12
Listing 4-15Using Functional Interfaces As Target Types of Lambda Expressions As Library Users
该程序获得一个人员列表,对该列表应用一个过滤器以获得一个只有男性的列表,将人员映射到他们的出生年份,并在每个男性的出生日期上加一年。它使用 lambda 表达式执行这些操作。注意代码的简洁;它只使用一行代码来执行每个操作。最值得注意的是使用了forEach()方法。这个方法采用了一个Consumer函数。然后每一项都被传递给这个函数。该函数可以对项目采取任何操作。您传递了一个在标准输出上打印项目的Consumer,如下所示:
FunctionUtil.forEach(list,
p -> System.out.println(p));
通常情况下,Consumer会对收到的物品施加一个动作来产生副作用。在这种情况下,它只是打印项目,而不会产生任何副作用。
方法引用
lambda 表达式表示被视为函数接口实例的匿名函数。方法引用是使用现有方法创建 lambda 表达式的一种快捷方式。使用方法引用使你的 lambda 表达式更易读、更简洁;它还允许您将现有方法用作 lambda 表达式。如果 lambda 表达式包含的主体是使用方法调用的表达式,则可以使用方法引用来代替该 lambda 表达式。
Note
方法引用不是 Java 中的新类型。它不是其他一些编程语言中使用的函数指针。它只是使用现有方法编写 lambda 表达式的简写。它只能在可以使用 lambda 表达式的地方使用。
在我解释方法引用的语法之前,让我们考虑一个例子。考虑以下代码片段:
import java.util.function.ToIntFunction;
...
ToIntFunction<String> lengthFunction = str ->
str.length();
String name = "Ellen";
int len = lengthFunction.applyAsInt(name);
System.out.println("Name = " + name +
", length = " + len);
Name = Ellen, length = 5
代码使用一个 lambda 表达式来定义一个匿名函数,该函数将一个String作为参数并返回它的长度。lambda 表达式的主体只包含一个方法调用,即String类的length()方法。您可以使用对String类的length()方法的方法引用来重写 lambda 表达式,如下所示:
import java.util.function.ToIntFunction;
...
ToIntFunction<String> lengthFunction = String::length;
String name = "Ellen";
int len = lengthFunction.applyAsInt(name);
System.out.println("Name = " + name +
", length = " + len);
Name = Ellen, length = 5
方法引用的一般语法是
<Qualifier>::<MethodName>
<Qualifier>取决于方法引用的类型。两个连续的冒号充当分隔符。<MethodName>是方法的名称。例如,在方法引用String::length中,String是限定符,length是方法名。
Note
方法引用在声明时不调用方法。稍后调用其目标类型的方法时,会调用该方法。
方法引用的语法只允许指定方法名。您不能指定该方法的参数类型和返回类型。回想一下,方法引用是 lambda 表达式的简写。目标类型通常是一个函数接口,它决定了方法的细节。如果该方法是重载方法,编译器将根据上下文选择最具体的方法。参见表 4-3 。
表 4-3
方法引用的类型
|句法
|
描述
|
| --- | --- |
| TypeName::staticMethod | 对类、接口或枚举的静态方法的方法引用。 |
| objectRef::instanceMethod | 对指定对象的实例方法的方法引用。 |
| ClassName::instanceMethod | 对指定类的任意对象的实例方法的方法引用。 |
| TypeName.super::instanceMethod | 对特定对象的超类型的实例方法的方法引用。 |
| ClassName::new | 对指定类的构造函数的构造函数引用。 |
| ArrayTypeName::new | 对指定数组类型的构造函数的数组构造函数引用。 |
使用方法引用在开始时可能会有点混乱。混淆的主要原因是将实际方法中参数的数量和类型映射到方法引用的过程。为了帮助理解语法,我在所有示例中使用了方法引用及其等效的 lambda 表达式。
静态方法引用
静态方法引用使用类型的静态方法作为 lambda 表达式。该类型可以是类、接口或枚举。考虑下面的Integer类的静态方法:
static String toBinaryString(int i)
toBinaryString()方法表示一个函数,它将一个int作为参数并返回一个String。您可以在 lambda 表达式中使用它,如下所示:
// Using a lambda expression
Function<Integer,String> func1 =
x -> Integer.toBinaryString(x);
System.out.println(func1.apply(17));
10001
编译器通过使用目标类型Function<Integer,String>,将x的类型推断为Integer,将 lambda 表达式的返回类型推断为String。
您可以使用静态方法引用重写该语句,如下所示:
// Using a method reference
Function<Integer, String> func2 =
Integer::toBinaryString;
System.out.println(func2.apply(17));
10001
编译器在赋值操作符的右边找到了对Integer类的toBinaryString()方法的静态方法引用。toBinaryString(方法将一个int作为参数,并返回一个String。方法引用的目标类型是一个以Integer为参数并返回String的函数。编译器验证在将目标类型的Integer参数类型解装箱到int之后,方法引用和目标类型是赋值兼容的。
考虑Integer类中的另一个静态方法sum():
static int sum(int a, int b)
方法引用应该是Integer::sum。让我们像在前面的例子中使用toBinaryString()方法一样使用它:
Function<Integer,Integer> func2 = Integer::sum;
// <- A compile-time error
Error: incompatible types: invalid
Function<Integer, Integer>
method sum in class Integer cannot
required: int,int
found: Integer
reason: actual and formal argument
method reference
func2 = Integer::sum;
be applied to given types
lists differ in length
错误消息指出方法引用Integer::sum与目标类型Function<Integer,Integer>的赋值不兼容。sum(int, int)方法有两个int参数,而目标类型只有一个Integer参数。参数数量不匹配导致了编译时错误。
要修复这个错误,方法引用Integer::sum的目标类型应该是一个函数接口,其抽象方法接受两个int参数并返回一个int。使用一个BiFunction<Integer,Integer, Integer>作为目标类型将有效。以下代码片段显示了如何使用方法引用Integer::sum以及等效的 lambda 表达式:
// Uses a lambda expression
BiFunction<Integer,Integer,Integer> func1 =
(x, y) -> Integer.sum(x, y);
System.out.println(func1.apply(17, 15));
// Uses a method reference
BiFunction<Integer,Integer,Integer> func2 =
Integer::sum;
System.out.println(func2.apply(17, 15));
32
32
让我们尝试使用Integer类的重载静态方法valueOf()的方法引用。该方法有三个版本:
-
static Integer valueOf(int i) -
static Integer valueOf(String s) -
static Integer valueOf(String s, int radix)
下面的代码片段展示了不同的目标类型将如何使用三个不同版本的Integer.valueOf()静态方法。读者可以练习使用 lambda 表达式编写以下代码片段:
// Uses Integer.valueOf(int)
Function<Integer,Integer> func1 = Integer::valueOf;
// Uses Integer.valueOf(String)
Function<String,Integer> func2 = Integer::valueOf;
// Uses Integer.valueOf(String, int)
BiFunction<String,Integer,Integer> func3 =
Integer::valueOf;
System.out.println(func1.apply(17));
System.out.println(func2.apply("17"));
System.out.println(func3.apply("10001", 2));
17
17
17
下面是这一类的最后一个例子。清单 4-13 中显示的Person类包含一个getPersons()静态方法,声明如下:
static List<Person> getPersons()
该方法不接受任何参数,并返回一个List<Person>。一个Supplier<T>表示一个没有参数的函数,返回一个T类型的结果。下面的代码片段使用方法引用Person::getPersons作为Supplier<List<Person>>:
Supplier<List<Person>> supplier = Person::getPersons;
List<Person> personList = supplier.get();
FunctionUtil.forEach(personList,
p -> System.out.println(p));
John Jacobs, MALE, 1975-01-20
Wally Inman, MALE, 1965-09-12
Donna Jacobs, FEMALE, 1970-09-12
实例方法引用
在对象的引用上调用实例方法。调用实例方法的对象引用被称为方法调用的接收者。方法调用的接收者可以是对象引用,也可以是计算结果为对象引用的表达式。下面的代码片段显示了String类的length()实例方法的接收者:
String name = "Kannan";
// name is the receiver of the length() method
int len1 = name.length();
// "Hello" is the receiver of the length() method
int len2 = "Hello".length();
// (new String("Kannan")) is the receiver of the length()
// method
int len3 = (new String("Kannan")).length();
在实例方法的方法引用中,可以显式指定方法调用的接收者,也可以在调用方法时隐式提供它。前者被称为绑定接收者,后者被称为非绑定接收者。实例方法引用的语法支持两种变体:
-
objectRef::instanceMethod -
ClassName::instanceMethod
对于绑定的接收者,使用objectRef::instanceMethod语法。考虑以下代码片段:
Supplier<Integer> supplier = () -> "Ellen".length();
System.out.println(supplier.get());
5
该语句使用一个 lambda 表达式来表示一个不带参数并返回一个int的函数。表达式主体使用一个名为“Ellen”的String对象来调用String类的length()实例方法。您可以使用实例方法引用重写该语句,将“Ellen”对象作为绑定接收者,并将Supplier<Integer>作为目标类型,如下所示:
Supplier<Integer> supplier = "Ellen"::length;
System.out.println(supplier.get());
5
考虑下面的代码片段来表示一个将String作为参数并返回void的Consumer<String>:
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("Hello");
Hello
这个 lambda 表达式调用了System.out对象上的println()方法。这可以用一个方法引用重写,用System.out作为绑定的接收者,如下所示:
Consumer<String> consumer = System.out::println;
consumer.accept("Hello");
Hello
当使用方法引用System.out::println时,编译器查看其目标类型,即Consumer<String>。它表示一个函数类型,该函数类型将一个String作为参数并返回void。编译器在System.out对象的PrintStream类中找到一个println(String)方法,并使用该方法作为方法引用。
作为这个类别的最后一个例子,您将使用方法引用System.out::println来打印人员列表,如下所示:
List<Person> list = Person.getPersons();
FunctionUtil.forEach(list, System.out::println);
John Jacobs, MALE, 1975-01-20
Wally Inman, MALE, 1965-09-12
Donna Jacobs, FEMALE, 1970-09-12
对于未绑定的接收器,使用ClassName::instanceMethod语法。考虑以下语句,其中 lambda 表达式将一个Person作为参数,并返回一个String:
Function<Person,String> fNameFunc =
(Person p) -> p.getFirstName();
可以使用实例方法引用重写该语句,如下所示:
Function<Person,String> fNameFunc = Person::getFirstName;
一开始,这是令人困惑的,原因有二:
-
语法与静态方法的方法引用的语法相同。
-
这就提出了一个问题:哪个对象是实例方法调用的接收者?
第一个困惑可以通过查看方法名并检查它是静态方法还是实例方法来消除。如果方法是实例方法,则方法引用表示实例方法引用。
第二个困惑可以通过记住一个规则来消除,即目标类型所表示的函数的第一个参数是方法调用的接收者。考虑一个名为String::length的实例方法引用,它使用了一个未绑定的接收器。接收者作为第一个参数提供给apply()方法,如下所示:
Function<String,Integer> strLengthFunc = String::length;
String name = "Ellen";
// name is the receiver of String::length
int len = strLengthFunc.apply(name);
System.out.println("name = " + name +
", length = " + len);
name = Ellen, length = 5
String类的实例方法concat()有如下声明:
String concat(String str)
方法引用String::concat代表一个目标类型的实例方法引用,其函数采用两个String参数并返回一个String。第一个参数将是concat()方法的接收者,第二个参数将被传递给concat()方法。以下代码片段显示了一个示例:
String greeting = "Hello";
String name = " Laynie";
// Uses a lambda expression
BiFunction<String,String,String> func1 =
(s1, s2) -> s1.concat(s2);
System.out.println(func1.apply(greeting, name));
// Uses an instance method reference on an unbound
// receiver
BiFunction<String,String,String> func2 = String::concat;
System.out.println(func2.apply(greeting, name));
Hello Laynie
Hello Laynie
作为这一类别的最后一个例子,您将使用方法引用Person::getFirstName,它是一个未绑定接收器上的实例方法引用,如下所示:
List<Person> personList = Person.getPersons();
// Maps each Person object to its first name
List<String> firstNameList = FunctionUtil.map(personList,
Person::getFirstName);
// Prints the first name list
FunctionUtil.forEach(firstNameList, System.out::println);
John
Wally
Donna
超类型实例方法引用
关键字super用作限定符来调用类或接口中被覆盖的方法。该关键字仅在实例上下文中可用。使用以下语法构造一个方法引用,该方法引用超类型中的实例方法和在当前实例上调用的方法:
TypeName.super::instanceMethod
考虑清单 4-16 和 4-17 中的Priced接口和Item类。Priced接口包含一个返回1.0的默认方法。Item类实现了Priced接口。它覆盖了Object类的toString()方法和Priced接口的getPrice()方法。我向Item类添加了三个构造函数,它们在标准输出中显示一条消息。我将在下一节的例子中使用它们。
// Item.java
package com.jdojo.lambda;
import java.util.function.Supplier;
public class Item implements Priced {
private String name = "Unknown";
private double price = 0.0;
public Item() {
System.out.println("Constructor Item() called.");
}
public Item(String name) {
this.name = name;
System.out.println("Constructor Item(String) " +
"called.");
}
public Item(String name, double price) {
this.name = name;
this.price = price;
System.out.println("Constructor " +
"Item(String, double) called.");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public double getPrice() {
return price;
}
@Override
public String toString() {
return "name = " + getName() +
", price = " + getPrice();
}
public void test() {
// Uses the Item.toString() method
Supplier<String> s1 = this::toString;
// Uses the Object.toString() method
Supplier<String> s2 = Item.super::toString;
// Uses the Item.getPrice() method
Supplier<Double> s3 = this::getPrice;
// Uses the Priced.getPrice() method
Supplier<Double> s4 = Priced.super::getPrice;
// Uses all method references and prints the
// results
System.out.println("this::toString: " + s1.get());
System.out.println("Item.super::toString: " +
s2.get());
System.out.println("this::getPrice: " + s3.get());
System.out.println("Priced.super::getPrice: " +
s4.get());
}
}
Listing 4-17An Item Class That Implements the Priced Interface
// Priced.java
package com.jdojo.lambda;
public interface Priced {
default double getPrice() {
return 1.0;
}
}
Listing 4-16A Priced Interface with a Default Method of getPrice()
Item类中的test()方法使用四个方法引用和一个绑定的接收器。接收者是对其调用test()方法的Item对象。
-
方法引用
this::toString指的是Item类的toString()方法。 -
方法引用
Item.super::toString指的是Object类的toString()方法,它是Item类的超类。 -
方法引用
this::getPrice指的是Item类的getPrice()方法。 -
方法引用
Priced.super::getPrice指的是定价接口的getPrice()方法,它是Item类的超接口。
清单 4-18 中的程序创建一个Item类的对象并调用它的test()方法。输出显示了四个方法引用正在使用的方法。
// ItemTest.java
package com.jdojo.lambda;
public class ItemTest {
public static void main(String[] args) {
Item apple = new Item("Apple", 0.75);
apple.test();
}
}
Constructor Item(String, double) called.
this::toString: name = Apple, price = 0.75
Item.super::toString: com.jdojo.lambda.Item@24d46ca6
this::getPrice: 0.75
Priced.super::getPrice: 1.0
Listing 4-18Testing the Item Class
构造函数引用
有时候,lambda 表达式的主体可能只是一个对象创建表达式。考虑下面两个使用String对象创建表达式作为 lambda 表达式主体的语句:
Supplier<String> func1 = () -> new String();
Function<String,String> func2 = str -> new String(str);
您可以通过用构造函数引用替换 lambda 表达式来重写这些语句,如下所示:
Supplier<String> func1 = String::new;
Function<String,String> func2 = String::new;
使用构造函数的语法如下:
-
ClassName::new -
ArrayTypeName::new
ClassName::new中的ClassName是可以实例化的类的名称;它不能是抽象类的名称。关键字new指的是类的构造函数。一个类可以有多个构造函数。语法没有提供引用特定构造函数的方法。编译器根据上下文选择特定的构造函数。它查看目标类型和目标类型的抽象方法中的参数数量。选择其参数数量与目标类型的抽象方法中的参数数量相匹配的构造函数。考虑下面的代码片段,它在 lambda 表达式中使用了清单 4-17 中所示的Item类的三个构造函数:
Supplier<Item> func1 = () -> new Item();
Function<String,Item> func2 = name -> new Item(name);
BiFunction<String,Double,Item> func3 =
(name, price) -> new Item(name, price);
System.out.println(func1.get());
System.out.println(func2.apply("Apple"));
System.out.println(func3.apply("Apple", 0.75));
Constructor Item() called.
name = Unknown, price = 0.0
Constructor Item(String) called.
name = Apple, price = 0.0
Constructor Item(String, double) called.
name = Apple, price = 0.75
下面的代码片段用构造函数引用Item::new替换了 lambda 表达式。输出显示了与之前相同的构造函数:
Supplier<Item> func1 = Item::new;
Function<String,Item> func2 = Item::new;
BiFunction<String,Double,Item> func3 = Item::new;
System.out.println(func1.get());
System.out.println(func2.apply("Apple"));
System.out.println(func3.apply("Apple", 0.75));
Constructor Item() called.
name = Unknown, price = 0.0
Constructor Item(String) called.
name = Apple, price = 0.0
Constructor Item(String, double) called.
name = Apple, price = 0.75
当声明
Supplier<Item> func1 = Item::new;
时,编译器发现目标类型Supplier<Item>不接受参数。因此,它使用了Item类的无参数构造函数。当声明
Function<String,Item> func2 = Item::new;
时,编译器发现目标类型Function<String,Item>带有一个String参数。因此,它使用接受String参数的Item类的构造函数。当声明
BiFunction<String,Double,Item> func3 = Item::new;
执行时,编译器发现目标类型BiFunction<String,Double,Item>有两个参数:一个String和一个Double。因此,它使用了接受一个String和一个double参数的Item类的构造函数。
以下语句生成一个编译时错误,因为编译器在接受Double参数的Item类中找不到构造函数:
Function<Double,Item> func4 = Item::new;
// <- A compile-time error
Java 中的数组没有构造函数。使用数组的构造函数引用有一个特殊的语法。数组构造函数被视为有一个 int 类型的参数,即数组的大小。以下代码片段显示了 lambda 表达式及其对一个int数组的等效构造函数引用:
// Uses a lambda expression
IntFunction<int[]> arrayCreator1 = size -> new int[size];
int[] empIds1 = arrayCreator1.apply(5);
// <- Creates an int array of five elements
// Uses an array constructor reference
IntFunction<int[]> arrayCreator2 = int[]::new;
int[] empIds2 = arrayCreator2.apply(5);
// <- Creates an int array of five elements
您还可以使用Function<Integer,R>类型来使用数组构造函数引用,其中R是数组类型:
// Uses an array constructor reference
Function<Integer,int[]> arrayCreator3 = int[]::new;
int[] empIds3 = arrayCreator3.apply(5);
// <- Creates an int array of five elements
数组的构造函数引用的语法支持创建多维数组。但是,您只能指定第一个尺寸的长度。以下语句创建一个二维 int 数组,其中第一维的长度为 5:
// Uses an array constructor reference
IntFunction<int[][]> TwoDimArrayCreator = int[][]::new;
int[][] matrix = TwoDimArrayCreator.apply(5);
// <- Creates an int[5][] array
您可能会尝试使用BiFunction<Integer,Integer,int[][]>来使用二维数组的构造函数引用来提供两个维度的长度。但是,不支持该语法。数组构造函数应该只接受一个参数——第一维的长度。以下语句会生成编译时错误:
BiFunction<Integer,Integer,int[][]> arrayCreator =
int[][]::new;
泛型方法引用
通常,当方法引用引用泛型方法时,编译器会计算出泛型类型参数的实际类型。考虑下面的java.util.Arrays类中的泛型方法:
static <T> List<T> asList(T... a)
asList()方法接受一个T类型的varargs参数并返回一个List<T>。你可以使用Arrays::asList作为方法参考。方法引用的语法允许您在两个连续的冒号后指定方法的实际类型参数。例如,如果将String对象传递给asList()方法,其方法引用可以写成Arrays::<String>asList。
Note
方法引用的语法还支持为泛型类型指定实际的类型参数。实际的类型参数是在两个连续的冒号之前指定的。例如,构造函数引用ArrayList<Long>::new指定Long作为通用ArrayList<T>类的实际类型参数。
下面的代码片段包含一个为泛型方法Arrays.asList()指定实际类型参数的示例。在代码中,Arrays::asList将同样工作,因为编译器将通过检查目标类型来推断String作为asList()方法的类型参数:
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
...
Function<String[],List<String>> asList =
Arrays::<String>asList;
String[] namesArray = {"Jim", "Ken", "Li"};
List<String> namesList = asList.apply(namesArray);
for(String name : namesList) {
System.out.println(name);
}
Jim
Ken
Li
词法范围
作用域是 Java 程序的一部分,在其中可以使用不带限定符的名字。类和方法定义了它们自己的作用域。范围可以是嵌套的。例如,一个方法作用域不是独立存在的,因为一个方法总是另一个构造的一部分,例如,一个类;内部类出现在另一个类的范围内;局部类和匿名类出现在方法的范围内。
尽管 lambda 表达式看起来像一个方法声明,但它并没有定义自己的作用域。它存在于它的封闭范围内。这被称为 lambda 表达式的词法范围。例如,当在方法内部使用 lambda 表达式时,lambda 表达式存在于方法的范围内。
关键字this和super在 lambda 表达式及其封闭方法中的含义是相同的。请注意,这与本地匿名内部类中这些关键字的含义不同,在本地匿名内部类中,关键字this指的是本地匿名内部类的当前实例,而不是其封闭类。
清单 4-19 包含一个名为Printer的函数接口的代码,您将使用它来打印本节示例中的消息。
// Printer.java
package com.jdojo.lambda;
@FunctionalInterface
public interface Printer {
void print(String msg);
}
Listing 4-19A Printer Functional Interface
清单 4-20 中的程序创建了Printer接口的两个实例:一个在getLambdaPrinter()方法中使用 lambda 表达式,另一个在getAnonymousPrinter()方法中使用匿名内部类。两个实例都在print()方法中使用关键字this。这两种方法都打印关键字this引用的类名。输出显示关键字this在getLambdaPrinter()方法和 lambda 表达式中具有相同的含义。然而,关键字this在getAnonymousPrinter()方法和匿名类中有不同的含义。
// ScopeTest.java
package com.jdojo.lambda;
public class ScopeTest {
public static void main(String[] args) {
ScopeTest test = new ScopeTest();
Printer lambdaPrinter = test.getLambdaPrinter();
lambdaPrinter.print("Lambda Expressions");
Printer anonymousPrinter = test.
getAnonymousPrinter();
anonymousPrinter.print("Anonymous Class");
}
public Printer getLambdaPrinter() {
System.out.println("getLambdaPrinter(): " +
this.getClass());
// Uses a lambda expression
Printer printer = msg -> {
// Here, this refers to the current object
// of the ScopeTest class
System.out.println(msg + ": " +
this.getClass());
};
return printer;
}
public Printer getAnonymousPrinter() {
System.out.println("getAnonymousPrinter(): " +
this.getClass());
// Uses an anonymous class
Printer printer = new Printer() {
@Override
public void print(String msg) {
// Here, this refers to the current
// object of the anonymous class
System.out.println(msg + ": " +
this.getClass());
}
};
return printer;
}
}
getLambdaPrinter(): class com.jdojo.lambda.ScopeTest
Lambda Expressions: class com.jdojo.lambda.ScopeTest
getAnonymousPrinter(): class com.jdojo.lambda.ScopeTest
Anonymous Class: class com.jdojo.lambda.ScopeTest\$1
Listing 4-20Testing Scope of a Lambda Expression and an Anonymous Class
lambda 表达式的词法范围意味着 lambda 表达式中声明的变量(包括其参数)存在于封闭范围内。范围中的简单名称必须是唯一的。这意味着 lambda 表达式不能用封闭范围内已经存在的名称重新定义变量。
main()方法中 lambda 表达式的以下代码生成了一个编译时错误,因为它的参数名msg已经在main()方法的作用域中定义了:
public class Test {
public static void main(String[] args) {
String msg = "Hello";
// A compile-time error. The msg variable is
// already defined and the lambda parameter is
// attempting to redefine it.
Printer printer = msg -> System.out.println(msg);
}
}
以下代码生成编译时错误的原因与名为msg的局部变量在 lambda 表达式主体内的范围内相同,并且 lambda 表达式试图用相同的名称msg声明局部变量:
public class Test {
public static void main(String[] args) {
String msg = "Hello";
Printer printer = msg1 -> {
String msg = "Hi"; // A compile-time error
System.out.println(msg1);
};
}
}
可变捕获
像局部和匿名内部类一样,lambda 表达式可以有效地访问最终局部变量。在以下两种情况下,局部变量实际上是最终变量:
-
声明为
final。 -
没有声明
final,只是初始化一次。
在下面的代码片段中,msg变量实际上是 final,因为它已经被声明为final。lambda 表达式访问其主体内部的变量:
public Printer test() {
final String msg = "Hello"; // msg is effectively final
Printer printer = msg1 -> System.out.println(msg +
" " + msg1);
return printer;
}
在下面的代码片段中,msg变量实际上是 final 变量,因为它被初始化了一次。lambda 表达式访问其主体内部的变量:
public Printer test() {
String msg = "Hello"; // msg is effectively final
Printer printer = msg1 ->
System.out.println(msg + " " + msg1);
return printer;
}
下面的代码片段与前面的示例略有不同。msg变量实际上是最终变量,因为它只被初始化过一次:
public Printer test() {
String msg;
msg = "Hello"; // msg is effectively final
Printer printer = msg1 ->
System.out.println(msg + " " + msg1);
return printer;
}
在下面的代码片段中,msg变量实际上不是最终变量,因为它被赋值了两次。lambda 表达式正在访问生成编译时错误的msg变量:
public Printer test() {
// msg is not effectively final as it is changed later
String msg = "Hello";
// A compile-time error
Printer printer = msg1 ->
System.out.println(msg + " " + msg1);
msg = "Hi";
// <- msg is changed making it effectively non-final
return printer;
}
以下代码片段生成了一个编译时错误,因为 lambda 表达式访问了在使用后按词法声明的msg变量。在 Java 中,不允许在方法范围内向前引用变量名。请注意,msg变量实际上是最终变量。
public Printer test() {
// A compile-time error. The msg variable is not
// declared yet.
Printer printer = msg1 ->
System.out.println(msg + " " + msg1);
String msg = "Hello"; // msg is effectively final
return printer;
}
您能猜到为什么下面的代码片段会产生编译时错误吗?
public Printer test() {
String msg = "Hello";
Printer printer = msg1 -> {
msg = "Hi " + msg1; // A compile-time error.
// Attempting to modify msg.
System.out.println(msg);
};
return printer;
}
Lambda 表达式访问局部变量msg。在 lambda 表达式中访问的任何局部变量实际上都必须是 final。lambda 表达式试图修改其主体内的msg变量,这会导致编译时错误。
Note
lambda 表达式可以访问一个类的实例和类变量,不管它们是否是最终的。如果实例和类变量不是最终的,它们可以在 lambda 表达式的主体中修改。lambda 表达式保留了其主体中使用的局部变量的副本。如果局部变量是引用变量,则保留引用的副本,而不是对象的副本。
清单 4-21 中的程序演示了如何访问 lambda 表达式中的局部变量和实例变量。
// VariableCapture.java
package com.jdojo.lambda;
public class VariableCapture {
private int counter = 0;
public static void main(String[] args) {
VariableCapture vc1 = new VariableCapture();
VariableCapture vc2 = new VariableCapture();
// Create lambdas
Printer p1 = vc1.createLambda(1);
Printer p2 = vc2.createLambda(100);
// Execute the lambda bodies
p1.print("Lambda #1");
p2.print("Lambda #2");
p1.print("Lambda #1");
p2.print("Lambda #2");
p1.print("Lambda #1");
p2.print("Lambda #2");
}
public Printer createLambda(int incrementBy) {
Printer printer = msg -> {
// Accesses instance and local variables
counter += incrementBy;
System.out.println(msg + ": counter = " +
counter);
};
return printer;
}
}
Lambda #1: counter = 1
Lambda #2: counter = 100
Lambda #1: counter = 2
Lambda #2: counter = 200
Lambda #1: counter = 3
Lambda #2: counter = 300
Listing 4-21Accessing Local and Instance Variables Inside Lambda Expressions
createLambda()方法使用 lambda 表达式来创建Printer函数接口的实例。lambda 表达式使用方法的参数incrementBy。在主体内部,它增加实例变量counter并打印其值。main()方法创建了VariableCapture类的两个实例,并通过将1和100作为incrementBy值传递来调用这些实例上的createLambda()方法。对于这两个实例,Printer对象的print()方法被调用了三次。输出显示,lambda 表达式捕获了incrementBy值,并在每次调用时递增counter实例变量。
跳跃和退出
诸如break、continue、return和throw之类的语句允许出现在 lambda 表达式的主体中。这些语句表示方法内部的跳转和方法的退出。在 lambda 表达式中,它们表示 lambda 表达式体中的跳转和从 lambda 表达式体中退出。它们表示 lambda 表达式中的局部跳转和退出。lambda 表达式中不允许非局部跳转和退出。清单 4-22 中的程序演示了在 lambda 表达式体中有效使用break和continue语句。
// LambdaJumps.java
package com.jdojo.lambda;
import java.util.function.Consumer;
public class LambdaJumps {
public static void main(String[] args) {
Consumer<int[]> printer = ids -> {
int printedCount = 0;
for (int id : ids) {
if (id % 2 != 0) {
continue;
}
System.out.println(id);
printedCount++;
// Break out of the loop after printing 3
// ids
if (printedCount == 3) {
break;
}
}
};
// Print an array of 8 integers
printer.accept(new int[]{1, 2, 3, 4, 5, 6, 7, 8});
}
}
2
4
6
Listing 4-22Using break and continue Statements Inside the Body of a Lambda Expression
在下面的代码片段中,break语句位于for loop 语句中,也位于 lambda 语句体中。如果这个break语句被允许,它将跳出 lambda 表达式的主体。这就是代码生成编译时错误的原因:
public void test() {
for(int i = 0; i < 5; i++) {
Consumer<Integer> evenIdPrinter = id -> {
if (id < 0) {
// A compile-time error. Attempting to
// break out of the lambda body
break;
}
};
}
}
递归 Lambda 表达式
有时,一个函数可能从它的主体中调用它自己。这样的函数称为递归函数。Lambda 表达式代表一个函数。然而,lambda 表达式不支持递归调用。如果需要递归函数,需要使用方法引用或者匿名内部类。
清单 4-23 中的程序展示了当需要递归 lambda 表达式时如何使用方法引用。它定义了一个名为factorial()的递归方法来计算一个整数的阶乘。在main()方法中,它使用方法引用RecursiveTest::factorial来代替 lambda 表达式。
// RecursiveTest.java
package com.jdojo.lambda;
import java.util.function.IntFunction;
public class RecursiveTest {
public static void main(String[] args) {
IntFunction<Long> factorialCalc =
RecursiveTest::factorial;
int n = 5;
long fact = factorialCalc.apply(n);
System.out.println("Factorial of " + n +
" is " + fact);
}
public static long factorial(int n) {
if (n < 0) {
String msg = "Number must not be negative.";
throw new IllegalArgumentException(msg);
}
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
}
factorial of 5 is 120
Listing 4-23Using a Method Reference When a Recursive Lambda Expression Is Needed
您可以使用匿名内部类获得相同的结果,如下所示:
IntFunction<Long> factorialCalc = new IntFunction<Long>() {
@Override
public Long apply(int n) {
if (n < 0) {
String msg = "Number must not be negative.";
throw new IllegalArgumentException(msg);
}
if (n == 0) {
return 1L;
} else {
return n * this.apply(n - 1);
}
}
};
比较对象
Comparator接口是一个函数接口,声明如下:
package java.util;
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
/* Other methods are not shown. */
}
Comparator<T>接口包含许多默认和静态方法,可以与 lambda 表达式一起使用来创建它的实例。值得研究该接口的 API 文档。在这一节中,我讨论了下面两个Comparator接口的方法:
-
static <T,U extends Comparable<? super U» Comparator<T> comparing(Function<? super T,? extends U> keyExtractor) -
default <U extends Comparable<? super U» Comparator<T> thenComparing(Function<? super T,? extends U> keyExtractor)
comparing()方法接受一个Function并返回一个Comparator。Function应该返回一个用于比较两个对象的Comparable。您可以创建一个Comparator对象来根据名字比较Person对象,如下所示:
Comparator<Person> firstNameComp =
Comparator.comparing(Person::getFirstName);
thenComparing()方法是默认方法。如果两个对象在基于主比较的排序顺序上相同,则使用它来指定次比较。下面的语句创建了一个Comparator<Person>,它根据Person对象的姓、名和 DOB 对这些对象进行排序:
Comparator<Person> lastFirstDobComp =
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparing(Person::getDob);
清单 4-24 中的程序展示了如何使用方法引用来创建一个Comparator对象来排序Person对象。它使用List接口的sort()默认方法对人员列表进行排序。sort()方法将一个Comparator作为参数。感谢 lambda 表达式和接口中的默认方法让排序任务变得如此简单!
/ ComparingObjects.java
package com.jdojo.lambda;
import java.util.Comparator;
import java.util.List;
public class ComparingObjects {
public static void main(String[] args) {
List<Person> persons = Person.getPersons();
// Sort using the first name
persons.sort(Comparator.comparing(
Person::getFirstName));
// Print the sorted list
System.out.println("Sorted by the first name:");
FunctionUtil.forEach(persons, System.out::println);
// Sort using the last name, first name, and then
// DOB
persons.sort(Comparator.comparing(
Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparing(Person::getDob));
// Print the sorted list
System.out.println("\nSorted by the last name, " +
"first name, and dob:");
FunctionUtil.forEach(persons, System.out::println);
}
}
Sorted by the first name:
Donna Jacobs, FEMALE, 1970-09-12
John Jacobs, MALE, 1975-01-20
Wally Inman, MALE, 1965-09-12
Sorted by the last name, first name, and dob:
Wally Inman, MALE, 1965-09-12
Donna Jacobs, FEMALE, 1970-09-12
John Jacobs, MALE, 1975-01-20
Listing 4-24Sorting a List of Person Objects
摘要
lambda 表达式是一个未命名的代码块(或未命名的函数),带有一个形参列表和一个主体。与匿名内部类相比,lambda 表达式提供了一种简洁的方式来创建函数接口的实例。就 Java 编程的表现力和流畅性而言,Lambda 表达式和接口中的默认方法赋予了 Java 编程语言新的生命。Java 集合库从 lambda 表达式中受益最大。
定义 lambda 表达式的语法类似于声明方法。lambda 表达式可能有一个形参列表和一个主体。lambda 表达式被计算为函数接口的实例。计算表达式时,不执行 lambda 表达式的主体。当调用函数接口的方法时,执行 lambda 表达式的主体。
lambda 表达式的设计目标之一是保持它的简洁和可读性。lambda 表达式语法支持常见用例的简写。方法引用是指定使用现有方法的 lambda 表达式的简写。
多边形表达式是一种类型取决于其使用上下文的表达式。lambda 表达式始终是 poly 表达式。lambda 表达式不能单独使用。它的类型由编译器从上下文中推断出来。lambda 表达式可用于赋值、方法调用、返回和强制转换。
当 lambda 表达式出现在方法内部时,它是词汇范围的。也就是说,lambda 表达式不定义自己的范围;相反,它发生在方法的范围内。lambda 表达式可以使用方法的有效最终局部变量。lambda 表达式可以使用诸如break、continue、return和throw之类的语句。break和continue语句指定了 lambda 表达式主体内部的局部跳转。试图跳转到 lambda 表达式的主体之外会产生编译时错误。return和throw语句退出 lambda 表达式的主体。
练习
练习 1
什么是 lambda 表达式,它们与函数接口有什么关系?
练习 2
lambda 表达式和匿名类有什么不同?你能总是用匿名类替换 lambda 表达式吗,反之亦然?
运动 3
下面两个 lambda 表达式有区别吗?
a. (int x, int y) -> { return x + y; }
b. (int x, int y) -> x + y
演习 4
如果有人向您展示以下 lambda 表达式,请解释它们可能代表的功能:
a. (int x, int y) -> x + y \\
b. (x, y) -> x + y \\
c. (String msg) -> { System.out.println(msg); }\\
d. () -> {}
锻炼 5
下面的 lambda 表达式可能代表哪种函数?
x -> x;
锻炼 6
下面一个MathUtil接口的声明会编译吗?解释你的答案。
@FunctionalInterface
public interface Operations {
int factorial(int n);
int abs(int n);
}
锻炼 7
下面的语句会编译吗?解释你的答案。
Object obj = x -> x + 1;
运动 8
下面的语句会编译吗?解释你的答案。
Function<Integer,Integer> f = x -> x + 1;
Object obj = f;
演习 9
当您运行下面的Scope类时,输出会是什么?
// Scope.java
package com.jdojo.lambda.exercises;
import java.util.function.Function;
public class Scope {
private static long n = 100;
private static Function<Long,Long> f = n -> n + 1;
public static void main(String[] args) {
System.out.println(n);
System.out.println(f.apply(n));
System.out.println(n);
}
}
运动 10
为什么下面的方法声明不编译?
public static void test() {
int n = 100;
Function<Integer,Integer> f = n -> n + 1;
System.out.println(f.apply(100));
}
演习 11
当下面的Capture类运行时,输出会是什么?
// Capture.java
package com.jdojo.lambda.exercises;
import java.util.function.Function;
public class Capture {
public static void main(String[] args) {
test();
test();
}
public static void test() {
int n = 100;
Function<Integer,Integer> f = x -> n + 1;
System.out.println(f.apply(100));
}
}
运动 12
假设有一个Person类,它包含四个构造函数。其中一个构造函数是无参数构造函数。给定一个构造函数引用,Person::new,你能说出它引用的是Person的哪个构造函数吗?
运动 13
下面的FeelingLucky接口声明会编译吗?请注意,它已经用@FunctionalInterface进行了注释。
@FunctionalInterface
public interface FeelingLucky {
void gamble();
public static void hitJackpot() {
System.out.println("You have won 80M dollars.");
}
}
运动 14
为什么下面的Mystery接口声明不编译?
@FunctionalInterface
public interface Mystery {
@Override
String toString();
}
运动 15
当下面的PredicateTest类运行时,输出会是什么?
// PredicateTest.java
package com.jdojo.lambda.exercises;
import java.util.function.Predicate;
public class PredicateTest {
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4, 5};
filterThenPrint(nums, n -> n%2 == 0);
filterThenPrint(nums, n -> n%2 == 1);
}
static void filterThenPrint(int[] nums,
Predicate<Integer> p) {
for(int x : nums) {
if(p.test(x)) {
System.out.println(x);
}
}
}
}
演习 16
当下面的SupplierTest类运行时,输出会是什么?解释你的答案。
/ SupplierTest.java
package com.jdojo.lambda.exercises;
import java.util.function.Supplier;
public class SupplierTest {
public static void main(String[] args) {
Supplier<Integer> supplier = () -> {
int counter = 0;
return ++counter;
};
System.out.println(supplier.get());
System.out.println(supplier.get());
}
}
演习 17
当下面的ConsumerTest类运行时,输出会是什么?
// ConsumerTest.java
package com.jdojo.lambda.exercises;
import java.util.function.Consumer;
public class ConsumerTest {
public static void main(String[] args) {
Consumer<String> c1 = System.out::println;
Consumer<String> c2 = s -> {};
consume(c1, "Hello");
consume(c2, "Hello");
}
static <T> void consume(Consumer<T> consumer,
T item) {
consumer.accept(item);
}
}