老规矩–妹妹镇楼:
Object类是所有Java类的根父类,如果在任意类的声明中没有通过extends指明继承的父类,则该类的默认父类就是java.lang.Object类,下面详细地介绍Object类的内容。
由于Object类是所有类的默认父类,则该类中的相关方法则会被子类一一继承,如果子类不重写这些方法则会默认使用这些方法。下面介绍一下Object类中的相关方法:
eqauls方法在面试中是一个高频的考点,该方法用于不同对象的比较,Object类中原始的equals方法如下所示:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到的是,原始的equals方法中仅仅是通过“==”号来进行比较,这个符号用于比较两个值是否相等,对于基本数据类型和引用类型意义不同。对于基本数据类型,只要两个变量的值相等,则为true;对于引用类型,由于引用的是内存地址,只有引用的地址是相同的才会返回true。
同时,需要注意的是使用“==”符号进行比较时,符号两边的数据类型必须兼容,否则会编译错误。如基础数据类型之间相互比较是可行的,但是如果将基础数据类型和对象进行比较则会出现编译错误。
因此,在定义一个类时,可以重写Object类中的equals()方法,自定义代码逻辑用来比较两个对象的内容是否相等,不仅仅是比较内存地址,还需要比较对象中的内容。如String类中首先比较地址,再比较String中的字符内容是否相同,但要注意的是String是有String常量的,所谓的String常量是直接使用“ ”符号定义的,两个内容相同的String常量使用“”进行比较范返回的是true,而两个String常量使用“”进行比较则返回false;对于Date类来说,是比较两个时间是否一致。
因此,在通常情况下重写类的equals()方法时,采用如下的步骤:
Object类中的hashCode方法用来返回对象的哈希值,多用于HashMap、HashSet等集合之中的数据查找,来提高对象查找的效率。从Object类的源码中可以看出,hashCode方法是一个native本地方法,返回一个int整数:
public native int hashCode();
而每个对象都会有一个hashCode返回值,这个int返回值一定会产生冲突的问题,从该本地方法的代码中可以看出,有多种生成hash值的策略,返回随机数、根据对象内存地址计算、输出内存地址、产生伪随机数等等。
由上面的内容可知,hashCode()方法用于产生所谓的对象的hash值来标识对象,但是可能会产生冲突,因此还需要equals()方法来协助继续比较两个对象才可以得出最终的结论。通常情况下,hashCode()方法能够直接判定出两个对象是否不同,若出现hash冲突的问题,则需要再次通过equals()方法判断,因此hashCode()方法和equals()方法联系非常紧密,需要遵守如下的规则:
根据前文所述,hashCode方法只负责计算得出一个int的hash值,如果两个对象的hash值发生了冲突,则需要通过equals方法中的逻辑继续比较。如果重写了equals方法,但是没有重写hashCode方法,则该类就继承了Object类中的hashCode方法,即使重写equals方法中的逻辑判断出两个对象是相等的,由于调用的是Object类中的hashCode方法,无法确保两个对象的hash值相同,因此在进行对象比较的时候,可能由于hash值不等被直接pass了,都不需要进行equals方法的比较,这样重写equals方法就失去了意义。
所谓的内存泄露问题指的是,当系统以为通过某些指令删除了指定内存地址上的对象时,这些对象事实上并没有被删除,导致这些内存地址无法被回收利用。
如HashSet的remove方法中,首先通过对象的hashCode值来比较对象并进行删除。若在该自定义类中重写了hashCode方法,并将对象的属性值参与到HashCode的计算之中,导致对象的hashCode发生变化,这样在remove对象时就无法准确找到原有的对象,导致对象删除失败内存泄露。
根据前文所述,Object类中的hashCode结果有多种计算策略,其中包括内存地址的计算方式,而Java的GC策略中,有多种策略会在GC后移动存活对象的内存地址,那么对象的hashCode是否会在GC后发生变化呢?
在JVM中有一个存储结构称为对象头,对象头中存储着hashCode的值,在hashCode方法未被调用时,存储的值为0,当hashCode首次计算后则会存储着hashCode的值,后续再次调用时直接从对象头中取出hashCode即可。因此,即使发生了GC,对象的内存地址改变了也不会影响hashCode的值。如在GC发生前调用过hashCode方法,则直接从对象头中取出值;若在GC发生后才调用hashCode方法,即使内存地址发生了改变,只会产生一个新的hashCode值,由于之前并没有生成后hashCode值,则不会存在hashCode的前后差异。
Object类中的toString()方法,返回的是String类型,返回的内容是该类的类型和它的引用地址,如下所示:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在一个类中可以自定义toString()方法,来自定义 输出内容。如String类中就重写了toString()方法,返回的是String字符串的值;如Integer包装类的toString()方法,也是将int数值包装成String对象返回,如下所示:
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
finalize()方法在该对象被垃圾回收之前调用,当我们显式地通过System.gc()进行垃圾回收时,会通知被回收的对象调用finalize()方法,因此子类中可以重写该方法,在垃圾回收前进行日志的记录。
Java中强调万物皆对象,但Java中有八种基本数据类型,这些数据类型并非属于类,只能作为数据进行计算,没有类的特点。在很多情况下无法与其他类进行交互,因此需要将这些数据类型包装为类,才能够与其他类进行交互。
下面是八种基本数据类型和对应的包装类:
如上文所述,所谓的包装类就是将基本数据类型包装在类中,因此也就需要将基本数据类型转换成包装类(装箱),也需要将包装类转换成基本数据类型(拆箱)。
装箱,可以通过包装类的构造方法以及静态方法valueOf():
//装箱
Integer t = new Integer(500);
Integer integer = Integer.valueOf(500);
拆箱,可以通过parseXXX()方法或xxxValue()方法取出基本数据类型:
//拆箱
int i = t.intValue();
int i1 = Integer.parseInt("500");
从JDK5之后,Java支持自动装箱、拆箱,因此在装箱与拆箱的写法上可以更加自由:
//自动装箱、拆箱
Integer t2 = 500;
int i2 = t2;
基本数据类型 -> String类,可通过String.valueOf()来生成String对象,也可以直接通过 基本数据类型+ ""的方式生成String对象:
//基本数据类型 ——> String类
String s = String.valueOf(123);
String s2 = 123 + "";
String类 -> 基本数据类型,可通过包装类的parseXXX()来获取基本数据类型:
//String类 -> 基本数据类型
int i3 = Integer.parseInt(s);
包装类 -> String类,可通过包装类对象的toString()方法生成String对象:
//包装类 -> String类
String s1 = t2.toString();
String类 -> 包装类,可通过该包装类的字符串形参的构造器生成,同样的包装类的valueOf静态方法也可以生成对应的包装类对象:
//String类 -> 包装类
Float aFloat = new Float("1.2F");
Integer integer1 = Integer.valueOf("1.2F");
因此,基本数据类型、包装类与String类之间可以通过装箱、拆箱不断转换,如下所示: