Java面試系列第2篇-Object類中的方法

歸去來兮辭發表於2020-04-26

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()方法進行內容的比較時,可以遵守以下幾點: 

(1)使用instanceof 操作符檢查“實參是否為正確的型別”。
(2)對於類中的每一個“關鍵域”,檢查實參中的域與當前物件中對應的域值。
  • 對於非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()方法的判斷。
我覺得如上2句的總結必須要有一個非常重要的前提,就是要在使用hashcode進行雜湊的前提下,否則談不上equals()相等,hashcode一定相等這種說法。
對於使用hashcode的map來說,map判斷物件的方法就是先判斷hashcode是否相等,如果相等再判斷equals方法是否返回true,只有同時滿足兩個條件,最後才會被認為是相等的。
Map查詢元素比線性搜尋更快,這是因為map利用hashkey去定位元素,這個定位查詢的過程分成兩步,內部原理中,map將物件儲存在類似陣列的陣列的區域,所以要經過兩個查詢,先找到hashcode相等的,然後在再在其中按線性搜尋使用equals方法,通過這2步來查詢一個物件。 
另外還有在書寫hashCode()方法時,為什麼要用31這個數字? 例如String類的hashCode()的實現如下:
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、執行緒等待和喚醒相關面試題  

最常見的面試題就是sleep()與wait()方法的區別,這個問題很簡單,呼叫sleep()方法不會釋放鎖,而呼叫wait()方法會阻塞當前執行緒並釋放當前執行緒持有的鎖。。
另外就是問wait()與notify()、notifyAll()方法相關的問題了,比如這幾個方法為什麼要定義在Object類中,一句話,因為Java中所有的物件都能當成鎖,也就是監視器物件。
我們需要明白,呼叫這幾個方法時,當前執行緒一定要持有鎖,否則呼叫這幾個方法會引起異常(也是一道面試題)。
有時候還需要書寫生產者-消費者模式,我們就用wait()與notify()、notifyAll()方法寫一個吧,如下:
// 倉庫
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(); // 喚醒在此物件監視器上等待的所有執行緒
	}
}
在同步方法開始時都會測試,如果生產了過多或不夠消費時,呼叫wait()方法阻塞當前執行緒並讓鎖。在同步方法最後都會呼叫notifyAll()方法,這算是給所有執行緒一個公平競爭鎖的機會吧,他會喚醒在synchronized方法和wait()上阻塞等待的執行緒,因為他們都將當前物件做為鎖物件。
 
 
 
 
 
 
 

 

相關文章