「每日分享」論JDK源碼的重要性:一道面試題引發的無限思考

點擊上方"java全棧技術"關注,每天學習一個java知識點

那我們就看一下這道面試題是什麼呢?差不多是這樣子的面試題

題目的意思是:定義了兩個Integer類型變量,通過swap方法交換這兩個變量的值。

看似簡單的題目,是不是不知道從何下手,我猜想有些大家第一想到的是這樣的解法:來看代碼:

運行結果如下:

從結果來看是錯誤的,不能解決我們的問題。為什麼?

在分析之前,我們先介紹一下Java訪問對象的方式。在 Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、 實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。

既然java棧中的是對象的引用,那麼我們如何使用對象那,主流的訪問方式有兩種:使用句柄和直接指針

(1)使用句柄:

如果使用句柄訪問方式, Java 堆中將會劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息,如圖:

(2)直接指針

如果使用直接指針訪問方式, Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息, reference 中直接存儲的就是對象地址,如圖:

這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是 reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而 reference 本身不需要被修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在 Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。

接著我們回到正題,這裡也是今天要講的第一個知識點:Java的傳值在java中,有兩種傳值方式:一種是按值傳遞,一種是引用傳遞!

那麼,按值傳遞意味著將當前的參數傳遞給方法的時候,方法中的變量接收的是傳過來變量的副本值(相當於拷貝了一份值),因此,我們修改了方法裡面的變量的值,並不會改變外面變量的值。

引用傳遞:傳遞的是指向值的地址的指針

那麼,請問大家,這裡是按值傳遞還是引用傳遞?好,老司機告訴你們,這裡是按值傳遞,為什麼?Integer不是對象嗎? 對象傳遞不是傳遞的指針嗎?大家有沒有去看過Integer類的源碼,看看這個類是怎麼定義的,我們來看下,實際上面Integer使用的final定義的,也就意味著通過Integer實例化的對象是不能改變的,跟String是不是差不多。所以這裡的話,是傳遞的值,我們來畫下圖:

那麼,我們首先看一下Java運行時數據區域:

我們一般在開發中認為JVM不過有堆和棧兩部分組成,但是實際的Java 虛擬機在執行 Java 程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷燬的時間,有的區域隨著虛擬機進程的啟動而存在,有些區域則是依賴用戶線程的啟動和結束而建立和銷燬。如下圖:

Java中的內存主要分為兩塊把:堆和棧,棧存儲變量本身,堆存儲對象的值,然後通過棧執行堆內存地址來建立關係。

通過swap方法後:意味著,會同樣創建兩個變量num1和num2,他們的值是剛剛拷貝過來的ab的值,此時內存中時怎麼變化的呢:

大家,知道為什麼會有地址指針這個東西,主要是我們的堆內存他主要是存儲的是一些對象,對象是最佔內存的,為了能夠節省對內從的空間,就出現了這種概念。好,講到這裡,至少大家應該清楚了一點:引用傳遞和按值傳遞的不同。

我們再來看,這個Integer他內部是如何賦值的,我們來看下:進入Integer類Ctrl+o搜索Integer構造方法:

然後我們發現這個value定義的是final類型的:

如果他有一個setValue()的方法的話,那麼我們是是不是可以通過這個方法來改變值,但是Integer並沒有提供。也就是說這種方法是行不通的,好,那麼我們今天講到第二個知識點:反射有沒有人在做這個題目的時候有沒有想過用反射來實現?

有想過的,看有多少人有往這個方面去想,我們剛剛看到Integer類中存在一個value值變量嗎?對吧,所以我們需要拿到這個value變量然後來改變他的值,對吧,那麼我們怎麼來做,我們可以通過反射的方式拿到這個變量,這個Filed,然後去改變他的值,對吧。我們來看下怎麼寫:

理論上來說,這種方式是一定能夠實現我們的要求的.Run下:報錯:“Class com.edu.example.test.Test can not access a member of class java.lang.Integer with modifiers "private final"”

報錯了,是不是,那麼這又是另外一個知識點:

私有的成員屬性是不能通過反射來賦值的!

那麼,如果要強攻,怎麼辦?實際上面,在java反射中,提供了一個叫設置訪問權限的東西,我們進入Field類中看下:

然後他裡面有一個setAccessible的方法:

這個方法就是用來設置成員屬性訪問權限的。我們看到最後是給obj.override=flag

那麼我們在回過頭來看下,Field的set方法:

這幾行代碼意味著,也就是說,如果override是false,就會調用Reflection.quickCheckMemberAccess(clazz, modifiers)來檢查成員屬性的訪問權限。

所以說,我們再來看,這個時候是不是就可以通過設置setAccessible(true)為true來標誌不需要訪問權限的檢查。這樣就可以修改value的值了。對不。我們來試驗下:

好,大家覺得這樣沒問題,結果如下:

結果是,a的值確實變了,但是b的值卻沒有變,首先說明通過這種方式確實可以改變值,但是為什麼b的值沒有變化呢?。請問為什麼?我們再回過頭來看看外面的方法,檢查一下,我們定義了:

有沒有發現什麼問題?

Integer是不是一個封裝類型,而他的值1,2,是不是一個int類型,是一個基本數據類型,那麼這裡是怎麼賦值的呢? 那麼我們按照正常來寫是不是這樣子的:

int a = 1;

但是為什麼使用Integer也不會報錯了,好,這就講到了我們又一個知識點:(筆記)

Java中的裝箱和拆箱

裝箱:把基本類型用它們相應的引用類型包裝起來,使其具有對象的性質。int包裝成Integer、float包裝成Float;

拆箱:和裝箱相反,將引用類型的對象簡化成值類型的數據;

Integer a = 100; // 這是自動裝箱 (編譯器調用的是static Integer valueOf(int i))

int b = new Integer(100); //這是自動拆箱

那麼我們來實際看下,我們耳聽為虛,眼見為實,我們來看下編譯的字節碼文件:

命令:javap -c Test.class

可以看到:

Jvm他自動做了裝箱操作,看的清清楚楚對吧,對吧

好那麼,我們來看下Integer.valueOf(1):源碼

意味著值大於IntegerCace.low小於IntegerCache.high的話:

會從IntegerCache中獲取,也就是從緩存中取值。

那麼我們來看下IntegerCache:

也就是說從-128到127直接的所有值,都是從緩存中獲取。而緩存中的值,是什麼時候放進去的,是jvm啟動的時候就放進去了,然後分配好內存地址。

你們有沒有發現,就短短几行代碼,怎麼就有這麼多知識,是不是都有點感覺不認識java了。很神奇吧,哈哈好,前面這兩行代碼我們分析完了對吧,好,然後,然後我們把ab的值傳進來,我們再來分析swap中的這段代碼,好吧,精華部分就是這段代碼了啊,這是精華部分,哈哈,我們來看:斷點到這句

然後按F5進去看下,把IntegerCache裡面的值全面拿出來放到notepat++

第一步:是不是需要獲取num2的值,那麼他從下標[2+128=130]IntegerCache中獲取值為:130下標,也就是第131個數字為:2

第二步:field.set(num1,num2),, 意味著第一步先獲取num1在IntegerCache中的值IntegerCache[1+128] =1 ,然後會修改IntegerCache[num1]的值為num2從Integercache中獲取到的值2, 也就是修改為:integerCache[129] = 2

第三步:下一行代碼執行

此時,再次拿出IntegerCache, 那麼下標為129,130的值都變成了2, 此時tmp的值為1,那麼從IntegerCache獲取到的值為IntegerCache[1+128=129] ,也就是獲取130行的數,也就是2,所以結果就是這樣。實際上面和下面這個是一樣的:

從這一句debug進去:發現走的緩存,然後從cache中第129個下標找到了。

所以,當我們的值是在【-127-128】的時候,他是從IntegerCache中獲取的。其實,我們可以這樣來驗證一下:

結果為:true

結果為:false

那麼,這個當時我其實又遇到這個坑,被坑慘了是吧。哈哈哈。

那麼我們怎麼解決最後的問題:(最初的面試問題)

1.

2.

3.取巧的方式: