常用的單列集合對象(Collection)實現原理詳解

1、概念

集合:存儲對象的容器。java面向對象的語言,對事物都已對象的形式來描述,所以爲了對多個對象進行操作存儲,集合是存儲對象常用的方法;

2、集合與數組的區別

相同點
1、集合與數組都是容器;
異同點
1、數組的長度是固定,而集合長度可變;
2、數組可以存儲基本數據類型,集合只能存儲對象數據;
3、數組存儲數據類型是單一的,集合可以存儲任意類型的對象;

3、集合的繼承關係(分類):

————| Collection:是所有單列集合的父類
——————| List : 有序,可重複的(按插入順序排序);
————————| ArrayList:內部維護了object[ ] 數組,查詢塊,增刪慢;
————————| LinkedList:鏈表數據結構,增刪塊,查詢慢,線性不安全;
————————| Vector:與ArrayList原理安全,但是同步的,線程安全,效率較低
——————| Set : 無序,不可重複;
————————| HashSet:存取速度快,線程不安全,底層是哈希表實現;
————————| TreeSet:紅-黑樹的數據結構,對具有自然順序的數據進行自然排序,

4、ArrayList解析

4.1、ArrayList原理

我們先看一下源碼:
這是ArrayList()無參的構造方法:

 /**
 /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

上面的代碼我們可以看到,聲明一個無參的ArrayList()對象,其實只是聲明瞭一個空的object數組,並且沒有指定長度,那麼長度是在什麼時候指定的尼?我們來看他的add()方法:

    /**
     * Default initial capacity.
     */
 private static final int DEFAULT_CAPACITY = 10;
 
 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

從上面的那一串的源碼我們可以解析出,聲明的ArrayList()對象是在調用add()方法時才進行長度聲明的。並且在每次調用add()方法時,都會進行一連串的數組長度判斷,如果長度不夠時,進行動態擴容,擴容後的大小:int newCapacity = oldCapacity + (oldCapacity >> 1);也就是原來的0.5倍。實際上的擴容都是生成一個長度爲newCapacity的新數組,再調用Arrays.copyOf()方法,將原來數組中的內容複製到新數組中去(複製數組這一過程是比較耗時的),之後再添加新元素;以上也正是爲什麼ArrayList()新增慢的原因了;

那爲什麼ArrayList對象的查詢是比較快呢?
ArrayList查詢塊是與數組的特性有關係的,
那說一下數組的特性:數組裏元素與元素之間的內存地址是連續的;
也就是說,在ArrayList裏,我要查詢第100的那一個元素,那麼直接將第一個元素的內存地址加上100就可以得到第一百的那一個元素了;

爲什麼ArrayList對象的刪除比較慢呢?

 public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

看源碼,在remove()方法執行裏都有複製數組這一操作在裏面,而這一操作是比較耗時的;之所以進行這一操作是因爲,舉個例子:假如刪除ArrayList裏面居中的某個元素,那麼數組中間就會有一個空格,這是會將這一元素的右邊的其他元素向左移動,補全移除後的空格,這一動作也就是複製數組了,所以耗時長。

4.2、ArrayList總結:

ArrayList的數據結構是一個Object[ ],使用無參的構造方法聲明一個ArrayList()的時候,默認分配的長度是10,當長度不夠時,可以動態擴容,擴容量爲原來的0.5倍;
ArrayList的增刪較慢是因爲這一過程需要進行一系列的數組長度判斷以及複製數組的操作在裏面;
ArrayList的查詢塊是因爲數組的特性:數組裏元素與元素之間的內存地址是連續的,所以查詢第某個元素,直接第一個元素的內存地址加上第多少個的排序就可以了;

5、LinkedList解析

5.1、基本概念

LinkedList:是雙鏈表的數據結構,將一個單元分成兩個部分,一個部分存儲元素,一部分存放下一個元素的內存地址。具有增刪塊,查詢慢的特點;
查詢:LinkedList裏元素的內存地址並不連續,需要上一個元素記住下一個元素的地址,查詢的時候需要從頭往下找(迭代),明顯沒有數組查詢塊。
新增:鏈表在插入元素時,直接讓上一個元素記住新元素的地址,讓新元素記住下一個元素的地址,這樣插入就快。
刪除:刪除時讓前一個元素記住後一個元素, 後一個元素記住前一個元素. 這樣的刪效率較高。

5.2、圖解LinkedList增刪數據

5.2.1、一般的順序添加:
在這裏插入圖片描述
根據添加順序,在狗娃裏存放狗蛋的內存地址,狗蛋裏存放鐵蛋的內存地址,以此類推,,,
5.2.2、刪除元素的圖解
在這裏插入圖片描述
5.2.3、兩元素之間插入數據
在這裏插入圖片描述

5.3、LinkedList總結

LinkedList是採用雙向鏈表實現存儲,按序號索引數據需要進行前向或後向遍歷,但是插入數據時只需要記錄本項的前後項即可,所以插入速度較快,哦,LinkedList是線性不安全的;

6、Vector解析(瞭解即可)

6.1、基本概念

Vector :底層與ArrayList一樣,也是維護了object[ ] 數組,實現方法也和ArrayList一樣,但是Vector是線性安全的,效率較低;

6.2、Vector和ArrayList的異同

相同點
內部都是維護了object[ ] 數組;
異同點
1、Vector是多線性安全的,效率較低;
2、ArrayList是單線程不同步的,效率較高;
3、Vector是JDK1.0出現的,而ArrayList是JDK1.2出現的;

6.3、總結

Vector描述的是一個線程安全的ArrayList。

7、HashSet解析

7.1、基本概念

HashSet底層是哈希表來實現的,元素不可重複,線程不同步,但是存取速度快;

7.2、運作原理

爲什麼HashSet存取速度快?
哈希表存放的是哈希值,HashSet存儲元素的順序不是按照插入時的順序來存儲的,而是根據哈希值來存儲,獲取元素也是按照哈希值來進行獲取的。正是因爲按照哈希值來存取元素,所以速度快;
HashSet是怎麼判斷元素爲重複元素的?
首先介紹一下哈希表的特點:桶式結構,就是哈希表的一個存儲位置可以存儲多個元素;
HashSet存放元素時,會先調用元素的hashCode()方法,獲取到元素的哈希值(默認爲內存地址),再經過哈希特有移位等運算就可以得到元素在哈希表中的存儲位置了;
得到元素在哈希表中的存儲位置之後,還發兩種情況
1、如果該存儲位置沒有其他的元素,則直接將該元素存放在該位置上;
2、如果該存儲位置有其他的元素,這是調用元素的equals()方法,判斷兩個元素是否相同。如果equals()方法返回true,則視爲該元素與這個位置上的其他元素重複,不進行存儲。如果equals()方法返回false,則直接在這個位置上存放該元素;
所以用HashSet存儲自定義數據類型的時候,得重寫對象的hashCode()equals()方法

下面舉一個例子:
自定義一個Book類,並且重寫了它的hashCode()equals()(使用id來判斷是否相同)方法,如下:

public class Book implements Comparable<Book>{
 	String name;
	int id;
	int price;
	
    public Book(String name, int id) {
		this.name = name;
		this.id = id;
	}
    @Override
	public int hashCode() {
		return super.hashCode();
	}
	@Override
	public boolean equals(Object obj) {
		Book book = (Book) obj;
		return this.getId() == book.getId();
	}
}

然後再上一張圖,來模擬存儲的過程:
在這裏插入范德薩描述z

7.3、注意要點

1、HashSet判斷元素是否相同,先調用的是hashCode()方法,再者纔有可能調用equals()方法(hashCode方法返回值相同,調用equals,否則不調用);
2、HashSet與ArrayList都有boolean contains(Object o)方法,但是HashSet使用hashCode和equals方法,ArrayList使用了equals方法。

8、TreeSet解析

8.1、基本概念

TreeSet採用的是紅-黑樹的數據結構,使用元素的自然順序對元素進行排序,自然順序比如:‘a’,“a”,1,…
當然,如果元素不具備自然順序,那麼對元素進行排序就有兩個方法:
方法一:讓該元素所屬的類得實現Comparable接口,重寫compareTo方法,指定比較規則,根據 compareTo方法的返回結果來進行判斷大小;
方法二:在創建TreeSet對象的時候在構造方法裏傳入一個比較器,根據 比較器返回結果來進行判斷大小,比較器的規則如下:
自定義一個類實現Comparator接口即可,把元素與元素之間的比較規則定義在compare方法內即可。
自定義比較器的格式 :

class  類名  implements Comparator{
 	   @Override
	   public int compare(Object o1, Object o2) {
	    。。。
	   }
 }

無論是方式一,還是方式二,方法的返回值都是有以下幾種結果:
1、返回值爲負整數、零或正整數,根據此來分別判斷是小於、等於還是大於指定對象;
2、如果返回爲0,(等於),則判斷次元素爲重複元素,不得添加。

8.2、例子

8.2.1、方式一例子

創建一個Book類,實現了Comparable接口,重寫了compareTo方法,並且在compareTo裏打印出了比較過程:

public class Book implements Comparable<Book>{
	private String name;
	private int id;
	private int price;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}
	@Override
	public String toString() {
		return "Book {編號:" + id +",書名:" + name + ",價格:"+price+ "}";
	}
	public Book(String name, int id) {
		this.name = name;
		this.id = id;
	}
	public Book(int id,String name,int price) {
		this.name = name;
		this.id = id;
		this.price = price;
	}
	@Override
	public boolean equals(Object obj) {
		Book book = (Book) obj;
		return this.getId() == book.getId();
	}
	@Override
	public int hashCode() {
		return super.hashCode();
	}
	@Override
	public int compareTo(Book bk) {
		System.out.println(this.name+"--->"+bk.name);
		return this.getId()-bk.getId();
	}
}

接下來是主方法:

public class TreeSetDemo {
	public static void main(String[] args) {
		Set<Book> tree = new TreeSet<Book>();
		tree.add(new Book(120,"敏捷開發",500));
		tree.add(new Book(110,"極限編程",550));
		tree.add(new Book(123,"消息隊列",360));
		tree.add(new Book(155,"編程思想",555));
		tree.add(new Book(985,"數據結構",752));
	}
}

來看一下重寫的compareTo()方法,可以得到是使用Bookid來排序的:

@Override
	public int compareTo(Book bk) {
		System.out.println(this.name+"--->"+bk.name);
		return this.getId()-bk.getId();
	}

主線程的運行結果看下圖:
在這裏插入圖片描述
運行的大致過程如下:
1、首先添加敏捷開發,自己與自己比較之後,作爲根節點;
2、再添加極限編程極限編程id=110小於根節點敏捷開發的id=120,根據紅-黑樹的規則:左小右大,所以極限編程放在敏捷開發的左邊;
3、接下來添加消息隊列消息隊列id=123大於根節點敏捷開發的id=120,根據紅-黑樹的規則:左小右大,所以消息隊列放在敏捷開發的右邊;
4、緊接着添加編程思想編程思想id=156大於根節點敏捷開發的id=120,根據紅-黑樹的規則:左小右大,所以消息隊列放在敏捷開發的右邊。之後編程思想,id=156再與消息隊列,id=123進行比較,由於編程思想大於消息隊列,所以編程思想放在消息隊列的右邊;
5、之後添加數據結構,id=985數據結構,id=985大於根節點敏捷開發,id=120,放在敏捷開發的右邊,再與消息隊列比較,大於消息隊列,放在消息隊列的右邊。然後再跟編程思想比較,並大於編程思想,放在編程思想的右邊;
注意:TreeSet每添加一個元素,都是從根節點開始比較的,另外紅-黑樹是能自動的調節根節點的;

8.2.2、方式二例子

創建一個Book類,不實現了Comparable接口。再自定義一個MyCompare類並實現Comparator,重寫compare方法:

class MyCompare implements Comparator<Book>{

	@Override
	public int compare(Book o1, Book o2) {
		if (o1.getPrice() == o2.getPrice()) {
			return o1.getId() - o2.getId();
		}
		return o1.getPrice() - o2.getPrice();
	}
}

在這一個compare方法裏可以看到,主要是使用price來進行排序,如果有價格相等的,再使用id進行排序
接下來再看主線程

public class TreeSetDemo {
	public static void main(String[] args) {
		MyCompare compare = new MyCompare();
		Set<Book> tree = new TreeSet<Book>(compare);
		tree.add(new Book(120,"敏捷開發",500));
		tree.add(new Book(110,"極限編程",550));
		tree.add(new Book(123,"消息隊列",360));
		tree.add(new Book(155,"編程思想",555));
		tree.add(new Book(985,"數據結構",752));
	}
}

在主線程裏將MyCompare對象作爲實參放在TreeSet的構造器裏;
它的運行過程與方式一差不多,所以此處略過

8.3、注意要點

1、紅-黑樹(二叉樹)的規則是左小右大
2、TreeSet集合裏添加元素時,如果元素本身不具備自然順序的特性,而元素所屬的類已經實現了Comparable接口, 在創建TreeSet對象的時候也傳入了比較器那麼是以比較器的比較規則優先使用。
3、在重寫compareTo或者compare方法時,必須要明確比較的主要條件相等時要比較次要條件。(如果只是比較書的價格,當兩本書的價格相同時,之後添加的那一本將被視爲重複,這不符合規律。所以還要準備次要的條件,比如書的編號,當價格相同時,再比較編號)
4、一般是推薦使用比較器(Comparator)的,因爲複用性較強。
5、TreeSet與hashCode()equals()方法是沒有任何關係。

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