Java的Object是所有引用类型的父类,定义的方法按照用途可以分为以下几种:
(1)构造函数
(2)hashCode() 和 equals() 函数用来判断对象是否相同
(3)wait()、wait(long)、wait(long,int)、notify()、notifyAll() 线程等待和唤醒
(4)toString()
(5)getClass() 获取运行时类型
(5)clone()
(6)finalize() 用于在垃圾回收。
这些方法经常会被问题到,所以需要记得。
由这几类方法涉及到的知识点非常多,我们现在总结一下根据这几个方法涉及的面试题。
1、对象的克隆涉及到的相关面试题目
涉及到的方法就是clone()。克隆就是为了快速构造一个和已有对象相同的副本。如果克隆对象,一般需要先创建一个对象,然后将原对象中的数据导入到新创建的对象中去,而不用根据已有对象进行手动赋值操作。
任何克隆的过程最终都将到达java.lang.Object 的clone()方法,而其在Object接口中定义如下
protected native Object clone() throws CloneNotSupportedException;
在面试中需要分清深克隆与浅克隆。克隆就是复制一个对象的复本。但一个对象中可能有基本数据类型,也同时含有引用类型。克隆后得到的新对象的基本类型的值修改了,原对象的值不会改变,这种适合shadow clone(浅克隆)。
如果你要改变一个非基本类型的值时,原对象的值却改变了,比如一个数组,内存中只copy地址,而这个地址指向的值并没有 copy。当clone时,两个地址指向了一个值。一旦这个值改变了,原来的值当然也变了,因为他们共用一个值。这就必须得用deep clone(深克隆)。举个例子如下:
public class ShadowClone implements Cloneable { private int a; // 基本类型 private String b; // 引用类型 private int[] c; // 引用类型 // 重写Object.clone()方法,并把protected改为public @Override public Object clone() { ShadowClone sc = null; try { sc = (ShadowClone) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return sc; } public int getA() { return a; } public void setA(int a) { this.a = a; } public String getB() { return b; } public void setB(String b) { this.b = b; } public int[] getC() { return c; } public void setC(int[] c) { this.c = c; } public static void main(String[] args) throws CloneNotSupportedException{ ShadowClone c1 = new ShadowClone(); //对c1赋值 c1.setA(50) ; c1.setB("test1"); c1.setC(new int[]{100}) ; System.out.println("克隆前c1: a="+c1.getA()+" b="+c1.getB()+" c="+c1.getC()[0]); ShadowClone c2 = (ShadowClone) c1.clone(); c2.setA(100) ; c2.setB("test2"); int []c = c2.getC() ; c[0]=500 ; System.out.println("克隆前c1: a="+c1.getA()+ " b="+c1.getB()+" c[0]="+c1.getC()[0]); System.out.println("克隆后c2: a="+c2.getA()+ " b="+c2.getB()+" c[0]="+c2.getC()[0]); } }
运行后打印如下信息:
克隆前c1: a=50 b=test1 c=100 克隆后c1: a=50 b=test1 c[0]=500 克隆后c2: a=100 b=test2 c[0]=500
c1与c2对象中的c数组的第1个元素都变为了500。需要要实现相互不影响,必须进行深copy,也就是对引用对象也调用clone()方法,如下实现深copy:
@Override public Object clone() { ShadowClone sc = null; try { sc = (ShadowClone) super.clone(); sc.setC(b.clone()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return sc; }
这样就不会相互影响了。
另外需要注意,对于引用类型来说,并没有在clone()方法中调用b.clone()方法来实现b对象的复制,但是仍然没有相互影响,这是由于Java中的字符串不可改变。就是在调用c1.clone()方法时,有两个指向同一字符串test1对象的引用,当调用c2.setB("test2")语句时,c2中的b指向了自己的字符串test2,所以就不会相互影响了。
2、hashCode()和equals()相关面试题目
equals()方法定义在Object类内并进行了简单的实现,如下:
public boolean equals(Object obj) { return (this == obj); }
比较两个原始类型比较的是内容,而如果比较引用类型的话,可以看到是通过==符号来比较的,所以比较的是引用地址,如果要自定义比较规则的话,可以覆写自己的equals()方法。 String 、Math、还有Integer、Double等封装类重写了Object中的equals()方法,让它不再简单的比较引用,而是比较对象所表示的实际内容。其实就是自定义我们实际想要比较的东西。比如说,班主任要比较两个学生Stu1和Stu2的成绩,那么需要重写Student类的equals()方法,在equals()方法中只进行简单的成绩比较即可,如果成绩相等,就返回true,这就是此时班主任眼中的相等。
首先来看第1道面试题目,手写equals()方法,在手写时需要注意以下几点:
当我们自己要重写equals()方法进行内容的比较时,可以遵守以下几点:
- 对于非float和double类型的原语类型域,使用==比较;
- 对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;
- 对于double域,使用Double.doubleToLongBits(adouble) 转换为int,再使用==比较;
- 对于对象引用域,递归调用equals()方法;
- 对于数组域,调用Arrays.equals()方法。
给一个字符串String实现的equals()实例,如下:
public boolean equals(Object anObject) { if (this == anObject) { // 反射性 return true; } 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; // 返回true时,表示长度相等,且字符序列中含有的字符相等 } } return false; }
另外的高频面试题目就是equals()和hashCode()之间的相互关系。
- 如果两个对象是相等的,那么他们必须拥有一样的hashcode,这是第一个前提;
- 如果两个对象有一样的hashcode,但仍不一定相等,因为还需要第二个要求,也就是equals()方法的判断。
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; }
循环中的每一步都对上一步的结果乘以一个系数31,选择这个数主要原因如下:
- 奇数 乘法运算时信息不丢失;
- 质数(质数又称为素数,是一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数) 特性能够使得它和其他数相乘后得到的结果比其他方式更容易产成唯一性,也就是hashCode值的冲突概率最小;
- 可优化为31 * i == (i << 5) - i,这样移位运算比乘法运算效率会高一些。
3、线程等待和唤醒相关面试题
// 仓库 class Godown { public static final int max_size = 100; // 最大库存量 public int curnum; // 当前库存量 Godown(int curnum) { this.curnum = curnum; } // 生产指定数量的产品 public synchronized void produce(int neednum) { while (neednum + curnum > max_size) { try { wait(); // 当前的生产线程等待,并让出锁 } catch (InterruptedException e) { e.printStackTrace(); } } // 满足生产条件,则进行生产,这里简单的更改当前库存量 curnum += neednum; System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum); notifyAll(); // 唤醒在此对象监视器上等待的所有线程 } // 消费指定数量的产品 public synchronized void consume(int neednum) { while (curnum < neednum) { try { wait(); // 当前的消费线程等待,并让出锁 } catch (InterruptedException e) { e.printStackTrace(); } } // 满足消费条件,则进行消费,这里简单的更改当前库存量 curnum -= neednum; System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum); notifyAll(); // 唤醒在此对象监视器上等待的所有线程 } }