【Java開發(fā)】Java面試七大專題第2篇:7. ArrayList,8. Iterator【附代碼文檔】

??????教程全知識點簡介:基礎(chǔ)篇 1. 二分查找 2. 冒泡排序 7. ArrayList 8. Iterator 9. LinkedList 10. HashMap 1)基本數(shù)據(jù)結(jié)構(gòu) 2)樹化與退化 3)索引計算 4)put 與擴容 5)并發(fā)問題 11. 單例模式 并發(fā)篇 1. 線程狀態(tài) 3. wait vs sleep 4. lock vs synchronized 虛擬機篇 1. JVM 內(nèi)存結(jié)構(gòu) 4. 內(nèi)存溢出 5. 類加載 6. 四種引用 7. finalize 框架篇 1. Spring refresh 流程 2. Spring bean 生命周期 6. Spring 注解 7. SpringBoot 自動配置原理 數(shù)據(jù)庫篇 1. 隔離級別 2. 快照讀與當(dāng)前讀 3. InnoDB vs MyISAM 4. 索引 索引基礎(chǔ) 5. 查詢語句執(zhí)行流程 6. undo log 與 redo log 7. 鎖 緩存篇 1. Redis 數(shù)據(jù)類型 2. keys 命令問題 3. 過期 key 的刪除策略 5. 緩存問題 6. 緩存原子性 7. LRU Cache 實現(xiàn) 分布式篇 1. CAP 定理 2. Paxos 算法 4. Gossip 協(xié)議 5. 分布式通用設(shè)計 6. 一致性 Hash(補充)


??????????code git倉庫:???https://gitee.com/xiaoshuai112/Backend/blob/master/Java/Java面試七大專題/note.md 直接get????

? 本教程項目亮點

?? 知識體系完整:覆蓋從基礎(chǔ)原理、核心方法到高階應(yīng)用的全流程內(nèi)容
?? 全技術(shù)鏈覆蓋:完整前后端技術(shù)棧,涵蓋開發(fā)必備技能
?? 從零到實戰(zhàn):適合 0 基礎(chǔ)入門到提升,循序漸進掌握核心能力
?? 豐富文檔與代碼示例:涵蓋多種場景,可運行、可復(fù)用
?? 工作與學(xué)習(xí)雙參考:不僅適合系統(tǒng)化學(xué)習(xí),更可作為日常開發(fā)中的查閱手冊
?? 模塊化知識結(jié)構(gòu):按知識點分章節(jié),便于快速定位和復(fù)習(xí)
?? 長期可用的技術(shù)積累:不止一次學(xué)習(xí),而是能伴隨工作與項目長期參考


??????全教程總章節(jié)


??????本篇主要內(nèi)容

7. ArrayList

要求

  • 掌握 ArrayList 擴容規(guī)則

擴容規(guī)則

  1. ArrayList() 會使用長度為零的數(shù)組

  2. ArrayList(int initialCapacity) 會使用指定容量的數(shù)組

  3. public ArrayList(Collection<? extends E> c) 會使用 c 的大小作為數(shù)組容量

  4. add(Object o) 首次擴容為 10,再次擴容為上次容量的 1.5 倍

  5. addAll(Collection c) 沒有元素時,擴容為 Math.max(10, 實際元素個數(shù)),有元素時為 Math.max(原容量 1.5 倍, 實際元素個數(shù))

其中第 4 點必須知道,其它幾點視個人情況而定

提示

  • 測試代碼見 day01.list.TestArrayList ,這里不再列出
  • 注意的是,示例中用反射方式來更直觀地反映 ArrayList 的擴容特征,但從 JDK 9 由于模塊化的影響,對反射做了較多限制,需要在運行測試代碼時添加 VM 參數(shù) --add-opens java.base/java.util=ALL-UNNAMED 方能運行通過,后面的例子都有相同問題

代碼說明

  • day01.list.TestArrayList#arrayListGrowRule 演示了 add(Object) 方法的擴容規(guī)則,輸入?yún)?shù) n 代表打印多少次擴容后的數(shù)組長度

8. Iterator

要求

  • 掌握什么是 Fail-Fast、什么是 Fail-Safe

Fail-Fast 與 Fail-Safe

  • ArrayList 是 fail-fast 的典型代表,遍歷的同時不能修改,盡快失敗

  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍歷的同時可以修改,原理是讀寫分離

提示

  • 測試代碼見 day01.list.FailFastVsFailSafe,這里不再列出

9. LinkedList

要求

  • 能夠說清楚 LinkedList 對比 ArrayList 的區(qū)別,并重視糾正部分錯誤的認知

LinkedList

  1. 基于雙向鏈表,無需連續(xù)內(nèi)存
  2. 隨機訪問慢(要沿著鏈表遍歷)
  3. 頭尾插入刪除性能高
  4. 占用內(nèi)存多

ArrayList

  1. 基于數(shù)組,需要連續(xù)內(nèi)存
  2. 隨機訪問快(指根據(jù)下標訪問)
  3. 尾部插入、刪除性能可以,其它部分插入、刪除都會移動數(shù)據(jù),因此性能會低
  4. 可以利用 cpu 緩存,局部性原理

代碼說明

  • day01.list.ArrayListVsLinkedList#randomAccess 對比隨機訪問性能
  • day01.list.ArrayListVsLinkedList#addMiddle 對比向中間插入性能
  • day01.list.ArrayListVsLinkedList#addFirst 對比頭部插入性能
  • day01.list.ArrayListVsLinkedList#addLast 對比尾部插入性能
  • day01.list.ArrayListVsLinkedList#linkedListSize 打印一個 LinkedList 占用內(nèi)存
  • day01.list.ArrayListVsLinkedList#arrayListSize 打印一個 ArrayList 占用內(nèi)存

10. HashMap

要求

  • 掌握 HashMap 的基本數(shù)據(jù)結(jié)構(gòu)
  • 掌握樹化
  • 理解索引計算方法、二次 hash 的意義、容量對索引計算的影響
  • 掌握 put 流程、擴容、擴容因子
  • 理解并發(fā)使用 HashMap 可能導(dǎo)致的問題
  • 理解 key 的設(shè)計

1)基本數(shù)據(jù)結(jié)構(gòu)

  • 1.7 數(shù)組 + 鏈表
  • 1.8 數(shù)組 + (鏈表 | 紅黑樹)

更形象的演示,見資料中的 hash-demo.jar,運行需要 jdk14 以上環(huán)境,進入 jar 包目錄,執(zhí)行下面命令

java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar

2)樹化與退化

Java Util Logging

樹化意義

  • 紅黑樹用來避免 DoS ,防止鏈表超長時性能下降,樹化應(yīng)當(dāng)是偶然情況,是保底策略
  • hash 表的查找,更新的時間復(fù)雜度是 O(1),而紅黑樹的查找,更新的時間復(fù)雜度是 O(log_2?n ),TreeNode 占用空間也比普通 Node 的大,如非必要,盡量還是使用鏈表
  • hash 值如果足夠隨機,則在 hash 表內(nèi)按泊松分布,在負載因子 0.75 的情況下,長度超過 8 的鏈表出現(xiàn)概率是 0.00000006,樹化閾值選擇 8 就是為了讓樹化幾率足夠小

樹化規(guī)則

  • 當(dāng)鏈表長度超過樹化閾值 8 時,先嘗試擴容來減少鏈表長度,如果數(shù)組容量已經(jīng) >=64,才會進行樹化

退化規(guī)則

  • 情況1:在擴容時如果拆分樹時,樹元素個數(shù) <= 6 則會退化鏈表
  • 情況2:remove 樹節(jié)點時,若 root、root.left、root.right、root.left.left 有一個為 null ,也會退化為鏈表

3)索引計算

索引計算方法

JDK 21 API 文檔

  • 首先,計算對象的 hashCode()
  • 再進行調(diào)用 HashMap 的 hash() 方法進行二次哈希
    • 二次 hash() 是為了綜合高位數(shù)據(jù),讓哈希分布更為均勻
  • 最后 & (capacity – 1) 得到索引

數(shù)組容量為何是 2 的 n 次冪

  1. 計算索引時效率更高:如果是 2 的 n 次冪可以使用位與運算代替取模
  2. 擴容時重新計算索引效率更高: hash & oldCap == 0 的元素留在原來位置 ,否則新位置 = 舊位置 + oldCap

注意

SpringDoc OpenAPI

  • 二次 hash 是為了配合 容量是 2 的 n 次冪 這一設(shè)計前提,如果 hash 表的容量不是 2 的 n 次冪,則不必二次 hash
  • 容量是 2 的 n 次冪 這一設(shè)計計算索引效率更好,但 hash 的分散性就不好,需要二次 hash 來作為補償,沒有采用這一設(shè)計的典型例子是 Hashtable

4)put 與擴容

put 流程

  1. HashMap 是懶惰創(chuàng)建數(shù)組的,首次使用才創(chuàng)建數(shù)組
  2. 計算索引(桶下標)
  3. 如果桶下標還沒人占用,創(chuàng)建 Node 占位返回
  4. 如果桶下標已經(jīng)有人占用
    1. 已經(jīng)是 TreeNode 走紅黑樹的添加或更新邏輯
    2. 是普通 Node,走鏈表的添加或更新邏輯,如果鏈表長度超過樹化閾值,走樹化邏輯
  5. 返回前檢查容量是否超過閾值,一旦超過進行擴容

1.7 與 1.8 的區(qū)別

  1. 鏈表插入節(jié)點時,1.7 是頭插法,1.8 是尾插法

  2. 1.7 是大于等于閾值且沒有空位時才擴容,而 1.8 是大于閾值就擴容

  3. 1.8 在擴容計算 Node 索引時,會優(yōu)化

擴容(加載)因子為何默認是 0.75f

  1. 在空間占用與查詢時間之間取得較好的權(quán)衡
  2. 大于這個值,空間節(jié)省了,但鏈表就會比較長影響性能
  3. 小于這個值,沖突減少了,但擴容就會更頻繁,空間占用也更多

5)并發(fā)問題

擴容死鏈(1.7 會存在)

1.7 源碼如下:

JSON-B API

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • e 和 next 都是局部變量,用來指向當(dāng)前節(jié)點和下一個節(jié)點
  • 線程1(綠色)的臨時變量 e 和 next 剛引用了這倆節(jié)點,還未來得及移動節(jié)點,發(fā)生了線程切換,由線程2(藍色)完成擴容和遷移
  • 線程2 擴容完成,由于頭插法,鏈表順序顛倒。但線程1 的臨時變量 e 和 next 還引用了這倆節(jié)點,還要再來一遍遷移
  • 第一次循環(huán)
    • 循環(huán)接著線程切換前運行,注意此時 e 指向的是節(jié)點 a,next 指向的是節(jié)點 b
    • e 頭插 a 節(jié)點,注意圖中畫了兩份 a 節(jié)點,但事實上只有一個(為了不讓箭頭特別亂畫了兩份)
    • 當(dāng)循環(huán)結(jié)束是 e 會指向 next 也就是 b 節(jié)點
  • 第二次循環(huán)
    • next 指向了節(jié)點 a

Eclipse IDE 文檔

  • e 頭插節(jié)點 b
  • 當(dāng)循環(huán)結(jié)束時,e 指向 next 也就是節(jié)點 a
  • 第三次循環(huán)
    • next 指向了 null
    • e 頭插節(jié)點 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死鏈已成
    • 當(dāng)循環(huán)結(jié)束時,e 指向 next 也就是 null,因此第四次循環(huán)時會正常退出

數(shù)據(jù)錯亂(1.7,1.8 都會存在)

  • 代碼參考 day01.map.HashMapMissData,具體調(diào)試步

11. 單例模式

要求

  • 掌握五種單例模式的實現(xiàn)方式
  • 理解為何 DCL 實現(xiàn)時要使用 volatile 修飾靜態(tài)變量
  • 了解 jdk 中用到單例的場景

餓漢式

public class Singleton1 implements Serializable {
    private Singleton1() {
        if (INSTANCE != null) {
            throw new RuntimeException("單例對象不能重復(fù)創(chuàng)建");
        }
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}
  • 構(gòu)造方法拋出異常是防止反射破壞單例
  • readResolve() 是防止反序列化破壞單例

枚舉餓漢式

NetBeans 文檔

public enum Singleton2 {
    INSTANCE;

    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
  • 枚舉餓漢式能天然防止反射、反序列化破壞單例

懶漢式

Caffeine 文檔

PowerMock 文檔

public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // Singleton3.class
    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容