萬字長文深刻理解java中的集合-附PDF下載

1. 前言

集合是用來存儲多個數據的,除了基本類型以外,集合應該是java中最最經常使用的類型了。java中的集合類型通常都集中在java.util包和java.util.concurrent包中。java

其中util包中的集合類是基礎的集合類,而concurrent包中的集合類是爲併發特別準備的集合類。node

集合類的父類有兩個,一個是java.util.Collection, 一個是java.util.Map。git

先看下Collection的定義:程序員

public interface Collection<E> extends Iterable<E> {
}

Collection繼承自Iterable接口,表示全部的Collection都是可遍歷的。而且Collection中能夠保存一種數據類型。github

再看下Map的定義:數組

public interface Map<K, V> {
}

能夠看到Map是一個頂級的接口,裏面能夠保持兩種數據類型,分別是key和value。緩存

其中Collection是List,Set和Queue的父類,這樣就組成了集合的四大類型:List,Queue,Set和Map,接下來咱們將會一一的進行講解。安全

2. List

先看下List的定義:數據結構

public interface List<E> extends Collection<E> {
}

List是一個接口,繼承自Collection,表示的是一個有序的鏈表,經常使用的list有ArrayList,LinkedList等等。多線程

2.1 fail-safe fail-fast知多少

咱們在使用集合類的時候,一般會須要去遍歷集合中的元素,並在遍歷中對其中的元素進行處理。這時候咱們就要用到Iterator,常常寫程序的朋友應該都知道,在Iterator遍歷的過程當中,是不可以修改集合數據的,不然就會拋出ConcurrentModificationException。

由於ConcurrentModificationException的存在,就把Iterator分紅了兩類,Fail-fast和Fail-safe。

2.1.1 Fail-fast Iterator

Fail-fast看名字就知道它的意思是失敗的很是快。就是說若是在遍歷的過程當中修改了集合的結構,則就會馬上報錯。

Fail-fast一般在下面兩種狀況下拋出ConcurrentModificationException:

  1. 單線程的環境中

若是在單線程的環境中,iterator建立以後,若是不是經過iterator自身的remove方法,而是經過調用其餘的方法修改了集合的結構,則會報錯。

  1. 多線程的環境中

若是一個線程中建立了iterator,而在另一個線程中修改了集合的結構,則會報錯。

咱們先看一個Fail-fast的例子:

Map<Integer,String> users = new HashMap<>();

        users.put(1, "jack");
        users.put(2, "alice");
        users.put(3, "jone");

        Iterator iterator1 = users.keySet().iterator();

        //not modify key, so no exception
        while (iterator1.hasNext())
        {
            log.info("{}",users.get(iterator1.next()));
            users.put(2, "mark");
        }

上面的例子中,咱們構建了一個Map,而後遍歷該map的key,在遍歷過程當中,咱們修改了map的value。

運行發現,程序完美執行,並無報任何異常。

這是由於咱們遍歷的是map的key,只要map的key沒有被手動修改,就沒有問題。

再看一個例子:

Map<Integer,String> users = new HashMap<>();

        users.put(1, "jack");
        users.put(2, "alice");
        users.put(3, "jone");

        Iterator iterator1 = users.keySet().iterator();

        Iterator iterator2 = users.keySet().iterator();
        //modify key,get exception
        while (iterator2.hasNext())
        {
            log.info("{}",users.get(iterator2.next()));
            users.put(4, "mark");
        }

上面的例子中,咱們在遍歷map的key的同時,對key進行了修改。這種狀況下就會報錯。

2.1.2 Fail-fast 的原理

爲何修改了集合的結構就會報異常呢?

咱們以ArrayList爲例,來說解下Fail-fast 的原理。

在AbstractList中,定義了一個modCount變量:

protected transient int modCount = 0;

在遍歷的過程當中都會去調用checkForComodification()方法來對modCount進行檢測:

public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

若是檢測的結果不是所預期的,就會報錯:

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

在建立Iterator的時候會複製當前的modCount進行比較,而這個modCount在每次集合修改的時候都會進行變更,最終致使Iterator中的modCount和現有的modCount是不一致的。

public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

注意,Fail-fast並不保證全部的修改都會報錯,咱們不可以依賴ConcurrentModificationException來判斷遍歷中集合是否被修改。

2.1.3 Fail-safe Iterator

咱們再來說一下Fail-safe,Fail-safe的意思是在遍歷的過程當中,若是對集合進行修改是不會報錯的。

Concurrent包下面的類型都是Fail-safe的。看一個ConcurrentHashMap的例子:

Map<Integer,String> users = new ConcurrentHashMap<>();

        users.put(1, "jack");
        users.put(2, "alice");
        users.put(3, "jone");

        Iterator iterator1 = users.keySet().iterator();

        //not modify key, so no exception
        while (iterator1.hasNext())
        {
            log.info("{}",users.get(iterator1.next()));
            users.put(2, "mark");
        }

        Iterator iterator2 = users.keySet().iterator();
        //modify key,get exception
        while (iterator2.hasNext())
        {
            log.info("{}",users.get(iterator2.next()));
            users.put(4, "mark");
        }

上面的例子完美執行,不會報錯。

2.2 Iterator to list的三種方法

集合的變量少不了使用Iterator,從集合Iterator很是簡單,直接調用Iterator方法就能夠了。

那麼如何從Iterator反過來生成List呢?今天教你們三個方法。

2.2.1 使用while

最簡單最基本的邏輯就是使用while來遍歷這個Iterator,在遍歷的過程當中將Iterator中的元素添加到新建的List中去。

以下面的代碼所示:

@Test
    public void useWhile(){
        List<String> stringList= new ArrayList<>();
        Iterator<String> stringIterator= Arrays.asList("a","b","c").iterator();
        while(stringIterator.hasNext()){
            stringList.add(stringIterator.next());
        }
        log.info("{}",stringList);
    }

2.2.2 使用ForEachRemaining

Iterator接口有個default方法:

default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

實際上這方法的底層就是封裝了while循環,那麼咱們能夠直接使用這個ForEachRemaining的方法:

@Test
    public void useForEachRemaining(){
        List<String> stringList= new ArrayList<>();
        Iterator<String> stringIterator= Arrays.asList("a","b","c").iterator();
        stringIterator.forEachRemaining(stringList::add);
        log.info("{}",stringList);
    }

2.2.3 使用stream

咱們知道構建Stream的時候,能夠調用StreamSupport的stream方法:

public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel)

該方法傳入一個spliterator參數。而Iterable接口正好有一個spliterator()的方法:

default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }

那麼咱們能夠將Iterator轉換爲Iterable,而後傳入stream。

仔細研究Iterable接口能夠發現,Iterable是一個FunctionalInterface,只須要實現下面的接口就好了:

Iterator<T> iterator();

利用lambda表達式,咱們能夠方便的將Iterator轉換爲Iterable:

Iterator<String> stringIterator= Arrays.asList("a","b","c").iterator();
        Iterable<String> stringIterable = () -> stringIterator;

最後將其換行成爲List:

List<String> stringList= StreamSupport.stream(stringIterable.spliterator(),false).collect(Collectors.toList());
        log.info("{}",stringList);

2.3 asList和ArrayList不得不說的故事

提到集合類,ArrayList應該是用到的很是多的類了。這裏的ArrayList是java.util.ArrayList,一般咱們怎麼建立ArrayList呢?

2.3.1 建立ArrayList

看下下面的例子:

List<String> names = new ArrayList<>();

上面的方法建立了一個ArrayList,若是咱們須要向其中添加元素的話,須要再調用add方法。

一般咱們會使用一種更加簡潔的辦法來建立List:

@Test
    public void testAsList(){
        List<String> names = Arrays.asList("alice", "bob", "jack");
        names.add("mark");

    }

看下asList方法的定義:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

很好,使用Arrays.asList,咱們能夠方便的建立ArrayList。

運行下上面的例子,奇怪的事情發生了,上面的例子竟然拋出了UnsupportedOperationException異常。

java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(AbstractList.java:148)
    at java.util.AbstractList.add(AbstractList.java:108)
    at com.flydean.AsListUsage.testAsList(AsListUsage.java:18)

2.3.2 UnsupportedOperationException

先講一下這個異常,UnsupportedOperationException是一個運行時異常,一般用在某些類中並無實現接口的某些方法。

爲何上面的ArrayList調用add方法會拋異常呢?

2.3.3 asList

咱們再來詳細的看一下Arrays.asList方法中返回的ArrayList:

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable

能夠看到,Arrays.asList返回的ArrayList是Arrays類中的一個內部類,並非java.util.ArrayList。

這個類繼承自AbstractList,在AbstractList中add方法是這樣定義的:

public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

好了,咱們的問題獲得瞭解決。

2.3.4 轉換

咱們使用Arrays.asList獲得ArrayList以後,能不能將其轉換成爲java.util.ArrayList呢?答案是確定的。

咱們看下下面的例子:

@Test
    public void testList(){
        List<String> names = new ArrayList<>(Arrays.asList("alice", "bob", "jack"));
        names.add("mark");
    }

上面的例子能夠正常執行。

在java中有不少一樣名字的類,咱們須要弄清楚他們究竟是什麼,不要混淆了。

2.4 Copy ArrayList的四種方式

ArrayList是咱們常常會用到的集合類,有時候咱們須要拷貝一個ArrayList,今天向你們介紹拷貝ArrayList經常使用的四種方式。

2.4.1 使用構造函數

ArrayList有個構造函數,能夠傳入一個集合:

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

上面的代碼咱們能夠看出,底層實際上調用了Arrays.copyOf方法來對數組進行拷貝。這個拷貝調用了系統的native arraycopy方法,注意這裏的拷貝是引用拷貝,而不是值的拷貝。這就意味着這若是拷貝以後對象的值發送了變化,源對象也會發生改變。

舉個例子:

@Test
    public void withConstructor(){
        List<String> stringList=new ArrayList<>(Arrays.asList("a","b","c"));
        List<String> copyList = new ArrayList<>(stringList);
        copyList.set(0,"e");
        log.info("{}",stringList);
        log.info("{}",copyList);

        List<CustBook> objectList=new ArrayList<>(Arrays.asList(new CustBook("a"),new CustBook("b"),new CustBook("c")));
        List<CustBook> copyobjectList = new ArrayList<>(objectList);
        copyobjectList.get(0).setName("e");
        log.info("{}",objectList);
        log.info("{}",copyobjectList);
    }

運行結果:

22:58:39.001 [main] INFO com.flydean.CopyList - [a, b, c]
22:58:39.008 [main] INFO com.flydean.CopyList - [e, b, c]
22:58:39.009 [main] INFO com.flydean.CopyList - [CustBook(name=e), CustBook(name=b), CustBook(name=c)]
22:58:39.009 [main] INFO com.flydean.CopyList - [CustBook(name=e), CustBook(name=b), CustBook(name=c)]

咱們看到對象的改變實際上改變了拷貝的源。而copyList.set(0,"e")實際上建立了一個新的String對象,並把它賦值到copyList的0位置。

2.4.2 使用addAll方法

List有一個addAll方法,咱們可使用這個方法來進行拷貝:

@Test
    public void withAddAll(){

        List<CustBook> objectList=new ArrayList<>(Arrays.asList(new CustBook("a"),new CustBook("b"),new CustBook("c")));
        List<CustBook> copyobjectList = new ArrayList<>();
        copyobjectList.addAll(objectList);
        copyobjectList.get(0).setName("e");
        log.info("{}",objectList);
        log.info("{}",copyobjectList);
    }

一樣的拷貝的是對象的引用。

2.4.3 使用Collections.copy

一樣的,使用Collections.copy也能夠獲得相同的效果,看下代碼:

@Test
    public void withCopy(){
        List<CustBook> objectList=new ArrayList<>(Arrays.asList(new CustBook("a"),new CustBook("b"),new CustBook("c")));
        List<CustBook> copyobjectList = new ArrayList<>(Arrays.asList(new CustBook("d"),new CustBook("e"),new CustBook("f")));
        Collections.copy(copyobjectList, objectList);
        copyobjectList.get(0).setName("e");
        log.info("{}",objectList);
        log.info("{}",copyobjectList);
    }

2.4.4 使用stream

咱們也可使用java 8引入的stream來實現:

@Test
    public void withStream(){

        List<CustBook> objectList=new ArrayList<>(Arrays.asList(new CustBook("a"),new CustBook("b"),new CustBook("c")));
        List<CustBook> copyobjectList=objectList.stream().collect(Collectors.toList());
        copyobjectList.get(0).setName("e");
        log.info("{}",objectList);
        log.info("{}",copyobjectList);

    }

好了,四種方法講完了,你們要注意四種方法都是引用拷貝,在使用的時候要當心。

3. Map

先看下Map的定義:

public interface Map<K, V> {
}

Map是一個key-value對的集合,其中key不可以重複,可是value能夠重複。經常使用的Map有TreeMap和hashMap。

3.1 深刻理解HashMap和TreeMap的區別

HashMap和TreeMap是Map家族中很是經常使用的兩個類,兩個類在使用上和本質上有什麼區別呢?本文將從這兩個方面進行深刻的探討,但願能揭露其本質。

3.1.1 HashMap和TreeMap本質區別

先看HashMap的定義:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

再看TreeMap的定義:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

從類的定義來看,HashMap和TreeMap都繼承自AbstractMap,不一樣的是HashMap實現的是Map接口,而TreeMap實現的是NavigableMap接口。NavigableMap是SortedMap的一種,實現了對Map中key的排序。

這樣二者的第一個區別就出來了,TreeMap是排序的而HashMap不是。

再看看HashMap和TreeMap的構造函數的區別。

public HashMap(int initialCapacity, float loadFactor)

HashMap除了默認的無參構造函數以外,還能夠接受兩個參數initialCapacity和loadFactor。

HashMap的底層結構是Node的數組:

transient Node<K,V>[] table

initialCapacity就是這個table的初始容量。若是你們不傳initialCapacity,HashMap提供了一個默認的值:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

當HashMap中存儲的數據過多的時候,table數組就會被裝滿,這時候就須要擴容,HashMap的擴容是以2的倍數來進行的。而loadFactor就指定了何時須要進行擴容操做。默認的loadFactor是0.75。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

再來看幾個很是有趣的變量:

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

上面的三個變量有什麼用呢?在java 8以前,HashMap解決hashcode衝突的方法是採用鏈表的形式,爲了提高效率,java 8將其轉成了TreeNode。何時會發送這個轉換呢?

這時候就要看這兩個變量TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD。

有的同窗可能發現了,TREEIFY_THRESHOLD爲何比UNTREEIFY_THRESHOLD大2呢?其實這個問題我也不知道,可是你看源代碼的話,用到UNTREEIFY_THRESHOLD時候,都用的是<=,而用到TREEIFY_THRESHOLD的時候,都用的是>= TREEIFY_THRESHOLD - 1,因此這兩個變量在本質上是同樣的。

MIN_TREEIFY_CAPACITY表示的是若是table轉換TreeNode的最小容量,只有capacity >= MIN_TREEIFY_CAPACITY的時候才容許TreeNode的轉換。

TreeMap和HashMap不一樣的是,TreeMap的底層是一個Entry:

private transient Entry<K,V> root

他的實現是一個紅黑樹,方便用來遍歷和搜索。

TreeMap的構造函數能夠傳入一個Comparator,實現自定義的比較方法。

public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

若是不提供自定義的比較方法,則使用的是key的natural order。

3.1.2 排序區別

咱們講完二者的本質以後,如今舉例說明,先看下二者對排序的區別:

@Test
    public void withOrder(){
        Map<String, String> books = new HashMap<>();
        books.put("bob", "books");
        books.put("c", "concurrent");
        books.put("a", "a lock");
        log.info("{}",books);
    }
@Test
    public void withOrder(){
        Map<String, String> books = new TreeMap<>();
        books.put("bob", "books");
        books.put("c", "concurrent");
        books.put("a", "a lock");
        log.info("{}",books);
    }

一樣的代碼,一個使用了HashMap,一個使用了TreeMap,咱們會發現TreeMap輸出的結果是排好序的,而HashMap的輸出結果是不定的。

3.1.3 Null值的區別

HashMap能夠容許一個null key和多個null value。而TreeMap不容許null key,可是能夠容許多個null value。

@Test
    public void withNull() {
        Map<String, String> hashmap = new HashMap<>();
        hashmap.put(null, null);
        log.info("{}",hashmap);
    }
@Test
    public void withNull() {
        Map<String, String> hashmap = new TreeMap<>();
        hashmap.put(null, null);
        log.info("{}",hashmap);
    }

HashMap會報出: NullPointerException。

3.1.4 性能區別

HashMap的底層是Array,因此HashMap在添加,查找,刪除等方法上面速度會很是快。而TreeMap的底層是一個Tree結構,因此速度會比較慢。

另外HashMap由於要保存一個Array,因此會形成空間的浪費,而TreeMap只保存要保持的節點,因此佔用的空間比較小。

HashMap若是出現hash衝突的話,效率會變差,不過在java 8進行TreeNode轉換以後,效率有很大的提高。

TreeMap在添加和刪除節點的時候會進行重排序,會對性能有所影響。

3.1.5 共同點

二者都不容許duplicate key,二者都不是線程安全的。

3.2 深刻理解HashMap和LinkedHashMap的區別

咱們知道HashMap的變量順序是不可預測的,這意味着便利的輸出順序並不必定和HashMap的插入順序是一致的。這個特性一般會對咱們的工做形成必定的困擾。爲了實現這個功能,咱們可使用LinkedHashMap。

3.2.1 LinkedHashMap詳解

先看下LinkedHashMap的定義:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap繼承自HashMap,因此HashMap的全部功能在LinkedHashMap均可以用。

LinkedHashMap和HashMap的區別就是新建立了一個Entry:

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

這個Entry繼承自HashMap.Node,多了一個before,after來實現Node之間的鏈接。

經過這個新建立的Entry,就能夠保證遍歷的順序和插入的順序一致。

3.2.2 插入

下面看一個LinkedHashMap插入的例子:

@Test
    public void insertOrder(){
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("ddd","desk");
        map.put("aaa","ask");
        map.put("ccc","check");
        map.keySet().forEach(System.out::println);
    }

輸出結果:

ddd
aaa
ccc

能夠看到輸出結果和插入結果是一致的。

3.2.3 訪問

除了遍歷的順序,LinkedHashMap還有一個很是有特點的訪問順序。

咱們再看一個LinkedHashMap的構造函數:

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

前面的兩個參數initialCapacity,loadFactor咱們以前已經講過了,如今看最後一個參數accessOrder。

當accessOrder設置成爲true的時候,就開啓了 access-order。

access order的意思是,將對象安裝最老訪問到最新訪問的順序排序。咱們看個例子:

@Test
    public void accessOrder(){
        LinkedHashMap<String, String> map = new LinkedHashMap<>(16, .75f, true);
        map.put("ddd","desk");
        map.put("aaa","ask");
        map.put("ccc","check");
        map.keySet().forEach(System.out::println);
        map.get("aaa");
        map.keySet().forEach(System.out::println);
    }

輸出結果:

ddd
aaa
ccc
ddd
ccc
aaa

咱們看到,由於訪問了一次「aaa「,從而致使遍歷的時候排到了最後。

3.2.4 removeEldestEntry

最後咱們看一下LinkedHashMap的一個特別的功能removeEldestEntry。這個方法是幹什麼的呢?

經過從新removeEldestEntry方法,可讓LinkedHashMap保存特定數目的Entry,一般用在LinkedHashMap用做緩存的狀況。

removeEldestEntry將會刪除最老的Entry,保留最新的。

ublic class CustLinkedHashMap<K, V> extends LinkedHashMap<K, V> {

    private static final int MAX_ENTRIES = 10;

    public CustLinkedHashMap(
            int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
}

看上面的一個自定義的例子,上面的例子咱們建立了一個保留10個Entry節點的LinkedHashMap。

3.2.5 總結

LinkedHashMap繼承自HashMap,同時提供了兩個很是有用的功能。

3.3 EnumMap和EnumSet

通常來講咱們會選擇使用HashMap來存儲key-value格式的數據,考慮這樣的特殊狀況,一個HashMap的key都來自於一個Enum類,這樣的狀況則能夠考慮使用本文要講的EnumMap。

3.3.1 EnumMap

先看一下EnumMap的定義和HashMap定義的比較:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

咱們能夠看到EnumMap幾乎和HashMap是同樣的,區別在於EnumMap的key是一個Enum。

下面看一個簡單的使用的例子:

先定義一個Enum:

public enum Types {
    RED, GREEN, BLACK, YELLO
}

再看下怎麼使用EnumMap:

@Test
    public void useEnumMap(){
        EnumMap<Types, String> activityMap = new EnumMap<>(Types.class);
        activityMap.put(Types.BLACK,"black");
        activityMap.put(Types.GREEN,"green");
        activityMap.put(Types.RED,"red");
    }

其餘的操做其實和hashMap是相似的,咱們這裏就很少講了。

3.3.2 何時使用EnumMap

由於在EnumMap中,全部的key的可能值在建立的時候已經知道了,因此使用EnumMap和hashMap相比,能夠提高效率。

同時,由於key比較簡單,因此EnumMap在實現中,也不須要像HashMap那樣考慮一些複雜的狀況。

3.3.3 EnumSet

跟EnumMap很相似,EnumSet是一個set,而後set中的元素都是某個Enum類型。

EnumSet是一個抽象類,要建立EnumSet類可使用EnumSet提供的兩個靜態方法,noneOf和allOf。

先看一個noneOf:

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

noneOf傳入一個Enum類,返回一個空的Enum類型的EnumSet。

從上面的代碼咱們能夠看到EnumSet有兩個實現,長度大於64的時候使用JumboEnumSet,小有64的時候使用RegularEnumSet。

注意,JumboEnumSet和RegularEnumSet不建議直接使用,他是內部使用的類。

再看一下allOf:

public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
        EnumSet<E> result = noneOf(elementType);
        result.addAll();
        return result;
    }

allOf很簡單,先調用noneOf建立空的set,而後調用addAll方法將全部的元素添加進去。

3.3.4 總結

EnumMap和EnumSet對特定的Enum對象作了優化,能夠在合適的狀況下使用。

3.4 SkipList和ConcurrentSkipListMap的實現

一開始據說SkipList我是一臉懵逼的,啥?還有SkipList?這個是什麼玩意。

後面通過個人不斷搜索和學習,終於明白了SkipList原來是一種數據結構,而java中的ConcurrentSkipListMap和ConcurrentSkipListSet就是這種結構的實現。

接下來就讓咱們一步一步的揭開SkipList和ConcurrentSkipListMap的面紗吧。

3.4.1 SkipList

先看下維基百科中SkipList的定義:

SkipList是一種層級結構。最底層的是排序過的最原始的linked list。

往上是一層一層的層級結構,每一個底層節點按照必定的機率出如今上一層list中。這個機率叫作p,一般p取1/2或者1/4。

先設定一個函數f,能夠隨機產生0和1這兩個數,而且這兩個數出現的概率是同樣的,那麼這時候的p就是1/2。

對每一個節點,咱們這樣操做:

咱們運行一次f,當f=1時,咱們將該節點插入到上層layer的list中去。當f=0時,不插入。

舉個例子,上圖中的list中有10個排序過的節點,第一個節點默認每層都有。對於第二個節點,運行f=0,不插入。對於第三個節點,運行f=1,將第三個節點插入layer 1,以此類推,最後獲得的layer 1 list中的節點有:1,3,4,6,9。

而後咱們再繼續往上構建layer。 最終獲得上圖的SkipList。

經過使用SkipList,咱們構建了多個List,包含不一樣的排序過的節點,從而提高List的查找效率。

咱們經過下圖能有一個更清晰的認識:

每次的查找都是從最頂層開始,由於最頂層的節點數最少,若是要查找的節點在list中的兩個節點中間,則向下移一層繼續查找,最終找到最底層要插入的位置,插入節點,而後再次調用機率函數f,決定是否向上複製節點。

其本質上至關於二分法查找,其查找的時間複雜度是O(logn)。

3.4.2 ConcurrentSkipListMap

ConcurrentSkipListMap是一個併發的SkipList,那麼它具備兩個特色,SkipList和concurrent。咱們分別來說解。

  • SkipList的實現

上面講解了SkipList的數據結構,接下來看下ConcurrentSkipListMap是怎麼實現這個skipList的:

ConcurrentSkipListMap中有三種結構,base nodes,Head nodes和index nodes。

base nodes組成了有序的鏈表結構,是ConcurrentSkipListMap的最底層實現。

static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;

        /**
         * Creates a new regular node.
         */
        Node(K key, Object value, Node<K,V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

上面能夠看到每一個Node都是一個k,v的entry,而且其有一個next指向下一個節點。

index nodes是構建SkipList上層結構的基本節點:

static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;

        /**
         * Creates index node with given values.
         */
        Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
            this.node = node;
            this.down = down;
            this.right = right;
        }
    }

從上面的構造咱們能夠看到,Index節點包含了Node節點,除此以外,Index還有兩個指針,一個指向同一個layer的下一個節點,一個指向下一層layer的節點。

這樣的結構能夠方便遍歷的實現。

最後看一下HeadIndex,HeadIndex表明的是Head節點:

static final class HeadIndex<K,V> extends Index<K,V> {
        final int level;
        HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
            super(node, down, right);
            this.level = level;
        }
    }

HeadIndex和Index很相似,只不過多了一個level字段,表示所在的層級。

在ConcurrentSkipListMap初始化的時候,會初始化HeadIndex:

head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),null, null, 1);

咱們能夠看到HeadIndex中的Node是key=null,value=BASE_HEADER的虛擬節點。初始的level=1。

  • concurrent的實現

接下來,咱們再看一下併發是怎麼實現的:

基本上併發類都是經過UNSAFE.compareAndSwapObject來實現的,ConcurrentSkipListMap也不例外。

假如咱們有三個節點,b-n-f。如今須要刪除節點n。

第一步,使用CAS將n的valu的值從non-null設置爲null。這個時候,任何外部的操做都會認爲這個節點是不存在的。可是那些內部的插入或者刪除操做仍是會繼續修改n的next指針。

第二步,使用CAS將n的next指針指向一個新的marker節點,從這個時候開始,n的next指針將不會指向任何其餘的節點。

咱們看下marker節點的定義:

Node(Node<K,V> next) {
            this.key = null;
            this.value = this;
            this.next = next;
        }

咱們能夠看到marker節點其實是一個key爲null,value是本身的節點。

第三步,使用CAS將b的next指針指向f。從這一步起,n節點不會再被其餘的程序訪問,這意味着n能夠被垃圾回收了。

咱們思考一下爲何要插入一個marker節點,這是由於咱們在刪除的時候,須要告訴全部的線程,節點n準備被刪除了,由於n原本就指向f節點,這個時候須要一箇中間節點來表示這個準備刪除的狀態。

4. Queue

先看下Queue的定義:

public interface Queue<E> extends Collection<E> {
}

Queue表示的是隊列,其特色就是先進先出。經常使用的Queue有DelayQueue,BlockingQueue等等。

4.1 java中的Queue家族

java中Collection集合有三你們族List,Set和Queue。固然Map也算是一種集合類,但Map並不繼承Collection接口。

List,Set在咱們的工做中會常用,一般用來存儲結果數據,而Queue因爲它的特殊性,一般用在生產者消費者模式中。

如今很火的消息中間件好比:Rabbit MQ等都是Queue這種數據結構的展開。

今天這篇文章將帶你們進入Queue家族。

4.1.1 Queue接口

先看下Queue的繼承關係和其中定義的方法:

Queue繼承自Collection,Collection繼承自Iterable。

Queue有三類主要的方法,咱們用個表格來看一下他們的區別:

方法類型 方法名稱 方法名稱 區別
Insert add offer 兩個方法都表示向Queue中添加某個元素,不一樣之處在於添加失敗的狀況,add只會返回true,若是添加失敗,會拋出異常。offer在添加失敗的時候會返回false。因此對那些有固定長度的Queue,優先使用offer方法。
Remove remove poll 若是Queue是空的狀況下,remove會拋出異常,而poll會返回null。
Examine element peek 獲取Queue頭部的元素,但不從Queue中刪除。二者的區別仍是在於Queue爲空的狀況下,element會拋出異常,而peek返回null。
注意,由於對poll和peek來講null是有特殊含義的,因此通常來講Queue中禁止插入null,可是在實現中仍是有一些類容許插入null好比LinkedList。

儘管如此,咱們在使用中仍是要避免插入null元素。

4.1.2 Queue的分類

通常來講Queue能夠分爲BlockingQueue,Deque和TransferQueue三種。

  • BlockingQueue

BlockingQueue是Queue的一種實現,它提供了兩種額外的功能:

  1. 噹噹前Queue是空的時候,從BlockingQueue中獲取元素的操做會被阻塞。
  2. 噹噹前Queue達到最大容量的時候,插入BlockingQueue的操做會被阻塞。

BlockingQueue的操做能夠分爲下面四類:

操做類型 Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek() not applicable not applicable

第一類是會拋出異常的操做,當遇到插入失敗,隊列爲空的時候拋出異常。

第二類是不會拋出異常的操做。

第三類是會Block的操做。當Queue爲空或者達到最大容量的時候。

第四類是time out的操做,在給定的時間裏會Block,超時會直接返回。

BlockingQueue是線程安全的Queue,能夠在生產者消費者模式的多線程中使用,以下所示:

class Producer implements Runnable {
   private final BlockingQueue queue;
   Producer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { queue.put(produce()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   Object produce() { ... }
 }

 class Consumer implements Runnable {
   private final BlockingQueue queue;
   Consumer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { consume(queue.take()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   void consume(Object x) { ... }
 }

 class Setup {
   void main() {
     BlockingQueue q = new SomeQueueImplementation();
     Producer p = new Producer(q);
     Consumer c1 = new Consumer(q);
     Consumer c2 = new Consumer(q);
     new Thread(p).start();
     new Thread(c1).start();
     new Thread(c2).start();
   }
 }

最後,在一個線程中向BlockQueue中插入元素以前的操做happens-before另一個線程中從BlockQueue中刪除或者獲取的操做。

  • Deque

Deque是Queue的子類,它表明double ended queue,也就是說能夠從Queue的頭部或者尾部插入和刪除元素。

一樣的,咱們也能夠將Deque的方法用下面的表格來表示,Deque的方法能夠分爲對頭部的操做和對尾部的操做:

方法類型 Throws exception Special value Throws exception Special value
Insert addFirst(e) offerFirst(e) addLast(e) offerLast(e)
Remove removeFirst() pollFirst() removeLast() pollLast()
Examine getFirst() peekFirst() getLast() peekLast()

和Queue的方法描述基本一致,這裏就很少講了。

當Deque以 FIFO (First-In-First-Out)的方法處理元素的時候,Deque就至關於一個Queue。

當Deque以LIFO (Last-In-First-Out)的方式處理元素的時候,Deque就至關於一個Stack。

  • TransferQueue

TransferQueue繼承自BlockingQueue,爲何叫Transfer呢?由於TransferQueue提供了一個transfer的方法,生產者能夠調用這個transfer方法,從而等待消費者調用take或者poll方法從Queue中拿取數據。

還提供了非阻塞和timeout版本的tryTransfer方法以供使用。

咱們舉個TransferQueue實現的生產者消費者的問題。

先定義一個生產者:

@Slf4j
@Data
@AllArgsConstructor
class Producer implements Runnable {
    private TransferQueue<String> transferQueue;

    private String name;

    private Integer messageCount;

    public static final AtomicInteger messageProduced = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < messageCount; i++) {
            try {
                boolean added = transferQueue.tryTransfer( "第"+i+"個", 2000, TimeUnit.MILLISECONDS);
                log.info("transfered {} 是否成功: {}","第"+i+"個",added);
                if(added){
                    messageProduced.incrementAndGet();
                }
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
        }
        log.info("total transfered {}",messageProduced.get());
    }
}

在生產者的run方法中,咱們調用了tryTransfer方法,等待2秒鐘,若是沒成功則直接返回。

再定義一個消費者:

@Slf4j
@Data
@AllArgsConstructor
public class Consumer implements Runnable {

    private TransferQueue<String> transferQueue;

    private String name;

    private int messageCount;

    public static final AtomicInteger messageConsumed = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < messageCount; i++) {
            try {
                String element = transferQueue.take();
                log.info("take {}",element );
                messageConsumed.incrementAndGet();
                Thread.sleep(500);
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
        }
        log.info("total consumed {}",messageConsumed.get());
    }

}

在run方法中,調用了transferQueue.take方法來取消息。

下面先看一下一個生產者,零個消費者的狀況:

@Test
    public void testOneProduceZeroConsumer() throws InterruptedException {

        TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
        ExecutorService exService = Executors.newFixedThreadPool(10);
        Producer producer = new Producer(transferQueue, "ProducerOne", 5);

        exService.execute(producer);

        exService.awaitTermination(50000, TimeUnit.MILLISECONDS);
        exService.shutdown();
    }

輸出結果:

[pool-1-thread-1] INFO com.flydean.Producer - transfered 第0個 是否成功: false
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第1個 是否成功: false
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第2個 是否成功: false
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第3個 是否成功: false
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第4個 是否成功: false
[pool-1-thread-1] INFO com.flydean.Producer - total transfered 0

能夠看到,由於沒有消費者,因此消息並無發送成功。

再看下一個有消費者的狀況:

@Test
    public void testOneProduceOneConsumer() throws InterruptedException {

        TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
        ExecutorService exService = Executors.newFixedThreadPool(10);
        Producer producer = new Producer(transferQueue, "ProducerOne", 2);
        Consumer consumer = new Consumer(transferQueue, "ConsumerOne", 2);

        exService.execute(producer);
        exService.execute(consumer);

        exService.awaitTermination(50000, TimeUnit.MILLISECONDS);
        exService.shutdown();
    }

輸出結果:

[pool-1-thread-2] INFO com.flydean.Consumer - take 第0個
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第0個 是否成功: true
[pool-1-thread-2] INFO com.flydean.Consumer - take 第1個
[pool-1-thread-1] INFO com.flydean.Producer - transfered 第1個 是否成功: true
[pool-1-thread-1] INFO com.flydean.Producer - total transfered 2
[pool-1-thread-2] INFO com.flydean.Consumer - total consumed 2

能夠看到Producer和Consumer是一個一個來生產和消費的。

4.2 PriorityQueue和PriorityBlockingQueue

Queue通常來講都是FIFO的,固然以前咱們也介紹過Deque能夠作爲棧來使用。今天咱們介紹一種PriorityQueue,能夠安裝對象的天然順序或者自定義順序在Queue中進行排序。

4.2.1 PriorityQueue

先看PriorityQueue,這個Queue繼承自AbstractQueue,是非線程安全的。

PriorityQueue的容量是unbounded的,也就是說它沒有容量大小的限制,因此你能夠無限添加元素,若是添加的太多,最後會報OutOfMemoryError異常。

這裏教你們一個識別的技能,只要集合類中帶有CAPACITY的,其底層實現大部分都是數組,由於只有數組纔有capacity,固然也有例外,好比LinkedBlockingDeque。

只要集合類中帶有comparator的,那麼這個集合必定是個有序集合。

咱們看下PriorityQueue:

private static final int DEFAULT_INITIAL_CAPACITY = 11;
 private final Comparator<? super E> comparator;

定義了初始Capacity和comparator,那麼PriorityQueue的底層實現就是Array,而且它是一個有序集合。

有序集合默認狀況下是按照natural ordering來排序的,若是你傳入了 Comparator,則會按照你指定的方式進行排序,咱們看兩個排序的例子:

@Slf4j
public class PriorityQueueUsage {

    @Test
    public void usePriorityQueue(){
        PriorityQueue<Integer> integerQueue = new PriorityQueue<>();

        integerQueue.add(1);
        integerQueue.add(3);
        integerQueue.add(2);

        int first = integerQueue.poll();
        int second = integerQueue.poll();
        int third = integerQueue.poll();

        log.info("{},{},{}",first,second,third);
    }

    @Test
    public void usePriorityQueueWithComparator(){
        PriorityQueue<Integer> integerQueue = new PriorityQueue<>((a,b)-> b-a);
        integerQueue.add(1);
        integerQueue.add(3);
        integerQueue.add(2);

        int first = integerQueue.poll();
        int second = integerQueue.poll();
        int third = integerQueue.poll();

        log.info("{},{},{}",first,second,third);
    }
}

默認狀況下會按照升序排列,第二個例子中咱們傳入了一個逆序的Comparator,則會按照逆序排列。

4.2.2 PriorityBlockingQueue

PriorityBlockingQueue是一個BlockingQueue,因此它是線程安全的。

咱們考慮這樣一個問題,若是兩個對象的natural ordering或者Comparator的順序是同樣的話,兩個對象的順序仍是固定的嗎?

出現這種狀況,默認順序是不能肯定的,可是咱們能夠這樣封裝對象,讓對象能夠在排序順序一致的狀況下,再按照建立順序先進先出FIFO的二次排序:

public class FIFOEntry<E extends Comparable<? super E>>
        implements Comparable<FIFOEntry<E>> {
    static final AtomicLong seq = new AtomicLong(0);
    final long seqNum;
    final E entry;
    public FIFOEntry(E entry) {
        seqNum = seq.getAndIncrement();
        this.entry = entry;
    }
    public E getEntry() { return entry; }
    public int compareTo(FIFOEntry<E> other) {
        int res = entry.compareTo(other.entry);
        if (res == 0 && other.entry != this.entry)
            res = (seqNum < other.seqNum ? -1 : 1);
        return res;
    }
}

上面的例子中,先比較兩個Entry的natural ordering,若是一致的話,再按照seqNum進行排序。

4.3 SynchronousQueue詳解

SynchronousQueue是BlockingQueue的一種,因此SynchronousQueue是線程安全的。SynchronousQueue和其餘的BlockingQueue不一樣的是SynchronousQueue的capacity是0。即SynchronousQueue不存儲任何元素。

也就是說SynchronousQueue的每一次insert操做,必須等待其餘線性的remove操做。而每個remove操做也必須等待其餘線程的insert操做。

這種特性可讓咱們想起了Exchanger。和Exchanger不一樣的是,使用SynchronousQueue能夠在兩個線程中傳遞同一個對象。一個線程放對象,另一個線程取對象。

4.3.1 舉例說明

咱們舉一個多線程中傳遞對象的例子。仍是舉生產者消費者的例子,在生產者中咱們建立一個對象,在消費者中咱們取出這個對象。先看一下用CountDownLatch該怎麼作:

@Test
    public void useCountdownLatch() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        AtomicReference<Object> atomicReference= new AtomicReference<>();
        CountDownLatch countDownLatch = new CountDownLatch(1);

        Runnable producer = () -> {
            Object object=new Object();
            atomicReference.set(object);
            log.info("produced {}",object);
            countDownLatch.countDown();
        };

        Runnable consumer = () -> {
            try {
                countDownLatch.await();
                Object object = atomicReference.get();
                log.info("consumed {}",object);
            } catch (InterruptedException ex) {
                log.error(ex.getMessage(),ex);
            }
        };

        executor.submit(producer);
        executor.submit(consumer);

        executor.awaitTermination(50000, TimeUnit.MILLISECONDS);
        executor.shutdown();
    }

上例中,咱們使用AtomicReference來存儲要傳遞的對象,而且定義了一個型號量爲1的CountDownLatch。

在producer中,咱們存儲對象,而且countDown。

在consumer中,咱們await,而後取出對象。

輸出結果:

[pool-1-thread-1] INFO com.flydean.SynchronousQueueUsage - produced java.lang.Object@683d1b4b
[pool-1-thread-2] INFO com.flydean.SynchronousQueueUsage - consumed java.lang.Object@683d1b4b

能夠看到傳入和輸出了同一個對象。

上面的例子咱們也能夠用SynchronousQueue來改寫:

@Test
    public void useSynchronousQueue() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        SynchronousQueue<Object> synchronousQueue=new SynchronousQueue<>();

        Runnable producer = () -> {
            Object object=new Object();
            try {
                synchronousQueue.put(object);
            } catch (InterruptedException ex) {
                log.error(ex.getMessage(),ex);
            }
            log.info("produced {}",object);
        };

        Runnable consumer = () -> {
            try {
                Object object = synchronousQueue.take();
                log.info("consumed {}",object);
            } catch (InterruptedException ex) {
                log.error(ex.getMessage(),ex);
            }
        };

        executor.submit(producer);
        executor.submit(consumer);

        executor.awaitTermination(50000, TimeUnit.MILLISECONDS);
        executor.shutdown();
    }

上面的例子中,若是咱們使用synchronousQueue,則能夠不用手動同步,也不須要額外的存儲。

若是咱們須要在代碼中用到這種線程中傳遞對象的狀況,那麼使用synchronousQueue吧。

4.4 DelayQueue的使用

今天給你們介紹一下DelayQueue,DelayQueue是BlockingQueue的一種,因此它是線程安全的,DelayQueue的特色就是插入Queue中的數據能夠按照自定義的delay時間進行排序。只有delay時間小於0的元素纔可以被取出。

4.4.1 DelayQueue

先看一下DelayQueue的定義:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

從定義能夠看到,DelayQueue中存入的對象都必須是Delayed的子類。

Delayed繼承自Comparable,而且須要實現一個getDelay的方法。

爲何這樣設計呢?

由於DelayQueue的底層存儲是一個PriorityQueue,在以前的文章中咱們講過了,PriorityQueue是一個可排序的Queue,其中的元素必須實現Comparable方法。而getDelay方法則用來判斷排序後的元素是否能夠從Queue中取出。

4.4.2 DelayQueue的應用

DelayQueue通常用於生產者消費者模式,咱們下面舉一個具體的例子。

首先要使用DelayQueue,必須自定義一個Delayed對象:

@Data
public class DelayedUser implements Delayed {
    private String name;
    private long avaibleTime;

    public DelayedUser(String name, long delayTime){
        this.name=name;
        //avaibleTime = 當前時間+ delayTime
        this.avaibleTime=delayTime + System.currentTimeMillis();

    }

    @Override
    public long getDelay(TimeUnit unit) {
        //判斷avaibleTime是否大於當前系統時間,並將結果轉換成MILLISECONDS
        long diffTime= avaibleTime- System.currentTimeMillis();
        return unit.convert(diffTime,TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        //compareTo用在DelayedUser的排序
        return (int)(this.avaibleTime - ((DelayedUser) o).getAvaibleTime());
    }
}

上面的對象中,咱們須要實現getDelay和compareTo方法。

接下來咱們建立一個生產者:

@Slf4j
@Data
@AllArgsConstructor
class DelayedQueueProducer implements Runnable {
    private DelayQueue<DelayedUser> delayQueue;

    private Integer messageCount;

    private long delayedTime;

    @Override
    public void run() {
        for (int i = 0; i < messageCount; i++) {
            try {
                DelayedUser delayedUser = new DelayedUser(
                        new Random().nextInt(1000)+"", delayedTime);
                log.info("put delayedUser {}",delayedUser);
                delayQueue.put(delayedUser);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
        }
    }
}

在生產者中,咱們每隔0.5秒建立一個新的DelayedUser對象,併入Queue。

再建立一個消費者:

@Slf4j
@Data
@AllArgsConstructor
public class DelayedQueueConsumer implements Runnable {

    private DelayQueue<DelayedUser> delayQueue;

    private int messageCount;

    @Override
    public void run() {
        for (int i = 0; i < messageCount; i++) {
            try {
                DelayedUser element = delayQueue.take();
                log.info("take {}",element );
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
        }
    }
}

在消費者中,咱們循環從queue中獲取對象。

最後看一個調用的例子:

@Test
    public void useDelayedQueue() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        DelayQueue<DelayedUser> queue = new DelayQueue<>();
        int messageCount = 2;
        long delayTime = 500;
        DelayedQueueConsumer consumer = new DelayedQueueConsumer(
                queue, messageCount);
        DelayedQueueProducer producer = new DelayedQueueProducer(
                queue, messageCount, delayTime);

        // when
        executor.submit(producer);
        executor.submit(consumer);

        // then
        executor.awaitTermination(5, TimeUnit.SECONDS);
        executor.shutdown();

    }

上面的測試例子中,咱們定義了兩個線程的線程池,生產者產生兩條消息,delayTime設置爲0.5秒,也就是說0.5秒以後,插入的對象可以被獲取到。

線程池在5秒以後會被關閉。

運行看下結果:

[pool-1-thread-1] INFO com.flydean.DelayedQueueProducer - put delayedUser DelayedUser(name=917, avaibleTime=1587623188389)
[pool-1-thread-2] INFO com.flydean.DelayedQueueConsumer - take DelayedUser(name=917, avaibleTime=1587623188389)
[pool-1-thread-1] INFO com.flydean.DelayedQueueProducer - put delayedUser DelayedUser(name=487, avaibleTime=1587623188899)
[pool-1-thread-2] INFO com.flydean.DelayedQueueConsumer - take DelayedUser(name=487, avaibleTime=1587623188899)

咱們看到消息的put和take是交替進行的,符合咱們的預期。

若是咱們作下修改,將delayTime修改成50000,那麼在線程池關閉以前插入的元素是不會過時的,也就是說消費者是沒法獲取到結果的。

DelayQueue是一種有奇怪特性的BlockingQueue,能夠在須要的時候使用。

5. 其餘的要點

5.1 Comparable和Comparator的區別

java.lang.Comparable和java.util.Comparator是兩個容易混淆的接口,二者都帶有比較的意思,那麼兩個接口到底有什麼區別,分別在什麼狀況下使用呢?

5.1.1 Comparable

Comparable是java.lang包下面的接口,lang包下面能夠看作是java的基礎語言接口。

實際上Comparable接口只定義了一個方法:

public int compareTo(T o);

實現這個接口的類都須要實現compareTo方法,表示兩個類之間的比較。

這個比較排序以後的order,按照java的說法叫作natural ordering。這個order用在一些可排序的集合好比:SortedSet,SortedMap等等。

當使用這些可排序的集合添加相應的對象時,就會調用compareTo方法來進行natural ordering的排序。

幾乎全部的數字類型對象:Integer, Long,Double等都實現了這個Comparable接口。

5.1.2 Comparator

Comparator是一個FunctionalInterface,須要實現compare方法:

int compare(T o1, T o2);

Comparator在java.util包中,表明其是一個工具類,用來輔助排序的。

在講Comparable的時候,咱們提到Comparable指定了對象的natural ordering,若是咱們在添加到可排序集合類的時候想按照咱們自定義的方式進行排序,這個時候就須要使用到Comparator了。

Collections.sort(List,Comparator),Arrays.sort(Object[],Comparator) 等這些輔助的方法類均可以經過傳入一個Comparator來自定義排序規則。

在排序過程當中,首先會去檢查Comparator是否存在,若是不存在則會使用默認的natural ordering。

還有一個區別就是Comparator容許對null參數的比較,而Comparable是不容許的,不然會爬出NullPointerException。

5.1.3 舉個例子

最後,咱們舉一個natural ordering和Comparator的例子:

@Test
    public void useCompare(){
        List<Integer> list1 = Arrays.asList(5, 3, 2, 4, 1);
        Collections.sort(list1);
        log.info("{}",list1);

        List<Integer> list2 = Arrays.asList(5, 3, 2, 4, 1);
        Collections.sort(list2, (a, b) -> b - a);
        log.info("{}",list2);
    }

輸出結果:

[main] INFO com.flydean.CompareUsage - [1, 2, 3, 4, 5]
[main] INFO com.flydean.CompareUsage - [5, 4, 3, 2, 1]

默認狀況下Integer是按照升序來排的,可是咱們能夠經過傳入一個Comparator來改變這個過程。

5.2 Reference和引用類型

java中有值類型也有引用類型,引用類型通常是針對於java中對象來講的,今天介紹一下java中的引用類型。java爲引用類型專門定義了一個類叫作Reference。Reference是跟java垃圾回收機制息息相關的類,經過探討Reference的實現能夠更加深刻的理解java的垃圾回收是怎麼工做的。

本文先從java中的四種引用類型開始,一步一步揭開Reference的面紗。

java中的四種引用類型分別是:強引用,軟引用,弱引用和虛引用。

5.2.1 強引用Strong Reference

java中的引用默認就是強引用,任何一個對象的賦值操做就產生了對這個對象的強引用。

咱們看一個例子:

public class StrongReferenceUsage {

    @Test
    public void stringReference(){
        Object obj = new Object();
    }
}

上面咱們new了一個Object對象,並將其賦值給obj,這個obj就是new Object()的強引用。

強引用的特性是隻要有強引用存在,被引用的對象就不會被垃圾回收。

5.2.2 軟引用Soft Reference

軟引用在java中有個專門的SoftReference類型,軟引用的意思是隻有在內存不足的狀況下,被引用的對象纔會被回收。

先看下SoftReference的定義:

public class SoftReference<T> extends Reference<T>

SoftReference繼承自Reference。它有兩種構造函數:

public SoftReference(T referent)

和:

public SoftReference(T referent, ReferenceQueue<? super T> q)

第一個參數很好理解,就是軟引用的對象,第二個參數叫作ReferenceQueue,是用來存儲封裝的待回收Reference對象的,ReferenceQueue中的對象是由Reference類中的ReferenceHandler內部類進行處理的。

咱們舉個SoftReference的例子:

@Test
    public void softReference(){
        Object obj = new Object();
        SoftReference<Object> soft = new SoftReference<>(obj);
        obj = null;
        log.info("{}",soft.get());
        System.gc();
        log.info("{}",soft.get());
    }

輸出結果:

22:50:43.733 [main] INFO com.flydean.SoftReferenceUsage - java.lang.Object@71bc1ae4
22:50:43.749 [main] INFO com.flydean.SoftReferenceUsage - java.lang.Object@71bc1ae4

能夠看到在內存充足的狀況下,SoftReference引用的對象是不會被回收的。

5.2.3 弱引用weak Reference

weakReference和softReference很相似,不一樣的是weekReference引用的對象只要垃圾回收執行,就會被回收,而無論是否內存不足。

一樣的WeakReference也有兩個構造函數:

public WeakReference(T referent);

 public WeakReference(T referent, ReferenceQueue<? super T> q);

含義和SoftReference一致,這裏就再也不重複表述了。

咱們看下弱引用的例子:

@Test
    public void weakReference() throws InterruptedException {
        Object obj = new Object();
        WeakReference<Object> weak = new WeakReference<>(obj);
        obj = null;
        log.info("{}",weak.get());
        System.gc();
        log.info("{}",weak.get());
    }

輸出結果:

22:58:02.019 [main] INFO com.flydean.WeakReferenceUsage - java.lang.Object@71bc1ae4
22:58:02.047 [main] INFO com.flydean.WeakReferenceUsage - null

咱們看到gc事後,弱引用的對象被回收掉了。

5.2.4 虛引用PhantomReference

PhantomReference的做用是跟蹤垃圾回收器收集對象的活動,在GC的過程當中,若是發現有PhantomReference,GC則會將引用放到ReferenceQueue中,由程序員本身處理,當程序員調用ReferenceQueue.pull()方法,將引用出ReferenceQueue移除以後,Reference對象會變成Inactive狀態,意味着被引用的對象能夠被回收了。

和SoftReference和WeakReference不一樣的是,PhantomReference只有一個構造函數,必須傳入ReferenceQueue:

public PhantomReference(T referent, ReferenceQueue<? super T> q)

看一個PhantomReference的例子:

@Slf4j
public class PhantomReferenceUsage {

    @Test
    public void usePhantomReference(){
        ReferenceQueue<Object> rq = new ReferenceQueue<>();
        Object obj = new Object();
        PhantomReference<Object> phantomReference = new PhantomReference<>(obj,rq);
        obj = null;
        log.info("{}",phantomReference.get());
        System.gc();
        Reference<Object> r = (Reference<Object>)rq.poll();
        log.info("{}",r);
    }
}

運行結果:

07:06:46.336 [main] INFO com.flydean.PhantomReferenceUsage - null
07:06:46.353 [main] INFO com.flydean.PhantomReferenceUsage - java.lang.ref.PhantomReference@136432db

咱們看到get的值是null,而GC事後,poll是有值的。

由於PhantomReference引用的是須要被垃圾回收的對象,因此在類的定義中,get一直都是返回null:

public T get() {
        return null;
    }

5.2.5 Reference和ReferenceQueue

講完上面的四種引用,接下來咱們談一下他們的父類Reference和ReferenceQueue的做用。

Reference是一個抽象類,每一個Reference都有一個指向的對象,在Reference中有5個很是重要的屬性:referent,next,discovered,pending,queue。

private T referent;         /* Treated specially by GC */
volatile ReferenceQueue<? super T> queue;
Reference next;
transient private Reference<T> discovered;  /* used by VM */
private static Reference<Object> pending = null;

每一個Reference均可以當作是一個節點,多個Reference經過next,discovered和pending這三個屬性進行關聯。

先用一張圖來對Reference有個總體的概念:

referent就是Reference實際引用的對象。

經過next屬性,能夠構建ReferenceQueue。

經過discovered屬性,能夠構建Discovered List。

經過pending屬性,能夠構建Pending List。

  • 四大狀態

在講這三個Queue/List以前,咱們先講一下Reference的四個狀態:

從上面的圖中,咱們能夠看到一個Reference能夠有四個狀態。

由於Reference有兩個構造函數,一個帶ReferenceQueue,一個不帶。

Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

對於帶ReferenceQueue的Reference,GC會把要回收對象的Reference放到ReferenceQueue中,後續該Reference須要程序員本身處理(調用poll方法)。

不帶ReferenceQueue的Reference,由GC本身處理,待回收的對象其Reference狀態會變成Inactive。

建立好了Reference,就進入active狀態。

active狀態下,若是引用對象的可到達狀態發送變化就會轉變成Inactive或Pending狀態。

Inactive狀態很好理解,到達Inactive狀態的Reference狀態不能被改變,會等待GC回收。

Pending狀態表明等待入Queue,Reference內部有個ReferenceHandler,會調用enqueue方法,將Pending對象入到Queue中。

入Queue的對象,其狀態就變成了Enqueued。

Enqueued狀態的對象,若是調用poll方法從ReferenceQueue拿出,則該Reference的狀態就變成了Inactive,等待GC的回收。

這就是Reference的一個完整的生命週期。

  • 三個Queue/List

有了上面四個狀態的概念,咱們接下來說三個Queue/List:ReferenceQueue,discovered List和pending List。

ReferenceQueue在講狀態的時候已經講過了,它本質是由Reference中的next鏈接而成的。用來存儲GC待回收的對象。

pending List就是待入ReferenceQueue的list。

discovered List這個有點特別,在Pending狀態時候,discovered List就等於pending List。

在Active狀態的時候,discovered List實際上維持的是一個引用鏈。經過這個引用鏈,咱們能夠得到引用的鏈式結構,當某個Reference狀態再也不是Active狀態時,須要將這個Reference從discovered List中刪除。

5.2.6 WeakHashMap

最後講一下WeakHashMap,WeakHashMap跟WeakReference有點相似,在WeakHashMap若是key再也不被使用,被賦值爲null的時候,該key對應的Entry會自動從WeakHashMap中刪除。

咱們舉個例子:

@Test
    public void useWeakHashMap(){
        WeakHashMap<Object, Object> map = new WeakHashMap<>();
        Object key1= new Object();
        Object value1= new Object();
        Object key2= new Object();
        Object value2= new Object();

        map.put(key1, value1);
        map.put(key2, value2);
        log.info("{}",map);

        key1 = null;
        System.gc();
        log.info("{}",map);

    }

輸出結果:

[main] INFO com.flydean.WeakHashMapUsage - {java.lang.Object@14899482=java.lang.Object@2437c6dc, java.lang.Object@11028347=java.lang.Object@1f89ab83}
[main] INFO com.flydean.WeakHashMapUsage - {java.lang.Object@14899482=java.lang.Object@2437c6dc}

能夠看到gc事後,WeakHashMap只有一個Entry了。

5.3 類型擦除type erasure

泛型是java從JDK 5開始引入的新特性,泛型的引入可讓咱們在代碼編譯的時候就強制檢查傳入的類型,從而提高了程序的健壯度。

泛型能夠用在類和接口上,在集合類中很是常見。本文將會講解泛型致使的類型擦除。

5.3.1 舉個例子

咱們先舉一個最簡單的例子:

@Slf4j
public class TypeErase {

    public static void main(String[] args) {
        ArrayList<String> stringArrayList = new ArrayList<String>();
        stringArrayList.add("a");
        stringArrayList.add("b");
        action(stringArrayList);
    }

    public static void action(ArrayList<Object> al){
        for(Object o: al)
            log.info("{}",o);
    }
}

上面的例子中,咱們定義了一個ArrayList,其中指定的類型是String。

而後調用了action方法,action方法須要傳入一個ArrayList,可是這個list的類型是Object。

乍看之下好像沒有問題,由於String是Object的子類,是能夠進行轉換的。

可是實際上代碼編譯出錯:

Error:(18, 16) java: 不兼容的類型: java.util.ArrayList<java.lang.String>沒法轉換爲java.util.ArrayList<java.lang.Object>

5.3.2 緣由

上面例子的緣由就是類型擦除(type erasure)。java中的泛型是在編譯時作檢測的。而編譯後生成的二進制文件中並不保存類型相關的信息。

上面的例子中,編譯以後無論是ArrayList<String> 仍是ArrayList<Object> 都會變成ArrayList。其中的類型Object/String對JVM是不可見的。

可是在編譯的過程當中,編譯器發現了二者的類型不一樣,而後拋出了錯誤。

5.3.3 解決辦法

要解決上面的問題,咱們可使用下面的辦法:

public static void actionTwo(ArrayList<?> al){
        for(Object o: al)
            log.info("{}",o);
    }

經過使用通配符?,能夠匹配任何類型,從而經過編譯。

可是要注意這裏actionTwo方法中,由於咱們不知道傳入的類型究竟是什麼,因此咱們不能在actionTwo中添加任何元素。

5.3.4 總結

從上面的例子咱們能夠看出,ArrayList<String>並非ArrayList<Object>的子類。若是必定要找出父子關係,那麼ArrayList<String>是Collection<String>的子類。

可是Object[] objArray是String[] strArr的父類。由於對Array來講,其具體的類型是已知的。

5.4 深刻理解java的泛型

泛型是JDK 5引入的概念,泛型的引入主要是爲了保證java中類型的安全性,有點像C++中的模板。

可是Java爲了保證向下兼容性,它的泛型所有都是在編譯期間實現的。編譯器執行類型檢查和類型推斷,而後生成普通的非泛型的字節碼。這種就叫作類型擦除。編譯器在編譯的過程當中執行類型檢查來保證類型安全,可是在隨後的字節碼生成以前將其擦除。

這樣就會帶來讓人困惑的結果。本文將會詳細講解泛型在java中的使用,以免進入誤區。

5.4.1 泛型和協變

有關協變和逆變的詳細說明能夠參考:

深刻理解協變和逆變

這裏我再總結一下,協變和逆變只有在類型聲明中的類型參數裏纔有意義,對參數化的方法沒有意義,由於該標記影響的是子類繼承行爲,而方法沒有子類。

固然java中沒有顯示的表示參數類型是協變仍是逆變。

協變意思是若是有兩個類 A<T> 和 A<C>, 其中C是T的子類,那麼咱們能夠用A<C>來替代A<T>。

逆變就是相反的關係。

Java中數組就是協變的,好比Integer是Number的子類,那麼Integer[]也是 Number[]的子類,咱們能夠在須要 Number[] 的時候傳入 Integer[]。

接下來咱們考慮泛型的狀況,List<Number> 是否是 List<Integer>的父類呢?很遺憾,並非。

咱們得出這樣一個結論:泛型不是協變的。

爲何呢?咱們舉個例子:

List<Integer> integerList = new ArrayList<>();
        List<Number> numberList = integerList; // compile error
        numberList.add(new Float(1.111));

假如integerList能夠賦值給numberList,那麼numberList能夠添加任意Number類型,好比Float,這樣就違背了泛型的初衷,向Integer list中添加了Float。因此上面的操做是不被容許的。

剛剛咱們講到Array是協變的,若是在Array中帶入泛型,則會發生編譯錯誤。好比new List<String>[10]是不合法的,可是 new List<?>[10]是能夠的。由於在泛型中?表示的是未知類型。

List<?>[] list1 = new List<?>[10];

List<String>[] list2 = new List<String>[10]; //compile error

5.4.2 泛型在使用中會遇到的問題

由於類型擦除的緣由,List<String>和List<Integer>在運行是都會被當作成爲List。因此咱們在使用泛型時候的一些操做會遇到問題。

假如咱們有一個泛型的類,類中有一個方法,方法的參數是泛型,咱們想在這個方法中對泛型參數進行一個拷貝操做。

public class CustUser<T> {

    public void useT(T param){
        T copy = new T(param);  // compile error
    }
}

上面操做會編譯失敗,由於咱們並不知道T是什麼,也不知道T到底有沒有相應的構造函數。

直接clone T是沒有辦法了,若是咱們想copy一個Set,set中的類型是未定義的該怎麼作呢?

public void useTSet(Set<?> set){
        Set<?> copy1 = new HashSet<?>(set);  // compile error
        Set<?> copy2 = new HashSet<>(set);
        Set<?> copy3 = new HashSet<Object>(set);  
    }

能夠看到?是不能直接用於實例化的。可是咱們能夠用下面的兩種方式代替。

再看看Array的使用:

public void useArray(){
         T[] typeArray1= new T[20];  //compile error
        T[] typeArray2=(T[]) new Object[20];
        T[] typeArray3 = (T[]) Array.newInstance(String.class, 20);
    }

一樣的,T是不能直接用於實例化的,可是咱們能夠用下面兩種方式代替。

5.4.3 類型擦除要注意的事項

由於類型擦除的緣由,咱們在接口實現中,實現同一個接口的兩個不一樣類型是無心義的:

public class someClass implements Comparable<Number>, Comparable<String> { ... } // no

由於在編譯事後的字節碼看來,兩個Comparable是同樣的。

一樣的,咱們使用T來作類型強制轉換也是沒有意義的:

public <T> T cast(T t, Object o) { return (T) o; }

由於編譯器並不知道這個強制轉換是對仍是錯。

總結

集合是java中一個很是重要的工具類型,但願你們可以熟練掌握。

本文的代碼例子https://github.com/ddean2009/learn-java-collections

本文的PDFjava-collection-all-in-one.pdf

本文已收錄於 http://www.flydean.com/java-collection-all-in-one/

最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

歡迎關注個人公衆號:「程序那些事」,懂技術,更懂你!

相關文章
相關標籤/搜索