Beginng_Java7(译):发现类和对象(第二章3.4节)(未完待续)

初始化类和对象

在使用类和对象之前,需要对它们进行适当的初始化。您已经了解到类加载后类字段被初始化为默认的零值,并且可以通过类字段初始化器在其声明中为它们赋值来初始化它们。例如,static int counter = 1;。类似地,当通过new分配对象的内存时,实例字段被初始化为默认值,并且随后可以通过实例字段初始化器在其声明中为它们赋值来初始化实例字段;例如,int numDoors = 4;。

已经讨论过的初始化的另一个方面是构造函数,它通常通过为各种实例字段赋值来初始化对象,但也能够执行任意代码,例如打开文件并读取文件内容的代码。

Java提供了两个额外的初始化功能:类初始化器和实例初始化器。在向您介绍这些功能之后,本节将讨论所有Java初始化程序执行其工作的顺序。

类初始化器

构造函数执行对象的初始化任务。 从类初始化角度来看,它们是类初始化器。

类初始值设定项是引入类主体的静态前缀块。 它用于通过一系列语句初始化加载的类。 例如,我曾经使用类初始化程序来加载自定义数据库驱动程序类。 清单2-16显示了加载详细信息。

清单2-16。 通过类初始化程序加载数据库驱动程序

class JDBCFilterDriver implements Driver
{
static private Driver d;
static
{
// Attempt to load JDBC-ODBC Bridge Driver and register that
// driver.
try
{
Class c = Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
d = (Driver) c.newInstance();
DriverManager.registerDriver(new JDBCFilterDriver());
}
catch (Exception e)
{
System.out.println(e);
}
}

//...
}

清单2-16的JDBCFilterDriver类使用其类初始化程序来加载和实例化描述Java的JDBC-ODBC Bridge Driver的类,并使用Java的数据库驱动程序注册JDBCFilterDriver实例。 虽然这个列表的面向JDBC的代码现在对你来说可能毫无意义,但是这个列表说明了类初始化器的用处。 (我在第9章讨论JDBC)

类可以声明类初始值设定项和类字段初始值设定项的组合,如清单2- 17所示。

清单2-17。 将类初始值设定项与类字段初始值设定项混合

class C
{
static
{
System.out.println("class initializer 1");
}
static int counter = 1;
static
{
System.out.println("class initializer 2");
System.out.println("counter = "+counter);
}
}

清单2-17声明了一个名为C的类,它指定了两个类初始值设定项和一个类字段初始值设定项。 当Java编译器将一个声明至少一个类初始化器或类字段初始化器的类编译成类文件时,它会创建一个特殊的void ()类方法,该方法按顺序存储所有类初始化器和类字段初始化器的字节码等价物 它们发生(从上到下)。


■注意不是有效的Java方法名称,但是从运行时角度来看是有效的名称。 选择尖括号作为名称的一部分,以防止名称与您可能在类中声明的任何clinit()方法冲突。

对于C类,()首先包含System.out.println(“class initializer 1”);的字节码等价物,然后它将包含与static int counter = 1;相当的字节码,它最终将包含 System.out.println的字节码等价物(“类初始化器2”); System.out.println(“counter =”+ counter);. 当C类加载到内存中时,<clinit()立即执行并生成以下输出:

class initializer 1
class initializer 2
counter = 1

实例初始化器

并非所有类都可以拥有构造函数,因为当我提供匿名类时,您将在第3章中发现。 对于这些类,Java提供实例初始化程序来处理实例初始化任务。

实例初始值设定项是引入类主体的块,而不是作为方法或构造函数的主体引入。 实例初始化程序用于通过一系列语句初始化对象,如清单2-18所示。

清单2-18。 通过实例初始化程序初始化一对数组

class Graphics
{
double[] sines;
double[] cosines;
{
sines = new double[360];
cosines = new double[sines.length];
for (int i = 0; i < sines.length; i++)
{
sines[i] = Math.sin(Math.toRadians(i));
cosines[i] = Math.cos(Math.toRadians(i));
}
}
}

清单2-18的Graphics类使用实例初始化器来创建对象的正弦和余弦数组,并将这些数组的元素初始化为0到359度范围内的正弦和余弦。 这样做是因为读取数组元素比在其他地方重复调用Math.sin()和Math.cos()更快; 表现很重要。 (第4章介绍了Math.sin()和Math.cos()。)类可以声明实例初始值设定项和实例字段初始值设定项的组合,如清单2-19所示。

清单2-19。 将实例初始值设定项与实例字段初始值设定项混合

class C
{
{
System.out.println("instance initializer 1");
}
int counter = 1;
{
System.out.println("instance initializer 2");
System.out.println("counter = "+counter);
}
}

清单2-19声明了一个名为C的类,它指定了两个实例初始值设定项和一个实例字段初始值设定项。 当Java编译器将类编译为类文件时,它会创建一个特殊的void ()方法,表示没有显式声明构造函数时的默认无参数构造函数; 否则,它为每个遇到的构造函数创建一个()方法。 此外,它在每个构造函数中按照它们出现的顺序(从上到下)存储所有实例初始值设定项和实例字段初始值设定项的等效字节码。


■注意不是有效的Java方法名称,但是从运行时角度来看是有效的名称。 选择尖括号作为名称的一部分,以防止名称与您可能在类中声明的任何init()方法冲突。


对于类C,()首先包含与System.out.println(“instance initializer 1”)相同的字节码;然后它将包含与int counter = 1;相当的字节码,它最终将包含 System.out.println的字节码等价物(“实例初始化器2”); System.out.println(“counter =”+ counter);. 当新的C()执行时,()立即执行并生成以下输出:


instance initializer 1
instance initializer 2
counter = 1

■注意您很少需要使用实例初始化程序,这在业界不常用


初始化清单

类的主体可以包含类字段初始值设定项,类初始值设定项,实例字段初始值设定项,实例初始值设定项和构造函数的混合。 (您应该更喜欢构造函数来实例化字段初始值设定项,尽管我不能这样做,并限制您将实例初始值设定项用于匿名类。)此外,类字段和实例字段初始化为默认值。 了解所有这些初始化发生的顺序对于防止混淆是必要的,因此请查看清单2-20。

清单2-20。 完整的初始化演示

class InitDemo
{
static double double1;
double double2;
static int int1;
int int2;
static String string1;
String string2;
static
{
System.out.println("[class] double1 = "+double1);
System.out.println("[class] int1 = "+int1);
System.out.println("[class] string1 = "+string1);
System.out.println();
}
{

class InitDemo
{
static double double1;
double double2;
static int int1;
int int2;
static String string1;
String string2;
static
{
System.out.println("[class] double1 = "+double1);
System.out.println("[class] int1 = "+int1);
System.out.println("[class] string1 = "+string1);
System.out.println();
}
{

static
{
System.out.println("[class] double3 = "+double3);
System.out.println();
}
{
System.out.println("[instance] double4 = "+double3);
System.out.println();
}
public static void main(String[] args)
{
System.out.println ("main() started");
System.out.println();
System.out.println("[class] double1 = "+double1);
System.out.println("[class] double3 = "+double3);
System.out.println("[class] int1 = "+int1);
System.out.println("[class] string1 = "+string1);
System.out.println();
for (int i = 0; i < 2; i++)

{
System.out.println("About to create InitDemo object");
System.out.println();
InitDemo id = new InitDemo();
System.out.println("id created");
System.out.println();
System.out.println("[instance] id.double2 = "+id.double2);
System.out.println("[instance] id.double4 = "+id.double4);
System.out.println("[instance] id.int2 = "+id.int2);
System.out.println("[instance] id.string2 = "+id.string2);

System.out.println();
}
}
}

清单2-20的InitDemo类为双精度浮点基本类型声明了两个类字段和两个实例字段,为整数基元类型声明了一个类字段和一个实例字段,为String引用类型声明了一个类字段和一个实例字段。 它还引入了一个显式初始化的类字段,一个显式初始化的实例字段,三个类初始化器,三个实例初始化器和一个构造函数。 如果编译并运行此代码,您将看到以下输出:

[class] double1 = 0.0
[class] int1 = 0
[class] string1 = null
[class] double3 = 10.0
main() started
[class] double1 = 1.0
[class] double3 = 10.0
[class] int1 = 1000000000
[class] string1 = abc
About to create InitDemo object
[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null
[instance] double4 = 10.0
InitDemo() called
id created

[instance] id.double2 = 1.0
[instance] id.double4 = 10.0
[instance] id.int2 = 1000000000
[instance] id.string2 = abc
About to create InitDemo object
[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null
[instance] double4 = 10.0
InitDemo() called

id created
[instance] id.double2 = 1.0
[instance] id.double4 = 10.0
[instance] id.int2 = 1000000000
[instance] id.string2 = abc

当您结合上述类初始化器和实例初始化器的讨论研究此输出时,您将发现一些有关初始化的有趣事实:

•类字段在加载类之后初始化为默认值或显式值。 在加载类之后,所有类字段都会立即归零为默认值。 ()方法中的代码执行显式初始化。

•所有类初始化发生在()方法返回之前。

•实例字段在对象创建期间初始化为默认值或显式值。 当new为对象分配内存时,它会将所有实例字段归零为默认值。 ()方法中的代码执行显式初始化。

•所有实例初始化都在()方法返回之前发生。
此外,由于初始化以自上而下的方式发生,因此在声明该字段之前尝试访问类字段的内容,或者在声明该字段之前尝试访问实例字段的内容会导致编译器报告非法转发 参考。

继承状态和行为

我们倾向于通过说“汽车是车辆”或“储蓄账户是银行账户”之类的东西来对事物进行分类。通过制作这些陈述,我们确实说汽车继承了车辆状态(例如,制造和颜色)和行为(例如,停车) 并且显示里程数),并且储蓄账户继承银行账户状态(例如,余额)和行为(例如,存款和取款)。 汽车,车辆,储蓄账户和银行账户是现实世界实体类别的示例,继承是类似实体类别之间的等级关系,其中一个类别从至少一个其他实体类别继承状态和行为。 从单个类别继承称为单继承,从至少两个类别继承称为多继承。

Java支持单继承和多继承以促进代码重用 - 为什么重新发明轮子? Java支持类上下文中的单继承,其中类通过类扩展从另一个类继承状态和行为。 因为涉及类,所以Java将这种继承称为实现继承。

Java仅在接口上下文中支持多继承,其中类通过接口实现从一个或多个接口继承行为模板,或者接口通过接口扩展从一个或多个接口继承行为模板。 因为涉及接口,所以Java将这种继承称为接口继承。 (我将在本章后面讨论接口。)

本节首先关注类扩展,向您介绍Java对实现继承的支持。 然后,它会向您介绍位于Java类层次结构顶部的特殊类。 在向您介绍组合之后,这是重用代码的实现继承的替代方法,本节将向您展示如何使用组合来克服实现继承的问题。

扩展类

Java提供了保留字extends,用于指定两个类之间的层次关系。 例如,假设您有一个Vehicle类,并且想要引入扩展Vehicle的Car和Truck类。 清单2-21使用extends来巩固这些关系。

清单2-21。 通过扩展关联类

清单2-21描述了被称为“is-a”关系的关系:汽车或卡车是一种载体。 在这种关系中,Vehicle被称为基类,父类或超类; 每个Car和Truck都被称为派生类,子类或子类。


■注意您无法扩展final类。 例如,如果您将Vehicle声明为final类Vehicle,编译器将在遇到类时报告错误Car extends Vehicle或类Truck extends Vehicle。 当开发人员不希望扩展这些类时(出于安全性或其他原因),他们会声明他们的类是final的。


除了能够提供自己的成员声明之外,Car和Truck中的每一个都能够从其Vehicle超类继承成员声明。 如清单2-22所示,非私有继承成员可供Car和Truck类的成员访问。

清单2-22。 继承成员


class Vehicle
{
private String make;
private String model;
private int year;
Vehicle(String make, String model, int year)
{
this.make = make;
this.model = model;
this.year = year;
}
String getMake()
{
return make;
}
String getModel()
{
return model;
}
int getYear()
{
return year;
}
}

class Car extends Vehicle
{
private int numWheels;
Car(String make, String model, int year, int numWheels)
{
super(make, model, year);
this.numWheels = numWheels;
}
public static void main(String[] args)
{
Car car = new Car("Toyota", "Camry", 2011, 4);
System.out.println("Make = "+car.getMake());
System.out.println("Model = "+car.getModel());
System.out.println("Year = "+car.getYear());
System.out.println("Number of wheels = "+car.numWheels);
System.out.println();
car = new Car("Aptera Motors", "Aptera 2e/2h", 2012, 3);
System.out.println("Make = "+car.getMake());
System.out.println("Model = "+car.getModel());
System.out.println("Year = "+car.getYear());
System.out.println("Number of wheels = "+car.numWheels);
}
}

class Truck extends Vehicle
{
private boolean isExtendedCab;
Truck(String make, String model, int year, boolean isExtendedCab)
{
super(make, model, year);
this.isExtendedCab = isExtendedCab;
}
public static void main(String[] args)
{
Truck truck = new Truck("Chevrolet", "Silverado", 2011, true);
System.out.println("Make = "+truck.getMake());
System.out.println("Model = "+truck.getModel());
System.out.println("Year = "+truck.getYear());
System.out.println("Extended cab = "+truck.isExtendedCab);

}
}

清单2-22的Vehicle类声明了存储车辆品牌,型号和年份的私有字段; 一个构造函数,初始化这些字段以传递参数; 和getter方法检索这些字段的值。

Car子类提供私有numWheels字段,初始化Car对象的Vehicle和Car层的构造函数,以及用于测试此类的main()类方法。 类似地,Truck子类提供私有的isExtendedCab字段,初始化Truck对象的Vehicle和Truck层的构造函数,以及用于测试此类的main()类方法。

Car’s和Truck的构造函数使用保留字super来调用Vehicle的构造函数,并使用面向车辆的参数,然后分别初始化Car的numWheels和Truck的isExtendedCab实例字段。 super()调用类似于指定this()来调用同一个类中的另一个构造函数,而是调用超类构造函数。


■注意super()调用只能出现在构造函数中。 此外,它必须是构造函数中指定的第一个代码。 如果未指定super(),并且超类没有无参数构造函数,则编译器将报告错误,因为当super()不存在时,子类构造函数必须调用无参数超类构造函数。


Car的main()方法创建两个Car对象,将每个对象初始化为特定的make,model,year和wheel数。 四个System.out.println()方法调用随后输出每个对象的信息。 类似地,Truck的main()方法创建一个Truck对象,并且还将此对象初始化为特定的品牌,型号,年份和标志(布尔值true / false值),表示卡车是扩展驾驶室。 前三个System.out.println()方法调用通过调用Car或Truck实例的继承getMake(),getModel()和getYear()方法来检索它们的信息。

最后的System.out.println()方法调用直接访问实例的numWheels或isExtendedCab实例字段。 虽然直接访问实例字段通常不是一个好主意(因为它违反了信息隐藏),但提供此访问权限的Car和Truck类的main()方法中的每一个仅用于测试这些类,并且不存在于 一个使用这些类的真正应用程序。

假设清单2-22存储在名为Vehicle.java的文件中,请执行javac Vehicle.java将此源代码编译为Vehicle.class,Car.class和Truck.class类文件。 然后执行java Car来测试Car类。 此执行导致以下输出:

Make = Toyota
Model = Camry
Year = 2011
Number of wheels = 4
Make = Aptera Motors
Model = Aptera 2e/2h
Year = 2012
Number of wheels = 3

继续,执行java Truck来测试Truck类。 此执行导致以下输出:

Make = Chevrolet
Model = Silverado
Year = 2011
Extended cab = true

子类可以覆盖(替换)继承的方法,以便调用子类的方法版本。 清单2-23显示了重写方法必须指定与被覆盖的方法相同的名称,参数列表和返回类型。
清单2-23。 覆盖方法

class Vehicle
{
private String make;
private String model;
private int year;
Vehicle(String make, String model, int year)
{
this.make = make;
this.model = model;
this.year = year;
}
void describe()
{
System.out.println(year+" "+make+" "+model);
}
}
class Car extends Vehicle
{
private int numWheels;
Car(String make, String model, int year, int numWheels)
{
super(make, model, year);
}
void describe()
{
System.out.print("This car is a "); // Print without newline – see Chapter 1.
super.describe();
}
public static void main(String[] args)

{
Car car = new Car("Ford", "Fiesta", 2009, 4);
car.describe();
}
}

清单2-23的Car类声明了一个describe()方法,它覆盖Vehicle的describe()方法以输出面向汽车的描述。 此方法使用保留字super通过super.describe();调用Vehicle的describe()方法。


■注意通过在方法的名称前加上保留字super和成员访问运算符,从覆盖子类方法调用超类方法。 如果不这样做,最终会递归调用子类的重写方法。 使用super和成员访问运算符从通过声明同名字段来掩盖这些字段的子类访问非私有超类字段。


如果你要编译清单2-23(javac Vehicle.java)并运行Car应用程序(java Car),你会发现Car的重写describe()方法执行而不是Vehicle的重写的describe()方法,并且输出这辆车是 2009款福特嘉年华。


■注意您无法覆盖最终方法。 例如,如果Vehicle的describe()方法被声明为final void describe(),则编译器会在遇到尝试在Car类中覆盖此方法时报告错误。 当开发人员不希望覆盖这些方法时(出于安全性或其他原因),他们会声明他们的方法是最终的。 此外,您不能使覆盖方法比它覆盖的方法更难访问。 例如,如果Car的describe()方法被声明为private void describe(),则编译器将报告错误,因为私有访问比默认包访问更难访问。 但是,通过声明public()可以使describe()更容易访问,就像在public void describe()中一样。


假设您要使用此处显示的方法替换清单2-23的describe()方法:

void describe(String owner)
{
System.out.print("This car, which is owned by "+owner+", is a ");
super.describe();
}

修改后的Car类现在有两个describe()方法,前面显式声明的方法和继承自Vehicle的方法。 void describe(String owner)方法不会覆盖Vehicle的describe()方法。 相反,它会重载此方法。

Java编译器可帮助您检测尝试重载而不是在编译时覆盖方法,方法是让子类的方法标头前缀为@Override注释,如下所示 - 我将在第3章讨论注释:

@Override
void describe()
{
System.out.print("This car is a ");
super.describe();
}

指定@Override会告诉编译器该方法会覆盖另一个方法。 如果重载该方法,编译器将报告错误。 如果没有此注释,编译器将不会报告错误,因为方法重载是一个有效的功能。


■提示养成使用@Override注释为重写方法添加前缀的习惯。 这个习惯会帮助你更快地发现超载错误。


我之前介绍了类和对象的初始化顺序,其中您了解到类成员始终首先被初始化,并且以自上而下的顺序(相同的顺序适用于实例成员)。 实现继承添加了更多细节:

•超类的类初始值设定项始终在子类的类初始值设定项之前执行。

•子类的构造函数始终在初始化子类层之前调用超类构造函数来初始化对象的超类层。

Java对实现继承的支持只允许您扩展单个类。 您不能扩展多个类,因为这样做可能会导致问题。 例如,假设Java支持多个实现继承,并且您决定通过清单2-24中所示的类结构对飞马(来自希腊神话)进行建模。

清单2-24。 多个实现继承的虚构演示

class Horse
{
void describe()
{
// Code that outputs a description of a horse's appearance and behaviors.
}
}
class Bird
{
void describe()
{
// Code that outputs a description of a bird's appearance and behaviors.
}
}
class FlyingHorse extends Horse, Bird
{
public static void main(String[] args)
{
FlyingHorse pegasus = new FlyingHorse();
pegasus.describe();
}
}

这个类结构揭示了每个Horse和Bird声明一个describe()方法产生的歧义。 FlyingHorse继承了以下哪些方法? 相关的歧义来自同名的字段,可能是不同类型的字段。 哪个字段是继承的?

终极超类

一个没有显式扩展另一个类的类隐式扩展了Java的Object类(位于java.lang包中 - 我将在下一章讨论包)。 例如,清单2-1的Image类扩展了Object,而清单2-21的Car和Truck类扩展了Vehicle,它扩展了Object。

Object是Java的终极超类,因为它作为每个其他类的祖先,但本身并不扩展任何其他类。 Object提供了一组其他类继承的通用方法。 表2-1描述了这些方法。

表2-1。 对象的方法

方法描述
Object clone()创建并返回当前对象的副本。
boolean equals(Object obj)确定当前对象是否等于obj标识的对象。
void finalize()完成当前对象。
Class <?> getClass()返回当前对象的Class对象。
int hashCode()返回当前对象的哈希码。
void notify()唤醒正在等待当前对象监视器的其中一个线程。
void notifyAll()唤醒等待当前对象监视器的所有线程。
String toString()返回当前对象的字符串表示形式。
void wait()导致当前线程在当前对象的监视器上等待,直到通过notify()或notifyAll()唤醒它。
void wait(long timeout)导致当前线程在当前对象的监视器上等待,直到通过notify()或notifyAll()唤醒它,或者直到指定的超时值(以毫秒为单位)已经过去,以先到者为准。
void wait(long timeout,int nanos)导致当前线程在当前对象的监视器上等待,直到通过notify()或notifyAll()唤醒它,或者直到指定的超时值(以毫秒为单位)加上nanos值(以纳秒为单位) 已过去,以先到者为准。

我将很快讨论clone(),equals(),finalize(),hashCode()和toString()方法,但推迟讨论getClass(),notify(),notifyAll()和wait()方法 到第4章。


■注意第6章介绍了java.util.Objects类,它提供了几个null-safe或null容错类方法,用于比较两个对象,计算对象的哈希代码,要求引用不为null,并返回一个 对象的字符串表示。


克隆

clone()方法克隆(复制)一个对象而不调用构造函数。 它将每个基元或引用字段的值复制到克隆中的对应项,称为浅复制或浅克隆。 清单2-25演示了这种行为。

清单2-25。 浅孔克隆一个Employee对象

class Employee implements Cloneable
{
String name;
int age;
Employee(String name, int age)
{
this.name = name;
this.age = age;
}
public static void main(String[] args) throws CloneNotSupportedException
{
Employee e1 = new Employee("John Doe", 46);
Employee e2 = (Employee) e1.clone();
System.out.println(e1 == e2); // Output: false
System.out.println(e1.name == e2.name); // Output: true
}
}

清单2-25声明了一个带有名称和年龄实例字段的Employee类,以及一个构造函数
初始化这些字段。 main()方法使用此构造函数初始化新的Employee对象
这些字段的副本给John Doe和46。


■注意类必须实现java.lang.Cloneable接口,否则不能通过Object的clone()方法对其实例进行浅层克隆 - 此方法执行运行时检查以查看该类是否实现了Cloneable。 (我将在本章后面讨论接口。)如果一个类没有实现Cloneable,clone()会抛出java.lang.CloneNotSupportedException(因为CloneNotSupportedException是一个经过检查的异常,所以清单2-25必须通过附加来满足编译器 将CloneNotSupportedException抛出到main()方法的头文件中。我将在下一章讨论异常。)String是一个不实现Cloneable的类的示例; 因此,String对象不能被浅层克隆。


在分配Employee对象对局部变量e1的引用之后,main()调用此变量的clone()方法来复制对象,然后将结果引用赋给变量e2。 需要(Employee)强制转换,因为clone()返回Object。

为了证明其引用分配给e1和e2的对象不同,main()接下来通过比较这些引用并输出布尔结果,该结果恰好为假。 为了证明Employee对象被浅层克隆,main()接下来通过比较两个Employee对象的名称字段中的引用,并输出布尔结果,这恰好是真的。


浅层克隆并不总是令人满意的,因为原始对象及其克隆通过它们的等效引用字段引用相同的对象。 例如,清单2-25中的每个Employee对象都通过其name字段引用相同的String对象。

虽然对于其实例是不可变的String不是问题,但是通过克隆的引用字段更改可变对象会导致原始(非克隆)对象通过其引用字段看到相同的更改。 例如,假设您向Employee添加名为hireDate的引用字段。 此字段的类型为Date,包含年,月和日实例字段。 由于Date旨在是可变的,因此您可以在分配给hireDate的Date实例中更改这些字段的内容

现在假设您计划更改克隆的日期,但希望保留原始的Employee对象的日期。 您无法通过浅层克隆执行此操作,因为原始Employee对象也可以看到更改。 要解决此问题,您必须修改克隆操作,以便为Employee clone的hireDate字段分配新的Date引用。 这个任务称为深度复制或深度克隆,如清单2-26所示。

清单2-26。 深入克隆Employee对象

class Date
{
int year, month, day;
Date(int year, int month, int day)
{
this.year = year;
this.month = month;
this.day = day;
}
}
class Employee implements Cloneable
{
String name;
int age;
Date hireDate;
Employee(String name, int age, Date hireDate)
{
this.name = name;
this.age = age;
this.hireDate = hireDate;
}
@Override
protected Object clone() throws CloneNotSupportedException
{
Employee emp = (Employee) super.clone();
if (hireDate != null) // no point cloning a null object (one that does not exist)
emp.hireDate = new Date(hireDate.year, hireDate.month, hireDate.day);
return emp;
}

public static void main(String[] args) throws CloneNotSupportedException
{
Employee e1 = new Employee("John Doe", 46, new Date(2000, 1, 20));
Employee e2 = (Employee) e1.clone();
System.out.println(e1 == e2); // Output: false
System.out.println(e1.name == e2.name); // Output: true
System.out.println(e1.hireDate == e2.hireDate); // Output: false
System.out.println(e2.hireDate.year+" "+e2.hireDate.month+" "+
e2.hireDate.day); // Output: 2000 1 20
}
}

清单2-26声明了Date和Employee类。 Date类声明年,月和日字段以及构造函数。

Employee重写clone()方法以深入克隆hireDate字段。 此方法首先调用Object的clone()方法,以浅层克隆当前Employee对象的实例字段,然后将新对象的引用存储在emp中。 接下来,它会为emp的hireDate字段分配一个新的Date对象; 此对象的字段初始化为与原始Employee对象的hireDate实例中的字段相同的值。

此时,您有一个带有浅克隆名称和年龄字段的Employee克隆,以及一个深度克隆的hireDate字段。 clone()方法通过返回此Employee克隆来完成。


■注意如果您没有从重写的clone()方法调用Object的clone()方法(因为您更喜欢深度克隆引用字段并对非引用字段进行浅层复制),则包含覆盖的类不需要 clone()方法实现Cloneable,但它应该实现此接口以保持一致性。 String不会覆盖clone(),因此无法深度克隆String对象。


Equality

和!=运算符比较两个原始值(例如整数)的相等性()或不等式(!=)。 这些运算符还比较两个引用,以查看它们是否引用相同的对象。 后一种比较称为身份检查。

您不能使用==和!=来确定两个对象在逻辑上是否相同(或不相同)。 例如,具有相同字段值的两个Truck对象在逻辑上是等效的。 但是,==因为它们的引用不同而将它们报告为不相等。


■注意因为和!=执行最快的比较,并且因为字符串比较需要快速执行(特别是在排序大量字符串时),String类包含允许文字字符串和字符串值常量表达式的特殊支持 通过和!=进行比较。 (当我在第4章中提出String时,我将讨论这种支持。)以下语句演示了这些比较:

System.out.println(“abc” == “abc”); // Output: true
System.out.println(“abc” == “a”+“bc”); // Output: true
System.out.println(“abc” == “Abc”); // Output: false
System.out.println(“abc” != “def”); // Output: true
System.out.println(“abc” == new String(“abc”)); // Output: false


除了引用相等之外,Java认识到需要支持逻辑相等,因此Java在Object类中提供了equals()方法。 由于此方法默认为比较引用,因此需要重写equals()以比较对象内容。

在重写equals()之前,请确保这是必要的。 例如,Java的java.lang.StringBuffer类(在第4章中讨论)不会覆盖equals()。 也许这个类的设计者认为没有必要确定两个StringBuffer对象在逻辑上是否等价。

您不能使用任意代码覆盖equals()。 这样做可能会对您的应用程序造成灾难性后果。 相反,您需要遵守此方法的Java文档中指定的合同,以及我接下来要介绍的合同。

equals()方法在非空对象引用上实现等价关系:

•它是自反的:对于任何非空参考值x,x.equals(x)返回true。

•它是对称的:对于任何非空参考值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才返回true。

•它是传递性的:对于任何非空参考值x,y和z,如果x.equals(y)返回true并且y.equals(z)返回true,则x.equals(z)返回true。

•它是一致的:对于任何非空引用值x和y,x.equals(y)的多次调用始终返回true或始终返回false,前提是不修改对象的equals()比较中使用的信息。

•对于任何非空引用值x,x.equals(null)返回false。

尽管这份合同看起来有些令人生畏,但要满足并不难。 为了证明,请看一下清单2-27的Point类中equals()方法的实现。

清单2-27。 逻辑比较Point对象

class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
@Override
public boolean equals(Object o)
{
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
public static void main(String[] args)
{
Point p1 = new Point(10, 20);
Point p2 = new Point(20, 30);
Point p3 = new Point(10, 20);
// Test reflexivity
System.out.println(p1.equals(p1)); // Output: true
// Test symmetry
System.out.println(p1.equals(p2)); // Output: false
System.out.println(p2.equals(p1)); // Output: false
// Test transitivity
System.out.println(p2.equals(p3)); // Output: false
System.out.println(p1.equals(p3)); // Output: true
// Test nullability
System.out.println(p1.equals(null)); // Output: false
// Extra test to further prove the instanceof operator's usefulness.
System.out.println(p1.equals("abc")); // Output: false
}
}

清单2-27的重写equals()方法以if语句开头,该语句使用instanceof运算符来确定传递给参数o的参数是否是Point类的实例。 如果不是,则if语句执行return false;。

Point实例的o instanceof满足契约的最后部分:对于任何非空引用值x,x.equals(null)返回false。 由于null引用不是任何类的实例,因此将此值传递给equals()会导致表达式求值为false。

如果将Point对象以外的对象传递给equals(),则Point实例的o实例还可以防止通过表达式(Point)o抛出java.lang.ClassCastException实例。 (我将在下一章讨论例外情况。)

在演员表之后,通过表达式p.x == x && p.y == y,只允许将点与其他点进行比较,从而满足合约的反身性,对称性和及物性要求。 通过确保equals()方法是确定性的来满足最终的合同要求,一致性。 换句话说,此方法不依赖于可能从方法调用更改为方法调用的任何字段值。


■提示您可以通过首先使用==来确定o的引用是否标识当前对象,从而优化耗时的equals()方法的性能。 只需指定if(o == this)是否返回true; 作为equals()方法的第一个语句。 清单2-27的equals()方法不需要这种优化,它具有令人满意的性能


重写equals()时始终覆盖hashCode()方法很重要。 我在清单2-27中没有这样做,因为我还没有正式引入hashCode()。

Finalization

终结是指通过finalize()方法进行清理,该方法称为终结器。 finalize()方法的Java文档声明当垃圾收集确定没有对该对象的更多引用时,finalize()“由对象上的垃圾收集器调用”。 子类重写finalize()方法以处置系统资源或执行其他清理。“

对象的finalize()版本什么都不做; 您必须使用任何所需的清理代码覆盖此方法。 因为JVM在应用程序终止之前可能永远不会调用finalize(),所以应该提供一个显式的清理方法,并且如果没有另外调用该方法,则finalize()将此方法称为安全网。


■注意永远不要依赖finalize()来释放有限的资源,例如图形上下文或文件描述符。 例如,如果应用程序对象打开文件,期望其finalize()方法将关闭它们,那么当迟到的JVM调用finalize()很慢时,应用程序可能会发现自己无法打开其他文件。 使这个问题变得更糟的原因是在另一个JVM上可能会更频繁地调用finalize(),从而导致这个太多的开放文件问题无法自我揭示。 因此,开发人员可能错误地认为应用程序在不同的JVM中表现一致。


如果您决定覆盖finalize(),则对象的子类层必须为其超类层提供执行最终化的机会。 您可以通过指定super.finalize()来完成此任务; 作为方法中的最后一个语句,以下示例演示:

@Override
protected void finalize() throws Throwable
{
try
{
// Perform subclass cleanup.
}
finally
{
super.finalize();
}
}

示例的finalize()声明会将Throwable抛出到方法头,因为清理代码可能会引发异常。 如果抛出异常,执行将离开方法,并且在没有try-finally的情况下,执行super.finalize(); 从不执行。 (我将讨论异常,最后在第3章中尝试。)

为了防止这种可能性,子类的清理代码在保留字try之后的块中执行。 如果抛出异常,Java的异常处理逻辑将执行最后保留字后面的块,并且super.finalize(); 执行超类的finalize()方法。

finalize()方法通常用于执行复活(使未引用的对象被引用),以实现在这些对象昂贵(按时间)创建时回收相同对象的对象池(数据库连接对象是一个示例)。

将此(对当前对象的引用)分配给类或实例字段(或另一个长寿命变量)时会发生复活。 例如,您可以指定r = this; 在finalize()中,将标识为this的未引用对象分配给名为r的类字段。

由于复活的可能性,对覆盖finalize()的对象的垃圾收集施加了严重的性能损失。 您将了解这种惩罚,并在第4章中更好地覆盖finalize()。


■注意无法再次调用复活对象的终结器。


Hash Codes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值