对象构造
我们之前已经编写过简单的构造器,可以定义对象的初始状态。但是,由于对象构造非常重要,使用Java提供了多种编写构造器的机制。
重载
有些类有多个构造器,例如,我们可以构造一个空的StringBuffer对象:
StringBuffer stringBuffer = new StringBuffer();
或者,可以指定一个初始字符串:
StringBuffer stringBuffer1 = new StringBuffer("aaa");
这种特征叫重载。如果多个方法有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行那个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会报错,因为根本不存在匹配。
Java允许重载任何方法,而不是构造器方法。因此,要完整的描述一个方法,需要指出方法名以及参数类型。这叫方法的签名。例如:String类有4个称为indexOf的公有方法。他们的签名是:
indexOf(int);
indexOf(int,int);
indexOf(String);
indexOf(String,int);
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同但是返回不同类型值的方法。
默认域初始化
如果在构造器中没有显式的给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。如果不明确地对域进行初始化,就会影响程序代码的可读性。
我们可以看一下Employee类。假定没有在构造器中对某些域进行初始化,就会默认地将salary域初始化为0,将name和hireDay初始化为null。
这并不是一个良好的编程习惯。因为,此时调用getName方法和getHireDay方法,会得到一个null引用。
无参数的构造器
很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值。例如,以下是Employee类的无参数构造函数:
public Employee{
name="";
salary=0;
hireDay=LocalDate.now();
}
如果一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域数值为默认值。
如果类中至少提供了一个构造器,但是没有提供无参构造器,那么在构造对象时如果没有提供参数就会被视为不合法。
显示域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值。
可以在类定义中,直接将一个值赋给任何域。
在执行构造器之前,先执行赋值初值,当一个类的所有构造器都希望把相同的值赋给某个特定的实例域时,这种方式特别有效。
初始值不一定是常量值。在下面的这个例子中,可以调用方法对域进行初始化。
class Employee{
private static int nextid;
private int id=assignid();
private static int assignid(){
int r=nextid;
nextid++;
return r;
}
}
调用另一个构造器
关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句如this(…),这个构造器将调用同一个类的另一个构造器。如下:
public Employee(double s){
this("aaa"+nextid,s);
nextid++;
}
当调用new Employee(6000)时,Employee(double)构造器将调用Employee(String,double)构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一部分即可。
初始化块
上面已经说过两种初始化数据域的方法。
- 在构造器中设置
- 在声明中赋值
事实上,Java还有第三种机制,称为初始化块。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:
class Employee{
private static int nextid;
private int id;
private String name;
private double salary;
{
id=nextid;
nextid++;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public Employee() {
this.name = "";
this.salary = 0;
}
}
在上述事例中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
由于初始化数据域有很多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,那么执行第二个构造器主体。
- 执行这个构造器的主体。
所以,我们应该精心地组织好初始化代码,这样有利于我们的理解。
我们可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。例如:
private static int nextid=1;
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
将代码放到一个块中,并标记关键字static。
static{
Random generator=new Random();
nextid=generator;
}
在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值为0,false和null。所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
下面的程序展现来看很多上述的特性:
- 重载构造器
- 有this(…)调用另一个构造器
- 无参数构造器
- 对象初始化块
- 静态初始化块
- 实例域初始化
public class test2 {
public static void main(String[] args) {
Employee[] e = new Employee[3];
e[0] = new Employee("aaa", 100);
e[1] = new Employee(400);
e[2] = new Employee();
for (Employee employee : e) {
System.out.println("name="+employee.getName()+" "+"salary="+employee.getSalary()+" "+"id="+employee.getId());
}
}
}
class Employee {
private static int nextid;
private int id;
private String name = "";
private double salary;
static {
Random random = new Random();
nextid=random.nextInt(10000);
}
{
id=nextid;
nextid++;
}
public Employee(String s,double a){
name=s;
salary=a;
}
public Employee(double s){
this("Employee #"+nextid,s);
}
public Employee() {
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}
类的设计技巧
1. 一定要保证数据私有
这是最重要的;绝对不要破坏封装性。有时候,需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。有的时候,数据的表示形式很可能会改变,但他们的使用方式不会经常发生变化。当保持数据私有的时候,它们的表示形式的变化不会对类的使用者产生影响,即使出现bug也易于检测。
2. 一定要对数据初始化
Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显示地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。
3. 不要在类中使用过多的基本类型
就是说,用其他类代替多个相关的基本类型的使用。这样易于类的理解和修改。例如,用一个称为a的新类替换一个b类中以下的实例域:
private String street;
private String city;
private String state;
private int zip;
这样很容易处理地址的变化。
4. 不是所有的域都需要独立的域访问器和域更改器
一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域。
5. 将职责过多的类进行分解
如果明显地可以将一个复杂的类分解为两个更为简单的类,就应该将其分解。
6. 类名与方法名要能够体现他们的职责
与变量应该有一个能够反映其含义的名字一样,类也应该如此。