类、对象和类型
导航 语言基础 主题: ) |
一个 **对象** 由 **字段** 和 **方法** 组成。字段也称为 *数据成员*、*特征*、*属性* 或 *特性*,描述了对象的 状态。方法通常描述与特定对象相关的操作。可以将对象视为名词,其字段为描述该名词的形容词,其方法为该名词可以执行的动词。
例如,跑车是一个对象。它的某些字段可能是它的高度、重量、加速度和速度。对象的字段只是保存有关该对象的数据。跑车的一些方法可能是 "驾驶"、"停车"、"比赛" 等。除非与跑车相关,否则方法并没有多大意义,字段也是如此。
让我们构建跑车对象的蓝图称为 *类*。类不会告诉我们跑车的速度有多快,或者它是什么颜色,但它确实告诉我们我们的跑车将有一个代表速度和颜色的字段,它们分别是数字和单词(或十六进制颜色代码)。类还为我们列出了方法,告诉汽车如何停车和驾驶,但这些方法仅靠蓝图无法执行任何操作——它们需要一个对象才能产生影响。
在 Java 中,类位于与自身名称类似的文件中。如果你想有一个名为 SportsCar
的类,它的源文件需要是 SportsCar.java
。通过在源文件中放置以下内容来创建类:
代码清单 3.13: SportsCar.java
public class SportsCar {
/* Insert your code here */
}
|
该类目前还没有做任何事情,因为你需要先添加方法和字段变量。
对象不同于基本类型,因为
- 基本类型不会被实例化。
- 在内存中,对于基本类型,只存储其值。对于对象,还可以存储对实例的引用。
- 在内存中,基本类型的分配空间是固定的,无论其值如何。对象的分配空间可以变化,例如对象是否被实例化。
- 基本类型没有可调用的方法。
- 基本类型不能被继承。
为了从类到对象,我们通过 *实例化* 来 "构建" 我们的对象。实例化仅仅意味着创建类的 *实例*。实例和对象是非常相似的术语,有时可以互换使用,但请记住,实例指的是 *特定对象*,它是从类创建的。
这种实例化是由类的其中一个方法带来的,称为 *构造函数*。顾名思义,构造函数根据蓝图构建对象。在幕后,这意味着为实例分配计算机内存,并将值分配给数据成员。
一般来说,有四种类型的构造函数:默认、非默认、复制和克隆。
**默认构造函数** 将构建最基本的实例。通常,这意味着将所有字段分配像 null、零或空字符串这样的值。但是,没有什么可以阻止你将默认跑车的颜色设置为红色,但这通常是不好的编程风格。如果你的基本汽车是红色而不是无色,另一个程序员会感到困惑。
代码部分 3.79:一个默认构造函数。
SportsCar car = new SportsCar();
|
**非默认构造函数** 用于创建具有为大多数(如果不是全部)对象的字段指定了值的实例。汽车是红色的,从 0-60 加速需要 12 秒,最高时速为 190 英里/小时,等等。
代码部分 3.80:一个非默认构造函数。
SportsCar car = new SportsCar("red", 12, 190);
|
**复制构造函数** 不包含在 Java 语言中,但是可以轻松地创建一个与复制构造函数相同功能的构造函数。重要的是要理解它是什么。顾名思义,复制构造函数创建一个新的实例,作为已经存在的实例的副本。在 Java 中,这也可以通过使用默认构造函数创建实例,然后使用赋值运算符使它们等价来实现。但这在所有语言中都不可能,所以只要记住这个术语就可以了。
Java 具有 **克隆对象** 的概念,其最终结果与复制构造函数类似。克隆对象比使用 new
关键字创建更快,因为所有对象内存都会一次性复制到目标克隆对象中。这可以通过实现 Cloneable
接口来实现,该接口允许 Object.clone()
方法执行逐字段复制。
代码部分 3.81:克隆对象。
SportsCar car = oldCar.clone();
|
当创建对象时,还会创建对该对象的引用。在 Java 中不能直接访问对象,只能通过该对象引用访问。该对象引用被分配了 *类型*。当将对象引用作为参数传递给方法时,我们需要该类型。Java 进行强类型检查。
类型基本上是可以通过该对象引用执行的功能/操作列表。对象引用类型基本上是一个契约,保证这些操作在运行时存在。
当创建汽车时,它会附带用户手册中列出的功能/操作列表,保证在使用汽车时这些功能/操作存在。
当你从类创建对象时,默认情况下它的类型与其类相同。这意味着类定义的所有功能/操作都存在并可用,并且可以使用。请参见以下内容:
代码部分 3.82:默认类型。
(new ClassName()).operations();
|
你可以将其分配给与类具有相同类型的变量:
代码部分 3.83:与类具有相同类型的变量。
ClassName objRefVariable = new ClassName();
objRefVariable.operations();
|
你可以将创建的对象引用分配给类、超类或类实现的接口:
代码部分 3.84:使用超类。
SuperClass objectRef = new ClassName(); // features/operations list are defined by the SuperClass class
...
Interface inter = new ClassName(); // features/operations list are defined by the interface
|
在汽车类比中,创建的汽车可能具有不同类型的驾驶员。我们为他们创建单独的用户手册,一个普通用户手册、一个高级用户手册、一个儿童用户手册或一个残疾人用户手册。每种类型的用户手册只描述适合该类型驾驶员的功能/操作。例如,高级驾驶员可能拥有其他档位来切换到更高的速度,而其他类型的用户则没有……
当汽车钥匙从成年人手中传递给儿童时,我们是在更换用户手册,这称为 *类型转换*。
在 Java 中,转换可以通过三种方式发生:
- 向上转换,沿着继承树向上,直到我们到达
Object
- 向上转换到类实现的接口
- 向下转换,直到我们到达创建对象的类
自动装箱和拆箱是 Java 1.5 之后引入的语言功能,在处理基本类型包装器类型时,可以极大地简化程序员的工作。请考虑以下代码片段:
代码部分 3.85:传统的对象创建。
int age = 23;
Integer ageObject = new Integer(age);
|
基本类型包装器对象是 Java 允许人们将基本数据类型视为对象的方式。因此,人们需要像上面展示的那样,将自己的基本数据类型 *包装* 到相应的基本类型包装器对象中。
从 Java 1.5 开始,你可以像下面这样编写,编译器会自动创建包装对象。不再需要额外包装基本类型。它已被 *自动装箱* 到您的 behalf
代码部分 3.86:自动装箱。
int age = 23;
Integer ageObject = age;
|
请记住,编译器仍然会创建缺少的包装器代码,因此在性能方面并没有真正获得任何好处。可以将此功能视为程序员的便利,而不是性能提升。 |
每个基本类型都有一个类包装器:
基本类型 | 类包装器 |
byte |
java.lang.Byte
|
char |
java.lang.Character
|
short |
java.lang.Short
|
int |
java.lang.Integer
|
long |
java.lang.Long
|
float |
java.lang.Float
|
double |
java.lang.Double
|
boolean |
java.lang.Boolean
|
void |
java.lang.Void
|
拆箱使用与装箱相反的过程。花点时间研究一下以下代码。if
语句需要一个 boolean
原生值,但它被赋予了一个 Boolean 包装对象。没问题!Java 1.5 将自动拆箱此对象。
代码部分 3.87:拆箱。
Boolean canMove = new Boolean(true);
if (canMove) {
System.out.println("This code is legal in Java 1.5");
}
|
问题 3.11:考虑以下代码
问题 3.11:自动装箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
这段代码中包含多少个自动装箱和拆箱操作?
答案 3.11:自动装箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);
|
3
- 第 1 行有一个自动装箱操作,用于赋值。
- 第 2 行有一个拆箱操作,用于进行加法运算。
- 第 2 行有一个自动装箱操作,用于赋值。
- 第 3 行没有自动装箱或拆箱操作,因为
println()
方法支持Integer
类作为参数。
java.lang.Object
类中的方法是继承的,因此所有类共享这些方法。
java.lang.Object.clone()
方法返回一个新对象,它是当前对象的副本。类必须实现标记接口 java.lang.Cloneable
以指示它们可以被克隆。
java.lang.Object.equals(java.lang.Object)
方法将对象与另一个对象进行比较,并返回一个 boolean
结果,指示这两个对象是否相等。从语义上讲,此方法比较对象的内容,而等式比较运算符 "==
" 比较对象引用。equals
方法被 java.util
包中的许多数据结构类使用。这些数据结构类中的一些还依赖于 Object.hashCode
方法 - 请参阅 hashCode
方法了解有关 equals
和 hashCode
之间契约的详细信息。实现 equals() 不像看起来那样简单,请参阅 'equals() 的秘密' 获取更多信息。
java.lang.Object.finalize()
方法在垃圾收集器释放对象内存之前恰好调用一次。类覆盖 finalize
以执行在回收对象之前必须执行的任何清理操作。大多数对象不需要覆盖 finalize
。
无法保证何时调用 finalize
方法,也无法保证为多个对象调用 finalize
方法的顺序。如果 JVM 在执行垃圾收集之前退出,操作系统可能会释放对象,在这种情况下,finalize
方法不会被调用。
finalize
方法应始终被声明为 protected
以防止其他类调用 finalize
方法。
protected void finalize() throws Throwable { ... }
java.lang.Object.getClass()
方法返回用于实例化对象的类的 java.lang.Class
对象。类对象是 Java 中 反射 的基类。java.lang.reflect
包中提供了额外的反射支持。
java.lang.Object.hashCode()
方法返回一个整数 (int
)。虽然不完全,但可以使用此整数来区分对象。它可以快速分离大多数对象,具有相同哈希码的对象将在之后以其他方式分离。它被提供关联数组的类使用,例如,实现 java.util.Map
接口的类。它们使用哈希码将对象存储在关联数组中。良好的 hashCode
实现将返回一个哈希码
- 稳定:不改变
- 均匀分布:不相等对象的哈希码倾向于不相等,并且哈希码在整数值上均匀分布。
第二点意味着两个不同的对象可以具有相同的哈希码,因此具有相同哈希码的两个对象不一定相同!
由于关联数组依赖于 equals
和 hashCode
方法,因此这两个方法之间有一个重要的契约,如果对象要插入到 Map
中,则必须维护此契约。
- 对于两个对象a 和b
a.equals(b) == b.equals(a)
- 如果
a.equals(b)
,则a.hashCode() == b.hashCode()
- 但
如果a.hashCode() == b.hashCode()
,则a.equals(b)
为了维护此契约,覆盖 equals
方法的类也必须覆盖 hashCode
方法,反之亦然,以便 hashCode
基于与 equals
相同的属性(或属性的子集)。
地图与对象之间的另一个契约是,一旦对象被插入到地图中,hashCode
和 equals
方法的结果就不会改变。出于这个原因,通常最好将哈希函数基于对象的不可变属性。
java.lang.Object.toString()
方法返回一个 java.lang.String
,其中包含对象的文本表示形式。当对象操作数与字符串连接运算符 (+
和 +=
) 一起使用时,编译器会隐式调用 toString
方法。
每个对象都有两个与之关联的线程等待列表。一个等待列表由 synchronized
关键字用于获取与对象关联的互斥锁。如果互斥锁当前由另一个线程持有,则当前线程将被添加到等待互斥锁的阻塞线程列表中。另一个等待列表用于通过 wait
和 notify
以及 notifyAll
方法完成的线程之间的信号传递。
使用 wait
/notify
允许在线程之间有效地协调任务。当一个线程需要等待另一个线程完成操作,或者需要等待事件发生时,线程可以暂停其执行并等待事件发生时通知。这与轮询形成对比,在轮询中,线程会反复休眠一小段时间,然后检查标志或其他条件指示器。轮询既更耗费计算资源(因为线程必须继续检查),响应速度也更慢(因为线程直到下次检查时才会注意到条件已改变)。
wait
方法有三个重载版本,用于支持以不同方式指定超时值:java.lang.Object.wait()
、java.lang.Object.wait(long)
和 java.lang.Object.wait(long, int)
。第一个方法使用超时值为零 (0),这意味着等待不会超时;第二个方法将毫秒数作为超时;第三个方法将纳秒数作为超时,计算为 1000000 * timeout + nanos
。
调用 wait
的线程被阻塞(从可执行线程集中删除)并添加到对象的等待列表中。线程将一直保留在对象的等待列表中,直到发生以下三种事件之一
- 另一个线程调用对象的
notify
或notifyAll
方法; - 另一个线程调用线程的
java.lang.Thread.interrupt
方法;或者 - 在对
wait
的调用中指定的非零超时过期。
wait
方法必须在对对象进行同步的块或方法中调用。这确保了 wait
和 notify
之间没有竞争条件。当线程被放入等待列表时,线程会释放对象的互斥锁。在线程从等待列表中移除并添加到可执行线程集中之后,它必须在继续执行之前获取对象的互斥锁。
java.lang.Object.notify()
和 java.lang.Object.notifyAll()
方法会从对象的等待列表中移除一个或多个线程,并将它们添加到可执行线程集中。notify
从等待列表中移除单个线程,而 notifyAll
则移除等待列表中的所有线程。notify
移除哪个线程是未指定的,取决于 JVM 实现。
notify 方法必须在对对象同步的块或方法中调用。这可以确保在 wait
和 notify
之间不存在竞争条件。