重学java-13.Object类
- Object类
- equals()
- 概念
- 实现
- 实现方法 I
- 实现方法 II
- getclass()与instanceof的用法
- hashCode()
- 概念
- 实现
- 为什么选择31作为乘数
- clone()
- 概念
- 实现
- 深拷贝与浅拷贝
- clone()的替换方案
- toString()
Object类
在java中,所有引用数据类型都可以向上转型为Object类。万物始祖Objcet
下面是Object类的主要方法
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
equals()
概念
源码如下
public boolean equals(Object obj) {
return (this == obj);
}
源码中关于该方法的注释:
- 自反性 :
x.equals(x)
结果应该返回true
; - 对称性 :
x.equals(y) == y.equals(x);
结果返回true
; - 传递性 :
if (x.equals(y) && y.equals(z)) x.equals(z);
- 一致性 :
x.equals(y)
的第一次调用为true
,那么x.equals(y)
的第二次,第三次等多次调用也应该为true
,但是前提条件是在进行比较之前,x和y都没有被修改。 -
x.equals(null)
应该返回false
。 - 这个方法返回true当且仅当x和y指向了同样的对象(x==y),这句话也就是说明了在默认情况下,Object类中的equals方法默认比较的是对象的地址,因为只有是相同的地址才会相等(x == y),
如果没有重写equals方法,那么默认就是比较的是地址
。
注意:无论何时这个equals方法被重写都有必要去重写hashCode方法
,这是为了维持hashCode的一种约定,相同的对象必须要有相同的hashCode值。
实现
根据源码可知,Object的equals()方法比较的是两个对象的地址,这显然不符合对象操作的需要,因而需要进行重写。
实现方法 I
public class Test{
public static void main(String[] args){
Book book = new Book("野草", 13);
Book book2 = new Book("野草", 13);
Book bookImp = new BookImpl("野草", 13);
System.out.println(book2.equals(book));//true
System.out.println(bookImp.equals(book));//false
}
}
class Book {
private String name;
private double price;
public Book(String name, double price) {
super();
this.name = name;
this.price = price;
}
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null || this.getClass() != obj.getClass()) return false; //这里有另一种写法
//if(obj == null || !(obj instanceof Book) ) return false;
Book book = (Book) obj;
if(this.name.equals(book.name) && this.price == book.price) return true;
return false;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
class BookImpl extends Book{
public BookImpl(String name, double price) {
super(name, price);
// TODO Auto-generated constructor stub
}
}
实现方法 II
public class Test{
public static void main(String[] args){
Book book = new Book("野草", 13);
Book book2 = new Book("野草", 13);
Book bookImp = new BookImpl("野草", 13);
System.out.println(book2.equals(book));//true
System.out.println(bookImp.equals(book));//true
}
}
class Book {
private String name;
private double price;
public Book(String name, double price) {
super();
this.name = name;
this.price = price;
}
public boolean equals(Object obj) {
if(this == obj) return true;
//if(obj == null || this.getClass() != obj.getClass()) return false; //这里有另一种写法
if(obj == null || !(obj instanceof Book) ) return false;
Book book = (Book) obj;
if(this.name.equals(book.name) && this.price == book.price) return true;
return false;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
class BookImpl extends Book{
public BookImpl(String name, double price) {
super(name, price);
// TODO Auto-generated constructor stub
}
}
getclass()与instanceof的用法
public class Test{
public static void main(String[] args){
A a = new A();
B b = new B();
System.out.println(a.getClass()==b.getClass());//false
System.out.println();
System.out.println(a.getClass().equals(A.class));//true
System.out.println(b.getClass().equals(B.class));//true
System.out.println(b.getClass().equals(A.class));//false
System.out.println(a.getClass().equals(B.class));//false
System.out.println();
System.out.println(a instanceof A);//true
System.out.println(a instanceof B);//false
System.out.println(b instanceof A);//true
System.out.println(b instanceof B);//true
System.out.println();
A c = new B();
System.out.println(c.getClass().equals(B.class));//true
}
}
class A{
}
class B extends A{
}
可以看出,getclass()
所获得的正是运行时的当前类(runtime class),只有实例化两个对象的类相同,这两个对象getclass()
才想通。
而 a instanceof B
可适用于B是实例化a的类,或者B是实例化a的类的父类或接口。
究竟用instanceof
还是 getclass()
对equals()
方法进行重写,取决于功能需求。
hashCode()
概念
hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。之所以这样,是因为Map、Set等集合类型都是先比较hash值,再进行equals()判断。
实现
public int hashCode() {
int result = 17;
result = result * 31 + this.name.hashCode();
result = result * 31 + Double.hashCode(this.price);
return result;
}
常用的实现方法:
第一步: 定义一个初始值,一般来说取17int result = 17;
第二步: 分别解析自定义类中与equals方法相关的字段(假如hashCode中考虑的字段在equals方法中没有考虑,则两个equals的对象就很可能具有不同的hashCode)
- 字段a类型为
boolean
则[hashCode] = a ? 1 : 0;
- 字段b类型为
byte/short/int/char
, 则[hashCode] = (int)b;
- 字段c类型为
long
, 则[hashCode] = (int) (c ^ c>>>32);
- 字段d类型为
float
, 则[hashCode] = d.hashCode()
(内部调用的是Float.hashCode(d), 而该静态方法内部调用的另一个静态方法是Float.floatToIntBits(d)
) - 字段e类型为
double
, 则[hashCode] = e.hashCode()
(内部调用的是Double.hashCode(e), 而该静态方法内部调用的另一个静态方法是Double.doubleToLongBits(e)
,得到一个long类型的值之后,跟情况三进行类似的操作,得到一个int类型的值) 引用类型
,若为null则hashCode为0,否则递归调用该引用类型的hashCode方法。数组类型
。(要获取数组类型的hashCode,可采用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + … + s[n-1], 该方法正是String类的hashCode实现所采用的算法)
第三步: 对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:
result = result * 31 + [hashCode];
为什么选择31作为乘数
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。
R一般取31
,理由如下:
- 更少的乘积结果冲突
31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。 - 31可以被JVM优化
一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x
,编译器会自动进行这个优化。
初始值选17也是同理。关于hashCode()的重写目前没有个标准答案,只有更好的没有最好的。
clone()
概念
需要注意一点,Java规定只有实现了Cloneable接口的类才能使用clone()方法
。
以下是clone()方法的源码。
protected native Object clone() throws CloneNotSupportedException;
可以看到clone在Object类里是protect方法,它不是public,因此只有子类重写clone()方法,其他类的实例才能对clone()进行调用。
实现
class CloneExample implements Cloneable {//实现Cloneable接口
public String str;
public CloneExample(String str) {
super();
this.str = str;
}
@Override
public CloneExample clone() throws CloneNotSupportedException {//重写为public
// TODO Auto-generated method stub
return (CloneExample) super.clone();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return this.str;
}
}
深拷贝与浅拷贝
我们在上面的CloneExample类中加入一个成员变量Book类。
class Book {
private int num2;
public Book(int num2) {
super();
this.num2 = num2;
}
//get and set...
}
class CloneExample implements Cloneable {
private int num1;
private Book book;
public CloneExample(int num1, Book book) {
super();
this.num1 = num1;
this.book = book;
}
@Override
public CloneExample clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return (CloneExample) super.clone();
}
//get and set...
}
实例化CloneExample类。
public static void main(String[] args) throws CloneNotSupportedException{
CloneExample ce1 = new CloneExample(123,new Book(456));
CloneExample ce2 = ce1.clone();
System.out.println(ce2.getNum1());
ce1.setNum1(1);
System.out.println(ce2.getNum1());
System.out.println(ce2.getBook().getNum2());
ce1.getBook().setNum2(4);
System.out.println(ce2.getBook().getNum2());
}
结果:
我们发现,在上述拷贝过程中拷贝对象与原对象的引用类型引用同一个对象
,这种拷贝叫浅拷贝。
而深拷贝是指全部拷贝原对象的内容,包括内存的引用类型也进行拷贝
。clone()方法默认是浅拷贝的,但我们可以通过重写clone方法实现深拷贝。
clone()的替换方案
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
class Book {
private int num2;
public Book(int num2) {
super();
this.num2 = num2;
//get and set
}
class CloneExample implements Cloneable {
private int num1;
private Book book;
public CloneExample(int num1, Book book) {
super();
this.num1 = num1;
this.book = book;
}
public CloneExample( CloneExample ce) {//拷贝构造函数
super();
this.num1 = ce.num1;
this.book = new Book(ce.getBook().getNum2());
}
//get and set
}
toString()
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}