原文:
docs.oracle.com/javase/tutorial/reallybigindex.html
对象
原文:
docs.oracle.com/javase/tutorial/java/javaOO/objects.html
一个典型的 Java 程序会创建许多对象,正如您所知,这些对象通过调用方法进行交互。通过这些对象之间的交互,程序可以执行各种任务,比如实现 GUI、运行动画,或者在网络上传输和接收信息。一旦一个对象完成了它被创建的工作,它的资源就会被回收以供其他对象使用。
这里有一个小程序,名为CreateObjectDemo
,它创建了三个对象:一个Point
对象和两个Rectangle
对象。您需要这三个源文件来编译这个程序。
public class CreateObjectDemo {
public static void main(String[] args) {
// Declare and create a point object and two rectangle objects.
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
// display rectOne's width, height, and area
System.out.println("Width of rectOne: " + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);
System.out.println("Area of rectOne: " + rectOne.getArea());
// set rectTwo's position
rectTwo.origin = originOne;
// display rectTwo's position
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
// move rectTwo and display its new position
rectTwo.move(40, 72);
System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
}
}
该程序创建、操作并显示有关各种对象的信息。以下是输出:
Width of rectOne: 100
Height of rectOne: 200
Area of rectOne: 20000
X Position of rectTwo: 23
Y Position of rectTwo: 94
X Position of rectTwo: 40
Y Position of rectTwo: 72
以下三个部分使用上述示例来描述程序中对象的生命周期。通过它们,您将学习如何编写代码来在您自己的程序中创建和使用对象。您还将了解系统在对象的生命周期结束时如何清理。
创建对象
原文:
docs.oracle.com/javase/tutorial/java/javaOO/objectcreation.html
正如你所知,一个类提供了对象的蓝图;你从一个类创建一个对象。以下语句取自 CreateObjectDemo
程序,每个语句创建一个对象并将其分配给一个变量:
Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);
第一行创建了一个 Point
类的对象,第二行和第三行分别创建了一个 Rectangle
类的对象。
这些语句中的每一个都有三个部分(下面详细讨论):
-
声明:加粗的代码都是将变量名与对象类型关联的变量声明。
-
实例化:
new
关键字是一个创建对象的 Java 操作符。 -
初始化:
new
操作符后跟一个构造函数调用,用于初始化新对象。
声明一个变量来引用一个对象
之前,你学到声明一个变量时,写的是:
*type name*;
这通知编译器,你将使用 name 来引用类型为 type 的数据。对于原始变量,这个声明也为变量保留了正确数量的内存。
你也可以单独在一行上声明一个引用变量。例如:
Point originOne;
如果你这样声明 originOne
,它的值将在实际创建对象并分配给它之前是不确定的。仅仅声明一个引用变量并不会创建一个对象。为此,你需要使用 new
操作符,如下一节所述。在你的代码中使用 originOne
之前,你必须为其分配一个对象。否则,你将会得到一个编译器错误。
处于这种状态的变量,当前没有引用任何对象,可以用以下方式表示(变量名 originOne
,加上指向空的引用):
实例化一个类
new
操作符通过为新对象分配内存并返回对该内存的引用来实例化一个类。new
操作符还调用对象的构造函数。
**注意:**短语“实例化一个类”与“创建一个对象”意思相同。当你创建一个对象时,你正在创建一个类的“实例”,因此“实例化”一个类。
new
操作符需要一个后缀参数:一个构造函数调用。构造函数的名称提供了要实例化的类的名称。
new
操作符返回一个引用指向它创建的对象。这个引用通常赋给适当类型的变量,如:
Point originOne = new Point(23, 94);
new
操作符返回的引用不一定要赋给一个变量。它也可以直接在表达式中使用。例如:
int height = new Rectangle().height;
这个语句将在下一节讨论。
初始化一个对象
这是 Point
类的代码:
public class Point {
public int x = 0;
public int y = 0; //constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
这个类包含一个构造函数。你可以通过构造函数的声明使用与类相同的名称且没有返回类型来识别构造函数。Point
类中的构造函数接受两个整数参数,如代码(int a, int b)
所声明。下面的语句为这些参数提供了值 23 和 94:
Point originOne = new Point(23, 94);
执行此语句的结果可以在下图中说明:
这是Rectangle
类的代码,其中包含四个构造函数:
public class Rectangle {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
public Rectangle() {
origin = new Point(0, 0);
}
public Rectangle(Point p) {
origin = p;
}
public Rectangle(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public Rectangle(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// a method for moving the rectangle
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
}
每个构造函数都允许你使用基本类型和引用类型为矩形的原点、宽度和高度提供初始值。如果一个类有多个构造函数,它们必须具有不同的签名。Java 编译器根据参数的数量和类型区分构造函数。当 Java 编译器遇到下面的代码时,它知道要调用Rectangle
类中需要一个Point
参数后跟两个整数参数的构造函数:
Rectangle rectOne = new Rectangle(originOne, 100, 200);
这调用了Rectangle
的一个构造函数,将origin
初始化为originOne
。此外,构造函数将width
设置为 100,height
设置为 200。现在有两个指向同一个Point 对象
的引用—一个对象可以有多个引用,如下图所示:
下面的代码行调用了需要两个整数参数的Rectangle
构造函数,这些参数为width
和height
提供了初始值。如果你检查构造函数内的代码,你会看到它创建了一个新的Point
对象,其x
和y
值被初始化为 0:
Rectangle rectTwo = new Rectangle(50, 100);
下面语句中使用的Rectangle
构造函数不带任何参数,因此被称为无参数构造函数:
Rectangle rect = new Rectangle();
所有类至少有一个构造函数。如果一个类没有明确声明任何构造函数,Java 编译器会自动提供一个无参数构造函数,称为默认构造函数。这个默认构造函数调用类父类的无参数构造函数,或者如果类没有其他父类,则调用Object
构造函数。如果父类没有构造函数(Object
有一个),编译器会拒绝程序。
使用对象
译文:
docs.oracle.com/javase/tutorial/java/javaOO/usingobject.html
创建对象后,您可能想要对其进行某些操作。您可能需要使用其中一个字段的值,更改其中一个字段,或调用其中一个方法执行操作。
引用对象的字段
通过它们的名称访问对象字段。您必须使用一个不含糊的名称。
您可以在其自身类中使用字段的简单名称。例如,我们可以在Rectangle
类中添加一个语句并打印width
和height
:
System.out.println("Width and height are: " + width + ", " + height);
在这种情况下,width
和height
是简单名称。
在对象的类之外的代码必须使用对象引用或表达式,后跟点(.)运算符,后跟一个简单的字段名称,如:
objectReference.fieldName
例如,CreateObjectDemo
类中的代码位于Rectangle
类的代码之外。因此,要引用Rectangle
对象rectOne
中的origin
、width
和height
字段,CreateObjectDemo
类必须分别使用名称rectOne.origin
、rectOne.width
和rectOne.height
。程序使用这两个名称来显示rectOne
的width
和height
:
System.out.println("Width of rectOne: " + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);
尝试在CreateObjectDemo
类中的代码中使用简单名称width
和height
是没有意义的 — 这些字段只存在于对象内部 — 并且会导致编译器错误。
稍后,程序使用类似的代码来显示有关rectTwo
的信息。相同类型的对象具有自己的相同实例字段的副本。因此,每个Rectangle
对象都有名为origin
、width
和height
的字段。当您通过对象引用访问实例字段时,您引用特定对象的字段。CreateObjectDemo
程序中的两个对象rectOne
和rectTwo
具有不同的origin
、width
和height
字段。
要访问字段,您可以使用一个命名引用对象,就像前面的例子中那样,或者您可以使用任何返回对象引用的表达式。请记住,new
运算符返回一个对象的引用。因此,您可以使用从new
返回的值来访问新对象的字段:
int height = new Rectangle().height;
此语句创建一个新的Rectangle
对象并立即获取其高度。实质上,该语句计算了Rectangle
的默认高度。请注意,在执行此语句后,程序不再具有对创建的Rectangle
的引用,因为程序从未将引用存储在任何地方。该对象没有引用,其资源可以被 Java 虚拟机回收。
调用对象的方法
您还可以使用对象引用来调用对象的方法。您将方法的简单名称附加到对象引用上,中间使用点运算符(.)。此外,您可以在括号内提供方法的任何参数。如果方法不需要任何参数,请使用空括号。
objectReference.methodName(argumentList);
或者:
objectReference.methodName();
Rectangle
类有两个方法:getArea()
用于计算矩形的面积和move()
用于改变矩形的原点。这是调用这两个方法的CreateObjectDemo
代码:
System.out.println("Area of rectOne: " + rectOne.getArea());
...
rectTwo.move(40, 72);
第一条语句调用rectOne
的getArea()
方法并显示结果。第二行移动rectTwo
,因为move()
方法为对象的origin.x
和origin.y
赋予新值。
与实例字段一样,objectReference必须是一个对象的引用。你可以使用一个变量名,但也可以使用任何返回对象引用的表达式。new
运算符返回一个对象引用,因此你可以使用从new
返回的值来调用新对象的方法:
new Rectangle(100, 50).getArea()
表达式new Rectangle(100, 50)
返回一个指向Rectangle
对象的对象引用。如图所示,你可以使用点符号来调用新Rectangle
的getArea()
方法来计算新矩形的面积。
一些方法,比如getArea()
,会返回一个值。对于返回值的方法,你可以在表达式中使用方法调用。你可以将返回值赋给一个变量,用它做决策,或者控制一个循环。这段代码将getArea()
返回的值赋给变量areaOfRectangle
:
int areaOfRectangle = new Rectangle(100, 50).getArea();
记住,在特定对象上调用方法就相当于向该对象发送消息。在这种情况下,getArea()
被调用的对象是构造函数返回的矩形。
垃圾收集器
一些面向对象的语言要求你跟踪你创建的所有对象,并在不再需要时显式销毁它们。显式管理内存是繁琐且容易出错的。Java 平台允许你创建任意多的对象(当然,受系统处理能力的限制),而且你不必担心销毁它们。Java 运行时环境在确定不再使用对象时删除对象。这个过程称为垃圾收集。
当没有更多引用指向对象时,对象就有资格进行垃圾收集。通常,变量中保存的引用在变量超出范围时会被丢弃。或者,你可以通过将变量设置为特殊值null
来显式丢弃对象引用。记住,一个程序可以有多个引用指向同一个对象;在对象有资格进行垃圾收集之前,所有对对象的引用都必须被丢弃。
Java 运行时环境有一个垃圾收集器,定期释放不再被引用的对象使用的内存。当垃圾收集器确定时机合适时,它会自动执行其任务。
类的更多内容
原文:
docs.oracle.com/javase/tutorial/java/javaOO/more.html
本节涵盖了更多与类相关的方面,这些方面依赖于在前面关于对象的章节中学到的对象引用和点
运算符:
-
从方法中返回值。
-
this
关键字。 -
类成员 vs. 实例成员。
-
访问控制。
从方法返回一个值
原文:
docs.oracle.com/javase/tutorial/java/javaOO/returnvalue.html
当方法达到return
语句时返回到调用它的代码处
-
完成方法中的所有语句,
-
或者达到
return
语句, -
抛出异常(稍后讨论),
先发生的那个。
在方法声明中声明方法的返回类型。在方法体内,使用return
语句返回值。
任何声明为void
的方法不返回值。它不需要包含return
语句,但可以包含。在这种情况下,return
语句可用于跳出控制流块并退出方法,简单地像这样使用:
return;
如果尝试从声明为void
的方法返回值,将会得到编译器错误。
任何未声明为void
的方法必须包含一个带有相应返回值的return
语句,就像这样:
return returnValue;
返回值的数据类型必须与方法声明的返回类型匹配;你不能从声明为返回布尔值的方法中返回整数值。
在关于对象的部分讨论的Rectangle
Rectangle
类中的getArea()
方法返回一个整数:
// a method for computing the area of the rectangle
public int getArea() {
return width * height;
}
这个方法返回表达式width*height
求值的整数。
getArea
方法返回一个基本类型。一个方法也可以返回一个引用类型。例如,在一个操作Bicycle
对象的程序中,我们可能有这样一个方法:
public Bicycle seeWhosFastest(Bicycle myBike, Bicycle yourBike,
Environment env) {
Bicycle fastest;
// code to calculate which bike is
// faster, given each bike's gear
// and cadence and given the
// environment (terrain and wind)
return fastest;
}
返回一个类或接口
如果这一部分让你困惑,可以跳过它,在完成关于接口和继承的课程后再回来阅读。
当一个方法使用类名作为返回类型,比如whosFastest
这样做时,返回对象的类型的类必须是返回类型的子类,或者是返回类型的确切类。假设你有一个类层次结构,其中ImaginaryNumber
是java.lang.Number
的子类,而java.lang.Number
又是Object
的子类,如下图所示。
ImaginaryNumber 的类层次结构
现在假设你有一个声明为返回Number
的方法:
public Number returnANumber() {
...
}
returnANumber
方法可以返回一个ImaginaryNumber
但不能返回一个Object
。ImaginaryNumber
是一个Number
,因为它是Number
的子类。然而,一个Object
不一定是一个Number
— 它可能是一个String
或其他类型。
你可以重写一个方法并定义它返回原方法的子类,就像这样:
public ImaginaryNumber returnANumber() {
...
}
这种技术称为协变返回类型,意味着返回类型允许与子类相同方向变化。
注意: 你也可以使用接口名称作为返回类型。在这种情况下,返回的对象必须实现指定的接口。
使用 this 关键字
原文:
docs.oracle.com/javase/tutorial/java/javaOO/thiskey.html
在实例方法或构造函数中,this
是对当前对象的引用 — 即正在调用其方法或构造函数的对象。你可以通过使用this
从实例方法或构造函数中引用当前对象的任何成员。
使用this
与字段
使用this
关键字的最常见原因是因为字段被方法或构造函数参数遮蔽。
例如,Point
类是这样写的
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int a, int b) {
x = a;
y = b;
}
}
但它也可以这样写:
public class Point {
public int x = 0;
public int y = 0;
//constructor
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
每个构造函数的参数都会遮蔽对象的一个字段 — 在构造函数内部,x
是构造函数第一个参数的一个本地副本。要引用Point
字段的**x
**,构造函数必须使用this.x
。
使用this
与构造函数
在构造函数内部,你也可以使用this
关键字来调用同一类中的另一个构造函数。这样做被称为显式构造函数调用。这里有另一个Rectangle
类,与 Objects 部分中的实现不同。
public class Rectangle {
private int x, y;
private int width, height;
public Rectangle() {
this(0, 0, 1, 1);
}
public Rectangle(int width, int height) {
this(0, 0, width, height);
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
...
}
该类包含一组构造函数。每个构造函数都初始化矩形的一些或全部成员变量。构造函数为任何未由参数提供初始值的成员变量提供默认值。例如,无参数构造函数在坐标 0,0 处创建一个 1x1 的Rectangle
。两个参数的构造函数调用四个参数的构造函数,传入宽度和高度,但始终使用 0,0 坐标。与以前一样,编译器根据参数的数量和类型确定要调用的构造函数。
如果存在,调用另一个构造函数必须是构造函数中的第一行。
控制类的成员的访问
原文:
docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
访问级别修饰符确定其他类是否可以使用特定字段或调用特定方法。有两个访问控制级别:
-
在顶层—
public
,或包私有(没有显式修饰符)。 -
在成员级别—
public
,private
,protected
,或包私有(没有显式修饰符)。
一个类可以用修饰符public
声明,这样该类对所有类都是可见的。如果一个类没有修饰符(默认情况,也称为包私有),它只在自己的包内可见(包是相关类的命名组 — 你将在后面的课程中了解它们。)
在成员级别,你也可以像顶级类一样使用public
修饰符或无修饰符(包私有),意义相同。对于成员,还有两个额外的访问修饰符:private
和protected
。private
修饰符指定该成员只能在其自己的类中访问。protected
修饰符指定该成员只能在其自己的包内访问(与包私有相同),并且可以由另一个包中其类的子类访问。
以下表格显示了每个修饰符允许的成员访问。
访问级别
修饰符 | 类 | 包 | 子类 | 全部 |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
无修饰符 | Y | Y | N | N |
private | Y | N | N | N |
第一列数据表示类本身是否可以访问由访问级别定义的成员。正如你所看到的,类总是可以访问自己的成员。第二列表示与该类在同一包中的类(不考虑它们的父类)是否可以访问该成员。第三列表示在声明在此包之外的类的子类是否可以访问该成员。第四列表示所有类是否可以访问该成员。
访问级别以两种方式影响你。首先,当你使用来自其他来源的类时,例如 Java 平台中的类,访问级别确定你自己的类可以使用那些类的成员。其次,当你编写一个类时,你需要决定你的类中每个成员变量和每个方法应该具有什么访问级别。
让我们看一组类,并看看访问级别如何影响可见性。以下图显示了此示例中的四个类及其关系。
用于说明访问级别的示例中的类和包
以下表格显示了可以应用于 Alpha 类的成员的每个访问修饰符的可见性。
可见性
修饰符 | Alpha | Beta | Alphasub | Gamma |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
无修饰符 | Y | Y | N | N |
private | Y | N | N | N |
选择访问级别的提示:
如果其他程序员使用您的类,您希望确保不会发生误用导致的错误。访问级别可以帮助您做到这一点。
-
对于特定成员,使用最严格的访问级别是有意义的。除非有充分理由,否则使用
private
。 -
除了常量外,避免使用
public
字段。(教程中的许多示例使用公共字段。这可能有助于简洁地说明一些要点,但不建议在生产代码中使用。)公共字段往往会将您与特定实现联系起来,并限制您在更改代码时的灵活性。
理解类成员
原文:
docs.oracle.com/javase/tutorial/java/javaOO/classvars.html
在本节中,我们讨论了使用static
关键字来创建属于类而不是类实例的字段和方法。
类变量
当从同一类蓝图创建多个对象时,它们各自拥有自己独特的实例变量副本。在Bicycle
类的情况下,实例变量是cadence
、gear
和speed
。每个Bicycle
对象都有自己的这些变量的值,存储在不同的内存位置。
有时,你希望拥有对所有对象都通用的变量。这可以通过static
修饰符来实现。在声明中具有static
修饰符的字段称为静态字段或类变量。它们与类关联,而不是与任何对象关联。类的每个实例共享一个类变量,它在内存中的一个固定位置。任何对象都可以更改类变量的值,但也可以在不创建类的实例的情况下操作类变量。
例如,假设你想创建多个Bicycle
对象并为每个分配一个从 1 开始的序列号。这个 ID 号对每个对象是唯一的,因此是一个实例变量。同时,你需要一个字段来跟踪已创建多少Bicycle
对象,以便知道要分配给下一个对象的 ID。这样的字段与任何单个对象无关,而是与整个类相关。为此,你需要一个类变量numberOfBicycles
,如下所示:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
// add an instance variable for the object ID
private int id;
// add a class variable for the
// number of Bicycle objects instantiated
private static int numberOfBicycles = 0;
...
}
类变量通过类名本身引用,如
Bicycle.numberOfBicycles
这样清楚地表明它们是类变量。
注意: 你也可以通过对象引用来引用静态字段
myBike.numberOfBicycles
但这是不被推荐的,因为它没有清楚地表明它们是类变量。
你可以使用Bicycle
构造函数来设置id
实例变量并递增numberOfBicycles
类变量:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence, int startSpeed, int startGear){
gear = startGear;
cadence = startCadence;
speed = startSpeed;
// increment number of Bicycles
// and assign ID number
id = ++numberOfBicycles;
}
// new method to return the ID instance variable
public int getID() {
return id;
}
...
}
类方法
Java 编程语言支持静态方法以及静态变量。具有static
修饰符的静态方法应该使用类名调用,而无需创建类的实例,如
ClassName.methodName(args)
注意: 你也可以通过对象引用来引用静态方法
instanceName.methodName(args)
但这是不被推荐的,因为它没有清楚地表明它们是类方法。
静态方法的一个常见用途是访问静态字段。例如,我们可以向Bicycle
类添加一个静态方法来访问numberOfBicycles
静态字段:
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
并非所有实例和类变量和方法的组合都被允许:
-
实例方法可以直接访问实例变量和实例方法。
-
实例方法可以直接访问类变量和类方法。
-
类方法可以直接访问类变量和类方法。
-
类方法不能直接访问实例变量或实例方法—它们必须使用对象引用。此外,类方法不能使用
this
关键字,因为this
没有实例可供参考。
常量
static
修饰符与final
修饰符结合使用,也用于定义常量。final
修饰符表示此字段的值不能更改。
例如,以下变量声明定义了一个名为PI
的常量,其值是圆周率的近似值(圆的周长与直径的比值):
static final double PI = 3.141592653589793;
以这种方式定义的常量不能被重新分配,如果程序尝试这样做,将在编译时出现错误。按照惯例,常量值的名称以大写字母拼写。如果名称由多个单词组成,则单词之间用下划线(_)分隔。
**注意:**如果原始类型或字符串被定义为常量,并且在编译时已知其值,则编译器会在代码中的所有位置用其值替换常量名称。这被称为编译时常量。如果常量在外部世界中的值发生变化(例如,如果立法规定 pi 实际上应该是 3.975),则需要重新编译使用此常量的任何类以获取当前值。
Bicycle
类
在本节中进行的所有修改后,Bicycle
类现在是:
public class Bicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public Bicycle(int startCadence,
int startSpeed,
int startGear) {
gear = startGear;
cadence = startCadence;
speed = startSpeed;
id = ++numberOfBicycles;
}
public int getID() {
return id;
}
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
public int getCadence() {
return cadence;
}
public void setCadence(int newValue) {
cadence = newValue;
}
public int getGear(){
return gear;
}
public void setGear(int newValue) {
gear = newValue;
}
public int getSpeed() {
return speed;
}
public void applyBrake(int decrement) {
speed -= decrement;
}
public void speedUp(int increment) {
speed += increment;
}
}
初始化字段
原文:
docs.oracle.com/javase/tutorial/java/javaOO/initial.html
正如你所见,你通常可以在声明中为字段提供初始值:
public class BedAndBreakfast {
// initialize to 10
public static int capacity = 10;
// initialize to false
private boolean full = false;
}
当初始化值可用且初始化可以放在一行时,这种方式效果很好。然而,这种初始化方式由于其简单性而有一些限制。如果初始化需要一些逻辑(例如,错误处理或使用for
循环填充复杂数组),简单赋值是不够的。实例变量可以在构造函数中初始化,可以在那里使用错误处理或其他逻辑。为了为类变量提供相同的功能,Java 编程语言包括静态初始化块。
**注意:**在类定义的开头声明字段并不是必需的,尽管这是最常见的做法。只需要在使用之前声明和初始化它们即可。
静态初始化块
静态初始化块是一个普通的代码块,用大括号{ }
括起来,并在static
关键字之前。这里是一个示例:
static {
// whatever code is needed for initialization goes here
}
一个类可以有任意数量的静态初始化块,并且它们可以出现在类体的任何位置。运行时系统保证静态初始化块按照它们在源代码中出现的顺序调用。
还有一种替代静态块的方法 — 你可以编写一个私有静态方法:
class Whatever {
public static varType myVar = initializeClassVariable();
private static varType initializeClassVariable() {
// initialization code goes here
}
}
私有静态方法的优势在于,如果需要重新初始化类变量,它们可以在以后被重用。
初始化实例成员
通常,你会将代码放在构造函数中初始化实例变量。有两种替代方法可以用来初始化实例变量:初始化块和 final 方法。
实例变量的初始化块看起来就像静态初始化块,但没有static
关键字:
{
// whatever code is needed for initialization goes here
}
Java 编译器将初始化块复制到每个构造函数中。因此,这种方法可以用于在多个构造函数之间共享一段代码。
final 方法不能在子类中被重写。这在接口和继承的课程中有讨论。这里是使用 final 方法初始化实例变量的示例:
class Whatever {
private varType myVar = initializeInstanceVariable();
protected final varType initializeInstanceVariable() {
// initialization code goes here
}
}
如果子类可能希望重用初始化方法,则这是特别有用的。该方法是 final 的,因为在实例初始化期间调用非 final 方法可能会导致问题。
创建和使用类和对象摘要
原文:
docs.oracle.com/javase/tutorial/java/javaOO/summaryclasses.html
类声明命名类并在大括号之间封装类体。类名可以由修饰符前置。类体包含类的字段、方法和构造函数。类使用字段来包含状态信息,并使用方法来实现行为。初始化类的新实例的构造函数使用类的名称,并且看起来像没有返回类型的方法。
您可以通过在声明中使用访问修饰符(如public
)来以相同的方式控制对类和成员的访问。
通过在成员声明中使用static
关键字来指定类变量或类方法。未声明为static
的成员隐式地是实例成员。类变量由类的所有实例共享,并且可以通过类名以及实例引用访问。类的实例会获得每个实例变量的自己的副本,必须通过实例引用访问。
通过使用new
运算符和构造函数,您可以从类创建对象。new
运算符返回一个对创建的对象的引用。您可以将引用分配给变量或直接使用它。
可以通过使用限定名称来引用在声明它们的类之外的代码可访问的实例变量和方法。实例变量的限定名称如下所示:
*objectReference.variableName*
方法的限定名称如下所示:
*objectReference.methodName(argumentList)*
或:
*objectReference.methodName()*
垃圾收集器会自动清理未使用的对象。如果程序不再持有对对象的引用,则该对象将被视为未使用。您可以通过将持有引用的变量设置为null
来显式丢弃引用。
问题和练习:类
原文:
docs.oracle.com/javase/tutorial/java/javaOO/QandE/creating-questions.html
问题
-
考虑以下类:
public class IdentifyMyParts { public static int x = 7; public int y = 3; }
-
类变量是什么?
-
实例变量是什么?
-
以下代码的输出是什么:
IdentifyMyParts a = new IdentifyMyParts(); IdentifyMyParts b = new IdentifyMyParts(); a.y = 5; b.y = 6; a.x = 1; b.x = 2; System.out.println("a.y = " + a.y); System.out.println("b.y = " + b.y); System.out.println("a.x = " + a.x); System.out.println("b.x = " + b.x); System.out.println("IdentifyMyParts.x = " + IdentifyMyParts.x);
-
练习
-
编写一个类,其实例代表一副扑克牌中的一张牌。扑克牌有两个独特的属性:等级和花色。确保保留你的解决方案,因为你将被要求在枚举类型中重新编写它。
提示:
你可以使用
assert
语句来检查你的赋值。你可以写:assert (boolean expression to test);
如果布尔表达式为假,你将收到一个错误消息。例如,
assert toString(ACE) == "Ace";
应该返回
true
,这样就不会有错误消息。如果你使用
assert
语句,你必须用ea
标志运行你的程序:java -ea YourProgram.class
-
编写一个类,其实例代表一副完整的扑克牌。你也应该保留这个解决方案。
-
3. 编写一个小程序来测试你的牌组和卡片类。这个程序可以简单到创建一副牌并显示其卡片。
检查你的答案。
问题和练习:对象
原文:
docs.oracle.com/javase/tutorial/java/javaOO/QandE/objects-questions.html
问题
-
以下程序有什么问题?
public class SomethingIsWrong { public static void main(String[] args) { Rectangle myRect; myRect.width = 40; myRect.height = 50; System.out.println("myRect's area is " + myRect.area()); } }
-
以下代码创建了一个数组和一个字符串对象。在代码执行后,这些对象有多少个引用?这两个对象是否有资格进行垃圾回收?
... String[] students = new String[10]; String studentName = "Peter Parker"; students[0] = studentName; studentName = null; ...
-
程序如何销毁它创建的对象?
练习
-
修复问题 1 中显示的名为
SomethingIsWrong
的程序。 -
给定以下名为
NumberHolder
的类,编写一些代码来创建该类的一个实例,初始化其两个成员变量,然后显示每个成员变量的值。public class NumberHolder { public int anInt; public float aFloat; }
检查你的答案。
嵌套类
原文:
docs.oracle.com/javase/tutorial/java/javaOO/nested.html
Java 编程语言允许您在另一个类中定义一个类。这样的类称为嵌套类,如下所示:
class OuterClass {
...
class NestedClass {
...
}
}
术语: 嵌套类分为两类:非静态和静态。非静态嵌套类称为内部类。声明为static
的嵌套类称为静态嵌套类。
class OuterClass {
...
class InnerClass {
...
}
static class StaticNestedClass {
...
}
}
嵌套类是其封闭类的成员。非静态嵌套类(内部类)可以访问封闭类的其他成员,即使它们被声明为 private。静态嵌套类无法访问封闭类的其他成员。作为OuterClass
的成员,嵌套类可以声明为private
、public
、protected
或包私有。(请记住,外部类只能声明为public
或包私有。)
为什么使用嵌套类?
使用嵌套类的引人注目的原因包括以下内容:
-
它是一种逻辑上将仅在一个地方使用的类分组的方法:如果一个类仅对另一个类有用,则将其嵌入该类并将两者保持在一起是合乎逻辑的。嵌套这样的“辅助类”使其包更加简洁。
-
它增加了封装性:考虑两个顶层类 A 和 B,其中 B 需要访问 A 的成员,否则这些成员将被声明为
private
。通过将类 B 隐藏在类 A 中,A 的成员可以被声明为 private,并且 B 可以访问它们。此外,B 本身可以对外部世界隐藏。 -
它可以导致更易读和易维护的代码:将小类嵌套在顶层类中可以使代码更接近其使用位置。
内部类
与实例方法和变量一样,内部类与其封闭类的实例相关联,并且可以直接访问该对象的方法和字段。此外,因为内部类与实例相关联,它本身不能定义任何静态成员。
作为内部类的实例存在于外部类的实例内部。考虑以下类:
class OuterClass {
...
class InnerClass {
...
}
}
InnerClass
的实例只能存在于OuterClass
的实例中,并且可以直接访问其封闭实例的方法和字段。
要实例化内部类,必须首先实例化外部类。然后,使用以下语法在外部对象中创建内部对象:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
有两种特殊类型的内部类:局部类和匿名类。
静态嵌套类
与类方法和变量一样,静态嵌套类与其外部类相关联。并且像静态类方法一样,静态嵌套类不能直接引用其封闭类中定义的实例变量或方法:它只能通过对象引用使用它们。内部类和嵌套静态类示例演示了这一点。
注意: 静态嵌套类与其外部类(以及其他类)的实例成员交互方式与任何其他顶层类相同。实际上,静态嵌套类在行为上就像是为了包装方便而嵌套在另一个顶层类中的顶层类。内部类和嵌套静态类示例也演示了这一点。
您可以像实例化顶层类一样实例化静态嵌套类:
StaticNestedClass staticNestedObject = new StaticNestedClass();
内部类和嵌套静态类示例
以下示例,OuterClass
,以及TopLevelClass
,演示了内部类(InnerClass
)、嵌套静态类(StaticNestedClass
)和顶层类(TopLevelClass
)可以访问OuterClass
的哪些类成员:
OuterClass.java
public class OuterClass {
String outerField = "Outer field";
static String staticOuterField = "Static outer field";
class InnerClass {
void accessMembers() {
System.out.println(outerField);
System.out.println(staticOuterField);
}
}
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field outerField
// System.out.println(outerField);
System.out.println(outer.outerField);
System.out.println(staticOuterField);
}
}
public static void main(String[] args) {
System.out.println("Inner class:");
System.out.println("------------");
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.accessMembers();
System.out.println("\nStatic nested class:");
System.out.println("--------------------");
StaticNestedClass staticNestedObject = new StaticNestedClass();
staticNestedObject.accessMembers(outerObject);
System.out.println("\nTop-level class:");
System.out.println("--------------------");
TopLevelClass topLevelObject = new TopLevelClass();
topLevelObject.accessMembers(outerObject);
}
}
TopLevelClass.java
public class TopLevelClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field OuterClass.outerField
// System.out.println(OuterClass.outerField);
System.out.println(outer.outerField);
System.out.println(OuterClass.staticOuterField);
}
}
此示例打印以下输出:
Inner class:
------------
Outer field
Static outer field
Static nested class:
--------------------
Outer field
Static outer field
Top-level class:
--------------------
Outer field
Static outer field
请注意,静态嵌套类与其外部类的实例成员交互方式与任何其他顶层类相同。静态嵌套类StaticNestedClass
无法直接访问outerField
,因为它是封闭类OuterClass
的实例变量。Java 编译器会在突出显示的语句处生成错误:
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field outerField
System.out.println(outerField);
}
}
要修复此错误,请通过对象引用访问outerField
:
System.out.println(outer.outerField);
同样,顶层类TopLevelClass
也无法直接访问outerField
。
遮蔽
如果特定范围(如内部类或方法定义)中的类型声明(如成员变量或参数名)与封闭范围中的另一个声明具有相同的名称,则声明会遮蔽封闭范围的声明。您不能仅通过名称引用被遮蔽的声明。以下示例,ShadowTest
,演示了这一点:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
以下是此示例的输出:
x = 23
this.x = 1
ShadowTest.this.x = 0
此示例定义了三个名为x
的变量:类ShadowTest
的成员变量,内部类FirstLevel
的成员变量以及方法methodInFirstLevel
中的参数。方法methodInFirstLevel
中定义的变量x
会遮蔽内部类FirstLevel
的变量。因此,当您在方法methodInFirstLevel
中使用变量x
时,它指的是方法参数。要引用内部类FirstLevel
的成员变量,请使用关键字this
表示封闭范围:
System.out.println("this.x = " + this.x);
通过类名引用封装更大范围的成员变量。例如,以下语句从方法methodInFirstLevel
中访问类ShadowTest
的成员变量:
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
序列化
内部类的序列化,包括局部和匿名类,是强烈不建议的。当 Java 编译器编译某些结构(如内部类)时,它会创建合成结构;这些是在源代码中没有对应构造的类、方法、字段和其他结构。合成结构使 Java 编译器能够实现新的 Java 语言特性,而无需更改 JVM。然而,合成结构在不同的 Java 编译器实现之间可能会有所不同,这意味着.class
文件在不同的实现之间也可能会有所不同。因此,如果您序列化一个内部类,然后在不同的 JRE 实现中反序列化它,可能会出现兼容性问题。有关在编译内部类时生成的合成结构的更多信息,请参见隐式和合成参数部分中的获取方法参数名称部分。
内部类示例
原文:
docs.oracle.com/javase/tutorial/java/javaOO/innerclasses.html
要查看内部类的使用,请首先考虑一个数组。在以下示例中,您创建一个数组,填充它的整数值,然后仅按升序输出数组的偶数索引值。
接下来的DataStructure.java
示例包括:
-
包含构造函数以创建包含连续整数值(0、1、2、3 等)的数组的实例的
DataStructure
外部类,并且包含一个打印具有偶数索引值的数组元素的方法。 -
EvenIterator
内部类,实现了DataStructureIterator
接口,该接口扩展了Iterator
<
Integer
>
接口。迭代器用于遍历数据结构,通常具有用于测试最后一个元素、检索当前元素和移动到下一个元素的方法。 -
一个
main
方法,实例化一个DataStructure
对象(ds
),然后调用printEven
方法来打印具有偶数索引值的数组arrayOfInts
的元素。
public class DataStructure {
// Create an array
private final static int SIZE = 15;
private int[] arrayOfInts = new int[SIZE];
public DataStructure() {
// fill the array with ascending integer values
for (int i = 0; i < SIZE; i++) {
arrayOfInts[i] = i;
}
}
public void printEven() {
// Print out values of even indices of the array
DataStructureIterator iterator = this.new EvenIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
}
interface DataStructureIterator extends java.util.Iterator<Integer> { }
// Inner class implements the DataStructureIterator interface,
// which extends the Iterator<Integer> interface
private class EvenIterator implements DataStructureIterator {
// Start stepping through the array from the beginning
private int nextIndex = 0;
public boolean hasNext() {
// Check if the current element is the last in the array
return (nextIndex <= SIZE - 1);
}
public Integer next() {
// Record a value of an even index of the array
Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);
// Get the next even element
nextIndex += 2;
return retValue;
}
}
public static void main(String s[]) {
// Fill the array with integer values and print out only
// values of even indices
DataStructure ds = new DataStructure();
ds.printEven();
}
}
输出为:
0 2 4 6 8 10 12 14
请注意,EvenIterator
类直接引用了DataStructure
对象的arrayOfInts
实例变量。
您可以使用内部类来实现辅助类,例如在此示例中所示的类。要处理用户界面事件,您必须知道如何使用内部类,因为事件处理机制广泛使用它们。
局部类和匿名类
有两种额外的内部类。您可以在方法体内声明一个内部类。这些类被称为局部类。您还可以在方法体内声明一个没有命名的内部类。这些类被称为匿名类。
修饰符
您可以为内部类使用与外部类的其他成员相同的修饰符。例如,您可以使用访问修饰符private
、public
和protected
来限制对内部类的访问,就像您用它们来限制对其他类成员的访问一样。
本地类
原文:
docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html
本地类是在块中定义的类,块是在平衡大括号之间的零个或多个语句组成的组。通常在方法体中定义本地类。
本节涵盖以下主题:
-
声明本地类
-
访问封闭类的成员
- 遮蔽和本地类
-
本地类类似于内部类
声明本地类
您可以在任何块中定义本地类(请参阅表达式、语句和块了解更多信息)。例如,您可以在方法体、for
循环或if
子句中定义本地类。
以下示例,LocalClassExample
,验证两个电话号码。它在方法validatePhoneNumber
中定义了本地类PhoneNumber
:
public class LocalClassExample {
static String regularExpression = "[⁰-9]";
public static void validatePhoneNumber(
String phoneNumber1, String phoneNumber2) {
final int numberLength = 10;
// Valid in JDK 8 and later:
// int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
public String getNumber() {
return formattedPhoneNumber;
}
// Valid in JDK 8 and later:
// public void printOriginalNumbers() {
// System.out.println("Original numbers are " + phoneNumber1 +
// " and " + phoneNumber2);
// }
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
// Valid in JDK 8 and later:
// myNumber1.printOriginalNumbers();
if (myNumber1.getNumber() == null)
System.out.println("First number is invalid");
else
System.out.println("First number is " + myNumber1.getNumber());
if (myNumber2.getNumber() == null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is " + myNumber2.getNumber());
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
}
该示例通过首先从电话号码中删除除数字 0 到 9 之外的所有字符来验证电话号码。然后,它检查电话号码是否恰好包含十个数字(北美电话号码的长度)。该示例打印如下内容:
First number is 1234567890
Second number is invalid
访问封闭类的成员
本地类可以访问其封闭类的成员。在前面的示例中,PhoneNumber
构造函数访问成员LocalClassExample.regularExpression
。
此外,本地类可以访问局部变量。但是,本地类只能访问声明为 final 的局部变量。当本地类访问封闭块的局部变量或参数时,它会捕获该变量或参数。例如,PhoneNumber
构造函数可以访问局部变量numberLength
,因为它声明为 final;numberLength
是一个捕获的变量。
然而,从 Java SE 8 开始,本地类可以访问封闭块中的局部变量和参数,这些变量是 final 或有效地 final。一旦初始化后值不会改变的变量或参数是有效地 final。例如,假设变量numberLength
没有声明为 final,并且您在PhoneNumber
构造函数中添加了突出显示的赋值语句以将有效电话号码的长度更改为 7 位:
PhoneNumber(String phoneNumber) {
numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
由于这个赋值语句,变量numberLength
不再是有效地 final。因此,当内部类PhoneNumber
尝试访问numberLength
变量时,Java 编译器生成类似于"从内部类引用的局部变量必须是 final 或有效地 final"的错误消息:
if (currentNumber.length() == numberLength)
从 Java SE 8 开始,如果你在方法中声明局部类,它可以访问方法的参数。例如,你可以在PhoneNumber
局部类中定义以下方法:
public void printOriginalNumbers() {
System.out.println("Original numbers are " + phoneNumber1 +
" and " + phoneNumber2);
}
方法printOriginalNumbers
访问方法validatePhoneNumber
的参数phoneNumber1
和phoneNumber2
。
遮蔽和局部类
在局部类中声明的类型(如变量)会遮蔽在外部作用域中具有相同名称的声明。更多信息请参见 Shadowing。
局部类类似于内部类
局部类类似于内部类,因为它们不能定义或声明任何静态成员。在静态方法中的局部类,比如在静态方法validatePhoneNumber
中定义的PhoneNumber
类,只能引用封闭类的静态成员。例如,如果你没有将成员变量regularExpression
定义为静态的,那么 Java 编译器会生成类似“非静态变量regularExpression
无法从静态上下文中引用”的错误。
局部类是非静态的,因为它们可以访问封闭块的实例成员。因此,它们不能包含大多数类型的静态声明。
你不能在块内部声明接口;接口本质上是静态的。例如,以下代码摘录不会编译,因为接口HelloThere
是在方法greetInEnglish
的主体内定义的:
public void greetInEnglish() {
interface HelloThere {
public void greet();
}
class EnglishHelloThere implements HelloThere {
public void greet() {
System.out.println("Hello " + name);
}
}
HelloThere myGreeting = new EnglishHelloThere();
myGreeting.greet();
}
你不能在局部类中声明静态初始化程序或成员接口。以下代码摘录不会编译,因为方法EnglishGoodbye.sayGoodbye
被声明为static
。当编译器遇到这个方法定义时,会生成类似“修饰符’static’仅允许在常量变量声明中使用”的错误:
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static void sayGoodbye() {
System.out.println("Bye bye");
}
}
EnglishGoodbye.sayGoodbye();
}
局部类可以拥有静态成员,前提是它们是常量变量。(常量变量是指声明为 final 并用编译时常量表达式初始化的原始类型或String
类型的变量。编译时常量表达式通常是一个可以在编译时评估的字符串或算术表达式。更多信息请参见理解类成员。)以下代码摘录可以编译,因为静态成员EnglishGoodbye.farewell
是一个常量变量:
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static final String farewell = "Bye bye";
public void sayGoodbye() {
System.out.println(farewell);
}
}
EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
myEnglishGoodbye.sayGoodbye();
}
匿名类
原文:
docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html
匿名类使您的代码更加简洁。它们使您能够同时声明和实例化一个类。它们类似于本地类,只是没有名称。如果您只需要使用本地类一次,请使用它们。
本节涵盖以下主题:
-
声明匿名类
-
匿名类的语法
-
访问封闭范围的本地变量,并声明和访问匿名类的成员
-
匿名类示例
声明匿名类
虽然本地类是类声明,匿名类是表达式,这意味着你在另一个表达式中定义类。以下示例,HelloWorldAnonymousClasses
,在本地变量frenchGreeting
和spanishGreeting
的初始化语句中使用了匿名类,但在变量englishGreeting
的初始化中使用了本地类:
public class HelloWorldAnonymousClasses {
interface HelloWorld {
public void greet();
public void greetSomeone(String someone);
}
public void sayHello() {
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
HelloWorld spanishGreeting = new HelloWorld() {
String name = "mundo";
public void greet() {
greetSomeone("mundo");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hola, " + name);
}
};
englishGreeting.greet();
frenchGreeting.greetSomeone("Fred");
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp =
new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
匿名类的语法
如前所述,匿名类是一个表达式。匿名类表达式的语法类似于构造函数的调用,只是其中包含一个代码块中的类定义。
考虑frenchGreeting
对象的实例化:
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
匿名类表达式包括以下内容:
-
new
运算符 -
要实现的接口名称或要扩展的类名称。在此示例中,匿名类正在实现接口
HelloWorld
。 -
包含传递给构造函数的参数的括号,就像普通的类实例创建表达式一样。注意:当您实现一个接口时,没有构造函数,所以您使用一个空的括号对,就像这个例子中一样。
-
一个类声明体。更具体地说,在类体中,允许方法声明,但不允许语句。
因为匿名类定义是一个表达式,所以它必须是语句的一部分。在此示例中,匿名类表达式是实例化frenchGreeting
对象的语句的一部分。(这就解释了为什么在右括号后有一个分号。)
访问封闭范围的本地变量,并声明和访问匿名类的成员
像本地类一样,匿名类可以捕获变量;它们对封闭范围的本地变量具有相同的访问权限:
-
匿名类可以访问其封闭类的成员。
-
匿名类无法访问其封闭范围中未声明为
final
或有效final
的本地变量。 -
像嵌套类一样,在匿名类中声明类型(如变量)会遮蔽封闭范围中具有相同名称的任何其他声明。有关更多信息,请参阅遮蔽。
匿名类在成员方面与局部类具有相同的限制:
-
您不能在匿名类中声明静态初始化程序或成员接口。
-
一个匿名类可以有静态成员,只要它们是常量变量。
请注意,您可以在匿名类中声明以下内容:
-
字段
-
额外的方法(即使它们不实现超类型的任何方法)
-
实例初始化程序
-
局部类
然而,你不能在匿名类中声明构造函数。
匿名类示例
匿名类经常用于图形用户界面(GUI)应用程序。
考虑 JavaFX 示例HelloWorld.java
(来自Hello World, JavaFX Style部分,取自Getting Started with JavaFX)。此示例创建一个包含**Say ‘Hello World’**按钮的框架。匿名类表达式被突出显示:
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
在此示例中,方法调用btn.setOnAction
指定了当您选择**Say ‘Hello World’**按钮时会发生什么。此方法需要一个EventHandler<ActionEvent>
类型的对象。EventHandler<ActionEvent>
接口只包含一个方法,即 handle。该示例使用匿名类表达式而不是使用新类来实现此方法。请注意,此表达式是传递给btn.setOnAction
方法的参数。
因为EventHandler<ActionEvent>
接口只包含一个方法,所以您可以使用 lambda 表达式代替匿名类表达式。有关更多信息,请参阅 Lambda 表达式部分。
匿名类非常适合实现包含两个或更多方法的接口。以下 JavaFX 示例来自自定义 UI 控件部分。突出显示的代码创建一个仅接受数字值的文本字段。它通过覆盖从TextInputControl
类继承的replaceText
和replaceSelection
方法,使用匿名类重新定义了TextField
类的默认实现。
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class CustomTextFieldSample extends Application {
final static Label label = new Label();
@Override
public void start(Stage stage) {
Group root = new Group();
Scene scene = new Scene(root, 300, 150);
stage.setScene(scene);
stage.setTitle("Text Field Sample");
GridPane grid = new GridPane();
grid.setPadding(new Insets(10, 10, 10, 10));
grid.setVgap(5);
grid.setHgap(5);
scene.setRoot(grid);
final Label dollar = new Label("$");
GridPane.setConstraints(dollar, 0, 0);
grid.getChildren().add(dollar);
final TextField sum = new TextField() {
@Override
public void replaceText(int start, int end, String text) {
if (!text.matches("[a-z, A-Z]")) {
super.replaceText(start, end, text);
}
label.setText("Enter a numeric value");
}
@Override
public void replaceSelection(String text) {
if (!text.matches("[a-z, A-Z]")) {
super.replaceSelection(text);
}
}
};
sum.setPromptText("Enter the total");
sum.setPrefColumnCount(10);
GridPane.setConstraints(sum, 1, 0);
grid.getChildren().add(sum);
Button submit = new Button("Submit");
GridPane.setConstraints(submit, 2, 0);
grid.getChildren().add(submit);
submit.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent e) {
label.setText(null);
}
});
GridPane.setConstraints(label, 0, 1);
GridPane.setColumnSpan(label, 3);
grid.getChildren().add(label);
scene.setRoot(grid);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Lambda 表达式
原文:
docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
匿名类的一个问题是,如果您的匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能显得笨拙和不清晰。在这些情况下,通常您试图将功能作为参数传递给另一个方法,例如当某人单击按钮时应执行什么操作。Lambda 表达式使您能够做到这一点,将功能视为方法参数,或将代码视为数据。
前一节,匿名类,向您展示了如何实现一个没有名称的基类。尽管这通常比具有名称的类更简洁,但对于只有一个方法的类来说,即使是匿名类似乎也有点过多和繁琐。Lambda 表达式让您更简洁地表达单方法类的实例。
本节涵盖以下主题:
-
Lambda 表达式的理想使用情况
-
方法 1:创建搜索符合一个特征的成员的方法
-
方法 2:创建更通用的搜索方法
-
方法 3:在本地类中指定搜索条件代码
-
方法 4:在匿名类中指定搜索条件代码
-
方法 5:使用 Lambda 表达式指定搜索条件代码
-
方法 6:使用 Lambda 表达式与标准功能接口
-
方法 7:在整个应用程序中使用 Lambda 表达式
-
方法 8:更广泛地使用泛型
-
方法 9:使用接受 Lambda 表达式作为参数的聚合操作
-
-
GUI 应用程序中的 Lambda 表达式
-
Lambda 表达式的语法
-
访问封闭范围的局部变量
-
目标类型
- 目标类型和方法参数
-
序列化
Lambda 表达式的理想使用情况
假设您正在创建一个社交网络应用程序。您希望创建一个功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。以下表格详细描述了这种用例:
字段 | 描述 |
---|---|
名称 | 对所选成员执行操作 |
主要执行者 | 管理员 |
前提条件 | 管理员已登录到系统。 |
后置条件 | 操作仅在符合指定条件的成员上执行。 |
主要成功场景 |
-
管理员指定要执行某个操作的成员的条件。
-
管理员指定对所选成员执行的操作。
-
管理员选择提交按钮。
-
系统找到所有符合指定条件的成员。
-
系统对所有匹配成员执行指定操作。
|
扩展 | 1a. 管理员在指定执行操作或选择提交按钮之前有选项预览符合指定条件的成员。 |
---|---|
出现频率 | 一天中多次。 |
假设这个社交网络应用程序的成员由以下Person
类表示:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假设您的社交网络应用程序的成员存储在List<Person>
实例中。
本节从一个简单的方法开始处理这种用例。它通过本地和匿名类改进了这种方法,然后以使用 lambda 表达式的高效简洁方法结束。在示例RosterTest
中找到本节描述的代码摘录。
方法 1:创建搜索符合一个特征的成员的方法
一个简单的方法是创建几种方法;每种方法搜索符合一个特征的成员,例如性别或年龄。以下方法打印比指定年龄更老的成员:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注意:List
是一个有序的Collection
。集合是将多个元素组合成单个单元的对象。集合用于存储、检索、操作和传递聚合数据。有关集合的更多信息,请参阅 Collections 教程。
这种方法可能会使您的应用程序变得脆弱,这是应用程序由于引入更新(如新数据类型)而无法工作的可能性。假设您升级了应用程序并更改了Person
类的结构,使其包含不同的成员变量;也许该类使用不同的数据类型或算法记录和测量年龄。您将不得不重写大量 API 以适应这种变化。此外,这种方法是不必要地限制性的;例如,如果您想打印比某个年龄更年轻的成员会怎样?
方法 2:创建更通用的搜索方法
以下方法比printPersonsOlderThan
更通用;它打印指定年龄范围内的成员:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
如果您想打印指定性别的成员,或者指定性别和年龄范围的组合会怎样?如果您决定更改Person
类并添加其他属性,例如关系状态或地理位置会怎样?尽管这种方法比printPersonsOlderThan
更通用,但尝试为每个可能的搜索查询创建单独的方法仍可能导致脆弱的代码。您可以将指定要搜索的条件的代码与不同类分开。
方法 3:在本地类中指定搜索条件代码
以下方法打印符合您指定搜索条件的成员:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
此方法检查roster
参数中包含的每个Person
实例是否满足CheckPerson
参数tester
中指定的搜索条件,方法是调用tester.test
方法。如果tester.test
方法返回true
值,则在Person
实例上调用printPersons
方法。
要指定搜索条件,您需要实现CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
以下类通过为test
方法指定实现来实现CheckPerson
接口。该方法过滤符合美国选择性服务资格的成员:如果其Person
参数是男性且年龄在 18 至 25 岁之间,则返回true
值:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
要使用此类,您需要创建一个新实例并调用printPersons
方法:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
尽管这种方法不太脆弱——如果更改Person
的结构,您不必重新编写方法——但仍然会有额外的代码:为应用程序中计划执行的每个搜索创建一个新接口和一个本地类。由于CheckPersonEligibleForSelectiveService
实现了一个接口,您可以使用匿名类代替本地类,避免为每个搜索声明一个新类的需要。
方法 4:在匿名类中指定搜索条件代码
下面方法printPersons
的一个参数是一个匿名类,用于过滤符合美国选择性服务资格的成员:即男性且年龄在 18 至 25 岁之间的成员:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这种方法减少了所需的代码量,因为您不必为要执行的每个搜索创建一个新类。然而,考虑到CheckPerson
接口仅包含一个方法,匿名类的语法很臃肿。在这种情况下,您可以使用 Lambda 表达式代替匿名类,如下一节所述。
方法 5:使用 Lambda 表达式指定搜索条件代码
CheckPerson
接口是一个函数式接口。函数式接口是仅包含一个抽象方法的任何接口。(函数式接口可以包含一个或多个默认方法或静态方法。)因为函数式接口仅包含一个抽象方法,所以在实现它时可以省略该方法的名称。为此,您可以使用 Lambda 表达式,如下面方法调用中所示:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
有关如何定义 Lambda 表达式的语法,请参阅 Lambda 表达式的语法。
您可以使用标准的函数式接口来替代CheckPerson
接口,从而进一步减少所需的代码量。
第六种方法:使用 Lambda 表达式与标准函数式接口
重新考虑CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。这个方法接受一个参数并返回一个boolean
值。这个方法如此简单,以至于在你的应用程序中定义一个可能不值得。因此,JDK 定义了几个标准的函数式接口,你可以在java.util.function
包中找到。
例如,你可以在CheckPerson
的位置使用Predicate<T>
接口。这个接口包含方法boolean test(T t)
:
interface Predicate<T> {
boolean test(T t);
}
接口Predicate<T>
是一个泛型接口的示例。(有关泛型的更多信息,请参阅泛型(更新)课程。)泛型类型(如泛型接口)在尖括号(<>
)内指定一个或多个类型参数。这个接口只包含一个类型参数T
。当你声明或实例化一个带有实际类型参数的泛型类型时,你就有了一个参数化类型。例如,参数化类型Predicate<Person>
如下所示:
interface Predicate<Person> {
boolean test(Person t);
}
这个参数化类型包含一个与CheckPerson.boolean test(Person p)
具有相同返回类型和参数的方法。因此,你可以像下面的方法演示的那样使用Predicate<T>
来替代CheckPerson
:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
因此,下面的方法调用与你在第 3 种方法:在本地类中指定搜索条件代码中调用printPersons
以获取符合选择性服务资格的成员时是相同的:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
这个方法中使用 Lambda 表达式的地方并不是唯一的。以下方法建议其他使用 Lambda 表达式的方式。
第七种方法:在整个应用程序中使用 Lambda 表达式
重新考虑printPersonsWithPredicate
方法,看看还能在哪里使用 Lambda 表达式:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
这个方法检查roster
参数中包含的每个Person
实例是否满足tester
参数指定的条件。如果Person
实例确实满足tester
指定的条件,则在Person
实例上调用printPerson
方法。
你可以指定一个不同的操作来执行那些满足tester
指定的条件的Person
实例,而不是调用printPerson
方法。你可以用 lambda 表达式指定这个操作。假设你想要一个类似于printPerson
的 lambda 表达式,一个接受一个参数(一个Person
类型的对象)并返回void
的。记住,要使用 lambda 表达式,你需要实现一个函数式接口。在这种情况下,你需要一个包含可以接受一个Person
类型参数并返回void
的抽象方法的函数式接口。Consumer<T>
接口包含方法void accept(T t)
,具有这些特征。以下方法用一个调用accept
方法的Consumer<Person>
实例替换了p.printPerson()
的调用:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
因此,以下方法调用与在方法 3:在本地类中指定搜索条件代码中调用printPersons
以获取符合应征条件的成员时是相同的。用于打印成员的 lambda 表达式被突出显示:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果你想对成员的个人资料做更多操作而不仅仅是打印它们。假设你想验证成员的个人资料或检索他们的联系信息?在这种情况下,你需要一个包含返回值的抽象方法的函数式接口。Function<T,R>
接口包含方法R apply(T t)
。以下方法检索由参数mapper
指定的数据,然后执行由参数block
指定的操作:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
以下方法从roster
中包含的每个符合应征条件的成员中检索电子邮件地址,然后打印它:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法 8:更广泛地使用泛型
重新考虑processPersonsWithFunction
方法。以下是一个通用版本,它接受一个包含任何数据类型元素的集合作为参数:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
要打印符合应征条件的成员的电子邮件地址,请按照以下方式调用processElements
方法:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
此方法调用执行以下操作:
-
从集合
source
中获取对象的源。在这个例子中,它从集合roster
中获取Person
对象的源。注意,集合roster
是一个List
类型的集合,也是一个Iterable
类型的对象。 -
过滤与
tester
对象匹配的对象。在这个例子中,Predicate
对象是一个指定哪些成员符合应征条件的 lambda 表达式。 -
将每个经过筛选的对象映射到由
mapper
对象指定的值。在这个例子中,Function
对象是一个返回成员电子邮件地址的 lambda 表达式。 -
根据
Consumer
对象block
指定的操作对每个映射对象执行动作。在此示例中,Consumer
对象是一个打印字符串的 Lambda 表达式,该字符串是由Function
对象返回的电子邮件地址。
您可以用聚合操作替换每个这些操作。
方法 9:使用接受 Lambda 表达式作为参数的聚合操作
以下示例使用聚合操作打印出集合roster
中符合选择性服务资格的成员的电子邮件地址:
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
以下表格将方法processElements
执行的每个操作与相应的聚合操作进行了映射:
processElements 操作 | 聚合操作 |
---|---|
获取对象源 | Stream<E> **stream**() |
过滤与Predicate 对象匹配的对象 | Stream<T> **filter**(Predicate<? super T> predicate) |
根据Function 对象将对象映射到另一个值 | <R> Stream<R> **map**(Function<? super T,? extends R> mapper) |
根据Consumer 对象指定的操作执行动作 | void **forEach**(Consumer<? super T> action) |
操作filter
、map
和forEach
是聚合操作。聚合操作处理来自流的元素,而不是直接来自集合(这就是为什么此示例中调用的第一个方法是stream
的原因)。流是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源(例如集合)传递值。管道是一系列流操作,本示例中是filter
-map
-forEach
。此外,聚合操作通常接受 Lambda 表达式作为参数,使您能够自定义它们的行为。
对于更深入讨论聚合操作,请参阅聚合操作课程。
GUI 应用程序中的 Lambda 表达式
要处理图形用户界面(GUI)应用程序中的事件,例如键盘操作、鼠标操作和滚动操作,通常需要创建事件处理程序,这通常涉及实现特定的接口。通常,事件处理程序接口是函数式接口;它们往往只有一个方法。
在 JavaFX 示例HelloWorld.java
(在上一节匿名类中讨论)中,您可以在此语句中用 Lambda 表达式替换突出显示的匿名类:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
方法调用btn.setOnAction
指定了当选择由btn
对象表示的按钮时会发生什么。此方法需要一个EventHandler<ActionEvent>
类型的对象。EventHandler<ActionEvent>
接口只包含一个方法void handle(T event)
。该接口是一个函数式接口,因此您可以使用以下突出显示的 Lambda 表达式来替换它:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda 表达式的语法
一个 lambda 表达式由以下内容组成:
-
用括号括起的逗号分隔的形式参数列表。
CheckPerson.test
方法包含一个参数p
,它表示Person
类的一个实例。注意:您可以在 lambda 表达式中省略参数的数据类型。此外,如果只有一个参数,您可以省略括号。例如,以下 lambda 表达式也是有效的:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
-
箭头标记,
->
-
一个由单个表达式或语句块组成的主体。本示例使用以下表达式:
p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
如果您指定一个单一表达式,那么 Java 运行时将评估该表达式,然后返回其值。或者,您可以使用一个返回语句:
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
返回语句不是一个表达式;在 lambda 表达式中,您必须用大括号(
{}
)括起语句。然而,在 void 方法调用中,您不必用大括号括起。例如,以下是一个有效的 lambda 表达式:email -> System.out.println(email)
请注意,lambda 表达式看起来很像方法声明;您可以将 lambda 表达式视为匿名方法——没有名称的方法。
以下示例Calculator
是一个使用多个形式参数的 lambda 表达式的示例:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
方法operateBinary
对两个整数操作数执行数学运算。操作本身由IntegerMath
的实例指定。该示例使用 lambda 表达式定义了两个操作,addition
和subtraction
。该示例打印如下内容:
40 + 2 = 42
20 - 10 = 10
访问封闭范围的局部变量
像局部类和匿名类一样,lambda 表达式可以捕获变量;它们对封闭范围的局部变量具有相同的访问权限。然而,与局部类和匿名类不同,lambda 表达式没有任何遮蔽问题(有关更多信息,请参见遮蔽)。Lambda 表达式是词法作用域的。这意味着它们不继承任何名称来自超类型,也不引入新的作用域级别。lambda 表达式中的声明被解释为在封闭环境中一样。以下示例LambdaScopeTest
演示了这一点:
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
int z = 2;
Consumer<Integer> myConsumer = (y) ->
{
// The following statement causes the compiler to generate
// the error "Local variable z defined in an enclosing scope
// must be final or effectively final"
//
// z = 99;
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
本示例生成以下输出:
x = 23
y = 23
z = 2
this.x = 1
LambdaScopeTest.this.x = 0
如果在 lambda 表达式myConsumer
的声明中,将参数x
替换为y
,那么编译器会生成一个错误:
Consumer<Integer> myConsumer = (x) -> {
// ...
}
编译器生成错误“Lambda 表达式的参数 x 不能重新声明在封闭范围中定义的另一个局部变量”,因为 lambda 表达式不引入新的作用域级别。因此,可以直接访问封闭范围的字段、方法和局部变量。例如,lambda 表达式直接访问方法methodInFirstLevel
的参数x
。要访问封闭类中的变量,请使用关键字this
。在这个例子中,this.x
指的是成员变量FirstLevel.x
。
然而,与本地和匿名类一样,lambda 表达式只能访问封闭块的局部变量和参数,这些变量必须是 final 或有效 final。在这个例子中,变量z
是有效 final;在初始化后其值不会改变。然而,假设在 lambda 表达式myConsumer
中添加以下赋值语句:
Consumer<Integer> myConsumer = (y) -> {
z = 99;
// ...
}
由于这个赋值语句,变量z
不再是有效 final。因此,Java 编译器生成类似于“定义在封闭范围中的局部变量 z 必须是 final 或有效 final”的错误消息。
目标类型
如何确定 lambda 表达式的类型?回想一下选择男性会员且年龄在 18 到 25 岁之间的 lambda 表达式:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
此 lambda 表达式在以下两个方法中使用:
-
方法 3:在本地类中指定搜索条件代码 中的
public static void printPersons(List<Person> roster, CheckPerson tester)
-
方法 6:使用标准函数接口和 Lambda 表达式 中的
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
当 Java 运行时调用方法 printPersons
时,它期望的数据类型是 CheckPerson
,因此 lambda 表达式就是这种类型。然而,当 Java 运行时调用方法 printPersonsWithPredicate
时,它期望的数据类型是 Predicate<Person>
,因此 lambda 表达式就是这种类型。这些方法期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java 编译器使用 lambda 表达式所在上下文或情况的目标类型。由此可知,只能在 Java 编译器能够确定目标类型的情况下使用 lambda 表达式:
-
变量声明
-
赋值语句
-
返回语句
-
数组初始化器
-
方法或构造函数参数
-
Lambda 表达式主体
-
条件表达式,
?:
-
强制类型转换表达式
目标类型和方法参数
对于方法参数,Java 编译器使用另外两个语言特性来确定目标类型:重载解析和类型参数推断。
考虑以下两个函数式接口(java.lang.Runnable
和java.util.concurrent.Callable<V>
):
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
方法Runnable.run
不返回值,而Callable<V>.call
返回值。
假设您已经重载了方法invoke
如下(有关重载方法的更多信息,请参见定义方法):
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
在下面的语句中将调用哪个方法?
String s = invoke(() -> "done");
将调用方法invoke(Callable<T>)
,因为该方法返回一个值;方法invoke(Runnable)
不返回值。在这种情况下,lambda 表达式() -> "done"
的类型是Callable<T>
。
序列化
如果 lambda 表达式的目标类型和捕获的参数都是可序列化的,则可以对其进行序列化。然而,与内部类一样,强烈不建议序列化 lambda 表达式。