02JAVA对象
2.1 概述
对象的特性:
-
行为——可以对对象施加哪些操作?对象的行为是用可调用的方法定义的。
-
状态——当施加了方法时,对象如何响应?对象状态的改变必须通过调用方法实现。
-
标识——如何辨别具有相同行为与状态的不同对象?每个对象都有一个唯一的标识。
2.1.1 对象与对象变量
在 Java 中使用构造器构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
//1 对象变量
Date deadline;
//2 对象
//该句构造了一个Date类型的对象,并且它的值是对新创建对象的引用
new Date();
//变量对象并没有实际包含一个对象,也没有引用对象。使用时要使用新构造的对象对对象变量进行初始化
//在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用,new操作符的返回值就是一个引用
Date deadline = new Date();
//多个对象变量可以引用同一个对象
Date deadline = new Date();
Date birthday = new Date();
2.1.2 对象的比较
一般使用 == 或 equals 方法来进行比较。其区别如下:
-
==
- 针对基本数据类型,比较的是数据内容
- 针对引用数据类型,比较的是引用地址(数组、类、接口)
-
equals方法
- equals()是方法,因此其只适用于比较引用数据类型
Object类中是基于 == 实现的,所以是比较的地址。若某类没有重写equals方法,则会比较地址,所以在自定义类时,一般会重写equals方法和hashcode方法;当某类重写了equals方法,如String类等,此时会比较内容,当内容相同时,就会返回true
// == 和 equals方法
@Test
public void test0() {
// 字面量形式定义String
String s1 = "aaaa";
String s2 = "aaaa";
// new形式定义String
String s3 = new String("aaaa");
String s4 = new String("aaaa");
// 未重写equals方法的类
Person p1 = new Person("zdp");
Person p2 = new Person("zdp");
// == 比较的是引用的地址
//1 字面量形式定义
System.out.println(s1 == s2); // true 存储在常量池中,具有唯一
System.out.println(p1.name == p2.name); // true 因为对象中的构造器给String类型的属性赋值时,是基于字面量形式
//2 new形式定义
System.out.println(s3 == s4); // false new形式是创建两个不同的对象,堆地址不同
System.out.println(s1 == s3); // false 本身的地址就不同
System.out.println("***********************************************************");
// equals方法 比较的内容
//1 字面量形式
System.out.println(s1.equals(s2)); // true 二者引用的地址相同,其内容肯定相同
//2 new形式定义的类
//2.1 equals方法被重写的类
System.out.println(s3.equals(s4)); // true 由于String类重写了equals方法,此时的equals方法只比较内容
//2.2 equals方法未重写的类
System.out.println(p1.equals(p2)); // false 未重写equals的类将调用Object类中的equals方法,该方法就是使用==来实现的,即比较引用的地址
//3 数组形式的引用数据类型
int[] a1 = {1, 2, 3, 4, 5};
int[] a2 = {1, 2, 3, 4, 5};
int[] a3 = new int[5];
int[] a4 = new int[5];
System.out.println(a1 == a2); // false 数组是引用数据类型,地址不同
System.out.println(a3 == a4); // false 是不同的对象
System.out.println(Arrays.equals(a1, a2)); // true Arrays类重写了equals方法,只比较数组的内容
}
(1)hashCode()
hashCode()
的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
定义在 JDK 的 Object
类中,这就意味着 Java 中的任何类都包含有 hashCode()
函数。另外需要注意的是: Object
的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
Map存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码,可以快速找到所需要的对象.
hashCode的作用
以“HashSet
如何检查重复”为例子。当你把对象加入 HashSet
时,HashSet
会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
(2)JAVA规定
Java 对于equals()方法和hashCode()方法的规定:
- 如果两个对象equals()方法相等则它们的hashCode()返回值一定要相同。
- 如果两个对象的hashCode()返回值相同,但它们的equals()方法不一定相等
- 两个对象的hashCode()返回值相等不能判断这两个对象是相等的,但两个对象的hashCode()返回值不相等则可以判定两个对象一定不相等
在object类中,hashcode()方法是本地方法,返回的是一个32位的int型hashcode,而object类中的equals()方法比较的是两个对象的地址值,如果equals()相等,说明两个对象地址值相等,当然它们的hashcode也就相等了;在String类中,equals()返回的是两个对象 地址—>内容 的比较,当两个对象内容相等时,hashcode()方法根据String类的重写代码的分析,也可知道hashcode()返回结果也会相等(String类内部对equals和hashCode进行了重写)。
// String 类中重写的equals源码
public boolean equals(Object anObject) {
//1 先比较地址,地址相同其内容肯定相同
if (this == anObject) {
return true;
}
//2 如果地址不相同就比较内容
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
/**String 类中重写的HashCode方法
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
(3)重写hashCode()方法
这是Object类关于这两个方法的源码,可以看出,Object类默认的equals()方法比较规则就是比较两个对象的内存地址。而hashCode()是本地方法, Java 的内存是安全的,因此无法根据散列码得到对象的内存地址。
当 equals() 方法被重写时通常有必要重写 hashCode() 方法来维护 hashCode() 方法的常规协定,该协定声明相等对象必须具有相等的哈希码,如果不这样做的话就会违反 hashCode() 方法的常规约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括 HashMap、HashSet 等。
关于 hashcode 的一些规定:
- 两个对象相等,hashcode一定相等
- 两个对象不等,hashcode不一定不等
- hashcode相等,两个对象不一定相等
- hashcode不等,两个对象一定不等
在重写equals()方法时需要重写hashcode()方法,否则会违反Java的通用约定,从而导致该类无法与所有基于hash的集合类结合在一起使用。
JAVA规定的第一条:如果两个对象equals()方法返回true,则hashCode()方法返回的hash值也一定会相同。因此在对equals()方法进行重写时,通常还需要重写hashCode()方法。
2.1.3 对象的实例化与初始化
类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;
类的实例化是指在类完全加载到内存中后创建对象的过程。
2.2 对象的构造
如果在编写一个类时没有编写构造器,那么系统会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象的时候会报错。即若有自定义的构造器,就必须要手动创建一个无参的默认的构造器。
2.2.1 构造器
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作符的执行被调用
如果在编写一个类时没有编写构造器,那么系统会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么在new创建实例对象时,将无法调用无参的构造器来创建对象,会在构造对象的时候会报错。
- 构造函数:当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。
- 构造函数重载函数:为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。
- 复制构造函数:Java不支持像C++中那样的复制构造函数。这个是因为Java不会创建默认的复制构造函数。
2.3 对象的创建
一个对象在可以被使用之前必须要被正确地实例化。在 Java 代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在 Java 规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。【主动引用】
2.3.1 new
这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们还可以调用任意的构造函数(无参的和有参的)。比如:Student student = new Student();
2.3.2 反射
Class的newInstance方法内部调用Constructor的newInstance方法。
(1)Class类的newInstance方法
也可以使用Class类的newInstance方法创建对象,这个newInstance方法调用无参的构造器创建对象,如:Student student2 = (Student)Class.forName(“根路径.Student”).newInstance(); 或者:Student stu = Student.class.newInstance();
(2)Constructor类的newInstance方法
本方法和Class类的newInstance方法很像,java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。如: Constructor constructor = Student.class.getInstance(); Student stu = constructor.newInstance();
2.3.3 Clone
在实际编程中,经常需要从某个已知对象A创建出另一个与A具有相同状态的对象B,并且对B的修改不会影响到A的状态。
// 这种方法不会实现上述的效果,因为a\b指向的是同一个地址,即他们的操作会互相干扰
Obj a = new Obj();
Obj b = a;
所以 Java 提供了一个简单有效的clone()方法来满足这个需求。
(1)浅复制
Java 中所有的类都默认继承自Object类,而Object类中提供了一个clone()方法,这个方法的作用是返回一个Object对象的复制,这个复制方法返回的是一个新的对象而不是一个引用。以下是使用clone()方法的步骤:
- 实现clone的类首先需要实现Cloneable接口(Cloneable接口实质是一个标识接口,没有任何的接口方法)
- 在类中重写Object类中的clone()方法
- 在clone()方法中调用super.clone()。无论clone类继承结构是什么,super.clone()会直接或间接的调用java.lang.Object类中的clone()方法
- 把浅复制的引用指向原型对象新的克隆体
(2)深复制
浅复制只适用于类中只存在基本数据类型的属性时,当类中含有其他类的引用时(聚合关系),此时在做复制操作时,就涉及到深复制。
在对对象调用clone()方法完成复制后,接着对对象中的非基本数据类型的属性也调用clone()方法完成深复制。
(3)区别
- 浅复制:被复制对象的所有变量都含有与原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象,换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象
- 深复制:被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制的新对象,而不知原来那些被引用的对象,换言之,深复制把浅复制的对象所引用的对象都复制了一遍,对引用数据类型,创建一个新的对象,并复制其内容
2.3.4 反序列化
主要用于在网络传输过程中的对象创建。
当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。如:
ObjectInputStream in = new ObjectInputStream (new FileInputStream("data.obj"));
Student stu3 = (Student)in.readObject();