Java面試知識點整理
目錄
==、equals()以及hashcode()
==
比較的是物件地址(引用),看是否為同一物件
Integer與int會自動拆箱、Integer和String的快取(Integer預設-128~127,String快取用過的、但new的就是new的)equals()
比較的是內容
但Object預設是使用==實現,Java內建類都已重寫,自定義的類也需要重寫hashcode
應跟隨equals()
的重寫
equals重寫後hashcode也應該重寫(Java規範,但不是語法級的),因為兩個物件equals,則他們的hashcode也應該相同。
Object預設的hashcode方法是native的,是根據物件地址進行計算的。自定義類重寫hashcode方法見自定義類如何重寫hashcode()方法
加號"+"
-
Java中“+”有兩個作用:加法和字串拼接。
1.1 當“+”兩邊的運算元至少有一個是字串時,“+”的作用是字串拼接,底層實現是呼叫new StringBuilder().append()方法,當有大量的“+”時會生成很多StringBuilder物件
1.2 當“+”兩邊都是數值型運算元時,“+”的作用是加法計算
1.3 注意char不是字串,char在記憶體中是以整數形式存在的。 -
輸出集合元素時,除了字串拼接的方法(+、StringBuilder.append),還可以考慮stream終端操作。
String的+是呼叫了new StringBuilder().append()方法
String、StringBuilder、StringBuffer
-
String
String的value[]陣列是final的,不可修改,對string進行拼接/修改時會建立新的String物件。 -
StringBuffer
StringBuffer正是為了解決修改String產生過多中間物件的問題(@Since JDK1.0
),提供append、replace、insert、delete等方法,但它是執行緒安全的,開銷較大; -
StringBuilder
StringBuilder(@Since JDK1.5
)是為了解決StringBuffer開銷大的問題,去掉了StringBuffer的synchronized,,更高效;
StringBuffer 和 StringBuilder二者都繼承自AbstractStringBuilder ,底層都是利用可修改的char陣列。 -
用哪一個
少量字串操作還是用String,易讀;
需要大量拼接/修改操作時,根據是否需要執行緒安全來選擇StringBuffer和StringBuilder。
使用StringBuffer 和 StringBuilder時注意能預知大小就在new的時候設定好capacity,避免擴容開銷(二者預設capacity為16,擴容時會用Arrays.copyOf()丟棄原有陣列然後建立新的陣列)。
從JVM的字串常量池理解String.intern()
ref1: 從字串到常量池,一文看懂String類
- class檔案的結構
- class常量池中存的是字面量和符號引用,也就是說他們存的並不是物件的例項,經過解析(resolve)之後,才會把符號引用替換為直接引用
不同版本的JVM記憶體模型 - 字串常量池中實際存放的是字串的引用,而不是字串例項
- String s = new String("1")是怎麼產生兩個物件的
- JDK1.6和JDK1.7對intern方法的實際操作區別(主要是1.7將駐留字串例項都存放在堆中,可以直接讓一個常量池引用指向駐留字串,而不用將堆中的字串物件拷貝到沒有該字串的永久代中)
- String s1 = new String("1") + new String("1")實際還呼叫了StringBuilder.append()方法
- JDK1.6的永久代、1.7/1.8的元空間都是對方法區的不同實現
ref2: 美團-深入解析String.intern()
- Java為了提高執行速度及節省記憶體,為8種基本型別和String都提供了常量池;
- 基本型別的常量池是系統協調的,字串常量池有兩種使用方法:雙引號引用,String.intern()
- JDK1.7之前,字串常量池位於Perm區,而Perm區預設只有4M;由於字串常量池太佔Perm區空間,1.7將字串常量池移到堆中。
- 1.7中呼叫intern()時,不再複製字串到Perm區的字串常量池中(然後StringTable中一個item指向這個駐留字串),而是讓堆中字串常量池的StringTable指向這個物件,使其成為駐留字串;
- String pool是通過維護StringTable指向字串來維護字串常量池的,StringTable的實現與HashMap相似,只是不能擴容,預設StringTable的陣列容量只有1009,如果使用intern往StringTable中放入了過多的指向駐留字串的引用,就會產生Hash衝突,連結串列增長,導致繼續呼叫intern()時效能大幅下降。
stream操作
- 怎麼用
- 資料來源:Collection、I/O流
- 中間操作:map()、filter()、distinct()、sorted()、limit()、mapToInt()、boxed()……
- 終端操作:收集為Collection、收集為Array、收集為String、count統計元素數量、max統計最大值、min統計最小值……
- 特點
- 只遍歷一次,流水線操作
- 內部迭代,而Collector的Iterator是一種外部迭代
- 什麼時候用
- 在小規模的集合(<10000)中,stream的效率其實不如iterator,但遍歷開銷基本上都小於1毫秒,即使是成倍的差距也可忽略不計;
而stream的寫法要比iterator高效且易讀,尤其是要進行多種操作時。 - 在超大規模的集合中(>1000w),stream的遍歷效率要好於iterator,但也不會好太多,parallelStream會好很多(前提時能用到多核)。
由ArrayList相關延伸出
-
為什麼elementData用transient修飾,這樣不就不能序列化了嗎?
ArrayList中elementData在快取中會預留一些空間(capacity-size),實際只有size個element需要被序列化,直接序列化會使capacity-size的那部分也被序列化。如果需要序列化,使用ArrayList.writeObject(ObjectOutputStream)和readObject(ObjectInputStream)方法即可。 -
ArrayList不是執行緒安全的,需要執行緒安全可選擇使用Vector或者CopyOnWriteArrayList。
這裡理解一下CopyOnWrite ,Java提供了兩個利用這個機制實現的執行緒安全集合——CopyOnWriteArrayList和opyOnWriteArraySet。
CopyOnWrite,就是寫時複製,例如ArrayList.add()方法中,先getArray()將array暫存到newElements中,修改newElements,之後再setArray(newElements)。這是一種讀寫分離的思想,讀和寫操作的是不同的集合,因此缺點也是很明顯的,其他執行緒還是有可能讀取到舊的資料。
另外,add時會使用ReentrantLock加鎖,不然會有多個執行緒複製出多個副本(引用)來,但即使這樣,頻繁地增刪改還是會複製很多副本,因此CopyOnWriteArrayList適合讀多改少的情景。
另外,值得注意的是,CopyOnWriteArrayList是沒有capacity的概念的,這也是可以通過add方法看出來的。
還有,CopyOnWriteArrayList的元素陣列array是volatile的,可以保證一個執行緒對array的修改對其他執行緒是立即可見的。
volatile與JVM
繼續上面提到的volatile,它的基本原理是什麼?
- 在此之前先了解Java記憶體模型JMM
- 主記憶體與執行緒的工作記憶體間的互動,8種操作的原子性(虛擬機器沒有直接將其中lock和unlock提供給使用者,但往上對應的synchronized可對應)
- 從主記憶體讀取變數到工作記憶體要用到哪些操作、工作記憶體將修改後的變數寫回主記憶體要用到哪些操作;
- 操作重排序: read a; load a; read b; load b; -> read a; read b; load b; load a;
- volatile變數禁止指令重排序優化。
- 對long和double(64位)普通變數的8種操作不保證原子性,JLS建議執行緒間共享的long和double變數宣告為volatile。
- 明確volatile的作用:
public class VolatileTest {
// boolean flag = true;
volatile boolean flag = true;
int i = 0;
public static void main(String[] args) throws Exception {
VolatileTest volatileTest = new VolatileTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (volatileTest.flag) {
volatileTest.i++;
}
System.out.println(volatileTest.i); //flag不是volatile的話,不會執行到這一步的
}
});
thread.start();
Thread.sleep(2000);
volatileTest.flag = false;
System.out.println("volatileTest.i = " + volatileTest.i);
}
}
ref:volatile關鍵字的作用
- volatile保證了當一個執行緒修改了變數時(該執行緒將工作區中的變數副本store&write到主記憶體中了),強制其他使用到這個變數的執行緒去主記憶體中讀取新值。
- volatile可以保證單次讀(read&load)/寫(store&write)操作的原子性(通過禁止指令重排序優化),但像i++這樣的複合操作是不能保證原子性的(i++用到了read&load&use&assign&store&write操作)。
- 基本原理
-
對於上述第一點,變數的可見性,主要是通過volatile變數寫操作的兩個特性實現的:
(1) 修改volatile變數時,會強制將修改後的值重新整理到主記憶體中;
(2) 修改volatile變數後,會導致其他執行緒工作記憶體中對應的變數值失效,從而不得不從主記憶體中重新讀取。 -
第二點,根據Java編譯器的重排序以及JSR定義的happen-before規則
具體參考refJava 併發程式設計:volatile的使用及其原理
- 與synchronized
4.1 多執行緒併發過程主要處理原子性、可見性、有序性問題;
synchronized三者都可以保證:
(1) 原子性:synchronized經過編譯之後,對應的是class檔案中的monitorenter和monitorexit這兩個位元組碼指令。這兩個位元組碼對應的記憶體模型的操作是lock(上鎖)和unlock(解鎖),而這兩個操作之間的執行都是原子的。
(2) 可見性:synchronized要求的對變數unlock之前,必須將變數值從工作記憶體更新到主記憶體。
另一方面,其他執行緒的工作記憶體中並不會存在該變數的副本,所以也就不存在舊值問題。
(3) 有序性:當執行緒執行到synchronized程式碼塊內時,其他執行緒無法獲得鎖、不能進入該程式碼塊,此時程式碼塊的執行就是單執行緒的,而指令重排序導致執行緒不安全是多執行緒同時執行情況下的。
refsynchronized為什麼具有可見性,原子性,有序性?
而根據上面對volatile的分析,它只能保證可見性和有序性,並不能保證原子性。
4.2 volatile和synchronized的區別
- 原子性: 如上所述,volatile不能保證,而synchronized可以保證;
- 可見性:首先是寫回:修改當前volatile變數後必須將修改後的值寫回到主記憶體,而synchronized要求的是unlock後寫回(即使沒有更新?);
再者是其他執行緒:修改volatile變數會致使其他執行緒工作記憶體中的變數值失效,而synchronized由於lock的存在,其他執行緒工作記憶體是沒有該變數的。 - 有序性:volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化,但由於同一時刻只有一個執行緒在執行該段程式碼,即使被優化,還是能保證有序性的。
- 使用範圍:volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的。
- 效能:volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
從JVM棧幀理解i=i++
- 將class檔案通過javap反編譯得到可讀的位元組碼:
javap -p -v Test.class
- 理解區域性變量表和運算元棧各負責什麼JVM 棧幀(Stack Frame)
- 再理解為什麼i=i++後,i的值不變java中i=i++問題解析
其實關鍵問題出在iinc指令上。
指令iinc對給定的區域性變數做自增操作,這條指令是少數幾個執行過程中完全不修改運算元棧的指令。它接收兩個運算元:
第1個區域性變量表的位置,第2個位累加數。比如常見的i++,就會產生這條指令
Java虛擬機器常用指令
這樣,執行i++時,先把i的值從區域性變量表中load到運算元棧中,iinc直接在區域性變量表中實現i自增;接著執行i=…的時候,把運算元棧的棧頂(未增)賦值給i了。
此處一個推測,執行++i時,先是執行iinc在區域性變量表中實現了自增,再將增後的i load到運算元棧中。
題外話,觀察位元組碼檔案,iinc指令和下一條指令的編號相差3,這個3是指需要下一層的3個指令才能完成iinc?