Java17 入门基础知识(七)
十一、对象和Object类
在本章中,您将学习:
-
Java 中的层次类结构
-
Object类是所有其他类的超类 -
如何用详细的例子使用
Object类的方法 -
如何在你的类中重新实现
Object类的方法 -
如何检查两个对象是否相等
-
不可变对象和可变对象的区别
-
如何使用
Objects类的实用方法来优雅地处理null值 -
lambda 表达式简介
本章中的所有类都是一个jdojo.object模块的成员,如清单 11-1 中所声明的。
// module-info.java
module jdojo.object {
exports com.jdojo.object;
}
Listing 11-1The Declaration of a jdojo.object Module
Object类
Java 在java.lang包中有一个Object类,它是java.base模块的成员。所有的 Java 类,那些包含在 Java 类库中的和那些你创建的,都直接或间接地扩展了Object类。所有 Java 类都是Object类的子类,而Object类是所有类的超类。注意Object类本身没有超类。
Java 中的类被安排在一个树状的层次结构中,其中Object类位于根(或顶部)。我们将在第二十章中详细讨论类的层次结构,其中包括继承。我们在本章中讨论了Object类的一些细节。
关于Object类有两条重要的规则。这里就不解释这些规则背后的原因了。在你阅读了第二十章之后,你会明白为什么你可以用Object类做这些事情的原因。
规则 1
Object类的引用变量可以保存任何类的对象的引用。正如任何引用变量都可以存储一个null引用一样,Object类型的引用变量也可以。考虑下面对Object类型的引用变量obj的声明:
Object obj;
您可以将 Java 中任何对象的引用分配给obj。以下所有语句都是有效的:
// Can assign the null reference
obj = null;
// Can assign a reference of an object of the Object class
obj = new Object();
// Can assign a reference of an object of the Account class
Account act = new Account();
obj = act;
// Can assign a reference to an object of any class. Assume that the AnyClass class exists
obj = new AnyClass();
这条规则的反面是不正确的。不能将Object类对象的引用赋给任何其他类型的引用变量。以下语句无效:
Account act = new Object(); // A compile-time error
有时,你可能会将一个特定类型的对象的引用存储在一个Object类型的引用变量中,然后你想将相同的引用赋回给一个Account类型的引用变量。您可以使用如下所示的强制转换来实现这一点:
Object obj2 = new Account(); Account act = (Account) obj2; // Must use a cast
有时您可能不确定Object类的引用变量是否包含对特定类型对象的引用。在这些情况下,您需要使用instanceof操作符进行测试。instanceof操作符的左操作数是一个引用变量,它的右操作数是一个类名——具体来说,是一个类型名,包括类和接口。如果其左操作数是其右操作数类型的引用,则返回true。否则,它返回false。关于instanceof操作器的更详细讨论,参见第二十章:
Object obj;
Cat c;
/* Do something here and store a reference in obj... */
if (obj instanceof Cat) {
// If we get here, obj holds a reference of a Cat for sure
c = (Cat)obj;
}
当您有一个将Object作为参数的方法时,您需要使用这个规则。您可以为Object类的参数传递任何对象的引用。请考虑下面的代码片段,它显示了一个方法声明:
public void m1(Object obj) {
// Code goes here
}
您可以通过多种不同方式调用m1():
m1(null); // Pass null reference
m1(new Object()); // Pass a reference of an object of the Object class
m1(new AnyClass()); // Pass a reference of an object of the AnyClass class
规则二
Object类包含九个方法,可以在 Java 的所有类中使用。我们可以把这些方法分为两类:
-
第一类方法已经在
Object类中实现。你应该在它们被实现的时候使用它们。您不能在您创建的任何类中重新实现(重新实现的技术术语是覆盖)这些方法。他们的实现是final。属于这一类别的方法有getClass()、notify()、notifyAll()和wait()。 -
第二类方法在
Object类中有一个默认的实现。您可以通过在您的类中重新实现它们来自定义它们的实现。属于这一类别的方法有toString()、equals()、hashCode()、clone()和finalize()。
Java 程序员必须理解正确使用Object类中的所有方法。我们将详细讨论它们,除了notify()、notifyAll()和wait()方法。这些方法用于线程同步(这超出了本书的范围,但将在本系列的第二卷中讨论)。表 11-1 列出了Object类中的所有方法,并附有简要描述。“Implemented”列中的“Yes”表示Object类已经实现了该方法,无需编写任何代码即可使用。此列中的“否”表示您需要在使用该方法之前实现它。“可定制”列中的“是”表示您可以在您的类中重新实现该方法来定制它。该列中的“否”表示Object类已经实现了该方法,其实现是final。
表 11-1
Object类中的方法
方法
|
执行
|
可定制的
|
描述
|
| --- | --- | --- | --- |
| public String toString() | 是 | 是 | 返回对象的字符串表示形式。通常,它用于调试目的。 |
| public boolean equals(Object obj) | 是 | 是 | 用于比较两个对象是否相等。 |
| public int hashCode() | 是 | 是 | 返回对象的哈希代码(整数)值。 |
| protected Object clone() throws CloneNotSupportedException | 不 | 是 | 用于制作对象的副本。 |
| protected void finalize() throws Throwable | 不 | 是 | 在对象被销毁之前由垃圾收集器调用。它在 Java SE 9 中已被弃用。 |
| public final Class getClass() | 是 | 不 | 返回对对象的Class对象的引用。 |
| public final void notify() | 是 | 不 | 通知对象的等待队列中的一个线程。 |
| public final void notifyAll() | 是 | 不 | 通知对象的等待队列中的所有线程。 |
| public final void wait()``throws InterruptedException``public final void wait(long timeout)``throws InterruptedException``public final void wait(long timeout, int nanos)``throws InterruptedException | 是 | 不 | 使线程在对象的等待队列中等待,超时或不超时。 |
要重新实现一个Object类的方法,你需要像在Object类中一样声明这个方法,然后在它的主体中编写你自己的代码。重新实现一个方法有更多的规则。我们将在第二十章介绍所有规则。你可以在你的类中重新实现Object类的toString()方法,比如说Test,如图所示:
public class Test {
/* Reimplement the toString() method of the Object class */
public String toString() {
return "Here is a string";
}
}
本书将在接下来的章节中详细讨论Object类的六个方法。
一个对象的类是什么?
Java 中的每个对象都属于一个类。您在源代码中定义了一个类,它被编译成二进制格式(扩展名为.class的类文件)。在运行时使用一个类之前,它的二进制表示被加载到 JVM 中。将类的二进制表示加载到 JVM 中是由一个称为类加载器的对象来处理的。通常,在一个 Java 应用程序中使用多个类装入器来装入不同类型的类。类加载器是类java.lang.ClassLoader的一个实例。Java 允许你通过扩展ClassLoader类来创建自己的类装入器。通常,您不需要创建自己的类装入器。Java 运行时将使用其内置的类加载器来加载您的类。
类装入器将类定义的二进制格式读入 JVM。二进制类格式可以从任何可访问的位置加载,例如本地文件系统、网络、数据库等。然后,它创建一个java.lang.Class<T>类的对象,这是 JVM 中类型T的二进制表示。注意类名java.lang.Class中的大写C。不同的类装入器可以在 JVM 中多次装入类定义的二进制格式。JVM 中的类由它的完全限定名和它的类装入器的组合来标识。通常,类的二进制定义在 JVM 中只加载一次。
Tip
您可以将Class<T>类的对象视为类源代码的运行时描述符。运行时,类的源代码由Class类的对象表示。事实上,Java 中的所有类型——类、接口和基本类型——在运行时都由一个Class类的实例来表示。
Object类的getClass()方法返回Class对象的引用。因为getClass()方法是在Object类中声明和实现的,所以可以对任何类型的引用变量使用这个方法。下面的代码片段显示了如何为一个Cat对象获取Class对象的引用:
Cat c = new Cat();
Class catClass = c.getClass();
Class类是泛型的,它的形式类型参数是由它的对象表示的类名。您可以使用泛型重写该语句,如下所示:
Class<Cat> catClass = c.getClass();
默认情况下,类定义只加载一次,每个 Java 类只有一个Class对象。我们不考虑那些你已经编写代码多次加载同一个类的情况。如果你在同一个类的不同对象上使用getClass()方法,你将得到同一个Class对象的引用。考虑以下代码片段:
Cat c2 = new Cat();
Cat c3 = new Cat();
Class catClass2 = c2.getClass();
Class catClass3 = c3.getClass();
这里,c2和c3是同一个Cat类的两个对象。因此,c2.getClass()和c3.getClass()返回同一个Class对象的引用,该对象代表 JVM 中的Cat类。表达式catClass2 == catClass3将计算为true。
Class类有许多有用的方法。您可以使用它的getName()方法来获取类的完全限定名。你可以使用它的getSimpleName()来获得类的简单名称,例如:
String fullName = catClass.getName();
String simpleName = catClass.getSimpleName();
Tip
当应用程序启动时,并不是应用程序中的所有类都被加载到 JVM 中。加载一个类,当应用程序第一次使用该类时,创建一个对应的Class对象。
计算对象的哈希代码
哈希代码是使用算法为一条信息计算的整数值。哈希代码也称为哈希和、哈希值或简称为哈希。从一条信息中计算整数的算法称为哈希函数。散列码的定义涉及三件事:
-
一条信息
-
一种算法
-
整数值
你有一条信息。你对它应用一个算法来产生一个整数值。你得到的整数值是你所拥有的信息的散列码。如果您更改信息片段或算法,计算出的哈希代码可能会更改,也可能不会更改。图 11-1 描绘了计算散列码的过程。
图 11-1
计算散列码的过程
计算散列码是一个单向过程。从哈希代码中获取原始信息并不容易,这也不是哈希代码计算的目标。
可以用来生成散列码的信息可以是任意的字节、字符、数字或它们的组合序列。例如,您可能想要计算字符串"Hello"的散列码。
哈希函数是什么样子的?哈希函数可能像下面的函数一样简单,它返回所有输入数据的整数零:
int myHashFunction(<your input data>) {
return 0; // Always return zero
}
这个散列函数符合散列函数的定义,尽管它不是一个实用的好函数。编写一个好的哈希函数不是一件容易的事情。在编写一个好的散列函数之前,您需要考虑关于输入数据的许多事情。
你为什么需要一个散列码?当数据存储在基于散列的集合(或容器)中时,需要它来有效地检索与之相关的数据。在将数据存储到容器中之前,会计算其哈希代码,然后将其存储在基于其哈希代码的位置(也称为桶)。当您想要检索数据时,可以使用它的哈希代码来查找它在容器中的位置,这样可以更快地检索信息。值得注意的是,使用散列码的数据的有效检索是基于散列码值在一个范围内的分布。如果生成的哈希代码不是均匀分布的,则数据检索可能效率不高。在最坏的情况下,对数据的检索可能与对存储在容器中的所有元素进行线性搜索一样糟糕。如果使用散列函数,容器中的所有元素都将存储在同一个桶中,这将需要搜索所有元素。使用一个好的散列函数,使它为您提供均匀分布的散列码,这对于实现一个高效的基于散列的容器以实现快速数据检索是至关重要的。
Java 中哈希码有什么用?Java 使用哈希代码的原因和上面描述的一样——从基于哈希的集合中高效地检索数据。如果你的类的对象在基于散列的集合中没有被用作键,例如在HashSet、HashMap等中。,您不必担心对象的哈希代码。
你可以用 Java 计算一个对象的散列码。在对象的情况下,将用于计算哈希代码的信息片段是组成对象状态的信息片段。Java 设计者认为对象的哈希代码非常重要,因此他们提供了一个默认实现来计算Object类中对象的哈希代码。
Object类包含一个hashCode()方法,该方法返回一个int,它是对象的哈希代码。此方法的默认实现通过将对象的内存地址转换为整数来计算对象的哈希代码。因为hashCode()方法是在Object类中定义的,所以它在 Java 的所有类中都可用。但是,您可以自由地在类中重写该实现。这里是当你在你的类中重写hashCode()方法时你必须遵循的规则。假设有两个对象引用,x和y:
-
如果
x.equals(y)返回true,x.hashCode()必须返回一个整数,等于y.hashCode()。也就是说,如果两个对象使用equals()方法相等,它们必须有相同的散列码。 -
如果
x.hashCode()等于y.hashCode(),则x.equals(y)不需要返回true。也就是说,如果两个对象使用hashCode()方法具有相同的散列码,那么使用equals()方法它们不一定相等。 -
如果在 Java 应用程序的同一次执行中对同一个对象多次调用
hashCode()方法,该方法必须返回相同的整数值。hashCode()和equals()方法紧密联系在一起。如果您的类重写这两个方法中的任何一个,它必须重写这两个方法,以便您的类的对象能够在基于哈希的集合中正确工作。另一个规则是,您应该只使用那些实例变量来计算对象的哈希代码,这些代码也在equals()方法中用于检查相等性。
如果你的类是可变的,你不应该在基于散列的集合中使用你的类的对象作为键。如果用作键的对象在使用后发生更改,您将无法在集合中定位该对象,因为在基于哈希的集合中定位对象是基于其哈希代码的。在这种情况下,集合中会有滞留的对象。
你应该如何为一个类实现一个hashCode()方法?以下是为您的类编写hashCode()方法的逻辑的一些准则,对于大多数目的来说是合理的:
-
从一个质数开始,比如 37:
-
使用以下逻辑分别计算原始数据类型的每个实例变量的哈希代码值。注意,您只需要在哈希代码计算中使用那些实例变量,它们也是
equals()方法逻辑的一部分。让我们将这一步的结果存储在一个int变量code中。我们假设value是实例变量的名字。对于
byte、short、int和char数据类型,使用它们的整数值作为
int hash = 37;
code = (int)value;
对于long数据类型,使用XOR作为 64 位的两半
code = (int)(value ^ (value >>> 32));
对于float数据类型,使用以下方法将其浮点值转换为等效的整数值
code = Float.floatToIntBits(value);
对于double数据类型,使用Double类的doubleToLongBits()方法将其浮点值转换为long,然后使用前面针对long数据类型描述的过程将长整型值转换为int值:
long longBits = Double.doubleToLongBits(value);
code = (int)(longBits ^ (longBits >>> 32));
对于boolean数据类型,使用1表示true,使用0表示false:
- 对于引用实例变量,如果是
null,则使用0。否则,调用它的hashCode()方法来获得它的散列码。假设ref是引用变量的名称:
code = (value ? 1 : 0);
- 使用以下公式计算哈希代码。在公式中使用
59是一个任意的决定。任何其他质数,比如说47,都可以很好地工作:
code = (ref == null ? 0: ref.hashCode());
-
对您想要包含在
hashCode()计算中的所有实例变量重复前面的三个步骤。 -
最后,从您的
hashCode()方法返回包含在hash变量中的值。
hash = hash * 59 + code;
这个方法是在 Java 中计算对象散列码的许多方法之一,但不是唯一的方法。如果你需要一个更强的散列函数,请查阅一本关于计算散列码的好教科书。所有原始包装类和String类都覆盖了hashCode()方法,以提供相当好的散列函数实现。
Tip
Java 7 中增加了一个名为java.lang.Objects的实用程序类。它包含一个hash()方法,该方法计算任意类型的任意数量的值的哈希代码。建议你使用Objects.hash()方法来计算一个对象的散列码。有关详细信息,请参阅本章后面的“Object类”一节。
清单 11-2 包含了一个Book类的代码。它展示了hashCode()方法的一个可能的实现。注意在hashCode()方法的声明中使用了@Override注释。当你在你的类中重新实现超类的方法时,你应该使用这个注释。注释用于用附加信息标记方法、字段和类。在这种情况下,@Override 告诉 Java 编译器您打算从超类或接口中覆盖一个方法。这将在第 20 和 21 章中详细介绍。
// Book.java
package com.jdojo.object;
public class Book {
private String title;
private String author;
private int pageCount;
private boolean hardCover;
private double price;
/* Other code goes here */
/* Must implement the equals() method too. */
@Override
public int hashCode() {
int hash = 37;
int code = 0;
// Use title
code = (title == null ? 0 : title.hashCode());
hash = hash * 59 + code;
// Use author
code = (author == null ? 0 : author.hashCode());
hash = hash * 59 + code;
// Use pageCount
code = pageCount;
hash = hash * 59 + code;
// Use hardCover
code = (hardCover ? 1 : 0);
hash = hash * 59 + code;
// Use price
long priceBits = Double.doubleToLongBits(price);
code = (int) (priceBits ^ (priceBits >>> 32));
hash = hash * 59 + code;
return hash;
}
}
Listing 11-2A Book Class That Reimplements the hashCode() Method
Book类有五个实例变量:title、author、pageCount、hardcover和price。该实现使用所有五个实例变量来计算一个Book对象的散列码。您还必须为Book类实现equals()方法,它必须使用所有五个实例变量来检查两个Book对象是否相等。您需要确保equals()方法和hashCode()方法在它们的逻辑中使用相同的实例变量集。假设您向Book类添加了一个实例变量。姑且称之为ISBN。因为ISBN惟一地标识了一本书,所以您可以只使用ISBN实例变量来计算它的散列码,并与另一个Book对象进行相等性比较。在这种情况下,只使用一个实例变量来计算哈希代码并检查相等性就足够了。
关于 Java 中对象的散列码有一些误解。开发人员认为散列码唯一地标识了一个对象,并且它必须是一个正整数。然而,它们不是真的。哈希代码不唯一地标识对象。两个不同的对象可能具有相同的哈希代码。散列码不必仅仅是正数。它可以是任何整数值,正的或负的。关于散列码的用法也存在混乱。它们仅用于从基于哈希的集合中高效检索数据。如果您的对象没有在基于散列的集合中用作键,并且您没有在您的类中覆盖equals()方法,那么您根本不需要担心在您的类中重新实现hashCode()方法。最有可能的是,它将覆盖equals()方法,这将提示您为您的类覆盖hashCode()方法。如果您不同时在您的类中重写并提供正确的hashCode()和equals()方法的实现,您的类的对象在基于散列的集合中将不会正常工作。Java 编译器或 Java 运行时永远不会对这两个方法在类中的不正确实现给出任何警告或错误。
比较对象是否相等
宇宙中的每个对象都不同于所有其他对象,Java 程序中的每个对象都不同于所有其他对象。所有对象都有唯一的标识。一个对象被分配的内存地址可以被当作它的标识,这将使它总是唯一的。如果两个对象具有相同的标识(或 Java 术语中的引用),则它们是相同的。考虑以下代码片段:
Object obj1;
Object obj2;
/* Do something... */
if (obj1 == obj2) {
/* obj1 and obj2 are the same object based on identity */
} else {
/* obj1 and obj2 are different objects based on identity */
}
这段代码使用身份比较来测试obj1和obj2是否相等。它比较两个对象的引用,以测试它们是否相等。
有时,如果两个对象基于它们的一些或所有实例变量具有相同的状态,您希望将它们视为相等。如果你想基于标准而不是引用(身份)来比较你的类的两个对象是否相等,你的类需要重新实现Object类的equals()方法。Object类中的equals()方法的默认实现比较作为参数传递的对象和调用该方法的对象的引用。如果两个引用相等,它返回true。否则返回false。换句话说,Object类中的equals()方法执行基于身份的相等性比较。该方法的实现如下。回想一下,类的实例方法中的关键字this指的是调用该方法的对象的引用:
public boolean equals(Object obj) {
return (this == obj);
}
考虑下面的代码片段。它使用相等运算符(==)比较一些Point对象,该运算符总是比较其两个操作数的引用。它还使用了Object类的equals()方法来比较相同的两个引用。输出显示结果是相同的。请注意,您的Point类不包含equals()方法。当您在Point对象上调用equals()方法时,将使用Object类中equals()方法的实现:
Point pt1 = new Point(10, 10);
Point pt2 = new Point(10, 10);
Point pt3 = new Point(12, 19);
Point pt4 = pt1;
System.out.println("pt1 == pt1: " + (pt1 == pt1));
System.out.println("pt1.equals(pt1): " + pt1.equals(pt1));
System.out.println("pt1 == pt2: " + (pt1 == pt2));
System.out.println("pt1.equals(pt2): " + pt1.equals(pt2));
System.out.println("pt1 == pt3: " + (pt1 == pt3));
System.out.println("pt1.equals(pt3): " + pt1.equals(pt3));
System.out.println("pt1 == pt4: " + (pt1 == pt4));
System.out.println("pt1.equals(pt4): " + pt1.equals(pt4));
pt1 == pt1: true
pt1.equals(pt1): true
pt1 == pt2: false
pt1.equals(pt2): false
pt1 == pt3: false
pt1.equals(pt3): false
pt1 == pt4: true
pt1.equals(pt4): true
实际上,如果两个点具有相同的(x,y)坐标,则认为它们是相同的。如果你想为你的Point类实现这个相等规则,你必须重新实现equals()方法,如清单 11-3 所示。
// SmartPoint.java
package com.jdojo.object;
public class SmartPoint {
private int x;
private int y;
public SmartPoint(int x, int y) {
this.x = x;
this.y = y;
}
/* Reimplement the equals() method */
@Override
public boolean equals(Object otherObject) {
// Are the same?
if (this == otherObject) {
return true;
}
// Is otherObject a null reference?
if (otherObject == null) {
return false;
}
// Do they belong to the same class?
if (this.getClass() != otherObject.getClass()) {
return false;
}
// Get the reference of otherObject in a SmartPoint variable
SmartPoint otherPoint = (SmartPoint)otherObject;
// Do they have the same x and y co-ordinates
boolean isSamePoint = (this.x == otherPoint.x && this.y == otherPoint.y);
return isSamePoint;
}
/* Reimplement hashCode() method of the Object class,
which is a requirement when you reimplement equals() method
*/
@Override
public int hashCode() {
return (this.x + this.y);
}
}
Listing 11-3A SmartPoint Class That Reimplements equals() and hashCode() Methods
你称你的新类为SmartPoint。Java 建议一起重新实现hashCode()和equals()方法,如果它们中的任何一个在你的类中被重新实现。如果你重新实现了equals()方法而不是hashCode()方法,Java 编译器不会抱怨。但是,当您在基于哈希的集合中使用类的对象时,将会得到不可预知的结果。
对hashCode()方法的唯一要求是,如果m.equals(n)方法返回true,m.hashCode()必须返回与n.hashCode()相同的值。因为您的equals()方法使用(x,y)坐标来测试相等性,所以您从hashCode()方法返回 x 和 y 坐标的和,这满足了技术要求。实际上,您需要使用更好的散列算法来计算散列值。
您已经在SmartPoint类的equals()方法中编写了几行代码。让我们一个一个地过一遍逻辑。首先,您需要检查传递的对象是否与调用该方法的对象相同。如果这两个对象是相同的,那么通过返回true来认为它们是相等的。这是通过以下代码完成的:
// Are they the same?
if (this == otherObject) {
return true;
}
如果传递的参数是null,两个对象不能相同。注意,调用方法的对象永远不能是null,因为不能在null引用上调用方法。当试图在null引用上调用方法时,Java 运行时将抛出运行时异常。下面的代码确保您正在比较两个非空对象:
// Is otherObject a null reference?
if (otherObject == null) {
return false;
}
该方法的参数类型为Object。这意味着可以传递任何类型的对象引用。例如,可以使用apple.equals(orange),其中apple和orange分别是对一个Apple对象和一个Orange对象的引用。在您的例子中,您只想将一个SmartPoint对象与另一个SmartPoint对象进行比较。为了确保被比较的对象属于同一类,您需要下面的代码。如果有人用一个不是SmartPoint对象的参数调用该方法,它将返回false:
// Do they have the same class?
if (this.getClass() != otherObject.getClass()) {
return false;
}
此时,您肯定有人试图比较两个具有不同身份(引用)的非空SmartPoint对象。现在你想比较两个物体的(x,y)坐标。要访问otherObject形参的 x 和 y 实例变量,必须将其转换成一个SmartPoint对象。下面的语句可以做到这一点:
// Get the reference of otherObject in a SmartPoint variable
SmartPoint otherPoint = (SmartPoint)otherObject;
此时,只需要比较两个SmartPoint对象的 x 和 y 实例变量的值。如果它们相同,则通过返回true认为两个对象相等。否则,两个对象不相等,你返回false。这是通过以下代码完成的:
// Do they have the same x and y coordinates
boolean isSamePoint = (this.x == otherPoint.x && this.y == otherPoint.y);
return isSamePoint;
是时候测试您在SmartPoint类中对equals()方法的重新实现了。清单 11-4 是你的测试类。您可以在输出中观察到,您有两种方法来比较两个SmartPoint对象是否相等。等式运算符(==)基于相同性对它们进行比较,equals()方法基于(x,y)坐标值对它们进行比较。注意,如果两个SmartPoint对象的(x,y)坐标相同,equals()方法返回true。
// SmartPointTest.java
package com.jdojo.object;
public class SmartPointTest {
public static void main(String[] args) {
SmartPoint pt1 = new SmartPoint(10, 10);
SmartPoint pt2 = new SmartPoint(10, 10);
SmartPoint pt3 = new SmartPoint(12, 19);
SmartPoint pt4 = pt1;
System.out.println("pt1 == pt1: " + (pt1 == pt1));
System.out.println("pt1.equals(pt1): " + pt1.equals(pt1));
System.out.println("pt1 == pt2: " + (pt1 == pt2));
System.out.println("pt1.equals(pt2): " + pt1.equals(pt2));
System.out.println("pt1 == pt3: " + (pt1 == pt3));
System.out.println("pt1.equals(pt3): " + pt1.equals(pt3));
System.out.println("pt1 == pt4: " + (pt1 == pt4));
System.out.println("pt1.equals(pt4): " + pt1.equals(pt4));
}
}
pt1 == pt1: true
pt1.equals(pt1): true
pt1 == pt2: false
pt1.equals(pt2): true
pt1 == pt3: false
pt1.equals(pt3): false
pt1 == pt4: true
pt1.equals(pt4): true
Listing 11-4A Test Class to Demonstrate the Difference Between Identity and State Comparisons
在你的类中有一些实现equals()方法的规范,所以当与 Java 的其他领域(例如,基于散列的集合)一起使用时,你的类将正确地工作。实施这些规范是类设计者的责任。如果您的类不符合这些规范,Java 编译器或 Java 运行时将不会生成任何错误。相反,你的类的对象将会行为不正确。例如,您将对象添加到集合中,但您可能无法检索它。下面是equals()方法实现的规范。假设x、y和z是三个对象的非空引用:
-
反身性:应该是反身性。表达式
x.equals(x)应该返回true。也就是说,一个对象必须等于它自己。 -
对称:应该是对称的。如果
x.equals(y)返回true,y.equals(x)必须返回true。也就是说,如果 x 等于y,y一定等于 x -
传递性:应该是传递性的。如果
x.equals(y)返回true,y.equals(z)返回true,x.equals(z)必须返回true。也就是说,如果x等于y,y等于z,x一定等于z。 -
一致性:应该是一致的。如果
x.equals(y)返回true,它应该一直返回true,直到 x 或 y 的状态被修改。如果x.equals(y)返回false,它应该一直返回false,直到 x 或 y 的状态被修改。 -
与空引用的比较:任何类的对象都不应该等于
null引用。表达式x.equals(null)应该总是返回false。 -
与 hashCode()方法的关系:如果
x.equals(y)返回true,x.hashCode()必须返回与y.hashCode()相同的值。也就是说,如果根据equals()方法,两个对象相等,那么它们必须具有从它们的hashCode()方法返回的相同散列码值。然而,事实可能恰恰相反。如果两个对象有相同的散列码,这并不意味着根据equals()方法它们必须相等。也就是说,如果x.hashCode()等于y.hashCode(),这并不意味着x.equals(y)将返回true。
您的SmartPoint类满足了equals()和hashCode()方法的所有六条规则。为SmartPoint类实现equals()方法相当容易。它有两个原始类型的实例变量,您在比较中使用了这两个变量。
对于应该使用多少实例变量来比较一个类的两个对象的相等性,没有规则。这完全取决于类的用途。例如,如果您有一个Account类,帐号本身就足以比较两个Account对象的相等性。但是,确保在equals()方法中使用相同的实例变量来比较相等性,在hashCode()方法中使用相同的实例变量来计算散列代码值。如果你的类有引用实例变量,你可以从你的类的equals()方法中调用它们的equals()方法。清单 11-5 展示了如何在equals()方法中使用引用实例变量比较。
// SmartCat.java
package com.jdojo.object;
public class SmartCat {
private String name;
public SmartCat(String name) {
this.name = name;
}
/* Reimplement the equals() method */
@Override
public boolean equals(Object otherObject) {
// Are they the same?
if (this == otherObject) {
return true;
}
// Is otherObject a null reference?
if (otherObject == null) {
return false;
}
// Do they belong to the same class?
if (this.getClass() != otherObject.getClass()) {
return false;
}
// Get the reference of otherObject is a SmartCat variable
SmartCat otherCat = (SmartCat)otherObject;
// Do they have the same names
boolean isSameName = (this.name == null ? otherCat.name == null
: this.name.equals(otherCat.name) );
return isSameName;
}
/* Reimplement the hashCode() method, which is a requirement
when you reimplement equals() method
*/
@Override
public int hashCode() {
return (this.name == null ? 0 : this.name.hashCode());
}
}
Listing 11-5Overriding the equals() and hashCode() Methods in a Class
SmartCat类有一个name实例变量,它的类型是String。String类有自己版本的equals()方法实现,可以逐个字符地比较两个字符串。SmartCat类的equals()方法调用name实例变量上的equals()方法来检查两个名称是否相等。类似地,它在其hashCode()方法中利用了String类中hashCode()方法的实现。
对象的字符串表示形式
一个对象由它的状态来表示,状态是在某个时间点上它的所有实例变量的值的组合。有时,通常在调试中,以字符串形式表示对象是有帮助的。表示对象的字符串中应该有什么?对象的字符串表示应该包含足够的可读格式的对象状态信息。Object类的toString()方法允许您编写自己的逻辑,将类的对象表示为字符串。Object类提供了toString()方法的默认实现。它返回以下格式的字符串:
<fully-qualified-class-name>@<hash-code-of-object-in-hexadecimal-format>
考虑下面的代码片段及其输出。您可能会得到不同的输出:
// Create two objects
Object obj = new Object();
IntHolder intHolder = new IntHolder(234);
// Get string representation of objects
String objStr = obj.toString();
String intHolderStr = intHolder.toString();
// Print the string representations
System.out.println(objStr);
System.out.println(intHolderStr);
java.lang.Object@360be0
com.jdojo.object.IntHolder@45a877
注意,IntHolder类没有toString()方法。尽管如此,您仍然能够使用intHolder引用变量调用toString()方法,因为Object类中的所有方法在所有类中都自动可用。
您可能会注意到,从toString()方法返回的用于IntHolder对象的字符串表示不是很有用。它不会给你任何关于IntHolder物体状态的线索。让我们在IntHolder类中重新实现toString()方法。您将调用新类SmartIntHolder。你的toString()方法应该返回什么?SmartIntHolder类的一个对象代表一个整数值。将存储的整数值作为字符串返回就可以了。您可以使用String类的valueOf()静态方法将一个整数值(比如说123)转换成一个String对象,如下所示:
String str = String.valueOf(123); // str contains "123" as a string
清单 11-6 包含了SmartIntHolder类的完整代码。
// SmartIntHolder.java
package com.jdojo.object;
public class SmartIntHolder {
private int value;
public SmartIntHolder(int value) {
this.value = value;
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
/* Reimplement toString() method of the Object class */
@Override
public String toString() {
// Return the stored value as a string
String str = String.valueOf(this.value);
return str;
}
}
Listing 11-6Reimplementing the toString() Method of the Object Class in the SmartIntHolder Class
以下代码片段向您展示了如何使用SmartIntHolder类的toString()方法:
// Create an object of the SmartIntHolder class
SmartIntHolder intHolder = new SmartIntHolder(234);
String intHolderStr = intHolder.toString();
System.out.println(intHolderStr);
// Change the value in SmartIntHolder object
intHolder.setValue(8967);
intHolderStr = intHolder.toString();
System.out.println(intHolderStr);
234
8967
在类中重新实现toString()方法没有特殊的技术要求。您需要确保它被声明为public,它的返回类型是String,并且不带任何参数。返回的字符串应该是人类可读的文本,以便在调用方法时给出对象状态的概念。建议在您创建的每个类中重新实现Object类的toString()方法。
假设您有一个Point类来表示一个 2D 点,如清单 11-7 所示。一个Point保存一个点的 x 和 y 坐标。Point类中的toString()方法的实现可以返回一个(x, y)形式的字符串,其中 x 和 y 是点的坐标。
// Point.java
package com.jdojo.object;
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
/* Reimplement toString() method of the Object class */
@Override
public String toString() {
String str = "(" + x + ", " + y + ")";
return str;
}
}
Listing 11-7A Point Class Whose Object Represents a 2D Point
类的toString()方法非常重要,Java 为你提供了使用它的简单方法。在需要对象的字符串表示的情况下,Java 会自动调用对象的toString()方法。值得一提的两种情况是
-
涉及对象引用的字符串串联表达式
-
使用对象引用作为参数调用
System.out.print()和System.out.println()方法
当你像这样连接一个字符串和一个对象时
String str = "Hello" + new Point(10, 20);
Java 在Point对象上调用toString()方法,并将返回值连接到"Hello"字符串。这条语句将把一个"Hello(10, 20)"字符串赋给str变量。该语句与下面的语句相同:
String str = "Hello" + new Point(10, 20).toString();
您可以使用字符串连接运算符(+)来连接不同类型的数据。首先,Java 在连接数据之前获取所有数据的字符串表示。在串联表达式中自动调用对象的toString()方法可以帮助您节省一些输入。如果串联中使用的对象引用是一个null引用,Java 使用一个"null"字符串作为字符串表示。
下面的代码片段清楚地说明了对对象引用的toString()方法的调用。当您单独使用对象的引用或者在字符串连接表达式中调用它的toString()方法时,您可能会发现结果是相同的。类似地,当您使用System.out.println(pt)时,Java 会自动调用pt引用变量上的toString()方法:
Point pt = new Point(10, 12);
// str1 and str2 will have the same contents
String str1 = "Test " + pt;
String str2 = "Test " + pt.toString();
System.out.println(pt);
System.out.println(pt.toString());
System.out.println(str1);
System.out.println(str2);
(10, 12)
(10, 12)
Test (10, 12)
Test (10, 12)
下面的代码片段展示了在字符串连接表达式和System.out.println()方法调用中使用null引用的效果。注意,当pt持有null引用时,不能使用pt.toString()。对null引用的任何方法的调用都会产生运行时异常:
// Set pt to null
Point pt = null;
String str3 = "Test " + pt;
System.out.println(pt);
System.out.println(str3);
//System.out.println(pt.toString()); /* Will generate a runtime exception */
null
Test null
克隆对象
Java 不提供自动机制来克隆(复制)对象。回想一下,当您将一个引用变量赋给另一个引用变量时,只复制对象的引用,而不是对象的内容。克隆一个对象意味着一点一点地复制对象的内容。如果你想克隆你的类的对象,你必须在你的类中重新实现clone()方法。一旦你重新实现了clone()方法,你应该能够通过调用clone()方法来克隆你的类的对象。Object类中的clone()方法声明如下:
protected Object clone() throws CloneNotSupportedException
您需要注意一些关于clone()方法声明的事情:
-
声明为
protected。因此,您将无法从客户端代码中调用它。以下代码无效: -
这意味着如果你想让客户端代码克隆你的类的对象,你需要在你的类中声明方法。
-
它的返回类型是
Object。这意味着您需要转换clone()方法的返回值。假设MyClass是可克隆的。您的克隆代码将如下所示:
Object obj = new Object();
Object clone = obj.clone(); // Error. Cannot access protected clone()
// method
MyClass mc = new MyClass();
MyClass clone = (MyClass)mc.clone(); // Need to use a cast
克隆对象不需要知道对象的任何内部细节。Object类中的clone()方法拥有克隆一个对象所需的所有代码。你需要做的就是从你的类的clone()方法中调用它。它将按位复制原始对象,并返回副本的引用。
Object类中的clone()方法抛出一个CloneNotSupportedException。这意味着当您调用Object类的clone()方法时,您需要将调用放在try-catch块中或者重新抛出异常。您将在第十三章中了解更多关于try-catch模块的信息。您可以选择不从您的类的clone()方法中抛出CloneNotSupportedException。以下代码片段放在您的类的clone()方法中,该方法使用super关键字调用Object类的clone()方法:
YourClass obj = null;
try {
// Call clone() method of the Object class using super.clone()
obj = (YourClass)super.clone();
} catch (CloneNotSupportedException e) {
e. printStackTrace();
}
return obj;
您必须做的一件重要事情是在您的类声明中添加一个implements Cloneable子句。Cloneable是在java.lang包中声明的接口。你会在第二十一章中了解到接口。现在,只需在类声明中添加这个子句。否则,当您在您的类的对象上调用clone()方法时,您将得到一个运行时错误。您的类声明必须如下所示:
public class MyClass implements Cloneable {
// Code for your class goes here
}
清单 11-8 包含了一个DoubleHolder类的完整代码。它覆盖了Object类的clone()方法。clone()方法中的注释解释了代码在做什么。DoubleHolder类的clone()方法没有throws子句,而Object类的clone()方法有。当您覆盖一个方法时,您可以选择删除在超类中声明的throws子句。
// DoubleHolder.java
package com.jdojo.object;
public class DoubleHolder implements Cloneable {
private double value;
public DoubleHolder(double value) {
this.value = value;
}
public void setValue(double value) {
this.value = value;
}
public double getValue() {
return this.value;
}
@Override
public Object clone() {
DoubleHolder copy = null;
try {
// Call the clone() method of the Object class, which will do a
// bit-by-bit copy and return the reference of the clone
copy = (DoubleHolder) super.clone();
} catch (CloneNotSupportedException e) {
// If anything goes wrong during cloning, print the error details
e.printStackTrace();
}
return copy;
}
}
Listing 11-8A DoubleHolder Class with Cloning Capability
一旦您的类正确实现了clone()方法,克隆您的类的对象就像调用它的clone()方法一样简单。下面的代码片段展示了如何克隆一个DoubleHolder类的对象。注意,您必须将从dh.clone()方法调用返回的引用转换为DoubleHolder类型:
DoubleHolder dh = new DoubleHolder(100.00);
DoubleHolder dhClone = (DoubleHolder) dh.clone();
此时,DoubleHolder类有两个独立的对象。dh变量引用原始对象,dhClone变量引用原始对象的克隆。原始对象和克隆对象持有相同的值100.00。但是,它们有该值的单独副本。如果更改原始对象中的值,例如dh.setValue(200),克隆对象中的值保持不变。清单 11-9 展示了如何使用clone()方法克隆一个DoubleHolder类的对象。输出证明,一旦克隆了一个对象,内存中就会有两个独立的对象。
// CloningTest.java
package com.jdojo.object;
public class CloningTest {
public static void main(String[] args) {
DoubleHolder dh = new DoubleHolder(100.00);
// Clone dh
DoubleHolder dhClone = (DoubleHolder)dh.clone();
// Print the values in original and clone
System.out.println("Original:" + dh.getValue());
System.out.println("Clone:" + dhClone.getValue());
// Change the value in original and clone
dh.setValue(200.00);
dhClone.setValue(400.00);
// Print the values in original and clone again
System.out.println("Original:" + dh.getValue());
System.out.println("Clone :" + dhClone.getValue());
}
}
Original:100.0
Clone:100.0
Original:200.0
Clone:400.0
Listing 11-9A Test Class to Demonstrate Object Cloning
在 Java 5 中,您不需要在您的类中将clone()方法的返回类型指定为Object类型。您可以在clone()方法声明中将您的类指定为返回类型。这不会强制客户端代码在调用您的类的clone()方法时使用强制转换。下面的代码片段显示了DoubleHolder类的变更代码,它只能在 Java 5 或更高版本中编译。它将DoubleHolder声明为clone()方法的返回类型,并在return语句中使用强制转换:
// DoubleHolder.java
package com.jdojo.object;
public class DoubleHolder implements Cloneable {
/* The same code goes here as before... */
public DoubleHolder clone() {
Object copy = null;
/* The same code goes here as before... */
return (DoubleHolder)copy;
}
}
使用前面对clone()方法的声明,您可以编写代码来克隆一个对象,如下所示。请注意,不再需要强制转换:
DoubleHolder dh = new DoubleHolder(100.00);
DoubleHolder dhClone = dh.clone();// Clone dh. No cast is needed
一个对象可能由另一个对象组成。在这种情况下,内存中分别存在两个对象——一个被包含的对象和一个容器对象。容器对象存储被包含对象的引用。克隆容器对象时,会克隆所包含对象的引用。克隆完成后,容器对象有两个副本;它们都引用了同一个包含的对象。这被称为浅克隆,因为复制的是引用,而不是对象。Object类的clone()方法只进行浅层克隆,除非你另外编码。图 11-2 显示了一个复合对象的内存状态,其中一个对象包含另一个对象的引用。图 11-3 显示了使用浅层克隆来克隆复合对象时的内存状态。您可能会注意到,在浅层克隆中,包含的对象由原始复合对象和克隆的复合对象共享。
图 11-3
使用浅层克隆克隆容器对象后的内存状态
图 11-2
复合对象。容器对象存储另一个对象(被包含的对象)的引用
在克隆复合对象的过程中,如果复制的是包含的对象,而不是它们的引用,则称为深度克隆。您必须克隆一个对象的所有引用变量所引用的所有对象,以获得深度克隆。一个复合对象可以有多个层次的被包含对象链接。例如,容器对象可能具有对另一个被包含对象的引用,而后者又具有对另一个被包含对象的引用,依此类推。您是否能够执行复合对象的深度克隆取决于许多因素。如果你有一个被包含对象的引用,它可能不支持深度克隆;在这种情况下,你必须满足于浅层克隆。你可能有一个被包含对象的引用,它本身就是一个复合对象。然而,包含的对象只支持浅层克隆,在这种情况下,您将不得不满足于浅层克隆。让我们看看浅层克隆和深层克隆的例子。
如果一个对象的引用实例变量存储了对不可变对象的引用,您不需要克隆它们。也就是说,如果一个复合对象包含的对象是不可变的,您不需要克隆包含的对象。在这种情况下,不可变包含对象的浅拷贝是好的。回想一下,不可变对象在创建后就不能修改了。不可变对象的引用可以被多个对象共享,而不会有任何副作用。这是拥有不可变对象的好处之一。如果一个复合对象包含一些对可变对象的引用和一些对不可变对象的引用,那么您必须克隆被引用的可变对象以拥有一个深度副本。清单 11-10 有一个ShallowClone类的代码。
// ShallowClone.java
package com.jdojo.object;
public class ShallowClone implements Cloneable {
private DoubleHolder holder = new DoubleHolder(0.0);
public ShallowClone(double value) {
this.holder.setValue(value);
}
public void setValue(double value) {
this.holder.setValue(value);
}
public double getValue() {
return this.holder.getValue();
}
@Override
public Object clone() {
ShallowClone copy = null;
try {
copy = (ShallowClone) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return copy;
}
}
Listing 11-10A ShallowClone Class That Supports Shallow Cloning
ShallowClone类的对象由DoubleHolder类的对象组成。ShallowClone类的clone()方法中的代码与DoubleHolder类的clone()方法中的代码相同。区别在于用于这两个类的实例变量的类型。DoubleHolder类有一个原始类型double的实例变量,而ShallowClone类有一个引用类型DoubleHolder的实例变量。当ShallowClone类调用Object类的clone()方法(使用super.clone())时,它接收自身的浅层副本。也就是说,它与其克隆体共享其实例变量中使用的DoubleHolder对象。
清单 11-11 有测试用例来测试ShallowClone类的一个对象及其克隆。输出显示,克隆后,通过原始对象更改值也会更改克隆对象中的值。这是因为ShallowClone对象将值存储在DoubleHolder类的另一个对象中,该对象由克隆对象和原始对象共享。
// ShallowCloneTest.java
package com.jdojo.object;
public class ShallowCloneTest {
public static void main(String[] args) {
ShallowClone sc = new ShallowClone(100.00);
ShallowClone scClone = (ShallowClone) sc.clone();
// Print the value in original and clone
System.out.println("Original: " + sc.getValue());
System.out.println("Clone: " + scClone.getValue());
// Change the value in original and it will change the value
// for clone too because we have done shallow cloning
sc.setValue(200.00);
// Print the value in original and clone
System.out.println("Original: " + sc.getValue());
System.out.println("Clone: " + scClone.getValue());
}
}
Original: 100.0
Clone: 100.0
Original: 200.0
Clone: 200.0
Listing 11-11A Test Class to Demonstrate the Shallow Copy Mechanism
在深度克隆中,需要克隆一个对象的所有引用实例变量引用的所有对象。您必须先执行浅层克隆,然后才能执行深层克隆。浅层克隆是通过调用Object类的clone()方法来执行的。然后,您需要编写代码来克隆所有引用实例变量。清单 11-12 展示了一个DeepClone类的代码,它执行深度克隆。
// DeepClone.java
package com.jdojo.object;
public class DeepClone implements Cloneable {
private DoubleHolder holder = new DoubleHolder(0.0);
public DeepClone(double value) {
this.holder.setValue(value);
}
public void setValue(double value) {
this.holder.setValue(value);
}
public double getValue() {
return this.holder.getValue();
}
@Override
public Object clone() {
DeepClone copy = null;
try {
copy = (DeepClone) super.clone();
// Need to clone the holder reference variable too
copy.holder = (DoubleHolder) this.holder.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return copy;
}
}
Listing 11-12A DeepClone Class That Performs Deep Cloning
如果你比较一下ShallowClone和DeepClone类的clone()方法中的代码,你会发现,对于深度克隆,你只需要多写一行代码:
// Need to clone the holder reference variable too
copy.holder = (DoubleHolder)this.holder.clone();
如果DoubleHolder类不可克隆会怎么样?在这种情况下,您将无法编写这个语句来克隆holder实例变量。您可以克隆 holder 实例变量,如下所示:
// Need to clone the holder reference variable too
copy.holder = new DoubleHolder(this.holder.getValue());
目标是克隆holder实例变量,并且不一定要通过调用它的clone()方法来完成。清单 11-13 展示了你的DeepClone类是如何工作的。将它的输出与ShallowCloneTest类的输出进行比较,看看有什么不同。
// DeepCloneTest.java
package com.jdojo.object;
public class DeepCloneTest {
public static void main(String[] args) {
DeepClone sc = new DeepClone(100.00);
DeepClone scClone = (DeepClone) sc.clone();
// Print the value in original and clone
System.out.println("Original: " + sc.getValue());
System.out.println("Clone: " + scClone.getValue());
// Change the value in original and it will not change the value
// for clone because we have done deep cloning
sc.setValue(200.00);
// Print the value in original and clone
System.out.println("Original: " + sc.getValue());
System.out.println("Clone: " + scClone.getValue());
}
}
Original: 100.0
Clone: 100.0
Original: 200.0
Clone: 100.0
Listing 11-13A Test Class to Test Deep Cloning of Objects
Tip
使用Object类的clone()方法并不是克隆一个对象的唯一方法。您可以使用其他方法来克隆对象。您可以提供一个复制构造器,它接受同一个类的对象并创建该对象的克隆。你可以在你的类中提供一个工厂方法,它可以接受一个对象并返回它的克隆。克隆对象的另一种方法是先序列化它,然后再反序列化它。这里不讨论对象的序列化和反序列化。
完成一个对象
有时,当对象被销毁时,该对象会使用需要释放的资源。当一个对象将要被销毁时,Java 为您提供了一种执行资源释放或其他类型的清理的方法。在 Java 中,你可以创建对象,但不能销毁对象。JVM 运行一个名为垃圾收集器的低优先级特殊任务来销毁所有不再被引用的对象。垃圾收集器让您有机会在对象被销毁之前执行清理代码。Object类有一个finalize()方法,声明如下:
protected void finalize() throws Throwable { }
Object类中的finalize()方法不做任何事情。您需要在类中重写该方法。在你的类的一个对象被销毁之前,你的类的finalize()方法将被垃圾收集器调用。清单 11-14 包含了Finalize类的代码。它覆盖了Object类的finalize()方法,并在标准输出中输出一条消息。您可以在此方法中执行任何清理逻辑。finalize()方法中的代码也被称为终结器。
// Finalize.java
package com.jdojo.object;
public class Finalize {
private int x;
public Finalize(int x) {
this.x = x;
}
@Override
public void finalize() {
System.out.println("Finalizing " + this.x);
/* Perform any cleanup work here... */
}
}
Listing 11-14A Finalize Class That Overrides the finalize() Method of the Object Class
垃圾回收器只为每个对象调用一次终结器。为对象运行终结器并不一定意味着该对象将在终结器完成后立即被销毁。当垃圾回收器确定不存在对该对象的引用时,将运行终结器。但是,当一个对象的终结器运行时,它可能会将自己的引用传递给程序的其他部分。这就是垃圾收集器在运行一个对象的终结器后再检查一次以确保该对象不存在引用,然后销毁(释放内存)该对象的原因。没有指定终结器的运行顺序和运行时间。甚至不能保证终结器会运行。这使得程序员在finalize()方法中编写清理逻辑变得不可靠。有更好的方法来执行清理逻辑,例如,使用一个try-finally块。建议不要依赖 Java 程序中的finalize()方法来清理对象使用的资源。
Tip
从 Java 9 开始,Object类中的finalize()方法就被弃用了,因为使用finalize()方法清理资源本身就存在问题。有几种更好的方法来清理资源,例如,使用try-with-resources和try-finally块。我们将在本书的第十三章讨论这些技术。为了完整起见,我们在本章中已经介绍了finalize()方法。
清单 11-15 包含测试你的Finalize类的终结器的代码。运行这个程序时,您可能会得到不同的输出。
// FinalizeTest.java
package com.jdojo.object;
public class FinalizeTest {
public static void main(String[] args) {
// Create many objects, say 2000000 objects.
for(int i = 0; i < 2000000; i++) {
new Finalize(i);
}
}
}
Finalizing 977620
Finalizing 977625
Finalizing 977627
Listing 11-15A Test Class to Test Finalizers
该程序创建了 2000000 个Finalize类的对象,但没有存储它们的引用。不要存储您创建的对象的引用,这一点很重要。只要持有对象的引用,它就不会被销毁,它的终结器也不会运行。从输出中可以看到,在程序完成之前,只有三个对象有机会运行它们的终结器。您可能根本得不到输出,或者得到不同的输出。如果没有得到任何输出,可以通过增加要创建的对象的数量来尝试。当垃圾收集器感觉内存不足时,它会销毁对象。您可能需要创建更多的对象来触发垃圾回收,这反过来将运行您的对象的终结器。
不可变对象
一个对象的状态在被创建后不能被改变,这个对象被称为不可变对象。对象不可变的类称为不可变类。如果一个对象的状态在它被创建后可以被改变(或变异),它被称为可变对象,它的类被称为可变类。
在我们进入创建和使用不可变对象的细节之前,让我们定义一下“不变性”这个词对象的实例变量定义了对象的状态。对象的状态有两种视图:内部和外部。对象的内部状态由其实例变量在某个时间点的实际值来定义。对象的外部状态由对象的用户(或客户端)在某个时间点看到的值定义。当我们声明一个对象是不可变的时,我们必须明确我们指的是对象的哪种状态是不可变的:内部状态、外部状态,或者两者都是。
通常,当我们在 Java 中使用短语“不可变对象”时,我们指的是外部不变性。在外部不变性中,对象可以在创建后改变其内部状态。但是,外部用户看不到其内部状态的变化。创建后,用户看不到其状态的任何变化。在内部不变性中,对象的状态在创建后不会改变。如果一个对象是内部不可变的,那么它也是外部不可变的。我们讨论两者的例子。
不可变对象比可变对象有几个优点。不可变对象可以被程序的不同区域共享,而不用担心它的状态变化。测试一个不可变的类很容易。不可变对象本质上是线程安全的。您不必从多个线程同步对不可变对象的访问,因为它的状态不会改变。有关线程同步的更多细节,请参考本系列的第二卷。不可变对象不必被复制并传递到同一个 Java 应用程序中的另一个程序区域,因为它的状态不会改变。你只需要传递它的引用,它就可以作为一个副本。它的引用可以用来访问它的内容。避免复制是一个很大的性能优势,因为它节省了时间和空间。
让我们从一个可变类开始,它的对象的状态可以在创建后修改。清单 11-16 包含一个IntHolder类的代码。
// IntHolder.java
package com.jdojo.object;
public class IntHolder {
private int value;
public IntHolder(int value) {
this.value = value;
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Listing 11-16An Example of a Mutable Class Whose Object’s State Can Be Changed After Creation
value实例变量定义了一个IntHolder对象的状态。您创建了一个IntHolder类的对象,如下所示:
IntHolder holder = new IntHolder(101);
int v = holder.getValue(); // Stores 101 in v
此时,value实例变量持有101,定义其状态。您可以使用 getter 和 setter 来获取和设置实例变量:
// Change the value
holder.setValue(505);
int w = holder.getValue(); // Stores 505 in w
此时,value实例变量已经从101变成了505。也就是说,对象的状态已经改变。通过setValue()方法促进了状态的改变。IntHolder类的对象是可变对象的例子。
让我们让IntHolder类成为不可变的。您所需要做的就是从其中移除setValue()方法,使其成为一个不可变的类。让我们把你的不可变版本的IntHolder类称为IntWrapper,如清单 11-17 所示。
// IntWrapper.java
package com.jdojo.object;
public class IntWrapper {
private final int value;
public IntWrapper(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Listing 11-17An Example of an Immutable Class
这就是如何创建一个IntWrapper类的对象:
IntWrapper wrapper = new IntWrapper(101);
此时,wrapper对象持有101,没有办法改变。因此,IntWrapper类是一个不可变的类,它的对象是不可变的对象。您可能已经注意到对IntHolder类做了两处修改,将其转换为IntWrapper类。移除了setValue()方法,并制作了value实例变量final。在这种情况下,没有必要使value实例变量final。使用final关键字可以让类的读者清楚你的意图,并且它可以保护value实例变量不被无意中更改。声明定义对象final的不可变状态的所有实例变量是一个好的实践(作为一个经验法则),这样 Java 编译器将在编译期间实施不变性。IntWrapper类的对象在内部和外部都是不可变的。一旦创建,就无法更改其状态。
让我们创建一个IntWrapper类的变体,它将是外部不可变但内部可变的。姑且称之为IntWrapper2。在清单 11-18 中列出。
// IntWrapper2.java
package com.jdojo.object;
public class IntWrapper2 {
private final int value;
private int halfValue = Integer.MAX_VALUE;
public IntWrapper2(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public int getHalfValue() {
// Compute half value if it is not already computed
if (this.halfValue == Integer.MAX_VALUE) {
// Cache the half value for future use
this.halfValue = this.value / 2;
}
return this.halfValue;
}
}
Listing 11-18An Example of an Externally Immutable and Internally Mutable Class
IntWrapper2添加了另一个名为halfValue的实例变量,它将保存传递给构造器的值的一半。这是一个微不足道的例子。然而,它的目的是解释外部和内部不可变对象的含义。假设(只是为了讨论起见)计算整数的一半是一个非常昂贵的过程,你不想在IntWrapper2类的构造器中计算它,尤其是如果不是每个人都要求这样做的话。halfValue实例变量被初始化为最大的整数值,这是一个标志,表明它还没有被计算。您添加了一个getHalfValue()方法,它检查您是否已经计算了一半的值。第一次,它会计算一半的值并缓存在halfValue实例变量中。从第二次开始,它将简单地返回缓存的值。
问题是,“一个IntWrapper2对象是不可变的吗?”答案是肯定和否定的。它是内部可变的。然而,它是外部不可变的。一旦它被创建,它的客户端将从getValue()和getHalfValue()方法中看到相同的返回值。然而,当第一次调用getHalfValue()方法时,它的状态(具体来说是halfValue)在其生命周期中会改变一次。但是,该对象的用户看不到此更改。此方法在所有后续调用中返回相同的值。像IntWrapper2这样的对象叫做不可变对象。回想一下,不可变对象通常意味着外部不可变。
Java 类库中的String类就是不可变类的一个例子。它使用了针对IntWrapper2类讨论的缓存技术。当第一次调用hashCode()方法时,String类为其内容计算散列码,并缓存该值。因此,String对象在内部改变它的状态,但不是为它的客户端。你不会遇到“Java 中的一个String对象是外部不可变和内部可变的”这种说法相反,你会遇到“Java 中的一个String对象是不可变的”这种说法你应该明白这意味着String对象至少是外部不可变的。
清单 11-19 显示了一个棘手的情况,试图创建一个不可变的类。IntHolderWrapper类没有可以直接让您修改存储在其valueHolder实例变量中的值的方法。它似乎是一个不可变的类。
// IntHolderWrapper.java
package com.jdojo.object;
public class IntHolderWrapper {
private final IntHolder valueHolder;
public IntHolderWrapper(int value) {
this.valueHolder = new IntHolder(value);
}
public IntHolder getIntHolder() {
return this.valueHolder;
}
public int getValue() {
return this.valueHolder.getValue();
}
}
Listing 11-19An Unsuccessful Attempt to Create an Immutable Class
清单 11-20 包含一个测试类来测试IntHolderWrapper类的不变性。
// BadImmutableTest.java
package com.jdojo.object;
public class BadImmutableTest {
public static void main(String[] args) {
IntHolderWrapper ihw = new IntHolderWrapper(101);
int value = ihw.getValue();
System.out.println("#1 value = " + value);
IntHolder holder = ihw.getIntHolder();
holder.setValue(207);
value = ihw.getValue();
System.out.println("#2 value = " + value);
}
}
#1 value = 101
#2 value = 207
Listing 11-20A Test Class to Test Immutability of the IntHolderWrapper Class
输出显示IntHolderWrapper类是可变的。对其getValue()方法的两次调用返回不同的值。罪魁祸首是它的getIntHolder()法。它返回valueHolder实例变量,这是一个引用变量。注意,valueHolder实例变量代表了一个IntHolder类的对象,它构成了一个IntHolderWrapper对象的状态。如果valueHolder引用变量引用的对象发生变化,IntHolderWrapper的状态也会发生变化。由于IntHolder对象是可变的,您不应该从getIntHolder()方法返回它对客户机的引用。以下两条语句从客户端代码更改对象的状态:
IntHolder holder = ihw.getIntHolder(); /* Got hold of instance variable */
holder.setValue(207); /* Change the state by changing the instance variable's state */
请注意,IntHolderWrapper类的设计者在返回valueHolder引用时忽略了一点,即即使没有直接的方法来改变IntHolderWrapper类的状态,也可以间接改变。
你如何改正这个问题?解决方法很简单。在getIntHolder()方法中,复制valueHolder对象并返回副本的引用,而不是实例变量本身。这样,如果客户端改变了这个值,它只会在客户端的副本中被改变,而不会在由IntHolderWrapper对象持有的副本中被改变。清单 11-21 包含了IntHolderWrapper类的正确的不可变版本,您称之为IntHolderWrapper2。
// IntHolderWrapper2.java
package com.jdojo.object;
public class IntHolderWrapper2 {
private final IntHolder valueHolder;
public IntHolderWrapper2(int value) {
this.valueHolder = new IntHolder(value);
}
public IntHolder getIntHolder() {
// Make a copy of valueHolder
int v = this.valueHolder.getValue();
IntHolder copy = new IntHolder(v);
// Return the copy instead of the original
return copy;
}
public int getValue() {
return this.valueHolder.getValue();
}
}
Listing 11-21A Modified, Immutable Version of the IntHolderWrapper Class
创建一个不可变的类比看起来要稍微复杂一些。我已经在本节中介绍了一些案例。这是另一个你需要小心的例子。假设你设计了一个不可变的类,它有一个引用类型的实例变量。假设它在其一个构造器中接受其引用类型实例变量的初始值。如果实例变量的类是可变类,则必须复制传递给其构造器的参数,并将副本存储在实例变量中。在构造器中传递对象引用的客户端代码可能会在以后通过同一引用更改该对象的状态。清单 11-22 展示了如何正确实现IntHolderWrapper3类的第二个构造器。它包含注释的第二个构造器的实现的错误版本。
// IntHolderWrapper3.java
package com.jdojo.object;
public class IntHolderWrapper3 {
private final IntHolder valueHolder;
public IntHolderWrapper3(int value) {
this.valueHolder = new IntHolder(value);
}
public IntHolderWrapper3(IntHolder holder) {
// Must make a copy of holder parameter
this.valueHolder = new IntHolder(holder.getValue());
/* Following implementation is incorrect. Client code will be able to change the
state of the object using holder reference later */
//this.valueHolder = holder; /* do not use it */
}
/* Rest of the code goes here... */
}
Listing 11-22Using a Copy Constructor to Correctly Implement an Immutable Class
Object类
JDK 在java.util包中包含一个名为Objects的实用程序类,用于处理对象。它由所有静态方法组成。Objects类的大多数方法都能优雅地处理null值。Java 9 给这个类增加了一些实用方法。Objects类中的方法根据它们执行的操作类型分为以下几类:
-
边界检查
-
比较对象
-
计算哈希代码
-
检查是否为空
-
验证参数
-
获取对象的字符串表示形式
边界检查
此类别中的方法用于检查索引或子范围是否在范围的界限内。通常,在执行涉及数组边界的操作之前,对数组使用这些方法。Java 中的数组是相同类型元素的集合。数组中的每个元素都有一个用于访问它们的索引。数组索引是从零开始的。第一个元素的索引为 0,第二个为 1,第三个为 2,依此类推。假设你有一个五个元素的数组,有人让你从索引 3 开始给他们数组的四个元素。此请求无效,因为数组索引范围是从 0 到 4,而请求的元素是从索引 3 到 6。Objects类包含以下三种执行边界检查的方法——所有这些方法都是在 Java 9 中添加的:
-
int checkFromIndexSize(int fromIndex, int size, int length) -
int checkFromToIndex(int fromIndex, int toIndex, int length) -
int checkIndex(int index, int length)
如果对索引或子范围的检查不在0到length的范围内,所有这些方法都会抛出IndexOutOfBoundsException,其中length是这些方法的参数之一。
checkFromIndexSize(int fromIndex, int size, int length)方法检查从fromIndex(含)到fromIndex + size(不含)的子范围是否在从0(含)到length(不含)的范围界限内。
checkFromToIndex(int fromIndex, int toIndex, int length)方法检查从fromIndex(含)到toIndex(不含)的子范围是否在从0(含)到length(不含)的范围界限内。
checkIndex(int index, int length)方法检查index是否在从0(含)到length(不含)的范围内。
比较对象
此类别中的方法用于比较对象以进行排序或相等。这一类别中有三种方法:
-
<T> int compare(T a, T b, Comparator<? super T> c) -
boolean deepEquals(Object a, Object b) -
boolean equals(Object a, Object b)
compare()方法用于比较两个对象以进行排序。如果两个参数相同,它将返回0。否则,它返回c.compare(a, b)的值。如果两个参数都是null,则返回0。
deepEquals()方法用于检查两个对象是否完全相等。如果两个参数完全相等,则返回true。否则,它返回false。如果两个参数都是null,则返回true。
方法比较两个对象是否相等。如果两个参数相等,它返回true。否则,它返回false。如果两个参数都是null,则返回true。如果只有一个参数是null,则返回false。
计算哈希代码
此类别中的方法用于计算一个或多个对象的哈希代码。这一类别中有两种方法:
-
int hash(Object... values) -
int hashCode(Object obj)
hash()方法在其参数中为所有指定的对象生成一个散列码。它可用于计算包含多个实例字段的对象的哈希代码。清单 11-23 包含了另一个版本的Book类。这一次,hashCode()方法使用Objects.hash()方法来计算一个Book对象的散列码。比较清单 11-2 中Book类的代码和清单 11-23 中Book2类的代码。注意使用Objects.hash()方法计算对象的散列码是多么容易。
// Book2.java
package com.jdojo.object;
import java.util.Objects;
public class Book2 {
private String title;
private String author;
private int pageCount;
private boolean hardCover;
private double price;
/* Other code goes here */
/* Must implement the equals() method too. */
@Override
public int hashCode() {
return Objects.hash(title, author, pageCount, hardCover, price);
}
}
Listing 11-23Using the Objects.hash() Method to Compute the Hash Code of an Object
如果将单个对象引用传递给Objects.hash()方法,则返回的哈希代码不等于从对象的hashCode()方法返回的哈希代码。换句话说,如果book是一个对象引用,book.hashCode()不等于Objects.hash(book)。
Objects.hashCode(Object obj)方法返回指定对象的散列码值。如果参数是null,则返回0。
检查是否为空
这类方法用于检查对象是否为空。这一类别中有两种方法:
-
boolean isNull(Object obj) -
boolean nonNull(Object obj)
如果指定的对象是null,则isNull()方法返回true。否则返回false。还可以使用比较运算符==检查一个对象是否为空,例如obj为null则obj == null返回true。Java 8 中加入了isNull()方法。它的存在是为了像Objects::isNull一样被用作方法引用。
nonNull()方法执行与isNull()方法相反的检查。它也被添加到 Java 8 中,像Objects::nonNull一样被用作方法引用。
验证参数
这个类别中的方法用于验证构造器和方法的required参数。您可以通过使用if语句编写几行代码来实现的目标,您可以使用这些方法在一行代码中实现。这一类别中有五种方法:
-
<T> T requireNonNull(T obj) -
<T> T requireNonNull(T obj, String message) -
<T> T requireNonNull(T obj, Supplier<String> messageSupplier) -
<T> T requireNonNullElse(T obj, T defaultObj) -
<T> T requireNonNullElseGet(T obj, Supplier<? extends T> supplier)
requireNonNull(T obj)方法检查参数是否不是null。如果参数是null,它抛出一个NullPointerException。此方法设计用于验证方法和构造器的参数。注意方法声明中的形式类型参数<T>。这使它成为一个泛型方法;任何类型的对象都可以作为参数传递给此方法。它的返回类型与传递的对象的类型相同。该方法被重载。该方法的第二个版本允许您为参数为null时抛出的NullPointerException指定消息。该方法的第三个版本将一个Supplier<String>作为第二个参数。它将消息的创建推迟到执行空值检查之后。如果第一个参数是null,则调用Supplier<String>对象的get()方法来获取在NullPointerException中使用的错误消息。使用供应商延迟了错误消息的构造,并且还为您提供了更多的选项,比如在错误消息中添加时间戳。
Java 9 在Objects类中添加了requireNonNullElse()和requireNonNullElseGet()方法。requireNonNullElse()方法返回第一个参数,如果它不是null;否则,如果第二个参数不是null,则返回第二个参数。如果两个参数都为空,它抛出一个NullPointerException。requireNonNullElseGet()方法返回第一个参数,如果它不是null;否则,返回从supplier的get()方法返回的 not null值。如果第一个参数是null并且供应商为空或者供应商返回null,则抛出一个NullPointerException。
获取对象的字符串表示形式
此类别中的方法用于获取对象的字符串表示形式。这一类别中有两种方法:
-
String toString(Object o) -
String toString(Object o, String nullDefault)
如果参数是null,toString()方法返回一个“null”字符串。对于非空参数,它返回通过调用参数的toString()方法返回的值。方法的第二个版本允许您在参数为 null 时指定默认返回的字符串。
使用Object类
清单 11-24 展示了如何使用Objects类的一些方法。程序使用 lambda 表达式创建一个Supplier<String>。Lambda 表达式在本书第二十一章详细讨论。这里用它来给你一个完整的例子。简而言之,lambda 表达式使用以下语法:参数 -> 表达式。如果 lambda 表达式没有参数,则使用以下语法:() -> 表达式。
// ObjectsTest.java
package com.jdojo.object;
import java.time.Instant;
import java.util.Objects;
import java.util.function.Supplier;
public class ObjectsTest {
public static void main(String[] args) {
// Compute hash code for two integers, a char, and a string
int hash = Objects.hash(10, 8900, '\u20b9', "Hello");
System.out.println("Hash Code is " + hash);
// Test for equality
boolean isEqual = Objects.equals(null, null);
System.out.println("null is equal to null: " + isEqual);
isEqual = Objects.equals(null, "XYZ");
System.out.println("null is equal to XYZ: " + isEqual);
// toString() method test
System.out.println("toString(null) is " + Objects.toString(null));
System.out.println("toString(null, \"XXX\") is " + Objects.toString(null, "XXX"));
// Testing requireNonNull(T obj, String message)
try {
printName("Doug Dyer");
printName(null);
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
// requireNonNull(T obj, Supplier<String> messageSupplier)
try {
// Using a lambda expression to create a Supplier<String> object.
// The Supplier returns a time stamped message.
Supplier<String> messageSupplier =
() -> "Name is required. Error generated on " + Instant.now();
printNameWithSupplier("Babalu", messageSupplier);
printNameWithSupplier(null, messageSupplier);
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
//<T> T requireNonNullElse(T obj, T defaultObj)
printNameWithDefault("Kishori Sharan");
// Default name "John Doe" will be used
printNameWithDefault(null);
}
public static void printName(String name) {
// Test name for not null. Generate a NullPointerException if it is null.
Objects.requireNonNull(name, "Name is required.");
// Print the name if the above statement did not throw an exception
System.out.println("Name is " + name);
}
public static void printNameWithSupplier(String name, Supplier<String> messageSupplier) {
// Test name for not null. Generate a NullPointerException if it is null.
Objects.requireNonNull(name, messageSupplier);
// Print the name if the above statement did not throw an exception
System.out.println("Name is " + name);
}
public static void printNameWithDefault(String name) {
// Test name for not null. Generate a NullPointerException if it is null.
Objects.requireNonNullElse(name, "John Doe");
// Print the name if the above statement did not throw an exception
System.out.println("Name is " + name);
}
}
Hash Code is 79643668
null is equal to null: true
null is equal to XYZ: false
toString(null) is null
toString(null, "XXX") is XXX
Name is Doug Dyer
Name is required.
Name is Babalu
Name is required. Error generated on 2017-07-29T02:44:25.974523900Z
Name is Kishori Sharan
Name is null
Listing 11-24A Test Class to Demonstrate the Use of the Methods of the Objects Class
摘要
Java 中的类是以树状层次结构排列的。树中的类具有超类-子类关系。Object类是类层次结构的根。它是 Java 中所有类的超类。Object类在java.lang包中,后者又在java.base模块中。Object类包含在所有类中自动可用的方法。有些方法已经实现,有些方法实现为空。类也可以重新实现Object类中的一些方法。Object类的引用变量可以存储 Java 中任何引用类型的引用。
加载到 JVM 中的每个类型都由一个Class<T>类的实例来表示。Object类的getClass()方法返回调用该方法的Object类型的Class<T>对象的引用。
哈希代码是使用算法为一条信息计算的整数值。哈希代码也称为哈希和、哈希值或简称为哈希。从一条信息中计算整数的算法称为哈希函数。Object类包含一个hashCode()方法,该方法返回一个int,它是对象的哈希代码。此方法的默认实现通过将对象的内存地址转换为整数来计算对象的哈希代码。因为hashCode()方法是在Object类中定义的,所以它在 Java 的所有类中都可用。但是,您可以自由地在类中重写该实现。
宇宙中的每个对象都不同于所有其他对象,Java 程序中的每个对象都不同于所有其他对象。所有对象都有唯一的标识。一个对象被分配的内存地址可以被当作它的标识,这将使它总是唯一的。如果两个对象具有相同的标识(或 Java 术语中的引用),则它们是相同的。Java 中的等号运算符(==)比较两个对象的引用,测试它们是否相等。有时,如果两个对象基于它们的一些或所有实例变量具有相同的状态,您希望将它们视为相等。如果你想基于标准而不是引用(身份)来比较你的类中的两个对象是否相等,你的类需要重新实现Object类的equals()方法。Object类中的equals()方法的默认实现比较作为参数传递的对象和调用该方法的对象的引用。
有时,通常在调试时,以字符串形式表示对象是有帮助的,字符串形式应该包含足够的可读格式的对象状态信息。Object类的toString()方法允许您编写自己的逻辑,将类的对象表示为字符串。Object类提供了toString()方法的默认实现。它返回一个字符串,该字符串包含对象的完全限定类名和十六进制格式的对象哈希代码。
克隆一个对象意味着一点一点地复制对象的内容。Java 不提供自动机制来克隆(复制)对象。如果你想要你的类的对象被克隆,你必须在你的类中重新实现Object类的clone()方法。一旦你重新实现了clone()方法,你应该能够通过调用clone()方法来克隆你的类的对象。
有时,当对象被销毁时,该对象会使用需要释放的资源。垃圾收集器通过调用对象的finalize()方法,让您有机会在对象被销毁之前执行清理代码。该方法在Object类中声明,其默认实现不做任何事情。finalize()方法中的代码也被称为终结器。您需要在您的类中重新实现finalize()方法,并编写释放资源的逻辑。finalize()方法是有问题的,在 Java 9 中已经被否决了。还有许多其他技术可以用来释放对象持有的资源。
Java 7 在java.util包中增加了一个实用程序类Objects。Java 8 和 Java 9 给这个类增加了一些方法。Objects类中的方法根据它们执行的操作类型分为以下几类:对一个范围内的索引或子范围进行边界检查、比较对象、计算哈希代码、检查空值、验证构造器和方法参数,以及获取对象的字符串表示。这个类中的大多数方法都是用来优雅地处理null值的。
EXERCISES
-
Java 中所有类的超类的全限定名是什么?
-
java.lang.Object类的超类是什么? -
说出三个在
Object类中可用的方法,并简要描述它们的用法。 -
什么是哈希码?Java 里什么时候用?
Object类中的什么方法用于返回对象的哈希代码? -
如何使用
==运算符比较两个对象? -
如果你想基于状态而不是引用来比较类中对象的相等性,那么
Object类的什么方法必须在你的类中被覆盖? -
Object类中equals()方法的默认实现是什么? -
在 Java 中,下列语句是正确的吗?
如果两个对象根据 equals(Object)方法是相等的,那么在这两个对象上调用 hashCode 方法必须产生相同的整数结果。
-
如果你的类覆盖了
Object类的equals()方法,那么Object类的哪一个方法也应该被你的类覆盖? -
Java 中的对象克隆是什么?什么是浅层克隆和深层克隆?
-
What method of the
Objectclass do you need to override in your class to allow cloning of objects of your class? Create aPhoneclass with two fields as shown:
```java
// Phone.java
package com.jdojo.object.excercise;
public class Phone {
private String areaCode;
private String number;
}
```
在`Phone`类中实现`clone()`方法,这样就可以正确地克隆`Phone`对象。类中的两个实例变量都是必需的。
12. 您需要覆盖Object类的什么方法来提供您的类的对象的字符串表示?通过实现toString()方法来增强Phone类。
-
finalize()方法在一个类中有什么用?你应该使用finalize()方法来清理你的类的对象持有的资源吗? -
什么是不可变对象和不可变类?使用不可变对象有什么好处?说出一个你经常使用的 Java 不可变类。
-
使用
Objects类的方法实现hashCode()方法和Phone类的其他方法。例如,在Phone类的构造器和方法中使用Objects类的requireNonNull()方法来验证参数值。 -
如何定义一个没有参数的 lambda 表达式,并返回字符串“Hello world”?
-
编写以下代码片段中缺少的部分,它将打印简单名称和
Phone类的完全限定名称:
```java
Phone p = new Phone();
Class cls = /* your code goes here */;
String simpleName = cls./* your code goes here */;
String fullyQualifedName = cls./* your code goes here */;
System.out.println("Simple class name: " + simpleName);
System.out.println("Fully qualified name: " + fullyQualifedName);
```
十二、包装类
在本章中,您将学习:
-
Java 中的包装类以及如何使用它们
-
如何从字符串中获取原始值
-
基元值如何在需要时自动装入包装对象
-
包装对象如何在需要时自动解装箱成原始值
本章中的所有类都是一个jdojo.wrapper模块的成员,如清单 12-1 中所声明的。
// module-info.java
module jdojo.wrapper {
exports com.jdojo.wrapper;
}
Listing 12-1The Declaration of a jdojo.wrapper Module
包装类
在前面的章节中,你已经知道了基本类型和引用类型是不兼容赋值的。您甚至不能将原始值与对象引用进行比较。Java 库的某些部分只能处理对象;例如,Java 中的集合只能处理对象。您不能创建原始值列表,如 1、3、8 和 10。您需要将原始值包装到对象中,然后才能将它们存储在列表或集合中。
自从 Java 第一次发布以来,原始值和引用值之间的赋值不兼容性就一直存在。Java 库在java.lang包中提供了八个类来代表八种基本类型中的每一种。这些类被称为包装类,,因为它们将原始值包装在一个对象中。表 12-1 列出了原语类型及其对应的包装类。注意包装类的名称。遵循 Java 命名类的惯例,它们以大写字母开头。
表 12-1
基本类型及其相应包装类的列表
|原语类型
|
包装类
|
| --- | --- |
| Byte | Byte |
| Short | Short |
| Int | Integer |
| Long | Long |
| Float | Float |
| double | Double |
| Char | Character |
| boolean | Boolean |
所有的包装类都是不可变的。它们提供了三种创建对象的方法:
-
使用构造器(已弃用)
-
使用
valueOf()工厂静态方法 -
使用
parseXxx()方法,其中Xxx是包装类的名称。它在Character级中不可用。
Tip
从 Java SE 9 开始,所有包装类中的所有构造器都被弃用,因为创建包装对象很少需要它们。被弃用意味着它们将在 Java 的未来版本中被删除。您应该使用其他方法,比如valueOf()和parseXxx()方法,来创建它们的对象。
除了Character之外,每个包装器类都至少提供了两个构造器:一个接受相应的原始类型的值,另一个接受一个String。Character类只提供了一个接受char的构造器。包装类中的所有构造器都不推荐使用,因此应该避免使用。创建包装类对象的首选方式是使用它们的valueOf()静态方法。下面的代码片段使用一些包装类的valueOf()方法创建了它们的对象:
Integer intObj1 = Integer.valueOf(100);
Integer intObj2 = Integer.valueOf("1969");
Double doubleObj1 = Double.valueOf(10.45);
Double doubleObj2 = Double.valueOf("234.60");
Character charObj1 = Character.valueOf('A');
使用valueOf()方法为整数数值(byte、short、int和long)创建对象可以更好地使用内存,因为该方法缓存了一些对象以供重用。这些原始类型的包装类缓存原始值在–128 和 127 之间的包装对象。例如,如果多次调用Integer.valueOf(25),则从缓存中返回同一个Integer对象的引用。然而,当您多次调用new Integer(25)时,每次调用都会创建一个新的Integer对象。清单 12-2 展示了为Integer包装类使用构造器和valueOf()方法的区别。
// CachedWrapperObjects.java
package com.jdojo.wrapper;
public class CachedWrapperObjects {
public static void main(String[] args) {
System.out.println("Using the constructor:");
// Create two Integer objects using constructors
Integer iv1 = new Integer(25);
Integer iv2 = new Integer(25);
System.out.println("iv1 = " + iv1 + ", iv2 = " + iv2);
// Compare iv1 and iv2 references
System.out.println("iv1 == iv2: " + (iv1 == iv2));
// Let's see if they are equal in values
System.out.println("iv1.equals(iv2): " + iv1.equals(iv2));
System.out.println("\nUsing the valueOf() method:");
// Create two Integer objects using the valueOf()
Integer iv3 = Integer.valueOf(25);
Integer iv4 = Integer.valueOf(25);
System.out.println("iv3 = " + iv3 + ", iv4 = " + iv4);
// Compare iv3 and iv4 references
System.out.println("iv3 == iv4: " + (iv3 == iv4));
// Let's see if they are equal in values
System.out.println("iv3.equals(iv4): " + iv3.equals(iv4));
}
}
Using the constructor:
iv1 = 25, iv2 = 25
iv1 == iv2: false
iv1.equals(iv2): true
Using the valueOf() method:
iv3 = 25, iv4 = 25
iv3 == iv4: true
iv3.equals(iv4): true
Listing 12-2The Difference Between Using Constructors and the valueOf() Method to Create Integer Objects
注意,iv1和iv2是对两个不同对象的引用,因为iv1 == iv2返回false。然而,iv3和iv4是对同一个对象的引用,因为iv3 == iv4返回true。当然,iv1、iv2、iv3、iv4表示25的同一个原始值,如equals()方法返回值所示。通常,程序使用较小的整数。如果你正在包装更大的整数,那么valueOf()方法在每次被调用时都会创建一个新的对象。
Tip
new操作符总是创建一个新对象。如果您不需要原始值的新对象,请使用包装类的valueOf()工厂方法,而不是使用构造器。包装器类中的equals()方法已经被重新实现,以比较包装器对象中被包装的原始值,而不是它们的引用。
数字包装类
Byte、Short、Integer、Long、Float和Double类是数字包装类。都是继承自Number类。Number类被声明为抽象的。您不能创建Number类的对象。但是,您可以声明Number类的引用变量。您可以将六个数字包装类中任何一个的对象引用分配给Number类的引用变量。
Number类包含六个方法。它们被命名为xxxValue(),其中xxx是六种原始数据类型之一的名称(byte、short、int、long、float和double)。方法的返回类型与xxx相同。也就是说,byteValue()方法返回一个byte,intValue()方法返回一个int等等。以下代码片段显示了如何从数字包装对象中检索不同的基元类型值:
// Creates an Integer object
Integer intObj = Integer.valueOf(100);
// Gets byte from Integer
byte b = intObj.byteValue();
// Gets double from Integer
double dd = intObj.doubleValue();
System.out.println("intObj = " + intObj);
System.out.println("byte from intObj = " + b);
System.out.println("double from intObj = " + dd);
// Creates a Double object
Double doubleObj = Double.valueOf("329.78");
// Gets different types of primitive values from Double
double d = doubleObj.doubleValue();
float f = doubleObj.floatValue();
int i = doubleObj.intValue();
long l = doubleObj.longValue();
System.out.println("doubleObj = " + doubleObj);
System.out.println("double from doubleObj = " + d);
System.out.println("float from doubleObj = " + f);
System.out.println("int from doubleObj = " + i);
System.out.println("long from doubleObj = " + l);
intObj = 100
byte from intObj = 100
double from intObj = 100.0
doubleObj = 329.78
double from doubleObj = 329.78
float from doubleObj = 329.78
int from doubleObj = 329
long from doubleObj = 329
Java 8 在一些数字包装类如Integer、Long、Float和Double中增加了一些方法如sum()、max()和min()。例如,Integer.sum(10, 20)只是返回 10 + 20 的结果。起初,你可能会想,“包装类的设计者除了添加这些琐碎的方法之外,难道没有任何有用的事情可做吗?我们是不是忘了用加法运算符+来加两个数,所以我们就用Integer.sum(10, 20)?”你的假设是错误的。这些方法是为了更大的目的而添加的。它们不打算用作Integer.sum(10, 20)。它们旨在用作处理集合的方法引用。
您的程序可能接收字符串形式的数字。您可能希望从这些字符串中获取原始值或包装对象。有时,字符串中的整数值可能以不同的基数(也称为基数)编码,例如十进制、二进制、十六进制等。包装类有助于处理包含原始值的字符串:
-
使用
valueOf()方法将字符串转换成包装对象。 -
使用
parseXxx()方法将字符串转换成原始值。
Byte、Short、Integer、Long、Float和Double类分别包含parseByte()、parseShort()、parseInt()、parseLong()、parseFloat()和parseDouble()方法,用于将字符串解析为原始值。
以下代码片段将包含二进制格式的整数的字符串转换为一个Integer对象和一个int值:
String str = "01111111";
int radix = 2;
// Creates an Integer object from the string
Integer intObject = Integer.valueOf(str, radix);
// Extracts the int value from the string
int intValue = Integer.parseInt(str, 2);
System.out.println("str = " + str);
System.out.println("intObject = " + intObject);
System.out.println("intValue = " + intValue);
str = 01111111
intObject = 127
intValue = 127
Java 9 在Integer和Long类中添加了一些方法来解析内容不全是整数的字符串。下面是Integer类中这类方法的列表。Long类中的方法名以Long结尾,这些方法返回long。所有这些方法都抛出一个NumberFormatException:
-
int parseInt(CharSequence s, int beginIndex, int endIndex, int radix) -
int parseUnsignedInt(CharSequence s, int beginIndex, int endIndex, int radix) -
int parseUnsignedInt(String s) -
int parseUnsignedInt(String s, int radix)
新版本的parseInt()方法将CharSequence参数(比如一个String)解析为指定radix中的有符号int,从指定的beginIndex开始,延伸到endIndex - 1。以下代码片段向您展示了如何使用新的parseInt()方法从字符串中的日期提取整数形式的年、月和日值,该字符串采用yyyy-mm-dd格式:
String dateStr = "2017-07-29";
int year = Integer.parseInt(dateStr, 0, 4, 10);
int month = Integer.parseInt(dateStr, 5, 7, 10);
int day = Integer.parseInt(dateStr, 8, 10, 10);
System.out.println("Year = " + year);
System.out.println("Month = " + month);
System.out.println("Day = " + day);
Year = 2017
Month = 7
Day = 29
三个版本的parseInt()方法将字符串解析为有符号整数,而三个版本的parseUnsignedInt()方法将字符串中的数字解析为指定基数的无符号整数。
所有数字包装类都包含几个有用的常量。它们的MIN_VALUE和MAX_VALUE常量代表了它们对应的原语类型所能代表的最小值和最大值。例如,Byte.MIN_VALUE常量是–128,Byte.MAX_VALUE常量是 127,这是可以存储在byte中的最小值和最大值。它们还有一个SIZE常量,表示相应原语类型的变量所占的位数。比如Byte.SIZE是 8,Integer.SIZE是 32。
通常,您从外部来源(例如,文件)接收字符串。如果字符串不能转换成数字,包装类将抛出一个NumberFormatException。通常将字符串解析逻辑放在try-catch块中并处理异常。
下面的代码片段试图将两个字符串解析成double值。第一个字符串包含一个有效的double,第二个包含一个无效的double。当调用parseDouble()方法解析第二个字符串时,抛出一个NumberFormatException:
String str1 = "123.89";
try {
double value1 = Double.parseDouble(str1);
System.out.println("value1 = " + value1);
} catch (NumberFormatException e) {
System.out.println("Error in parsing " + str1);
}
String str2 = "78H.90"; // An invalid double
try {
double value2 = Double.parseDouble(str2);
System.out.println("value2 = " + value2);
} catch (NumberFormatException e) {
System.out.println("Error in parsing " + str2);
}
value1 = 123.89
Error in parsing 78H.90
Note
java.math包包含了BigDecimal和BigInteger类。它们用于保存大的十进制数和整数,它们不适合原始类型double和long。这些类是可变的,它们通常不被称为包装类。如果您对大数字执行计算,并且不希望丢失超出标准基本类型范围的中间值,请使用它们。
字符包装类
Character类的一个对象包装了一个char值。该类包含几个在处理字符时很有用的常量和方法。例如,它包含isLetter()和isDigit()方法来检查字符是字母还是数字。toUpperCase()和toLowerCase()方法将一个字符转换成大写和小写。值得研究一下这个类的 API 文档。该类提供了一个构造器和一个工厂valueOf()方法来从char创建对象。使用工厂方法以获得更好的性能。charValue()方法返回对象包装的char。以下代码片段显示了如何创建Character对象以及如何使用它们的一些方法:
// Using the constructor
Character c1 = new Character('A');
// Using the factory method - preferred
Character c2 = Character.valueOf('2');
Character c3 = Character.valueOf('ñ');
// Getting the wrapped char values
char cc1 = c1.charValue();
char cc2 = c2.charValue();
char cc3 = c3.charValue();
System.out.println("c1 = " + c1);
System.out.println("c2 = " + c2);
System.out.println("c3 = " + c3);
// Using some Character class methods on c1
System.out.println("isLowerCase c1 = " + Character.isLowerCase(cc1));
System.out.println("isDigit c1 = " + Character.isDigit(cc1));
System.out.println("isLetter c1 = " + Character.isLetter(cc1));
System.out.println("Lowercase of c1 = " + Character.toLowerCase(cc1));
// Using some Character class methods on c2
System.out.println("isLowerCase c2 = " + Character.isLowerCase(cc2));
System.out.println("isDigit c2 = " + Character.isDigit(cc2));
System.out.println("isLetter c2 = " + Character.isLetter(cc2));
System.out.println("Lowercase of c2 = " + Character.toLowerCase(cc2));
System.out.println("Uppercase of c3 = " + Character.toUpperCase(cc3));
c1 = A
c2 = 2
c3 = ñ
isLowerCase c1 = false
isDigit c1 = false
isLetter c1 = true
Lowercase of c1 = a
isLowerCase c2 = false
isDigit c2 = true
isLetter c2 = false
Lowercase of c2 = 2
Uppercase of c3 = Ñ
布尔包装类
一个Boolean类的对象包装了一个boolean。Boolean.TRUE和Boolean.FALSE是两个Boolean类型的常量,代表布尔值true和false。您可以使用构造器或valueOf()工厂方法创建一个Boolean对象。解析字符串时,该类将“true”(忽略所有字符的大小写)视为true,将任何其他字符串视为false。您应该总是使用这个类的valueOf()方法来创建一个Boolean对象,因为它返回的是Boolean.TRUE或Boolean.FALSE常量,而不是创建新的对象。下面的代码片段展示了如何使用Boolean类。每个语句中的变量名表示在Boolean对象中表示的布尔值(true或false)的类型:
// Using constructors
Boolean b11True = new Boolean(true);
Boolean b21True = new Boolean("true");
Boolean b31True = new Boolean("tRuE");
Boolean b41False = new Boolean("false");
Boolean b51False = new Boolean("how is this"); // false
// Using the factory methods
Boolean b12True = Boolean.valueOf(true);
Boolean b22True = Boolean.valueOf("true");
Boolean b32True = Boolean.valueOf("tRuE");
Boolean b42False = Boolean.valueOf("false");
Boolean b52False = Boolean.valueOf("how is this"); // false
// Getting a boolean value from a Boolean object
boolean bbTrue = b12True.booleanValue();
// Parsing strings to boolean values
boolean bTrue = Boolean.parseBoolean("true");
boolean bFalse = Boolean.parseBoolean("This string evaluates to false");
// Using constants
Boolean bcTrue = Boolean.TRUE;
Boolean bcFalse = Boolean.FALSE;
// Printing some Boolean objects
System.out.println("bcTrue = " + bcTrue);
System.out.println("bcFalse = " + bcFalse);
bcTrue = true
bcFalse = false
无符号数字运算
Java 不支持无符号原始整数数据类型。byte、short、int和long是有符号数据类型。对于有符号数据类型,值范围的一半用于存储正数,另一半用于存储负数,因为 1 位用于存储值的符号。比如一个byte需要 8 位;其范围是–128 到 127。如果在一个字节中只存储正数,它的范围应该是 0–255。
Java 8 在包装类中添加了一些静态方法,这些方法支持将有符号值中的位视为无符号整数的操作。Byte类包含两个静态方法:
-
int toUnsignedInt(byte x) -
long toUnsignedLong(byte x)
这些方法将指定的字节参数转换为一个int和一个long,就好像该字节存储了一个无符号值一样。如果指定的byte参数为零或正数,转换后的int和long值将与参数值相同。如果参数是负数,转换后的数字将是 2 8 + x。例如,对于 10 的输入,返回值将是 10,对于–10 的输入,返回值将是 28+(–10),即 246。负数以 2 的补码形式存储。值–10 将存储为 11110110。最高有效位 1 表示它是一个负数。前 7 位(1110110)的二进制补码是 001010,十进制数是 10。如果将一个字节中的实际位 11110110 视为无符号整数,则其值为 246 (128 + 64 + 32 + 16 + 0 + 4 + 2 + 0)。以下代码片段显示了如何获取以无符号整数形式存储在字节中的值:
byte b = -10;
int x = Byte.toUnsignedInt(b);
System.out.println("Signed value in byte = " + b);
System.out.println("Unsigned value in byte = " + x);
Signed value in byte = -10
Unsigned value in byte = 246
Short类包含与Byte类相同的两个方法,除了它们接受一个short作为参数并将其转换为一个int和一个long。Java 9 在Short类中添加了一个新的静态方法compareUnsigned(short x, short y),该方法比较两个短值,并将这些值视为无符号值。如果x等于y,则返回 0;如果x小于y,则小于 0 的值为无符号值;以及如果x大于y则大于 0 的值作为无符号值。
Integer类包含以下静态方法来支持无符号操作和转换:
-
int compareUnsigned(int x, int y) -
int divideUnsigned(int dividend, int divisor) -
int parseUnsignedInt(String s) -
int parseUnsignedInt(String s, int radix) -
int remainderUnsigned(int dividend, int divisor) -
long toUnsignedLong(int x) -
String toUnsignedString(int i) -
String toUnsignedString(int i, int radix)
注意,Integer类不包含addUnsigned()、subtractUnsigned()和multiplyUnsigned()方法,因为这三个操作在两个有符号和两个无符号操作数上是按位相同的。下面的代码片段显示了对两个int变量的除法运算,就好像它们的位代表无符号值一样:
// Two negative ints
int x = -10;
int y = -2;
// Performs signed division
System.out.println("Signed x = " + x);
System.out.println("Signed y = " + y);
System.out.println("Signed x/y = " + (x/y));
// Performs unsigned division by treating x and y holding unsigned values
long ux = Integer.toUnsignedLong(x);
long uy = Integer.toUnsignedLong(y);
int uQuotient = Integer.divideUnsigned(x, y);
System.out.println("Unsigned x = " + ux);
System.out.println("Unsigned y = " + uy);
System.out.println("Unsigned x/y = " + uQuotient);
Signed x = -10
Signed y = -2
Signed x/y = 5
Unsigned x = 4294967286
Unsigned y = 4294967294
Unsigned x/y = 0
Long类包含执行无符号运算的方法。这些方法类似于Integer类中的方法。请注意,您不能将存储在long中的值转换为无符号值,因为您需要比long数据类型提供的更大的存储空间才能这样做,但是long是 Java 提供的最大的整数数据类型。这就是Byte和Short类有toUsignedInt()和toUnSignedLong()方法的原因,因为int和long比byte和short大。事实上,要将有符号数据类型X的值作为无符号值存储在有符号数据类型Y中,数据类型Y的大小至少需要两倍于X的大小。遵循这个存储要求,在Integer类中有一个toUnsignedLong()方法,但是在Long类中没有这样的方法。
汽车爆炸和脱氧核糖核酸病毒
自动装箱和取消装箱用于在原始数据类型和它们对应的包装类之间自动转换值。它们完全在编译器中实现。在我们定义自动装箱/取消装箱之前,让我们讨论一个例子。这个例子很简单,但是它的目的是演示在 Java 5 中添加自动装箱功能之前,当您在原始类型和它们的包装对象之间进行转换时,您必须经历的痛苦,反之亦然。
假设您有一个方法,它接受两个int值,将它们相加,然后返回一个int值。你可能会说,“这个方法有什么大不了的?”应该像下面这样简单:
// Only method code is shown
public static int add(int a, int b) {
return a + b;
}
该方法可以如下使用:
int a = 200;
int b = 300;
int result = add(a, b); // result will get a value of 500
你是对的,这种方法没有什么大不了的。让我们给逻辑增加一点扭曲。考虑用同样的方法处理Integer对象而不是int值。以下是相同方法的代码:
public static Integer add(Integer a, Integer b) {
int aValue = a.intValue();
int bValue = b.intValue();
int resultValue = aValue + bValue;
Integer result = Integer.valueOf(resultValue);
return result;
}
你注意到当你改变同样的方法来使用Integer对象时所涉及的复杂性了吗?您必须执行三件事情来在Integer对象中添加两个int值:
-
使用
intValue()方法将方法参数a和b从Integer对象展开为int值: -
将两个
int值相加:
int aValue = a.intValue();
int bValue = b.intValue();
- 将结果包装到一个新的
Integer对象中,并返回结果:
int resultValue = aValue + bValue;
Integer result = Integer.valueOf(resultValue);
return result;
清单 12-3 有完整的代码来演示add()方法的使用。
// MathUtil.java
package com.jdojo.wrapper;
public class MathUtil {
public static Integer add(Integer a, Integer b) {
int aValue = a.intValue();
int bValue = b.intValue();
int resultValue = aValue + bValue;
Integer result = Integer.valueOf(resultValue);
return result;
}
public static void main(String[] args) {
int iValue = 200;
int jValue = 300;
int kValue;
/* will hold result as int */
// Box iValue and jValue into Integer objects
Integer i = Integer.valueOf(iValue);
Integer j = Integer.valueOf(jValue);
// Store returned value of the add() method in an Integer object k
Integer k = MathUtil.add(i, j);
// Unbox Integer object's int value into kValue int variable
kValue = k.intValue();
// Display the result using int variables
System.out.println(iValue + " + " + jValue + " = " + kValue);
}
}
200 + 300 = 500
Listing 12-3Adding Two int Values Using Integer Objects
请注意将两个int值相加所需的代码量。对于 Java 开发人员来说,将一个int值打包/解包到一个Integer,反之亦然,是一件痛苦的事情。Java 设计者意识到了这一点(尽管为时已晚),他们为您自动化了这个包装和解包过程。
从一个原始数据类型(byte、short、int、long、float、double、char和boolean)到其对应的包装对象(Byte、Integer、Long、Float、Double、Character和Boolean)的自动包装被称为自动装箱。相反,从包装对象到其相应的原始数据类型值的解包装,被称为解装箱。对于自动装箱/取消装箱,以下代码是有效的:
Integer n = 200; // Boxing
int a = n; // Unboxing
编译器将用下面的语句替换前面的语句:
Integer n = Integer.valueOf(200);
int a = n.intValue();
清单 12-3 中列出的MathUtil类的main()方法中的代码可以重写如下。装箱和取消装箱是自动完成的:
int iValue = 200;
int jValue = 300;
int kValue = MathUtil.add(iValue, jValue);
System.out.println(iValue + " + " + jValue + " = " + kValue);
Tip
自动装箱/取消装箱是在编译代码时执行的。JVM 完全不知道编译器执行的装箱和拆箱操作。
小心空值
自动装箱/取消装箱使您不必编写额外的代码行。这也让你的代码看起来更整洁。然而,它确实带来了一些惊喜。其中一个惊喜是在你意想不到的地方得到一个NullPointerException。基本类型不能被赋予一个null值,而引用类型可以有一个null值。装箱和取消装箱发生在基本类型和引用类型之间。请看下面的代码片段:
Integer n = null; // n can be assigned a null value
int a = n; // will throw NullPointerException at run time
在这段代码中,假设您不控制null到n的赋值。作为方法调用的结果,您可能会得到一个null Integer对象,例如int a = getSomeValue(),其中getSomeValue()返回一个Integer对象。在这样的地方,你可能会大吃一惊。然而,它会发生,因为在这种情况下int a = n被转换为int a = n.intValue()并且n是null。这个惊喜是你从自动装箱/拆箱中获得的优势的一部分,你需要意识到这一点。
重载方法和自动装箱/取消装箱
当您调用一个重载方法并希望依赖自动装箱/取消装箱特性时,您会有一些惊讶。假设一个类中有两个方法:
public void test(Integer iObject) {
System.out.println("Integer=" + iObject);
}
public void test(int iValue) {
System.out.println("int=" + iValue);
}
假设您对test()方法进行了两次调用:
test(101);
test(Integer.valueOf(101));
以下哪一项将是输出?
int=101
Integer=101
或者
Integer=101
int=101
使用自动装箱/取消装箱的方法调用规则遵循两步过程:
-
如果传递的实际参数是原始类型(如在
test(10)中)-
尝试找到一个具有原始类型参数的方法。如果没有完全匹配,请尝试扩大基本类型以找到匹配。
-
如果上一步失败,将原始类型装箱并尝试寻找匹配。
-
-
如果传递的实际参数是引用类型(如在
test(Integer.valueOf(101)中)-
尝试找到一个带有引用类型参数的方法。如果匹配,调用该方法。在这种情况下,匹配不一定要精确。它应该遵循子类型和超类型分配规则。
-
如果上一步失败,将引用类型取消装箱到相应的基元类型,并尝试查找精确匹配,或者扩大基元类型并查找匹配。
-
如果您将这些规则应用于前面的代码片段,它将打印如下内容:
int=101
Integer=101
假设你有以下两个test()方法:
public void test(Integer iObject) {
System.out.println("Integer=" + iObject);
}
public void test(long iValue) {
System.out.println("long=" + iValue);
}
如果使用下面的代码,会打印出什么?
test(101);
test(Integer.valueOf(101));
它将打印以下内容:
long=101
Integer=101
对test(101)的第一次调用将试图为一个int参数找到一个精确匹配。它没有找到方法test(int),所以它扩展了int数据类型,找到一个匹配的test(long),并调用这个方法。假设您有如下两个test()方法:
public void test(Long lObject) {
System.out.println("Long=" + lObject);
}
public void test(long lValue) {
System.out.println("long=" + lValue);
}
如果执行下面的代码,会打印出什么?
test(101);
test(Integer.valueOf(101));
它将打印以下内容:
long=101
long=101
看到这个输出,你感到惊讶吗?应用我列出的规则,您会发现这个输出遵循了这些规则。对test(101)的调用是清楚的,因为它将 101 从int扩展到long并执行test(long)方法。为了调用test(Integer.valueOf(101)),它寻找一个方法test(Integer),但是没有找到。也就是说,一个Integer永远不会被加宽到Long。因此,它取消Integer到int的装箱,并寻找一个test(int)方法,但没有找到。现在,它加宽了int并找到test(long)并执行它。
考虑以下三种test()方法。我在前面的方法列表中添加了一个test(Number nObject)方法:
public void test(Long lObject) {
System.out.println("Long=" + lObject);
}
public void test(Number nObject) {
System.out.println("Number=" + nObject);
}
public void test(long lValue) {
System.out.println("long=" + lValue);
}
如果执行下面的代码,会打印出什么?
test(101);
test(Integer.valueOf(101));
它将打印以下内容:
long=101
Number=101
看输出是不是又惊了?应用我列出的规则。对test(101)的调用是明确的。为了调用test(Integer.valueOf(101)),它寻找一个方法test(Integer),但是没有找到。它试图根据规则列表中的规则 2(a)将Integer参数扩展为Number或Object。回想一下,所有数字包装类都是从Number类继承的。所以可以将一个Integer赋值给一个Number类型的变量。它在第二个test(Number nObject)方法中找到一个匹配并执行它。
我还有一个惊喜给你。考虑以下两种test()方法:
public void test(Long lObject) {
System.out.println("Long=" + lObject);
}
public void test(Object obj) {
System.out.println("Object=" + obj);
}
当你执行下面的代码时会打印出什么?
test(101);
test(Integer.valueOf(101));
这一次,您将获得以下输出:
Object=101
Object=101
有意义吗?不完全是。下面是解释。当它调用test(101)时,它必须将int装箱为Integer,因为test(int)没有匹配项,即使在扩大了int值之后。于是test(101)变成了test(Integer.valueOf(101))。现在它也找不到任何test(Integer)。注意Integer是一个引用类型,它继承了Number类,后者又继承了Object类。因此,一个Integer总是一个Object,Java 允许你将一个子类型(Integer)的对象赋给一个超类型(Object)的变量。这就是在这种情况下调用test(Object)的原因。第二个调用test(Integer.valueOf(101)),工作方式相同。它尝试使用test(Integer)方法。当它没有找到它时,基于引用类型的子类型和超类型分配规则,它的下一个匹配是test(Object)。
比较运算符和自动装箱/取消装箱
本节讨论比较操作==、>、>=、<和<=。只有==(逻辑等式运算符)可以同时用于引用类型和原始类型。其他运算符只能用于基元类型。
我们先来看看容易的(>, >=, <,和<=))。如果一个数值包装对象与这些比较操作符一起使用,它必须被取消装箱,并且在比较中必须使用相应的基元类型。考虑以下代码片段:
Integer a = 100;
Integer b = 100;
System.out.println("a : " + a);
System.out.println("b : " + b);
System.out.println("a > b: " + (a > b));
System.out.println("a >= b: " + (a >= b));
System.out.println("a < b: " + (a < b));
System.out.println("a <= b: " + (a <= b));
a : 100
b : 100
a > b: false
a >= b: true
a < b: false
a <= b: true
这个输出没有任何惊喜。如果用这些比较运算符混合引用和原语这两种类型,仍然会得到相同的结果。首先,取消对引用类型的装箱,并与两个基元类型进行比较,例如:
if (101 > Integer.valueOf(100)) {
// Do something
}
被转换为
if(101 <= (Integer.valueOf(100)).intValue()) {
// Do something
}
现在,让我们讨论一下==操作符和自动装箱规则。如果两个操作数都是基元类型,则使用值比较将它们作为基元类型进行比较。如果两个操作数都是引用类型,则比较它们的引用。在这两种情况下,不会发生自动装箱/取消装箱。当一个操作数是引用类型,而另一个是基元类型时,引用类型被取消装箱为基元类型,并进行值比较。让我们看看每种类型的例子。
考虑下面的代码片段。这是一个对==操作符使用两种基本类型操作数的例子:
int a = 100;
int b = 100;
int c = 505;
System.out.println(a == b); // will print true
System.out.println(a == c); // will print false
考虑以下代码片段:
Integer aa = Integer.valueOf(100);
Integer bb = new Integer(100);
Integer cc = new Integer(505);
System.out.println(aa == bb); // will print false
System.out.println(aa == cc); // will print false
在这段代码中,没有发生自动装箱/取消装箱。这里,aa == bb和aa == cc比较的是aa、bb、cc的引用,而不是它们的值。用new操作符创建的每个对象都有一个唯一的参考。现在,这里有一个惊喜:考虑下面的代码片段。这一次您依赖于自动装箱:
Integer aaa = 100; // Boxing – Integer.valueOf(100)
Integer bbb = 100; // Boxing – Integer.valueOf(100)
Integer ccc = 505; // Boxing – Integer.valueOf(505)
Integer ddd = 505; // Boxing – Integer.valueOf(505)
System.out.println(aaa == bbb); // will print true
System.out.println(aaa == ccc); // will print false
System.out.println(ccc == ddd); // will print false
您使用了aaa、bbb、ccc和ddd作为参考类型。aaa == bbb true怎么样,而ccc == ddd false呢?好吧。这一次,自动装箱功能没有带来任何惊喜。相反,它来自于Integer.valueOf()方法。对于–128 和 127 之间的所有值,Integer类缓存Integer对象引用。当您调用它的valueOf()方法时,就会用到缓存。例如,如果您调用Integer.valueOf(100)两次,那么您将从缓存中获得同一个Integer对象的引用,该引用表示int值为 100。但是,如果您调用Integer.valueOf(n),其中n在范围–128 到 127 之外,那么每次调用都会创建一个新对象。这就是aaa和bbb具有来自缓存的相同引用,而ccc和ddd具有不同引用的原因。Byte、Short、Character和Long类也缓存-128 到 127 范围内的值的对象引用。
收集和自动装箱/拆箱
自动装箱/取消装箱有助于您处理集合。集合仅适用于引用类型。不能在集合中使用基元类型。如果要在集合中存储基元类型,必须在存储基元值之前对其进行包装,并在检索后对其进行解包装。假设你有一个List,你想在其中存储整数。你应该这样做:
List list = new ArrayList();
list.add(Integer.valueOf(101));
Integer a = (Integer) list.get(0);
int aValue = a.intValue();
你又回到了起点。List接口的add()和get()方法与Object类型一起工作,并且您必须再次求助于包装和解开原始类型。自动装箱/取消装箱可以帮助您将基元类型包装为引用类型,这段代码可以重写如下:
List list = new ArrayList();
list.add(101); // Autoboxing will work here
Integer a = (Integer) list.get(0);
int aValue = a.intValue();
/*int aValue = list.get(0); */ // auto-unboxing won't compile
因为get()方法的返回类型是Object,所以这段代码中的最后一条语句将不起作用。注意,拆箱是从一个原始包装器类型(比如Integer)到其对应的原始类型(比如int)进行的。如果你试图将一个Object引用类型赋给一个int原始类型,自动拆箱不会发生。事实上,您的代码甚至无法编译,因为不允许Object到int的转换。
尝试以下代码:
List<Integer> list = new ArrayList<>();
list.add(101); // autoboxing will work
int aValue = list.get(0); // auto-unboxing will work, too
所有集合类都是通用的。它们声明了形式类型参数。在创建List对象时,在尖括号(<Integer>)中指定Integer类型,告诉编译器List将只保存一个Integer类型的对象。这使得编译器可以在您处理List对象时自由地包装和展开您的原始int值。
摘要
对于每种原始数据类型,Java 都提供了一个类来将原始数据类型的值表示为对象。Java 不支持无符号原始数字数据类型和无符号数字运算。Java 8 通过在包装类中添加一些方法,增加了对原始数据类型的无符号操作的有限支持。Java 9 在Integer和Long类中添加了一些方法,将字符串解析为无符号整数。Java 9 还在Short类中添加了一个方法,将两个short值作为无符号short值进行比较。
Java 不允许在同一个表达式中混合原始类型和引用类型的值。将原始值转换成它们的包装对象是不方便的,反之亦然。Java 5 增加了对根据上下文自动将原始值转换为包装对象的支持,反之亦然。这个特性叫做自动装箱/拆箱。例如,它允许将整数 25 赋给Integer对象的引用;编译器使用表达式Integer.valueOf(25)自动将整数 25 装入包装对象中。
QUESTIONS AND EXERCISES
-
Java 中的包装类是什么?命名以下原始类型的包装类:
byte、int、long和char。 -
使用包装类
Integer,打印int数据类型的最大值和最小值。 -
数值包装类的超类的名字是什么?
-
假设你有一个字符串
"1969"。完成下面的代码片段,将字符串中的整数值存储到一个int变量和一个Integer对象中:String str = "1969"; int value = /* Your code goes here */; Integer object = /* Your code goes here */; -
您有一个字符串
"7B1",它包含一个十六进制格式的整数。使用Integer类解析并将其值存储在int变量中。 -
下面的代码片段可以编译吗?如果会,请描述规则/原因:
-
您有一个 1969 的整数值,您想以十六进制格式打印它的值。完成以下代码片段来实现这一点:
int x = 1969; String str = Integer./* your code goes here */; System.out.println("1969 in hex is " + str); -
为什么下面的语句不能编译
Integer x = 19;
Double x = 1969;
而下面的说法呢?
double y = 1969;
一定要明白这些说法无效和有效背后的原因。描述以下语句是如何编译的:
-
以下代码片段的输出会是什么?解释你的答案:
Number x = 1969; System.out.println(x.getClass().getSimpleName()); -
当下面的代码片段运行时,输出会是什么?
Double x = 128.5; System.out.println(x.intValue()); System.out.println(x.byteValue());
Number x = 1969;