5.类和对象

110 阅读39分钟

在题为“面向对象编程概念”的课程中,面向对象概念的介绍以自行车类为例,以赛车、山地自行车和双人自行车为子类。下面是类的可能实现的示例代码,为您提供类声明的概述。本文的后续部分 课 将逐步备份和解释类声明。目前,不要关心细节。Bicycle

public class Bicycle {
        
    // the Bicycle class has
    // three fields
    public int cadence;
    public int gear;
    public int speed;
        
    // the Bicycle class has
    // one constructor
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }
        
    // the Bicycle class has
    // four methods
    public void setCadence(int newValue) {
        cadence = newValue;
    }
        
    public void setGear(int newValue) {
        gear = newValue;
    }
        
    public void applyBrake(int decrement) {
        speed -= decrement;
    }
        
    public void speedUp(int increment) {
        speed += increment;
    }
        
}

作为子类的类的类的类声明可能如下所示:MountainBike Bicycle

public class MountainBike extends Bicycle {
        
    // the MountainBike subclass has
    // one field
    public int seatHeight;

    // the MountainBike subclass has
    // one constructor
    public MountainBike(int startHeight, int startCadence,
                        int startSpeed, int startGear) {
        super(startCadence, startSpeed, startGear);
        seatHeight = startHeight;
    }   
        
    // the MountainBike subclass has
    // one method
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   

}

MountainBike继承了 的所有字段和方法,并添加了字段和设置它的方法(山地自行车的座椅可以根据地形需要上下移动)。Bicycle seatHeight

声明类

您已经看到按以下方式定义的类:

class MyClass {
    // field, constructor, and 
    // method declarations
}

这是一个类声明。类主体(大括号之间的区域)包含为从类创建的对象的生命周期提供的所有代码:用于初始化新对象的构造函数、提供类及其对象状态的字段的声明,以及用于实现类及其对象行为的方法。

前面的类声明是最小的。它仅包含类声明中必需的那些组件。可以在类声明的开头提供有关类的详细信息,例如其超类的名称、它是否实现任何接口等。例如

class MyClass extends MySuperClass implements YourInterface {
    // field, constructor, and
    // method declarations
}

表示它是 的子类,并且它实现了接口。MyClass MySuperClass YourInterface

您还可以在一开始就添加修饰符,例如 public 或 private,这样您就可以看到类声明的开头行可能会变得非常复杂。修饰符 public 和 private,它们决定了其他类可以访问的内容,本文稍后将讨论 课。 教训 on interfaces and inheritance 将解释如何在类声明中使用 extends 和 implements 关键字以及为什么使用。目前,您无需担心这些额外的并发症。MyClass

通常,类声明可以按顺序包含以下组件:

  1. 修饰符,例如 publicprivate 以及稍后会遇到的许多其他修饰符。(但是,请注意,private 修饰符只能应用于嵌套类
  2. 类名,首字母按约定大写。
  3. 类的父类(超类)的名称(如果有),前面有关键字 extendend。一个类只能扩展(子类)一个父类。
  4. 由类实现的接口的逗号分隔列表(如果有),前面有关键字 implements。一个类可以实现多个接口。
  5. 类正文,用大括号 {} 括起来。

声明成员变量

有几种类型的变量:

  • 类中的成员变量 - 这些变量称为字段
  • 方法或代码块中的变量 - 这些变量称为局部变量
  • 方法声明中的变量 - 这些变量称为参数

该类使用以下代码行来定义其字段:Bicycle

public int cadence;
public int gear;
public int speed;

字段声明由三个部分组成,按顺序排列:

  1. 零个或多个修饰符,例如 或 .public``private
  2. 字段的类型。
  3. 字段的名称。

的字段命名为 、 和 和 的数据类型均为整数 ()。关键字将这些字段标识为公共成员,可由可以访问该类的任何对象访问。Bicycle``cadence``gear``speed``int``public

访问修饰符

使用的第一个(最左边)修饰符允许您控制哪些其他类可以访问成员字段。目前,仅考虑 和 。其他访问修饰符将在后面讨论。public``private

  • public修饰符 - 可从所有类访问该字段。
  • private修饰符 - 字段只能在其自己的类中访问。

本着封装的精神,将字段设为私有是很常见的。这意味着只能从 Bicycle 类直接访问它们。但是,我们仍然需要访问这些值。这可以通过添加公共方法来间接完成,这些方法为我们获取字段值:

public class Bicycle {
        
    private int cadence;
    private int gear;
    private int speed;
        
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }
        
    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;
    }
}

类型

所有变量都必须具有类型。您可以使用基元类型,例如 、 、 等。或者,您可以使用引用类型,例如字符串、数组或对象。int``float``boolean

变量名称

所有变量(无论是字段、局部变量还是参数)都遵循语言基础知识中介绍的相同命名规则和约定 课程,变量 - 命名

在这个 课 请注意,方法和类名称使用相同的命名规则和约定,但

  • 类名的首字母应大写,并且
  • 方法名称中的第一个(或唯一)单词应该是动词。

定义方法

下面是一个典型的方法声明示例:

public double calculateAnswer(double wingSpan, int numberOfEngines,
                              double length, double grossTons) {
    //do the calculation here
}

方法声明的唯一必需元素是方法的返回类型、名称、一对括号和大括号之间的正文。() {}

更一般地说,方法声明有六个组件,按顺序排列:

  1. 修饰符 - 例如 、 以及稍后将了解的其他修饰符。public private
  2. 返回类型 - 方法返回的值的数据类型,或者方法未返回值的数据类型。void
  3. 方法名称 - 字段名称的规则也适用于方法名称,但约定略有不同。
  4. 括号中的参数列表 - 以逗号分隔的输入参数列表,前面是其数据类型,用括号括起来。如果没有参数,则必须使用空括号。()
  5. 例外列表 - 稍后将讨论。
  6. 方法主体,括在大括号之间 - 方法的代码,包括局部变量的声明,位于此处。

修饰符、返回类型和参数将在后面讨论 课。 例外情况将在后面讨论 课。


定义: 方法声明的两个组件由方法签名组成,即方法的名称和参数类型。


上面声明的方法的签名是:

calculateAnswer(double, int, double, double)

命名方法

尽管方法名称可以是任何合法标识符,但代码约定会限制方法名称。按照惯例,方法名称应为小写动词或以小写动词开头的多词名称,后跟形容词、名词等。在多词名称中,第二个单词和后面每个单词的第一个字母都应大写。以下是一些示例:

run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty

通常,方法在其类中具有唯一的名称。但是,由于方法重载,某个方法可能与其他方法同名。

重载方法

Java 编程语言支持重载方法,Java 可以区分具有不同方法签名的方法。这意味着,如果类中的方法具有不同的参数列表,则它们可以具有相同的名称(对此有一些限定条件将在 题为 “接口和继承”)。

假设您有一个类,该类可以使用书法绘制各种类型的数据(字符串、整数等),并且包含用于绘制每种数据类型的方法。为每个方法使用新名称(例如,、、等)很麻烦。在 Java 编程语言中,您可以对所有绘图方法使用相同的名称,但为每个方法传递不同的参数列表。因此,数据绘制类可以声明四个名为 的方法,每个方法都有不同的参数列表。drawString drawInteger drawFloat draw

public class DataArtist {
    ...
    public void draw(String s) {
        ...
    }
    public void draw(int i) {
        ...
    }
    public void draw(double f) {
        ...
    }
    public void draw(int i, double f) {
        ...
    }
}

重载方法的区别在于传递到方法中的参数的数量和类型。在代码示例中,是不同且唯一的方法,因为它们需要不同的参数类型。draw(String s) draw(int i)

不能声明具有相同名称、相同数量和类型的参数的多个方法,因为编译器无法区分它们。

编译器在区分方法时不考虑返回类型,因此即使两个方法具有不同的返回类型,也不能声明具有相同签名的两个方法。


注意: 应谨慎使用重载方法,因为它们会使代码的可读性大大降低。

为类提供构造函数

类包含构造函数,调用这些构造函数是为了从类蓝图创建对象。构造函数声明类似于方法声明,只不过它们使用类的名称,并且没有返回类型。例如,有一个构造函数:Bicycle

public Bicycle(int startCadence, int startSpeed, int startGear) {
    gear = startGear;
    cadence = startCadence;
    speed = startSpeed;
}

要创建一个名为 的新对象,运算符会调用构造函数:Bicycle``myBike``new

Bicycle myBike = new Bicycle(30, 0, 8);

new Bicycle(30, 0, 8)在内存中为对象创建空间并初始化其字段。

虽然只有一个构造函数,但它可以有其他构造函数,包括一个无参数构造函数:Bicycle

public Bicycle() {
    gear = 1;
    cadence = 10;
    speed = 0;
}

Bicycle yourBike = new Bicycle();调用无参构造函数以创建名为yourBike的新对象Bicycle

这两个构造函数都可以在 中声明,因为它们具有不同的参数列表。与方法一样,Java 平台根据列表中的参数数量及其类型来区分构造函数。不能为同一类编写具有相同数量和类型参数的两个构造函数,因为平台无法区分它们。这样做会导致编译时错误。Bicycle

您不必为类提供任何构造函数,但在执行此操作时必须小心。编译器会自动为任何没有构造函数的类提供无参数的默认构造函数。此默认构造函数将调用超类的无参数构造函数。在这种情况下,如果超类没有无参数构造函数,编译器会抱怨,因此您必须验证它是否具有。如果您的类没有显式超类,则它有一个隐式超类  ,它确实具有无参数构造函数。Object

您可以自己使用超类构造函数。这个开头的MountainBike类就是这样做的。

可以在构造函数的声明中使用访问修饰符来控制哪些其他类可以调用构造函数。


注意: 如果另一个类无法调用构造函数,则它不能直接创建对象。MyClass MyClass

将信息传递给方法或构造函数

方法或构造函数的声明声明该方法或构造函数的参数的编号和类型。例如,以下是根据贷款金额、利率、贷款期限(期限数)和贷款的未来价值计算房屋贷款每月还款额的方法:

public double computePayment(
                  double loanAmt,
                  double rate,
                  double futureValue,
                  int numPeriods) {
    double interest = rate / 100.0;
    double partial1 = Math.pow((1 + interest), 
                    - numPeriods);
    double denominator = (1 - partial1) / interest;
    double answer = (-loanAmt / denominator)
                    - ((futureValue * partial1) / denominator);
    return answer;
}

该方法有四个参数:贷款金额、利率、未来价值和期数。前三个是双精度浮点数,第四个是整数。这些参数在方法主体中使用,在运行时将采用传入的参数的值。


注:  参数是指方法声明中的变量列表。参数是调用方法时传入的实际值。调用方法时,使用的参数必须在类型和顺序上与声明的参数匹配。


参数类型

可以将任何数据类型用于方法或构造函数的参数。这包括基元数据类型(如双精度值、浮点数和整数),如您在方法中看到的,以及引用数据类型(如对象和数组)。computePayment

下面是一个接受数组作为参数的方法示例。在此示例中,该方法创建一个新对象,并从对象数组(假设该对象是表示 x、y 坐标的类)对其进行初始化:Polygon``Point``Point

public Polygon polygonFrom(Point[] corners) {
    // method body goes here
}

注意: 如果要将方法传递到方法中,请使用 lambda 表达式方法引用


任意数量的参数

可以使用名为 varargs 的构造将任意数量的值传递给方法。当您不知道将有多少特定类型的参数传递给该方法时,请使用 varargs。这是手动创建数组的快捷方式(以前的方法可以使用 varargs 而不是数组)。

要使用 varargs,请在最后一个参数的类型后面加上省略号(三个点,...),然后是空格和参数名称。然后,可以使用任意数量的该参数(包括无参数)调用该方法。

public Polygon polygonFrom(Point... corners) {
    int numberOfSides = corners.length;
    double squareOfSide1, lengthOfSide1;
    squareOfSide1 = (corners[1].x - corners[0].x)
                     * (corners[1].x - corners[0].x) 
                     + (corners[1].y - corners[0].y)
                     * (corners[1].y - corners[0].y);
    lengthOfSide1 = Math.sqrt(squareOfSide1);

    // more method body code follows that creates and returns a 
    // polygon connecting the Points
}

您可以看到,在方法内部,它被视为一个数组。可以使用数组或参数序列调用该方法。无论哪种情况,方法主体中的代码都会将参数视为数组。corners

您最常看到的是带有打印方法的 varargs;例如,此方法:printf

public PrintStream printf(String format, Object... args)

允许您打印任意数量的对象。可以这样写:

System.out.printf("%s: %d, %s%n", name, idnum, address);

或者像这样

System.out.printf("%s: %d, %s, %s, %s%n", name, idnum, address, phone, email);

或者具有不同数量的参数。

参数名称

向方法或构造函数声明参数时,需要为该参数提供一个名称。此名称在方法主体中用于引用传入的参数。

参数的名称在其作用域中必须是唯一的。它不能与同一方法或构造函数的另一个参数的名称相同,也不能是方法或构造函数中局部变量的名称。

参数可以与类的某个字段同名。如果是这种情况,则称该参数会遮蔽该字段。阴影字段可能会使代码难以阅读,并且通常仅在设置特定字段的构造函数和方法中使用。例如,请考虑以下类及其方法:Circle setOrigin

public class Circle {
    private int x, y, radius;
    public void setOrigin(int x, int y) {
        ...
    }
}

该类有三个字段:、 和 。该方法有两个参数,每个参数都与其中一个字段同名。每个方法参数都隐藏共享其名称的字段。因此,使用简单名称或在方法主体中引用参数,而不是字段。若要访问该字段,必须使用限定名称。这将在后面讨论 课 在标题为“使用关键字”的部分中。Circle``x``y``radius``setOrigin``x``y``this

传递基元数据类型参数

基元参数(如 an 或 a )按值传递到方法中。这意味着对参数值的任何更改都仅存在于方法的范围内。当方法返回时,参数将消失,对它们所做的任何更改都将丢失。下面是一个示例:int``double

public class PassPrimitiveByValue {

    public static void main(String[] args) {
           
        int x = 3;
           
        // invoke passMethod() with 
        // x as argument
        passMethod(x);
           
        // print x to see if its 
        // value has changed
        System.out.println("After invoking passMethod, x = " + x);
           
    }
        
    // change parameter in passMethod()
    public static void passMethod(int p) {
        p = 10;
    }
}

运行此程序时,输出为:

After invoking passMethod, x = 3

传递引用数据类型参数

引用数据类型参数(如对象)也按值传递到方法中。这意味着,当方法返回时,传入的引用仍引用与以前相同的对象。但是,如果对象字段的值具有适当的访问级别,则可以在方法中更改它们。

例如,考虑任意类中移动对象的方法:Circle

public void moveCircle(Circle circle, int deltaX, int deltaY) {
    // code to move origin of circle to x+deltaX, y+deltaY
    circle.setX(circle.getX() + deltaX);
    circle.setY(circle.getY() + deltaY);
        
    // code to assign a new reference to circle
    circle = new Circle(0, 0);
}

使用以下参数调用该方法:

moveCircle(myCircle, 23, 56)

在方法中,最初是指 .该方法将引用对象(即 )的 x 和 y 坐标分别更改 23 和 56。当方法返回时,这些更改将持续存在。然后,使用 分配对新对象的引用。但是,此重新分配没有永久性,因为引用是按值传入的,无法更改。在方法中,指向的对象已更改,但是,当方法返回时,仍引用与调用方法之前相同的对象。

对象

一个典型的 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

以下三个部分使用上面的示例来描述程序中对象的生命周期。从他们那里,您将学习如何编写代码,以便在您自己的程序中创建和使用对象。您还将了解系统如何在对象生命周期结束后进行清理。

创建对象

如您所知,类提供对象的蓝图;从类创建对象。从 CreateObjectDemo 程序中获取的以下每个语句都会创建一个对象并将其分配给变量:

Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);

第一行创建 Point 类的对象,第二行和第三行分别创建 Rectangle 类的对象。

这些陈述中的每一个都有三个部分(下面详细讨论):

  1. 声明:粗体设置的代码是将变量名称与对象类型关联的所有变量声明
  2. 实例化new 关键字是创建对象的 Java 运算符。
  3. 初始化new 运算符后跟对构造函数的调用,该构造函数初始化新对象。

声明一个变量来引用一个对象

之前,您了解到要声明变量,您需要编写:

type name;

这将通知编译器,您将使用 name 来引用类型为 type 的数据。对于基元变量,此声明还会为变量保留适当的内存量。

您还可以在引用变量自己的行上声明引用变量。例如:

Point originOne;

如果这样声明,则在实际创建对象并将其分配给对象之前,其值将不确定。仅声明引用变量不会创建对象。为此,您需要使用运算符,如下一节所述。必须先将对象分配给该对象,然后才能在代码中使用该对象。否则,您将收到编译器错误。originOne``new``originOne

处于此状态的变量(当前不引用任何对象)可以按如下方式说明(变量名称,加上指向任何对象的引用):originOne

originOne 为 null。

实例化类

new 运算符通过为新对象分配内存并返回对该内存的引用来实例化类。new 运算符还调用对象构造函数。


注意: 短语“实例化类”与“创建对象”的含义相同。当你创建一个对象时,你正在创建一个类的“实例”,因此“实例化”了一个类。


new 运算符需要一个 postfix 参数:对构造函数的调用。构造函数的名称提供要实例化的类的名称。

new 运算符返回对它所创建对象的引用。此引用通常分配给适当类型的变量,例如:

Point originOne = new Point(23, 94);

运算符返回的引用不必分配给变量。它也可以直接在表达式中使用。例如:

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);

执行此语句的结果如下图所示:

originOne 现在指向一个 Point 对象。

下面是 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);

这将调用初始化为 的构造函数之一。此外,构造函数设置为 100 和 200。现在,有两个对同一 Point 对象的引用 - 一个对象可以有多个对它的引用,如下图所示:Rectangle``origin``originOne``width``height

现在,矩形的 origin 变量也指向 Point。

下面的代码行调用需要两个整数参数的构造函数,这两个参数提供宽度高度的初始值。如果检查构造函数中的代码,将看到它创建了一个新的 Point 对象,其 x 和 y 值初始化为 0:Rectangle

Rectangle rectTwo = new Rectangle(50, 100);

以下语句中使用的 Rectangle 构造函数不接受任何参数,因此称为无参数构造函数

Rectangle rect = new Rectangle();

所有类都至少有一个构造函数。如果一个类没有显式声明任何函数,那么 Java 编译器会自动提供一个无参数构造函数,称为默认构造函数。此默认构造函数调用类父级的无参数构造函数,如果类没有其他父级,则调用构造函数。如果父级没有构造函数(确实有构造函数),编译器将拒绝该程序。

使用对象

一旦你创建了一个对象,你可能想用它来做一些事情。您可能需要使用其某个字段的值、更改其某个字段或调用其方法之一来执行操作。

引用对象的字段

对象字段按其名称进行访问。您必须使用明确的名称。

您可以为其自己的类中的字段使用简单名称。例如,我们可以在类中添加一个语句,该语句打印 和 :Rectangle``width``height

System.out.println("Width and height are: " + width + ", " + height);

在本例中,和是简单的名称。width``height

对象类之外的代码必须使用对象引用或表达式,后跟点 (.) 运算符,后跟简单的字段名称,如下所示:

objectReference.fieldName

例如,CreateObjectDemo 类中的代码位于 Rectangle 类的代码之外。因此,若要引用名为 rectOne 的 Rectangle 对象中的 origin、width 和 height 字段,CreateObjectDemo 类必须分别使用名称 rectOne.origin``、rectOne.width 和 `````rectOne.height。 ````程序使用其中两个名称来显示 rectOne 的宽度高度`:

System.out.println("Width of rectOne: "  + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);

尝试使用 CreateObjectDemo 类中代码中的简单名称 width 和 height 是没有意义的——这些字段仅存在于对象中——并导致编译器错误。

稍后,该程序使用类似的代码来显示有关 rectTwo 的信息。相同类型的对象具有相同实例字段的副本。因此,每个 Rectangle 对象都具有名为 originwidth 和 height 的字段。通过对象引用访问实例字段时,将引用该特定对象的字段。CreateObjectDemo 程序中的两个对象 rectOne 和 rectTwo 具有不同的原点宽度高度字段。

若要访问字段,可以使用对对象的命名引用(如前面的示例所示),也可以使用返回对象引用的任何表达式。回想一下,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 的方法并显示结果。第二行移动 rectTwo,因为 move() 方法将新值分配给对象的 origin.x 和 origin.y。```getArea()`

与实例字段一样,objectReference 必须是对对象的引用。可以使用变量名称,但也可以使用返回对象引用的任何表达式。new 运算符返回一个对象引用,因此您可以使用从 new 返回的值来调用新对象的方法:

new Rectangle(100, 50).getArea()

表达式 new Rectangle(100, 50) 返回引用 Rectangle 对象的对象引用。如图所示,您可以使用点表示法调用新矩形的 getArea() 方法来计算新矩形的面积。

某些方法(例如 getArea())返回一个值。对于返回值的方法,可以在表达式中使用方法调用。您可以将返回值分配给变量,使用它来做出决策或控制循环。此代码将 getArea() 返回的值分配给变量:areaOfRectangle

int areaOfRectangle = new Rectangle(100, 50).getArea();

请记住,在特定对象上调用方法与向该对象发送消息相同。在本例中,调用 getArea() 的对象是构造函数返回的矩形。

垃圾回收器

某些面向对象的语言要求您跟踪您创建的所有对象,并在不再需要它们时显式销毁它们。显式管理内存既繁琐又容易出错。Java 平台允许您创建任意数量的对象(当然,受系统可以处理的范围的限制),并且您不必担心销毁它们。Java 运行时环境在确定不再使用对象时会删除这些对象。此过程称为垃圾回收

当没有对该对象的更多引用时,该对象有资格进行垃圾回收。当变量超出范围时,通常会删除变量中保存的引用。或者,可以通过将变量设置为特殊值 null 来显式删除对象引用。请记住,一个程序可以对同一对象有多个引用;必须先删除对对象的所有引用,然后该对象才有资格进行垃圾回收。

Java 运行时环境有一个垃圾回收器,该回收器会定期释放不再引用的对象使用的内存。垃圾回收器在确定时机成熟时会自动执行其工作。

有关课程的更多信息

本节介绍依赖于使用对象引用的类的更多方面,以及您在前面有关对象的部分中了解的运算符:dot

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

从方法返回值

方法返回到调用它的代码,当它

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

以先发生者为准。

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

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

return;

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

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

return returnValue;

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

在有关对象的章节中讨论的 Rectangle 类中的方法返回一个整数:getArea()``Rectangle

    // 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``Object

ImaginaryNumber 的类层次结构

ImaginaryNumber 的类层次结构

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

public Number returnANumber() {
    ...
}

该方法可以返回 . 是 ,因为它是 的子类。但是,an 不一定是 a — 它可以是 a 或另一种类型。returnANumber``ImaginaryNumber``Object``ImaginaryNumber``Number``Number``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 是构造函数第一个参数的本地副本。若要引用字段 x,构造函数必须使用 .Point``this.x

使用this调用构造函数

在构造函数中,还可以使用 关键字调用同一类中的另一个构造函数。这样做称为显式构造函数调用。这是另一个类,其实现与 Objects 部分中的实现不同。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 坐标。和以前一样,编译器根据参数的数量和类型确定要调用的构造函数。Rectangle

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

控制对类成员的访问

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

  • 在顶层 - 或 package-private(无显式修饰符)。public
  • 在成员级别—, , ,或 package-private(无显式修饰符)。public private protected

一个类可以用修饰符声明,在这种情况下,该类对任何地方的所有类都是可见的。如果一个类没有修饰符(默认值,也称为 package-private),则它仅在其自己的包中可见(包被命名为相关类组 - 您将在后面了解它们 课程。public

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

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

修饰语亚纲世界
publicYYYY
protectedYYYN
无修饰符YYNN
privateYNNN

第一个数据列指示类本身是否有权访问由访问级别定义的成员。如您所见,类始终可以访问自己的成员。第二列指示与该类位于同一包中的类(无论其父级如何)是否有权访问该成员。第三列指示在此包外部声明的类的子类是否有权访问该成员。第四列指示是否所有类都有权访问该成员。

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

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

用于说明访问级别的示例的类和包

用于说明访问级别的示例的类和包

下表显示了 Alpha 类的成员对于可应用于它们的每个访问修饰符的可见位置。

修饰语阿尔法试用版阿尔法子伽马
publicYYYY
protectedYYYN
无修饰符YYNN
privateYNNN

选择访问级别的提示: 

如果其他程序员使用您的类,您需要确保不会发生误用错误。访问级别可以帮助您执行此操作。

  • 使用对特定成员有意义的最严格的访问级别。除非您有充分的理由不这样做,否则请使用。private
  • 避免使用除常量之外的字段。(许多例子 教程 使用公共字段。这可能有助于简明扼要地说明一些要点,但不建议将其用于生产代码。公共字段往往会将您链接到特定的实现,并限制您更改代码的灵活性。public

了解类成员

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

成员变量

当从同一类蓝图创建多个对象时,每个对象都有自己不同的实例变量副本。对于类,实例变量为 、 和 。每个对象都有自己的这些变量值,Bicycle cadence gear speed Bicycle存储在不同的内存位置。

有时,您希望具有所有对象通用的变量。这是通过修饰符完成的。声明中包含修饰符的字段称为静态字段类变量。它们与类相关联,而不是与任何对象相关联。类的每个实例共享一个类变量,该变量位于内存中的一个固定位置。任何对象都可以更改类变量的值,但也可以在不创建类实例的情况下操作类变量。static static

例如,假设您要创建多个对象并为每个对象分配一个序列号,第一个对象从 1 开始。此 ID 号对于每个对象都是唯一的,因此是一个实例变量。同时,您需要一个字段来跟踪已创建的对象数量,以便知道要为下一个对象分配什么 ID。这样的字段与任何单个对象无关,而是与整个类相关。为此,您需要一个类变量,如下所示:Bicycle Bicycle 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(圆的周长与其直径之比)的近似值: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;
}

当初始化值可用并且初始化可以放在一行上时,这很有效。但是,这种初始化形式由于其简单性而具有局限性。如果初始化需要一些逻辑(例如,错误处理或填充复杂数组的循环),则简单的赋值是不够的。实例变量可以在构造函数中初始化,其中可以使用错误处理或其他逻辑。为了为类变量提供相同的功能,Java 编程语言包括静态初始化块for


注意: 没有必要在类定义的开头声明字段,尽管这是最常见的做法。在使用它们之前,只需要声明和初始化它们。


初始化静态代码块

静态初始化块是用大括号括起来并在关键字前面的普通代码块。下面是一个示例:{ } 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 方法初始化实例变量的示例:

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