使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。
day021 对象构造(重载、默认域初始化、无参数的构造器、显示域初始化、参数名、调用另一个构造器、初始化块)
我们前面已经学习了编写简单的构造器,可以定义对象的初始状态。但是,由于对象构造非常重要,所以Java提供了多种编写构造器的机制。下面详细地学习这些机制。
1.重载
有些类有多个构造器。例如,可以如下构造一个空的StringBuilder对象:
StringBuilder messages = new StringBuilder();
或者,可以指定一个初始字符串:
StringBuilder todoList = new StringBuilderC"To do:\n");
这种特征叫做重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。(这个过程被称为重载解析(overloadingresolution)。)
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。例如,String类有4个称为indexOf的公有方法。它们的签名是
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
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
3.无参数的构造器
很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值。例如,以下是Employee类的无参数构造函数:
public Employee()
{
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。例如:
Employee(String name, double salary, int y, int ra, int d)
对于这个类,构造默认的雇员属于不合法。也就是,调用
e = new Employee();
将会产生错误。
需要注意的是:仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器如果在编写类的时候,给出了一个构造器,哪怕是很简单的,要想让这个类的用户能够采用下列方式构造实例:
new ClassName()
就必须提供一个默认的构造器(即不带参数的构造器)。当然,如果希望所有域被赋予默认值,可以采用下列格式:
new ClassName()
{
}
4.显示域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。
可以在类定义中,直接将一个值赋给任何域。例如:
class Employee
{
private String name = "";
...
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值。在下面的例子中,可以调用方法对域进行初始化。仔细看一下Employee类,其中每个雇员有一个id域。可以使用下列方式进行初始化:
class Employee
{
private static int nextId;
private int id = assignId();
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
...
}
5.参数名
在编写很小的构造器时(这是十分常见的),常常在参数命名上出现错误。
通常,参数用单个字符命名:
public Employee(String n, double s)
{
name = n;
salary = s;
}
但这样做有一个缺陷:只有阅读代码才能够了解参数n和参数s的含义。有些程序员在每个参数前面加上一个前缀“a”:
public Employee(String aName, double aSalary)
{
name = aName;
salary = aSalary;
}
这样很清晰::每一个读者一眼就能够看懂参数的含义。
还一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是,可以采用this.salary的形式访问实例域。回想一下,this指示隐式参数,也就是所构造的对象。下面是一个示例:
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
6.调用另一个构造器
关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:
public Employee(double s)
{
//calls the Employee(String ,double)constructor
this("Employee #" + nextId,s);
nextId++;
}
当调用newEmployee(60000)时,Employee(double)构造器将调用Employee(String,double)构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
7.初始化块
前面已经讲过两种初始化数据域的方法:
1.在构造器中设置值
2.在声明中赋值
实际上,Java还有第三种机制,称为初始化块(initializationblock)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如,
class Employee
{
private static int nextId;
private String name ;
private double salary;
private int id;
//object initialization block
{
id = nextId;
nextId++;
}
public Employee(String n ,double s)
{
name = n;
salary = s;
}
public Employee()
{
name = "";
salary = 0;
}
...
}
在这个示例中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。即使在类的后面定义,仍然可以在初始化块中设置域。但是,为了避免循环定义,不要读取在后面初始化的域。
由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
1.所有数据域被初始化为默认值(0、false或null)。
2.按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
3.如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
4.执行这个构造器的主体。
当然,应该精心地组织好初始化代码,这样有利于其他程序员的理解。例如,如果让类的构造器行为依赖于数据域声明的顺序,那就会显得很奇怪并且容易引起错误。
可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。前面已经介绍过第一种机制:
private static int nextId = 1;
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字static。下面是一个示例。其功能是将雇员ID的起始值赋予一个小于10000的随机整数。
//static initialization block
static
{
Random generator = new Random();
//set nextId to a random number between 0 and 9999
nextId = generator.nextInt(10000);
}
在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值是0、false或null。所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
下面的程序展示了上面讨论的很多特性:
•重载构造器
•用this(...)调用另一个构造器
•无参数构造器
•对象初始化块
•静态初始化块
•实例域初始化
//对象构造
//zzhao
import java.util.*;
public class ConstructorTest
{
public static void main(String[] args)
{
//fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Tom",40000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
//print out information about all Employee objects
for (Employee e : staff)
{
System.out.println("name="+e.getName()+",id="+e.getId()+",salary="+e.getSalary());
}
}
}
class Employee
{
private static int nextId;
private String name = "";//instance field initialization
private double salary;
private int id;
//static initialization block
static
{
Random generator = new Random();
//set nextId to a random number between 0 and 9999
nextId = generator.nextInt(10000);
}
//object initialization block
{
id = nextId;
nextId++;
}
//three overloaded constructors
public Employee(String n ,double s)
{
name = n;
salary = s;
}
public Employee(double s)
{
//calls the Employee(String ,double)constructor
this("Employee #" + nextId,s);
}
//the default constructor
public Employee()
{
//name initialized to "" --see above
//salary not explicitly set --initialized to 0
//id initialized in initialization block
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
}
运行的结果:
8.对象析构与finalize方法
有些面向对象的程序设计语言,特别是C++,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作是回收分配给对象的存储空间。由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。
可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。对象用完时,可以应用一个close方法来完成相应的清理操作。