更多关于类的内容

关于类的更多内容

本节将深入探讨类的相关内容,这些内容依赖于对象引用和点运算符的使用,而这些概念已在前面的对象章节中介绍过:

  • 方法的返回值。
  • 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() ),返回对象的类型必须是该返回类型的子类或其本身。假设存在如下图所示的类层次结构:ImaginaryNumberjava.lang.Number的子类,而后者又是 Object 的子类。

现在假设你声明了一个返回 Number 的方法:

public Number returnANumber() {
    ...
}

returnANumber() 方法可以返回 ImaginaryNumber 类型,但不能返回 Object 类型。
ImaginaryNumber 的实例同时也是 Number 的实例,因为 ImaginaryNumberNumber 的子类。然而,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的矩形。双参数构造函数会调用四参数构造函数,传入 widthheight 参数,但始终使用(0,0)坐标。与之前相同,编译器会根据参数数量和类型决定调用哪个构造函数。

若存在其他构造函数调用,必须将其置于构造函数的第一行。

控制对类成员的访问

访问级别修饰符决定其他类是否可以使用特定字段或调用特定方法。访问控制分为两个级别:

  • 在最高级别—— public,或 package-private(无显式修饰符)。
  • 在成员级别—— public, private, protectedpackage-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、gearspeed 。每个 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 blocksfinal 方法。
实例变量的初始化器块与静态初始化器块完全相同,只是省略了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 来显式释放引用。