更多关于类的内容
关于类的更多内容
本节将深入探讨类的相关内容,这些内容依赖于对象引用和点运算符的使用,而这些概念已在前面的对象章节中介绍过:
- 方法的返回值。
- this 关键字。
- 类成员与实例成员的区别。
- 访问控制。
从方法中返回一个值
当以下场景发生时,方法会返回给调用它的代码一个值:
- 方法中的所有语句都已执行完毕
- 遇到一个 return 语句
- 方法抛出一个异常
- 以先发生者为准
在方法声明中声明方法的返回类型。在方法主体中,使用 return语句返回值。
任何声明为 void 的方法都不返回值。此类方法无需包含 return 语句,但可以包含。在这种情况下,return 语句可用于跳出控制流块并退出方法,其用法如下:
return;
若尝试从声明为 void 的方法中返回值,将引发编译器错误。
任何未声明为 void 的方法都必须包含一个带有对应返回值的 return 语句,如下所示:
return returnValue;
方法的返回值数据类型必须与方法声明的返回类型一致;无法从声明为返回布尔值的方法中返回整数值。
在对象章节中讨论的矩形类中的 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;
}
返回类或者接口
若本节内容令您困惑,请先跳过,待完成接口与继承章节的学习后再返回此处。
当方法使用类名作为返回类型时(例如 seeWhosFastest() ),返回对象的类型必须是该返回类型的子类或其本身。假设存在如下图所示的类层次结构:ImaginaryNumber 是 java.lang.Number的子类,而后者又是 Object 的子类。
现在假设你声明了一个返回 Number 的方法:
public Number returnANumber() {
...
}
returnANumber() 方法可以返回 ImaginaryNumber 类型,但不能返回 Object 类型。
ImaginaryNumber 的实例同时也是 Number 的实例,因为 ImaginaryNumber 是 Number 的子类。然而,Object 并不一定属于 Number 类型——它可能是 String 或其他类型。
这种称为 covariant return type 的技术,意味着返回类型可以与子类沿相同方向发生变化。
使用 this 关键字
在实例方法或构造函数中,this 是对当前对象的引用——即正在被调用的方法或构造函数所属的对象。通过使用 this,可以在实例方法或构造函数内部访问当前对象的任何成员。
与字段配合使用
使用 this 关键字最常见的原因是某个字段被方法或构造函数参数所 shadowed。
例如,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 类,其实现方式与“对象”部分中的不同。
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的矩形。双参数构造函数会调用四参数构造函数,传入 width 和 height 参数,但始终使用(0,0)坐标。与之前相同,编译器会根据参数数量和类型决定调用哪个构造函数。
若存在其他构造函数调用,必须将其置于构造函数的第一行。
控制对类成员的访问
访问级别修饰符决定其他类是否可以使用特定字段或调用特定方法。访问控制分为两个级别:
- 在最高级别—— public,或 package-private(无显式修饰符)。
- 在成员级别—— public, private, protected 或 package-private(无显式修饰符)。
类可以使用修饰符 public 声明,此时该类对所有类都可见。如果类没有修饰符(默认状态,也称为包私有),则仅在其所属包内可见(包是命名相关的类组——你将在后续章节中学习相关内容)。
在成员级别,您也可以像最高层级一样使用 public 修饰符或不使用修饰符(包私有),且含义相同。成员还拥有两个额外的访问修饰符: private(私有)和 protected(受保护)。private 修饰符表示该成员仅能在其所属类内部访问;protected 修饰符则表示该成员不仅可在其所属包内访问(与包私有相同),还允许其他包中该类的子类访问。
下表显示了每个修饰符允许的成员访问权限。
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| public | Y | Y | Y | Y |
| protected | Y | Y | Y | N |
| no-modifier | Y | Y | N | N |
| private | Y | N | N | N |
第一列数据表示类本身是否具有访问由访问级别定义的成员的权限。如您所见,类始终可以访问其自身的成员。
第二列指示与该类位于同一包中的类(无论其父类关系)是否可访问该成员。
第三列指示该类在该包外部声明的子类是否能够访问该成员。
第四列表示所有类是否都能访问该成员。
访问权限以两种方式影响你。首先,当你使用来自其他来源的类时(例如Java平台中的类),访问权限决定了你的类可以使用这些类的哪些成员。其次,当你编写一个类时,需要为类中的每个成员变量和方法确定应具有的访问权限级别。
访问级别的设置建议
如果其他程序员使用你的类,你需要确保不会因误用而引发错误。访问权限级别能帮助你实现这一点。
为特定成员设置最合理的访问权限级别。除非有充分理由,否则请使用私有权限。
除常量外,应避免使用 public 字段。教程中的许多示例都使用了 public 字段。虽然这有助于简洁地说明某些要点,但不建议在生产代码中采用。这是因为 public 字段往往会将您与特定实现绑定,限制您修改代码的灵活性,因此并非良好实践。
理解类成员
在本节中,我们将探讨如何使用static关键字来创建属于类本身而非类实例的字段和方法。
类变量
当多个对象从同一类蓝图创建时,它们各自拥有独立的实例变量副本。以 Bicycle 类为例,其实例变量包括 candence、gear 和 speed 。每个 Bicycle 对象都为这些变量存储着各自的值,且这些值保存在不同的内存位置。
有时,你需要创建适用于所有对象的公共变量。这可以通过 static 修饰符实现。声明中带有 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 关键字,因为此时不存在实例供其引用。
常量
static 修饰符与 final 修饰符结合使用时,也可用于定义常量。final 修饰符表明该字段的值不可更改。
例如,以下变量声明定义了一个名为 PI 的常量,其值是圆周率(圆的周长与直径之比)的近似值:
static final double PI = 3.141592653589793;
以这种方式定义的常量不可重新赋值,若程序尝试如此操作,将引发编译时错误。按惯例,常量值的名称采用全大写字母书写。若名称由多个单词组成,则各单词间以下划线(_)分隔。
注意:若将基本类型或字符串定义为常量且其值在编译时已知,编译器会将代码中所有常量名称替换为其值。此类常量称为编译时常量。若外部环境中常量的值发生变化(例如法定圆周率π实际应为3.975),则需重新编译所有使用该常量的类以获取最新值。
Bicycle 类
经过本节的所有修改后,Bicycle 类现在长这样:
public class NewBicycle {
private int cadence;
private int gear;
private int speed;
private int id;
private static int numberOfBicycles = 0;
public NewBicycle(int startCadence, int startSpeed, int startGear) {
gear = startGear;
cadence = startCadence;
speed = startSpeed;
id = ++numberOfBicycles;
}
public int getCadence() {
return cadence;
}
public int getGear() {
return gear;
}
public int getSpeed() {
return speed;
}
public int getId() {
return id;
}
public static int getNumberOfBicycles() {
return numberOfBicycles;
}
public void setCadence(int cadence) {
this.cadence = cadence;
}
public void setGear(int gear) {
this.gear = gear;
}
public void applyBrake(int decrement) {
speed -= decrement;
}
public void speedUp(int increment) {
speed += increment;
}
}
初始化字段
正如你所见,你通常可以在字段声明中为其提供初始值:
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
}
}
私有静态方法的优势在于,若需重新初始化类变量,它们可供后续复用。
请注意,您无法重新定义静态代码块的内容。一旦写入,就无法阻止该代码块的执行。如果静态代码块的内容因任何原因无法执行,则应用程序将无法正常工作,因为您将无法为该类实例化任何对象。当静态代码块包含访问外部资源(如文件系统或网络)的代码时,可能会发生这种情况。
初始化实例成员
通常,您会在构造函数中放置初始化实例变量的代码。除了使用构造函数初始化实例变量外,还有两种替代方案:initializer blocks 和 final 方法。
实例变量的初始化器块与静态初始化器块完全相同,只是省略了static关键字:
{
// whatever code is needed for initialization goes here
}
Java编译器会将初始化器代码块复制到每个构造函数中。因此,这种方法可用于在多个构造函数间共享一段代码。
final 修饰的方法不能在子类中被重写。这将在继承章节中进行讨论。以下是一个使用最终方法初始化实例变量的示例:
class Whatever {
private varType myVar = initializeInstanceVariable();
protected final varType initializeInstanceVariable() {
// initialization code goes here
}
}
这在子类可能需要重用初始化方法时尤其有用。该方法被声明为final,因为在实例初始化过程中调用非final方法可能会引发问题。
创建和使用类与对象的总结
类声明用于命名类,并将类主体用大括号括起。类名前可添加修饰符。类主体包含该类的字段、方法和构造函数。类通过字段存储状态信息,通过方法实现行为。用于初始化类新实例的构造函数采用类名命名,其形式类似于无返回类型的方法。
类及其成员的访问权限控制方式相同:通过在声明中使用访问修饰符(如public)来实现。
通过在成员声明中使用 static 关键字,可以指定类变量或类方法。未声明为 static 的成员默认为实例成员。类变量由该类的全部实例共享,既可通过类名访问,也可通过实例引用访问。类实例各自拥有实例变量的独立副本,必须通过实例引用进行访问。
通过使用 new 运算符和构造函数,可以从类创建对象。new 运算符返回对所创建对象的引用。该引用可赋值给变量或直接使用。
对于声明所在类外部代码可访问的实例变量和方法,可通过限定名进行引用。实例变量的限定名格式如下:
objectReference.variableName
方法的限定名称如下所示:
objectReference.methodName(argumentList)
或者
objectReference.methodName()
垃圾回收器会自动清理未使用的对象。当程序中不再存在指向某个对象的引用时,该对象即被视为未使用。您可以通过将持有引用的变量设置为 null 来显式释放引用。