构造器,可以定义对象的初始状态,不仅如此,由于对象构造非常重要,所以Java提供了多种编写构造器的机制。
重载
有些类有多个构造器。例如,可以构造一个空的StringBuilder对象:
StringBuilder message = new StringBuilder();
或者,可以指定一个初始化字符串:
StringBuilder todoList = new StringBuilder("To do:\n");
这种特征就叫做重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。
- Java允许重载任何方法,而不只是构造器方法。
- 不能有两个名字相同、参数类型也相同,返回值类型不同的方法。
默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。如果不明确地对域进行初始化,就会影响程序代码的可读性。
如Employee类,假定没有在构造器中对某些域进行初始化,就会默认地将salary域初始化为0,将name和hireDay域初始化为null。
如果此时调用getName方法或getHireDay方法,则会得到一个null引用,这不是我们所希望的结果,也会产生很多的Bug:
LocalDate h = harry.getHireDay();
int year = h.getYear(); // 这里会出错,因为变量h是null的引用,null没有getYear()方法。
无参数的构造器
很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值。如:Employee类的无参数构造函数:
public Employee() {
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数会被视为不合法。如,在Employee类中提供了带参数的构造器:
Employee(String name, double salary, int y, int d)
对于这个类,由于提供了一个带参的构造器,再调用默认构造器的话属于不合法,也就是,调用
e = new Employee();
将会产生错误。
- 仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。
显式域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎么调用构造器,每个实例域都可以被设置为一个有意义的处置,这是一种很好的设计习惯。
可以在类定义中,直接将一个值赋给任何域。如:
class Employee{
private String name = "":
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值。如,可以通过调用方法对域进行初始化:
class Employee {
private static int nextId;
private int id = assignId();
private static int assignId() {
int r = nextId;
nextId++;
return r;
}
}
参数名
在编写很小的构造器时,常常在参数命名上出现各种问题。
通常,参数用单个字符命名(图方便):
public Employee(String n, double s) {
name = n;
salary = s;
}
这样做没有错误,但是很不友好(对人不友好),也相当于一个缺陷:只有在阅读代码才能了解参数n和参数s的含义。
有些程序员在每个参数前面加上一个无意义的前缀“a”:
public Employee(String aName, String aSalary) {
name = aName;
salary = aSalary;
}
这样就比单个字符的好了很多,很清晰,在看到参数名的时候就知道参数的含义。
还有一种常用的技巧(非常常用),它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。如:如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是,可以采用this.salary的形式访问实例域。如:
public Employee(String name, String salary) {
this.name = name;
this.salary = salary;
}
调用另一个构造器
关键字this应用方法的隐式参数。这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。如:
public Employee(double s) {
// 调用另外一个构造器Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
当调用Employee(double)构造器的时候,将先调用Employee(String, double)构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码只编写一次即可。
初始化块
我们已经接触了两种初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
实际上,Java还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。如:
class Employee {
private static int nextId;
private int id;
private String name;
private double salary;
// 初始化块
{
id = nextId;
nextId++;
}
// 构造器
public Employee(String name, String salary) {
this.name = name;
this.salary = salary;
}
...
}
在这个例子中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必须的,也不常见(非常的不常见)。通常会直接将初始化代码放在构造器中。
由于初始化数据有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(0、false、null)。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器主体。
可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。如:
// 初始化值
private static int nextId = 1;
如果对类的静态域进行初始化的代码比价复杂,那么可以使用静态的初始化块,将代码放在一个块中,并标记关键字static,如:
// 静态初始化块,将雇员的ID初始赋予一个小于10000的随机整数
static {
Random generator = new Randow();
nextId = generator.nextInt(10000);
}
在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置为其他值,否则默认的初始值是0、false、null。所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
捐赠
若你感觉读到这篇文章对你有启发,能引起你的思考。请不要吝啬你的钱包,你的任何打赏或者捐赠都是对我莫大的鼓励。