Java面試總結

前言

工作加實習兩年了,想總結和記錄這幾天的面試重點和題目,盡量把答案寫出來,由于大多網上搜的或者查閱書籍,如有錯誤還忘指出。
大多數好的公司面的比較有深度,主要還是要靠自學和積累,文章中答案都不深度討論,后續會細細分析。

基礎篇

集合和多線程是Java基礎的重中之重

1. Map 有多少個實現類?

Java自帶了各種 Map 類,這些 Map 類可歸為三種類型:

  • 通用 Map,用于在應用程序中管理映射,通常在 Java.util 程序包中實現:HashMap Hashtable Properties LinkedHashMap IdentityHashMap TreeMap WeakHashMap ConcurrentHashMap
  • 專用 Map,你通常不必親自創建此類 Map,而是通過某些其他類對其進行訪問:java.util.Attributes javax.print.attribute.standard.PrinterStateReasons java.security.Provider java.awt.RenderingHints javax.swing.UIDefaults
  • 一個用于幫助實現你自己的 Map 類的抽象類:AbstractMap

不用記住所有的,根據自己比較常用和了解的來回答就可以。

2. HashMap 是不是有序的?有序的 Map 類是哪個?Hashtable 是如何實現線程安全的?

HashMap 不是有序的,下一題通過源碼來解析HashMap的結構。

LinkedHashMap 是有序的,并且可以分為按照插入順序和訪問順序的鏈表,默認是按插入順序排序的。在創建的時候如果是new LinkedHashMap<String, String>(16,0.75f,true),true就代表按照訪問順序排序,那么調用 get 方法后,會將這次訪問的元素移至尾部,不斷訪問可以形成按訪問順序排序的鏈表。

根據源碼可以看到,Hashtable 中的方法都加上了 synchronized 關鍵字。

3. HashMap 的實現原理以及如何擴容?

眾所周知 HashMap 是數組和鏈表的結合,如下圖所示:

map.png

左邊是連續的數組,我們可以稱為哈希桶,右邊連接著鏈表。

具體如何實現我們來看源碼,在 HashMap 定義中有一個屬性Entry<K,V>[] table屬性,所以可以看到 HashMap 的實質是一個 Entry 數組。
看 HashMap 的初始化:

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];  //初始化
        initHashSeedAsNeeded(capacity);
    }

我們看到table = new Entry[capacity]這行,這行就是 HashMap 的初始化。capacity 是容量,容量的默認大小是16,最大不能超過2的30次方。然后我們來看 put() 方法:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);  //計算出key的hash值
        int i = indexFor(hash, table.length);  //通過容量來找到key對應哈希桶的位置
         //在對應的哈希桶上的鏈表查找是否有相同的key,如果有則覆蓋
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;
            //這里解釋了map就是根據對象的hashcode和equals方法來決定有沒有重復的key
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //添加一個鍵值對
        addEntry(hash, key, value, i);
        return null;
    }

可以看到,addEntry 才是真正在添加一個鍵值對,addEntry 方法中有擴容的操作,這個等會兒再看,所以我們直接看添加的鍵值對的操作:

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];    //拿到哈希桶中存放的地址
        //新建的entry指向剛剛那個地址,并且哈希桶指向新建的entry
        table[bucketIndex] = new Entry<>(hash, key, value, e);  
        size++;
}

以上注釋就說明當加入一個新鍵值對的時候,新的鍵值對找到對應的哈希桶之后就插入到鏈表的頭結點上。

接下來看擴容,我們說到數組有容量,默認16,在 HashMap 的定義中還有負載因子,默認為0.75,一旦數組存放的元素超過16*0.75=12個就需要增大哈希桶。我們看 addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);  //如果是默認16,則增大到32
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
 }

接下來看 resize 方法:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];  //創建新的數組,大小為原來數組的兩倍
        //將所有元素按照存放規則重新存放到新的數組中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));  
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

每次擴容都會重新計算所有 key 的哈希值以及將所有元素重排,比較浪費資源,所以在創建 HashMap 時,我們盡量初始化適當的容量以減少元素重排帶來的開支。

4. List的實現類以及區別?
  • ArrayList 是最常用的 List 實現類,內部是通過數組實現的,它允許對元素進行快速隨機訪問。數組的缺點是每個元素之間不能有間隔,當數組大小不滿足時需要增加存儲能力,就要將已經有數組的數據復制到新的存儲空間中。當從 ArrayList 的中間位置插入或者刪除元素時,需要對數組進行復制、移動、代價比較高。因此,它適合隨機查找和遍歷,不適合插入和刪除,允許空元素。
  • LinkedList 是用鏈表結構存儲數據的,很適合數據的動態插入和刪除,隨機訪問和遍歷速度比較慢。另外,接口中沒有定義的方法get、remove、insertList,專門用于操作表頭和表尾元素,可以當作堆棧、隊列和雙向隊列使用。LinkedList 沒有同步方法。如果多個線程同時訪問一個List,則必須自己實現訪問同步,一種解決方法是在創建List時構造一個同步的List:
    List list = Collections.synchronizedList(new LinkedList())
  • 應該避免使用 Vector,它只存在于支持遺留代碼的類庫中。
  • CopyOnWriteArrayList 是 List 的一個特殊實現,專門用于并發編程。
5. 如何實現線程并發?

用 Lock 接口的實現類或者 synchroized 關鍵字。

6. Lock 類比起 synchroized,優勢在哪里?

Lock 接口是 JavaSE5 引入的新接口,最大的優勢是為讀和寫分別提供了鎖。
延伸:如果需要實現一個高效的緩存,它允許多個用戶讀,但只允許一個用戶寫,以此來保證它的完整性,如何實現?
讀寫鎖 ReadWriteLock 擁有更加強大的功能,它可以分為讀鎖和解鎖。讀鎖可以允許多個進行讀操作的線程同時進入,但不允許寫進程進入;寫鎖只允許一個寫進程進入,在這期間任何進程都不能再進入。

要注意的是每個讀寫鎖都有掛鎖和解鎖,最好將每一對掛鎖和解鎖操作都用 try、finally 來套入中間的代碼,這樣就會防止因異常發生而造成死鎖的情況。

下面是一段示例:

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
    //用于關閉線程
    public volatile static boolean blag= true;
    public static void main(String[] args) throws InterruptedException {
        final TheData myData = new TheData();  //各線程的共享資源
        for (int i = 0; i < 3; i++) {   //開啟三個讀線程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(blag){
                        myData.get();
                    }
                }
            }).start();
        }
        for (int i = 0; i < 3; i++) {   //開啟三個寫線程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (blag) {
                        myData.put(new Random().nextInt(10000));
                    }
                }
            }).start();
        }
        Thread.sleep(5000);
        BLAG = false;
    }

}
/**
 * 模擬同步讀寫
 * @author hedy
 *
 */
class TheData{
    private Object data=null;
    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    public void get(){
        rwl.readLock().lock();   //讀鎖開啟,讀線程均可進入
        try {
            System.out.println(Thread.currentThread().getName()+" is ready to read");
            Thread.sleep(new Random().nextInt(100));
            System.out.println(Thread.currentThread().getName()+" have read data "+data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rwl.readLock().unlock();    //讀鎖解鎖
        }
    }
    
    public void put(Object data){
        rwl.writeLock().lock(); //寫鎖開啟,這時只有一個寫線程進入
        try {
            System.out.println(Thread.currentThread().getName()+" is ready to write");
            Thread.sleep(new Random().nextInt(100));
            this.data = data;
            System.out.println(Thread.currentThread().getName()+" have write data "+data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.writeLock().unlock();   //寫鎖解鎖
        }
        
    }
}
7. java中的 wait() 和 sleep() 方法有何不同?

最大的不同是在等待 wait 時會釋放鎖,而 sleep 一直持有鎖,wait
通常被用于線程間交互,sleep 通常被用于暫停執行。
其他不同有:

  • sleep 是 Thread 類的靜態方法,wait 是 Object 方法
  • wait,notify 和 notifyAll 只能在同步控制方法或者同步控制塊里面使用,而 sleep 可以在任何地方使用
  • sleep 必須捕獲異常,而 wait,notiry 和 notifyAll 不需要捕獲異常
8. 如何實現阻塞隊列(BlockingQueue)?

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列為空時,獲取元素的線程會等待隊列為飛控;當隊列滿時,存儲元素的線程會等待隊列可用。阻塞隊列常用于生產者和消費者的場景,生產者是往隊列里添加元素的線程,消費者是從隊列里拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器里拿元素。

阻塞隊列的簡單實現:

import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {
    
    private List<Object> queue = new LinkedList<Object>();  //存儲快
    private int limit = 10; //默認隊列大小
    public BlockingQueue(int limit){
        this.limit = limit;
    }
    public BlockingQueue() {}
    public synchronized void enQueue(Object item) throws InterruptedException{
        while (this.queue.size()==this.limit) {
            wait(); //很多資料上寫不需要捕獲異常,但看源碼還是有異常聲明
        }
        if (this.queue.size()==0) {
            notifyAll();
        }
        this.queue.add(item);   //元素添加到鏈表最后
    }

    public synchronized Object deQueue() throws InterruptedException{
        while (this.queue.size()==0){
            wait();
        }
        if (this.queue.size()==this.limit) {
            notifyAll();
        }
        return this.queue.remove(0);    //返回第一個數據,符合隊列先進先出原理
    }
}

延伸:利用 Executors 創建線程池中的消息隊列情況,ThreadPoolExecutor 是 Executors 的底層,建議使用 Executors ,想了解的可以自己查找資料。

9. 創建一千個線程對一個 static 的數據進行++,之后會不會是1000,為什么?如果將 static 的數據加上 volatile 呢?

首先我們直接用代碼執行一下看結果:

public class VolatileTest {

    public static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1000,4000, 
                                  60l, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        
        for (int i = 0; i < 4000; i++) {  //由于1000個線程結果測試對比不明顯,所以創建4000個
            executor.execute(new ThreadPoolTask());
        }
        executor.shutdown();  //執行完畢后關閉線程池
        while(!executor.isTerminated()){
            Thread.sleep(1000);
        }
        System.out.println(VolatileTest.count);  //線程都關閉之后輸出結果
    }
    
}
class ThreadPoolTask implements Runnable{

    @Override
    public void run() {
        VolatileTest.count++;
    }
    
}

結果都不盡相同,大約在4000以下徘徊。接下來分析一下原因。Java 內存模型是這樣的:

  • 所有的變量都存儲在主內存中
  • 每個線程都有自己獨立的工作內存,里面保證該線程是使用到的變量副本(即內存中變量的拷貝)

JVM 還規定:

  • 線程共享變量的所有操作必須在自己的工作內存中進行,不能直接從主內存中讀寫;
  • 不同線程之間無法直接訪問其他線程工作內存中的變量,線程間變量值得傳遞需要通過主內存來完成;

通過以上規定我們想象一下4000個線程在同時操作count++的場景:
首先 A 線程從主內存中拿到 count 值,比如說 0,放到了自己的工作內存中,在 A 沒處理完,B 線程就去主內存拿 count 值,也是 0,當 A、B 線程處理完之后將 count 值再放入主內存時 count 變成了1,實際我們要的結果是 2,這時候就出現了錯誤。
接下來,如果我們將 count 前加 volatile 關鍵字,運行結果依然不保證是4000。先了解一下線程的可見性和原子性的定義:

  • 可見性:一個線程對共享變量的修改,能夠及時的被其他線程見到
  • 原子性:一旦操作開始,那么它一定可以在可能發生的“上下文切換”之前執行完畢

而* volatile保證變量對線程的可見性,但不保證原子性 * ,如果要看volatile為什么不保證原子性可以看這篇文章

而如果把 count 改為 AtomicInteger 類型,則即可以保證對線程的可見性以及原子性。

10. 在 synchroized 方法上加 static 和不加 static 的區別是什么?

synchroized 在修飾方法時稱為對象鎖,加了 static 則是類鎖,對象鎖則是每個對象都有一把鎖,而類鎖只有一個,即所有對象在調用此方法的時候共同用同一把鎖。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容

  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,330評論 11 349
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區別 13、...
    Miley_MOJIE閱讀 3,722評論 0 11
  • http://python.jobbole.com/85231/ 關于專業技能寫完項目接著寫寫一名3年工作經驗的J...
    燕京博士閱讀 7,605評論 1 118
  • Java SE 基礎: 封裝、繼承、多態 封裝: 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,129評論 0 8
  • 從本篇文章中學到的最重要的概念 It is far more important for a stu...
    郭靜瑜37閱讀 212評論 2 1