本章涵盖了 Java 编程语言编程的基础,涉及类、对象等内容。

类和对象

类和对象(Class and Object)描述了如何从已创建的对象编写类以及如何创建和使用对象。

声明类

类的声明(class declaration)方式如下:

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

类可以继承自一个超类并实现接口:

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

一般来说,类声明可以按照以下顺序包括这些组件:

  1. 修饰符(modifier),如 public,_private_和其他一些你以后会遇到的。
  2. 类名称,按照惯例大写的初始字母。
  3. 该类的父级(超类,superclass)的名称;如果有,在关键字 extends 之前加上。 一个类只能扩展一个父类。
  4. 以逗号分隔的由类实现的接口列;如果有的话,在关键字 implements 之前。 一个类可以实现多个接口。
  5. 类的主体,被大括号 {} 包围。

声明成员变量

有几种变量:

  • 类中的成员变量(member variable) - 这些称为字段(field)。
  • 方法或代码块中的变量 - 这些被称为局部变量(local variable)。
  • 方法声明中的变量 - 这些称为参数(parameter)。

字段声明由以下三个部分组成:

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

访问修饰符

最左侧的修饰符可让您控制其他类对成员字段的访问。 目前,只考虑 public 和 private。

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

以封装的精神,通常使字段私有。需要访问这些值时, 可以通过添加获取字段值的公共方法间接完成。

声明方法

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

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

声明一个变量必须的元素是方法的返回类型、一对圆括号 ()、和在大括号 {} 内的主体。

更一般地说,方法声明有六个组成部分:

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

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

命名方法

按照惯例,方法名应该时一个小写的动词(verb)或以小写的动词开头且后面跟形容词、名词等的多词名称。多词名称采用驼峰式命名规则,即从第二个单词开始,每个单词的首字母大写,如 runFast、isEmpty 等。

通常,方法在类中具有唯一的名称。但由于方法重载,一个方法可能和其他方法具有相同名称,但方法参数是不一样的。

方法重载

Java编程语言支持重载方法,Java可以区分不同方法签名的方法。 这意味着如果一个类中的方法具有不同的参数列表,那么类中的方法可以具有相同的名称。

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

在区分方法时,编译器不会考虑返回类型,因此即使有不同的返回类型,也不能声明具有相同签名的两个方法。 注意:过载的方法应该谨慎使用,因为它们可以使代码可读性降低。

构造函数

一个类包含被调用以从类蓝图创建对象的构造函数(Constructor)。 构造函数声明看起来像方法声明,除了它们使用类的名称并且没有返回类型。

您不必为类提供任何构造函数,但在这么做时必须小心。 编译器自动为没有构造函数的任何类提供无参数的默认构造函数。 这个默认构造函数将调用超类的无参数构造函数。 在这种情况下,如果超类没有无参数的构造函数,编译器将会抱怨,因此您必须验证它是否正确。 如果你的类没有明确的超类,那么它有一个隐含的 Object 的超类,它有一个无参数的构造函数。

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

传递信息到一个方法或构造函数

方法或构造函数的声明声明了该方法或构造函数的参数的数量和类型。

参数类型

您可以使用任何数据类型作为方法或构造函数的参数。 这包括原始数据类型,如 double、float 和 interger,以及引用数据类型,如对象和数组。

任意数量的参数

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

要使用 varargs,可以在最后一个参数类型后面跟随省略号(ellipsis;三个点,…),之后是空格和参数名称来。 然后可以使用任意数量的参数来调用该方法,包括 none。

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
}

参数名称

当您向方法或构造函数声明参数时,您将为该参数提供一个名称。该名称在方法体内用于引用传入参数。

参数的名称在其范围内必须是唯一的。它不能与同一方法或构造函数的另一个参数的名称相同,并且它不能是方法或构造函数中局部变量的名称。

一个参数可以与一个类的字段具有相同的名称。如果是这种情况,则称该参数会影响该字段(shadow the field)。

传递原始数据类型参数

原始数据类型(Primitive Data Type)参数(如 int 或 double )通过值传递给方法。 这意味着对参数值的任何更改只存在于该方法的范围内。 当方法返回时,参数将消失,对它们的任何更改都将丢失。

传递引用数据类型参数

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

对象

创建对象

你知道,一个类提供了对象的蓝图; 你从类创建一个对象。

对象 Point originOne = new Point(23, 94); 声明有三个部分:

  1. 声明(declaration):将变量名称与对象类型相关联的变量声明,如 Point originOne
  2. 实例化(instantlation):new 关键字是创建对象的 Java 操作符。
  3. 初始化(initialization):new 运算符后跟一个构造函数的调用,该构造函数初始化新对象。

声明一个变量指向一个对象

type name;

这将通知编译器,您将使用 name 来引用类型为 type 的数据。 使用原始变量,该声明还为变量保留适当的内存量。

您也可以声明一个引用变量。

实例化一个类

new 操作符通过为新对象分配内存并返回对该内存的引用来实例化一个类。new 操作符也调用对象构造函数。

new 运算符需要一个单独的(single)、后缀(postfix)参数:调用构造函数。 构造函数的名称提供要实例化的类的名称。

new 操作符返回对它创建的对象的引用。 该引用通常被分配给适当类型的变量。

Point originOne = new Point(23, 94);

new 操作符返回的引用不必分配给变量。 它也可以直接在表达式中使用。

int height = new Rectangle().height;

初始化一个对象

所有类都至少有一个构造函数。 如果一个类没有明确声明,Java 编译器会自动提供一个无参数的构造函数,称为默认构造函数。 这个默认构造函数调用父类的无参构造函数,或者如果类没有其他父对象,则调用 Object 构造函数。 如果父类没有构造函数(Object有一个),编译器将拒绝该程序。

使用对象

引用对象的字段

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

您可以在自己的类中为一个字段使用一个简单的名称。

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

上述示例中,widthheight 就是简单的名称。

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

objectReference.fieldName

要访问某个字段,可以使用对象的命名引用,如上例所示,也可以使用返回对象引用的任何表达式。 回想一下,new 操作符返回对对象的引用。 所以可以使用从 new 返回的值来访问一个新的对象的字段:

int height = new Rectangle().height;

调用对象的方法

您还可以使用对象引用来调用对象的方法。 您将方法的简单名称附加到对象引用中,并使用中间点运算符(.)。 此外,您在封闭括号内提供该方法的任何参数。 如果该方法不需要任何参数,请使用空括号。

objectReference.methodName(argumentList);
objectReference.methodName();

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

new Rectangle(100, 50).getArea()

更多有关类的信息

从方法中返回一个值

当方法首先遇到如下情形时,将返回到调用它的代码:

  • 完成方法内的所有语句
  • 到达一个 return 语句
  • 抛出一个异常

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

任何声明为 void 的方法都不会返回一个值。 它不需要包含一个 return 语句,但也可以这样做。 在这种情况下,return语句可以使用在控制流程块外的分支并简单地使用这个方法:

return;

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

方法可以返回一个主类型,也可以返回一个引用类型。

// return a primitive type
public int getArea() {
    return width * height;
}

// return a reference type
public Bicycle seeWhosFastest(Bicycle myBike, Bicycle yourBike,
                              Environment env) {
    Bicycle fastest;
    return fastest;
}

返回类或接口

当方法使用类名作为其返回类型时,返回类型的类必须是返回类型的子类或者确切的类。

假定定义一个返回 Number 的方法:

public Number returnANumber() {
    ...
}

上述方法可以返回 ImaginaryNumber 而不是对象。ImaginaryNumber 是 Number 的子类。但是,对象(Object)不一定是 Number —— 它可以是 String 或其它类型。

您可以重写一个方法并将其定义为返回原始方法的一个子类:

public ImaginaryNumber returnANumber() {
    ...
}

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

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

使用 this 关键字

在实例方法或构造函数中,this 是对当前对象的引用 —— 即其方法或构造函数被调用的对象。 您可以通过使用 this 从实例方法或构造函数中引用当前对象的任何成员。

this 与字段一起使用

使用 this 关键字的最常见原因是因为字段被方法或构造函数参数遮蔽(shadowed)。

例如,类:

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

构造函数的每个参数都会隐藏(shadow)一个对象的字段 —— 在构造函数中 x 是构造函数第一个参数的本地副本。 要引用 Point 字段 x,构造函数必须使用 this.x .

this 与构造器一起使用

在构造函数(Constructor)中,也可以使用 this 关键字来调用同一个类中的另一个构造函数。 这样做被称为显式构造函数调用(explicit constructor invocation)。

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

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

控制对类成员的访问

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

  • 在顶级 —— public 或 package-private(没有明确的修饰符)
  • 在成员级别 —— public、private、protected 或 package-private(没有明确的修饰符)

一个类可以用修饰符 public 声明,在这种情况下,这个类对所有的类都是可见的。 如果一个类没有修饰符(默认,也称为 package-private ),它只能在它自己的包中可见(包是相关类的命名组。)

在成员级别,您也可以像使用顶级类一样使用 public 修饰符或不使用修饰符(package-private),其含义相同。 对于成员来说,还有两个额外的访问修饰符:private 和 protected。 private 修饰符指定成员只能在自己的类中访问。protected 修饰符指定该成员只能在其自己的包内访问(与 package-private 一样),另外还可以在另一个包中访问该类的子类。

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

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 平台中的类)时,访问级别确定您自己的类可以使用哪些类的成员。其次,当你编写一个类时,你需要决定每个成员变量和类中每个方法应具有的访问级别。

理解类成员

类变量

当多个对象是从同一个类的蓝图创建的时候,它们都有自己独立的实例变量(instance variables)副本。

有时候,你想拥有所有对象通用的变量,用 static 修饰符来完成。在声明中具有 static 修饰符的字段称为静态字段(static field)或类变量(class variable)。 他们与类,而不是与任何对象相关联。每个类的实例共享一个类变量,它位于内存中的一个固定位置。任何对象都可以改变类变量的值,但是也可以在不创建类的实例的情况下操作类变量。

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

但是这是不鼓励的,因为它没有说明它们是类变量。

类方法

Java 编程语言支持静态方法以及静态变量。在声明中使用 static 修饰符的静态方法应该用类名来调用,而不需要创建类的实例,如

ClassName.methodName(args)

你也可以参考像对象引用的静态方法

instanceName.methodName(args)

但这是不鼓励的,因为它没有说明它们是阶级方法。

静态方法的一个常见用途是访问静态字段。例如:

public static int getNumberOfBicycles() {
    return numberOfBicycles;
}

不是所有的实例和类变量以及方法的组合都是允许的:

  • 实例方法可以直接访问实例变量和实例方法。
  • 实例方法可以直接访问类变量和类方法。
  • 类方法可以直接访问类变量和类方法。
  • 类方法不能直接访问实例变量或实例方法 - 它们必须使用对象引用。此外,类方法不能使用 this 关键字,因为没有这个实例可以引用。

常量

static 修饰符与 final 修饰符一起也用于定义常量。final 修饰符表示这个字段的值不能改变。

static final double PI = 3.141592653589793;

以这种方式定义的常量不能被重新赋值,如果你的程序试图这样做,这是一个编译时错误。按照惯例,常数值的名字拼写成大写字母。如果名称由多个单词组成,则单词由下划线(_)分隔。

**注意:**如果基本类型(primitive type)或字符串被定义为一个常量,并且该值在编译时是已知的,那么编译器会将代码中的常量名称替换为它的值。这被称为编译时常量(compile-time constant)。如果外面世界的常的值发生变化(例如,如果规定 pi 实际应该是 3.975),则需要重新编译任何使用此常量的类来获取当前值。

初始化字段

通常可以在其声明中为字段提供初始值:

public class BedAndBreakfast {

    // initialize to 10
    public static int capacity = 10;

    // initialize to false
    private boolean full = false;
}

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

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

静态初始化块

静态初始化块(static initialization block)是用大括号({})括起来的正常的代码块,并以 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
    }
}

如果子类可能要重新使用初始化方法,这是特别有用的。该方法是最终的(final),因为在实例初始化期间调用非最终(non-final)方法可能会导致问题。

参考

  1. Learning the Java Language