系列中的上一篇
当前教程
关于类的更多内容
系列中的下一篇

系列中的上一篇: 创建和使用对象

系列中的下一篇: 嵌套类

关于类的更多内容

 

关于类的更多内容

本节介绍了更多关于类的方面,这些方面依赖于使用对象引用和点运算符,您在前面关于对象的章节中学习过这些内容。

  • 从方法中返回值。
  • this 关键字。
  • 类成员与实例成员。
  • 访问控制。

 

从方法中返回值

当方法

  • 完成方法中的所有语句,
  • 到达 return 语句,或
  • 抛出异常(稍后介绍),
  • 以先发生者为准,方法将返回到调用它的代码。

您在方法声明中声明方法的返回类型。在方法体中,您使用 return 语句来返回值。

任何声明为 void 的方法都不会返回值。它不需要包含 return 语句,但可以包含。在这种情况下,return 语句可用于从控制流块中分支出来并退出方法,并且仅以这种方式使用

return;

如果您尝试从声明为 void 的方法中返回值,则会收到编译器错误。

任何未声明为 void 的方法都必须包含一个 return 语句,该语句具有相应的返回值,如下所示

return returnValue;

返回值的数据类型必须与方法的声明返回类型匹配;您不能从声明为返回 boolean 的方法中返回整数值。

在关于对象的章节中讨论的 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;
}

 

返回值类或接口

如果您对本节感到困惑,请跳过它,并在完成关于接口和继承的章节后返回。

当方法使用类名作为其返回类型时,例如 seeWhosFastest() 所做的那样,返回对象的类型的类必须是返回类型的子类,或与返回类型完全相同。假设您有一个类层次结构,其中 ImaginaryNumberjava.lang.Number 的子类,而 java.lang.Number 又依次是 Object 的子类,如下面的图所示。

The class hierarchy for `ImaginaryNumber`

ImaginaryNumber 的类层次结构

现在假设您有一个声明为返回 Number 的方法

public Number returnANumber() {
    ...
}

returnANumber() 方法可以返回 ImaginaryNumber,但不能返回 ObjectImaginaryNumber 的实例也是 Number 的实例,因为 ImaginaryNumberNumber 的子类。但是,Object 不一定是 Number——它可能是 String 或其他类型。

您可以覆盖方法并将其定义为返回原始方法的子类,如下所示

public ImaginaryNumber returnANumber() {
    ...
}

这种技术称为协变返回类型,这意味着允许返回类型与子类在相同方向上变化。

注意:您也可以使用接口名作为返回类型。在这种情况下,返回的对象必须实现指定的接口。

 

使用 this 关键字

在实例方法或构造函数中,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 类,其实现与对象部分中的实现不同。

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 或包私有(没有显式修饰符)。
  • 在成员级别——publicprivateprotected 或包私有(没有显式修饰符)。

类可以使用修饰符 public 声明,在这种情况下,该类对所有地方的所有类可见。如果类没有修饰符(默认,也称为包私有),则它仅在其自己的包中可见(包是相关类的命名组——您将在后面的章节中了解它们)。

在成员级别,您也可以像顶层类一样使用 public 修饰符或不使用修饰符(包私有),并且含义相同。对于成员,还有两个额外的访问修饰符:privateprotectedprivate 修饰符指定成员只能在其自己的类中访问。protected 修饰符指定成员只能在其自己的包中访问(与包私有相同),此外,还可以由其类的子类在另一个包中访问。

下表显示了每个修饰符允许对成员的访问。

修饰符 子类 世界
public Y Y Y Y
protected Y Y Y N
无修饰符 Y Y N N
private Y N N N

第一列数据指示类本身是否可以访问由访问级别定义的成员。如您所见,类始终可以访问其自己的成员。

第二列指示与类位于同一包中的类(无论其父类如何)是否可以访问该成员。

第三列指示在该包之外声明的类的子类是否可以访问该成员。

第四列指示所有类是否可以访问该成员。

访问级别以两种方式影响您。首先,当您使用来自其他来源的类时,例如 Java 平台中的类,访问级别决定您自己的类可以使用这些类的哪些成员。其次,当您编写类时,您需要决定类中的每个成员变量和每个方法应该具有什么访问级别。

选择访问级别的提示

如果其他程序员使用您的类,您希望确保无法发生因误用造成的错误。访问级别可以帮助您做到这一点。

对特定成员使用最严格的访问级别。除非有充分的理由,否则使用 private

避免 public 字段,除了常量。教程中的许多示例使用 public 字段。这可能有助于简洁地说明某些要点,但不建议用于生产代码。这不是一个好习惯,因为 public 字段往往会将您绑定到特定实现,并限制您更改代码的灵活性。

 

了解类成员

在本节中,我们将讨论使用 static 关键字创建属于类而不是属于类实例的字段和方法。

类变量

当从同一个类蓝图创建多个对象时,每个对象都拥有自己实例变量的独立副本。在Bicycle类的情况下,实例变量是cadencegearspeed。每个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 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;
    }
}

 

初始化字段

如您所见,您通常可以在字段声明中提供其初始值

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
    }
}

私有静态方法的优点是,如果需要重新初始化类变量,可以稍后重复使用它们。

您应该知道,您不能重新定义静态块的内容。一旦编写了它,您就无法阻止执行此块。如果静态块的内容由于任何原因无法执行,那么您的应用程序将无法正常工作,因为您将无法为该类实例化任何对象。如果您的静态块包含访问某些外部资源(如文件系统或网络)的代码,则可能会发生这种情况。

初始化实例成员

通常,您会在构造函数中放置初始化实例变量的代码。使用构造函数初始化实例变量有两种替代方法:初始化块和最终方法。

实例变量的初始化块看起来就像静态初始化块,但没有static关键字

{
    // whatever code is needed for initialization goes here
}

Java 编译器将初始化块复制到每个构造函数中。因此,这种方法可用于在多个构造函数之间共享代码块。

最终方法不能在子类中被覆盖。这将在关于继承的部分中讨论。以下是如何使用最终方法初始化实例变量的示例

class Whatever {
    private varType myVar = initializeInstanceVariable();
        
    protected final varType initializeInstanceVariable() {

        // initialization code goes here
    }
}

这在子类可能想要重复使用初始化方法时特别有用。该方法是最终的,因为在实例初始化期间调用非最终方法会导致问题。

 

创建和使用类和对象的总结

类声明命名类,并在花括号之间包含类主体。类名前面可以加上修饰符。类主体包含类的字段、方法和构造函数。类使用字段来保存状态信息,并使用方法来实现行为。初始化类新实例的构造函数使用类名,看起来像没有返回值类型的方法。

您以相同的方式控制对类和成员的访问:在它们的声明中使用访问修饰符,例如public

您通过在成员声明中使用static关键字来指定类变量或类方法。未声明为static的成员隐式地是实例成员。类变量由类的所有实例共享,可以通过类名和实例引用进行访问。类的实例获得每个实例变量的自己的副本,必须通过实例引用进行访问。

您通过使用new运算符和构造函数从类创建对象。new运算符返回对已创建对象的引用。您可以将引用分配给变量或直接使用它。

可以从声明它们的类之外的代码访问的实例变量和方法可以通过使用限定名称来引用。实例变量的限定名称如下所示

objectReference.variableName

方法的限定名称如下所示

objectReference.methodName(argumentList)

objectReference.methodName()

垃圾收集器会自动清理未使用的对象。如果程序不再保存对对象的引用,则该对象将被视为未使用。您可以通过将保存引用的变量设置为null来显式地删除引用。


最后更新: 2024 年 1 月 5 日


系列中的上一篇
当前教程
关于类的更多内容
系列中的下一篇

系列中的上一篇: 创建和使用对象

系列中的下一篇: 嵌套类