1. 程式人生 > 實用技巧 >Java面試知識點整理

Java面試知識點整理

目錄

==、equals()以及hashcode()

ref: ==、equals()、hashcode()

  1. ==比較的是物件地址(引用),看是否為同一物件
    Integer與int會自動拆箱、Integer和String的快取(Integer預設-128~127,String快取用過的、但new的就是new的)
  2. equals()比較的是內容
    但Object預設是使用==實現,Java內建類都已重寫,自定義的類也需要重寫
  3. hashcode應跟隨equals()的重寫
    equals重寫後hashcode也應該重寫(Java規範,但不是語法級的),因為兩個物件equals,則他們的hashcode也應該相同。
    Object預設的hashcode方法是native的,是根據物件地址進行計算的。自定義類重寫hashcode方法見自定義類如何重寫hashcode()方法

加號"+"

  1. Java中“+”有兩個作用:加法和字串拼接。
    1.1 當“+”兩邊的運算元至少有一個是字串時,“+”的作用是字串拼接,底層實現是呼叫new StringBuilder().append()方法,當有大量的“+”時會生成很多StringBuilder物件
    1.2 當“+”兩邊都是數值型運算元時,“+”的作用是加法計算
    1.3 注意char不是字串,char在記憶體中是以整數形式存在的。

  2. 輸出集合元素時,除了字串拼接的方法(+、StringBuilder.append),還可以考慮stream終端操作。
    String的+是呼叫了new StringBuilder().append()方法

String、StringBuilder、StringBuffer

  1. String
    String的value[]陣列是final的,不可修改,對string進行拼接/修改時會建立新的String物件。

  2. StringBuffer
    StringBuffer正是為了解決修改String產生過多中間物件的問題(@Since JDK1.0),提供append、replace、insert、delete等方法,但它是執行緒安全的,開銷較大;

  3. StringBuilder
    StringBuilder(@Since JDK1.5)是為了解決StringBuffer開銷大的問題,去掉了StringBuffer的synchronized,,更高效;
    StringBuffer 和 StringBuilder二者都繼承自AbstractStringBuilder ,底層都是利用可修改的char陣列。

  4. 用哪一個
    少量字串操作還是用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操作

JDK8 Stream 資料流效率分析

  1. 怎麼用
  • 資料來源:Collection、I/O流
  • 中間操作:map()、filter()、distinct()、sorted()、limit()、mapToInt()、boxed()……
  • 終端操作:收集為Collection、收集為Array、收集為String、count統計元素數量、max統計最大值、min統計最小值……
  1. 特點
  • 只遍歷一次,流水線操作
  • 內部迭代,而Collector的Iterator是一種外部迭代
  1. 什麼時候用
  • 在小規模的集合(<10000)中,stream的效率其實不如iterator,但遍歷開銷基本上都小於1毫秒,即使是成倍的差距也可忽略不計;
    而stream的寫法要比iterator高效且易讀,尤其是要進行多種操作時。
  • 在超大規模的集合中(>1000w),stream的遍歷效率要好於iterator,但也不會好太多,parallelStream會好很多(前提時能用到多核)。

由ArrayList相關延伸出

  1. 為什麼elementData用transient修飾,這樣不就不能序列化了嗎?
    ArrayList中elementData在快取中會預留一些空間(capacity-size),實際只有size個element需要被序列化,直接序列化會使capacity-size的那部分也被序列化。如果需要序列化,使用ArrayList.writeObject(ObjectOutputStream)和readObject(ObjectInputStream)方法即可。

  2. 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,它的基本原理是什麼?

  1. 在此之前先了解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。
  1. 明確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操作)。
  1. 基本原理
  • 對於上述第一點,變數的可見性,主要是通過volatile變數寫操作的兩個特性實現的:
    (1) 修改volatile變數時,會強制將修改後的值重新整理到主記憶體中;
    (2) 修改volatile變數後,會導致其他執行緒工作記憶體中對應的變數值失效,從而不得不從主記憶體中重新讀取。

  • 第二點,根據Java編譯器的重排序以及JSR定義的happen-before規則

具體參考refJava 併發程式設計:volatile的使用及其原理

  1. 與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++

  1. 將class檔案通過javap反編譯得到可讀的位元組碼:
    javap -p -v Test.class
  2. 理解區域性變量表和運算元棧各負責什麼JVM 棧幀(Stack Frame)
  3. 再理解為什麼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?

推薦閱讀

美團技術團隊