一、概述
1、介绍
Object 类属于 java.lang 包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入。
Object 类是所有类的基类,如果一个类没有使用 extends 标识继承另外一个类,那么这个类默认继承Object类。任何类都直接或间接继承此类。
类结构图:
代码示例:Object类源码
1 public class Object { 2 3 private static native void registerNatives(); 4 static { 5 registerNatives(); 6 } 7 8 public final native Class<?> getClass(); 9 10 public native int hashCode(); 11 12 public boolean equals(Object obj) { 13 return (this == obj); 14 } 15 16 protected native Object clone() throws CloneNotSupportedException; 17 18 public String toString() { 19 return getClass().getName() + "@" + Integer.toHexString(hashCode()); 20 } 21 22 public final native void notify(); 23 24 public final native void notifyAll(); 25 26 public final native void wait(long timeout) throws InterruptedException; 27 28 public final void wait(long timeout, int nanos) throws InterruptedException { 29 if (timeout < 0) { 30 throw new IllegalArgumentException("timeout value is negative"); 31 } 32 33 if (nanos < 0 || nanos > 999999) { 34 throw new IllegalArgumentException( 35 "nanosecond timeout value out of range"); 36 } 37 38 if (nanos > 0) { 39 timeout++; 40 } 41 42 wait(timeout); 43 } 44 45 public final void wait() throws InterruptedException { 46 wait(0); 47 } 48 49 protected void finalize() throws Throwable { } 50 }
2、为什么java.lang包下的类不需要手动导入?
使用 java.lang 包下的所有类,都不需要手动导入。Java中导包有两种方法:
①、单类型导入(single-type-import),例如import java.util.Date
②、按需类型导入(type-import-on-demand),例如import java.util.*
单类型导入:需要什么类便导入什么类,这种方式是导入指定的 public 类或者接口。
按需类型导入:比如import java.util.*,* 并不是导入java.util包下的所有类,而是根据名字按需导入。
Java编译器会从启动目录(bootstrap),扩展目录(extension)和用户类路径下去定位需要导入的类,而这些目录仅仅给出了类的顶层目录,编译器的类文件定位方法大致可以理解为如下公式:
顶层路径名 包名 文件名.class = 绝对路径
单类型导入知道包名和文件名,所以编译器可以一次性定位到所需的类文件。按需类型导入则比较复杂,编译器会把包名和文件名进行排列组合,然后对所有的可能性进行类文件查找定位。例如:
package com;
import java.io.*;
import java.util.*;
如果文件中使用到了 File 类,那么编译器会按如下几个步骤来进行查找 File 类:
①、File // File类属于无名包,即没有package语句,编译器会首先搜索无名包
②、com.File // File类属于当前包,就是当前编译类的包路径
③、java.lang.File // 由于编译器会自动导入java.lang包,所以也会从该包下查找
④、java.io.File
⑤、java.util.File
......
注意:编译器找到 java.io.File 类之后并不会停止下一步的寻找,而是把所有的可能性都查找完以确定是否有类导入冲突。
假设此时的顶层路径有三个,那么编译器就会进行3*5=15次查找。如果查找完成后,编译器发现了两个同名的类,就会报错。要删除你不用的那个类,然后再编译。
结论:按需类型导入是绝对不会降低Java代码的执行效率的,但会影响Java代码的编译速度。所以,编码时最好使用单类型导入,这样不仅能提高编译速度,也能避免命名冲突。
二、类源码
1、类构造器
一个类必须要有一个构造器,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDK的Object类源码中,是看不到构造器的,系统会自动添加一个无参构造器。
可以通过 Object obj = new Object();构造一个Object类的对象。
2、equals(Object obj)方法
比较的是对象的引用是否指向同一块内存地址。一般情况下比较两个对象时比较他的值是否一致,所以要进行重写。
Object.equals方法
代码示例:案例
1 public class Main { 2 3 public static void main(String[] args) { 4 int i = 1, j = 1; 5 System.out.println(i == j); // true 6 7 Person p1 = new Person("张三", 20); 8 Person p2 = new Person("张三", 20); 9 10 System.out.println(p1 == p2); // false 11 System.out.println(p1.equals(p2)); // false 12 } 13 } 14 15 class Person { 16 private String name; 17 private int age; 18 19 public Person(String name, int age) { 20 this.name = name; 21 this.age = age; 22 } 23 }
如图,对象在栈,堆空间的表示:p1变量存放在栈中,存放的是对象在堆空间的地址(引用)。
那么,不难理解程序运行的结果:
①基本数据类型值是否相等?i == j,true。
②两个对象的引用是否相等?p1 == p2,false。显然 p1 = 0x0001,p2 = 0x0002。所以 p1 == p2 为false。
③两个对象是否相等?false。
从源码可以看到,在 Object 类中的 equals 方法与 == 运算符是等价的,也是比较的两个对象的引用是否相等。所以 p1.equals(p2) 为false。
然而,这两个对象都是("张三",20),如何才能比较两个对象是否相等呢?
答:需要程序员复写 equals 方法。
equals 用于比较两个对象是否相等。那么,两个对象如何才认为是相等的呢?这个判定逻辑由程序员自己定义。如下:
代码示例:Person类的 equals 方法
1 @Override 2 public boolean equals(Object o) { 3 if (this == o) return true; // 引用相同,则一定相等 4 if (o == null || !(o instanceof Person)) return false; 5 6 Person person = (Person) o; 7 8 // age和name都相同,对象才相等 9 if (age != person.age) return false; 10 return name.equals(person.name); 11 }
不难看懂,这里复写了Object类中的 equals 方法,定义了我们想要的比较方式来比较对象是否相等。
这里,定义对象所有的属性相等,则相等。再运行 p1.equals(p2) 为 true。
当然,也可以写为 age == person.age ,则只要 Person 的年龄相同,则对象相等。读者可自行验证。
equals() 方法和 == 运算符的区别:
equals()
|
== 运算符
|
|
比较类型
|
对象是否相等(程序员自定义)
|
①基本类型的值;②对象的引用是否相等
|
String.equals方法
源码示例:
1 // jdk1.8源码 2 public boolean equals(Object anObject) { 3 if (this == anObject) { 4 return true; 5 } 6 if (anObject instanceof String) { 7 String anotherString = (String)anObject; 8 int n = value.length; 9 if (n == anotherString.value.length) { 10 char v1[] = value; 11 char v2[] = anotherString.value; 12 int i = 0; 13 while (n-- != 0) { 14 if (v1[i] != v2[i]) 15 return false; 16 i++; 17 } 18 return true; 19 } 20 } 21 return false; 22 }
上述源码不难读懂,String类复写了Object类中的 equals 方法,定义了判断字符串相等的方式,是每个字符都相同。
String 是引用类型,比较时重点在于字符串的内容是否相等。所以不能用 == 运算符,而应该使用 equals 方法。
equals方法原则
在Java规范中,对 equals 方法的使用必须遵循以下几个原则:
①自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
②对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
③传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
④一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象中 equals 比较所用的属性没有被修改。
⑤对于任何非空引用值 x,x.equals(null) 都应返回 false。
在上述案例中,定义一个 Person 类的子类 Student,也重写 equals 方法:
1 class Student extends Person { 2 3 public Student(String name, int age) { 4 super(name, age); 5 } 6 7 @Override 8 public boolean equals(Object o) { 9 if (this == o) return true; 10 if (o == null || !(o instanceof Student)) return false; 11 12 if (!super.equals(o)) return false; 13 14 return super.equals(o); 15 } 16 17 } 18 19 public class Main { 20 21 public static void main(String[] args) { 22 Person p = new Person("Tom", 22); 23 Student s = new Student("Tom", 22); 24 25 System.out.println(p.equals(s)); // true 26 System.out.println(s.equals(p)); // false 27 } 28 }
结果显然是不正确的,这违反了equals 的对称性。看一下下面代码的结果:
1 System.out.println(p instanceof Student); // 人是学生吗?fasle 2 System.out.println(s instanceof Person); // 学生是人吗?true
问题出现在 instanceof 关键字上,实际上用 instanceof 关键字是做不到对称性的要求的。那么,怎么办呢?
可以用 getClass()方法取代 instanceof 运算符。getClass() 方法也是 Object 类中的一个方法,作用是返回一个对象的运行时类。
代码示例:修改Person类的 equals 方法
1 @Override 2 public boolean equals(Object o) { 3 if (this == o) return true; 4 if (o == null || getClass() != o.getClass()) return false; 5 6 Person person = (Person) o; 7 8 if (age != person.age) return false; 9 return name.equals(person.name); 10 }
getClass()与 instanceof 的使用界定!
使用 getClass() 不符合多态的定义,比如 AbstractSet 抽象类,它有两个子类 TreeSet 和 HashSet,他们分别使用不同的算法实现查找集合的操作,但无论集合采用哪种方式实现,都需要拥有对两个集合进行比较的功能,如果使用 getClass() 实现equals方法的重写,那么就不能在两个不同子类的对象进行相等的比较。而且集合类比较特殊,其子类是不需要自定义相等的概念的。
源码示例:TreeSet 和 HashSet 中 equals 方法都是 AbstractSet定义
1 public boolean equals(Object o) { 2 if (o == this) 3 return true; 4 5 if (!(o instanceof Set)) 6 return false; 7 Collection<?> c = (Collection<?>) o; 8 if (c.size() != size()) 9 return false; 10 try { 11 return containsAll(c); 12 } catch (ClassCastException unused) { 13 return false; 14 } catch (NullPointerException unused) { 15 return false; 16 } 17 }
他两的使用有如下建议:
①、如果子类要拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测。
②、如果由父类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同的子类的对象之间进行相等的比较。
最后请注意:无论何时重写 equals 方法,通常都必须重写hashCode方法,以维护hashCode方法的一般约定,该方法声明相等对象必须具有相同的哈希值。关于这个,介绍有关集合的源码时,会重点说明!
3、hashCode()方法
源码示例:
1 public native int hashCode();
这是一个用 native 修饰的方法,本地方法。作用是返回对象的散列码,是 int 类型的数值。
关于这个方法,介绍有关集合的源码时,会重点说明!
4、getClass()方法
源码示例:
1 public final native Class<?> getClass();
这是一个用 native 修饰的方法,本地方法。关于此关键字,详解请参看JVM系列。它是由操作系统实现,该方法的作用是返回一个对象的运行时类对象。
虽然,此方法返回的是一个 Class<?> ,但是实际是:
1 final Class<? extends String> aClass = "".getClass();
类型为 T 的变量 getClass 方法,返回值类型其实是Class<? extends T>,而不是方法声明中的Class<?>。
在官方文档中也有说明:
https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#getClass--
5、toString()方法
源码示例:
1 public String toString() { 2 return getClass().getName() + "@" + Integer.toHexString(hashCode()); 3 }
getClass().getName():返回对象的全类名
Integer.toHexString(hashCode()):以16进制无符号整数形式返回此哈希码的字符串表示形式。
打印某个对象时,默认是调用 toString 方法,比如 System.out.println(person),等价于 System.out.println(person.toString())。
6、notify()/notifyAll()/wait() 方法
用于多线程之间的通信方法,详情请阅读多线程相关知识。
7、finalize 方法
该方法用于垃圾回收,一般由 JVM 自动调用,不需要程序员去手动调用该方法。详情请阅读 JVM 系列。
8、registerNatives 方法
源码示例:
1 private static native void registerNatives(); 2 static { 3 registerNatives(); 4 }
这是一个本地方法,一个类定义了本地方法后,想要调用操作系统的实现,必须还要装载本地库。这里是通过静态代码块来完成本地库的载入代码。
静态代码块是一个类在初始化过程中必定会执行的内容(详细请阅读JVM相关知识),所以在类加载的时候会执行该方法,通过该方法来注册本地方法。
在官方文档中也有说明:
https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html