4.6 对象构造
构造器可以定义对象的初始状态。但是,由于对象构造非常重要,所以Java提供了多种编写构造器的机制。下面将详细介绍这些机制。
重载
有些类有多个构造器。例如,可以如下构造一个空的StringBuilder对象:
var messages = new StringBuilder();
或者,可以指定一个初始字符串:
var todolist = new StringBuilder("To do:\n");
这种功能叫做重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好(这个查找匹配的过程被称为重载解析(overloading resolution))。
程序示例
public class HuangZiHanTest
{
public static void main(String[] huangzihan_args)
{
var huangzihan_messages = new StringBuilder("黄子涵");
var huangzihan_todoList = new StringBuilder("黄子涵是帅哥!!!\n");
System.out.println(huangzihan_messages);
System.out.println(huangzihan_todoList);
}
}
运行结果
黄子涵
黄子涵是帅哥!!!
注释
方法的签名(signature)
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指定方法名以及参数类型。这叫作方法的签名(signature)。例如,String类有4个名为indexOf的公共方法。它们的签名是
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却有不同返回类型的方法。
程序示例
public class HuangZiHanTest
{
public static void main(String[] huangzihan_args)
{
String huangzihan = new String("黄子涵");
System.out.println("************************************************");
System.out.println("测试huangzihan.indexOf(int ch)");
System.out.println(huangzihan.indexOf('黄'));
System.out.println(huangzihan.indexOf('子'));
System.out.println(huangzihan.indexOf('涵'));
System.out.println(huangzihan.indexOf('!')); //原字符串不包含“!”
System.out.println(huangzihan.indexOf('\u9ec4')); //“黄”字的Unicode编码是\u9ec4
System.out.println(huangzihan.indexOf('\u5b50')); //“子”字的Unicode编码是\u5b50
System.out.println(huangzihan.indexOf('\u6db5')); //“涵”字的Unicode编码是\u6db5
System.out.println(huangzihan.indexOf('\u0021')); //“!”字的Unicode编码是\u0021
System.out.println("************************************************");
System.out.println();
System.out.println("************************************************");
System.out.println("测试huangzihan.indexOf(int ch, int fromIndex)");
System.out.println("返回值为指定索引查找指定字符中第一个的索引:");
System.out.println(huangzihan.indexOf('涵', 2));
System.out.println(huangzihan.indexOf('涵', 1));
System.out.println(huangzihan.indexOf('涵', 0));
System.out.println(huangzihan.indexOf('子', 1));
System.out.println(huangzihan.indexOf('子', 0));
System.out.println(huangzihan.indexOf('黄', 0));
System.out.println();
System.out.println("返回值为-1:");
System.out.println(huangzihan.indexOf('黄', 1));
System.out.println(huangzihan.indexOf('黄', 2));
System.out.println(huangzihan.indexOf('子', 2));
System.out.println(huangzihan.indexOf('!', 0));
System.out.println(huangzihan.indexOf('!', 1));
System.out.println(huangzihan.indexOf('!', 2));
System.out.println("************************************************");
System.out.println();
System.out.println("************************************************");
System.out.println("测试huangzihan.indexOf(String str)");
System.out.println("返回值为第一个字符的索引:");
System.out.println(huangzihan.indexOf("黄")); //返回“黄”的索引
System.out.println(huangzihan.indexOf("子")); //返回“子”的索引
System.out.println(huangzihan.indexOf("涵")); //返回“涵”的索引
System.out.println(huangzihan.indexOf("黄子")); //返回“黄子”中“黄”的索引
System.out.println(huangzihan.indexOf("黄子涵")); //返回“黄子涵”中“黄”的索引
System.out.println(huangzihan.indexOf("子涵")); //返回“子涵”中“子”的索引
System.out.println("返回值为-1:");
System.out.println(huangzihan.indexOf("!")); //原字符串不包含“!”
System.out.println(huangzihan.indexOf("黄子涵!")); //原字符串不包含“!”
System.out.println(huangzihan.indexOf("黄涵")); //比原字符串少了“子”
System.out.println("************************************************");
System.out.println();
System.out.println("************************************************");
System.out.println("测试huangzihan.indexOf(String str, int fromIndex)");
System.out.println("返回值为指定索引查找指定字符串中第一个的索引:");
System.out.println(huangzihan.indexOf("黄", 0));
System.out.println(huangzihan.indexOf("黄子", 0));
System.out.println(huangzihan.indexOf("黄子涵", 0));
System.out.println(huangzihan.indexOf("子涵", 0));
System.out.println(huangzihan.indexOf("涵", 0));
System.out.println(huangzihan.indexOf("子涵", 1));
System.out.println(huangzihan.indexOf("涵", 1));
System.out.println(huangzihan.indexOf("涵", 2));
System.out.println("返回值为-1");
System.out.println(huangzihan.indexOf("黄", 1));
System.out.println(huangzihan.indexOf("黄子", 1));
System.out.println(huangzihan.indexOf("黄子涵", 1));
System.out.println(huangzihan.indexOf("黄", 2));
System.out.println(huangzihan.indexOf("黄子", 2));
System.out.println(huangzihan.indexOf("黄子涵", 2));
System.out.println(huangzihan.indexOf("子涵", 2));
System.out.println(huangzihan.indexOf("黄子涵!", 0));
System.out.println(huangzihan.indexOf("黄子涵!", 1));
System.out.println(huangzihan.indexOf("黄子涵!", 2));
System.out.println(huangzihan.indexOf("黄涵", 0));
System.out.println(huangzihan.indexOf("黄涵!", 1));
System.out.println(huangzihan.indexOf("黄涵!", 2));
System.out.println("************************************************");
}
}
运行结果
************************************************
测试huangzihan.indexOf(int ch)
0
1
2
-1
0
1
2
-1
************************************************
************************************************
测试huangzihan.indexOf(int ch, int fromIndex)
返回值为指定索引查找指定字符中第一个的索引:
2
2
2
1
1
0
返回值为-1:
-1
-1
-1
-1
-1
-1
************************************************
************************************************
测试huangzihan.indexOf(String str)
返回值为第一个字符的索引:
0
1
2
0
0
1
返回值为-1:
-1
-1
-1
************************************************
************************************************
测试huangzihan.indexOf(String str, int fromIndex)
返回值为指定索引查找指定字符串中第一个的索引:
0
0
0
1
2
1
2
2
返回值为-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
-1
************************************************
默认字段初始化
如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为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
无参数的构造器
很多类都包含一个无参数的构造器,由无参数构造器创建对象时,对象的状态会设置为适当的默认值。例如,以下是Employee类的无参数构造器:
public Employee()
{
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果写一个类时没有编写构造器,就会为你提供一个无参数构造器。这个构造器将所有的实例字段设置为默认值。于是,实例字段中的数值型数据设置为0,布尔型数据设置为false,所有对象变量将设置为null。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。例如,下面程序中的Employee类提供了一个简单的构造器:
public Employee(String n, double s, int year, int month, int day)
对于这个类,构造默认的员工就是不合法的。也就是说,调用
e = new Employee();
将会产生错误。
程序示例
import java.time.LocalDate;
public class HuangZiHanTest
{
public static void main(String[] huangzihan_args)
{
Huangzihan_Employee huangzihan_e = new Huangzihan_Employee(null, 0, 0, 0, 0);
System.out.println(huangzihan_e.huangzihan_getName());
System.out.println(huangzihan_e.huangzihan_getSalary());
System.out.println(huangzihan_e.getHireDay());
}
}
class Huangzihan_Employee
{
// instance fields
private String huangzihan_name;
private double huangzihan_salary;
private LocalDate huangzihan_hireDay;
// constructor
public Huangzihan_Employee(String huangzihan_n, double huangzihan_s, int huangzihan_year, int huangzihan_month, int huangzihan_day)
{
huangzihan_name = "";
huangzihan_salary = 0;
huangzihan_hireDay = LocalDate.now();
}
// a method
public String huangzihan_getName()
{
return huangzihan_name;
}
public double huangzihan_getSalary()
{
return huangzihan_salary;
}
public LocalDate getHireDay()
{
return huangzihan_hireDay;
}
}
运行结果
0.0
2021-07-18
警告
请记住,仅当类没有任何其他构造器的时候,你才会得到一个默认的无参数构造器。编写类的时候,如果写了一个你自己的构造器,要想让这个类的用户能够通过以下调用构造一个实例:
new ClassName()
你就必须提供一个无参数的构造器。当然,如果希望所有字段被赋予默认值,只需要提供以下代码:
public ClassName()
显式字段初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。不管怎样调用构造器,每个实例字段都要设置为一个有意义的初值,确保这一点总是一个好主意。
可以在类定义中直接为任何字段赋值。例如:
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;
}
}
参数名
在编写很小的构造器时(这十分常见),常常在参数命名时感到困惑。
用单个字母作为参数名
我们通常喜欢用单个字母作为参数名:
public Employee(String n, double s)
{
name = n;
salary = s;
}
但这样做有一个缺点:只有阅读代码才能够了解参数n和参数s的含义。
参数名前面加前缀“a”
有些程序员在每个参数前面加上一个前缀“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;
}
调用另一个构造器
关键字this
关键字this指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
当调用
new Employe(60000)
时,Employee(double)
构造器将调用Employee(String, double)
构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码只需要编写一次即可。
初始化块
两种初始化数据字段的方法
前面已经讲过两种初始化数据字段的方法:
- 在构造器中设置值;
- 在声明中赋值。
实际上,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 n, double s)
{
name = n;
salary = s;
}
public Employee()
{
name = "";
salary = 0;
}
. . .
}
在这个示例中,无论使用哪个构造器构造对象,id字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。
注释
可以在初始化块中设置字段,即使这些字段在类后面才定义,这是合法的。但是,为了避免循环定义,不允许读取在后面初始化的字段。具体规则请参看Java语言规范的网址(http://docs.oracle.com/javase/specs)。这些规则太过复杂,让编译器的实现者都很头疼,所以较早的Java版本中这些规则的实现存在一些小错误。因此建议总是将初始化块放在字段定义之后。
调用构造器的具体处理步骤
由于初始化数据字段有多种途径,所以列出构造过程的所有路径可能让人很困惑。下面是调用构造器的具体处理步骤:
- 如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
- 否则,
- a) 所有数据字段初始化为其默认值(0、false或nul)。
- b)按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。
- 执行构造器主体代码。
当然,应该精心地组织好初始化代码,这样有利于其他程序员理解。例如,如果让类的构造器依赖于数据字段声明的顺序,那就会显得很奇怪并且容易引起错误。
可以通过提供一个初始值,或者使用一个静态的初始化块来初始化静态字段。前面已经介绍过第一种机制:
private static int nextId = 1;
如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字static。下面是一个示例。其功能是将员工ID的起始值赋予一个小于10 000的随机整数。
// static initialization block
static
{
var generator = new Random();
nextId = generator.nextInt(10000);
}
在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值是0、false或null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。
注释
让人惊讶的是,在JDK6之前,都可以用Java编写一个没有main方法的“Hello, World”程序。
public class Hello
{
static
{
System.out.println("Hello, World");
}
}
当用java Hello调用这个类时,就会加载这个类,静态初始化块将会打印“Hello, World”。在此之后才会显示一个消息指出main未定义。从Java 7以后,java程序首先会检查是否有一个main方法。
下面程序展示了本节讨论的很多特性:
- 重载构造器;
- 用this(...)调用另一个构造器;
- 无参数构造器;
- 对象初始化块;
- 静态初始化块;
- 实例字段初始化。
程序示例
import java.time.LocalDate;
import java.util.Random;
/*
* @功能:该程序演示了对象构造。
* @版本:1.02
* @时间:2021-07-19
* @作者:黄子涵
*
*/
public class HuangZiHanTest
{
public static void main(String[] huangzihan_args)
{
//用三个Employee对象填充人员数组
var huangzihan = new Huangzihan_Employee[3];
huangzihan[0] = new Huangzihan_Employee("huangzihan", 40000);
huangzihan[1] = new Huangzihan_Employee(60000);
huangzihan[2] = new Huangzihan_Employee();
//打印出所有Employee对象的信息
for(Huangzihan_Employee huangzihan_e : huangzihan)
{
System.out.println("名字=" + huangzihan_e.huangzihan_getName() + ",id=" +
huangzihan_e.huangzihan_getId() + ",工资=" + huangzihan_e.huangzihan_getSalary());
}
}
}
class Huangzihan_Employee
{
private static int huangzihan_nextId;
private int huangzihan_id;
private String huangzihan_name = ""; //实例字段初始化
private double huangzihan_salary;
//静态初始化块
static
{
var huangzihan_generator = new Random();
//将 nextId 设置为 0 到 9999 之间的随机数
huangzihan_nextId = huangzihan_generator.nextInt(10000);
}
//对象初始化块
{
huangzihan_id = huangzihan_nextId;
huangzihan_nextId++;
}
//三个重载构造函数
public Huangzihan_Employee(String huangzihan_n, double huangzihan_s)
{
huangzihan_name = huangzihan_n;
huangzihan_salary = huangzihan_s;
}
public Huangzihan_Employee(double huangzihan_s)
{
//调用Employee(String,double)构造函数
this("Huangzihan_Employee #" + huangzihan_nextId, huangzihan_s);
}
//默认构造函数
public Huangzihan_Employee()
{
//名称初始化为""。--见上文
//工资未明确设置--初始化为0
//id在初始化块中初始化
}
public String huangzihan_getName()
{
return huangzihan_name;
}
public double huangzihan_getSalary()
{
return huangzihan_salary;
}
public int huangzihan_getId()
{
return huangzihan_id;
}
}
运行结果
名字=huangzihan,id=4294,工资=40000.0
名字=Huangzihan_Employee #4295,id=4295,工资=60000.0
名字=,id=4296,工资=0.0
对象析构与finalize方法
有些面向对象的程序设计语言,特别是C++,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作是回收分配给对象的存储空间。由于Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不支持析构器。
当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用显得十分重要。
如果一个资源一旦使用完就需要立即关闭,那么应当提供一个close方法来完成必要的清理工作。可以在对象使用完时调用这个close方法。
如果可以等到虚拟机退出,那么可以用方法Runtime.addShutdownHook增加一个“关闭钩”(shutdown hook)。在Java 9中,可以使用Cleaner类注册一个动作,当对象不再可达时(除了清洁器还能访问,其他对象都无法访问这个对象),就会完成这个动作。在实际中这些情况很少见。可以参见API文档来了解这两种方法的详细内容。
警告
不要使用finalize方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃。