1

类的更多方面

本节介绍依赖于使用对象引用的类的更多方面以及你在前面的对象部分中了解到的点运算符。

从方法返回值

方法返回到调用它的代码。

  • 完成方法中的所有语句。
  • 到达return语句。
  • 或抛出异常(稍后介绍)。

以先发生者为准。

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

声明为void的任何方法都不返回值,它不需要包含return语句,但它可能会这样做,在这种情况下,可以使用return语句分支出控制流程块并退出该方法,并且可以像这样使用:

return;

如果你尝试从声明为void的方法返回值,则会出现编译器错误。

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

return returnValue;

返回值的数据类型必须与方法声明的返回类型匹配,你不能从声明为返回布尔值的方法返回一个整数值。

在对象部分中讨论的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,返回对象的类型类必须是返回类型的子类或确切的类。假设你有一个类层次结构,其中ImaginaryNumberjava.lang.Number的子类,而java.lang.Number又是Object的子类,如下图所示。

classes-hierarchy.gif

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

public Number returnANumber() {
    ...
}

returnANumber方法可以返回ImaginaryNumber而不是ObjectImaginaryNumber是一个Number,因为它是Number的子类,但是,Object不一定是Number — 它可以是String或其他类型。

你可以重写方法并定义它以返回原始子类的方法,如下所示:

public ImaginaryNumber returnANumber() {
    ...
}

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

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

使用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矩形。双参数构造函数调用四参数构造函数,传递宽度和高度,但始终使用0,0坐标,和之前一样,编译器根据参数的数量和类型确定要调用的构造函数。

如果存在,则另一个构造函数的调用必须是构造函数中的第一行。

控制对类成员的访问

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

  • 在顶级 — publicpackage-private(没有显式修饰符)。
  • 在成员级别 — publicprivateprotectedpackage-private(无显式修饰符)。

可以使用修饰符public声明一个类,在这种情况下,该类对于所有类都可见,如果一个类没有修饰符(默认,也称为包私有),它只在自己的包中可见(包是相关类的命名组 — 你将在后面的课程中了解它们)。

在成员级别,你也可以使用public修饰符或无修饰符(package-private),就像使用顶级类一样,并且具有相同的含义。对于成员,还有两个额外的访问修饰符:privateprotectedprivate修饰符指定只能在其自己的类中访问该成员,protected修饰符指定只能在其自己的包中访问该成员(与package-private一样),此外,还可以在另一个包中通过其类的子类访问该成员。

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

修饰符 子类 所有
public Y Y Y Y
protected Y Y Y N
无修饰符 Y Y N N
private Y N N N

第一个数据列指示类本身是否可以访问由访问级别定义的成员,如你所见,类始终可以访问自己的成员,第二列指示与该类相同的包中的类(不管父子关系)可以访问该成员,第三列指示在此包外声明的该类的子类是否可以访问该成员,第四列指示是否所有类都可以访问该成员。

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

让我们看一下类的集合,看看访问级别如何影响可见性,下图显示了此示例中的四个类以及它们之间的关系。

classes-access.gif

下表显示了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字段(本教程中的许多示例都使用public字段,这可能有助于简明地说明一些要点,但不建议用于生产代码),public字段倾向于将你链接到特定实现,并限制你更改代码的灵活性。

了解类成员

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

类变量

当从同一个类蓝图创建许多对象时,它们每个都有自己不同的实例变量副本,在Bicycle类的情况下,实例变量是cadencegearspeed,每个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关键字,因为没有要引用的实例。

常量

static修饰符与final修饰符结合使用,也用于定义常量,final修饰符表示此字段的值不能更改。

例如,以下变量声明定义了一个名为PI的常量,其值是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;
    }
}

初始化字段

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

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方法可能会导致问题。

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

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

你可以通过相同的方式控制对类和成员的访问:在声明中使用诸如public之类的访问修饰符。

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

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

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

objectReference.variableName

方法的限定名称如下所示:

objectReference.methodName(argumentList)

或者:

objectReference.methodName()

垃圾收集器自动清理未使用的对象,如果程序不再包含对它的引用,则不使用该对象,你可以通过将包含引用的变量设置为null来显式删除引用。


上一篇: 对象
下一篇:嵌套类

博弈
2.5k 声望1.5k 粉丝

态度决定一切