4.4 静态域与静态方法
4.4.1 静态域
如果将域定义为static
,则为静态域。每一个对象对于所有的实例域都有自己的一份拷贝。而每一份拷贝中的静态域是共享的。
class Employee {
private static int nextId = 1;
private int id;
...
}
现在,每一个雇员对象都有一个自己的id
域,但这个类中的所有实例将共享一个nextId
域。即使没有一个雇员对象,静态域nextId也存在。它属于类,而不属于任何独立的对象。
静态域也被称为类域。术语static
只是沿用了C++的叫法,并无实际意义。
下面实现一个与静态域有关的方法:
public void setId() {
id = nextId;
nextId++;
}
假定为harry
设定雇员标识码:
harry.setId();
harry
的id
域被设置为静态域nextId
当前的值,并且静态域nextId
的值加1:
harry.id = Employee.nextId;
Employee.nextId++;
4.4.2 静态常量
静态变量使用得比较少,但静态常量却使用得比较多。例如,在Math
类中定义了一个静态常量:
public class Math {
...
public static final double PI = 3.1415926;
...
}
在程序中,可以使用Math.PI
的形式获得这个常量。
static
保证PI
是属于类,不属于任何独立的对象,从而不需要创建Math的对象就可以访问PI
。final
保证PI
初始化之后不会被修改。
前面曾经提过,由于每个类对象都可以对公开域进行修改,所以,最好不要将域设计为public
。但是,公有常量(final
域)是没问题的,因为被声明为final
,所以在声明后不允许再进行赋值修改。
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法,例如Math
类的pow
方法就是一个静态方法。表达式
Math.pow(x, a)
Employee
类的静态方法不能访问id
实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。例如:
public static int getNextId() {
return nextId; // return static field
}
可以通过类名调用这个方法:
int n = Employee.getNextId();
在下面的两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需要的参数都是通过显式参数提供(例如:
Math.pow
) - 一个方法只需要访问类的静态域(例如:
Employee.getNextId
)
4.4.4 工厂方法
静态方法还有另一种常见的用途。类似LocalDate
和NumberFormat
的类使用静态工厂方法(factory method)来构造对象。已经见过工厂方法LocalDate.now
和LocalDate.of
。比如,NumberFormat
类使用工厂方法生成不同风格的格式化对象:
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints S0.10
System.out.println(percentFomatter.format(x)); // prints 10%
为什么NumberFormat
类不利用构造器完成这些操作呢?这主要有两个原因:
-
无法命名构造器。构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不用的名字。
-
当使用构造器时,无法改变所构造的对象类型。而
Factory
方法将返回一个DecimalFormat
类对象,这是NumberFormat
的子类。
4.4.5 main方法
public class Application {
public static void main(String[] args) {
// constructs objects here
}
}
main
方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main
方法将执行并创建程序所需要的对象。
提示:每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。
4.5 方法参数
Java程序设计语言总是按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
例如:
public static void tripleValue(double x) {
x = 3 * x;
}
然后调用这个方法:
double percent = 10;
tripleValue(percent);
调用这个方法之后,percent的值还是10。
然而,方法参数共有两种类型:
- 基本数据类型(数字、布尔值)
- 对象引用
一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了,可以很容易地利用下面这个方法实现将一个雇员的薪金提高两倍的操作:
public static void tripleSalary(Employee x) {
x.raiseSalary(200);
}
当调用
harry = new Employee(...);
tripleSalary(harry);
方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用了一个对象。
如果Java对对象采用的是按引用调用,那么这个方法就应该能够实现交换数据的效果:
Employee a = new Employee("Alice", ...);
Employee b = new Employee("Bob", ...);
swap(a, b);
但是,方法并没有改变存储在变量a和b的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝。
// x refers to Alice, y to Bob
Employee temp = x;
x = y;
y = temp;
// now x refers to Bob, y to Alice
最终,在方法结束时参数变量x和y被丢弃了。原来的变量a和b仍然引用这个方法调用之前所引用的对象。
这个过程说明:Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
下面总结一下Java中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
4.6 对象构造
4.6.1 重载
有些类有多个构造器。例如,可以如下构造一个空的StringBuilder对象:
StringBuilder messages = new StringBuilder();
或者,可以指定一个初始字符串:
StringBuilder todoList = new StringBuilder("To do:\n");
这种特征叫做重载(overloading)。如果多个方法有相同的名字、不同的参数,便产生了重载。
重载解析(overloading resolution):编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数, 就会产生编译时错误,因为根本不存在匹配, 或者没有一个比其他的更好。
Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。例如,
String
类有 4 个称为indexOf
的公有方法。它们的签名是
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
返回类型不是方法签名的一部分。也就是说, 不能有两个名字相同、 参数类型也相同却返回不同类型值的方法。
4.6.2 默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值: 数值为 0、布尔值为 false、 对象引用为 null。但是,这并不是一种良好的编程习惯,如果不明确地对域进行初始化,就会影响代码的可读性。
这是域与局部变量的主要不同点。 必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域, 将会被自动初始化为默认值( 0、false 或 null )。 (原因)
例如, 仔细看一下 Employee
类。 假定没有在构造器中对某些域进行初始化, 就会默认地将 salary
域初始化为 0
, 将 name
和 hireDay
域初始化为 null
。如果此时调用 getName
方法或 getHireDay
方法,则会得到一个 null
引用,这应该不是我们所希望的结果:
LocalDate h = harry.getHireDay();
int year = h.getYear(); // throws exception if h is null!!
4.6.3 无参数的构造器
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是, 实例域中的数值型数据设置为0
、 布尔型数据设置为false
、 所有对象变量将设置为null
。
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器, 则在构造对象时如果没有提供参数就会被视为不合法。例如,Employee
类提供了一个简单的构造器:
Employee(String name, double salary, int year, int month, int day)
对于这个类,构造默认的雇员是不合法的,也就是调用:
e = new Employee();
就会产生错误。
警告⚠️:请记住,仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器如果在编写类的时候, 给出了一个构造器, 哪怕是很简单的, 要想让这个类的用户能够采用下列方式构造实例:
new ClassName()
就必须提供一个默认的构造器 ( 即不带参数的构造器)。 当然, 如果希望所有域被赋予默认值, 可以采用下列格式:
public ClassName() { }
4.6.4 显式域初始化
可以在类定义中,直接将一个值赋给任何域。例如:
class Employee {
private String name = "";
...
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值。在下面的例子中, 可以调用方法对域进行初始化。在Employee
类中,其中每个雇员有一个id
域。可以使用下列方式进行初始化:
class Employee {
private static int nextId = 1;
private int id = assignId();
...
private static int assignId() {
int r = nextId;
nextId++;
return r;
}
...
}
4.6.5 参数名
参数变量可以用同样的名字将实例域屏蔽起来。 例如,如果将参数命名为salary
, salary
将引用这个参数, 而不是实例域。但是,可以采用 this
,salary
的形式访问实例域。this
指示隐式参数, 也就是所构造的对象。下面是一个示例:
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
4.6.6 调用另一个构造器
关键字 this
引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如 this(...)
, 这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:
public Employee(double s) {
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
当调用new Employee(60000)
时,Employee(double)
构造器将调用Employee(String ,double)
构造器。
采用这种方式使用this
关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
4.6.7 初始化块
前面已经讲过两种初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
实际上,Java还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:
class Employee {
private static int nextId;
private int id;
private String name;
private double salary;
// object initialization block 只要构造类的对象,该块中的代码就会被执行
{
id = nextId;
nextId++;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public Employee() {
this.name = "";
this.salary = 0;
}
...
}
在这个示例中,无论使用哪个构造器构造对象,id
域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(
0
、false
或null
) 。 - 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
当然,应该精心地组织好初始化代码,这样有利于其他程序员的理解。 例如, 如果让类的构造器行为依赖于数据域声明的顺序, 那就会显得很奇怪并且容易引起错误。
可以通过提供一个初始化值, 或者使用一个静态的初始化块来对静态域进行初始化。前面已经介绍过第一种机制:
private static int nextId = 1;
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字 static
。下面是一个示例。其功能是将雇员 ID 的起始值赋予一个小于 10 000 的随机整数。
// 静态初始化块
static {
Random generator = new Random();
nextId = generator.nextInt(10000);
}
在类第一次加载的时候, 将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值是 0
、 false
或 null
。 所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
下面的示例代码使用了上述的很多特性:
class Employee {
private static int nextId;
private String name = ""; // 可以在声明时对实例域进行赋值
private double salary;
private int id;
// 静态初始化块(static initialization block)
static {
Random generator = new Random();
nextId = generator.nextInt(10000);
}
// 初始化块(object initialization block)
{
id = nextId;
nextId++;
}
public Employee(String name, double salary) {
this.name = name; // 使用与实例域同名的变量来屏蔽
this.salary = salary;
}
// 同名方法,不同的参数类型和个数来重载
public Employee(double salary) {
this("Employee#" + nextId, salary); // 使用this(...)来调用了另一个构造器
}
// 无参的默认构造器,将实例域设为默认值
public Employee() {
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public int getId() {
return id;
}
}
4.6.8 对象析构和finalize方法
有些面向对象的程序设计语言,特别是 C++, 有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中, 最常见的操作是回收分配给对象的存储空间。由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。
当然,某些对象使用了内存之外的其他资源, 例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时, 将其回收和再利用将显得十分重要。
可以为任何一个类添加 finalize
方法。finalize
方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用 finalize
方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。