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(); // 喚醒在此對象監視器上等待的所有線程 } }