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


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()上阻塞等待的線程,因為他們都將當前對象做為鎖對象。
 
 
 
 
 
 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM