国产无遮挡裸体免费直播视频,久久精品国产蜜臀av,动漫在线视频一区二区,欧亚日韩一区二区三区,久艹在线 免费视频,国产精品美女网站免费,正在播放 97超级视频在线观看,斗破苍穹年番在线观看免费,51最新乱码中文字幕

Java集合框架的Collection分支詳解

 更新時(shí)間:2026年01月28日 10:25:51   作者:AuYeung.歐陽  
本文主要介紹了Java集合框架中的Collection接口及其三個(gè)核心子接口List、Set和Queue,分別詳細(xì)介紹了ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet等實(shí)現(xiàn)類的特性和用法,感興趣的朋友跟隨小編一起看看吧

Collection 接口概覽

在Java集合框架中,Collection 是最基礎(chǔ)的根接口。需要注意的是,雖然 Map 同屬集合框架范疇,但它與Collection并非繼承關(guān)系,而是兩個(gè)獨(dú)立的頂級(jí)接口。

Collection 接口定義在 java.util 包中,是一個(gè)泛型接口,繼承自 Iterable 接口。

package java.util;
// 繼承了 Iterable,意味著所有 Collection 實(shí)現(xiàn)類都可以使用 for-each 循環(huán)
public interface Collection<E> extends Iterable<E> {
    // .......
}

從架構(gòu)層面來看,Collection 接口主要擴(kuò)展為三大核心子接口:

  • List:有序、可重復(fù)的集合
  • Set:無序、不可重復(fù)的集合
  • Queue:隊(duì)列,通常遵循FIFO(先進(jìn)先出)規(guī)則

Collection 定義了所有單列集合共有的操作。根據(jù)功能可分為四大類:添加、刪除、查詢(判斷)及其他操作:

添加操作

方法簽名描述注意事項(xiàng)
boolean add(E e)向集合中添加一個(gè)元素 e

若集合發(fā)生變化(例如 Set 成功插入元素),則返回 true;若集合未發(fā)生變化(例如 Set 中已存在該元素),則返回 false

boolean addAll(Collection<? extends  E> c)將指定集合 c 中的所有元素添加到當(dāng)前集合

相當(dāng)于將另一個(gè)集合合并到當(dāng)前集合中

刪除操作

方法簽名描述注意事項(xiàng)
boolean remove(Object e)刪除集合中指定的單個(gè)元素 o如果存在并刪除成功返回 true;不存在返回false
boolean removeAll(Collection<? extends E> c)刪除當(dāng)前集合中包含在集合 c 里的所有元素執(zhí)行差集運(yùn)算(當(dāng)前集合減去集合c)
void clear()清空集合中所有元素集合中的元素清空,但集合對象本身還存在
default boolean removeIf(Predicate<? super E> filter)(JDK 8 新增) 刪除滿足給定條件的所有元素配合 Lambda 表達(dá)式使用,非常強(qiáng)大

查詢與判斷

方法簽名描述注意事項(xiàng)
int size()返回集合中元素的個(gè)數(shù)最大值是 Integer.MAX_VALUE(2^{31} - 1)
boolean isEmpty()判斷集合是否為空(size == 0建議用此方法判斷,而不是 size() == 0,因?yàn)檎Z義更清晰
boolean contains(Object o)判斷集合是否包含指定元素 o底層依賴 equals() 方法
boolean containsAll(Collection<?> c)判斷當(dāng)前集合是否包含集合 c 中的所有元素

   判斷當(dāng)前集合是否包含子集 c

數(shù)組轉(zhuǎn)換與遍歷

方法簽名描述注意事項(xiàng)
Object[] toArray()將集合轉(zhuǎn)換為Object類型數(shù)組類型丟失,通常不推薦使用
T[] toArray(T[] a)將集合轉(zhuǎn)換為指定類型的數(shù)組

推薦使用。當(dāng)數(shù)組 a 容量不足時(shí),JDK 會(huì)自動(dòng)創(chuàng)建新的數(shù)組

Iterator iterator()返回在此集合上進(jìn)行迭代的迭代器用于遍歷和刪除元素

自 JDK 8 起,Collection 接口新增了多個(gè) default 方法,顯著提升了集合操作的函數(shù)式編程能力和使用便捷性:

  • default boolean removeIf(Predicate filter):用于刪除滿足條件的元素
  • default Stream<E> stream():用于將集合轉(zhuǎn)換為流(Stream)。它返回一個(gè)順序流(非并行流)
  • default Spliterator<E> spliterator():為集合創(chuàng)建一個(gè)分割迭代器,用于并行遍歷

注意:以上方法在 List 和 Set 中的具體行為可能略有不同(例如 List允許元素重復(fù),add操作始終返回true;而Set不允許重復(fù)元素,當(dāng)嘗試添加已存在元素時(shí)會(huì)返回false)。

一句話總結(jié):Collection 定義了單列集合 “能干什么”,而 List、Set、Queue 定義了 "怎么干"(具體的行為約束)。

Collection 家族詳解

1. List

List 是一個(gè)有序的集合(序列),可以精確控制每個(gè)元素的插入位置。用戶可以通過索引來訪問元素。

核心特點(diǎn):

  • 有序:元素按插入順序排列,每個(gè)元素都有索引。
  • 可重復(fù):可以存儲(chǔ)任意多個(gè)相同的元素。

1.1 ArrayList

ArrayList 是 List 接口的一個(gè)可變長數(shù)組實(shí)現(xiàn)。它可以動(dòng)態(tài)地增長和縮減其容量,以容納任意數(shù)量的的元素。

核心特點(diǎn):

  • 基于數(shù)組實(shí)現(xiàn):其底層是一個(gè) Object[] 數(shù)組,這決定了它的大部分性能特性。
  • 有序:元素按照插入順序排列,每個(gè)元素都有一個(gè)精確的索引(從 0 開始)。
  • 可重復(fù):可以存儲(chǔ)任意數(shù)量的重復(fù)元素,包含 null。
  • 非線程安全:多個(gè)線程同時(shí)修改一個(gè) ArrayList 實(shí)例時(shí),需要外部同步。否則可能會(huì)導(dǎo)致數(shù)據(jù)不一致。
底層原理:動(dòng)態(tài)數(shù)組的奧秘

ArrayList 的精髓在于其 "動(dòng)態(tài)" 二字,那它是如何實(shí)現(xiàn)一個(gè)可以動(dòng)態(tài)擴(kuò)容的數(shù)組呢?

核心存儲(chǔ):elementData 與 size        

要理解ArrayList,首先要區(qū)分兩個(gè)看似相似但意義完全不同的概念:容量 大小。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默認(rèn)初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 共享的空數(shù)組實(shí)列
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //默認(rèn)大小的空數(shù)組實(shí)列
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 用于存儲(chǔ)元素的數(shù)組緩沖區(qū)。ArrayList 的容量就是這個(gè)數(shù)組的長度。
    // transient 關(guān)鍵字表示該字段不會(huì)被序列化
    transient Object[] elementData;
    // ArrayList 中包含的元素?cái)?shù)量
    private int size;
    // ..........
}
  • elementData(容量):這是ArrayList的倉庫。它是一個(gè) Object[] 數(shù)組,其長度(elementData.length)代表了 ArrayList 當(dāng)前最多能夠裝多少個(gè)元素。
  • size(大小):這是 ArrayList 的庫存清單。它是一個(gè) int 值,記錄了倉庫中實(shí)際存放了多少個(gè)元素。它永遠(yuǎn)小于或等于 elementData.length.

 誕生之初

ArrayList 提供了三種構(gòu)造函數(shù),它們決定了 elementData 的初始狀態(tài)。

 ArrayList()

這是最常用的無參構(gòu)造函數(shù)。

public ArrayList(){
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

這里關(guān)鍵點(diǎn)在于理解為什么會(huì)初始化一個(gè)空數(shù)組,而不是一個(gè)有默認(rèn)容量10 的數(shù)組。這是一種延遲初始化(懶加載)的優(yōu)化策略。在舊版本(JDK1.6及之前)中,它會(huì)直接創(chuàng)建一個(gè)容量為10的數(shù)組。這就意味著,即使你創(chuàng)建了一個(gè)ArrayList,但馬上就丟棄了,或者只往里面放了一兩個(gè)元素,系統(tǒng)也會(huì)預(yù)先分配10個(gè)對象引用的內(nèi)存空間,這可能就造成了內(nèi)存浪費(fèi)。故而從JDK1.7開始,ArrayList的實(shí)現(xiàn)進(jìn)行了優(yōu)化,采用了懶加載策略。當(dāng)你 new ArrayList() 時(shí),它只是將 elementData 指向一個(gè)共享的空數(shù)組,此時(shí)幾乎沒有占用額外的內(nèi)存空間(除對象本身的少量開銷)。真正的容量10 是在第一次添加元素時(shí)進(jìn)行分配的,ArrayList 會(huì)檢查當(dāng)前的 elementData 是不是那個(gè)默認(rèn)的空數(shù)組,如果是,它就會(huì)為這個(gè)數(shù)組分配初始容量的內(nèi)存空間,然后將新元素放入其中。

//JDK 1.6
public ArrayList() {
        this(10); // 直接調(diào)用帶容量的構(gòu)造函數(shù),傳入 10
}
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity]; // 立即創(chuàng)建一個(gè)長度為 10 的數(shù)組
}

 ArrayList(int initialCapacity)

這個(gè)構(gòu)造函數(shù)接受一個(gè)整數(shù)參數(shù),用于初始化 ArrayList 的底層數(shù)組大小。

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 如果初始容量大于0,創(chuàng)建對應(yīng)大小的Object數(shù)組
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 如果初始容量為0,使用預(yù)定義的空數(shù)組常量
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 如果初始容量為負(fù)數(shù),拋出非法參數(shù)異常
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

當(dāng)你知道大概要存儲(chǔ)多少元素時(shí),可以預(yù)先指定容量,避免頻繁擴(kuò)容帶來的性能開銷。

注意:

  • 初始容量過小會(huì)導(dǎo)致頻繁擴(kuò)容,影響性能
  • 初始容量過大會(huì)浪費(fèi)內(nèi)存空間

ArrayList(Collection<? extends E> c)

這個(gè)構(gòu)造函數(shù)接收一個(gè)實(shí)現(xiàn)Collection接口的集合作為參數(shù),用于創(chuàng)建一個(gè)新的ArrayList實(shí)列。

public ArrayList(Collection<? extends E> c) {
    // 使用Collection的toArray()方法將傳入的集合轉(zhuǎn)換為數(shù)組
    Object[] a = c.toArray();
    // 將數(shù)組長度賦值給 size,同時(shí)判斷數(shù)組長度是否為0
    // 如果數(shù)組長度不等于 0,則向下繼續(xù)執(zhí)行
    // 否則,就使用預(yù)定義的空數(shù)組EMPTY_ELEMENTDATA
    if ((size = a.length) != 0) {
        //判斷集合本身是否是ArrayList
        //如果是,就直接使用其內(nèi)部數(shù)組(elementData)
        //否則,使用Arrays.copyOf()創(chuàng)建一個(gè)新的數(shù)組副本
        //其目的是為了保證新創(chuàng)建的ArrayList的內(nèi)部數(shù)組完全獨(dú)立,不受原集合影響
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        elementData = EMPTY_ELEMENTDATA;
    }
}

該構(gòu)造函數(shù)主要用于將其他類型的集合轉(zhuǎn)換為ArrayList、創(chuàng)建一個(gè)已存在集合的ArrayList副本和快速初始化一個(gè)包含特定元素的ArrayList。在使用時(shí)需要注意類型安全的問題,雖然使用了<? extends E>通配符,但在編譯時(shí)會(huì)進(jìn)行類型檢查,如果傳入的集合包含不兼容的類型,在編譯時(shí)會(huì)報(bào)錯(cuò)。

擴(kuò)容機(jī)制

擴(kuò)容是 ArrayList 最核心、也最耗時(shí)的操作。接下來通過add(E e)方法追蹤下 ArrayList 的擴(kuò)容過程。

步驟 1:添加元素 add(E e)

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 關(guān)鍵:檢查容量是否足夠
    elementData[size++] = e; // 將元素放入數(shù)組,并增加 size
    return true;
}

在放入元素前,它會(huì)先調(diào)用ensureCapacityInternal( size +1 ),其目的是在放入第 size + 1 個(gè)元素前,檢查下容量夠不夠。

步驟 2:容量檢查 ensureCapacityInternal(size + 1)

private void ensureCapacityInternal(int minCapacity) {
    // 如果elementData 是否是默認(rèn)的空數(shù)組
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果是,則將最小容量設(shè)置為默認(rèn)容量(10)和請求容量中的較大值
        // 確保了第一次添加元素時(shí),數(shù)組至少有10個(gè)元素的空間
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

這就是 延遲初始化(懶加載)的體現(xiàn):第一次調(diào)用add(E e)方法時(shí),minCapacity會(huì)被直接提升到10,為后續(xù)的元素預(yù)留空間

步驟 3:觸發(fā)擴(kuò)容判斷 ensureExplicitCapacity(int minCapacity)

 private void ensureExplicitCapacity(int minCapacity) {
    // 修改計(jì)數(shù)器,用于迭代器的快速失敗機(jī)制
    // 快速失敗機(jī)制(fail-fast):是一種錯(cuò)誤檢測機(jī)制。當(dāng)在迭代過程中,
    // 如果檢測到集合結(jié)構(gòu)被修改(非迭代器自身的方法導(dǎo)致的修改)                                                               
    // 迭代器會(huì)立即拋出ConcurrentModificationException異常,
    // 而不是繼續(xù)執(zhí) 行可能導(dǎo)致不確定行為。
    modCount++;
    // 檢查是否需要擴(kuò)容 (所需最小容量大于當(dāng)前數(shù)組的長度)
    if (minCapacity - elementData.length > 0) 
        // 執(zhí)行擴(kuò)容操作 grow(int minCapacity)
        grow(minCapacity);
}

步驟 4:擴(kuò)容的核心 grow(int minCapacity)

private void grow(int minCapacity) {
    // overflow-conscious code
    // 舊容量      
    int oldCapacity = elementData.length; 
    // 新容量 = 舊容量 + 舊容量/2 (即 1.5 倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    // 如果 1.5 倍后還不夠,就用所需的最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 處理極端情況,防止新容量超過 Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 關(guān)鍵:復(fù)制數(shù)組
    elementData = Arrays.copyOf(elementData, newCapacity);
}

性能的權(quán)衡:為何擴(kuò)容因子是1.5倍:

  • 避免頻繁擴(kuò)容:如果擴(kuò)容因子過?。ㄈ?1.1 倍),那么擴(kuò)容會(huì)非常頻繁,容易導(dǎo)致大量的數(shù)組復(fù)制開銷。
  • 避免內(nèi)存浪費(fèi):如果擴(kuò)容因子過大(如 2.0 倍,Vector的選擇),可能會(huì)導(dǎo)致一次擴(kuò)容后預(yù)留大量未用空間,造成內(nèi)存浪費(fèi)。對于超大列表,這種內(nèi)存浪費(fèi)是驚人的。
  • 數(shù)學(xué)上的合理性:1.5 倍(oldCapacity + (oldCapacity >> 1))可以通過位運(yùn)算快速完成,比乘法效率略高。它在空間和時(shí)間成本之間取得了很好的平衡。

Arrays.copyOf():

它是java.util.Arrays 工具類中的一個(gè)核心方法,主要功能是復(fù)制一個(gè)指定的數(shù)組,并可以指定新數(shù)組的長度,從而實(shí)現(xiàn)截取和擴(kuò)容。這個(gè)方法是ArrayList 動(dòng)態(tài)數(shù)組實(shí)現(xiàn)擴(kuò)容的關(guān)鍵。

Arrays.copyOf()的內(nèi)部工作流:

  1. 創(chuàng)建新的數(shù)組:在堆內(nèi)存中開辟一塊新的、連續(xù)的內(nèi)存空間,大小為newCapacity。
  2. 數(shù)據(jù)復(fù)制:使用System.arraycopy() 這個(gè)本地方法,將 elementData 中的所有元素(從索引0 至 size -1 ) 按字節(jié)拷貝到新的數(shù)組中。這是一個(gè)O(n)的操作。
  3. 引用切換:將 elementData 的引用指向新的數(shù)組,舊數(shù)組會(huì)在下一次GC時(shí)被回收。
使用建議
  • 擴(kuò)容操作涉及數(shù)組復(fù)制,相對耗時(shí)。如果能預(yù)估元素?cái)?shù)量,建議在初始化時(shí)指定容量,避免多次擴(kuò)容。
  • 1.5 倍的擴(kuò)容策略也存在可能導(dǎo)致內(nèi)存浪費(fèi),如不需要添加元素時(shí),可以使用 trimToSize() 釋放多余空間。
  • ArrayList不是線程安全的,在多線程環(huán)境下使用可能會(huì)導(dǎo)致出現(xiàn)數(shù)據(jù)不一致的情況。

1.2 Vector

它是最早的集合類之一,是線程安全列表的鼻祖。然而在如今的現(xiàn)代java開發(fā)中,它的身影卻是越來越少見了,甚至被貼上了 “過時(shí)” 的標(biāo)簽。Vector 與 ArrayList 非常相似,它也是一個(gè)基于動(dòng)態(tài)數(shù)組實(shí)現(xiàn)的 List 。它同樣是有序的、允許重復(fù)元素和 null 值的。

Vector 最獨(dú)一無二的特點(diǎn)是:它是線程安全的。

這就意味著,它的所有公共方法都被 synchronized 關(guān)鍵字修飾。當(dāng)一個(gè)線程訪問 Vector 的任何一個(gè)方法時(shí),它會(huì)先獲取 Vector 實(shí)例的對象鎖,其他試圖訪問的線程必須等待,直至鎖被釋放。

public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}
public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}
public synchronized void removeAllElements() {
    modCount++;
    // Let gc do its work
    for (int i = 0; i < elementCount; i++)
        elementData[i] = null;
    elementCount = 0;
}
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
}
public synchronized E set(int index, E element) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);
    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
    elementData[--elementCount] = null; // Let gc do its work
    return oldValue;
}
//...........

性能分析:

Vector的性能瓶頸在于其粗粒度的鎖。即使只是進(jìn)行一個(gè)簡單的 get() 讀操作,也需要獲取整個(gè)對象的鎖,這會(huì)阻塞所有其他線程的讀和寫操作。在并發(fā)度稍高的場景下,這種串行化的執(zhí)行方式會(huì)成為嚴(yán)重的性能瓶頸。

底層原理:加鎖的動(dòng)態(tài)數(shù)組

Vector 的底層實(shí)現(xiàn)與 ArrayList 高度相似,都是基于一個(gè)可動(dòng)態(tài)擴(kuò)容的數(shù)組。

核心成員變量

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    protected Object[] elementData; // 存儲(chǔ)元素的數(shù)組
    protected int elementCount; // 實(shí)際元素?cái)?shù)量(相當(dāng)于 ArrayList 中的 size)
    protected int capacityIncrement; // 容量增長系數(shù)
    private static final long serialVersionUID = -2767605614048989439L;
    // 定義一個(gè)數(shù)組理論上的最大容量上限
    // 注意:這個(gè) 8 不是一個(gè)魔法數(shù)字,它是基于對主流 JVM 實(shí)現(xiàn)對象內(nèi)存布局的經(jīng)驗(yàn)值。
    // 它足夠大,可以覆蓋大多數(shù)虛擬機(jī)中數(shù)組對象的額外開銷。
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 
    //...........
}

與 ArrayList 相比,Vector 多了一個(gè) capacityIncrement 字段。這個(gè)字段用于自定義擴(kuò)容時(shí)的增長量。如果設(shè)置為 0 (默認(rèn)值),則容量翻倍。

擴(kuò)容機(jī)制

Vector 的擴(kuò)容機(jī)制與 ArrayList 類似,都是在容量不足時(shí)創(chuàng)建一個(gè)新數(shù)組并復(fù)制舊數(shù)據(jù)。但關(guān)鍵區(qū)別在于擴(kuò)容的倍數(shù)。

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length; // 獲取當(dāng)前數(shù)組容量 
    // 計(jì)算新容量 newCapacity:
    // 如果設(shè)置了capacityIncrement(擴(kuò)容增量),則新容量 = 舊容量 + capacityIncrement
    // 如果沒有設(shè)置capacityIncrement(默認(rèn)為0),則新容量 = 舊容量的2倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    // 檢查新容量是否滿足最小需求,如果不滿足則使用minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 檢查新容量是否超過最大數(shù)組限制(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 使用Arrays.copyOf創(chuàng)建新數(shù)組并復(fù)制原有元素
    elementData = Arrays.copyOf(elementData, newCapacity);
}

這是 Vector 特有的擴(kuò)容機(jī)制,與 ArrayList 不同(ArrayList 默認(rèn) 1.5 倍)。

capacityIncrement 可以在構(gòu)造 Vector 時(shí)指定,用于控制每次擴(kuò)容的增長量。

方法 hugeCapacity(minCapacity) 處理容量超過Integer.MAX_VALUE - 8的情況。

為什么 Vector 會(huì)被棄用?

Vector 被標(biāo)記為 “過時(shí)”,并非是因?yàn)橛蠦ug,而是有了更好的代替方案。綜上所述,因?yàn)?Vector 所有的公共方法都被 synchronized 關(guān)鍵字修飾,所以其帶來的性能開銷在并發(fā)的場景下是難以接受的。

更好的替代品:

  • Collections.synchronizedList(new ArrayList<>()):它是一個(gè)包裝器,它和Vector一樣,也是通過synchronized為每個(gè)方法加鎖保證線程安全。它的出現(xiàn),使開發(fā)者可以用更靈活的ArrayList來獲得線程安全能力,而不必鎖在Vector這個(gè)具體的實(shí)現(xiàn)上。
  • java.util.concurrent.CopyOnWriteArrayList:這是JDK 1.5 引入的并發(fā)集合。它采用 “寫時(shí)復(fù)制”策略,實(shí)現(xiàn)了讀無操作鎖,極大提升了讀多寫少場景下的并發(fā)性能。是對Vector的降維打擊。

1.3 LinkedList

LinkedList 是基于節(jié)點(diǎn)的集合,其中的每個(gè)元素都包含了對前一個(gè)元素和后一個(gè)元素的引用。這種結(jié)構(gòu)就像是一列火車,每個(gè)車廂都連接著前一節(jié)和后一節(jié)車廂。

核心特點(diǎn):

  • 基于雙向鏈表實(shí)現(xiàn):與 ArrayList 相比,這是最根本的區(qū)別。
  • 有序:元素按插入順序排列。
  • 可重復(fù):可以存儲(chǔ)任意相同元素和 null 值。
  • 非線程安全:與 ArrayList 一樣,不是線程安全的。
  • 雙重身份:它不僅實(shí)現(xiàn)了 List 接口,還實(shí)現(xiàn)了 Deque 接口(雙端隊(duì)列),這意味著它可以被當(dāng)作隊(duì)列來使用。
底層原理:雙向鏈表的節(jié)點(diǎn)藝術(shù)

LinkedList 的優(yōu)雅與強(qiáng)大,完全隱藏在其精巧的節(jié)點(diǎn)結(jié)構(gòu)和指針操作之中。

基石:Node<E> 內(nèi)部類

LinkedList 的一切都始于這個(gè)私有的靜態(tài)內(nèi)部類。它不僅是一個(gè)基礎(chǔ)的數(shù)據(jù)容器,更是一個(gè)具備 “雙向感知” 功能的連接節(jié)點(diǎn)。

private static class Node<E> {
    E item; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)據(jù)
    Node<E> next; // 指向下一個(gè)節(jié)點(diǎn)的引用
    Node<E> prev; // 指向上一個(gè)節(jié)點(diǎn)的引用
    // 構(gòu)造函數(shù):建立車廂之間的連接
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element; // 設(shè)置當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
        this.next = next; // 設(shè)置下一個(gè)節(jié)點(diǎn)
        this.prev = prev; // 設(shè)置上一個(gè)節(jié)點(diǎn)
    }
}

雙向鏈表節(jié)點(diǎn)由三部分組成:存儲(chǔ)的數(shù)據(jù)(item)、指向前驅(qū)節(jié)點(diǎn)的指針(prev)以及指向后繼節(jié)點(diǎn)的指針(next)。

宏觀結(jié)構(gòu):first、last和size

LinkedList 類本身并不直接存儲(chǔ)元素,而是通過三個(gè)核心成員變量來管理整個(gè)鏈表的狀態(tài)。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0; // 記錄LinkedList中元素的數(shù)量,初始化為0,表示空鏈表
    transient Node<E> first; // 頭指針,指向鏈表的第一個(gè)節(jié)點(diǎn),若鏈表為空則為 null
    transient Node<E> last; // 尾指針,指向鏈表的最后一個(gè)節(jié)點(diǎn),若鏈表為空則為 null
    //........
}

這種結(jié)構(gòu)讓頭部和尾部的操作變得非常高效,因?yàn)榭梢灾苯油ㄟ^指針訪問。

性能分析:快與慢的另一面

操作類型時(shí)間復(fù)雜度原因分析
訪問( get(int index)O(n)數(shù)組可以通過索引直接計(jì)算地址,但鏈表必須從 first 或 last 開始,一個(gè)一個(gè)地向前或向后遍歷,直至查找到目標(biāo)索引。
頭部/尾部添加(addFirst/addLastO(1)只需要修改 first 或 last 的引用,并讓新節(jié)點(diǎn)指向舊的頭部/尾部即可,不涉及數(shù)據(jù)移動(dòng)。
中間插入(add(int index, E e)O(n)雖然插入本身是 O(1)(只需修改前后節(jié)點(diǎn)的指針),但找到插入位置需要 O(n) 的時(shí)間去遍歷。
頭部/尾部刪除(removeFirst/removeLastO(1)同頭部/尾部添加,只需修改 first 或 last 的引用。
中間刪除(remove(int index)O(n)與中間插入同理,主要時(shí)間消耗在找到待刪除節(jié)點(diǎn)上。
查找 (contains(Object o))O(n)必須從頭到尾遍歷整個(gè)鏈表。

訪問( get(int index) )

public E get(int index) {
    // 檢查傳入索引是否有效
    checkElementIndex(index); 
    // 如果索引有效,調(diào)用 node(int index) 方法找到對應(yīng)索引得節(jié)點(diǎn),并返回找到的節(jié)點(diǎn)的 item 字段
    return node(index).item; 
}
private void checkElementIndex(int index) {
    // 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException的異常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    // 判斷索引是否在0到size-1范圍內(nèi)
    return index >= 0 && index < size;
}
Node<E> node(int index) {
    // 根據(jù)索引與鏈表長度的關(guān)系決定是從頭開始遍歷還是從尾開始遍歷,以提高查找效率
    // 如果index小于size的一半(size >> 1 (size/2))
    if (index < (size >> 1)) {
        Node<E> x = first;  // 從頭節(jié)點(diǎn)開始
        for (int i = 0; i < index; i++) // 向后遍歷index次
            x = x.next;
        return x;
    // 否則從last節(jié)點(diǎn)開始向前遍歷
    } else {
        Node<E> x = last; // 從尾節(jié)點(diǎn)開始
        for (int i = size - 1; i > index; i--)  // 向前遍歷(size-1-index)次
            x = x.prev;
        return x;
    }
}

鏈表的get操作時(shí)間復(fù)雜度為O(n) .因?yàn)榭赡苄枰闅v鏈表。且索引必須在 0 到 size-1 范圍內(nèi),否則會(huì)拋出異常。如若在多線程環(huán)境下使用,需要外部同步,因?yàn)樵摲椒ú皇蔷€程安全的。

頭部/尾部添加 (addFirst/addLast)

public void addFirst(E e){
    linkFirst(e); // 將實(shí)際元素 e 鏈接到列表開頭
}
public void linkFirst(E e){
    // 保存當(dāng)前頭節(jié)點(diǎn)的引用
    final Node<E> f = first; 
    // 創(chuàng)建新的節(jié)點(diǎn),它的 prev 是 null,next 是舊的頭節(jié)點(diǎn) f
    final Node<E> newNode = new Node<>(null, e, f); 
    // 讓 first 指針指向新節(jié)點(diǎn),新節(jié)點(diǎn)成為新的頭
    first = newNode;
    // 處理邊界情況:如果鏈表原來為空
    if (f == null)
        // 鏈表為空時(shí),新節(jié)點(diǎn)既是頭也是尾
        last = newNode;
    else
        // 如果鏈表不為空,讓舊頭節(jié)點(diǎn)的 prev 指向新節(jié)點(diǎn)
        f.prev = newNode;
    size++; // 增加 size 大小
    modCount++; // 修改計(jì)數(shù)器,用于快速失敗
}
public void addLast(E e){
    linkLast(e); // 將實(shí)際元素 e 鏈接到列表尾部
}
void linkLast(E e){
    // 保存當(dāng)前尾部節(jié)點(diǎn)的引用
    final Node<E> f = last;
    // 創(chuàng)建新的節(jié)點(diǎn),它的 prev 是 舊的尾部節(jié)點(diǎn) l,next 是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 讓 last 指針指向新節(jié)點(diǎn),新節(jié)點(diǎn)成為新的尾部
    last = newNode;
    // 處理邊界情況:如果鏈表原來為空
    if (l == null)
        // 鏈表為空時(shí),新節(jié)點(diǎn)既是頭也是尾
        first= newNode;
    else
        // 如果鏈表不為空,讓舊尾部節(jié)點(diǎn)的 next 指向新節(jié)點(diǎn)
        l.next= newNode;
    size++; // 增加 size 大小
    modCount++; // 修改計(jì)數(shù)器,用于快速失敗
}

addFirst / addLast 方法本身是一個(gè)簡單的包裝器,實(shí)際操作操作是由 linkFirst / linkLast 完成。linkFirst / linkLast 方法通常會(huì)在鏈表數(shù)據(jù)結(jié)構(gòu)中實(shí)現(xiàn),它會(huì)在鏈表頭部/尾部插入一個(gè)新的節(jié)點(diǎn),并將新節(jié)點(diǎn)的 next / prev 引用指向原 頭節(jié)點(diǎn)/尾節(jié)點(diǎn),然后更新鏈表的 頭節(jié)點(diǎn)/尾節(jié)點(diǎn) 為新節(jié)點(diǎn)。

在指定位置插入元素( add(int index,E e) )

public void add(int index, E element){
    // 檢查索引是否越界
    checkPositionIndex(index);
    // 如果插入位置是末尾
    if(index == size)
        linkLast(element); //調(diào)用linkLast 方法添加至尾部
    else
        linkBefore(element,node(index)); // 否則,在指定節(jié)點(diǎn)前插入
}
private void checkPositionIndex(int index){
    // 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException異常
    if(!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index){
    // 判斷索引是否在0到size范圍內(nèi)
    return index >= 0 && index <= size;
}
void linkBefore(E e, Node<E> succ){
    // 獲取目標(biāo)節(jié)點(diǎn) succ 的前驅(qū)節(jié)點(diǎn)
    final Node<E> pred = succ.prev;
    // 創(chuàng)建新節(jié)點(diǎn)newNode,設(shè)置其前驅(qū)為pred,后繼為succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 更新succ的前驅(qū)指針指向newNode
    succ.prev = newNode;
    // 目標(biāo)節(jié)點(diǎn) succ 的前驅(qū)節(jié)點(diǎn) pred 是否為null,判斷是否在鏈表頭部插入
    if (pred == null)
        first = newNode; // 如果pred為null,說明succ是第一個(gè)節(jié)點(diǎn),需要更新first指針
    else
        pred.next = newNode; // 否則,更新pred的后繼指針指向newNode
    size++; // 增加鏈表大小size
    modCount++; // 修改計(jì)數(shù)modCount,用于快速失敗
}

add( int index, E e) 的實(shí)現(xiàn)是一個(gè)典型的 “查找 + 插入” 的模式:

  1. 檢查索引的合法性
  2. 判斷位置,選擇最高效的路徑(末尾直接插入,其他位置先查找)
  3. 查找節(jié)點(diǎn)時(shí)采用雙向遍歷優(yōu)化,減少一半的遍歷時(shí)間
  4. 插入節(jié)點(diǎn)本身是高效的 O(1) 操作,只涉及指針修改

頭部/尾部刪除(removeFirst/removeLast)

public E removeFirst(){
    // 獲取鏈表頭節(jié)點(diǎn),并用 final 修飾,確保引用不會(huì)被改變
    final Node<E> f = first; 
    // 檢查鏈表是否為空
    //如果為空(first為null),拋出NoSuchElementException異常
    if (f == null) 
        throw new NoSuchElementException(); 
    //調(diào)用unlinkFirst方法實(shí)際執(zhí)行刪除操作,返回被刪除節(jié)點(diǎn)的值
    return unlinkFirst(f); 
}
private E unlinkFirst(Node<E> f){
    // 保存要?jiǎng)h除節(jié)點(diǎn)的元素值
    final E element = f.item; 
    // 保存下一個(gè)節(jié)點(diǎn)的引用
    final Node<E> next = f.next;
    // 清空當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
    // 顯式地將引用置為null可以幫助垃圾回收器更快回收內(nèi)存
    f.item = null;
    f.next = null; // help GC
    // 將鏈表的first指針指向下一個(gè)節(jié)點(diǎn)
    first = next;
    // 如果下一個(gè)節(jié)點(diǎn)為null,說明鏈表現(xiàn)在為空
    if (next == null)
        last = null;
    // 否則將下一個(gè)節(jié)點(diǎn)的前驅(qū)指針設(shè)為null
    else
        next.prev = null;
    // 更新鏈表大小和修改次數(shù)
    size--;
    modCount++;
    // 返回被刪除節(jié)點(diǎn)的元素值
    return element;
}
public E removeLast(){
    // 獲取鏈表尾節(jié)點(diǎn),并用 final 修飾,確保引用不會(huì)被改變
    final Node<E> l = last;
    // 檢查鏈表是否為空
    //如果為空(last為null),拋出NoSuchElementException異常
    if (l == null)
        throw new NoSuchElementException();
    //調(diào)用unlinkLast方法實(shí)際執(zhí)行刪除操作,返回被刪除節(jié)點(diǎn)的值
    return unlinkLast(l); 
}
private E unlinkLast(Node<E> l){
    // 保存要?jiǎng)h除節(jié)點(diǎn)的值
    final E element = l.item;
    // 保存前一個(gè)節(jié)點(diǎn)的引用
    final Node<E> prev = l.prev;
    // 清空當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
    // 顯式地將引用置為null可以幫助垃圾回收器更快回收內(nèi)存
    l.item = null;
    l.prev = null; // help GC
    // 將鏈表的last指針指向上一個(gè)節(jié)點(diǎn)
    last = prev;
    // 如果上一個(gè)節(jié)點(diǎn)為null,說明鏈表現(xiàn)在為空
    if (prev == null)
        first = null;
    // 否則將上一個(gè)節(jié)點(diǎn)的后繼指針設(shè)為null
    else
        prev.next = null;
    // 更新鏈表大小和修改次數(shù)
    size--;
    modCount++;
     // 返回被刪除節(jié)點(diǎn)的元素值
    return element;
}

在執(zhí)行 removeFirst 或 removeLast 方法刪除首尾元素時(shí),會(huì)先獲取目標(biāo)節(jié)點(diǎn)并進(jìn)行非空校驗(yàn)。若節(jié)點(diǎn)為空,則拋出 NoSuchElementException 異常;若節(jié)點(diǎn)存在,則調(diào)用對應(yīng)的 unlinkFirst 或 unlinkLast 方法完成刪除操作。

刪除指定位置元素( remove(int index) )

public E remove(int index){
     檢查索引是否有效
     checkElementIndex(index);
     // 調(diào)用 unlink 方法刪除指定位置元素,并返回刪除元素的值
     return unlink(node(index));
}
private void checkElementIndex(int index){
    // 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException的異常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    // 判斷索引是否在0到size-1范圍內(nèi)
    return index >= 0 && index < size;
}
E unlink(Node<E> x){
    // assert x != null;
    // 保存刪除目標(biāo)元素 x及元素本身的前驅(qū)和后繼引用,使用final修飾,確保引用不會(huì)被修改
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    // 如果前驅(qū)節(jié)點(diǎn)為null,說明要移除的是頭節(jié)點(diǎn),則將first指針指向后繼節(jié)點(diǎn)next
    if (prev == null) {
        first = next;
    // 否則,則將前驅(qū)節(jié)點(diǎn)的next指針指向當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)next,并斷開當(dāng)前節(jié)點(diǎn)與前驅(qū)節(jié)點(diǎn)的連接
    } else {
        prev.next = next;
        x.prev = null;
    }
    // 如果后繼節(jié)點(diǎn)為null,說明要移除的是尾節(jié)點(diǎn),則將last指針指向前驅(qū)節(jié)點(diǎn)prev
    if (next == null) {
        last = prev;
    // 否則,則將后繼節(jié)點(diǎn)的prev指針指向前驅(qū)節(jié)點(diǎn)prev,并斷開當(dāng)前節(jié)點(diǎn)與后繼節(jié)點(diǎn)的連接
    } else {
        next.prev = prev;
        x.next = null;
    }
    // 將被移除節(jié)點(diǎn)的item設(shè)為null,幫助垃圾回收
    x.item = null;
    // 減少鏈表的大小size
    size--;
    // 增加修改計(jì)數(shù)modCount(用于迭代時(shí)的快速失敗機(jī)制)
    modCount++;
    // 返回被移除節(jié)點(diǎn)的元素值
    return element;
}

刪除指定位置元素remove(int index)的時(shí)間復(fù)雜度為O(n),原因在于需要先通過node(index)方法定位目標(biāo)節(jié)點(diǎn),這個(gè)定位過程需要線性時(shí)間。雖然實(shí)際刪除操作僅需常數(shù)時(shí)間O(1),但整體時(shí)間復(fù)雜度仍由較耗時(shí)的定位步驟決定。

查找 ( contains(Object o) )

public boolean contains(Object o){
     // 調(diào)用 indexOf 查找目標(biāo)元素 o 的索引,并判斷目標(biāo)元素索引不等于 -1
     // 目標(biāo)元素索引等于 -1,等式不成立,返回 false; 不等于則返回 true
     return indexOf(o) != -1;
}
public int indexOf(Object o){
    // 初始化索引記數(shù)器
    int index = 0; 
    // 分別處理查找 null 值和非 null 值的情況,避免出現(xiàn)NullPointerException
    // 處理查找null值的情況
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    // 處理查找非null值的情況
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    // 未找到元素,返回-1
    return -1;
}

contains 方法將“查找元素是否存在”的問題,巧妙地轉(zhuǎn)換成了“查找元素的索引是否為-1”的問題。如若查找目標(biāo)元素 o 在鏈表中存在相同元素,只會(huì)返回第一個(gè)匹配項(xiàng)的索引。 

LinkedList的內(nèi)存與空間局部性

  • 內(nèi)存開銷:每個(gè)元素都需要一個(gè) Node 對象來包裝。除了存儲(chǔ)元素 E 本身,每個(gè) Node 還額外需要兩個(gè)引用( prev 和 next )的空間。在 64 位JVM (開啟壓縮指針)中,一個(gè) Node 對象頭占 12字節(jié),兩個(gè)引用占 8字節(jié),總共20字節(jié)的開銷,這其中還不包含元素 E 本身的大小。而 ArrayList 只是一個(gè)數(shù)組中的引用。
  • 空間局部性差:ArrayList 的元素在內(nèi)存中是連續(xù)存放的,當(dāng) CPU 訪問一個(gè)元素時(shí),可以預(yù)加載其相鄰的元素到緩存中,這就極大地提高了遍歷性能。而 LinkedList 的節(jié)點(diǎn)在內(nèi)存中是散落的,CPU 無法有效的預(yù)取,緩存命中率低,遍歷性能遠(yuǎn)不如 ArrayList。

迭代器:ListIterator

如果說普通的 Iterator 只能讓你在集合中”單向、只讀(主要)“地前進(jìn),那么 ListIterator 就像是給了你一輛可以倒車、隨時(shí)變速、還能邊開邊修路的全地形車。它是 Java 集合專門為 List 接口設(shè)計(jì)的強(qiáng)大迭代器。

普通的 Iterator 由有三個(gè)明顯的局限:

  • 單向遍歷:只能從前往后走,不能回頭
  • 無法獲取索引:在迭代過程中,存在無法知道當(dāng)前已經(jīng)迭代到了幾個(gè)元素
  • 修改能力弱:只能通過 remove() 方法刪除元素,不能添加或替換元素

ListIterator 正是為了克服這些局限而誕生,它允許你:

  • 雙向遍歷:向前或向后遍歷
  • 獲取索引:精確知道當(dāng)前光標(biāo)的位置
  • 強(qiáng)大的修改能力:可以在遍歷過程中添加、刪除、修改元素

概念:光標(biāo)

想象列表元素之間有間隙,光標(biāo)就落在這個(gè)間隙里:

          元素(0)   元素(1)   元素(2)   元素(3)
             A             B              C            D
        |              |              |              |              |
     ^(0)         ^(1)         ^(2)         ^(3)         ^(4)

  • 初始狀態(tài),光標(biāo)在位置0(即第一個(gè)元素之前)。
  • next() 會(huì)跳過光標(biāo)后的元素,并將光標(biāo)向后移動(dòng)。
  • previous() 會(huì)跳過光標(biāo)前的元素,并將光標(biāo)向前移動(dòng)。

核心方法

ListIterator 接口繼承 Iterator,并擴(kuò)展以下核心方法:

遍歷與移動(dòng)

方法作用光標(biāo)變化返回值
next()返回下一個(gè)元素,并向后移動(dòng)光標(biāo)index -> index+1下一個(gè)元素
previous()返回上一個(gè)元素,并向前移動(dòng)光標(biāo)index -> index-1上一個(gè)元素
hasNext()檢查后面是否還有元素不變true / false
hasPrevious()檢查前面是否還有元素不變true / false

索引查詢

方法作用返回值
nextIndex()返回下一次調(diào)用next()時(shí)返回的元素的索引int
previousIndex()返回下一次調(diào)用previous()時(shí)返回的元素的索引int

修改操作

方法作用前置條件
set(E e)替換最后一次通過 next() 或 previous() 返回的元素必須先調(diào)用 next() 或 previous(),且在這之后沒調(diào)用過 add() 或 remove()
add(E e)插入一個(gè)元素到光標(biāo)當(dāng)前位置無前置條件
remova()刪除最后一次通過 next() 或 previous() 返回的元素必須先調(diào)用 next() 或 previous(),且在這之后沒調(diào)用過 add() 或 remove()
import java.util.LinkedList;
import java.util.ListIterator;
public class ListIteratorDemo {
    public static void main(String[] args){
        LinkedList<String> list = new LinkedList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        System.out.println("原始列表: " + list); // [A, B, C, D]
        // 獲取 ListIterator,從索引 0 開始
        ListIterator<String> iterator = list.listIterator();
        // 1. 獲取第一個(gè)元素 "A"
        System.out.println("next: " + iterator.next()); // 返回 A, 光標(biāo)移到 A 后
        // 光標(biāo)位置: A | B C D
        // 2. 在當(dāng)前光標(biāo)位置(即 A 和 B 之間)插入 "New"
        iterator.add("New"); 
        System.out.println("add后: " + list); // [A, New, B, C, D]
        // 光標(biāo)位置: A New | B C D
        // 3. 獲取下一個(gè)元素 "B"
        System.out.println("next: " + iterator.next()); // 返回 B, 光標(biāo)移到 B 后
        // 光標(biāo)位置: A New B | C D
        // 4. 獲取下一個(gè)元素 "C"
        System.out.println("next: " + iterator.next()); // 返回 C, 光標(biāo)移到 C 后
        // 注意:最后一次返回的是 C,所以接下來的 set 會(huì)替換 C
        // 光標(biāo)位置: A New B C | D
        // 5. 將剛剛獲取的 "C" 替換為 "Replace_C"
        iterator.set("Replace_C");
        System.out.println("set后: " + list); // [A, New, B, Replace_C, D]
        // 6. 向后回退:獲取前一個(gè)元素
        // 此時(shí)光標(biāo)在 C(即Replace_C) 后,前一個(gè)就是 Replace_C
        System.out.println("previous: " + iterator.previous()); //返回 Replace_C, 光標(biāo)回退
        // 光標(biāo)位置: A New B | Replace_C D
        // 7. 刪除剛剛回退獲取的元素 "Replace_C"
        iterator.remove();
        System.out.println("remove后: " + list); // [A, New, B, D]
    }
}

注意:

雖然 ListIterator 允許在迭代過程中修改列表(通過它自己的add/remove/set),但如果你在迭代過程中使用了列表對象自身的方法(如list.add())去修改結(jié)構(gòu),迭代器會(huì)立即拋出ConcurrentModificationException。

1.4 CopyOnWriteArrayList

CopyOnWriteArrayList 是 java.util.concurrent 包下的成員,它是線程安全的 ArrayList。它的名字 CopyOnWrite (寫時(shí)復(fù)制)已經(jīng)揭示了它的核心秘密當(dāng)你需要修改列表時(shí),不要直接在原數(shù)組上改,而是先把原數(shù)組復(fù)制一份,在副本上修改,修改完成后,再讓引用指向新數(shù)組。

核心思想:讀寫分離(讀線程與寫線程互不干擾)

CopyOnWriteArrayList 解決并發(fā)問題的思路與 Vector 或 synchronizedList 截然不同:

  • Vector 等悲觀鎖方案:為了保證數(shù)據(jù)一致性,無論是讀還是寫,都要搶同一把鎖。這就導(dǎo)致了讀操作被寫操作阻塞,性能低下。
  • CopyOnWriteArrayList 樂觀鎖方案:讀操作采用無鎖設(shè)計(jì),因?yàn)樗粫?huì)修改數(shù)據(jù)(允許讀取到稍舊的數(shù)據(jù))。寫操作則通過加鎖機(jī)制確保原子性,防止多線程并發(fā)復(fù)制。加鎖后,所有寫操作都在新副本上進(jìn)行。
底層原理解析

CopyOnWriteArrayList是一個(gè)設(shè)計(jì)精巧的并發(fā)容器。它用寫操作的昂貴代價(jià)(復(fù)制數(shù)組)換取了讀操作的極致性能(無鎖)。

核心成員變量 

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 序列化版本號(hào),用于實(shí)現(xiàn) Serializable 接口
    private static final long serialVersionUID = 8673264195747942595L;
    // ReentrantLock 可重入鎖,用來保護(hù)所有修改操作
    // transient 修飾,表示在序列化時(shí)不會(huì)保存
    final transient ReentrantLock lock = new ReentrantLock();
    // 存儲(chǔ)實(shí)際數(shù)據(jù)的數(shù)組
    // volatile關(guān)鍵字確??梢娦裕匆粋€(gè)線程修改了數(shù)組,其他線程能立即看到變化
    private transient volatile Object[] array;
    final Object[] getArray() {
        return array;
    }
    final void setArray(Object[] a) {
        array = a;
    }
    //.............
}

注意這里使用了  volatile,這是 CopyOnWriteArrayList 保證線程安全的基石之一,它確保了當(dāng)寫操作把 array 指針指向新數(shù)組時(shí),讀線程能立刻感知到。

讀操作:get(int index) —— 完美無鎖

public E get(int index) {
    // 先調(diào)用 getArray() 獲取 array 引用,不加任何鎖
    // 然后調(diào)用另一個(gè)重載的get方法,傳入數(shù)組和索引,返回?cái)?shù)組中的元素
    return get(getArray(),index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

讀操作性能極佳,即使存在并發(fā)寫操作,讀線程也能無阻塞地并行讀取,且不會(huì)拋出 ConcurrentModificationException。不過需要注意其弱一致性問題:當(dāng)寫線程修改數(shù)據(jù)但尚未更新數(shù)組引用時(shí),讀線程可能讀取到舊數(shù)據(jù)。雖然這不適用于對實(shí)時(shí)性要求極高的系統(tǒng),但對于大多數(shù)業(yè)務(wù)場景(如配置列表、白名單等)來說完全可接受。

寫操作:add(E e) —— 寫時(shí)復(fù)制

public boolean add(E e) {
    // 獲取鎖對象
    final ReentrantLock lock = this.lock;
    // 加鎖,確保線程安全
    lock.lock();
    try {
        // 獲取當(dāng)前數(shù)組
        Object[] elements = getArray();
        // 獲取當(dāng)前數(shù)組長度
        int len = elements.length;
        // 創(chuàng)建一個(gè)新數(shù)組,長度為原數(shù)組+1,并拷貝舊數(shù)組
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新數(shù)組末尾添加新元素
        newElements[len] = e;
        // 將新數(shù)組設(shè)置為底層數(shù)組
        setArray(newElements);
        return true;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

寫操作存在著明顯的性能瓶頸延遲問題

  • 內(nèi)存消耗:每次寫入都需要完整復(fù)制底層數(shù)組。當(dāng)處理大型數(shù)組(例如存儲(chǔ)10萬個(gè)對象)且頻繁修改時(shí),會(huì)產(chǎn)生巨大的內(nèi)存壓力,容易觸發(fā)Young GC甚至Full GC。
  • 性能損耗:盡管僅涉及加鎖和復(fù)制操作,但對于大規(guī)模數(shù)據(jù)(N值較大),Arrays.copyof的時(shí)間復(fù)雜度達(dá)到O(n),導(dǎo)致寫入效率較低。
  • 弱一致性:寫入過程中其他線程可能仍在讀取舊數(shù)組,存在短暫的數(shù)據(jù)不一致。CopyOnWriteList返回的迭代器基于"數(shù)組快照"機(jī)制,無法反映后續(xù)的修改。
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");
        // 迭代器創(chuàng)建時(shí),拿到的是 [A, B] 的快照
        Iterator<String> it = list.iterator(); 
        // 主線程修改了列表
        list.add("C"); 
        while(it.hasNext()) {
            // 只會(huì)打印 A 和 B,不會(huì)拋出異常,也看不到 C
            System.out.println(it.next()); 
        }
    }
}

2. Set

Set 是一個(gè)無序且唯一的集合。它不允許包含重復(fù)的元素,就像學(xué)校班級(jí)的一個(gè)花名冊,每個(gè)學(xué)生都是獨(dú)一無二的。

核心特點(diǎn):

  • 元素唯一:不允許出現(xiàn)重復(fù)元素(最多只能包含一個(gè) null 元素)。
  • 無序:無法確保元素的存儲(chǔ)與遍歷順序 。

2.1 HashSet

HashSet 雖然它的名字帶個(gè) "Set",但它實(shí)際上是披著 "Set" 外衣的 HashMap。它是一個(gè)偽裝者,它所有的核心特性——唯一性、快速查找、無序性——全部來自 HashMap。

底層原理解析

理解 HashSet 的核心,就在于理解它是如何利用 HashMap 來實(shí)現(xiàn)唯一性的。

揭秘 HashSet 的本質(zhì)

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    // 序列化版本號(hào)
    static final long serialVersionUID = -5024744406713321676L;
    // HashSet 的核心,實(shí)際上使用 HashMap 來存儲(chǔ)數(shù)據(jù)
    // transient 關(guān)鍵字表示 map 不參與序列化
    private transient HashMap<E,Object> map;
    // 一個(gè)固定的 Object 對象,作為 HashMap 中所有 value 的占位值
    private static final Object PRESENT = new Object();
    // 創(chuàng)建 HashSet,底層 HashMap 使用默認(rèn)初始容量(16)和加載因子(0.75)
    public HashSet() {
        map = new HashMap<>();
    }
    // 創(chuàng)建一個(gè)包含指定集合元素的 HashSet,根據(jù)集合大小計(jì)算合適的初始容量,確保不會(huì)頻繁擴(kuò)容
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    // 創(chuàng)建 HashSet,允許自定義初始容量和加載因子
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    // 創(chuàng)建 HashSet, 允許自定義初始容量
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }   
}

HashSet 本質(zhì)上是通過 HashMap 實(shí)現(xiàn)的特殊結(jié)構(gòu)。它將所有元素作為 HashMap 的鍵存儲(chǔ),利用 HashMap 鍵不可重復(fù)的特性來實(shí)現(xiàn)去重功能。為了維持 HashMap 的鍵值對結(jié)構(gòu),HashSet 使用一個(gè)名為 PRESENT 的常量作為占位符填充值字段。

核心操作源碼解析

public boolean add(E e) {
    // 直接用 map 的 put 方法存儲(chǔ)元素 e
    // e 是 key, PRESENT 是固定的 value
    return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
    // 直接調(diào)用 map 的 remove 方法刪除元素 o
    return map.remove(o)==PRESENT;
}
public boolean contains(Object o) {
    // 直接調(diào)用 map 的 containsKey 方法查找元素 o
    return map.containsKey(o);
}

HashSet 的所有核心操作(包括 add、remove 和 contains)實(shí)際上都是通過直接調(diào)用底層 HashMap 的對應(yīng)方法來實(shí)現(xiàn)的,這體現(xiàn)了典型的委托模式設(shè)計(jì)。具體實(shí)現(xiàn)細(xì)節(jié)如下:

  1. 在 HashSet 內(nèi)部維護(hù)了一個(gè) HashMap 實(shí)例作為存儲(chǔ)容器
  2. 當(dāng)調(diào)用 add(E e) 方法時(shí),實(shí)際上是執(zhí)行 map.put(e, PRESENT) 操作:
    • PRESENT 是一個(gè)固定的 Object 對象作為占位值
    • 這樣設(shè)計(jì)可以復(fù)用 HashMap 的鍵唯一性特性
  3. remove(Object o) 方法對應(yīng) map.remove(o) 調(diào)用
    • 返回的是是否成功移除,而非被移除的值
  4. contains(Object o) 方法直接委托給 map.containsKey(o)

這種委托模式的優(yōu)勢在于:

  • 代碼復(fù)用:直接利用 HashMap 已經(jīng)實(shí)現(xiàn)的哈希表功能
  • 維護(hù)簡單:HashSet 只需要關(guān)注集合接口的實(shí)現(xiàn)
  • 性能保證:所有操作的時(shí)間復(fù)雜度與 HashMap 一致

這種設(shè)計(jì)模式在 Java 集合框架中很常見,類似的還有:

  • TreeSet 委托給 TreeMap
  • LinkedHashSet 委托給 LinkedHashMap

HashSet 的去重機(jī)制:基于 HashMap 的委托實(shí)現(xiàn)

HashSet 的核心特性在于其能夠自動(dòng)過濾重復(fù)元素,而這一機(jī)制并非由其自身復(fù)雜的邏輯實(shí)現(xiàn),而是全權(quán)委托給了底層的 HashMap。正如 JDK 官方源碼注釋所明確指出的:

* This class implements the <tt>Set</tt> interface, backed by a hash table
* (actually a <tt>HashMap</tt> instance). It makes no guarantees as to the
* iteration order of the set; in particular, it does not guarantee that the
* order will remain constant over time. This class permits the <tt>null</tt>
* element.

中文釋義:此類實(shí)現(xiàn)了<tt>Set</tt>接口,以哈希表支持(實(shí)際上是<tt>HashMap</tt>實(shí)例)。它不保證集合的迭代順序;特別是,它不保證順序會(huì)隨時(shí)間保持不變。此類允許<tt>null</tt>元素。

這種“委托模式”在 HashSet 的 add 方法中體現(xiàn)得淋漓盡致。

// Adds the specified element to this set if it is not already present.
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

HashSet 將待存儲(chǔ)的元素 e 作為 Key 傳入 HashMap,并使用靜態(tài)常量對象 PRESENT 作為占位 Value。

去重的具體判定完全依賴于 map.put() 的返回值:

  • 當(dāng)方法返回 null 時(shí),表示 HashMap 中原本不存在該 Key,判定為不重復(fù),插入成功。
  • 當(dāng)方法返回非 null 值(即舊的 PRESENT)時(shí),表示 HashMap 中已存在相同的 Key,判定為重復(fù),插入失敗。

深入分析 map.put() 方法后,我們發(fā)現(xiàn) HashSet 的去重功能實(shí)際上是通過 putVal() 方法實(shí)現(xiàn)的。

public V put(K key, V value) {
    // 調(diào)用hash(key)方法計(jì)算鍵的哈希值
    // 將哈希值、鍵、值以及其他參數(shù)傳遞給putVal方法
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    // 如果 key 是 null,返回 0;否則將 h 右移 16 位并與自身異或(高位運(yùn)算,減少?zèng)_突)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// hash: key的哈希值
// key: 要存儲(chǔ)的鍵
// value: 要存儲(chǔ)的值
// onlyIfAbsent: 如果為true,則僅在鍵不存在時(shí)才插入
// evict: 是否在插入后執(zhí)行可能的移除操作(用于LinkedHashMap)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 檢查內(nèi)部數(shù)組table是否為空,如果是則通過resize()初始化
    // resize()會(huì)創(chuàng)建一個(gè)新的數(shù)組并返回
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 使用(n-1) & hash計(jì)算key在數(shù)組中的索引位置
    // 如果該位置為空,直接創(chuàng)建新節(jié)點(diǎn)放入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 處理哈希沖突
    else {
        Node<K,V> e; K k;
        // 直接匹配:檢查第一個(gè)節(jié)點(diǎn)是否與要插入的key相同
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 記錄當(dāng)前節(jié)點(diǎn)
        // 紅黑樹節(jié)點(diǎn)
        else if (p instanceof TreeNode)
            // 調(diào)用putTreeVal方法處理
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 鏈表節(jié)點(diǎn):遍歷鏈表,查找是否已存在相同的key
        else {
            for (int binCount = 0; ; ++binCount) {
                // 如果遍歷到鏈表尾部還沒找到重復(fù)的,說明元素不重復(fù),掛在尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 當(dāng)鏈表長度達(dá)到TREEIFY_THRESHOLD(默認(rèn)8)時(shí),轉(zhuǎn)換為紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                 // 在鏈表中判斷:Hash值相同 且 (引用相同 或 equals相同)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 找到重復(fù)了,跳出循環(huán),e 指向重復(fù)節(jié)點(diǎn)
                p = e;
            }
        }
        // e 不為 null,說明在 Map 中找到了現(xiàn)有的 Key(即元素重復(fù))
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 根據(jù) onlyIfAbsent 決定是否覆蓋(HashSet 永遠(yuǎn)是覆蓋)
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            // 返回舊值,這對 HashSet 意味著 add 失敗
            return oldValue;
        }
    }
    // 如果程序能走到這里,說明 e 為 null,是新增節(jié)點(diǎn)
    ++modCount; //增加修改計(jì)數(shù)
    // 檢查是否需要擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null; // 返回 null,這對 HashSet 意味著 add 成功
}

根據(jù)源碼分析,HashSet 判斷重復(fù)元素的機(jī)制可分為兩個(gè)關(guān)鍵步驟:

1. 哈希值比對(hashCode):

  • 首先計(jì)算新元素的 hashCode()
  • 若該哈希值與集合中現(xiàn)有元素均不相同,則判定為不重復(fù)元素,直接存入
  • 若出現(xiàn)哈希值相同的情況(哈希沖突),則進(jìn)入下一步判斷

2. 內(nèi)容比對(equals):

  • 當(dāng)哈希值相同時(shí),調(diào)用 equals() 方法進(jìn)行內(nèi)容比較
  • 若 equals() 返回 true,則判定為重復(fù)對象,拒絕存入
  • 若 equals() 返回 false,則視為不同對象,將其添加到該哈希值對應(yīng)的鏈表(或紅黑樹)結(jié)構(gòu)中

hashCode 與 equals 的“黃金契約”

在 Java 集合框架中,hashCode 與 equals 并非兩個(gè)單獨(dú)的方法,它們之間存在著一種隱含的、必須嚴(yán)格遵守的 “契約”。這種契約不僅是設(shè)計(jì)原則,更是保證對象能被正確存儲(chǔ)(特別是存入 HashSet 或作為 HashMap 的 Key)的法律底線。

簡單來說,這個(gè)契約可以概括為三個(gè)核心規(guī)則:

1、相等的對象必須擁有相等的 Hash 碼。這是契約的基石。如果兩個(gè)對象通過 equals 判定為邏輯上的 “同一者”,那么它們調(diào)用 hashCode 返回的整數(shù)值必須完全一致。 

  • 反例:如果兩個(gè)對象 equals 為 true,但 hash 值不同,它們在 HashMap 中會(huì)被分發(fā)給不同的“房間"(數(shù)組下標(biāo))。這不僅違反了唯一性邏輯,更會(huì)導(dǎo)致去重機(jī)制徹底失效——集合會(huì)將它們認(rèn)為是兩個(gè)完全不同的陌生人。

2、Hash 碼相同的對象,不一定相等。這是 Hash 碰撞的必然結(jié)果。由于 Hash 算法是將任意長度的輸入映射到固定長度的輸出,沖突是不可避免的。

  • 解讀:僅僅因?yàn)閮蓚€(gè)對象落在了同一個(gè)“房間”(Hash 值相同),并不能說明它們是同一個(gè)對象。此時(shí),必須由 equals 方法出馬進(jìn)行最后的“人臉識(shí)別”,以決定是覆蓋(相同)還是掛載(不同)到鏈表上。

3、重寫 equals 時(shí),必須重寫 hashCode。這是 Java 開發(fā)的鐵律。如果你為了自定義對象的比較邏輯而重寫了 equals,卻保留了繼承自 Object 的默認(rèn) hashCode(基于內(nèi)存地址計(jì)算),你就親手撕毀了這個(gè)契約。

  • 重寫 equals,不重寫 hashCode 造成后果:你將得到一個(gè)看似正常、實(shí)則內(nèi)部混亂的集合。對象可能會(huì)被錯(cuò)誤地覆蓋,或者重復(fù)數(shù)據(jù)無法被剔除。

在 Java.util.HashSet 的源碼注釋明確指出,該類的底層實(shí)現(xiàn)基于哈希表(實(shí)際是一個(gè) HashMap 實(shí)例)。其去重機(jī)制完全依賴于 HashMap 對 Key 的唯一性約束。在 add(E e) 方法實(shí)現(xiàn)中(源碼:return map.put(e, PRESENT) == null),通過判斷 map.put 的返回值來確定元素是否已添加。這就要求存儲(chǔ)對象必須嚴(yán)格遵循 Object 類的契約:當(dāng)兩個(gè)對象相等時(shí),調(diào)用它們的 hashCode 方法必須返回相同的整數(shù)值,這樣才能確保先計(jì)算哈希值再比較對象的去重流程正常運(yùn)作。

總結(jié):hashCode 負(fù)責(zé)宏觀定位(將對象快速分流到對應(yīng)的存儲(chǔ)區(qū)域),而 equals 負(fù)責(zé)微觀甄別(在區(qū)域內(nèi)精確比對對象內(nèi)容)。只有當(dāng)“定位”與“甄別”的標(biāo)準(zhǔn)保持一致時(shí),Java 的哈希集合才能高效、準(zhǔn)確地運(yùn)行。

HashSet 的擴(kuò)容機(jī)制

HashSet 的擴(kuò)容機(jī)制是其保證查詢效率和存儲(chǔ)容量的核心手段。本質(zhì)上,它是底層 HashMap 擴(kuò)容機(jī)制的直接映射。當(dāng)元素?cái)?shù)量增加到一定程度,數(shù)組的長度會(huì)翻倍,并且所有元素的位置會(huì)重新計(jì)算。

擴(kuò)容的“扳機(jī)”(核心參數(shù))

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
{
    // 默認(rèn)初始容量(初始容量為2的4次方)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
    // 最大容量(最大容量為2的30次方)
    static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
    // 默認(rèn)負(fù)載因子(默認(rèn)的負(fù)載因子為0.75,加載因子 = 哈希表中元素?cái)?shù)量 / 容量)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // .........
}

注意:
1、容量必須是2的冪次方,這是 HashMap 設(shè)計(jì)的重要特點(diǎn),其目的:

  • 使用位運(yùn)算代替模運(yùn)算,提供性能
  • 使哈希分布更均勻

2、這些常量的選擇都是經(jīng)過性能測試和優(yōu)化的結(jié)果:

  • 初始容量 16:足夠小以節(jié)省初始內(nèi)存,又足夠大以減少早期擴(kuò)容
  • 最大容量 1073741824:這個(gè)限制主要是因?yàn)閿?shù)組在 Java 中最大長度限制,避免內(nèi)存溢出??紤]到實(shí)際使用場景,這個(gè)容量已經(jīng)足夠大了
  • 負(fù)載因子 0.75:0.75 是一個(gè)權(quán)衡值,在時(shí)間和空間成本之間尋求平衡,太大會(huì)導(dǎo)致空間浪費(fèi),太小又會(huì)導(dǎo)致哈希沖突增多,影響性能

3、在實(shí)際使用中:

  • 如果知道大概要存儲(chǔ)多少元素,可以指定初始容量以減少擴(kuò)容次數(shù)
  • 如果內(nèi)存充足且追求查詢性能,可以適當(dāng)降低加載因子
  • 如果內(nèi)存緊張,可以適當(dāng)提高加載因子

觸發(fā)判斷:HashMap 的 putVal() 方法

在 HashMap.putVal 中,當(dāng)元素插入成功后,會(huì)檢查是否需要擴(kuò)容。

// java.util.HashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
{
    // ......(省略前面的計(jì)算索引和處理鏈表邏輯)
    ++modCount;
    // 插入成功后,size 自增
    // 【核心擴(kuò)容判斷】
    // size:當(dāng)前元素?cái)?shù)量
    // threshold:擴(kuò)容閾值(容量*負(fù)載因子)
    if(++size > threshold)
        resize(); // 觸發(fā)擴(kuò)容
    afterNodeInsertion(evict);
    return null;
}

當(dāng) HashSet 中的實(shí)際元素個(gè)數(shù)(size) 大于 擴(kuò)容閾值(默認(rèn):16 * 0.75 = 12) 時(shí),就會(huì)觸發(fā)擴(kuò)容。

注意:

判斷標(biāo)準(zhǔn)基于元素?cái)?shù)量而非數(shù)組占用情況。即使數(shù)組某個(gè)位置存在長鏈表,只要元素總數(shù)未超過擴(kuò)容閾值(12),通常不會(huì)觸發(fā)擴(kuò)容。

除非鏈表長度達(dá)到 8 且數(shù)組長度未達(dá)到 64(size < 64  & 鏈表長度 >= 8)時(shí),這通常是因?yàn)?nbsp;數(shù)組容量太小,導(dǎo)致大量元素?cái)D在同一個(gè)桶位(Hash 碰撞嚴(yán)重),而不是因?yàn)檫@些元素本身的 Hash 代碼沖突。

這種情況下,會(huì)先進(jìn)行擴(kuò)容(嘗試打散:長鏈表打散成短鏈表),把 Hash 取模的范圍變大,將原本擠在一起的元素會(huì)被重新分配到不同的索引下。如果擴(kuò)容后,數(shù)組大了,鏈表還是很長,再轉(zhuǎn)紅黑樹進(jìn)行處理。這屬于另一種特殊情況優(yōu)化(預(yù)防性擴(kuò)容優(yōu)化/延遲樹化)。

這實(shí)際上是一個(gè)兩段式的防御策略

  • 低水位防御(數(shù)組 < 64):用擴(kuò)容解決沖突
  • 高水位防御(數(shù)組 >= 64):用紅黑樹解決沖突

擴(kuò)容的核心:HashMap 的 resize() 方法

這是擴(kuò)容的靈魂所在。

// java.util.HashMap
final Node<K,V>[] resize(){    
    // 獲取舊的 table
    Node<K,V>[] oldTab = table; 
    // 獲取舊表格(oldTab)的容量
    // 使用三元運(yùn)算符判斷:如果oldTab為null,則oldCap為0;否則為oldTab的長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length; 
    // 獲取舊的擴(kuò)容閾值(threshold)
    // HashMap需要擴(kuò)容的臨界值,通常為容量*加載因子
    int oldThr = threshold;
    // 聲明新的容量(newCap)和新的閾值(newThr)
    int newCap, newThr = 0;
    // 如果舊數(shù)組不為空
    if (oldCap > 0) {
        // 如果舊容量已經(jīng)達(dá)到最大值,不再擴(kuò)容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab; // 返回舊表格
        }
        // 【核心邏輯】:新容量 = 舊容量左移1位 (即乘以2)
        // (newCap = oldCap << 1) 例如 16 -> 32
        // 如果計(jì)算的新容量沒達(dá)到最大值且舊容量大于默認(rèn)初始容量,計(jì)算新閾值
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新閾值也乘以2
            newThr = oldThr << 1;
    }
    // 當(dāng)oldThr > 0時(shí),說明HashMap已經(jīng)被初始化過
    // 這種情況下,新的容量(newCap)直接設(shè)置為舊的閾值(oldThr)
    // 注釋"initial capacity was placed in threshold"表明初始容量被存儲(chǔ)在了閾值中
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 初始化時(shí),如果用戶沒有指定初始容量(即 threshold 為 0)的情況下
    else {
        // 設(shè)置默認(rèn)容量和擴(kuò)容閾值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新閾值是 0 (比如使用默認(rèn)構(gòu)造函數(shù)時(shí)),重新計(jì)算
    if (newThr == 0) {
        // 新的容量:(newCap)乘以加載因子(loadFactor)
        float ft = (float)newCap * loadFactor;
        // 使用浮點(diǎn)數(shù)ft進(jìn)行中間計(jì)算,避免精度丟失,將計(jì)算結(jié)果轉(zhuǎn)換為整數(shù)賦值給threshold
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);   
    }
    threshold = newThr; // 更新全局閾值
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 創(chuàng)建新的數(shù)組
    table = newTab; // 將新數(shù)組賦值給 table
    // 如果舊數(shù)組有數(shù)據(jù),開始遍歷遷移
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 如果當(dāng)前位置有元素
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 顯式地將引用置為null幫助垃圾回收器更快回收內(nèi)存
                // 單個(gè)節(jié)點(diǎn)處理
                if (e.next == null)
                    // 直接計(jì)算新位置并放入
                    newTab[e.hash & (newCap - 1)] = e;
                // 紅黑樹處理
                else if (e instanceof TreeNode)
                    // 調(diào)用split方法進(jìn)行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 鏈表處理
                // 通過維護(hù)兩個(gè)鏈表(lo和hi),將元素分別加入對應(yīng)的鏈表
                else {
                    // loHead/loTail:保持原索引位置的鏈表
                    // hiHead/hiTail:需要移動(dòng)到"原索引+舊容量"位置的鏈表
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 判斷元素的位置是否需要移動(dòng)
                        // 如果(e.hash & oldCap) == 0,說明元素在新數(shù)組中的位置不變
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 如果(e.hash & oldCap) != 0
                        // 元素需要移動(dòng)到新位置,新位置 = 原位置 + oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 處理低位鏈表
                    if (loTail != null) {
                        loTail.next = null; // 斷開低位鏈表的最后一個(gè)節(jié)點(diǎn)的next指針
                        newTab[j] = loHead; // 將低位鏈表放在新數(shù)組的原索引位置
                    }
                    // 處理高位鏈表
                    if (hiTail != null) {
                        hiTail.next = null; // 斷開高位鏈表的最后一個(gè)節(jié)點(diǎn)的next指針
                        newTab[j + oldCap] = hiHead; // 將高位鏈表放在新數(shù)組的原索引+oldCap位置
                    }
                }
            }
        }
    }
    return newTab;
}

HashSet 的擴(kuò)容實(shí)際上是執(zhí)行 HashMap 的 resize() 方法。這一過程可細(xì)分為四個(gè)關(guān)鍵步驟:

第一階段:計(jì)算新容量與閾值 (計(jì)算階段)

1、檢查舊容量:

  • 獲取當(dāng)前數(shù)組的長度 oldCap
  • 如果當(dāng)前數(shù)組長度已經(jīng)達(dá)到最大限制(2^30),則將閾值設(shè)為 Integer.MAX_VALUE,不再擴(kuò)容,直接返回

2、確定新容量:

  • 如果舊容量大于 0 且未達(dá)上限,新容量 = 舊容量 << 1(即 乘以 2
  • 例如:16 變?yōu)?32,32 變?yōu)?64

3、確定新閾值:

  • 新的擴(kuò)容閾值 = 新容量 × 負(fù)載因子(默認(rèn) 0.75)
  • 例如:新容量 16 × 0.75 = 12。這意味著當(dāng)存入第 13 個(gè)元素時(shí),會(huì)再次觸發(fā)擴(kuò)容

第二階段:申請新數(shù)組 (內(nèi)存階段)

1、創(chuàng)建新桶數(shù)組:

  • 在內(nèi)存中創(chuàng)建一個(gè)新的 Node 數(shù)組,大小為第一步計(jì)算出的 newCap

2、暫存:

  • 此時(shí),新數(shù)組是空的,舊數(shù)組依然存在,里面存著所有數(shù)據(jù)

第三階段:數(shù)據(jù)遷移 (核心遷移階段)

這是擴(kuò)容最耗時(shí)的一步,需要遍歷舊數(shù)組中的每個(gè)元素,并將其放入新數(shù)組。

1、遍歷舊數(shù)組:

  • 依次取出每個(gè)位置上的數(shù)據(jù)(可能是 null,可能是單個(gè)節(jié)點(diǎn),可能是鏈表,也可能是紅黑樹)

2、分類處理:

  • 單個(gè)元素:直接計(jì)算新索引,放入新數(shù)組
  • 紅黑樹:將樹拆分為低位樹和高位樹,如果節(jié)點(diǎn)數(shù)太少(<=6),會(huì)退化回鏈表
  • 鏈表:利用(e.hash & oldCap) 判斷,如果(e.hash & oldCap) == 0,元素在擴(kuò)容后,索引不變(仍在 j 位置);如果(e.hash & oldCap) != 0,元素在擴(kuò)容后,移動(dòng)到 j + oldCap 位置。這樣就將一個(gè)長鏈表拆分成了兩個(gè)短鏈表,并保持了原有順序(尾插法)

第四階段:引用切換與清理 (收尾階段)

1、切換引用:

  • 將 HashMap 內(nèi)部維護(hù)的 table 屬性指向新的數(shù)組

2、置空舊數(shù)組:

  • 原數(shù)組的引用被丟棄,等待 JVM 垃圾回收器(GC)進(jìn)行回收

簡易流程圖:

開始
  ↓
檢查元素個(gè)數(shù) > 閾值 ?
  ↓ (是)
計(jì)算新容量 (x2) 和新閾值
  ↓
創(chuàng)建一個(gè)更大的新數(shù)組 (2倍大小)
  ↓
遍歷舊數(shù)組的每個(gè)桶位
  ↓
判斷數(shù)據(jù)類型?
  ├─ 單個(gè)節(jié)點(diǎn) → 直接移到新位置
  ├─ 紅黑樹   → 拆樹/移樹
  └─ 鏈表     → (hash & oldCap) 判斷
                ├─ == 0 → 留在原位
                └─ != 0 → 移到 (原位 + 舊容量)
  ↓
將 table 指向新數(shù)組
  ↓
擴(kuò)容結(jié)束

在 JDK 1.7 版本中,擴(kuò)容時(shí)需要重新計(jì)算每個(gè)元素的索引位置(index = hash & (newCap - 1)),并且采用頭插法會(huì)導(dǎo)致鏈表順序反轉(zhuǎn)。而在 JDK 1.8 版本中,它不需要重新計(jì)算 Hash,也不需要通過 & (newCap - 1) 重新計(jì)算索引,只需要判斷一個(gè)位( if ((e.hash & oldCap) == 0) ),就能決定元素是留在原地,還是移動(dòng)到 原位置 + 原容量 的地方。這極大地提升了擴(kuò)容效率。

為了防止 Hash 沖突嚴(yán)重導(dǎo)致鏈表變成“長龍”(查詢退化成 O(n)),JDK 1.8 引入了紅黑樹優(yōu)化方案:

  • 觸發(fā)條件:鏈表長度>8且數(shù)組容量>64時(shí)自動(dòng)轉(zhuǎn)為紅黑樹
  • 退化機(jī)制:元素?cái)?shù)量≤6時(shí)自動(dòng)恢復(fù)為鏈表結(jié)構(gòu),節(jié)省存儲(chǔ)空間

注意:如果數(shù)組容量沒到 64,只是鏈表長了(Hash 沖突極多),HashSet 會(huì)選擇優(yōu)先擴(kuò)容數(shù)組,而不是轉(zhuǎn)樹。因?yàn)閿U(kuò)容能打散鏈表。

2.2 LinkedHashSet

LinkedHashSet 是 Java 集合中 Set 接口的一個(gè)重要實(shí)現(xiàn)類,位于 java.util 包中。它是 HashSet 的子類,在具有哈希表查找效率的同時(shí),結(jié)合了鏈表的結(jié)構(gòu)來維護(hù)插入順序。

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable 
{
    // ..........
}

一句話概括:LinkedHashSet 是一種有序集合(這是它與 HashSet 的主要區(qū)別)。雖然需要維護(hù)額外鏈表來保證順序?qū)е滦阅苈缘陀?HashSet,但它在增刪操作上依然保持高效。與所有 Set 實(shí)現(xiàn)一樣,LinkedHashSet 不允許存儲(chǔ)重復(fù)元素。

  • 繼承關(guān)系:LinkedHashSet 繼承 HashSet
  • 數(shù)據(jù)結(jié)構(gòu):其底層實(shí)現(xiàn)是一個(gè) LinkedHashMap(委托給 LinkedHashMap)
    • 哈希表:負(fù)責(zé)根據(jù)元素的 hashCode 存儲(chǔ)數(shù)據(jù),保證元素的唯一性和快速查詢(O(1)時(shí)間復(fù)雜度)
    • 雙向鏈表:負(fù)責(zé)維護(hù)元素的插入順序(或訪問順序),保證遍歷時(shí)的有序性
底層原理解析:

在 Java 集合框架江湖中,LinkedHashSet 是一個(gè)非常奇特的存在。它是 Java 源碼設(shè)計(jì)中 “組合優(yōu)于繼承” 和 “代碼復(fù)用” 的一個(gè)精彩案例。

源碼初印象:極簡主義

如果你去翻閱 JDK 源碼,你會(huì)發(fā)現(xiàn)一個(gè)驚人的事實(shí):LinkedHashSet 類的源碼極其簡短,所有的核心邏輯竟然都在它的父類 HashSet 里。

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable 
{
    // 序列化/反序列化的版本控制ID
    private static final long serialVersionUID = -2851667679971038690L;
    // 創(chuàng)建一個(gè)指定初始容量和加載因子的空LinkedHashSet
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
    // 創(chuàng)建一個(gè)指定初始容量,默認(rèn)加載因子(0.75)的空LinkedHashSet
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    // 創(chuàng)建一個(gè)默認(rèn)初始容量(16)和默認(rèn)加載因子(0.75)的空LinkedHashSet
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    // 創(chuàng)建一個(gè)包含指定集合所有元素的LinkedHashSet,初始容量設(shè)為max(2*c.size(), 11)
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }
    // 可分割迭代器
    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}

整個(gè)類只有四個(gè)構(gòu)造函數(shù),所有的構(gòu)造函數(shù)都調(diào)用了 super(.... , true),沒有任何 add、remove 或 iterator 方法的實(shí)現(xiàn)。這意味著它完全復(fù)用了父類 HashSet 的邏輯。

源碼核心:HashSet 中的 “后門”

為了支持 LinkedHashSet,HashSet 在源碼中預(yù)留了一個(gè)特殊的構(gòu)造方法。這個(gè)方法通常是 protected 或者 default(包級(jí)私有)的,專門供子類或同包下的類使用。

// java.util.HashSet

// 底層存儲(chǔ) Map
private transient HashMap<E,Object> map;


/**
 * 構(gòu)造一個(gè)新的空的鏈接哈希集合實(shí)例。
 * (這個(gè)構(gòu)造函數(shù)只給 LinkedHashSet 訪問,dummy 沒有實(shí)際意義)
 */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    // 注意這里:new 的不是 HashMap,而是 LinkedHashMap!
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

LinkedHashSet 繼承自 HashSet。在構(gòu)造過程中,它會(huì)調(diào)用 HashSet 的特殊構(gòu)造函數(shù) HashSet(int initialCapacity, float loadFactor, boolean dummy)。這個(gè)構(gòu)造函數(shù)初始化了父類 HashSet 的 map 成員變量,但并非創(chuàng)建普通的 HashMap,而是實(shí)例化了一個(gè) LinkedHashMap。因此,LinkedHashSet 本質(zhì)上仍是 HashSet,只是其內(nèi)部 map 變量實(shí)際指向的是 LinkedHashMap 實(shí)例。

LinkedHashSet 如何保證其有序性

既然 LinkedHashSet 底層是 LinkedHashMap,那么它的有序性就完全由 LinkedHashMap 決定。

在 HashMap 中,每個(gè)節(jié)點(diǎn)都是 Node<K,V> (數(shù)組+鏈表/紅黑樹)。而在 LinkedHashMap 中,節(jié)點(diǎn)被替換為了 Entry<K,V>,它繼承自 HashMap.Node,并增加了兩個(gè)指針。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
{
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // 存儲(chǔ)鍵的哈希值,用于快速定位
        final K key; // 存儲(chǔ)鍵,用final修飾表示鍵不可變
        V value; // 存儲(chǔ)值,可以被修改
        Node<K,V> next; // 指向下一個(gè)節(jié)點(diǎn)的引用,用于處理哈希沖突
        //........
    }
}
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
    // 靜態(tài)內(nèi)部類
    // 繼承自 HashMap.Node,說明其具備了 HashMap 中節(jié)點(diǎn)的所有基本特性
    static class Entry<K,V> extends HashMap.Node<K,V>{
        // 新增的兩個(gè)指針,用于維護(hù)雙向鏈表
        // before(前驅(qū)指針):指向前一個(gè)節(jié)點(diǎn)
        // after(后繼指針):指向后一個(gè)節(jié)點(diǎn)
        Entry<K,V> before, after;
        // 構(gòu)造函數(shù)
        // 接收四個(gè)參數(shù):hash值、鍵、值和下一個(gè)節(jié)點(diǎn)
        // 通過 super() 調(diào)用父類 HashMap.Node 的構(gòu)造函數(shù),確保新建節(jié)點(diǎn)具備 HashMap 節(jié)點(diǎn)的所有基本屬性
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
}

這意味著,LinkedHashMap 中的每個(gè)節(jié)點(diǎn)都同時(shí)處在 “兩個(gè)鏈表” 中:

  • 哈希桶鏈表(單向):用于解決 hash 沖突,保證快速查找(復(fù)用了 HashMap 中的 next 指針)
  • 雙向鏈表(雙向):用于維護(hù)順序(新增了前驅(qū)指針 before 和 后繼指針 after)

      哈希表數(shù)組              雙向鏈表 (維護(hù)順序)
     +----------+           +--------<--------+--------->----------+
     |  Index 0  | -----> |  Entry A       |  Entry B          |
     +----------+           |  (before:null) |  (before:A)     |
                                |  (after:B)     |  (after:null)       |
     +----------+           +----------------+-------------------+
     |  Index 1  | -----> |  Entry C
     +----------+           |  (before:null)  <-- 注意:如果C是最后插入的
                                |  (after:null)      鏈表指針會(huì)重新連接

核心成員變量:定義 “有序” 的規(guī)則

LinkedHashMap 提供了兩種有序模式:

  • 插入順序:元素按照添加的先后順序排列,先插入的元素在前
  • 訪問順序(LRU 機(jī)制):最近被訪問(get/put)的元素會(huì)被移至末尾,實(shí)現(xiàn)最近最少使用算法

這兩種模式通過一個(gè) boolean 類型的標(biāo)志位進(jìn)行切換控制。

// java.util.linkedHashMap
// 雙向鏈表的頭節(jié)點(diǎn)
// transient關(guān)鍵字表示這個(gè)字段不會(huì)被序列化
transient LinkedHashMap.Entry<K,V> head;
// 雙向鏈表的尾節(jié)點(diǎn)
// transient關(guān)鍵字表示這個(gè)字段不會(huì)被序列化
transient LinkedHashMap.Entry<K,V> tail;
// 這是一個(gè) final 字段,決定了 LinkedHashMap 的迭代順序
// 當(dāng)為 true 時(shí),按照訪問順序迭代(最近訪問的放在最后)
// 當(dāng)為 false 時(shí),按照插入順序迭代 (默認(rèn)為 false)
final boolean accessOrder;

節(jié)點(diǎn)上鏈

既然數(shù)據(jù)結(jié)構(gòu)變了,那么在插入新節(jié)點(diǎn)時(shí),LinkedHashMap 必然要維護(hù)這個(gè)雙向鏈表。

當(dāng) HashMap 調(diào)用 put 添加元素時(shí),發(fā)現(xiàn)計(jì)算出的哈希位置當(dāng)前沒有數(shù)據(jù),需要?jiǎng)?chuàng)建一個(gè)新節(jié)點(diǎn)。此時(shí)會(huì)調(diào)用 LinkedHashMap 重寫的 newNode 方法。

// java.util.LinkedHashMap
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 創(chuàng)建新節(jié)點(diǎn) p
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 調(diào)用鏈接方法,把新增節(jié)點(diǎn) p 掛在鏈表尾部
    linkNodeLast(p);
    return p;
} 
private void linkNodeLast(LinkedHashMap。Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail; // 保存當(dāng)前尾節(jié)點(diǎn)
    tail = p; // 將新節(jié)點(diǎn)p設(shè)置為新的尾節(jié)點(diǎn)
    if (last == null) // 如果鏈表為空
        head = p; // 將新節(jié)點(diǎn)同時(shí)設(shè)為頭節(jié)點(diǎn)
    else { // 如果鏈表不為空
        p.before = last; // 將新節(jié)點(diǎn)的前驅(qū)指針指向原尾節(jié)點(diǎn)
        last.after = p; // 將原尾節(jié)點(diǎn)的后繼指針指向新節(jié)點(diǎn)
    }
}

HashMap 在執(zhí)行 map.put 操作時(shí),首先計(jì)算鍵的哈希值來確定數(shù)組下標(biāo)。LinkedHashMap 則通過重寫 newNode 方法,在節(jié)點(diǎn)創(chuàng)建過程中生成特殊的 Entry 對象,并調(diào)用 linkNodeLast 方法將該 Entry 通過 before 和 after 指針鏈接到雙向鏈表末尾。無論哈希值如何分布,雙向鏈表始終按照插入順序進(jìn)行擴(kuò)展。

訪問順序與 LRU 實(shí)現(xiàn)

LinkedHashMap 內(nèi)部維護(hù)了一個(gè)雙向鏈表,這個(gè)鏈表的順序由 accessOrder 參數(shù)控制:

  • accessOrder = false (默認(rèn)):按照插入順序排序(FIFO)
  • accessOrder = true:按照訪問順序排序(LRU)。這就是 LRU 的開關(guān)
// java.util.LinkedHashMap
public V get(Object key) {
    // 聲明一個(gè) Node 類型的變量 e,用于存儲(chǔ)找到的節(jié)點(diǎn)
    Node<K,V> e;
    // 調(diào)用 getNode 方法查找節(jié)點(diǎn)
    // 如果找不到對應(yīng)節(jié)點(diǎn)(e為null),直接返回null
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果accessOrder為true,表示按照訪問順序排序
    if (accessOrder)
        // 調(diào)用 afterNodeAccess(e) 方法將最近訪問的節(jié)點(diǎn)移到鏈表末尾
        afterNodeAccess(e);
    // 返回找到的節(jié)點(diǎn)的value值
    return e.value;
}
void afterNodeAccess(Node<K,V> e) {    
    LinkedHashMap.Entry<K,V> last;
    // 檢查是否需要重新排序(accessOrder為true)且節(jié)點(diǎn)不在尾部
    if (accessOrder && (last = tail) != e) {
        // 保存當(dāng)前節(jié)點(diǎn)的前后節(jié)點(diǎn)引用
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 將p的after置為null,因?yàn)樗鼘⒊蔀樾碌奈补?jié)點(diǎn)
        p.after = null;
        // 如果b為null,說明p是頭節(jié)點(diǎn),更新頭節(jié)點(diǎn)為a;否則,將b的after指向a
        if (b == null)
            head = a;
        else
            b.after = a;
        // 如果a不為null,將a的before指向b;否則,更新last為b
        if (a != null)
            a.before = b;
        else
            last = b;
        // 如果last為null,說明鏈表為空,將頭節(jié)點(diǎn)head設(shè)置為p;否則,將p連接到last之后
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // 更新尾節(jié)點(diǎn)tail為p
        tail = p;
        // 增加修改計(jì)數(shù)器
        ++modCount;
    }
}

當(dāng) accessOrder 設(shè)置為 true 時(shí),每次執(zhí)行 get(key) 操作(即訪問數(shù)據(jù)),LinkedHashMap 會(huì)在內(nèi)部自動(dòng)調(diào)用 afterNodeAccess 方法,將被訪問的節(jié)點(diǎn)移至雙向鏈表尾部。

這就好比排隊(duì)。

  • 插入順序:來了一個(gè)人,站到隊(duì)尾,以后不管他怎么說話,位置不動(dòng)
  • 訪問順序:來了一個(gè)人,站到隊(duì)尾。一旦這個(gè)人和你說了句話(訪問了 get),他立馬插隊(duì)跑到隊(duì)尾去

隊(duì)首的永遠(yuǎn)是最久沒有被訪問的元素。這就是 LRU(Least Recently Used)緩存淘汰策略的基礎(chǔ)!

工作流程圖如下:

數(shù)據(jù)結(jié)構(gòu)示意:

[ 最近最少使用 ] <-----> [ ...中間數(shù)據(jù)... ] <-----> [ 最近剛使用 ]
   (Head)                                                                                        (Tail)
       ↑                                                                                                ↑
   |--- 當(dāng)緩存滿時(shí),刪除 Head(最久未用)                          |--- 新訪問或插入的數(shù)據(jù)移到這

  • 鏈表頭部:存放的是最久未被訪問的數(shù)據(jù)
  • 鏈表尾部:存放的是最近剛被訪問的數(shù)據(jù)
  • 淘汰策略:當(dāng)緩存數(shù)量達(dá)到預(yù)設(shè)上限時(shí),刪除鏈表頭部的節(jié)點(diǎn)

注意:

  • 線程安全:LinkedHashMap 并非線程安全類,在多線程環(huán)境下使用時(shí)需進(jìn)行外部同步控制
  • 性能考慮:每次訪問節(jié)點(diǎn)都可能觸發(fā)鏈表的調(diào)整操作,這在頻繁訪問時(shí)會(huì)有一定的性能開銷。如果不需要按訪問順序排序,建議將accessOrder設(shè)為false,以避免不必要的開銷
  • 內(nèi)存占用:LinkedHashMap 在 HashMap 的基礎(chǔ)上額外維護(hù)了一個(gè)雙向鏈表結(jié)構(gòu),因此會(huì)消耗更多的內(nèi)存空間。??????

迭代器:有序

HashMap 的迭代器通過按數(shù)組索引順序(0 到 15)遍歷來訪問元素,因此其遍歷順序是不確定的。相比之下,LinkedHashMap 通過重寫迭代器邏輯,實(shí)現(xiàn)了有序遍歷。

// java.util.LinkedHashMap
abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next; // 指向下一個(gè)要遍歷的節(jié)點(diǎn)
    LinkedHashMap.Entry<K,V> current; // 當(dāng)前正在處理的節(jié)點(diǎn)
    int expectedModCount; // 預(yù)期的修改次數(shù),用于快速失敗(fail-fast)機(jī)制
    LinkedHashIterator() {
        next = head; // 從雙向鏈表的頭節(jié)點(diǎn)開始
        expectedModCount = modCount; // 記錄創(chuàng)建迭代器時(shí)的修改次數(shù)
        current = null; // 當(dāng)前節(jié)點(diǎn)初始化為null
    }
    // 判斷是否還有下一個(gè)元素
    public final boolean hasNext() {
        return next != null;
    }
    final LinkedHashMap.Entry<K,V> nextNode(){
        LinkedHashMap.Entry<K,V> e = next;
        // 檢查是否有并發(fā)修改
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null) 
            throw new NoSuchElementException();
        // 更新當(dāng)前節(jié)點(diǎn)
        current = e; 
        // 移動(dòng)到下一個(gè)節(jié)點(diǎn)(通過after指針)
        next = e.after; 
        return e;
    }
}

迭代器無需關(guān)注數(shù)組的存儲(chǔ)位置,只需沿著 after 指針依次訪問即可。由于 after 指針已按順序維護(hù),遍歷結(jié)果自然保持有序。

LinkedHashMap 通過"雙鏈路"機(jī)制,巧妙實(shí)現(xiàn)了 O(1) 查詢性能與可預(yù)測遍歷順序的完美結(jié)合。

2.3、TreeSet

TreeSet 是 Java 集合框架中 Set 接口的一個(gè)實(shí)現(xiàn)類,它位于 java.util 包中。與 HashSet 基于哈希表實(shí)現(xiàn)不同,TreeSet 是基于紅黑樹(Red-Black Tree)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的。

核心特點(diǎn):

  • 有序性:TreeSet 中的元素會(huì)按照自然順序或者自定義比較器進(jìn)行排序
  • 唯一性:作為 Set 的實(shí)現(xiàn),它不包含重復(fù)元素
  • 非線程安全:與大多數(shù)集合一樣,TreeSet 不是線程安全的。如果需要在多線程環(huán)境下使用,必須進(jìn)行外部同步

注意:LinkedHashSet 和 TreeSet 雖然都是有序集合,但它們的排序機(jī)制存在本質(zhì)區(qū)別。LinkedHashSet 嚴(yán)格維護(hù)元素的插入順序,遍歷時(shí)會(huì)按照 A→B→C 的順序輸出,與元素插入時(shí)間完全一致。而 TreeSet 則依據(jù)元素的自然排序規(guī)則(如字母順序或數(shù)值大?。┳詣?dòng)排序(使用自然排序,元素不能類型不能比較 null,否則會(huì)拋出 NullPointerException),無論插入順序如何,遍歷結(jié)果始終是 A→B→C 這樣的有序序列。

代碼示例如下:

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
// 插入順序 vs 排序順序
public class OrderDemo {
    public static void main(String[] args) {
        String[] letters = {"C", "A", "B", "A"};
        // LinkedHashSet: 維持插入順序
        Set<String> linkedSet = new LinkedHashSet<>(Arrays.asList(letters));
        System.out.println("LinkedHashSet: " + linkedSet);
        // 輸出: [C, A, B] -> 去重了,且 C 在最前面,因?yàn)樗堑谝粋€(gè)插入的
        // TreeSet: 按字典序排序
        Set<String> treeSet = new TreeSet<>(Arrays.asList(letters));
        System.out.println("TreeSet: " + treeSet);
        // 輸出: [A, B, C] -> 去重了,且按 A, B, C 排列,完全忽略了插入順序
    }
}
TreeSet 的繼承體系

類圖結(jié)構(gòu):

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    // ..........    
}
  • AbstractSet:提供了 Set 接口的骨干實(shí)現(xiàn),減少了實(shí)現(xiàn)此接口所需的工作量
  • NavigableSet:擴(kuò)展了 SortedSet,提供導(dǎo)航方法(如 lower、floor、ceiling、higher 等),允許對集合進(jìn)行最接近匹配的搜索
  • Cloneable:支持對象克隆
  • Serializable:支持序列化
底層原理解析

TreeSet 是 Java 集合框架中著名的 “偽裝者”。打開 JDK 源碼,你會(huì)發(fā)現(xiàn) TreeSet.java 只有 300 多行代碼,且沒有任何核心算法。它的一切行為,都直接委托給了一個(gè)內(nèi)部私有的 NavigableMap 對象,而這個(gè)對象運(yùn)行時(shí)通常是 TreeMap 的實(shí)例。

核心成員變量

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    // TreeSet 內(nèi)部使用一個(gè) NavigableMap (實(shí)際是 TreeMap) 來存儲(chǔ)元素
    private transient NavigableMap<E,Object> m;
    // 這個(gè)是一個(gè)虛擬值,用于在 TreeMap 中關(guān)聯(lián)集合的元素
    // 因?yàn)?TreeMap 存儲(chǔ)的是鍵值對,而 TreeSet 只需要鍵
    private static final Object PRESENT = new Object();
    //.............
}
  • m:這是 TreeSet 的核心。NavigableMap 是一個(gè)接口,TreeSet 默認(rèn)使用其實(shí)現(xiàn)類 TreeMap。TreeSet 的元素實(shí)際是作為這個(gè) NavigableMap 的 Key 存儲(chǔ)
  • PRESENT:這是一個(gè)靜態(tài)的 Object 對象,作為所有的 TreeMap 條目的 value。因?yàn)?TreeSet 只關(guān)心元素本身(即 Key),所以所有的 value 都指向同一個(gè) PRESENT 對象

底層數(shù)據(jù)結(jié)構(gòu)(TreeMap.Entry<K,V>)                

// java.util.TreeMap
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left; // 左子節(jié)點(diǎn)
    Entry<K,V> right; // 右子節(jié)點(diǎn)
    Entry<K,V> parent; // 父節(jié)點(diǎn)
    boolean color = BLACK; // 節(jié)點(diǎn)顏色,默認(rèn)為黑
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
    // .........
}  
  • Parent 指針:這是一個(gè)雙向鏈表結(jié)構(gòu)的體現(xiàn)。不僅知道子節(jié)點(diǎn),還能回溯父節(jié)點(diǎn)。這使得 TreeSet 能夠輕松實(shí)現(xiàn) lower()(前驅(qū))、higher()(后繼)等導(dǎo)航操作。
  • Color 屬性:紅黑樹通過顏色約束(根黑、葉黑、紅不相連、黑高相同)來保證平衡,從而將查詢的復(fù)雜度控制在 O(log n)。

構(gòu)造方法

TreeSet 提供了多個(gè)構(gòu)造方法,它們主要區(qū)別在于如何初始化內(nèi)部的 m(TreeMap)以及是否提供自定義比較器。

// java.util.TreeSet
// 無參構(gòu)造器,使用自然排序
public TreeSet() {
    this(new TreeMap<E,Object>());
}
// 帶比較器的構(gòu)造器,使用自定義排序
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
// 構(gòu)造一個(gè)包含指定集合元素的新 TreeSet,使用自然排序
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}
// 構(gòu)造一個(gè)包含指定有序集合元素的新 TreeSet,按照相同順序
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}
// 包訪問權(quán)限的構(gòu)造器,直接傳入一個(gè) NavigableMap
// 主要用于子類或者特定場景,比如 headSet, subSet, tailSet 等返回的子集
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

所有公共構(gòu)造方法最終都會(huì)通過 this(new TreeMap<>(...)) 或 this(m) 來初始化內(nèi)部的 NavigableMap 對象 m。若未指定 Comparator,TreeMap 將默認(rèn)采用元素的自然排序(此時(shí)元素需實(shí)現(xiàn) Comparable 接口,如果元素類未實(shí)現(xiàn)該接口,會(huì)拋出 ClassCastException)。

核心流程:add 操作的源碼級(jí)復(fù)現(xiàn)

要理解add方法,首先需要掌握這些規(guī)則——源碼中的fixAfterInsertion(插入修復(fù))機(jī)制正是為了在規(guī)則被破壞后重新修復(fù)它們。

紅黑樹的五大鐵律:

  1. 節(jié)點(diǎn)非黑即紅
  2. 根節(jié)點(diǎn)是黑色
  3. 所有葉子節(jié)點(diǎn)(NIL 空節(jié)點(diǎn))是黑色
  4. 紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)必須是黑色(即不能有連續(xù)的兩個(gè)紅色節(jié)點(diǎn))
  5. 從任一節(jié)點(diǎn)到其每個(gè)葉子節(jié)點(diǎn)的所有路徑都包含相同數(shù)目的黑色節(jié)點(diǎn)

add() 方法:排序規(guī)則

// java.util.TreeSet
public boolean add(E e) {
    // 直接調(diào)用底層 TreeMap 的 put 方法
    // 如果之前不存在該 key,則 put 返回 null,add 返回 true (添加成功)
    // 如果之前已存在該 key (根據(jù)排序規(guī)則),則 put 返回舊 value,add 返回 false (添加失敗,元素已存在)
    return m.put(e,PRESENT) == null;
}

這是最常用的操作。表面上看是往集合中添加?xùn)|西,底層實(shí)際上是在操作紅黑樹。

// java.util.TreeMap
public V put(K key, V value) {
    Entry<K,V> t = root; // 從根節(jié)點(diǎn)開始
    // 特殊情況:樹是空的
    // 此時(shí) new Entry 的 parent 是 null,默認(rèn) color 是 BLACK
    // (Entry構(gòu)造器中 color 默認(rèn) BLACK,但在 insert 修正里可能會(huì)變)
    if (t == null) {
        compare(key, key); // 類型檢查,確保 key 是可比較的
        root = new Entry<>(key, value, null); // 創(chuàng)建根節(jié)點(diǎn)
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    Comparator<? super K> cpr = comparator;
    // 如果有自定義比較器,使用比較器進(jìn)行比較
    if (cpr != null) {
        // 遍歷樹找到合適的插入位置
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0) // 小,往左走
                t = t.left;
            else if (cmp > 0) // 大,往右走
                t = t.right;
            else // 相等,替換值, TreeSet 這里返回 PRESENT
                return t.setValue(value);
        } while (t != null);     
    }
    // 如果沒有自定義比較器,使用鍵的自然排序
    else {
        // 鍵不能為null 且 鍵必須實(shí)現(xiàn)Comparable接口
        if (key == null)
            throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 創(chuàng)建新節(jié)點(diǎn)并插入到合適位置
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    // 調(diào)用fixAfterInsertion維護(hù)紅黑樹的平衡
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

插入新節(jié)點(diǎn)(默認(rèn)紅色)后,可能違反了“紅色節(jié)點(diǎn)不能相連”的規(guī)則。JDK 源碼通過變色和旋轉(zhuǎn)來修復(fù)。

// java.util.TreeMap
private void fixAfterInsertion(Entry<K,V> x) {
    // 將新插入的節(jié)點(diǎn)設(shè)為紅色
    x.color = RED;
    // 當(dāng) x 不是根節(jié)點(diǎn),且 x 的父節(jié)點(diǎn)是紅色時(shí)(違反規(guī)則4),需要循環(huán)修復(fù)
    while (x != null && x != root && x.parent.color == RED) {
        // 判斷父節(jié)點(diǎn)是祖父節(jié)點(diǎn)的左孩子還是右孩子
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y 是叔節(jié)點(diǎn)
            // 如果叔節(jié)點(diǎn)是紅色
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK); // 父節(jié)點(diǎn)變黑
                setColor(y, BLACK); // 叔節(jié)點(diǎn)變黑
                setColor(parentOf(parentOf(x)), RED); // 祖父節(jié)點(diǎn)變紅
                x = parentOf(parentOf(x)); // 指針上移,以祖父為當(dāng)前節(jié)點(diǎn)繼續(xù)循環(huán)
            } else {
                // 如果叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是右孩子
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);  // 左旋(以父節(jié)點(diǎn)為中心)
                    rotateLeft(x);
                }
                // 叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是左孩子
                setColor(parentOf(x), BLACK); // 父節(jié)點(diǎn)變黑
                setColor(parentOf(parentOf(x)), RED); // 祖父節(jié)點(diǎn)變紅
                rotateRight(parentOf(parentOf(x))); // 右旋(以祖父節(jié)點(diǎn)為中心)
            }
        } else {
            // 父節(jié)點(diǎn)是祖父的右孩子,邏輯同上,只是左右互換(鏡像情況)
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }    
        }
    }
     // 確保根節(jié)點(diǎn)始終是黑色(符合規(guī)則2:根節(jié)點(diǎn)是黑色)
    root.color = BLACK;
}

當(dāng)插入新節(jié)點(diǎn)導(dǎo)致“雙紅沖突”(父節(jié)點(diǎn)與子節(jié)點(diǎn)同為紅色)時(shí),遵循以下處理邏輯:

1、叔節(jié)點(diǎn)為紅——染色上溯

  • 操作:將父節(jié)點(diǎn)與叔節(jié)點(diǎn)染為黑色,祖父節(jié)點(diǎn)染為紅色
  • 邏輯:相當(dāng)于將紅色沖突“上傳”給祖父,將祖父視為新節(jié)點(diǎn)繼續(xù)向上循環(huán)判斷

2、叔節(jié)點(diǎn)為黑 + 當(dāng)前為右子——左旋重定向

  • 操作:以父節(jié)點(diǎn)為中心進(jìn)行左旋,將當(dāng)前節(jié)點(diǎn)指向原父節(jié)點(diǎn)
  • 邏輯:將“內(nèi)側(cè)”沖突轉(zhuǎn)換為“外側(cè)”,以便統(tǒng)一處理。

3、叔節(jié)點(diǎn)為黑 + 當(dāng)前為左子——變色換位

  • 操作:將父節(jié)點(diǎn)染黑,祖父節(jié)點(diǎn)染紅,以祖父節(jié)點(diǎn)為中心進(jìn)行右旋
  • 邏輯:通過變色和旋轉(zhuǎn),將父節(jié)點(diǎn)提升為新的局部根節(jié)點(diǎn),從而徹底解決沖突,終止循環(huán)

注意:若父節(jié)點(diǎn)是祖父的右孩子,上述邏輯中的“左/右”和“內(nèi)/外”需鏡像互換。

具體實(shí)現(xiàn)步驟:

1、初始化檢查(檢查樹是否為空,若為空):

  • 調(diào)用 compare 進(jìn)行類型檢查
  • 創(chuàng)建新的根節(jié)點(diǎn)
  • 更新 size 和 modCount
  • 返回 null (因?yàn)槭切虏迦耄?/li>

2、查找插入位置:

  • 準(zhǔn)備變量:比較結(jié)果 cmp、父節(jié)點(diǎn) parent 、比較器 cpr
  • 使用比較器情況:
    • 如果有自定義比較器:
      • 保存當(dāng)前節(jié)點(diǎn)為父節(jié)點(diǎn)
      • 比較key和當(dāng)前節(jié)點(diǎn)key
      • 根據(jù)比較結(jié)果向左或向右移動(dòng)(小,往左走;大,往右走)
      • 如果相等,更新值并返回舊值
    • 如果沒有自定義比較器:
      • 檢查key是否為null
      • 將key強(qiáng)制轉(zhuǎn)換為Comparable
      • 使用自然排序進(jìn)行比較
      • 同樣根據(jù)比較結(jié)果移動(dòng)或更新值

3、插入新節(jié)點(diǎn):

  • 創(chuàng)建新的Entry節(jié)點(diǎn)
  • 根據(jù)最后的比較結(jié)果決定插入到父節(jié)點(diǎn)的左子樹還是右子樹

4、維護(hù)紅黑樹性質(zhì):

  • 調(diào)用fixAfterInsertion調(diào)整樹的結(jié)構(gòu),保持紅黑樹的平衡
  • 更新size和modCount
  • 返回null(表示新插入)

流程圖如下:

遍歷機(jī)制:迭代器的實(shí)現(xiàn)

TreeSet 的 iterator() 方法返回了一個(gè)按順序排列的迭代器。它是如何做到 “從小到大” 遍歷的?

// java.util.TreeSet
public Iterator<E> iterator() {
    return m.navigableKeySet().iterator();
}

進(jìn)入 TreeMap 的內(nèi)部類 KeyIterator 或 NavigableSubMap 的迭代器實(shí)現(xiàn):

// java.util.TreeMap
final class KeyIterator extends PrivateEntryIterator<K> {
    KeyIterator(Entry<K,V> first) {
        super(first);
    }
    public K next() {
        return nextEntry().key;
    }
}

核心在于 nextEntry() 方法。它利用了紅黑樹的性質(zhì):中序遍歷。

final Entry<K,V> nextEntry() {
    Entry<K,V> e = next;
    if (e == null)
        throw new NoSuchElementException();
    // 預(yù)先計(jì)算下一個(gè)節(jié)點(diǎn),保存在 next 變量中
    // 這里的算法是:如果當(dāng)前節(jié)點(diǎn)有右子樹,下一個(gè)節(jié)點(diǎn)是右子樹的最左節(jié)點(diǎn);
    // 如果沒有右子樹,下一個(gè)節(jié)點(diǎn)是第一個(gè)“祖先”節(jié)點(diǎn),該節(jié)點(diǎn)的左子樹包含當(dāng)前節(jié)點(diǎn)。
    if ((next = successor(e)) == null)
        next = null;
    lastReturned = e;
    return e;
}
// 尋找后繼節(jié)點(diǎn)的算法
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        //  有右子樹:找右子樹中最左邊的節(jié)點(diǎn)(最小的)
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        //  無右子樹:向上找第一個(gè)左拐的父節(jié)點(diǎn)
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

這種迭代方式保證了 TreeSet 的遍歷是嚴(yán)格按照元素大小順序進(jìn)行的,時(shí)間復(fù)雜度為 O(1)(均攤,因?yàn)槊總€(gè)節(jié)點(diǎn)被訪問兩次,一次是向下查找,一次是向上回溯父節(jié)點(diǎn))。

高級(jí)特性:NavigableSet 的實(shí)現(xiàn)

TreeSet 實(shí)現(xiàn)了 NavigableSet,這意味著它不僅能排序,還能在有序集合中進(jìn)行 “搜索” 。

floor(E e) 與 ceiling(E e)

這些方法也是基于 TreeMap 的二叉查找特性實(shí)現(xiàn)的。

// java.util.TreeSet
public E floor(E e) {
    return m.floorKey(e);
}
public K floorKey(K key) {
    Entry<K,V> p = getFloorEntry(key);
    return (p == null) ? null : p.key;
}
// 核心:尋找小于等于 key 的最大節(jié)點(diǎn)
final Entry<K,V> getFloorEntry(K key) {
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = compare(key, p.key);
        if (cmp > 0) { // key > p.key
            if (p.right != null)
                p = p.right; // 往右找更大的
            else
                return p; // 沒右了,當(dāng)前 p 就是小于 key 的最大值
        } else if (cmp < 0) { // key < p.key
            if (p.left != null)
                p = p.left; // 往左找更小的
            else {
                // 沒左了,說明當(dāng)前 p 太大了,得往回找父節(jié)點(diǎn)
                Entry<K,V> parent = p.parent;
                Entry<K,V> ch = p;
                while (parent != null && ch == parent.left) {
                    ch = parent;
                    parent = parent.parent;
                }
                return parent;
            }
        } else
            return p; // 相等,直接返回
    }
    return null;
}

子視圖 subMap 的驚人效率

public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                             E toElement, boolean toInclusive) {
    return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                  toElement, toInclusive));
}

注意: 返回的 TreeSet 并不是復(fù)制數(shù)據(jù)!它持有了一個(gè)指向原 TreeMap 的視圖對象(通常是 TreeMap.AscendingSubMap)。這個(gè)視圖對象僅僅記錄了 fromElement 和 toElement 的邊界。

當(dāng)你對這個(gè)子集進(jìn)行遍歷時(shí),迭代器會(huì)在每次 next() 時(shí)檢查是否越界。如果你向子集中添加元素,add 方法內(nèi)部會(huì)攔截并檢查新元素是否在范圍內(nèi),如果不在范圍直接拋出 IllegalArgumentException。

這意味著:

  • subSet 操作是 O(1) 的,極其輕量
  • 無論原集合多大,獲取子集幾乎沒有內(nèi)存開銷

3、Queue

在 Java 集合框架的龐大體系中,Queue(隊(duì)列)是一個(gè)非常特殊的且重要的接口。如果說 List 是為了存儲(chǔ)和索引,Set 是為了去重,那么 Queue 的存在就是為了處理數(shù)據(jù)。

Queue 通常用于模擬 “先進(jìn)先出(FIFO)” 的數(shù)據(jù)結(jié)構(gòu),但在 Java 語境下,它的含義遠(yuǎn)不止如此。它涵蓋了一系列以有序處理為核心的集合。

 Queue 繼承自 Collection 接口,主要用于在處理之前保存元素。除了標(biāo)準(zhǔn)的集合操作外,Queue 提供了三組特定的操作方式,這是其最顯著的特征:

  • 插入:向隊(duì)列尾部添加元素
  • 移除:移除并返回隊(duì)列頭部元素
  • 檢查:返回隊(duì)列頭部元素但不移除

為了應(yīng)對不同的業(yè)務(wù)場景(特別是容量受限的場景),Queue 為每種操作都定義了兩種方法:

操作拋出異常返回特殊值說明
插入add(E e)offer(E e)隊(duì)列滿時(shí),add 拋出IllegalStateException異常,offer 返回 false
移除remove()poll()隊(duì)列空時(shí),remove 拋出 NoSuchElementException,poll 返回 null
檢查element()peek()隊(duì)列空時(shí),element 拋出 NoSuchElementException,peek 返回 null
3.1 Deque 雙端隊(duì)列(Java 官方推薦的 棧 的替代者)

Deque 是 “ Double Ended Queue” 的縮寫,讀音通常為 “Deck”。它繼承自 Queue 接口,定義了一個(gè)支持在兩端進(jìn)行元素插入和移除的線性集合。

這意味著:

  • 它可以作為 FIFO 隊(duì)列 使用(一端進(jìn),另一端出)
  • 它可以作為 LIFO 棧 使用(同一端進(jìn),同一端出)
  • 它可以作為 雙端隊(duì)列 使用(兩端都可以進(jìn)出)

Deque 的核心在于 “雙端”,打破了傳統(tǒng) Queue 只能隊(duì)尾進(jìn)、對頭出的限制。

3.2 ArrayDeque (Java 中實(shí)現(xiàn)棧和隊(duì)列的首選類)

在 Java 集合框架的工具箱中,ArrayDeque 是一把鋒利、高效且專精的單刃刀。它不是最老資格的(Vector 和 Stack 比它老),也不是功能最全的(LinkedList 實(shí)現(xiàn)了 List 接口),但它是在線性數(shù)據(jù)操作(棧、隊(duì)列、雙端隊(duì)列)場景下的性能王者。

ArrayDeque (全稱 "Array Double Ended Queue(數(shù)組雙端隊(duì)列)")是 Deque 接口的一種可變數(shù)組實(shí)現(xiàn)。

核心特性:

  • 雙端:可以在頭部或尾部高效地插入或刪除元素
  • 非線程安全:沒有 synchronized 修飾,適用于單線程環(huán)境
  • 禁止 null:不允許插入 null 元素(為了區(qū)分空隊(duì)列和 null 值)

為什么 ArrayDeque 這么快?

ArrayDeque 之所以被稱為 Java 集合框架中的 “性能王者”,并非由單一因素決定,而是數(shù)據(jù)結(jié)構(gòu)、算法設(shè)計(jì)、內(nèi)存管理和硬件優(yōu)化四個(gè)方面共同作用的結(jié)果。

數(shù)據(jù)結(jié)構(gòu)層面:循環(huán)數(shù)組 vs 鏈表節(jié)點(diǎn)

這是 ArrayDeque 和 LinkedList (常見的雙端隊(duì)列替代品) 最根本的區(qū)別

  • 指針開銷的消除:
    • LinkedList:每存儲(chǔ)一個(gè)元素,都需要?jiǎng)?chuàng)建一個(gè) Node 對象,內(nèi)部包含 prev(前驅(qū)指針)、next(后繼指針)、item(節(jié)點(diǎn)數(shù)據(jù))三個(gè)引用。在 64 位 JVM 中,僅對線頭和引用就要占用幾十個(gè)字節(jié),內(nèi)存開銷是數(shù)據(jù)本身的好幾倍
    • ArrayDeque:底層是純粹的對象數(shù)組 Object[]。數(shù)據(jù)直接存儲(chǔ),沒有額外的包裝對象。這意味著同樣的內(nèi)存空間,可以存儲(chǔ)更多的數(shù)據(jù),減少 GC(垃圾回收)掃描和回收的壓力
  • 物理內(nèi)存連續(xù)性(CPU 緩存友好):
    • LinkedList:節(jié)點(diǎn)在堆內(nèi)存中是離散分布的。CPU 讀完節(jié)點(diǎn) A,去讀節(jié)點(diǎn) B 時(shí),很可能發(fā)生緩存未命中,必須重新從較慢的主存中讀取數(shù)據(jù)
    • ArrayDeque:數(shù)組在內(nèi)存中是連續(xù)分配的。CPU 在讀取 element[0] 時(shí),會(huì)智能地將 element[1] 至 element[n] 一并加載到 L1/L2 緩存行中。當(dāng)遍歷或連續(xù)操作時(shí),CPU 命中緩存的概率極高,幾乎不需要等待主存

算法設(shè)計(jì)層面:位運(yùn)算 vs 取模

這是 ArrayDeque 比普通數(shù)組隊(duì)列快的原因

  • 位運(yùn)算替代取模:
    • 普通隊(duì)列:實(shí)現(xiàn)循環(huán)邏輯通常使用取模運(yùn)算 index = (index + 1) % length。取模運(yùn)算在 CPU層面涉及除法指令,這是一條昂貴的指令,周期很長。
    • ArrayDeque:強(qiáng)制要求數(shù)組長度為 2 的冪次方。它使用位運(yùn)算 index = (index + 1) & (length - 1)。& 運(yùn)算是 CPU 最基礎(chǔ)的原位操作,只需要一個(gè)時(shí)鐘周期,速度比取??鞄资?/li>
  • O(1) 的雙端操作:
    • 不同于 ArrayList 在頭部插入需要移動(dòng) System.arraycopy 所有元素,ArrayDeque 的 addFirst 和 addLast 僅僅是修改 head 或 tail 指針的值。沒有拷貝數(shù)據(jù),時(shí)間復(fù)雜度穩(wěn)定為 O(1)

并發(fā)策略層面:無鎖設(shè)計(jì) vs 同步鎖

這是 ArrayDeque 比古老的 Stack 或 Vector 快的原因

  •  去除 synchronized:
    • Stack / Vector:為了保證線程安全,所有公共方法都添加了 synchronized 。這意味著即使是單線程操作,也需要獲取鎖。加鎖、釋放鎖、掛起線程(在競爭時(shí))都會(huì)帶來巨大的性能消耗
    • ArrayDeque:完全非線程安全。它假設(shè)你在單線程環(huán)境下使用,或由外部自己控制并發(fā)。因此,它甩掉了所有同步鎖的包袱,飛快運(yùn)行

內(nèi)存操作層面:預(yù)分配 vs 動(dòng)態(tài)分配

  • 擴(kuò)容策略優(yōu)化:
    • ArrayDeque 默認(rèn)初始容量為 16,每次擴(kuò)容為原來的 2 倍。這種指數(shù)級(jí)擴(kuò)容策略分?jǐn)偭藬U(kuò)容的時(shí)間復(fù)雜度,使得 add 操作的平均時(shí)間復(fù)雜度依然為 O(1)
    • 雖然擴(kuò)容時(shí)也需要 Arrays.copyOf,但由于它是基于數(shù)組的,批量內(nèi)存復(fù)制(利用 CPU 的 SIMD 指令集)效率非常高,遠(yuǎn)高于鏈表逐個(gè)節(jié)點(diǎn)的分配和鏈接

底層原理解析

ArrayDeque 是 Java 集合框架中務(wù)實(shí)的代表。它沒有花哨的功能,專注于把 “雙端操作” 做到極致。

核心源碼結(jié)構(gòu):精簡的基石

public class ArrayDeque<E> extends AbstractCollection<E>
                           implements Deque<E>, Cloneable, Serializable
{
    // 用于存儲(chǔ)元素的 
    transient Object[] elements;
    // 對頭指針:指向隊(duì)首元素
    transient int head;
    // 隊(duì)尾指針:指向下一個(gè)待插入元素的位置(隊(duì)尾元素的下一個(gè)位置)
    transient int tail;
    // 最小初始容量
    private static final int MIN_INITIAL_CAPACITY = 8;
}
  • elements.length:數(shù)組長度始終保持為 2 的冪次方(8,16,32........)。這是 ArrayDeque 高效的基石。
  • Head 與 Tail:這是兩個(gè)游標(biāo)
    • head:指向雙端隊(duì)列頭部元素的索引
    • tail:指向雙端隊(duì)列尾部下一個(gè)可以插入元素的索引位置
    • 如果隊(duì)列為空,head 的值等于 tail

核心操作源碼解析

1、添加元素

// java.util.ArrayDeque
// 對頭入隊(duì)
public void addFirst(E e){
    // 安全檢查: ArrayDeque 不允許 null
    if (e == null)
        throw new NullPointerException();
    // 元素入隊(duì),head 指針向前移動(dòng)一位
    elements[head = (head - 1) & (elements.length - 1)] = e;
    // 在插入元素后,檢查 head 是否追上了 tail
    // 在 ArrayDeque 中,數(shù)組中始終至少留一個(gè)空位
    // 當(dāng) head == tail 時(shí),說明數(shù)組已經(jīng)滿了,沒有空位了
    if (head == tail)
        doubleCapacity();
}
// 隊(duì)尾入隊(duì)
public void addLast(E e){
    // 安全檢查: ArrayDeque 不允許 null
    if (e == null)
        throw new NullPointerException();
    // 元素入隊(duì),放在 tail 指向的位置
    elements[tail] = e;
    // tail 指針后移并判斷是否需要擴(kuò)容
    // (tail + 1) & (elements.length - 1) 等同于 (tail + 1) % length
    // 如果移動(dòng)后的 tail 撞上了 head,說明數(shù)組已經(jīng)滿了
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

2、刪除元素

// java.util.ArrayDeque
// 刪除隊(duì)列頭部元素
public E removeFirst() {
    // 調(diào)用 pollFirst() 方法嘗試獲取并移除隊(duì)列頭部的元素,并將結(jié)果賦值給變量 x
    E x = pollFirst();
    // 如果 pollFirst() 返回 null(意味著隊(duì)列為空),則拋出一個(gè) NoSuchElementException 異常
    if(x == null)
        throw new NoSuchElementException();
    // (隊(duì)列不為空)返回獲取到的元素 x
    return x;
}
public E pollFirst() {
    // 將當(dāng)前隊(duì)列頭部的索引 head 賦值給局部變量 h
    int h = head;
    // @SuppressWarnings("unchecked") 用于抑制編譯器的“未檢查類型轉(zhuǎn)換”警告
    // elements 數(shù)組是 Object[] 類型,將其中的元素強(qiáng)制轉(zhuǎn)換為泛型 E 類型時(shí),編譯器會(huì)發(fā)出警告
    // 這里開發(fā)人員確認(rèn)這種轉(zhuǎn)換是安全的,因此忽略警告 
    @SuppressWarnings("unchecked")
    // 取出數(shù)組中索引為 h 的元素,并強(qiáng)制轉(zhuǎn)換為泛型 E 類型,賦值給 result
    E result = (E) elements[h];
    // 判斷取出的元素是否為 null
    if (result == null)
        return null;
    // 將原頭部位置的數(shù)組元素顯式設(shè)為 null
    // 手動(dòng)置為 null,防止內(nèi)存泄漏
    // 在 Java 中,只要一個(gè)對象被引用,垃圾回收器(GC)就不會(huì)回收它
    // 雖然我們即將把 head 指針移走,但數(shù)組中該位置仍然引用著舊的對象
    // 如不手動(dòng)置為 null,這個(gè)對象可能一直占用內(nèi)存直到數(shù)組被覆蓋或回收
    elements[h] = null;
    // 計(jì)算新的頭部索引
    head = (h + 1) & (elements.length - 1);
    // 返回之前取出的元素
    return result;
}
// 刪除隊(duì)列尾部元素
public E removeLast() {
    // 調(diào)用 pollLast() 方法嘗試獲取并移除隊(duì)列尾部的元素,并將結(jié)果賦值給變量 x
    E x = pollLast();
    // 如果 pollLast() 返回 null(意味著隊(duì)列為空),則拋出一個(gè) NoSuchElementException 異常
    if(x == null)
        throw new NoSuchElementException();
    // (隊(duì)列不為空)返回獲取到的元素 x
    return x;
}
public E pollLast() {
    // 計(jì)算隊(duì)尾元素索引
    // tail 指向下一個(gè)可用元素的位置(即當(dāng)前隊(duì)尾元素的下一位)
    // 實(shí)際的最后一個(gè)元素索引是 tail - 1
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    // 取出數(shù)組中索引為 t 的元素,并強(qiáng)制轉(zhuǎn)換為泛型 E 類型,賦值給 result
    E result = (E) elements[t];
    // 判斷取出的元素是否為 null
    if (result == null)
        return null;
    // 顯式置空,幫助 GC
    elements[t] = null;
    // 將隊(duì)尾指針 tail 移動(dòng)到剛才取出元素的位置
    tail = t;
    // 返回取出的元素
    return result;
}

擴(kuò)容機(jī)制:doubleCapacity

當(dāng) head == tail 時(shí),數(shù)組已滿,需要進(jìn)行 2 倍擴(kuò)容。這個(gè)方法非常巧妙,它不僅擴(kuò)大了容量,還重排了數(shù)據(jù)。

// java.util.ArrayDeque
private void doubleCapacity() {
    // 斷言 head 等于 tail
    // 在循環(huán)數(shù)組中,當(dāng)隊(duì)列滿時(shí),head 和 tail 會(huì)指向同一個(gè)位置
    // (即下一個(gè)要插入的位置會(huì)被覆蓋,或者表示沒有空間)
    // 這個(gè)斷言確保該方法只在隊(duì)列滿時(shí)被調(diào)用
    assert head == tail;
    // 保存當(dāng)前隊(duì)首的索引位置到變量 p
    int p = head;
    // 獲取舊數(shù)組的長度 n
    int n = elements.length;
    // 計(jì)算從 head (即 p) 到數(shù)組末尾的元素個(gè)數(shù)
    int r = n - p; 
    // 計(jì)算新容量,n 左移 1 位相當(dāng)于 n * 2
    int newCapacity = n << 1;
    // 檢查新容量是否溢出
    // 如果 n 已經(jīng)非常大(接近 Integer.MAX_VALUE),n * 2 會(huì)變成負(fù)數(shù)(整數(shù)溢出)
    // 此時(shí)隊(duì)列已經(jīng)無法繼續(xù)擴(kuò)容,拋出異常
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 創(chuàng)建一個(gè)容量為 newCapacity 的新數(shù)組 a
    Object[] a = new Object[newCapacity];
    // 復(fù)制 head 到數(shù)組末尾的部分 到 新數(shù)組的開頭
    System.arraycopy(elements, p, a, 0, r);
    // 復(fù)制 數(shù)組開頭 到 tail 的部分 到 新數(shù)組的后面
    System.arraycopy(elements, 0, a, r, p);
    // 將隊(duì)列內(nèi)部的數(shù)組引用指向新數(shù)組 a
    elements = a;
    // 重置隊(duì)首 head 為新數(shù)組的 0 索引
    head = 0;
    // 重置隊(duì)尾 tail 為舊數(shù)組的長度 n
    tail = n;
}

ArrayDeque 的數(shù)據(jù)在循環(huán)數(shù)組中可能跨越了數(shù)組末尾(一部分在頭,一部分在尾)。擴(kuò)容時(shí),它通過兩次 System.arraycopy 將這兩部分?jǐn)?shù)據(jù)“拼”在一起,在新數(shù)組中變成了連續(xù)數(shù)據(jù)。這種重排保證了后續(xù)操作的內(nèi)存連續(xù)性,進(jìn)一步提高緩存命中率。

圖解擴(kuò)容過程:

假設(shè) ArrayDeque 的底層數(shù)組 elements 的初始容量為 8(索引 0 ~7)。

1.擴(kuò)容前的狀態(tài)(隊(duì)列已滿)

此時(shí),隊(duì)列中已經(jīng)存入了 8 個(gè)元素,數(shù)組已滿,無法再插入新元素

  • head 指向索引 4(隊(duì)首元素是 A)
  • tail 指向索引 4(下一個(gè)要插入的位置)
  • 注意:因?yàn)閿?shù)組是循環(huán)使用的,元素被分成了兩部分:
    • 右半部分:從 head(4)到數(shù)組末尾(7),存放了元素 A、B、C、D
    • 左半部分:從 數(shù)組開頭(0)到 head(4)之前,存放了元素 E、F、G、H

數(shù)組視圖:

索引:  0   1    2    3   4   5    6   7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]

2、執(zhí)行 doubleCapacity() 的過程

// java.util.ArrayDeque( doubleCapacity() )
int p = head;            // p = 4
int n = elements.length; // n = 8
int r = n - p;           // r = 8 - 4 = 4 (head右邊有4個(gè)元素)
int newCapacity = n << 1; // newCapacity = 16
Object[] a = new Object[newCapacity]; // 創(chuàng)建新數(shù)組,長度 16
// 步驟 1: 復(fù)制右半部分 [A, B, C, D] 到新數(shù)組開頭
System.arraycopy(elements, p, a, 0, r);
// 步驟 2: 復(fù)制左半部分 [E, F, G, H] 到新數(shù)組后面
System.arraycopy(elements, 0, a, r, p);
elements = a; // 引用指向新數(shù)組
head = 0;     // 重置 head
tail = n;     // 重置 tail = 8

步驟1:復(fù)制右半部分

將舊數(shù)組中從 head(索引 4)開始的 r(4個(gè))元素復(fù)制到新數(shù)組的索引 0 處

舊數(shù)組:

索引:  0   1   2    3    4   5    6   7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]
                                |--- 這4個(gè) ---|

新數(shù)組(復(fù)制右半部分后):

索引:  0   1    2   3   4   5   6   7   8   9  10  11  12  13  14  15
數(shù)據(jù): [A] [B] [C] [D] [ ]  [ ]  [ ]  [ ]  [ ]  [ ]  [ ]   [ ]   [ ]   [ ]   [ ]   [ ]
          |--- 這4個(gè) ---|

步驟2:復(fù)制左半部分

將舊數(shù)組中從索引 0 開始的 p(4個(gè))元素復(fù)制到新數(shù)組的索引 r (4) 處

舊數(shù)組:

索引:  0   1   2    3    4   5    6   7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]
          |--- 這4個(gè) ---|

新數(shù)組(復(fù)制左半部分后):

索引:  0   1    2   3    4    5    6     7    8   9  10  11  12  13  14  15
數(shù)據(jù): [A] [B] [C] [D] [E]  [F]  [G]  [H]  [ ]  [ ]  [ ]   [ ]   [ ]   [ ]   [ ]   [ ]
                               |---    這4個(gè)   ---|

3、擴(kuò)容后的狀態(tài)

復(fù)制完成后,更新指針:

  • head 設(shè)為 0 
  • tail 設(shè)為舊數(shù)組長度 8

最終新數(shù)組視圖:

索引:  0   1    2   3    4    5    6     7    8   9  10  11  12  13  14  15
數(shù)據(jù): [A] [B] [C] [D] [E]  [F]  [G]  [H]  [ ]  [ ]  [ ]   [ ]   [ ]   [ ]   [ ]   [ ]
           ^                                              ^
        head=0                                    tail=8

通過這個(gè)過程,可以看到:

  • 邏輯連續(xù),物理分離的數(shù)據(jù)(A,B,C,D 和 E,F,G,H)被重新拼接成物理連續(xù)的數(shù)據(jù)(A,B,C,D,E,F,G,H)
  • head 回到了數(shù)組的起點(diǎn),這簡化了后續(xù)的索引計(jì)算
  • tail 指向了最后一個(gè)元素的下一個(gè)位置(8),正好是舊數(shù)組的長度,為新元素的插入留出了空間
3.3 PriorityQueue(打破先來后到的“特權(quán)隊(duì)列”)

在 Java 集合框架中,如果說 LinkedList 和 ArrayDeque 遵循的是嚴(yán)格的 “先來后到”(FIFO,先進(jìn)先出)原則,那么 PriorityQueue 就是一個(gè)徹底的 “打破規(guī)則者”。

它不關(guān)心你是什么時(shí)候排隊(duì)的,它只關(guān)心你的優(yōu)先級(jí)。

PriorityQueue(優(yōu)先級(jí)隊(duì)列)是 Java 中基于優(yōu)先級(jí)堆(Priority Heap)實(shí)現(xiàn)的無界優(yōu)先級(jí)隊(duì)列。

核心特性:

  • 無界:理論上可以無限擴(kuò)容(直到內(nèi)存溢出),初始容量默認(rèn)為 11
  • 無序:當(dāng)你遍歷 PriorityQueue 時(shí),元素不一定是有序的
  • 有序的出隊(duì):每次調(diào)用 pop() 或 remove() 時(shí),它保證取出的都是當(dāng)前隊(duì)列中優(yōu)先級(jí)最高(最小或最大)的元素
  • 不支持 null:不允許插入 null 值

注意:

PriorityQueue 具有"內(nèi)部無序但出隊(duì)有序"的特性,這一特點(diǎn)容易讓人產(chǎn)生混淆。要準(zhǔn)確理解這一機(jī)制,關(guān)鍵在于區(qū)分"內(nèi)存中的實(shí)際存儲(chǔ)結(jié)構(gòu)"和"邏輯上的優(yōu)先級(jí)關(guān)系"。

簡而言之:PriorityQueue 就像一個(gè)雜亂無章的房間,但配備了精準(zhǔn)的導(dǎo)航雷達(dá)。

它是堆,不是有序數(shù)組

很多人的直覺認(rèn)為,優(yōu)先級(jí)隊(duì)列應(yīng)該排列似 [1,2,3,4,5] 這樣整齊的數(shù)組。但 PriorityQueue 底層維護(hù)的是二叉堆。

堆的核心規(guī)則只有一條:

  • 小頂堆:父節(jié)點(diǎn)的值不大于其子節(jié)點(diǎn)的

  • 規(guī)則僅限于父子之間,不涉及兄弟節(jié)點(diǎn)

這導(dǎo)致了只需確保父節(jié)點(diǎn)不小于子節(jié)點(diǎn)即可,而對左右子節(jié)點(diǎn)的大小關(guān)系,或是同一層級(jí)最后一個(gè)節(jié)點(diǎn)與下一層級(jí)首個(gè)節(jié)點(diǎn)的相對大小并無具體要求。

圖解存儲(chǔ)過程:

假設(shè)我們依次插入數(shù)字:5,3,1,4,2。

如果是有序數(shù)組,它們在內(nèi)存中是這樣的:[ 1,2,3,4,5 ](非常整齊)

但在 PriorityQueue(二叉堆)中,它們的邏輯結(jié)構(gòu)(樹狀)是這樣的:

       1  <-- 堆頂 (最小)
      /   \
    3     2
   /  \
 5    4

對應(yīng)到底層數(shù)組 Object[] queue 的存儲(chǔ)順序是:

索引:[0]  [1]  [2]  [3]  [4]

數(shù)值:[ 1, 3, 2, 5, 4]

這個(gè)數(shù)組的順序是錯(cuò)亂的!3排在2前面,5又排在4前面,完全沒有按照順序排列。如果你直接遍歷數(shù)組(比如用for(int i : pq)),得到的結(jié)果會(huì)是1、3、2、5、4,這就是典型的"內(nèi)部無序"現(xiàn)象。

出隊(duì)操作:有序

雖然數(shù)組里的元素亂七八槽,但堆頂(索引為 0 的位置)永遠(yuǎn)是所有元素中最小的那個(gè)。

調(diào)用 poll 方法時(shí)的執(zhí)行邏輯如下:

  1. 移除堆頂元素:直接取出數(shù)組首元素 queue[0](當(dāng)前最小值)
  2. 填補(bǔ)空缺:將數(shù)組末尾元素移動(dòng)到堆頂位置
  3. 下沉調(diào)整
    • 若當(dāng)前父節(jié)點(diǎn)(堆頂)值大于子節(jié)點(diǎn),則進(jìn)行下沉操作
    • 父節(jié)點(diǎn)與較小的子節(jié)點(diǎn)交換位置
    • 若交換后仍不滿足堆性質(zhì),則繼續(xù)下沉直至堆底
  4. 形成新堆:經(jīng)過調(diào)整后,原第二小的元素會(huì)升至堆頂位置

圖解過程:

第一次 poll 前:

       1
      /  \
    3    2
   /  \
 5    4

輸出:1

調(diào)整后(poll 后):

       2  <-- 新的王者 (第二小)
      /  \
    4    3
   /
 5

數(shù)組變成了:[2, 4, 3, 5, .....] (依然是亂序的,但堆頂變了)

第二次 poll(),你會(huì)得到 2。

第三次 poll(),你會(huì)得到 3。

結(jié)論:盡管數(shù)組內(nèi)部元素始終是無序排列的,但每次調(diào)用 poll() 方法都能準(zhǔn)確獲取當(dāng)前最小值。通過連續(xù)調(diào)用該方法,最終就能得到一個(gè)有序的輸出序列。

import java.util.PriorityQueue;
public class priorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        // 亂序插入
        queue.add(3);
        queue.add(1);
        queue.add(2);
        queue.add(5);
        queue.add(4);
        // 驗(yàn)證“內(nèi)部無序”:直接打印集合
        System.out.println("直接打印內(nèi)部結(jié)構(gòu)(無序): " + queue);
        // 輸出可能是: [1, 2, 3, 5, 4]  <-- 注意這不是完全排序,只是堆結(jié)構(gòu)
        // 驗(yàn)證“出隊(duì)有序”:使用 poll 遍歷
        System.out.print("依次出隊(duì)(有序): ");
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
        // 輸出: 1 2 3 4 5
    }
}

為什么這樣設(shè)計(jì)?

你可能會(huì)疑惑:"既然有序數(shù)組也能實(shí)現(xiàn)快速遍歷和出隊(duì),為什么不直接采用這種存儲(chǔ)方式呢?"

這就涉及到了時(shí)間復(fù)雜度的權(quán)衡:

操作PriorityQueue有序數(shù)組差距
插入(add)O(log n)O(n)堆完勝
刪除堆頂(poll)O(log n)O(n)堆完勝
獲取最小值O(1)O(1)平手

原因:

  • 有序數(shù)組:每次插入一個(gè)新元素,都要把后面比它大的元素全部往后挪一格(System.arraycopy),非常慢
  • PriorityQueue(堆):插入操作只需將新元素置于末尾,然后像冒泡排序那樣逐層上浮(最多 log n 次),無需移動(dòng)其他元素
底層原理解析

PriorityQueue 是 Java 集合框架中的特殊實(shí)現(xiàn)。與 ArrayList 的線性存儲(chǔ)方式和 HashMap 的哈希結(jié)構(gòu)不同,它采用二叉堆算法實(shí)現(xiàn),能夠在 O(log n) 時(shí)間復(fù)雜度內(nèi)高效獲取極值(最大值或最小值)。

核心源碼結(jié)構(gòu):簡而精

// java.util
public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {
    // 默認(rèn)初始容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    // 底層存儲(chǔ)數(shù)組:通過二叉堆的邏輯來維護(hù)這個(gè)數(shù)組
    transient Object[] queue;
    // 當(dāng)前元素個(gè)數(shù)
    private int size = 0;
    // 比較器:如果為 null,則使用自然排序
    private final Comparator<? super E> comparator;
    // 修改次數(shù)(用于 fail-fast 機(jī)制)
    transient int modCount = 0;
}

盡管名為 Queue(隊(duì)列),但其底層實(shí)現(xiàn)采用了 Object[] queue 數(shù)組結(jié)構(gòu)。通過巧妙運(yùn)用數(shù)組索引關(guān)系,該結(jié)構(gòu)完美模擬了完全二叉樹的特性。

具體實(shí)現(xiàn)細(xì)節(jié)如下:

數(shù)組索引從0開始,通過數(shù)學(xué)關(guān)系構(gòu)建樹形結(jié)構(gòu):

  • 對于任意節(jié)點(diǎn) i(0 ≤ i < size):
    • 其左子節(jié)點(diǎn)索引為 2 * i + 1
    • 其右子節(jié)點(diǎn)索引為 2 * i + 2
    • 其父節(jié)點(diǎn)索引為 (i - 1) >>> 1(無符號(hào)右移,等同于除以 2)

這種數(shù)組實(shí)現(xiàn)方式具有以下優(yōu)勢:

  • 內(nèi)存連續(xù),訪問效率高
  • 不需要額外的指針存儲(chǔ)空間
  • 完全二叉樹的性質(zhì)保證了O(log n)的時(shí)間復(fù)雜度

入隊(duì):offer(e) 與 “上浮”算法

PriorityQueue 的 add 方法實(shí)際上是調(diào)用了 offer 方法。其核心入隊(duì)邏輯分為兩步:首先將新元素添加到隊(duì)列末尾,然后通過"上浮"操作將其調(diào)整到合適的位置。

// java.util.PriorityQueue
public boolean add(E e) {
    return offer(e);
}
public boolean offer(E e) {
    // 非空檢查
    // PriorityQueue 不允許插入 null 元素,如果插入 null 拋出異常
    if(e == null)
        throw new NullPointerException();
    // 修改計(jì)數(shù)器更新
    modCount++;
    // 獲取當(dāng)前隊(duì)列的大小 i
    int i = size;
    // 如果當(dāng)前元素?cái)?shù)量 i 已經(jīng)大于或等于底層數(shù)組 queue 的長度,說明數(shù)組已滿
    if (i >= queue.length)
        // 調(diào)用 grow(i + 1) 方法進(jìn)行擴(kuò)容,以確保能容納新元素
        grow(i + 1);
    // 更新隊(duì)列大小 i
    size = i + 1;
    // 如果插入前隊(duì)列是空的(i == 0),直接將元素放在數(shù)組的第一個(gè)位置 queue[0]
    if (i == 0)
        queue[0] = e;
    // 如果隊(duì)列不為空,調(diào)用 siftUp(i, e) 方法,執(zhí)行上浮調(diào)整
    else
        siftUp(i, e);
    return true;
}

核心算法:siftUp

這是維護(hù)小頂堆性質(zhì)的核心操作。具體實(shí)現(xiàn)時(shí),先將新元素 e 插入數(shù)組末尾(位置 i),然后將其與父節(jié)點(diǎn)進(jìn)行比較。若發(fā)現(xiàn)新元素小于父節(jié)點(diǎn)(符合小頂堆特性),則交換兩者位置。該過程循環(huán)執(zhí)行,直至新元素到達(dá)堆頂或不再小于其父節(jié)點(diǎn)時(shí)終止。 

// java.util.PriorityQueue
private void siftUp(int k,E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
private void siftUpUsingComparator(int k,E x) {
    // 循環(huán)條件:while (k > 0)
    // 只要當(dāng)前節(jié)點(diǎn)索引 k 不是根節(jié)點(diǎn)(根節(jié)點(diǎn)索引為 0),就繼續(xù)嘗試上浮
    while (k > 0) {
        // 計(jì)算當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)索引
        int parent = (k - 1) >>> 1;
        // 取出父節(jié)點(diǎn)存儲(chǔ)的元素
        Object e = queue[parent];
        // 使用比較器比較當(dāng)前待插入元素 x 和父節(jié)點(diǎn)元素 e
        // 如果 x 大于或等于 e,則 break 跳出循環(huán)。此時(shí)位置 k 就是 x 的最終歸宿
        if (comparator.compare(x, (E) e) >= 0)
            break;
        // 如果 x 小于 e,說明違反了堆序性質(zhì)(子節(jié)點(diǎn)比父節(jié)點(diǎn)?。枰粨Q位置。
        queue[k] = e;
        // 更新當(dāng)前索引 k 為父節(jié)點(diǎn)的索引,準(zhǔn)備繼續(xù)向上層比較
        k = parent;
    }
    // 循環(huán)結(jié)束后(要么找到了合適的位置,要么到了根節(jié)點(diǎn)),將目標(biāo)元素 key 放入最終確定的位置 k
    queue[k] = x;
}
private void siftUpComparable(int k,E x) {
    // 將傳入的元素 x 強(qiáng)制轉(zhuǎn)換為 Comparable 類型
    // PriorityQueue 需要比較元素的大小來確定優(yōu)先級(jí)
    Comparable<? super E> key = (Comparable<? super E>) x;
    // 開始循環(huán)。只要當(dāng)前節(jié)點(diǎn)索引 k 大于 0(即當(dāng)前節(jié)點(diǎn)不是根節(jié)點(diǎn)),就繼續(xù)嘗試上浮
    while (k > 0) {
        // 計(jì)算當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)索引
        int parent = (k - 1) >>> 1;
        // 取出父節(jié)點(diǎn)存儲(chǔ)的元素
        Object e = queue[parent];
        // 比較當(dāng)前插入元素 key 和父節(jié)點(diǎn)元素 e 的大小
        // 如果 key >= e
        // 說明堆的性質(zhì)已經(jīng)滿足(父節(jié)點(diǎn)小于等于子節(jié)點(diǎn)),無需繼續(xù)交換,直接 break 跳出循環(huán)
        if (key.compareTo((E) e) >= 0)
            break;
        // 如果 key < e,說明子節(jié)點(diǎn)比父節(jié)點(diǎn)小,違反了最小堆性質(zhì)
        // 則將父節(jié)點(diǎn) e 向下移動(dòng)到當(dāng)前子節(jié)點(diǎn) k 的位置
        queue[k] = e;
        // 更新當(dāng)前索引 k 為父節(jié)點(diǎn)的索引,準(zhǔn)備繼續(xù)向上層比較
        k = parent;
    } 
    // 循環(huán)結(jié)束后(要么找到了合適的位置,要么到了根節(jié)點(diǎn)),將目標(biāo)元素 key 放入最終確定的位置 k
    queue[k] = key;
}

入隊(duì)操作流程:將新元素添加至數(shù)組末尾,隨后進(jìn)行上浮調(diào)整。循環(huán)比較該元素與其父節(jié)點(diǎn),若優(yōu)先級(jí)更高則交換位置,直至到達(dá)堆頂或滿足堆條件為止。

出隊(duì):poll() 與 下沉算法

poll() 操作需要移除堆頂?shù)淖钚≈翟亍R瞥?,我們將?shù)組末尾的元素移至堆頂位置,然后通過"下沉"操作使其重新找到合適的位置,以維持堆結(jié)構(gòu)的完整性。

// java.util.PriorityQueue
public E poll() {
    // 檢查隊(duì)列是否為空
    if (size == 0)
        return null; // 如果為空,返回 null(區(qū)別于 remove() 方法拋出異常)
    // 記錄操作前的元素個(gè)數(shù),并將 size 減 1
    // s 變成了原數(shù)組最后一個(gè)元素的下標(biāo)
    int s = --size;
    // 更新修改計(jì)數(shù)器
    modCount++;
    // 獲取堆頂元素(即最小值),作為最終要返回的結(jié)果
    E result = (E) queue[0];
    // 獲取隊(duì)列末尾的元素
    E x = (E) queue[s];
    // 將原末尾位置置為 null,幫助 GC 回收,避免內(nèi)存泄漏
    queue[s] = null;
    // 如果隊(duì)列中不止一個(gè)元素,則需要執(zhí)行下沉操作
    // 將剛才取出的末尾元素 x 放到堆頂位置(下標(biāo) 0),并開始下沉
    if (s != 0)
        siftDown(0, x);
    // 返回之前獲取的最小值
    return result;
}

核心算法:siftDown

將堆末尾的大元素移至堆頂后,由于它必然大于子節(jié)點(diǎn),需要通過下沉操作進(jìn)行調(diào)整。具體下沉規(guī)則是:父節(jié)點(diǎn)需要與左右孩子中較小者進(jìn)行交換。

// java.util.PriorityQueue
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
    // 計(jì)算非葉子節(jié)點(diǎn)邊界
    int half = size >>> 1; // 無符號(hào)右移(等同于 size / 2)
    while (k < half) {
        // 計(jì)算當(dāng)前節(jié)點(diǎn) k 的左孩子的索引
        int child = (k << 1) + 1;
        // 獲取左孩子元素 
        Object c = queue[child];
        // 計(jì)算右孩子的索引
        int right = child + 1;
        // 如果右孩子存在,且右孩子比左孩子小
        if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right]; // 更新 child 指向右孩子
        // 如果當(dāng)前元素 x 已經(jīng)小于等于較小的孩子,說明下沉結(jié)束
        if (comparator.compare(x, (E) c) <= 0)
            break;
        // 否則,交換位置(孩子上浮成為父節(jié)點(diǎn))
        queue[k] = c;
        k = child;
    }
    // 將末尾元素放到最終的位置
    queue[k] = x;
}
private void siftDownComparable(int k, E x) {
    // 將待下沉的元素 x 強(qiáng)制轉(zhuǎn)換為 Comparable 接口類型
    // 因?yàn)樾枰{(diào)用 compareTo 方法來比較元素大小
    Comparable<? super E> key = (Comparable<? super E>)x;
    // 計(jì)算非葉子節(jié)點(diǎn)邊界
    int half = size >>> 1; // 無符號(hào)右移(等同于 size / 2)
    while (k < half) {
        // 計(jì)算當(dāng)前節(jié)點(diǎn) k 的左孩子的索引
        int child = (k << 1) + 1;
        // 獲取左孩子元素
        Object c = queue[child];
        // 計(jì)算右孩子的索引
        int right = child + 1;
        // 比較左右孩子
        // right<size:檢查右孩子是否存在(是否越界)
        // c.compareTo(queue[right])>0:如果左孩子比右孩子大(compareTo 返回正數(shù))說明右孩子更小
        if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            // 滿足條件,將 c 指向右孩子,并將 child 更新為右孩子的索引
            c = queue[child = right];
        // 如果 key 小于等于 c(compareTo 返回 0 或負(fù)數(shù)),說明堆的順序已經(jīng)正確(父節(jié)點(diǎn)小于子節(jié)點(diǎn))
        if (key.compareTo((E) c) <= 0)
            // 跳出循環(huán),下沉結(jié)束
            break;
        // 如果 key 大于 c,說明堆性質(zhì)被破壞。將較小的子節(jié)點(diǎn) c 移動(dòng)到當(dāng)前父節(jié)點(diǎn) k 的位置
        queue[k] = c;
        // 更新 k 為子節(jié)點(diǎn)的位置,準(zhǔn)備進(jìn)行下一輪比較
        k = child;
    }
    // 循環(huán)結(jié)束后,將原始元素 key 放到最終確定的位置 k 上
    queue[k] = key;
}

出隊(duì)操作:移除堆頂?shù)淖钚≈翟睾?,將?shù)組末尾元素移至堆頂。隨后將該元素與其左右子節(jié)點(diǎn)中較小者進(jìn)行比較,若當(dāng)前元素較大則交換位置(下沉操作)。重復(fù)此過程直至堆恢復(fù)平衡狀態(tài)。

擴(kuò)容機(jī)制:grow

PriorityQueue 的擴(kuò)容策略很有意思,不是簡單的 2 倍擴(kuò)容,而是根據(jù)當(dāng)前容量階段性的增長。

// java.util.PriorityQueue
private void grow(int minCapacity) {
    // 獲取當(dāng)前底層數(shù)組 queue 的長度
    int oldCapacity = queue.length;
    // 如果舊容量小于 64,則擴(kuò)容 2 倍 + 2
    // 如果舊容量大于 64,則擴(kuò)容 1.5 倍 (即 oldCapacity + (oldCapacity >> 1))
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
    // 防止溢出,如果計(jì)算出的容量過大
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 調(diào)用 hugeCapacity 方法嘗試分配
        newCapacity = hugeCapacity(minCapacity);
    // 調(diào)用 Arrays.copyOf 方法,將原數(shù)組中的數(shù)據(jù)復(fù)制到一個(gè)長度為 newCapacity 的新數(shù)組中,并將 queue 引用指向這個(gè)新數(shù)組
    queue = Arrays.copyOf(queue, newCapacity);  
}
private static int hugeCapacity(int minCapacity) {
    // 溢出檢查:通過檢查 minCapacity 是否小于 0,來判斷是否發(fā)生了整數(shù)溢出
    // 在 Java 中,int 是有符號(hào)的 32 位整數(shù),最大值為 Integer.MAX_VALUE(約 21 億)
    // 如果之前的計(jì)算(例如 minCapacity = oldCapacity + (oldCapacity >> 1))導(dǎo)致數(shù)值超過了 int 的最大值
    // 由于整數(shù)溢出,結(jié)果會(huì)變成負(fù)數(shù)
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    // 如果 minCapacity 大于 MAX_ARRAY_SIZE,則直接返回 Integer.MAX_VALUE(即 2147483647)嘗試分配理論上的最大數(shù)組長度
    // 如果 minCapacity 小于或等于 MAX_ARRAY_SIZE,則返回 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8(即 2147483639))
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

PriorityQueue 是非線程安全的類,其 grow 方法未實(shí)現(xiàn)同步機(jī)制。在多線程并發(fā)添加元素觸發(fā)擴(kuò)容時(shí),可能導(dǎo)致數(shù)據(jù)不一致或數(shù)組越界問題。建議在多線程場景下使用線程安全的 PriorityBlockingQueue 替代。

到此這篇關(guān)于Java集合框架的 Collection 分支的文章就介紹到這了,更多相關(guān)Java集合框架Collection 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java?11新特性HttpClient主要組件及發(fā)送請求示例詳解

    java?11新特性HttpClient主要組件及發(fā)送請求示例詳解

    這篇文章主要為大家介紹了java?11新特性HttpClient主要組件及發(fā)送請求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Spring4改造Dubbo實(shí)現(xiàn)注解配置兼容的完整指南

    Spring4改造Dubbo實(shí)現(xiàn)注解配置兼容的完整指南

    在微服務(wù)架構(gòu)中,Dubbo作為一款高性能的Java RPC框架,被廣泛應(yīng)用于分布式系統(tǒng)中,本文將探討如何改造Dubbo,使其能夠更好地兼容Spring4的注解配置
    2025-07-07
  • java數(shù)據(jù)結(jié)構(gòu)和算法學(xué)習(xí)之漢諾塔示例

    java數(shù)據(jù)結(jié)構(gòu)和算法學(xué)習(xí)之漢諾塔示例

    這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)和算法中的漢諾塔示例,需要的朋友可以參考下
    2014-02-02
  • Spring Boot加密配置文件方法介紹

    Spring Boot加密配置文件方法介紹

    這篇文章主要介紹了SpringBoot加密配置文件,近期在對開發(fā)框架安全策略方面進(jìn)行升級(jí)優(yōu)化,提供一些通用場景的解決方案,本文針對配置文件加密進(jìn)行簡單的分享
    2023-01-01
  • java編程abstract類和方法詳解

    java編程abstract類和方法詳解

    這篇文章主要介紹了java編程abstract類和方法詳解,具有一定借鑒價(jià)值,需要的朋友可以參考下。
    2017-12-12
  • Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式

    Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式

    這篇文章主要介紹了Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解

    SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解

    這篇文章主要介紹了SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-03-03
  • Java項(xiàng)目打包Docker鏡像全流程

    Java項(xiàng)目打包Docker鏡像全流程

    本文是一份超詳細(xì)的Java項(xiàng)目Docker化實(shí)戰(zhàn)手冊,從環(huán)境準(zhǔn)備到最終上線,手把手帶你完成整個(gè)容器化部署流程,無論你是剛接觸Docker的新手,還是想系統(tǒng)梳理容器化流程的開發(fā)者,這篇文章都能給你帶來實(shí)實(shí)在在的幫助,需要的朋友可以參考下
    2025-04-04
  • Spring Boot 整合 MongoDB的示例

    Spring Boot 整合 MongoDB的示例

    這篇文章主要介紹了Spring Boot 整合 MongoDB的示例,幫助大家更好的理解和學(xué)習(xí)spring boot框架,感興趣的朋友可以了解下
    2020-10-10
  • Java 基礎(chǔ)語法之解析 Java 的包和繼承

    Java 基礎(chǔ)語法之解析 Java 的包和繼承

    包是組織類的一種方式,繼承顧名思義,比如誰繼承了長輩的產(chǎn)業(yè),其實(shí)這里的繼承和我們生活中的繼承很類似,下面文字將為大家詳細(xì)介紹Java的包和繼承
    2021-09-09

最新評(píng)論

日本免费视频午夜福利视频| 2022国产综合在线干| 久久久久久97三级| 天天干天天日天天干天天操| 久久麻豆亚洲精品av| 国产aⅴ一线在线观看| 激情五月婷婷免费视频| 国产免费av一区二区凹凸四季| 国产一区二区三免费视频| 六月婷婷激情一区二区三区| 国产一区av澳门在线观看| 真实国产乱子伦一区二区| 曰本无码人妻丰满熟妇啪啪| 国产在线91观看免费观看| 在线观看一区二区三级| 99re国产在线精品| 99久久中文字幕一本人| 中国把吊插入阴蒂的视频| 亚洲综合图片20p| 2020国产在线不卡视频| 青青青国产免费视频| 国产自拍黄片在线观看| 欧洲黄页网免费观看| 亚洲精品色在线观看视频| av亚洲中文天堂字幕网| av亚洲中文天堂字幕网| 天天操天天干天天艹| 在线播放 日韩 av| 国产大鸡巴大鸡巴操小骚逼小骚逼| 午夜dv内射一区区| 都市激情校园春色狠狠| 成人国产激情自拍三区| 午夜91一区二区三区| 国产成人自拍视频播放| 国产高清在线观看1区2区| 天天色天天操天天透| 久久久精品国产亚洲AV一 | 阿v天堂2014 一区亚洲| 2020韩国午夜女主播在线| 国产精品国产三级国产午| 最新欧美一二三视频| 日日夜夜大香蕉伊人| 肏插流水妹子在线乐播下载 | 日韩av有码一区二区三区4| 操人妻嗷嗷叫视频一区二区| 亚洲va欧美va人人爽3p| 91超碰青青中文字幕| 91精品国产综合久久久蜜 | 在线免费视频 自拍| 精品美女福利在线观看| 77久久久久国产精产品| 久草极品美女视频在线观看| 九色精品视频在线播放| 91中文字幕免费在线观看| 天天干夜夜操啊啊啊| 一色桃子人妻一区二区三区| 91桃色成人网络在线观看| 欧美男人大鸡吧插女人视频 | 激情内射在线免费观看| 亚洲自拍偷拍综合色| 亚洲一区二区激情在线| 亚洲国际青青操综合网站| 首之国产AV医生和护士小芳| 青青草精品在线视频观看| 女生被男生插的视频网站| 精品人人人妻人人玩日产欧| 婷婷色中文亚洲网68| 亚洲av色图18p| 欧美日韩在线精品一区二区三| 男人插女人视频网站| 国产美女一区在线观看| 久精品人妻一区二区三区| 偷拍3456eee| 99精品国产aⅴ在线观看| 丰满少妇翘臀后进式| 亚洲嫩模一区二区三区| 国产片免费观看在线观看| 久久丁香婷婷六月天| 日本韩国免费福利精品| 青青草视频手机免费在线观看| av日韩在线免费播放| 18禁无翼鸟成人在线| 日韩av有码中文字幕| 日本女人一级免费片| 日本韩国免费一区二区三区视频| 成年人啪啪视频在线观看| 亚洲国产欧美一区二区三区久久| 亚洲av黄色在线网站| 在线视频精品你懂的| 欧美 亚洲 另类综合| 欧美一级色视频美日韩| 在线制服丝袜中文字幕| 成人免费毛片aaaa| 激情伦理欧美日韩中文字幕| 欧美视频中文一区二区三区| 人妻激情图片视频小说| 久精品人妻一区二区三区| 日本熟女精品一区二区三区| 国产性感美女福利视频| 久久久久久九九99精品| 久久久久久性虐视频| 98视频精品在线观看| 天天干夜夜操啊啊啊| 国产精品欧美日韩区二区| 国产va在线观看精品| 可以免费看的www视频你懂的| 早川濑里奈av黑人番号| 9l人妻人人爽人人爽| 欧美专区第八页一区在线播放| 中文字幕日韩精品就在这里| 午夜免费体验区在线观看| 亚洲精品成人网久久久久久小说| 性感美女高潮视频久久久| 乱亲女秽乱长久久久| nagger可以指黑人吗| 国产在线观看免费人成短视频| 欧美一级色视频美日韩| 操的小逼流水的文章| 免费手机黄页网址大全| 在线新三级黄伊人网| 国产亚洲欧美视频网站| 欧美一区二区三区高清不卡tv | 激情小视频国产在线| 狠狠嗨日韩综合久久| 日本一区美女福利视频| 人妻爱爱 中文字幕| 国产真实乱子伦a视频| 国产亚洲精品品视频在线| 欧美女同性恋免费a| 97青青青手机在线视频| 色婷婷精品大在线观看| 国产janese在线播放| 香港一级特黄大片在线播放| 日韩熟女av天堂系列| 国产女孩喷水在线观看| 亚洲国产精品免费在线观看| 天天干天天操天天插天天日| 91综合久久亚洲综合| av手机免费在线观看高潮| 99热99re在线播放| 中文字幕一区二区人妻电影冢本| 蜜桃专区一区二区在线观看| 亚洲第一黄色在线观看| 美女视频福利免费看| 2012中文字幕在线高清| 天天色天天操天天透| 天天射夜夜操狠狠干| 天美传媒mv视频在线观看| 免费十精品十国产网站| 天天日天天做天天日天天做| 成年午夜免费无码区| 亚洲欧美福利在线观看| 综合页自拍视频在线播放| 韩国三级aaaaa高清视频| 51精品视频免费在线观看| 日噜噜噜夜夜噜噜噜天天噜噜噜| 日本精品一区二区三区在线视频。| 91麻豆精品秘密入口在线观看| 亚洲欧美自拍另类图片| 人妻丝袜精品中文字幕| 精品一区二区三区午夜| 黄色片年轻人在线观看| 一区二区在线观看少妇| 亚洲va国产va欧美va在线| 免费看高清av的网站| 中文字幕亚洲中文字幕| 国产在线自在拍91国语自产精品 | 99久久激情婷婷综合五月天| 一区二区三区久久久91| 久久久精品欧洲亚洲av| 色婷婷久久久久swag精品| 天天日天天透天天操| 欧美日本国产自视大全| 在线观看免费视频网| 91自产国产精品视频| 国产精品视频资源在线播放| 日韩美女福利视频网| 国产剧情演绎系列丝袜高跟| 天天躁夜夜躁日日躁a麻豆| 天干天天天色天天日天天射| 中文字幕av一区在线观看| 亚洲av琪琪男人的天堂| AV无码一区二区三区不卡| 久久午夜夜伦痒痒想咳嗽P| 欧美精品资源在线观看| 日本av高清免费网站| 欧美精品欧美极品欧美视频| 国产乱子伦精品视频潮优女| 久久精品在线观看一区二区| 亚洲免费在线视频网站| 亚洲va天堂va国产va久| 久久久极品久久蜜桃| 一区二区三区另类在线| 毛片av在线免费看| 中文字幕AV在线免费看 | 首之国产AV医生和护士小芳| 精品久久久久久久久久久久人妻 | 亚洲欧美自拍另类图片| 国产三级片久久久久久久| 日韩三级黄色片网站| 99精品视频之69精品视频| 天天日天天玩天天摸| 少妇ww搡性bbb91| 端庄人妻堕落挣扎沉沦| 青草青永久在线视频18| yellow在线播放av啊啊啊| 午夜美女福利小视频| 精品91自产拍在线观看一区| 国产乱子伦一二三区| 国产欧美精品一区二区高清| 欧美亚洲牲夜夜综合久久| 国产白嫩美女一区二区| 国产精品伦理片一区二区| 老司机福利精品免费视频一区二区| 韩国一级特黄大片做受| 一二三区在线观看视频| 国产性色生活片毛片春晓精品 | 欧美一区二区三区乱码在线播放 | 极品粉嫩小泬白浆20p主播 | 女蜜桃臀紧身瑜伽裤| 日本少妇人妻xxxxxhd| 伊人成人在线综合网| 99精品视频之69精品视频 | 成人高清在线观看视频| 在线国产日韩欧美视频| 中文字幕在线视频一区二区三区| 黄色大片男人操女人逼| 久久久久久久99精品| 熟女国产一区亚洲中文字幕| 激情人妻校园春色亚洲欧美 | 日韩一个色综合导航| 婷婷综合蜜桃av在线| 夜夜嗨av蜜臀av| 97精品人妻一区二区三区精品| 丰满的子国产在线观看| 中文字幕高清在线免费播放 | 亚国产成人精品久久久| 久草视频在线免播放| 18禁精品网站久久| 国产一区二区火爆视频| 97国产在线观看高清| 亚洲精品 日韩电影| 欧美3p在线观看一区二区三区| 18禁网站一区二区三区四区| 在线观看一区二区三级| 日韩一区二区电国产精品| 国产精品一二三不卡带免费视频| 精品视频一区二区三区四区五区| 中文字幕日韩91人妻在线| 97年大学生大白天操逼| 成人在线欧美日韩国产| 成人福利视频免费在线| 日本裸体熟妇区二区欧美| 欧美3p在线观看一区二区三区| 日本av在线一区二区三区| 日韩a级黄色小视频| av视屏免费在线播放| 视频一区二区综合精品| 天天躁夜夜躁日日躁a麻豆| 亚洲免费国产在线日韩| 欧美女同性恋免费a| 欧美少妇性一区二区三区| 在线免费观看av日韩| 极品丝袜一区二区三区| 天天操天天爽天天干| 亚洲欧美清纯唯美另类| 国产麻豆剧传媒精品国产av蜜桃| 97人妻夜夜爽二区欧美极品| 亚洲 中文 自拍 另类 欧美| rct470中文字幕在线| 成年午夜免费无码区| 91极品大一女神正在播放| 亚洲成人情色电影在线观看| 午夜精品久久久久久99热| 黑人变态深video特大巨大| 青青草人人妻人人妻| 国产在线自在拍91国语自产精品| 日韩欧美一级黄片亚洲| 亚洲日本一区二区三区| 国产亚洲四十路五十路| 97精品综合久久在线| 亚洲最大免费在线观看| 亚洲高清免费在线观看视频| 蜜桃色婷婷久久久福利在线| 人妻无码中文字幕专区| 天天操天天干天天艹| 精品一区二区三区三区色爱| 亚洲av成人网在线观看| 日本成人一区二区不卡免费在线| 欧美xxx成人在线| 操人妻嗷嗷叫视频一区二区| 亚洲欧美另类自拍偷拍色图| 国产真实乱子伦a视频| 亚洲伊人久久精品影院一美女洗澡| 国产真实灌醉下药美女av福利| 天天摸天天日天天操| 精品亚洲国产中文自在线| 自拍偷拍一区二区三区图片| 青青青视频自偷自拍38碰| 国产高潮无码喷水AV片在线观看 | 亚洲一区二区久久久人妻| 天天干狠狠干天天操| 成人蜜臀午夜久久一区| 91精品综合久久久久3d动漫| 欧美日韩精品永久免费网址| 在线免费观看视频一二区| 99的爱精品免费视频| 日本性感美女写真视频| 91国偷自产一区二区三区精品| 天天射夜夜操综合网| 五十路av熟女松本翔子| 国产美女午夜福利久久| 亚洲精品无码色午夜福利理论片| 1024久久国产精品| 都市家庭人妻激情自拍视频| 青青草在观免费国产精品| 亚洲成人线上免费视频观看| 亚洲丝袜老师诱惑在线观看| 天天干夜夜操天天舔| 91试看福利一分钟| 国产亚洲视频在线二区| 最新激情中文字幕视频| 3344免费偷拍视频| 一个人免费在线观看ww视频| 国产精品国产精品一区二区| 老司机你懂得福利视频| 日本高清成人一区二区三区| 天天干狠狠干天天操| 天天色天天操天天透| 久久久91蜜桃精品ad| 白嫩白嫩美女极品国产在线观看| 亚洲午夜精品小视频| 免费十精品十国产网站| 黄色三级网站免费下载| 久久久麻豆精亚洲av麻花| 精品一区二区三区欧美| 啊啊啊想要被插进去视频| 91色秘乱一区二区三区| 78色精品一区二区三区| 大香蕉伊人中文字幕| 亚洲va欧美va人人爽3p| 欧美另类一区二区视频| 一级黄色av在线观看| 97资源人妻免费在线视频| 国产午夜亚洲精品麻豆| av中文字幕国产在线观看| 一区二区三区日本伦理| 大尺度激情四射网站| 在线免费观看亚洲精品电影| 性色av一区二区三区久久久| 欧美老妇精品另类不卡片| 乱亲女秽乱长久久久| 国产三级影院在线观看| av线天堂在线观看| 亚洲av色香蕉一区二区三区| 五月精品丁香久久久久福利社| 欧美在线偷拍视频免费看| 中国老熟女偷拍第一页| 好男人视频在线免费观看网站| 国产麻豆剧果冻传媒app| 免费高清自慰一区二区三区网站| 成人av电影免费版| 男人天堂av天天操| 国产女人被做到高潮免费视频| av成人在线观看一区| 美女骚逼日出水来了| 性色蜜臀av一区二区三区| 国产精品黄色的av| 国产午夜亚洲精品不卡在线观看| 99国内小视频在现欢看| 国语对白xxxx乱大交| 在线观看亚洲人成免费网址| 久久久久91精品推荐99| 日本中文字幕一二区视频| 国产成人精品久久二区91| 国产精品自偷自拍啪啪啪| 五月天久久激情视频| 大白屁股精品视频国产| 国产在线一区二区三区麻酥酥| 99精品视频在线观看免费播放| 久久久久久国产精品| 成熟丰满熟妇高潮xx×xx| 97人妻色免费视频| 黄色成年网站午夜在线观看 | 老熟妇凹凸淫老妇女av在线观看| 一区二区三区蜜臀在线| 国产剧情演绎系列丝袜高跟| 中文字幕 码 在线视频| 亚洲欧美一区二区三区电影| 天干天天天色天天日天天射| 一区二区三区毛片国产一区| 2020国产在线不卡视频| 中文乱理伦片在线观看| 国产精品一区二区久久久av| 中文字幕在线欧美精品| 91九色porny国产蝌蚪视频| 久草视频 久草视频2| 经典av尤物一区二区| 啊啊啊视频试看人妻| www久久久久久久久久久| 少妇露脸深喉口爆吞精| 国产一区成人在线观看视频| 美女少妇亚洲精选av| 亚洲高清视频在线不卡| 人人妻人人爱人人草| 人妻丝袜av在线播放网址| 丝袜国产专区在线观看| 女同久久精品秋霞网| 2020中文字幕在线播放| 中文字幕人妻一区二区视频 | 曰本无码人妻丰满熟妇啪啪| 丰满少妇人妻xxxxx| 蜜桃视频入口久久久| 啊啊啊想要被插进去视频| 日韩伦理短片在线观看| 久久国产精品精品美女| 97超碰国语国产97超碰| 99热久久这里只有精品8| 日本午夜久久女同精女女| 成人亚洲国产综合精品| 欧美黄片精彩在线免费观看| 天天日天天干天天要| 日韩美女搞黄视频免费| 80电影天堂网官网| 日本人妻少妇18—xx| 极品丝袜一区二区三区| 亚洲欧美国产综合777| 蜜桃视频17c在线一区二区| 毛片av在线免费看| 亚洲人成精品久久久久久久| 大陆胖女人与丈夫操b国语高清| 性欧美日本大妈母与子| 久久精品视频一区二区三区四区| 亚洲码av无色中文| 国产自拍在线观看成人| 国产成人精品亚洲男人的天堂| 国产精品成久久久久三级蜜臀av| 激情五月婷婷免费视频| 婷婷六月天中文字幕| 韩国黄色一级二级三级| 40道精品招牌菜特色| 人妻丝袜榨强中文字幕| 日本精品美女在线观看| 美女骚逼日出水来了| 懂色av之国产精品| 97成人免费在线观看网站| 91香蕉成人app下载| 亚洲激情唯美亚洲激情图片| 亚洲另类图片蜜臀av| 激情啪啪啪啪一区二区三区| 黑人巨大精品欧美视频| 婷婷五月亚洲综合在线| 91国内精品自线在拍白富美| 亚洲免费成人a v| 视频在线免费观看你懂得| 亚洲精品一线二线在线观看| 经典国语激情内射视频| 久久人人做人人妻人人玩精品vr| av天堂资源最新版在线看| 在线观看免费岛国av| 久久艹在线观看视频| 97欧洲一区二区精品免费| 天天日天天干天天搡| 国产刺激激情美女网站| 欧美乱妇无乱码一区二区| 日日操综合成人av| 91精品啪在线免费| 操的小逼流水的文章| 久久精品亚洲成在人线a| 久久午夜夜伦痒痒想咳嗽P| 热久久只有这里有精品| 国产日本精品久久久久久久| 欧洲国产成人精品91铁牛tv| 色伦色伦777国产精品| 在线不卡日韩视频播放| 亚洲精品无码色午夜福利理论片| 精品黑人一区二区三区久久国产| 77久久久久国产精产品| 日本人妻少妇18—xx| 久久丁香花五月天色婷婷| 日日日日日日日日夜夜夜夜夜夜| 99精品免费观看视频| 男生舔女生逼逼的视频| 亚洲精品 欧美日韩| 国产一区二区久久久裸臀| 绝色少妇高潮3在线观看| 成年人的在线免费视频| 91亚洲精品干熟女蜜桃频道 | 中文字幕中文字幕人妻| 婷婷午夜国产精品久久久| 久精品人妻一区二区三区| 91免费黄片可看视频| 三级av中文字幕在线观看| 日韩国产乱码中文字幕| 日比视频老公慢点好舒服啊| 日韩av大胆在线观看| 亚洲精品在线资源站| 99re久久这里都是精品视频| 国产老熟女伦老熟妇ⅹ| 日本后入视频在线观看| 9久在线视频只有精品| 国产一区成人在线观看视频 | 丝袜长腿第一页在线| 国产一区成人在线观看视频| 中国黄片视频一区91| 久久久精品999精品日本| 操的小逼流水的文章| 美女少妇亚洲精选av| 亚洲人妻30pwc| 亚洲av男人天堂久久| 日韩欧美在线观看不卡一区二区| 欧美熟妇一区二区三区仙踪林| 成年午夜免费无码区| 国产精品一二三不卡带免费视频| 成人区人妻精品一区二视频| 自拍偷区二区三区麻豆| 干逼又爽又黄又免费的视频| 日本免费视频午夜福利视频| 久久精品亚洲成在人线a| 又色又爽又黄又刺激av网站| 色狠狠av线不卡香蕉一区二区| sspd152中文字幕在线| 91p0rny九色露脸熟女| 日本韩国在线观看一区二区| 色婷婷久久久久swag精品| 日本av熟女在线视频| 夜色17s精品人妻熟女| 可以免费看的www视频你懂的| 大陆av手机在线观看| 美洲精品一二三产区区别| 美女小视频网站在线| 啪啪啪啪啪啪啪啪啪啪黄色| 欧美激情电影免费在线| 播放日本一区二区三区电影| 可以在线观看的av中文字幕| 偷青青国产精品青青在线观看| 久久久久久性虐视频| 青娱乐最新视频在线| 精品91自产拍在线观看一区| 青青青青青免费视频| 天天干夜夜操天天舔| 亚洲区欧美区另类最新章节| 国产极品精品免费视频| 免费黄页网站4188| 夜女神免费福利视频| 韩国一级特黄大片做受| 韩国爱爱视频中文字幕| 午夜精品福利91av| 超碰在线中文字幕一区二区| 亚洲av自拍偷拍综合| 天天干天天日天天干天天操| 91麻豆精品91久久久久同性| 97人妻总资源视频| 国产精品一二三不卡带免费视频| 91成人在线观看免费视频| 91免费放福利在线观看| 欧美一区二区中文字幕电影 | 亚洲成人免费看电影| 日韩人妻丝袜中文字幕| 欧美偷拍亚洲一区二区| 欧美女同性恋免费a| 五十路在线观看完整版| 精品老妇女久久9g国产| 人人妻人人爽人人添夜| 欧美一级视频一区二区| 99久久激情婷婷综合五月天| 亚洲av无乱一区二区三区性色| 秋霞午夜av福利经典影视| 欧美色婷婷综合在线| 午夜在线观看一区视频| 亚洲av可乐操首页| 久久这里只有精彩视频免费| 中文字幕亚洲中文字幕| 国产97视频在线精品| 黄色三级网站免费下载| 十八禁在线观看地址免费| 欧美国产亚洲中英文字幕| 大陆精品一区二区三区久久| 亚洲一区二区三区av网站| 亚洲欧美激情中文字幕| 久久精品美女免费视频| 中文字幕在线第一页成人| 人妻少妇精品久久久久久| 日本www中文字幕| 日本美女成人在线视频| 亚洲成人黄色一区二区三区| 91亚洲国产成人精品性色| 毛片av在线免费看| 中文字幕最新久久久| 国产一区自拍黄视频免费观看| 美女被肏内射视频网站| 91精品国产观看免费| 最新97国产在线视频| 99精品一区二区三区的区| 激情小视频国产在线| 日本av熟女在线视频| 揄拍成人国产精品免费看视频| 极品粉嫩小泬白浆20p主播| 亚洲专区激情在线观看视频| yy6080国产在线视频| 国产不卡av在线免费| 青青青青青青青青青国产精品视频| 人妻自拍视频中国大陆| 亚洲第一黄色在线观看| 人妻丝袜诱惑我操她视频| 综合国产成人在线观看| 国产精品手机在线看片| 日本后入视频在线观看 | 天天干夜夜操天天舔| 天天通天天透天天插| 欧美美女人体视频一区| 91人妻精品久久久久久久网站| 日韩欧美高清免费在线| 熟女在线视频一区二区三区| 成人av亚洲一区二区| 亚洲自拍偷拍综合色| 国产日韩av一区二区在线| 成人24小时免费视频| 日本成人一区二区不卡免费在线| 国产精品黄大片在线播放| 97小视频人妻一区二区| 亚洲推理片免费看网站| 动漫美女的小穴视频| 国产激情av网站在线观看| 日韩av大胆在线观看| 亚洲国产成人在线一区| 成人久久精品一区二区三区| 99热碰碰热精品a中文| 在线观看欧美黄片一区二区三区| 一区二区三区激情在线| 欧美80老妇人性视频| 亚洲国产精品黑丝美女| 午夜免费体验区在线观看| 在线亚洲天堂色播av电影| 黄色片黄色片wyaa| 天天做天天干天天舔| 大鸡巴操b视频在线| 国产日本精品久久久久久久| 伊人成人综合开心网| 91精品啪在线免费| av手机在线免费观看日韩av| 国产黄网站在线观看播放| 午夜精品久久久久久99热| www日韩a级s片av| 亚洲最大黄了色网站| 2012中文字幕在线高清| 亚洲成人线上免费视频观看| 91精品国产黑色丝袜| 成人av久久精品一区二区| 久草福利电影在线观看| 大白屁股精品视频国产| 美女小视频网站在线| 啪啪啪18禁一区二区三区| 97超碰国语国产97超碰| 欧洲欧美日韩国产在线| 久久99久久99精品影院| 亚洲av无乱一区二区三区性色 | 99热99这里精品6国产| 欧美色婷婷综合在线| 成人亚洲国产综合精品| 国产精品中文av在线播放| 一区二区三区四区视频| 日日日日日日日日夜夜夜夜夜夜| 人人妻人人爱人人草| 国产无遮挡裸体免费直播视频| 晚上一个人看操B片| 亚欧在线视频你懂的| 狍和女人的王色毛片| av手机免费在线观看高潮| 亚洲av黄色在线网站| 午夜精彩视频免费一区| 天天日天天添天天爽| 国产精品sm调教视频| 色伦色伦777国产精品| 好太好爽好想要免费| 午夜成午夜成年片在线观看| 亚洲精品av在线观看| 搡老熟女一区二区在线观看| 欧美国产亚洲中英文字幕| 婷婷六月天中文字幕| 人妻在线精品录音叫床| 青青擦在线视频国产在线| 超碰中文字幕免费观看| 97人人模人人爽人人喊| 日韩三级黄色片网站| 2022中文字幕在线| 亚洲视频在线视频看视频在线| 中文字幕一区二 区二三区四区| 久久丁香花五月天色婷婷| 男女第一次视频在线观看| 综合色区亚洲熟妇shxstz| 久久久久久久久久一区二区三区 | 中文字幕高清在线免费播放| av手机在线观播放网站| 91色网站免费在线观看 | 精品亚洲在线免费观看| 久久久久久久久久一区二区三区| 国产女人露脸高潮对白视频| 丝袜美腿视频诱惑亚洲无| 色爱av一区二区三区| 久久久久久久久久久免费女人| 玖玖一区二区在线观看| 天天日天天日天天射天天干| 2021久久免费视频| av老司机精品在线观看| 国产成人精品久久二区91| 国产免费高清视频视频| 人妻自拍视频中国大陆| 午夜精品一区二区三区更新| 日韩影片一区二区三区不卡免费| 精品人人人妻人人玩日产欧| 欧美成人综合色在线噜噜| 亚洲午夜在线视频福利| 欧美精品黑人性xxxx| 在线观看av亚洲情色| 99精品国产免费久久| 粉嫩欧美美人妻小视频| 亚洲精品无码久久久久不卡| 免费十精品十国产网站| 日本脱亚入欧是指什么| 天堂中文字幕翔田av| 一区二区视频视频视频| 成年人免费看在线视频| 国产九色91在线观看精品| 成年人午夜黄片视频资源| 老有所依在线观看完整版| 一二三区在线观看视频| 少妇人妻真实精品视频| 国产精品久久9999| 久草视频在线免播放| 蜜臀成人av在线播放| 欧美 亚洲 另类综合| 成人蜜桃美臀九一一区二区三区| av成人在线观看一区| 日韩在线视频观看有码在线| 午夜免费体验区在线观看| 白白操白白色在线免费视频 | 中文字幕av一区在线观看| 国产揄拍高清国内精品对白| 小泽玛利亚视频在线观看| 久久久麻豆精亚洲av麻花| 中文字幕免费在线免费| 扒开腿挺进肉嫩小18禁视频| 中出中文字幕在线观看| 一区二区三区视频,福利一区二区| av久久精品北条麻妃av观看| 不卡日韩av在线观看| av在线免费观看亚洲天堂| 国产精品国产精品一区二区| av欧美网站在线观看| 绝顶痉挛大潮喷高潮无码| 男生舔女生逼逼视频| 国产日韩一区二区在线看| 日韩国产乱码中文字幕| 专门看国产熟妇的网站| 免费啪啪啪在线观看视频| 精品久久久久久久久久久a√国产| yellow在线播放av啊啊啊| 亚洲最大黄了色网站| 91国内精品久久久久精品一| 亚洲男人的天堂a在线| 97成人免费在线观看网站| 免费黄页网站4188| 欧美香蕉人妻精品一区二区| 国产午夜激情福利小视频在线| 国产丰满熟女成人视频| 日本性感美女视频网站| 黄网十四区丁香社区激情五月天| 欧美国产亚洲中英文字幕| 视频 一区二区在线观看| 成人午夜电影在线观看 久久| 日本特级片中文字幕| 色爱av一区二区三区| 精品一区二区三区在线观看| 国产精品国产三级国产精东 | 亚洲国产成人无码麻豆艾秋| 天天操夜夜骑日日摸| 99热这里只有国产精品6| 摧残蹂躏av一二三区| 性感美女福利视频网站| 天天干天天日天天谢综合156| 日本女人一级免费片| 天天日天天透天天操| 亚洲青青操骚货在线视频| 在线免费观看靠比视频的网站| 91精品啪在线免费| 久久www免费人成一看片| 狠狠鲁狠狠操天天晚上干干| 色婷婷综合激情五月免费观看| av欧美网站在线观看| huangse网站在线观看| 综合一区二区三区蜜臀| 99精品视频在线观看婷婷| 18禁精品网站久久| 日韩熟女系列一区二区三区| 丰满的子国产在线观看| 被大鸡吧操的好舒服视频免费| 青青青青爽手机在线| 日本www中文字幕| 国产成人午夜精品福利| 五十路老熟女码av| 亚洲午夜电影之麻豆| 一区二区三区国产精选在线播放 | 欧美日韩国产一区二区三区三州 | 亚洲免费在线视频网站| 91久久人澡人人添人人爽乱| 亚洲美女美妇久久字幕组| 一区国内二区日韩三区欧美| 天天做天天爽夜夜做少妇| 黄色片黄色片wyaa| 亚洲免费福利一区二区三区| 岛国一区二区三区视频在线| 最新国产精品网址在线观看| 亚洲最大免费在线观看| 一区二区三区日韩久久| 中文字幕免费福利视频6| AV无码一区二区三区不卡| 亚洲国产40页第21页| 日韩近亲视频在线观看| 天天干天天日天天干天天操| 男女之间激情网午夜在线| 精产国品久久一二三产区区别| 国产视频一区二区午夜| 国产一区二区久久久裸臀| 黄工厂精品视频在线观看| okirakuhuhu在线观看| 18禁美女无遮挡免费| 成人蜜桃美臀九一一区二区三区| 欧美视频一区免费在线| 亚洲国产精品久久久久久6| 成人午夜电影在线观看 久久| 9色在线视频免费观看| 熟女少妇激情五十路| 免费无毒热热热热热热久| 天堂va蜜桃一区入口| 欧美区一区二区三视频| 日韩av有码一区二区三区4 | 五十路息与子猛烈交尾视频| 日本少妇人妻xxxxxhd| 久久永久免费精品人妻专区| 成人av在线资源网站| 日韩中文字幕精品淫| 91社福利《在线观看| 自拍偷拍 国产资源| 一级黄色片夫妻性生活| 欧美麻豆av在线播放| 超碰中文字幕免费观看| 亚洲特黄aaaa片| yellow在线播放av啊啊啊| 国产熟妇乱妇熟色T区| 日曰摸日日碰夜夜爽歪歪| 五十路av熟女松本翔子| 好吊视频—区二区三区| 99re国产在线精品| 香港一级特黄大片在线播放| 涩涩的视频在线观看视频| av日韩在线免费播放| 一二三中文乱码亚洲乱码one| 欧美香蕉人妻精品一区二区| 亚洲第一黄色在线观看| 好吊操视频这里只有精品| h国产小视频福利在线观看| 97人人妻人人澡人人爽人人精品| 国产精品久久久久网| 亚洲欧美福利在线观看| 色爱av一区二区三区| 激情人妻校园春色亚洲欧美| 亚洲一级美女啪啪啪| 黄色视频在线观看高清无码| 91精品高清一区二区三区| 日本免费一级黄色录像| 亚洲第一伊人天堂网| 日韩特级黄片高清在线看| 国产日韩欧美视频在线导航| 国产成人无码精品久久久电影| 一二三中文乱码亚洲乱码one| 亚洲国产欧美国产综合在线| 亚洲欧美久久久久久久久| 狠狠躁夜夜躁人人爽天天天天97| 白白操白白色在线免费视频| 啪啪啪操人视频在线播放| 成人亚洲精品国产精品 | 在线免费观看99视频| 人人人妻人人澡人人| 久久久久久久久久一区二区三区| 在线观看视频 你懂的| 五十路老熟女码av| 99re国产在线精品| aiss午夜免费视频| 免费观看丰满少妇做受| 亚洲免费成人a v| 美女少妇亚洲精选av| 男人插女人视频网站| 日本美女成人在线视频| 午夜场射精嗯嗯啊啊视频| 91 亚洲视频在线观看| caoporn蜜桃视频| 久久久久91精品推荐99| 欧美激情电影免费在线| 一区二区三区四区视频在线播放| 男人操女人逼逼视频网站| av在线shipin| 日本高清在线不卡一区二区| 夜夜嗨av一区二区三区中文字幕| 懂色av之国产精品| aⅴ五十路av熟女中出| 国产精品久久久黄网站| 男人操女人逼逼视频网站| 免费黄高清无码国产| 亚洲精品午夜久久久久| 夜夜骑夜夜操夜夜奸| free性日本少妇| 老司机午夜精品视频资源| 精内国产乱码久久久久久| 超鹏97历史在线观看| 精品一区二区三区在线观看| av俺也去在线播放| 阴茎插到阴道里面的视频| 97超碰人人搞人人| 国产视频精品资源网站| 人妻少妇精品久久久久久| 中文字幕在线第一页成人| 亚洲免费在线视频网站| 99精品国自产在线人| brazzers欧熟精品系列| 天天日天天做天天日天天做| 韩国AV无码不卡在线播放| 国产亚洲精品品视频在线| 亚洲专区激情在线观看视频| 超污视频在线观看污污污| 在线可以看的视频你懂的| 国产性生活中老年人视频网站| 91九色国产熟女一区二区| 日本美女成人在线视频| 色婷婷六月亚洲综合香蕉| 人人在线视频一区二区| 在线国产日韩欧美视频| 青青青激情在线观看视频| 亚洲精品成人网久久久久久小说| 国产一区二区久久久裸臀| 2020av天堂网在线观看| 91chinese在线视频| 精品一区二区三区三区88| 亚洲国产精品久久久久蜜桃| 欧美一区二区中文字幕电影| 熟女在线视频一区二区三区| 国产美女午夜福利久久| 亚洲无线观看国产高清在线| 伊人网中文字幕在线视频| 欧美另类重口味极品在线观看| 九色精品视频在线播放| 中国熟女@视频91| 国产女人叫床高潮大片视频| 亚洲中文精品人人免费| 99热99这里精品6国产| 欧美日韩熟女一区二区三区| 久久久久久9999久久久久| 绝顶痉挛大潮喷高潮无码| 绝顶痉挛大潮喷高潮无码 | 日本一二三中文字幕| 天天干天天操天天摸天天射| 日曰摸日日碰夜夜爽歪歪| 91亚洲手机在线视频播放| 免费一级特黄特色大片在线观看| 日本五十路熟新垣里子| 国产不卡av在线免费| 国产在线一区二区三区麻酥酥| 91av精品视频在线| 日日夜夜精品一二三| 亚洲男人让女人爽的视频| 午夜激情高清在线观看| 粉嫩av蜜乳av蜜臀| 午夜大尺度无码福利视频| 五月天中文字幕内射| 欧美日韩激情啪啪啪| 日本韩国免费福利精品| 天天色天天操天天舔| 五色婷婷综合狠狠爱| 久草福利电影在线观看| 性感美女福利视频网站| 亚洲精品亚洲人成在线导航| 欧美黄片精彩在线免费观看 | 亚洲欧美激情中文字幕| 99热国产精品666| 最新欧美一二三视频| 久久久久久久久久一区二区三区| 一色桃子人妻一区二区三区| a v欧美一区=区三区| 爱爱免费在线观看视频| 亚洲成a人片777777| 无码中文字幕波多野不卡| 久久精品美女免费视频| 日视频免费在线观看| 92福利视频午夜1000看| yy6080国产在线视频| 国产欧美精品一区二区高清 | 精品黑人一区二区三区久久国产| 最新的中文字幕 亚洲| 九色精品视频在线播放| 5528327男人天堂| 成人久久精品一区二区三区| 91chinese在线视频| 国产av一区2区3区| 国产1区,2区,3区| 欧美黑人巨大性xxxxx猛交| 中文字幕 亚洲av| 欧美成人综合视频一区二区| 国产真实乱子伦a视频| 特一级特级黄色网片| 丝袜肉丝一区二区三区四区在线看| 国产片免费观看在线观看| 久久这里只有精品热视频| 日本少妇高清视频xxxxx| 国产精品午夜国产小视频| 成人sm视频在线观看| 精品一区二区三区三区88| 国产欧美日韩在线观看不卡| 韩国黄色一级二级三级| 57pao国产一区二区| 亚洲av自拍偷拍综合| 91精品国产麻豆国产| 福利国产视频在线观看| 999热精品视频在线| 国产精品黄片免费在线观看| 人妻无码中文字幕专区| 青青伊人一精品视频| 欧美视频不卡一区四区| 美味人妻2在线播放| 色吉吉影音天天干天天操| 91精品国产综合久久久蜜| 亚洲午夜电影之麻豆| 久久久久久久久久久久久97| 日韩美女精品视频在线观看网站| 青青草成人福利电影| 日韩欧美一级黄片亚洲| 美女福利视频网址导航| 日本女人一级免费片| 91精品高清一区二区三区| 成人国产小视频在线观看| 91麻豆精品久久久久| 免费看美女脱光衣服的视频| 免费在线黄色观看网站| 欲乱人妻少妇在线视频裸| 亚洲最大免费在线观看| 成人伊人精品色xxxx视频| 11久久久久久久久久久| av成人在线观看一区| 天天操天天干天天日狠狠插 | 成人av在线资源网站| 2022精品久久久久久中文字幕| 无套猛戳丰满少妇人妻| 国产高清在线观看1区2区| 99久久久无码国产精品性出奶水| 欧美乱妇无乱码一区二区| 女同互舔一区二区三区| 97瑟瑟超碰在线香蕉| 亚洲成av人无码不卡影片一| 扒开腿挺进肉嫩小18禁视频| 99re国产在线精品| 中文字幕在线第一页成人| 欧美区一区二区三视频| 伊人成人综合开心网| 日本免费视频午夜福利视频| 欧洲黄页网免费观看| 黄色视频在线观看高清无码 | caoporn蜜桃视频| 青青青青草手机在线视频免费看| 性色av一区二区三区久久久| 动漫美女的小穴视频| 超碰公开大香蕉97| 日韩黄色片在线观看网站| 亚洲熟妇x久久av久久| 在线制服丝袜中文字幕| 久久久久久久久久一区二区三区 | 97超碰免费在线视频| 精品首页在线观看视频| 久久h视频在线观看| yellow在线播放av啊啊啊| www久久久久久久久久久| 红桃av成人在线观看| 午夜精品福利91av| av亚洲中文天堂字幕网| 美女骚逼日出水来了| 欧美日韩激情啪啪啪| 日韩中文字幕福利av| 天天日天天日天天射天天干| 美女操逼免费短视频下载链接| 岳太深了紧紧的中文字幕| 久久这里只有精彩视频免费| 成人高潮aa毛片免费| 一区二区三区激情在线| 一区二区三区另类在线| 又粗又硬又猛又黄免费30| 精品人妻每日一部精品| 午夜免费体验区在线观看| 国产av一区2区3区| 91精品国产黑色丝袜| 好了av中文字幕在线| 亚洲一区二区人妻av| 国产一区二区三免费视频| 国产性色生活片毛片春晓精品| 日本韩国免费福利精品| 懂色av之国产精品| 亚洲av无码成人精品区辽| 亚洲午夜电影之麻豆| 成人亚洲国产综合精品| nagger可以指黑人吗| 好男人视频在线免费观看网站| 国产精品人久久久久久| 中文字幕亚洲久久久| 国产一区二区久久久裸臀| 中文字幕 人妻精品| 色花堂在线av中文字幕九九| av乱码一区二区三区| 天天做天天干天天操天天射| 青青色国产视频在线| 免费一级黄色av网站| 老司机你懂得福利视频| 绯色av蜜臀vs少妇| 欧美在线一二三视频| 懂色av蜜桃a v| 九色精品视频在线播放| 精内国产乱码久久久久久| 美洲精品一二三产区区别| 亚洲1区2区3区精华液| 亚洲日本一区二区三区| 成年午夜免费无码区| 亚洲一区二区三区uij| 久久久精品999精品日本| 亚洲公开视频在线观看| 国产janese在线播放| 亚洲第一伊人天堂网| 日本韩国在线观看一区二区| 99精品国自产在线人| 国产亚洲精品视频合集| 97精品成人一区二区三区| 国产黄网站在线观看播放| 天天色天天操天天透| 欧洲黄页网免费观看| 国产自拍在线观看成人| 激情国产小视频在线| 欧美特色aaa大片| 亚洲精品无码久久久久不卡| 在线不卡成人黄色精品| 中国熟女@视频91| 国产精品亚洲а∨天堂免| 被大鸡吧操的好舒服视频免费| 日韩中文字幕在线播放第二页| 人妻少妇一区二区三区蜜桃| 三级等保密码要求条款| 性感美女高潮视频久久久| 韩国三级aaaaa高清视频| 国产精品亚洲а∨天堂免| www日韩a级s片av| 中文字幕 人妻精品| 55夜色66夜色国产精品站| 国产精品熟女久久久久浪潮| 天天干夜夜操啊啊啊| 少妇人妻真实精品视频| 亚洲中文字幕综合小综合| 日本一区美女福利视频| 岛国av高清在线成人在线| 老师啊太大了啊啊啊尻视频| 婷婷激情四射在线观看视频| 美女 午夜 在线视频| 天天日天天干天天要| 亚洲免费国产在线日韩| 国产揄拍高清国内精品对白| 东京干手机福利视频| 日本一二三中文字幕| 久草电影免费在线观看| av亚洲中文天堂字幕网| av在线资源中文字幕| 精品av国产一区二区三区四区| 啪啪啪啪啪啪啪啪啪啪黄色| 精品高跟鞋丝袜一区二区| 国产麻豆精品人妻av| 欧美熟妇一区二区三区仙踪林| 97人妻人人澡爽人人精品| 成人sm视频在线观看| 2017亚洲男人天堂| 一区二区三区欧美日韩高清播放| 欧美精品伦理三区四区| 9色在线视频免费观看| 天天躁日日躁狠狠躁av麻豆| 天堂av在线最新版在线| 成人福利视频免费在线| 又色又爽又黄的美女裸体| 天天做天天干天天舔| 国产精品视频欧美一区二区| 国产精品久久久黄网站| 国产高清精品极品美女| 久久久噜噜噜久久熟女av| 老司机深夜免费福利视频在线观看| av日韩在线免费播放| 国产又粗又黄又硬又爽| 鸡巴操逼一级黄色气| 亚洲另类图片蜜臀av| 国产精品3p和黑人大战| 国产一区自拍黄视频免费观看| 9国产精品久久久久老师 | 久久久久久久99精品| 亚洲一区二区激情在线| 好吊视频—区二区三区| 特一级特级黄色网片| 老司机免费福利视频网| 99热这里只有精品中文| 91色老99久久九九爱精品| 日韩欧美国产精品91| 国产福利小视频二区| 亚洲2021av天堂| 亚洲精品午夜久久久久| 日本女人一级免费片| 亚洲天堂精品久久久| 免费观看污视频网站| 中文字幕 码 在线视频| 密臀av一区在线观看| 蜜桃专区一区二区在线观看| 日本少妇人妻xxxxxhd| 亚洲av日韩精品久久久| 91精品高清一区二区三区| 偷拍自拍亚洲美腿丝袜| 中文字幕1卡1区2区3区| 天天日天天爽天天爽| 欧美另类一区二区视频| 亚洲欧美综合在线探花| 亚洲欧美成人综合视频| 国产老熟女伦老熟妇ⅹ| 国产av自拍偷拍盛宴| 任你操任你干精品在线视频| 日本熟妇一区二区x x| 国产日本欧美亚洲精品视| 日本丰满熟妇大屁股久久| 亚洲激情,偷拍视频| 国产又粗又猛又爽又黄的视频美国| 午夜蜜桃一区二区三区| 宅男噜噜噜666免费观看| 国产V亚洲V天堂无码欠欠| 自拍偷拍,中文字幕| 女生自摸在线观看一区二区三区| 久久久久久久久久久免费女人| 国产中文字幕四区在线观看| 无套猛戳丰满少妇人妻| 久久久精品精品视频视频| 精品视频一区二区三区四区五区| 高清成人av一区三区| 亚洲成高清a人片在线观看| 久草视频在线免播放| 成年女人免费播放视频| 99热色原网这里只有精品| 国产久久久精品毛片| 黄色黄色黄片78在线| 日日日日日日日日夜夜夜夜夜夜| 不卡一区一区三区在线| 青青擦在线视频国产在线| av在线资源中文字幕| 班长撕开乳罩揉我胸好爽| av无限看熟女人妻另类av| 91老熟女连续高潮对白| 青青在线视频性感少妇和隔壁黑丝| 毛片一级完整版免费| 99re久久这里都是精品视频| 日本五十路熟新垣里子| 狠狠操狠狠操免费视频| 欧亚乱色一区二区三区| 青青在线视频性感少妇和隔壁黑丝| 中文字幕一区二区亚洲一区| 91p0rny九色露脸熟女| 91麻豆精品久久久久| 国产成人自拍视频播放| 国产亚洲精品视频合集| 日本韩国免费福利精品| 在线免费观看靠比视频的网站| 天天日天天天天天天天天天天 | av中文在线天堂精品| 熟女人妻三十路四十路人妻斩| 中文字幕乱码人妻电影| 传媒在线播放国产精品一区| 精品乱子伦一区二区三区免费播| av在线免费中文字幕| 福利视频广场一区二区| 亚洲国产欧美一区二区丝袜黑人| 亚洲av黄色在线网站| 大骚逼91抽插出水视频| 欧美一级视频一区二区| 大鸡吧插入女阴道黄色片| av在线观看网址av| 国产日韩精品免费在线| 亚洲变态另类色图天堂网| 亚洲欧美国产麻豆综合| 狠狠躁夜夜躁人人爽天天天天97| 巨乳人妻日下部加奈被邻居中出| 婷婷激情四射在线观看视频| 五十路在线观看完整版| 欧美熟妇一区二区三区仙踪林| okirakuhuhu在线观看| 免费av岛国天堂网站| 女蜜桃臀紧身瑜伽裤| 自拍偷拍日韩欧美亚洲| 2o22av在线视频| av在线播放国产不卡| 2020国产在线不卡视频| 天天操天天操天天碰| 激情五月婷婷免费视频| 亚洲av在线观看尤物| 人妻少妇av在线观看| 精彩视频99免费在线| 亚洲一区二区三区精品视频在线| 非洲黑人一级特黄片| 经典国语激情内射视频| 2017亚洲男人天堂| 国产精品探花熟女在线观看| 99久久99久国产黄毛片| 黄色在线观看免费观看在线| 亚洲av自拍天堂网| 天美传媒mv视频在线观看| 精品久久婷婷免费视频| 日本人妻欲求不满中文字幕| 精品久久婷婷免费视频| 国产亚洲成人免费在线观看| 日韩av免费观看一区| 夫妻在线观看视频91| 亚洲一区二区三区久久受| 被大鸡吧操的好舒服视频免费| 久久久久只精品国产三级| gav成人免费播放| 污污小视频91在线观看| 亚洲av黄色在线网站| 国产精选一区在线播放| 天天干天天日天天干天天操| 国产一区二区在线欧美| av视网站在线观看| 青春草视频在线免费播放| 亚洲熟女女同志女同| 啊用力插好舒服视频| 国产又粗又猛又爽又黄的视频美国 | 哥哥姐姐综合激情小说| 蜜桃专区一区二区在线观看| 超碰在线中文字幕一区二区| 水蜜桃一区二区三区在线观看视频| 日本a级视频老女人| 少妇人妻久久久久视频黄片| 国产又粗又猛又爽又黄的视频在线| 亚洲嫩模一区二区三区| 国产精品人久久久久久| 少妇人妻久久久久视频黄片| 亚洲一区av中文字幕在线观看| 91香蕉成人app下载| 三级av中文字幕在线观看| 人妻少妇中文有码精品| 天天色天天操天天舔| 蜜桃精品久久久一区二区| 99热久久极品热亚洲| 大香蕉大香蕉在线有码 av| 午夜激情高清在线观看| 亚洲综合乱码一区二区| 日本一道二三区视频久久| 日日操综合成人av| 日本啪啪啪啪啪啪啪| 国产1区,2区,3区| 亚洲国产免费av一区二区三区| 久久久久只精品国产三级| 在线免费91激情四射| 天天摸天天干天天操科普| 日韩精品激情在线观看| 亚洲一区制服丝袜美腿| 日韩欧美国产一区ab| 久久热这里这里只有精品| 欧美日韩在线精品一区二区三| 亚洲无线观看国产高清在线| 岛国毛片视频免费在线观看| 91chinese在线视频| 欧美va不卡视频在线观看| 激情五月婷婷免费视频| 一区二区三区激情在线| 我想看操逼黄色大片| 日韩成人性色生活片| 超碰97人人澡人人| 少妇一区二区三区久久久| 热久久只有这里有精品| 成年美女黄网站18禁久久| 插小穴高清无码中文字幕| 熟女少妇激情五十路| 亚洲图片偷拍自拍区| av在线shipin| 综合一区二区三区蜜臀| 91精品视频在线观看免费| 视频一区 视频二区 视频| 午夜影院在线观看视频羞羞羞| 成人动漫大肉棒插进去视频| 男女啪啪视频免费在线观看| 成年人啪啪视频在线观看| 成人蜜臀午夜久久一区| 日本五十路熟新垣里子| 老司机免费视频网站在线看| mm131美女午夜爽爽爽| 欧美日本在线视频一区| japanese五十路熟女熟妇| 成人精品视频99第一页| 欧美3p在线观看一区二区三区| 亚洲一级美女啪啪啪| 日日日日日日日日夜夜夜夜夜夜| 国产麻豆91在线视频| 开心 色 六月 婷婷| 男生舔女生逼逼的视频| 国产片免费观看在线观看| 在线观看视频一区麻豆| 欧美 亚洲 另类综合| 老司机福利精品视频在线| 欧美aa一级一区三区四区| 91麻豆精品久久久久| 亚洲欧美成人综合在线观看| 一区二区在线观看少妇| 蜜桃色婷婷久久久福利在线| 99精品视频在线观看免费播放 | 含骚鸡巴玩逼逼视频| 在线视频国产欧美日韩| 中字幕人妻熟女人妻a62v网| 日韩中文字幕精品淫| 青青青aaaa免费| 亚洲激情偷拍一区二区| 最新97国产在线视频| 久草视频 久草视频2| free性日本少妇| 久久久91蜜桃精品ad| 一区二区三区四区视频| 亚洲午夜在线视频福利| 成人24小时免费视频| 中文字幕高清免费在线人妻 | 亚洲人一区二区中文字幕| 粉嫩av蜜乳av蜜臀| 三上悠亚和黑人665番号| 久久h视频在线观看| 日本高清成人一区二区三区| 91chinese在线视频| 肏插流水妹子在线乐播下载| 高清一区二区欧美系列| 国产午夜无码福利在线看| 少妇ww搡性bbb91| 密臀av一区在线观看| 久久这里只有精品热视频| 97超碰最新免费在线观看| 亚洲福利天堂久久久久久 | 2020韩国午夜女主播在线| 97精品视频在线观看| 日韩精品中文字幕福利| 免费十精品十国产网站| 亚洲国产精品中文字幕网站| 欲满人妻中文字幕在线| 免费观看污视频网站| 日韩美女精品视频在线观看网站 | 日韩人妻xxxxx| 午夜福利资源综合激情午夜福利资| 精品91高清在线观看| 国产日韩精品一二三区久久久| 中文字幕一区二 区二三区四区 | 大陆胖女人与丈夫操b国语高清| 1024久久国产精品| 精内国产乱码久久久久久| 精品人妻伦一二三区久| 久久这里只有精品热视频 | 日本成人不卡一区二区| 中文乱理伦片在线观看| jiujiure精品视频在线| 欧美偷拍亚洲一区二区| yellow在线播放av啊啊啊| 激情五月婷婷免费视频| 亚洲精品一线二线在线观看| 欧美日韩激情啪啪啪| 51国产成人精品视频| 国产亚洲欧美另类在线观看| 免费69视频在线看| 五十路熟女人妻一区二| 人人妻人人爱人人草| 天天操,天天干,天天射| 狠狠操狠狠操免费视频| 欧美精产国品一二三产品价格 | av大全在线播放免费| 18禁精品网站久久| 把腿张开让我插进去视频| 啪啪啪啪啪啪啪免费视频| 五色婷婷综合狠狠爱| 91久久人澡人人添人人爽乱| 午夜精品一区二区三区城中村| 成年人啪啪视频在线观看| 888欧美视频在线| 99热色原网这里只有精品| 午夜蜜桃一区二区三区| 亚洲成人av一区久久| 97人人妻人人澡人人爽人人精品| 中文字幕高清资源站| 久久综合老鸭窝色综合久久| 国产白袜脚足J棉袜在线观看| 中文字幕一区二区三区蜜月| 夜色撩人久久7777| 免费一级特黄特色大片在线观看| 99久久激情婷婷综合五月天| 国产揄拍高清国内精品对白| 一色桃子人妻一区二区三区| 中文字幕一区二 区二三区四区 | 天天日天天天天天天天天天天 | 精品久久久久久久久久久久人妻| 在线观看国产免费麻豆| 中英文字幕av一区| 亚洲欧美人精品高清| 天天操天天弄天天射| 黄片大全在线观看观看| 国产精品中文av在线播放| 亚洲天堂有码中文字幕视频| 特一级特级黄色网片| 丰满少妇翘臀后进式| 天天做天天干天天舔| 国际av大片在线免费观看| 成人福利视频免费在线| 色婷婷综合激情五月免费观看| 亚洲国产精品中文字幕网站| 美女小视频网站在线| 亚洲成人三级在线播放| 人妻熟女中文字幕aⅴ在线| 日韩中文字幕精品淫| 成人高清在线观看视频| 天天射,天天操,天天说| 青青操免费日综合视频观看| 亚洲最大免费在线观看| 五月色婷婷综合开心网4438| 国产大鸡巴大鸡巴操小骚逼小骚逼| 精品一区二区亚洲欧美| 久久这里只有精品热视频 | 天天操夜夜骑日日摸| 亚洲激情偷拍一区二区| 国产精品系列在线观看一区二区| 80电影天堂网官网| 护士特殊服务久久久久久久| 98精产国品一二三产区区别| 青青青青青操视频在线观看| 特级无码毛片免费视频播放| 日本18禁久久久久久| 国产精品自拍视频大全| 伊人情人综合成人久久网小说| aⅴ精产国品一二三产品| 视频在线亚洲一区二区| 亚洲一区二区人妻av| 日美女屁股黄邑视频| 亚洲熟女久久久36d| 五月天色婷婷在线观看视频免费| 1024久久国产精品| 大鸡巴插入美女黑黑的阴毛| 91国内精品久久久久精品一| 欧美日本aⅴ免费视频| 亚洲少妇人妻无码精品| 热思思国产99re| 久久这里只有精品热视频 | 三级等保密码要求条款| 亚洲一级 片内射视正片| 欧美精产国品一二三区| 91亚洲精品干熟女蜜桃频道| 亚洲少妇高潮免费观看| 人妻在线精品录音叫床| 亚洲精品国偷自产在线观看蜜桃| 在线免费观看av日韩| 午夜在线一区二区免费| 爆乳骚货内射骚货内射在线 | 99久久成人日韩欧美精品| 美女吃鸡巴操逼高潮视频| 91九色porny蝌蚪国产成人| 午夜精品一区二区三区4| 国产精彩福利精品视频| 人妻爱爱 中文字幕| 少妇人妻久久久久视频黄片| 亚洲精品国产综合久久久久久久久| 欧美久久一区二区伊人| 青青青青草手机在线视频免费看| 日韩欧美国产精品91| 最新激情中文字幕视频| 亚洲欧美成人综合视频| 大陆精品一区二区三区久久| 亚洲成人熟妇一区二区三区 | 一区二区三区四区视频在线播放| 91综合久久亚洲综合| 狠狠操操操操操操操操操| 天堂中文字幕翔田av| 日本免费午夜视频网站| 2018在线福利视频| 天天日天天天天天天天天天天| 亚洲一区二区三区精品视频在线| 久久久极品久久蜜桃| 日韩人妻丝袜中文字幕| 毛片av在线免费看| 在线观看免费av网址大全| 日本少妇的秘密免费视频| 爆乳骚货内射骚货内射在线| 国产成人一区二区三区电影网站| 不卡日韩av在线观看| 黄片色呦呦视频免费看| 日本韩国在线观看一区二区| 国产成人精品久久二区91| 欧美日韩精品永久免费网址| 亚洲一区自拍高清免费视频| 姐姐的朋友2在线观看中文字幕| 无套猛戳丰满少妇人妻| 农村胖女人操逼视频| 1区2区3区4区视频在线观看| 91在线免费观看成人| 成人综合亚洲欧美一区| 午夜久久久久久久精品熟女| 19一区二区三区在线播放| 任我爽精品视频在线播放| 天天日夜夜操天天摸| 久精品人妻一区二区三区| 精品久久久久久高潮| 久久麻豆亚洲精品av| 99热这里只有国产精品6| 亚洲天堂成人在线观看视频网站| 污污小视频91在线观看| 人妻丝袜榨强中文字幕| av高潮迭起在线观看| 在线观看911精品国产| 色吉吉影音天天干天天操 | 99久久成人日韩欧美精品| 美女日逼视频免费观看| 成年人黄视频在线观看| 国产aⅴ一线在线观看| 天天日天天日天天擦| 岛国免费大片在线观看| a v欧美一区=区三区| 人妻久久久精品69系列| 亚洲精品国产在线电影| 亚洲欧美国产综合777| 91av精品视频在线| 狠狠鲁狠狠操天天晚上干干| 伊人成人综合开心网| 丝袜亚洲另类欧美变态| 久久精品美女免费视频| 夜夜嗨av蜜臀av| 欧美日韩v中文在线| 精品亚洲国产中文自在线| 一区二区三区国产精选在线播放| 综合页自拍视频在线播放| 天堂av在线播放免费| 黄色资源视频网站日韩| 国产精品国产精品一区二区| 色婷婷综合激情五月免费观看| 久久这里有免费精品| 天天日天天添天天爽| 人妻凌辱欧美丰满熟妇| 青青草成人福利电影| 成人久久精品一区二区三区| 欧美日韩激情啪啪啪| 中英文字幕av一区| 57pao国产一区二区| 又色又爽又黄又刺激av网站| 亚洲青青操骚货在线视频| 绝顶痉挛大潮喷高潮无码 | 国产使劲操在线播放| 红桃av成人在线观看| 久久久久91精品推荐99| 久久久久久97三级| 国产av国片精品一区二区| 一区二区三区四区中文| 午夜精品亚洲精品五月色| 久久热这里这里只有精品| 99国产精品窥熟女精品| sejizz在线视频| 亚洲偷自拍高清视频| 日韩欧美一级精品在线观看| 激情五月婷婷综合色啪| 日韩激情文学在线视频| 偷青青国产精品青青在线观看| 黄网十四区丁香社区激情五月天| 久草视频在线免播放| 影音先锋女人av噜噜色| 国产精品欧美日韩区二区| 亚洲精品欧美日韩在线播放| 中文字幕—97超碰网| 精品91自产拍在线观看一区| 真实国产乱子伦一区二区| 999久久久久999| 首之国产AV医生和护士小芳| 婷婷五月亚洲综合在线| 91人妻精品久久久久久久网站 | 国产福利小视频二区| 亚洲伊人色一综合网| 国产露脸对白在线观看| 三级av中文字幕在线观看| 直接观看免费黄网站| 日韩欧美高清免费在线| 黄色中文字幕在线播放| 成熟熟女国产精品一区| 男女啪啪啪啪啪的网站| 人人爽亚洲av人人爽av| 男生舔女生逼逼视频| 五十路熟女av天堂| 青青青青青青青在线播放视频| 91精品国产91青青碰| 国产精彩福利精品视频| 亚洲欧美另类手机在线| 男生舔女生逼逼视频| 亚洲国产成人无码麻豆艾秋| 中文字幕人妻av在线观看| 日本高清在线不卡一区二区| 久久免看30视频口爆视频| 中文字幕人妻熟女在线电影| av网址国产在线观看| 激情国产小视频在线| 9l人妻人人爽人人爽| 岛国黄色大片在线观看| 男人天堂色男人av| 宅男噜噜噜666免费观看| 97香蕉碰碰人妻国产樱花| 2022国产综合在线干| 亚洲免费在线视频网站| 老司机免费视频网站在线看| 黑人大几巴狂插日本少妇| 人妻丝袜诱惑我操她视频| 天天躁夜夜躁日日躁a麻豆| 五十路人妻熟女av一区二区| 老司机午夜精品视频资源| 女人精品内射国产99| 久久午夜夜伦痒痒想咳嗽P| 黄色三级网站免费下载| 青青青青草手机在线视频免费看| 人妻少妇一区二区三区蜜桃| 最新91精品视频在线| 中国视频一区二区三区| 青青在线视频性感少妇和隔壁黑丝 | 国产三级影院在线观看| 午夜精品久久久久久99热| 亚洲综合一区二区精品久久| 免费在线看的黄片视频| 99久久成人日韩欧美精品| 日韩三级黄色片网站| 日本a级视频老女人| 男人的天堂一区二区在线观看| 在线国产日韩欧美视频| 337p日本大胆欧美人| 国产在线观看免费人成短视频| 91免费放福利在线观看| 91老熟女连续高潮对白| 97人妻无码AV碰碰视频| av手机在线免费观看日韩av| 伊人网中文字幕在线视频| 激情小视频国产在线| 中文字幕第一页国产在线| 久久精品久久精品亚洲人| 久久久久只精品国产三级| 99精品免费观看视频| av男人天堂狠狠干| 久精品人妻一区二区三区| 国产自拍黄片在线观看| free性日本少妇| 精品美女久久久久久| 天天操天天爽天天干| 日本高清成人一区二区三区| 国产之丝袜脚在线一区二区三区| 亚洲自拍偷拍综合色| 91在线免费观看成人| 人妻丝袜av在线播放网址| 中文字幕 亚洲av| 亚洲成人av一区久久| 亚洲福利精品福利精品福利| 五色婷婷综合狠狠爱| 唐人色亚洲av嫩草| 亚洲超碰97人人做人人爱| 天天草天天色天天干| 蜜臀av久久久久久久| 一个色综合男人天堂| 日本一区二区三区免费小视频| 婷婷久久久综合中文字幕| 青青青视频自偷自拍38碰| 蝴蝶伊人久久中文娱乐网| 亚洲专区激情在线观看视频| 大鸡巴操娇小玲珑的女孩逼| 日韩视频一区二区免费观看| 亚洲av色香蕉一区二区三区| 哥哥姐姐综合激情小说| 日本少妇精品免费视频| 激情国产小视频在线| 一二三中文乱码亚洲乱码one| 亚洲成人av在线一区二区| 大鸡吧插逼逼视频免费看| 久久久91蜜桃精品ad| 欧美一级色视频美日韩| 亚洲福利天堂久久久久久| 93人妻人人揉人人澡人人| 超级碰碰在线视频免费观看| 国产中文精品在线观看| 精品久久久久久久久久久99| 动漫av网站18禁| 97超碰最新免费在线观看| 操的小逼流水的文章| 国产一区二区火爆视频| 天天摸天天日天天操| 老熟妇xxxhd老熟女| 日本av高清免费网站| 人人妻人人澡人人爽人人dvl| 精品国产乱码一区二区三区乱| 天天做天天爽夜夜做少妇| 午夜精品一区二区三区4| 欧美日韩v中文在线| 日本特级片中文字幕| 日本熟妇色熟妇在线观看| 在线观看亚洲人成免费网址| 66久久久久久久久久久| 黄色视频在线观看高清无码 | 亚洲精品久久视频婷婷| 1区2区3区4区视频在线观看| 天天做天天干天天操天天射| 99热这里只有国产精品6| 亚洲国产香蕉视频在线播放 | 欧美精品免费aaaaaa| 一级黄色片夫妻性生活| 亚洲高清视频在线不卡| 亚洲图库另类图片区| 青青伊人一精品视频| 免费在线福利小视频| 欧美男同性恋69视频| 久久综合老鸭窝色综合久久| 日本精品一区二区三区在线视频。 | 久久久久五月天丁香社区| 日本av高清免费网站| 热思思国产99re| 女蜜桃臀紧身瑜伽裤| 特级无码毛片免费视频播放| 十八禁在线观看地址免费| 日视频免费在线观看| 国产又色又刺激在线视频| 日韩精品电影亚洲一区| 天天色天天操天天舔| 日韩欧美高清免费在线| 孕妇奶水仑乱A级毛片免费看| 欧美一区二区三区乱码在线播放 | 成人18禁网站在线播放| 亚洲视频在线视频看视频在线| 男生舔女生逼逼视频| 欧美日韩一区二区电影在线观看| 亚洲高清一区二区三区视频在线| 欧美精品一区二区三区xxxx| 99热久久这里只有精品| 色哟哟国产精品入口| 三级等保密码要求条款| 天天色天天操天天舔| 白嫩白嫩美女极品国产在线观看| 97瑟瑟超碰在线香蕉| 性感美女高潮视频久久久| 亚洲天天干 夜夜操| 精品黑人巨大在线一区| 人妻激情图片视频小说| 成人动漫大肉棒插进去视频| 深田咏美亚洲一区二区| 亚洲欧美清纯唯美另类| 天天想要天天操天天干| 国产亚洲精品视频合集| 久草视频首页在线观看| 国产视频一区在线观看| 无忧传媒在线观看视频| 中文亚洲欧美日韩无线码| 春色激情网欧美成人| 大屁股肉感人妻中文字幕在线| 久久精品在线观看一区二区| 亚洲一区二区人妻av| 福利午夜视频在线合集| 青娱乐在线免费视频盛宴| 91传媒一区二区三区| 国产第一美女一区二区三区四区 | 青青青国产片免费观看视频 | 日本一道二三区视频久久 | 91传媒一区二区三区| 人妻丝袜诱惑我操她视频| 极品粉嫩小泬白浆20p主播| 东京热男人的av天堂| 伊人综合免费在线视频| 一区二区三区综合视频| 色综合色综合色综合色| av网址在线播放大全| 亚洲午夜电影之麻豆 | 福利视频一区二区三区筱慧| 亚洲 中文字幕在线 日韩| 姐姐的朋友2在线观看中文字幕 | 少妇高潮无套内谢麻豆| 黑人3p华裔熟女普通话| 亚洲熟女女同志女同| 激情伦理欧美日韩中文字幕| 国产亚州色婷婷久久99精品| 五月天中文字幕内射| 97少妇精品在线观看| 99re久久这里都是精品视频| 天天操天天操天天碰| 91‖亚洲‖国产熟女| 亚洲熟女久久久36d| 少妇人妻真实精品视频| 中文字幕一区二 区二三区四区| 国产成人自拍视频在线免费观看| 激情啪啪啪啪一区二区三区| 国产日本精品久久久久久久| 女生自摸在线观看一区二区三区| 又粗又长 明星操逼小视频| 57pao国产一区二区| 日本www中文字幕| 久久农村老妇乱69系列| 99精品国产免费久久| 日韩美av高清在线| 综合一区二区三区蜜臀| 国产伦精品一区二区三区竹菊| aⅴ精产国品一二三产品| 久久精品亚洲成在人线a| 久草视频 久草视频2| 社区自拍揄拍尻屁你懂的 | 中文字日产幕乱六区蜜桃| 国产精品伦理片一区二区| 无码国产精品一区二区高潮久久4 日韩欧美一级精品在线观看 | 天天干狠狠干天天操| 日日操综合成人av| 午夜免费观看精品视频| 亚洲一区二区三区uij| 中文字幕,亚洲人妻| 91大屁股国产一区二区| 中文字幕在线欧美精品| 亚洲成人熟妇一区二区三区| 国产性感美女福利视频| 中文字幕综合一区二区| 亚洲第一伊人天堂网| 中文字幕一区二区自拍| 在线观看的黄色免费网站| 精品老妇女久久9g国产| 亚洲 欧美 精品 激情 偷拍 | 五月天久久激情视频| 国产中文精品在线观看| 日本一本午夜在线播放| 免费国产性生活视频| 精品国产亚洲av一淫| 欧美专区第八页一区在线播放| 亚洲国产免费av一区二区三区| 大白屁股精品视频国产| 久久精品视频一区二区三区四区| 天天日天天干天天插舔舔| 亚洲午夜高清在线观看| 日韩av中文在线免费观看| 动漫美女的小穴视频| 欧美亚洲少妇福利视频| 欧美成人猛片aaaaaaa| 黄片三级三级三级在线观看| 影音先锋女人av噜噜色| av在线观看网址av| 中文字幕 人妻精品| www日韩毛片av| 日韩亚洲高清在线观看| 国产精品久久综合久久| 亚洲精品 欧美日韩| 97年大学生大白天操逼| 美女少妇亚洲精选av| 国产又粗又黄又硬又爽| 十八禁在线观看地址免费| 青春草视频在线免费播放| av高潮迭起在线观看| 99热久久这里只有精品| 亚洲女人的天堂av| 国产极品美女久久久久久| 日韩人妻xxxxx| 天天插天天色天天日| 日韩成人综艺在线播放| 91色秘乱一区二区三区| 韩国亚洲欧美超一级在线播放视频| 中文字幕最新久久久| 日韩美女综合中文字幕pp| 大胸性感美女羞爽操逼毛片| 极品丝袜一区二区三区| 亚洲精品麻豆免费在线观看| 女生被男生插的视频网站| 亚洲欧美国产综合777| 欧美亚洲中文字幕一区二区三区| 东京干手机福利视频| 国产精品久久久久网| 国产亚洲精品品视频在线| 巨乳人妻日下部加奈被邻居中出| 黄色片黄色片wyaa| 欧美在线一二三视频| 国产精品sm调教视频| av天堂中文字幕最新| 国产精品久久久久久美女校花| 青青青青青手机视频| 性感美女诱惑福利视频| 大香蕉日本伊人中文在线| 人妻凌辱欧美丰满熟妇| 五十路丰满人妻熟妇| 九色精品视频在线播放| 国产福利小视频二区| 天天干天天操天天爽天天摸| 只有精品亚洲视频在线观看| 精品国产在线手机在线| 日本高清撒尿pissing| 乱亲女秽乱长久久久| 40道精品招牌菜特色| 18禁精品网站久久| 精品一区二区三区午夜| 色狠狠av线不卡香蕉一区二区| 熟女人妻在线观看视频| 91精品国产黑色丝袜| 亚洲欧美一区二区三区电影| 久久久制服丝袜中文字幕| 超pen在线观看视频公开97| 绝顶痉挛大潮喷高潮无码| 一区二区三区 自拍偷拍| 99久久激情婷婷综合五月天| 国产精品久久综合久久| 欧美视频不卡一区四区| 91国偷自产一区二区三区精品| 在线视频这里只有精品自拍| jiujiure精品视频在线| 91人妻人人做人人爽在线| 亚洲在线一区二区欧美| 美女福利视频网址导航| 成人av久久精品一区二区| 最新国产精品拍在线观看| 在线视频这里只有精品自拍| 免费费一级特黄真人片| 熟女国产一区亚洲中文字幕| 91极品大一女神正在播放| 大香蕉伊人中文字幕| 亚洲另类图片蜜臀av| 视频一区二区在线免费播放| 中文字幕日韩精品就在这里| 五十路av熟女松本翔子| 一区二区三区 自拍偷拍| 91色老99久久九九爱精品| 欧美va亚洲va天堂va| 国产一级麻豆精品免费| 男生舔女生逼逼的视频| 中文字幕一区的人妻欧美日韩| 免费观看成年人视频在线观看| 爱爱免费在线观看视频| 日韩三级电影华丽的外出| 88成人免费av网站| 亚洲天堂成人在线观看视频网站| 天天操天天干天天艹| 人妻激情图片视频小说| 黄色黄色黄片78在线| 91国内视频在线观看| 亚洲av自拍偷拍综合| 亚洲高清视频在线不卡| 欧美精产国品一二三区| 日本av在线一区二区三区| 精品久久婷婷免费视频| 伊人成人在线综合网| av在线播放国产不卡| 久久久久久cao我的性感人妻| 亚洲一级av大片免费观看| 91chinese在线视频| 国产精品精品精品999| 天堂av在线官网中文| 日韩人妻丝袜中文字幕| 少妇被强干到高潮视频在线观看 | 青青草人人妻人人妻| 大香蕉大香蕉大香蕉大香蕉大香蕉 | 中文字幕在线永久免费播放| 青青青爽视频在线播放| 欧美专区第八页一区在线播放| 97a片免费在线观看| 免费看高清av的网站| 免费黄页网站4188| 五月天色婷婷在线观看视频免费| 免费在线福利小视频| 亚洲老熟妇日本老妇| 日韩伦理短片在线观看| 亚洲精品乱码久久久久久密桃明 | av俺也去在线播放| 在线观看免费视频色97| 亚洲欧美综合在线探花| 国产自拍黄片在线观看| 51国产偷自视频在线播放| av网址在线播放大全| 欧美第一页在线免费观看视频| 黑人大几巴狂插日本少妇| 一区二区熟女人妻视频| 亚洲丝袜老师诱惑在线观看| 深夜男人福利在线观看| 99精品一区二区三区的区| 国产福利小视频免费观看| 欧美一区二区三区在线资源 | 日韩亚洲高清在线观看| av天堂加勒比在线| 亚洲老熟妇日本老妇| 亚洲熟妇x久久av久久| 精品91高清在线观看| 亚洲最大黄了色网站| 中字幕人妻熟女人妻a62v网| 免费在线观看视频啪啪| 久久久久久九九99精品| 亚欧在线视频你懂的| 北条麻妃av在线免费观看| 伊人情人综合成人久久网小说 | 免费观看理论片完整版| 北条麻妃av在线免费观看| 欧美香蕉人妻精品一区二区| 国产揄拍高清国内精品对白| 水蜜桃国产一区二区三区| 亚洲男人的天堂a在线| 国产视频网站一区二区三区 | 老司机免费视频网站在线看| 女生自摸在线观看一区二区三区| 国产女人被做到高潮免费视频| 亚洲成人国产av在线| 欧美精品久久久久久影院| 亚洲欧美激情人妻偷拍| 亚洲超碰97人人做人人爱| 91传媒一区二区三区| 又粗又硬又猛又黄免费30| yy6080国产在线视频| 免费十精品十国产网站| 伊人开心婷婷国产av| 好吊视频—区二区三区| 日本韩国免费福利精品| 大肉大捧一进一出好爽在线视频 | 男女啪啪啪啪啪的网站| 国产日韩精品免费在线| 特一级特级黄色网片| 北条麻妃av在线免费观看| 精品一区二区三区在线观看| 懂色av之国产精品| 美女操逼免费短视频下载链接| 黑人性生活视频免费看| 青青草在观免费国产精品| 绝色少妇高潮3在线观看| 亚洲免费在线视频网站| 亚洲精品无码久久久久不卡| 51国产成人精品视频| 人妻在线精品录音叫床| 久久这里只有精品热视频| 97年大学生大白天操逼| 成人在线欧美日韩国产| 精品国产乱码一区二区三区乱| 日日操夜夜撸天天干| 熟女人妻在线观看视频| v888av在线观看视频| 亚洲国产香蕉视频在线播放| 亚洲 欧美 精品 激情 偷拍 | 国产成人精品一区在线观看| 香蕉91一区二区三区| 91国产在线免费播放| 亚洲va欧美va人人爽3p| 欧美另类一区二区视频| av大全在线播放免费| 欧美aa一级一区三区四区| 国产亚洲精品视频合集| 免费观看丰满少妇做受| 少妇高潮一区二区三区| 国产麻豆剧传媒精品国产av蜜桃| 91p0rny九色露脸熟女| 国产精品久久综合久久| 国产成人精品午夜福利训2021| 成年人的在线免费视频| 青青热久免费精品视频在线观看| 18禁精品网站久久| 国产av欧美精品高潮网站| 国产亚洲四十路五十路| 天天射,天天操,天天说| 国产91嫩草久久成人在线视频| 日视频免费在线观看| 黄色资源视频网站日韩| 啊啊啊视频试看人妻| 欧美成人黄片一区二区三区| 欧美老鸡巴日小嫩逼| 一区二区三区国产精选在线播放 | 人妻久久久精品69系列| 日日日日日日日日夜夜夜夜夜夜| 亚洲欧洲一区二区在线观看| 99人妻视频免费在线| 国产+亚洲+欧美+另类| 亚洲欧美另类手机在线| 久草视频首页在线观看| 亚洲国产精品中文字幕网站| 人妻少妇亚洲一区二区| 神马午夜在线观看视频| 欧美亚洲一二三区蜜臀| 少妇人妻100系列| 国产精品女邻居小骚货| 日日操综合成人av| 欧美aa一级一区三区四区| 亚洲成人黄色一区二区三区| 大肉大捧一进一出好爽在线视频 | 亚洲欧美激情国产综合久久久| 日本一区二区三区免费小视频| 久久久久国产成人精品亚洲午夜| 天美传媒mv视频在线观看| 91精品激情五月婷婷在线| 又大又湿又爽又紧A视频| 久草福利电影在线观看| 亚洲熟女综合色一区二区三区四区 | 亚洲精品欧美日韩在线播放| 天天想要天天操天天干| 动漫美女的小穴视频| 成人国产影院在线观看| 中文字幕中文字幕人妻| 亚洲蜜臀av一区二区三区九色| 又色又爽又黄又刺激av网站| 久精品人妻一区二区三区 | 夏目彩春在线中文字幕| 午夜影院在线观看视频羞羞羞| 深田咏美亚洲一区二区 | 国产之丝袜脚在线一区二区三区| aiss午夜免费视频| 亚洲丝袜老师诱惑在线观看| 天堂v男人视频在线观看| 日韩无码国产精品强奸乱伦| 97超碰国语国产97超碰| 欧美日韩熟女一区二区三区| 大屁股熟女一区二区三区| 久精品人妻一区二区三区| 精品视频中文字幕在线播放| 青青青青青青青青青青草青青| 91九色porny蝌蚪国产成人| 欧美日韩情色在线观看| 欧美亚洲国产成人免费在线| 全国亚洲男人的天堂| 国产亚洲欧美另类在线观看| 天天躁夜夜躁日日躁a麻豆| 极品性荡少妇一区二区色欲| 国产欧美精品不卡在线| 日韩成人免费电影二区| 一二三区在线观看视频| 国产+亚洲+欧美+另类| avjpm亚洲伊人久久| 噜噜色噜噜噜久色超碰| 亚洲欧美激情国产综合久久久| 欧美一级视频一区二区| 久久精品亚洲国产av香蕉| 国产又粗又硬又猛的毛片视频| 久久久极品久久蜜桃| 一级黄色片夫妻性生活| 青青青青青青草国产| 亚洲av人人澡人人爽人人爱| 亚洲一区久久免费视频| 亚洲欧美清纯唯美另类| 男人插女人视频网站| 亚洲嫩模一区二区三区| 少妇露脸深喉口爆吞精| 黄片三级三级三级在线观看| 丰满少妇人妻xxxxx| 免费国产性生活视频| 91大神福利视频网| 亚洲国产免费av一区二区三区| av高潮迭起在线观看| 亚洲另类综合一区小说| 人妻少妇一区二区三区蜜桃| ka0ri在线视频| 黄色无码鸡吧操逼视频| 午夜国产福利在线观看| 亚洲免费成人a v| 婷婷色国产黑丝少妇勾搭AV| 人妻少妇av在线观看| 天天日天天添天天爽| 久久久久久久久久久久久97| 大香蕉日本伊人中文在线| 大鸡八强奸视频在线观看| 天天操天天干天天艹| 亚国产成人精品久久久| 日本黄色特一级视频| 中文字幕,亚洲人妻| 婷婷午夜国产精品久久久| 欧亚乱色一区二区三区| 亚洲男人让女人爽的视频| 免费av岛国天堂网站| 免费成人av中文字幕| sejizz在线视频| 91快播视频在线观看| 午夜婷婷在线观看视频| 成人精品在线观看视频| 在线国产中文字幕视频| 久久久制服丝袜中文字幕| 国产亚洲精品品视频在线| 女警官打开双腿沦为性奴| 91大屁股国产一区二区| 色秀欧美视频第一页| 中英文字幕av一区| 午夜婷婷在线观看视频| 揄拍成人国产精品免费看视频| 午夜频道成人在线91| 亚洲综合色在线免费观看| 在线制服丝袜中文字幕| asmr福利视频在线观看| 狠狠的往里顶撞h百合| 最近中文字幕国产在线| 国产欧美精品不卡在线| av一本二本在线观看| 欧美精品黑人性xxxx| 大陆精品一区二区三区久久| 欧美成人精品在线观看| 自拍偷拍日韩欧美亚洲| 国产精品久久久久久久精品视频| 国产一区二区欧美三区| 青娱乐极品视频青青草| 最新97国产在线视频| 在线观看的a站 最新| 最新激情中文字幕视频| 中文字幕亚洲中文字幕| 黄片大全在线观看观看| 日韩精品啪啪视频一道免费| 天天射夜夜操狠狠干| 天堂av在线最新版在线| 欧美日韩情色在线观看| 精品国产成人亚洲午夜| 日本美女性生活一级片| 天天操,天天干,天天射| 五色婷婷综合狠狠爱| 欧美日韩精品永久免费网址 | 亚洲天堂有码中文字幕视频| avjpm亚洲伊人久久| 天天干天天爱天天色| 精品国产成人亚洲午夜| 视频二区在线视频观看| 一区二区三区久久久91| 久久精品36亚洲精品束缚| 精品黑人一区二区三区久久国产| 日本熟女50视频免费| 久久精品国产亚洲精品166m| 久草视频在线一区二区三区资源站| 日本黄色三级高清视频| japanese五十路熟女熟妇| 四虎永久在线精品免费区二区| 91九色国产porny蝌蚪| 亚洲综合乱码一区二区| 日韩一区二区电国产精品| 亚洲中文字字幕乱码| 日本午夜爽爽爽爽爽视频在线观看| 亚洲 欧美 精品 激情 偷拍| 日本三极片视频网站观看| 国产黄网站在线观看播放| 亚洲自拍偷拍综合色| 国产一区二区视频观看| 久久精品美女免费视频| 色呦呦视频在线观看视频| 色综合久久无码中文字幕波多| 久久人人做人人妻人人玩精品vr| 国产亚洲视频在线观看| 日本后入视频在线观看| 亚洲中文字幕校园春色| 亚洲综合另类欧美久久| 激情色图一区二区三区| 欧美成人一二三在线网| 青青草在观免费国产精品| 在线免费91激情四射| 欧美aa一级一区三区四区| 青青青青青手机视频| 亚洲成人午夜电影在线观看 | 天天夜天天日天天日| 国产一级精品综合av| 日韩欧美一级精品在线观看| 护士小嫩嫩又紧又爽20p| 久久久久久久久久久久久97| 一区二区熟女人妻视频| 国产黑丝高跟鞋视频在线播放| 2020国产在线不卡视频| 男人和女人激情视频| 亚洲一区制服丝袜美腿| 在线免费观看亚洲精品电影 | 熟女妇女老妇一二三区| av老司机精品在线观看| 国产亚洲欧美45p| 国产精品久久久久久久女人18| 18禁精品网站久久| 日本女大学生的黄色小视频| 成人亚洲精品国产精品| 在线观看av2025| 中字幕人妻熟女人妻a62v网| 精品黑人一区二区三区久久国产 | 抽查舔水白紧大视频| 2021国产一区二区| 亚洲另类综合一区小说| 成人免费做爰高潮视频| 男大肉棒猛烈插女免费视频| 四川乱子伦视频国产vip| 春色激情网欧美成人| 99热色原网这里只有精品| 2022国产精品视频| 99热久久这里只有精品8| 亚洲av自拍偷拍综合| 日本av高清免费网站| 91久久国产成人免费网站| 久久这里只有精品热视频| 夜夜嗨av蜜臀av| 1区2区3区不卡视频| 3D动漫精品啪啪一区二区下载 | 美女福利视频网址导航| 91久久综合男人天堂| 66久久久久久久久久久| 天天干夜夜操啊啊啊| 国产高清在线在线视频| 天堂中文字幕翔田av| 老鸭窝日韩精品视频观看| 天天日天天干天天干天天日| 北条麻妃肉色丝袜视频| 97瑟瑟超碰在线香蕉| 同居了嫂子在线播高清中文| 天天日天天干天天舔天天射| 亚洲中文字幕乱码区| 超碰97人人做人人爱| brazzers欧熟精品系列| 国产女人被做到高潮免费视频 | 99热久久这里只有精品8| 亚洲最大黄 嗯色 操 啊| 91免费黄片可看视频| 女同性ⅹxx女同h偷拍| 韩国一级特黄大片做受| 亚洲人人妻一区二区三区| 少妇人妻二三区视频| 亚洲伊人久久精品影院一美女洗澡| 91成人在线观看免费视频| 91高清成人在线视频| 男生舔女生逼逼视频| 天天摸天天亲天天舔天天操天天爽| 日韩伦理短片在线观看| 国产精品黄片免费在线观看| 四川乱子伦视频国产vip| 亚洲av极品精品在线观看| 人妻无码中文字幕专区| 国产大学生援交正在播放| 欧美视频不卡一区四区| 久久久久国产成人精品亚洲午夜| 国产精品系列在线观看一区二区| av乱码一区二区三区| 国产综合高清在线观看| 久久久久久九九99精品| 亚洲人妻视频在线网| 成人福利视频免费在线| 国产在线一区二区三区麻酥酥| 精品人妻每日一部精品| 日本一道二三区视频久久| 粉嫩欧美美人妻小视频| 天天日天天透天天操| 国产精品探花熟女在线观看|