类
在题为面向对象的编程概念课程中对面向对象概念的介绍以自行车课为例,以赛车,山地自行车和双人自行车为子类,下面是可能实现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;
}
}
作为Bicycle
的子类的MountainBike
类的类声明可能如下所示:
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
修饰符决定了其他类可以访问MyClass
的内容,本课程稍后将对此进行讨论。关于接口和继承的课程将解释如何以及为什么在类声明中使用extends
和implements
关键字,目前你不需要担心这些额外的。
通常,类声明可以按顺序包含这些组件:
- 修饰符,例如
public
、private
以及稍后你将遇到的许多其他修饰符。 - 类名,首字母大写。
- 类的父级(超类)的名称(如果有)以关键字
extends
开头,一个类只能extend
(子类)一个父类。 - 由类实现的以逗号分隔的接口列表(如果有),前面是关键字
implements
,一个类可以implement
多个接口。 - 类体,被括号围绕,
{}
。
声明成员变量
有几种变量:
- 类中的成员变量 — 这些变量称为字段。
- 方法或代码块中的变量 — 这些变量称为局部变量。
- 方法声明中的变量 — 这些变量称为参数。
Bicycle
类使用以下代码行来定义其字段:
public int cadence;
public int gear;
public int speed;
字段声明按顺序由三个部分组成:
- 零个或多个修饰符,例如
public
或private
。 - 该字段的类型。
- 该字段的名称。
Bicycle
的字段被命名为cadence
、gear
和speed
,并且都是整数数据类型(int),public
关键字将这些字段标识为公共成员,可由任何可以访问该类的对象访问。
访问修饰符
使用的第一个(最左侧)修饰符允许你控制哪些其他类可以访问成员字段,目前,只考虑public
和private
,其他访问修饰符将在后面讨论。
-
public
修饰符 — 可以从所有类访问该字段。 -
private
修饰符 — 该字段只能在其自己的类中访问。
本着封装的精神,将字段设为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
}
方法声明中唯一必需的元素是方法的返回类型、名称、一对圆括号()
和大括号之间的主体{}
。
更一般地,方法声明有六个组件,顺序如下:
- 修饰符 — 例如
public
、private
和其他你将在稍后了解的内容。 - 返回类型 — 方法返回的值的数据类型,如果方法未返回值,则返回
void
。 - 方法名称 — 字段名称的规则也适用于方法名称,但约定略有不同。
- 括号中的参数列表 — 以逗号分隔的输入参数列表,前面是数据类型,括在括号中
()
,如果没有参数,则必须使用空括号。 - 一个异常列表 — 稍后讨论。
- 括号之间的方法体 — 方法的代码,包括局部变量的声明,在这里。
修饰符、返回类型和参数将在本课程的后面部分讨论,异常将在后面的课程中讨论。
定义:方法声明的两个组件包括方法签名 — 方法的名称和参数类型。
上面声明的方法的签名是:
calculateAnswer(double, int, double, double)
命名方法
虽然方法名称可以是任何合法标识符,但代码约定限制方法名称,按照惯例,方法名称应该是小写的动词或以小写的动词开头的多单词名称,后跟形容词、名词等。在多单词名称中,第二个和后面每个单词的第一个字母应该大写,这里有些例子:
run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty
通常,方法在其类中具有唯一名称,但是,由于方法重载,方法可能与其他方法具有相同的名称。
重载方法
Java编程语言支持重载方法,Java可以区分具有不同方法签名的方法,这意味着如果类中的方法具有不同的参数列表,则它们可以具有相同的名称(有一些条件,将在标题为“接口和继承”的课程中讨论)。
假设你有一个类可以使用书法来绘制各种类型的数据(字符串、整数等),并且包含绘制每种数据类型的方法,为每个方法使用新名称很麻烦 — 例如,drawString
、drawInteger
、drawFloat
等等。在Java编程语言中,你可以对所有绘图方法使用相同的名称,但是为每个方法传递不同的参数列表,因此,数据绘图类可能会声明四个名为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;
}
要创建一个名为myBike
的新Bicycle
对象,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
对象。
两个构造函数都可以在Bicycle
中声明,因为它们具有不同的参数列表,与方法一样,Java平台根据列表中的参数数量及其类型来区分构造函数。你不能为相同类编写两个具有相同参数数量和类型的构造函数,因为平台无法区分它们,这样做会导致编译时错误。
你不必为你的类提供任何构造函数,但在执行此操作时必须小心,编译器自动为没有构造函数的任何类提供无参数的默认构造函数,此默认构造函数将调用超类的无参数构造函数,在这种情况下,如果超类没有无参数构造函数,编译器将会报错,因此你必须验证它是否有,如果你的类没有显式的超类,那么它有一个隐式的超类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
方法中看到的双精度数、浮点数和整数,以及引用数据类型,如对象和数组。
这是一个接受数组作为参数的方法示例,在此示例中,该方法创建一个新的Polygon
对象,并从Point
对象数组中初始化它(假设Point
是一个表示x,y坐标的类):
public Polygon polygonFrom(Point[] corners) {
// method body goes here
}
注意:如果要将方法传递给方法,请使用
lambda
表达式或方法引用。
任意数量的参数
你可以使用名为可变参数的构造将任意数量的值传递给方法,当你不知道将多少特定类型的参数传递给该方法时,你可以使用可变参数,这是手动创建数组的快捷方式(前一种方法可以使用可变参数而不是数组)。
要使用可变参数,你通过省略号跟随最后一个参数的类型(三个点,...
),然后是空格和参数名称,然后可以使用任何数量的参数调用该方法,包括无参数。
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
被视为数组,可以使用数组或参数序列调用该方法,在任何一种情况下,方法体中的代码都会将参数视为数组。
你最常见的是使用打印方法的可变参数,例如,这个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
关键字”一节中讨论。
传递原始数据类型参数
原始参数(如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)
在方法内部,circle
最初引用的是myCircle
,该方法将circle
引用的对象(即myCircle
)的x和y坐标分别改变23和56,方法返回时,这些更改将保持不变。然后circle
被赋予新的Circle
对象的引用,其中x = y = 0
,但是,这种重新分配没有永久性,因为引用是按值传递的,不能更改,在该方法中,circle
指向的对象已更改,但是,当方法返回时,myCircle
仍然引用与调用方法之前相同的Circle
对象。