Java基礎(九)

框架是個好東西,可早晚有一天會過時,這世界上就沒有亙古不變的東西,來學下Java基礎吧

集合類專輯

List,Set,Map的區別

  • List存儲一組不唯一(可以有多個元素引用相同的對象)的,有序的對象
  • Set不允許有重複的集合,不會有多個元素引用相同對象
  • Map使用Key-Value存儲,兩個Key可以引用相同的值,但不能重複

關於Map的Key值說明:

    public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>();
        String s1 = "LuckyCurve";
        String s2 = s1;
        map.put(s1, 1);
        map.put(s2, 1);
    }

不會出錯,如果放兩個s1則會出錯

ArrayList和LinkedList 的區別

  • 是否爲線程安全

兩者都是非線程安全的

  • 底層數據結構

ArrayList底層使用Object數組來存取元素,LinkedList底層使用的是雙向鏈表

  • 插入和刪除元素是否受元素位置的影響

ArrayList底層採用的是數組存儲結構,受插入位置的影響

LinkedList底層採用雙向鏈表結構,插入和刪除元素不受元素位置的影響,時間複雜度近似爲O(1),如果是指定位置插入元素的話,那麼時間複雜度近似爲O(n)【需要先移動到指定位置】

  • 是否支持快速隨機訪問

ArrayList支持,LinkedList不支持

  • 內存空間佔用

ArrayList的空間浪費主要體現在列表的結尾會預留一定的容量空間,而LinkedList主要的內存浪費體現在後繼和前趨元素的地址記錄上

補充:RandomAccess 接口

ArrayList實現了該接口,而LinkedList沒有實現該接口

源碼:

public interface RandomAccess {
}

該接口只能起到標記作用,標記這個接口的實現類是否具有隨機訪問功能

循環方式推薦:

實現了RandomAccess 的list【可以推出他的底層實現原理爲數組】,推薦使用普通for循環,其次是foreach

沒實現RandomAccess 的list【底層爲鏈表】,優先使用iterator遍歷【foreach也是可以的,因爲foreach的底層實現就是iterator】,數據容量大的list,千萬不要使用普通for循環,每次都要訪問到指定元素,時間複雜度爲O(n^2)

Vector

Vector類的所有方法都是同步的,可以保證線程安全,但是如果只有一個線程訪問Vector,會在同步操作上花費大量時間

一般用ArrayList取代Vector,雖然ArrayList不是同步的,所以在不需要保證線程安全的前提下建議使用ArrayList

ArrayList的擴容機制

基於Java11

ArrayList的底層是數組,是定長的,爲什麼我們可以一直向ArrayList裏面添加元素呢,由於他的擴容機制

debug以下代碼,觀察list的size

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);		list size = 0
    list.add(1);		list size = 1
    list.add(1);		list size = 2
    System.out.println(list);		list size = 3
}

後面的註釋標識的是當前語句運行前的狀態

可以觀察出,在創建ArrayList時候,並沒有給其底層數組分配內存空間,直到add的時候纔對數組的長度進行擴充

Size只是表示當前存儲對象,並不表示底層數組分配的內存空間,真正表示底層數組的是elemenData.length

transient Object[] elementData;

調試發現,創建ArrayList的時候elementData的長度爲0,增加一個元素後elementData的長度爲10,

原理就在這個函數當中,這個函數的觸發條件就是size == elementData.length,傳入的參數minCapacity爲size+1,函數的返回值即爲elementData.length的值

private int newCapacity(int minCapacity) {
    int oldCapacity = this.elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
A:          return Math.max(10, minCapacity);
        } else if (minCapacity < 0) {
            throw new OutOfMemoryError();
        } else {
            return minCapacity;
        }
    } else {
B:      return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);
    }
}

當第一次add的時候,程序走到A這兒,返回10

以後任意一次發生size == elementData.length,都走到B處,返回newCapacity,即oldCapacity + (oldCapacity >> 1);右移運算

這即爲ArrayList的底層擴容機制

ensureCapacity方法

public void ensureCapacity(int minCapacity) {
    if (minCapacity > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || minCapacity > 10)) {
        ++this.modCount;
        this.grow(minCapacity);
    }

}

在ArrayList沒有被吊用過,很明顯是提供給用戶調用的

最好在 add 大量元素之前用 ensureCapacity 方法,以減少grow方法調用的次數

可以做個試驗

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    final int N = 10000000;
    long millis1 = System.currentTimeMillis();
    for (int i = 0; i < N; i++) {
        list.add(i);
    }
    long millis2 = System.currentTimeMillis();
    System.out.println("沒調用ensureCapacity用時:" + (millis2 - millis1));
}

<<<沒調用ensureCapacity用時:463

    
    public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    final int N = 10000000;
    long millis1 = System.currentTimeMillis();
    list.ensureCapacity(N);
    for (int i = 0; i < N; i++) {
        list.add(i);
    }
    long millis2 = System.currentTimeMillis();
    System.out.println("調用ensureCapacity用時:" + (millis2 - millis1));
}

<<<沒調用ensureCapacity用時:295

如果目標樣品更大,差距將會更多

所以最好在插入大量元素到ArrayList裏面之前先調用上述方法,減少重新分配空間次數

HashMap和HashTable【現在基本不用】

HashMap和HashTable都是Map的底層實現類,區別:

  • 線程安全:HashMap是非線程安全的,HashTable是線程安全的

  • 效率:HashMap因爲是非線程安全的,效率高【HashTable基本被淘汰,不要在程序中使用它】

  • 對null的處理:HashMap支持null作爲Key,但僅能有一個【Key不重複】,Value可以有一個多個null,而HashTable的KV只要有null直接拋出NullPointException異常

  • HashMap默認初始化大小爲16,以後擴容成原來的兩倍,HashTable默認初始化大小爲11,後來擴容成2n+1

如果給定了初始值,HashTable直接創建給定的大小,而HashMap將其擴容成2的n次方再來創建表

  • 底層結構:HashMap當鏈表長度大於8的時候,將鏈表轉換成紅黑樹,減少搜索時間,HashTable沒有這樣的機制

HashSet與HashMap

HashSet的底層儲存原理運用的就是HashMap

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();

除了 clone()writeObject()readObject()是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法

比較:

HashMap HashSet
實現Map接口 實現Set接口
存儲鍵值對 僅存儲對象
通過put方法向map中添加元素 通過add方法向set中添加元素
通過key計算HashCode 通過對象成員計算HashCode

HashSet如何檢查重複:

​ 當調用add方法將對象傳入HashSet中的時候,會計算對象的HashCode,如果在Set集合中沒有發現與其相同的HashCode,則認爲對象沒有重複,如果發現了相同的hashCode,則調用equals方法來檢查hashCode相等的對象是否相同,如果相等,HashSet不會讓其加入成功【不會報錯,也不會拋出任何異常,僅僅只是不讓加入】

add的源碼,PRESENT是一個Object對象

    public boolean add(E e) {
        return this.map.put(e, PRESENT) == null;
    }

HashCode和equals的相關規定:

  • 兩個對象相等,則hashcode一定相同
  • 兩個對象相等,兩個equals返回的值都爲true(equals有對稱性)
  • 兩個對象有相同的hashcode,也不一定相等
  • equals方法被覆蓋過,那麼hashcode也要被覆蓋
  • hashcode默認行爲是對堆上的對象產生獨特值,如果沒有被重寫,則只有相同對象的兩個引用相等

如果沒有重寫equals和hashcode方法的例子,觀察輸出結果:

public class Test {
    public static void main(String[] args) {
        HashMap<Data, Object> map = new HashMap<>();
        Data data1 = new Data(1, "lee");
        Data data2 = new Data(1, "lee");
        map.put(data1, 1);
        map.put(data2, 2);
        System.out.println(map);
    }
}
<<<{{1,lee}=1, {1,lee}=2}

class Data {
    private Integer id;
    private String username;

    public Data(Integer id, String username) {
        this.id = id;
        this.username = username;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "{" + id + "," + username + "}";
    }
}

會認爲這兩個對象不相等

如果要讓程序判斷這兩個Java對象相等,則按照步驟

1.先覆蓋hashcode,讓兩個對象的hashcode相等,則會調用對象的equals方法

2.在覆蓋equals,改變兩個對象的邏輯相等條件

代碼如下【非常的簡陋,沒遵從規範,後面會改善的】【建議看下java.lang包下的類的實現】:

    @Override
    public int hashCode() {
        return id.hashCode()+username.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof Data) {
            Data data = (Data) obj;
            return this.id == data.id && this.username == data.username;
        }
        return false;
    }

<<<{{1,lee}=2}

HashMap的底層實現

Java8之前是數組和鏈表結合在一起,也就是鏈表散列。HashMap將key的hashcode經過擾動函數處理後得到hash值,然後再遍歷所有位置,如果當前位置存在元素的話,則比較元素hashcode值,hashcode相等則調用equals方法,如果還是相等則進行value值得覆蓋

Hash 值的範圍值 - 2147483648 到 2147483647,前後加起來大概 40 億的映射空間

擾動函數:

​ 防止某些被作爲HashMap的Key的對象的hashcode方法實現較差,有很高的兩個對象的hashcode相等的機率,從而增加了equals方法調用的機率,降低性能。擾動函數的思想就是增大hashcode的精度,讓兩個原來判別爲相等的hashcode經過此方法後不相等,降低equals被調用的機率,提升性能。

HashMap多線程操作導致死循環問題

問題在於 :併發下的Rehash造成元素之間形成循環鏈表,雖然JDK1.8解決了這個問題,還是不推薦使用,在多線程下HashMap還會造成數據丟失等其他問題,併發環境下推薦使用ConcurrentHashMap

HashTable和ConcurrentHashMap的區別

既然ConcurrentHashMap也具有線程安全的特性,那麼和HashTable的區別是什麼呢?

主要的區別體現在實現方式不同:

  • 底層數據結構:

ConcurrentHashMap在JDK1.7之前使用分段的數組+ 鏈表的結構

JDK1.8採用的數據結構和HashMap1.8一樣,爲數組+鏈表/紅黑樹形式

HashTable的底層數據結構和HashMap1.8之前的結構近似【底層是HashMap主體】,數組+鏈表

  • 實現線程安全的方式(重要):

1.JDK1.7的時候concurrentHashMap採用分段鎖Segment )的形式,每一把鎖只鎖住容器中的一段數據,多線程可以同時訪問不同數據段的數據,提高了併發率

1.8的時候則直接廢棄了分段鎖的概念,採用Node 數組 + 鏈表 + 紅黑樹 的數據結構,併發控制使用synchronized和CAS來操作(JDK1.6的時候對Synchronized鎖做了優化),看起來就像是優化過且線程安全的HashMap

HashTable則直接使用synchronized將所有數據都鎖起來,效率非常低下,且競爭越激烈效率越低

comparable 和 Comparator 的區別

  • comparable接口屬於 java.lang包,有一個compareTo方法用於排序
public interface Comparable<T> {
    int compareTo(T var1);
}
  • Comparator接口屬於Java.util,有很多抽象方法,有一個compare(Object obj1, Object obj2)方法用來排序

需要對一個集合進行排序時候可以重寫任意一個方法,

當要對一個集合進行兩種排序,要重寫compareTo和自制的Comparator方法 或者實現兩個Comparator方法

使用區別:集合中的類(要進行比較的類)去實現comparable 接口,使用Collections.sort(集合,Comparator )排序才需要實現Comparator 接口

兩個數op1與op2相比較,compare(op1,op2),返回正數表示op1>op2,返回負數表示op1<op2,最後的排列規則爲從小到大

public static void main(String[] args) {
    Integer[] a = {5, 3, 9, 4, 5};
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(a));
    Collections.sort(list, (Integer arg1, Integer arg2) -> {
        if (arg1 > arg2) {
            return 1;
        } else if (arg1 == arg2) {
            return 0;
        } else {
            return -1;
        }
    });
    System.out.println(list);
}

<<<[3, 4, 5, 5, 9]

集合框架底層數據結構總結

  • List
    • ArrayList:Object數組
    • Vector:Object數組
    • LinkedList:雙向鏈表(1.6之前爲循環鏈表,到1.7則取消了循環)
  • Set
    • HashSet(無序):基於HashMap來實現的,將值存貯到HashMap的Key當中
    • LinkedHashSet:繼承自HashSet,內部是通過LinkedHashMap的key來存儲值的
    • TreeSet(有序):紅黑樹
  • Map
    • HashMap:1.8之前使用數組+鏈表,數組是主題,鏈表主要解決Hash衝突【拉鍊法】,1.8之後當到達閾值的時候自動轉換爲紅黑樹
    • LinkedHashMap:繼承自HashMap,底層依舊沿用HashMap,並在其基礎上加了一條雙向鏈表,使得上面的結構可以保證鍵值對的插入順序
    • HashTable:數組+鏈表,和1.8前的HashMap一樣
    • TreeMap:紅黑樹

如何選用集合?

主要根據集合的特點來選用,比如我們需要根據鍵值獲取到元素值時就選用 Map 接口下的集合,需要排序時選擇 TreeMap, 不需要排序時就選擇 HashMap, 需要保證線程安全就選用 ConcurrentHashMap. 當我們只需要存放元素值時,就選擇實現 Collection 接口的集合,需要保證元素唯一時選擇實現 Set 接口的集合比如 TreeSet 或 HashSet,不需要就選擇實現 List 接口的比如 ArrayList 或 LinkedList,然後再根據實現這些接口的集合的特點來選用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章