跳转到内容

类、对象和类型

100% developed
来自维基教科书,开放的书籍,为开放的世界

导航 语言基础 主题: v  d  e )


一个 **对象** 由 **字段** 和 **方法** 组成。字段也称为 *数据成员*、*特征*、*属性* 或 *特性*,描述了对象的 状态。方法通常描述与特定对象相关的操作。可以将对象视为名词,其字段为描述该名词的形容词,其方法为该名词可以执行的动词。

例如,跑车是一个对象。它的某些字段可能是它的高度、重量、加速度和速度。对象的字段只是保存有关该对象的数据。跑车的一些方法可能是 "驾驶"、"停车"、"比赛" 等。除非与跑车相关,否则方法并没有多大意义,字段也是如此。

让我们构建跑车对象的蓝图称为 *类*。类不会告诉我们跑车的速度有多快,或者它是什么颜色,但它确实告诉我们我们的跑车将有一个代表速度和颜色的字段,它们分别是数字和单词(或十六进制颜色代码)。类还为我们列出了方法,告诉汽车如何停车和驾驶,但这些方法仅靠蓝图无法执行任何操作——它们需要一个对象才能产生影响。

在 Java 中,类位于与自身名称类似的文件中。如果你想有一个名为 SportsCar 的类,它的源文件需要是 SportsCar.java。通过在源文件中放置以下内容来创建类:

Computer code 代码清单 3.13: SportsCar.java
public class SportsCar {
   /* Insert your code here */
}

该类目前还没有做任何事情,因为你需要先添加方法和字段变量。

对象不同于基本类型,因为

  1. 基本类型不会被实例化。
  2. 在内存中,对于基本类型,只存储其值。对于对象,还可以存储对实例的引用。
  3. 在内存中,基本类型的分配空间是固定的,无论其值如何。对象的分配空间可以变化,例如对象是否被实例化。
  4. 基本类型没有可调用的方法。
  5. 基本类型不能被继承。

实例化和构造函数

[编辑 | 编辑源代码]

为了从类到对象,我们通过 *实例化* 来 "构建" 我们的对象。实例化仅仅意味着创建类的 *实例*。实例和对象是非常相似的术语,有时可以互换使用,但请记住,实例指的是 *特定对象*,它是从类创建的。

这种实例化是由类的其中一个方法带来的,称为 *构造函数*。顾名思义,构造函数根据蓝图构建对象。在幕后,这意味着为实例分配计算机内存,并将值分配给数据成员。

一般来说,有四种类型的构造函数:默认、非默认、复制和克隆。

**默认构造函数** 将构建最基本的实例。通常,这意味着将所有字段分配像 null、零或空字符串这样的值。但是,没有什么可以阻止你将默认跑车的颜色设置为红色,但这通常是不好的编程风格。如果你的基本汽车是红色而不是无色,另一个程序员会感到困惑。

Example 代码部分 3.79:一个默认构造函数。
SportsCar car = new SportsCar();

**非默认构造函数** 用于创建具有为大多数(如果不是全部)对象的字段指定了值的实例。汽车是红色的,从 0-60 加速需要 12 秒,最高时速为 190 英里/小时,等等。

Example 代码部分 3.80:一个非默认构造函数。
SportsCar car = new SportsCar("red", 12, 190);

**复制构造函数** 不包含在 Java 语言中,但是可以轻松地创建一个与复制构造函数相同功能的构造函数。重要的是要理解它是什么。顾名思义,复制构造函数创建一个新的实例,作为已经存在的实例的副本。在 Java 中,这也可以通过使用默认构造函数创建实例,然后使用赋值运算符使它们等价来实现。但这在所有语言中都不可能,所以只要记住这个术语就可以了。

Java 具有 **克隆对象** 的概念,其最终结果与复制构造函数类似。克隆对象比使用 new 关键字创建更快,因为所有对象内存都会一次性复制到目标克隆对象中。这可以通过实现 Cloneable 接口来实现,该接口允许 Object.clone() 方法执行逐字段复制。

Example 代码部分 3.81:克隆对象。
SportsCar car = oldCar.clone();

当创建对象时,还会创建对该对象的引用。在 Java 中不能直接访问对象,只能通过该对象引用访问。该对象引用被分配了 *类型*。当将对象引用作为参数传递给方法时,我们需要该类型。Java 进行强类型检查。

类型基本上是可以通过该对象引用执行的功能/操作列表。对象引用类型基本上是一个契约,保证这些操作在运行时存在。

当创建汽车时,它会附带用户手册中列出的功能/操作列表,保证在使用汽车时这些功能/操作存在。

当你从类创建对象时,默认情况下它的类型与其类相同。这意味着类定义的所有功能/操作都存在并可用,并且可以使用。请参见以下内容:

Example 代码部分 3.82:默认类型。
(new ClassName()).operations();

你可以将其分配给与类具有相同类型的变量:

Example 代码部分 3.83:与类具有相同类型的变量。
ClassName objRefVariable = new ClassName();
objRefVariable.operations();

你可以将创建的对象引用分配给类、超类或类实现的接口:

Example 代码部分 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 之后引入的语言功能,在处理基本类型包装器类型时,可以极大地简化程序员的工作。请考虑以下代码片段:

Example 代码部分 3.85:传统的对象创建。
int age = 23;
Integer ageObject = new Integer(age);

基本类型包装器对象是 Java 允许人们将基本数据类型视为对象的方式。因此,人们需要像上面展示的那样,将自己的基本数据类型 *包装* 到相应的基本类型包装器对象中。

从 Java 1.5 开始,你可以像下面这样编写,编译器会自动创建包装对象。不再需要额外包装基本类型。它已被 *自动装箱* 到您的 behalf

Example 代码部分 3.86:自动装箱。
int age = 23;
Integer ageObject = age;
Note 请记住,编译器仍然会创建缺少的包装器代码,因此在性能方面并没有真正获得任何好处。可以将此功能视为程序员的便利,而不是性能提升。

每个基本类型都有一个类包装器:

基本类型 类包装器
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 将自动拆箱此对象。

Example 代码部分 3.87:拆箱。
Boolean canMove = new Boolean(true);
 
if (canMove) {
  System.out.println("This code is legal in Java 1.5");
}
测试你的知识

问题 3.11:考虑以下代码

Example 问题 3.11:自动装箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);

这段代码中包含多少个自动装箱和拆箱操作?

答案
Example 答案 3.11:自动装箱/拆箱。
Integer a = 10;
Integer b = a + 2;
System.out.println(b);

3

  • 第 1 行有一个自动装箱操作,用于赋值。
  • 第 2 行有一个拆箱操作,用于进行加法运算。
  • 第 2 行有一个自动装箱操作,用于赋值。
  • 第 3 行没有自动装箱或拆箱操作,因为 println() 方法支持 Integer 类作为参数。

Object 类中的方法

[编辑 | 编辑源代码]

java.lang.Object 类中的方法是继承的,因此所有类共享这些方法。

clone 方法

[编辑 | 编辑源代码]

java.lang.Object.clone() 方法返回一个新对象,它是当前对象的副本。类必须实现标记接口 java.lang.Cloneable 以指示它们可以被克隆。

equals 方法

[编辑 | 编辑源代码]

java.lang.Object.equals(java.lang.Object) 方法将对象与另一个对象进行比较,并返回一个 boolean 结果,指示这两个对象是否相等。从语义上讲,此方法比较对象的内容,而等式比较运算符 "==" 比较对象引用。equals 方法被 java.util 包中的许多数据结构类使用。这些数据结构类中的一些还依赖于 Object.hashCode 方法 - 请参阅 hashCode 方法了解有关 equalshashCode 之间契约的详细信息。实现 equals() 不像看起来那样简单,请参阅 'equals() 的秘密' 获取更多信息。

finalize 方法

[编辑 | 编辑源代码]

java.lang.Object.finalize() 方法在垃圾收集器释放对象内存之前恰好调用一次。类覆盖 finalize 以执行在回收对象之前必须执行的任何清理操作。大多数对象不需要覆盖 finalize

无法保证何时调用 finalize 方法,也无法保证为多个对象调用 finalize 方法的顺序。如果 JVM 在执行垃圾收集之前退出,操作系统可能会释放对象,在这种情况下,finalize 方法不会被调用。

finalize 方法应始终被声明为 protected 以防止其他类调用 finalize 方法。

protected void finalize() throws Throwable { ... }

getClass 方法

[编辑 | 编辑源代码]

java.lang.Object.getClass() 方法返回用于实例化对象的类的 java.lang.Class 对象。类对象是 Java 中 反射 的基类。java.lang.reflect 包中提供了额外的反射支持。

hashCode 方法

[编辑 | 编辑源代码]

java.lang.Object.hashCode() 方法返回一个整数 (int)。虽然不完全,但可以使用此整数来区分对象。它可以快速分离大多数对象,具有相同哈希码的对象将在之后以其他方式分离。它被提供关联数组的类使用,例如,实现 java.util.Map 接口的类。它们使用哈希码将对象存储在关联数组中。良好的 hashCode 实现将返回一个哈希码

  • 稳定:不改变
  • 均匀分布:不相等对象的哈希码倾向于不相等,并且哈希码在整数值上均匀分布。

第二点意味着两个不同的对象可以具有相同的哈希码,因此具有相同哈希码的两个对象不一定相同

由于关联数组依赖于 equalshashCode 方法,因此这两个方法之间有一个重要的契约,如果对象要插入到 Map 中,则必须维护此契约。

对于两个对象ab
  • a.equals(b) == b.equals(a)
  • 如果 a.equals(b),则 a.hashCode() == b.hashCode()
  • 如果 a.hashCode() == b.hashCode(),则 a.equals(b)

为了维护此契约,覆盖 equals 方法的类也必须覆盖 hashCode 方法,反之亦然,以便 hashCode 基于与 equals 相同的属性(或属性的子集)。

地图与对象之间的另一个契约是,一旦对象被插入到地图中,hashCodeequals 方法的结果就不会改变。出于这个原因,通常最好将哈希函数基于对象的不可变属性。

toString 方法

[编辑 | 编辑源代码]

java.lang.Object.toString() 方法返回一个 java.lang.String,其中包含对象的文本表示形式。当对象操作数与字符串连接运算符 (++=) 一起使用时,编译器会隐式调用 toString 方法。

waitnotify 线程信号方法

[编辑 | 编辑源代码]

每个对象都有两个与之关联的线程等待列表。一个等待列表由 synchronized 关键字用于获取与对象关联的互斥锁。如果互斥锁当前由另一个线程持有,则当前线程将被添加到等待互斥锁的阻塞线程列表中。另一个等待列表用于通过 waitnotify 以及 notifyAll 方法完成的线程之间的信号传递。

使用 wait/notify 允许在线程之间有效地协调任务。当一个线程需要等待另一个线程完成操作,或者需要等待事件发生时,线程可以暂停其执行并等待事件发生时通知。这与轮询形成对比,在轮询中,线程会反复休眠一小段时间,然后检查标志或其他条件指示器。轮询既更耗费计算资源(因为线程必须继续检查),响应速度也更慢(因为线程直到下次检查时才会注意到条件已改变)。

wait 方法

[编辑 | 编辑源代码]

wait 方法有三个重载版本,用于支持以不同方式指定超时值:java.lang.Object.wait()java.lang.Object.wait(long)java.lang.Object.wait(long, int)。第一个方法使用超时值为零 (0),这意味着等待不会超时;第二个方法将毫秒数作为超时;第三个方法将纳秒数作为超时,计算为 1000000 * timeout + nanos

调用 wait 的线程被阻塞(从可执行线程集中删除)并添加到对象的等待列表中。线程将一直保留在对象的等待列表中,直到发生以下三种事件之一

  1. 另一个线程调用对象的 notifynotifyAll 方法;
  2. 另一个线程调用线程的 java.lang.Thread.interrupt 方法;或者
  3. 在对 wait 的调用中指定的非零超时过期。

wait 方法必须在对对象进行同步的块或方法中调用。这确保了 waitnotify 之间没有竞争条件。当线程被放入等待列表时,线程会释放对象的互斥锁。在线程从等待列表中移除并添加到可执行线程集中之后,它必须在继续执行之前获取对象的互斥锁。

notifynotifyAll 方法

[编辑 | 编辑源代码]

java.lang.Object.notify()java.lang.Object.notifyAll() 方法会从对象的等待列表中移除一个或多个线程,并将它们添加到可执行线程集中。notify 从等待列表中移除单个线程,而 notifyAll 则移除等待列表中的所有线程。notify 移除哪个线程是未指定的,取决于 JVM 实现。

notify 方法必须在对对象同步的块或方法中调用。这可以确保在 waitnotify 之间不存在竞争条件。


华夏公益教科书