夜光:Java成神之路(七)擅長的語言

夜光序言:

 

 

勝利是一切,不知何時已經成爲使命,勝利的一方永遠是對的,我不允許我的人生有敗北兩字。

 

 

 

 

 
 
正文:
 
                                              以道御術 / 以術識道



 

2. 集合的安全性問題

 

請問 ArrayList、HashSet、HashMap 是線程安全的嗎?如果不是我想要線程安全的集合怎麼辦?
 
 
 
我們都看過上面那些集合的源碼(如果沒有那就看看吧),每個方法都沒有加鎖,顯然都是線程不安全的。
 
話又說過來如果他們安全了也就沒第二問了。
 
在集合中 Vector 和 HashTable 倒是線程安全的。你打開源碼會發現其實就是把各自核心方法添加上了synchronized 關鍵字。
 
 
 
Collections 工具類提供了相關的 API,可以讓上面那 3 個不安全的集合變爲安全的。
 
// Collections.synchronizedCollection(c)
// Collections.synchronizedList(list)
// Collections.synchronizedMap(m)
// Collections.synchronizedSet(s)
上面幾個函數都有對應的返回值類型,傳入什麼類型返回什麼類型。
 
 
打開源碼其實實現原理非常簡單,就是將集合的核心方法添加上了 synchronized 關鍵字

 


3. ArrayList 內部用什麼實現的?

 

(回答這樣的問題,不要只回答個皮毛,可以再介紹一下 ArrayList 內部是如何實現數組的增加和刪除的,因爲數組在創建的時候長度是固定的,那麼就有個問題我們往 ArrayList 中不斷的添加對象,它是如何管理這些數組呢?)

 

ArrayList 內部是用 Object[]實現的。
 
 
接下來我們分別分析 ArrayList 的構造、add、remove、clear 方法的實現原理

 

一、構造函數

 

1)空參構造

/**
 * Constructs a new {@code ArrayList} instance with zero initial capacity.
 */
 public ArrayList() {
 array = EmptyArray.OBJECT;
}
array 是一個 Object[]類型。當我們 new 一個空參構造時系統調用了 EmptyArray.OBJECT 屬性
 

 

EmptyArray 僅僅是一個系統的類庫,該類源碼如下:

 
public final class EmptyArray {
 private EmptyArray() {}
 public static final boolean[] BOOLEAN = new boolean[0];
 public static final byte[] BYTE = new byte[0];
 public static final char[] CHAR = new char[0];
 public static final double[] DOUBLE = new double[0];
 public static final int[] INT = new int[0];
 public static final Class<?>[] CLASS = new Class[0];
 public static final Object[] OBJECT = new Object[0];
 public static final String[] STRING = new String[0];
 public static final Throwable[] THROWABLE = new Throwable[0];
 public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}
也就是說當我們 new 一個空參 ArrayList 的時候,系統內部使用了一個 new Object[0]數組
 

2)帶參構造 1

/**
 * Constructs a new instance of {@code ArrayList} with the specified
 * initial capacity.
 *
 * @param capacity
 * the initial capacity of this {@code ArrayList}.
 */
 public ArrayList(int capacity) {
 if (capacity < 0) {
 throw new IllegalArgumentException("capacity < 0: " + capacity);
}
 array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}

 

3)帶參構造 2

/**
 * Constructs a new instance of {@code ArrayList} containing the elements of
 * the specified collection.
 *
 * @param collection
 * the collection of elements to add.
 */
 public ArrayList(Collection<? extends E> collection) {
 if (collection == null) {
 throw new NullPointerException("collection == null");
 }
 Object[] a = collection.toArray();
 if (a.getClass() != Object[].class) {
 Object[] newArray = new Object[a.length];
 System.arraycopy(a, 0, newArray, 0, a.length);
 a = newArray;
 }
 array = a;
 size = a.length;
 }
如果調用構造函數的時候傳入了一個 Collection 的子類,那麼先判斷該集合是否爲 null,爲 null 則拋出空指針異常。如果不是則將該集合轉換爲數組 a,然後將該數組賦值爲成員變量 array,將該數組的長度作爲成員變量 size。
 
 
裏面它先判斷 a.getClass 是否等於 Object[].class,其實一般都是相等的,我也暫時沒想明白爲什麼多加了這個判斷, toArray 方法是 Collection 接口定義的,因此其所有的子類都有這樣的方法,list 集合的 toArray 和 Set 集合的 toArray 返回的都是 Object[]數組。
 
 
 

這裏講些題外話,其實在看 Java 源碼的時候,作者的很多意圖都很費人心思,我能知道他的目標是啥,但是不知道他爲何這樣寫。比如對於 ArrayList, array 是他的成員變量

 

但是每次在方法中使用該成員變量的時候作者都會重新在方法中開闢一個局部變量,然後給局部變量賦值爲 array,然後再使用,有人可能說這是爲了防止併發修改 array

 

畢竟 array 是成員變量,大家都可以使用因此需要將 array 變爲局部變量,然後再使用,這樣的說法並不是都成立的,也許有時候就是老外們寫代碼的一個習慣而已。

 


 

二、add 方法

add 方法有兩個重載,這裏只研究最簡單的那個。

/**
 * Adds the specified object at the end of this {@code ArrayList}.
 *
 * @param object
 * the object to add.
 * @return always true
 */
 @Override public boolean add(E object) {
 Object[] a = array;
 int s = size;
 if (s == a.length) {
 Object[] newArray = new Object[s +
 (s < (MIN_CAPACITY_INCREMENT / 2) ?
 MIN_CAPACITY_INCREMENT : s >> 1)];
 System.arraycopy(a, 0, newArray, 0, s);
 array = a = newArray;
 }
 a[s] = object;
 size = s + 1;
 modCount++;
 return true;
 }
1、首先將成員變量 array 賦值給局部變量 a,將成員變量 size 賦值給局部變量 s。
 

2、判斷集合的長度 s 是否等於數組的長度(如果集合的長度已經等於數組的長度了,說明數組已經滿了,該重新分配新數組了),重新分配數組的時候需要計算新分配內存的空間大小,如果當前的長度小於 MIN_CAPACITY_INCREMENT/2(這個常量值是 12,除以 2 就是 6,也就是如果當前集合長度小於 6)則分配 12 個長度,如果集合長度大於 6 則分配當前長度 s 的一半長度。

 

這裏面用到了三元運算符和位運算,s >> 1,意思就是將 s 往右移 1 位,相當於 s=s/2,只不過位運算是效率最高的運算。

 

3、將新添加的 object 對象作爲數組的 a[s]個元素
 
4、修改集合長度 size 爲 s+1
 
5、modCotun++,該變量是父類中聲明的,用於記錄集合修改的次數,記錄集合修改的次數是爲了防止在用迭代器迭代集合時避免併發修改異常,或者說用於判斷是否出現併發修改異常的。
 
6、return true,這個返回值意義不大,因爲一直返回 true,除非報了一個運行時異常。
 

 


 

三、remove 方法

 

remove 方法有兩個重載,我們只研究 remove(int index)方法。

/**
 * Removes the object at the specified location from this list.
 *
 * @param index
 * the index of the object to remove.
 * @return the removed object.
 * @throws IndexOutOfBoundsException
 * when {@code location < 0 || location >= size()}
 */
 @Override public E remove(int index) {
 Object[] a = array;
 int s = size;
 if (index >= s) {
 throwIndexOutOfBoundsException(index, s);
 }
 @SuppressWarnings("unchecked") 
 E result = (E) a[index];
 System.arraycopy(a, index + 1, a, index, --s - index);
 a[s] = null; // Prevent memory leak
 size = s;
 modCount++;
 return result;
 }

 

1、先將成員變量 array 和 size 賦值給局部變量 a 和 s。
 
2、判斷形參 index 是否大於等於集合的長度,如果成了則拋出運行時異常
 
3、獲取數組中腳標爲 index 的對象 result,該對象作爲方法的返回值

 

4、調用 System 的 arraycopy 函數,拷貝原理如下圖所示。

 
 

5、接下來就是很重要的一個工作,因爲刪除了一個元素,而且集合整體向前移動了一位,因此需要將集合最後一個元素設置爲 null,否則就可能內存泄露。

 

6、重新給成員變量 array 和 size 賦值

 
7、記錄修改次數
 
8、返回刪除的元素(讓用戶再看最後一眼)
 
 

 

四、clear 方法

/**
 * Removes all elements from this {@code ArrayList}, leaving it empty.
 *
 * @see #isEmpty
 * @see #size
 */
 @Override public void clear() {
 if (size != 0) {
 Arrays.fill(array, 0, size, null);
 size = 0;
 modCount++;
 }
 }

 

如果集合長度不等於 0,則將所有數組的值都設置爲 null,然後將成員變量 size 設置爲 0 即可,最後讓修改記錄加 1

 

 

4. 併發集合和普通集合如何區別?

 
併發集合常見的有 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque 等。
 
併發集合位 於 java.util.concurrent 包 下 , 是 jdk1.5 之 後 才 有 的 , 主 要 作 者 是 Doug Lea http://baike.baidu.com/view/3141057.htm)完成的。

 

在 java 中有普通集合、同步(線程安全)的集合、併發集合。

 
普通集合通常性能最高,但是不保證多線程的安全性和併發的可靠性。
 
線程安全集合僅僅是給集合添加了 synchronized 同步鎖,嚴重犧牲了性能,而且對併發的效率就更低了,併發集合則通過複雜的策略不僅保證了多線程的安全又提高的併發時的效率。
 
 

 

參考閱讀:

ConcurrentHashMap 是線程安全的 HashMap 的實現,默認構造同樣有 initialCapacity 和 loadFactor 屬性,不過還多了一個 concurrencyLevel 屬性,三屬性默認值分別爲 16、0.75 及 16。
 
其內部使用鎖分段技術,維持這鎖Segment 的數組,在 Segment 數組中又存放着 Entity[]數組,內部 hash 算法將數據較均勻分佈在不同鎖中。
 
put 操作:並沒有在此方法上加上 synchronized,首先對 key.hashcode 進行 hash 操作,得到 key 的 hash 值。
 
hash操作的算法和 map也不同,根據此 hash 值計算並獲取其對應的數組中的 Segment對象(繼承自ReentrantLock),接着調用此 Segment 對象的 put 方法來完成當前操作。
 
ConcurrentHashMap 基於 concurrencyLevel 劃分出了多個 Segment 來對 key-value 進行存儲,從而避免每次 put 操作都得鎖住整個數組。在默認的情況下,最佳情況下可允許 16 個線程併發無阻塞的操作集合對象,儘可能地減少併發時的阻塞現象。
 
get(key)
 
首先對 key.hashCode 進行 hash 操作,基於其值找到對應的 Segment 對象,調用其 get 方法完成當前操作。
 
而 Segment 的 get 操作首先通過 hash 值和對象數組大小減 1 的值進行按位與操作來獲取數組上對應位置的HashEntry。
 
 
在這個步驟中,可能會因爲對象數組大小的改變,以及數組上對應位置的 HashEntry 產生不一致性,那麼 ConcurrentHashMap 是如何保證的?
 
對象數組大小的改變只有在 put 操作時有可能發生,由於 HashEntry 對象數組對應的變量是 volatile 類型的,因此可以保證如 HashEntry 對象數組大小發生改變,讀操作可看到最新的對象數組大小。
 
在獲取到了 HashEntry 對象後,怎麼能保證它及其 next 屬性構成的鏈表上的對象不會改變呢?這點ConcurrentHashMap 採用了一個簡單的方式,即 HashEntry 對象中的 hash、key、next 屬性都是 final 的,這也就意味着沒辦法插入一個 HashEntry 對象到基於 next 屬性構成的鏈表中間或末尾。
 
這樣就可以保證當獲取到 HashEntry對象後,其基於 next 屬性構建的鏈表是不會發生變化的。

 

ConcurrentHashMap 默認情況下采用將數據分爲 16 個段進行存儲,並且 16 個段分別持有各自不同的鎖 Segment,鎖僅用於 put 和 remove 等改變集合對象的操作,基於 volatile 及 HashEntry 鏈表的不變性實現了讀取的不加鎖。
 
 
這些方式使得 ConcurrentHashMap 能夠保持極好的併發支持,尤其是對於讀遠比插入和刪除頻繁的 Map 而言,而它採用的這些方法也可謂是對於 Java 內存模型、併發機制深刻掌握的體現。
 
 

 

5. List 的三個子類的特點

ArrayList 底層結構是數組,底層查詢快,增刪慢。
 
LinkedList 底層結構是鏈表型的,增刪快,查詢慢。
 
voctor 底層結構是數組 線程安全的,增刪慢,查詢慢。
 
 

 

6. List 和 Map、Set 的區別

 
 

6.1 結構特點

 
List 和 Set 是存儲單列數據的集合,Map 是存儲鍵和值這樣的雙列數據的集合;List 中存儲的數據是有順序,並且允許重複;
 
Map 中存儲的數據是沒有順序的,其鍵是不能重複的,它的值是可以有重複的,Set 中存儲的數據是無序的,且不允許有重複,但元素在集合中的位置由元素的 hashcode 決定,位置是固定的(Set 集合根據 hashcode 來進行數據的存儲,所以位置是固定的,但是位置不是用戶可以控制的,所以對於用戶來說 set 中的元素還是無序的);

 

 

6.2 實現類

 
List 接口有三個實現類(LinkedList:基於鏈表實現,鏈表內存是散亂的,每一個元素存儲本身內存地址的同時還存儲下一個元素的地址。鏈表增刪快,查找慢;ArrayList:基於數組實現,非線程安全的,效率高,便於索引,但不便於插入刪除;Vector:基於數組實現,線程安全的,效率低)。
 
Map 接口有三個實現類(HashMap:基於 hash 表的 Map 接口實現,非線程安全,高效,支持 null 值和 null鍵;HashTable:線程安全,低效,不支持 null 值和 null 鍵;
 
LinkedHashMap:是 HashMap 的一個子類,保存了記錄的插入順序;SortMap 接口:TreeMap,能夠把它保存的記錄根據鍵排序,默認是鍵值的升序排序)。
 
Set 接口有兩個實現類
HashSet:底層是由 HashMap 實現,不允許集合中有重複的值,使用該方式時需要重寫 equals()和 hashCode()方法;
LinkedHashSet:繼承與 HashSet,同時又基於 LinkedHashMap 來進行實現,底層使用的是 LinkedHashMp)。
 
 

6.3 區別

 
List 集合中對象按照索引位置排序,可以有重複對象,允許按照對象在集合中的索引位置檢索對象,例如通過list.get(i)方法來獲取集合中的元素;
 
Map 中的每一個元素包含一個鍵和一個值,成對出現,鍵對象不可以重複,值對象可以重複;
 
Set 集合中的對象不按照特定的方式排序,並且沒有重複對象,但它的實現類能對集合中的對象按照特定的方式排序,例如 TreeSet 類,可以按照默認順序,也可以通過實現 Java.util.Comparator<Type>接口來自定義排序方式。
 
 
 

 

7. HashMap 和 HashTable 有什麼區別?

 
HashMap 是線程不安全的,HashMap 是一個接口,是 Map 的一個子接口,是將鍵映射到值得對象,不允許鍵值重複
 
允許空鍵和空值;
 
由於非線程安全,HashMap 的效率要較 HashTable 的效率高一些.
 
HashTable 是線程安全的一個集合,不允許 null 值作爲一個 key 值或者 Value 值;

 

HashTable 是 sychronize,多個線程訪問時不需要自己爲它的方法實現同步,而 HashMap 在被多個線程訪問的時候需要自己爲它的方法實現同步;


 
 
 

8. 數組和鏈表分別比較適合用於什麼場景,爲什麼?

 

8.1 數組和鏈表簡介

 
在計算機中要對給定的數據集進行若干處理,首要任務是把數據集的一部分(當數據量非常大時,可能只能一部分一部分地讀取數據到內存中來處理)或全部存儲到內存中,然後再對內存中的數據進行各種處理。
 
例如,對於數據集 S{1,2,3,4,5,6},要求 S 中元素的和,首先要把數據存儲到內存中,然後再將內存中的數據相加。
 
當內存空間中有足夠大的連續空間時,可以把數據連續的存放在內存中,各種編程語言中的數組一般都是按這種方式存儲的(也可能有例外),
 
如圖 1(b);當內存中只有一些離散的可用空間時,想連續存儲數據就非常困難了,這時能想到的一種解決方式是移動內存中的數據,把離散的空間聚集成連續的一塊大空間,
 
如圖 1(c)所示,這樣做當然也可以,但是這種情況因爲可能要移動別人的數據,所以會存在一些困難,移動的過程中也有可能會把一些別人的重要數據給丟失。

 

另外一種,不影響別人的數據存儲方式是把數據集中的數據分開離散地存儲到這些不連續空間中,

 
如圖(d)。這時爲了能把數據集中的所有數據聯繫起來,需要在前一塊數據的存儲空間中記錄下一塊數據的地址,這樣只要知道第一塊內存空間的地址就能環環相扣地把數據集整體聯繫在一起了。
 
 
C/C++中用指針實現的鏈表就是這種存儲形式。
 
 

 

由上可知,內存中的存儲形式可以分爲連續存儲和離散存儲兩種。因此,數據的物理存儲結構就有連續存儲和離散存儲兩種,它們對應了我們通常所說的數組和鏈表,
 
 

8.2 數組和鏈表的區別

 
 
數組是將元素在內存中連續存儲的;它的優點:因爲數據是連續存儲的,內存地址連續,所以在查找數據的時候效率比較高;它的缺點:在存儲之前,我們需要申請一塊連續的內存空間,並且在編譯的時候就必須確定好它的空間的大小。
 
 
在運行的時候空間的大小是無法隨着你的需要進行增加和減少而改變的,當數據量比較大的時候,有可能會出現越界的情況,數據比較小的時候,又有可能會浪費掉內存空間。
 
在改變數據個數時,增加、插入、刪除數據效率比較低鏈表是動態申請內存空間,不需要像數組需要提前申請好內存的大小,鏈表只需在用的時候申請就可以,根據需要來動態申請或者刪除內存空間,對於數據增加和刪除以及插入比數組靈活。
 

 

還有就是鏈表中數據在內存中可以在任意的位置,通過應用來關聯數據(就是通過存在元素的指針來聯繫)

 

8.3 鏈表和數組使用場景

 
數組應用場景:數據比較少;經常做的運算是按序號訪問數據元素;數組更容易實現,任何高級語言都支持;構建的線性表較穩定。
 
 
鏈表應用場景:對線性表的長度或者規模難以估計;頻繁做插入刪除操作;構建動態性比較強的線性表。
 

 

 

9. Java 中 ArrayList 和 Linkedlist 區別?

 
ArrayList 和 Vector 使用了數組的實現,可以認爲 ArrayList 或者 Vector 封裝了對內部數組的操作,比如向數組中添加,刪除,插入新的元素或者數據的擴展和重定向。
 
LinkedList 使用了循環雙向鏈表數據結構。
與基於數組的 ArrayList 相比,這是兩種截然不同的實現技術,這也決定了它們將適用於完全不同的工作場景。
 
 
LinkedList 鏈表由一系列表項連接而成。
 
 

一個表項總是包含 3 個部分:元素內容,前驅表和後驅表,如圖所示:

 
 
在下圖展示了一個包含 3 個元素的 LinkedList 的各個表項間的連接關係。
 
在 JDK 的實現中,無論 LikedList 是否爲空,鏈表內部都有一個 header 表項,它既表示鏈表的開始,也表示鏈表的結尾。
 

表項 header 的後驅表項便是鏈表中第一個元素,表項 header 的前驅表項便是鏈表中最後一個元素

 

 

10. List a=new ArrayList()和 ArrayList a =new ArrayList()的區別?

List list = new ArrayList();這句創建了一個 ArrayList 的對象後把上溯到了 List。
 
此時它是一個 List 對象了,有些ArrayList 有但是 List 沒有的屬性和方法,它就不能再用了。
 
 
而 ArrayList list=new ArrayList();創建一對象則保留了ArrayList 的所有屬性。 所以需要用到 ArrayList 獨有的方法的時候不能用前者。

 

 

實例代碼如下:

 List list = new ArrayList();
 ArrayList arrayList = new ArrayList();
 list.trimToSize(); //錯誤,沒有該方法。
 arrayList.trimToSize(); //ArrayList 裏有該方法。

 


 

11. 要對集合更新操作時,ArrayList 和 LinkedList 哪個更適合?

1.ArrayList 是實現了基於動態數組的數據結構,LinkedList 基於鏈表的數據結構。
 
2.如果集合數據是對於集合隨機訪問 get 和 set,ArrayList 絕對優於 LinkedList,因爲 LinkedList 要移動指針。
 
3.如果集合數據是對於集合新增和刪除操作 add 和 remove,LinedList 比較佔優勢,因爲 ArrayList 要移動數據。
 
 
ArrayList 和 LinkedList 是兩個集合類,用於存儲一系列的對象引用(references)。
 
例如我們可以用 ArrayList 來存儲一系列的 String 或者 Integer。
 
那 麼 ArrayList 和 LinkedList 在性能上有什麼差別呢?什麼時候應該用 ArrayList
 
什麼時候又該用 LinkedList 呢?
 
 

一.時間複雜度

 
首先一點關鍵的是,ArrayList 的內部實現是基於基礎的對象數組的,因此,它使用 get 方法訪問列表中的任意一個元素時(random access),它的速度要比 LinkedList 快。
 
LinkedList 中的 get 方法是按照順序從列表的一端開始檢查,直到另外一端。
 
對 LinkedList 而言,訪問列表中的某個指定元素沒有更快的方法了。
 
假設我們有一個很大的列表,它裏面的元素已經排好序了,這個列表可能是 ArrayList 類型的也可能是 LinkedList 類型的,現在我們對這個列表來進行二分查找(binary search)
 
 
比較列表是 ArrayList 和 LinkedList 時的查詢速度
1. ArrayList 消耗時間:15 
2. LinkedList 消耗時間:2596

 

這個結果不是固定的,但是基本上 ArrayList 的時間要明顯小於 LinkedList 的時間。

 

因此在這種情況下不宜用LinkedList。二分查找法使用的隨機訪問(random access)策略,而 LinkedList 是不支持快速的隨機訪問的。

對一個LinkedList 做隨機訪問所消耗的時間與這個 list 的大小是成比例的。而相應的,在 ArrayList 中進行隨機訪問所消耗的時間是固定的。

這是否表明 ArrayList 總是比 LinkedList 性能要好呢?這並不一定,在某些情況下 LinkedList 的表現要優於ArrayList,有些算法在 LinkedList 中實現 時效率更高。
 
比方說,利用 Collections.reverse 方法對列表進行反轉時, 其性能就要好些。看這樣一個例子,加入我們有一個列表,要對其進行大量的插入和刪除操作,在這種情況下 LinkedList 就是一個較好的選擇。
 
 
請看如下一個極端的例子,我們重複的在一個列表的開端插入一個元素:
package com.hy.集合;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListDemo {
    static final int N = 50000;

    static long timeList(List list) {
        long start = System.currentTimeMillis();
        Object o = new Object();
        for (int i = 0; i < N; i++)
            list.add(0, o);
        return System.currentTimeMillis() - start;
    }

    public static void main(String[] args) {
        System.out.println("ArrayList 耗時:" + timeList(new ArrayList()));
        System.out.println("LinkedList 耗時:" + timeList(new LinkedList()));
    }
}

 

 

二.空間複雜度

在 LinkedList 中有一個私有的內部類,定義如下
private static class Entry { 
 Object element;
 Entry next;
 Entry previous;
}

 

每個 Entry 對象 reference 列表 中的一個元素,同時還有在 LinkedList 中它的上一個元素和下一個元素。
 
一個有 1000 個元素的 LinkedList 對象將有 1000 個鏈接在一起 的 Entry 對象,每個對象都對應於列表中的一個元素。
 
這樣的話,在一個 LinkedList 結構中將有一個很大的空間開銷,因爲它要存儲這 1000 個 Entity 對象的相關信息。
ArrayList 使用一個內置的數組來存 儲元素,這個數組的起始容量是 10.當數組需要增長時,新的容量按如下公式獲得:新容量=(舊容量*3)/2+1,也就是說每一次容量大概會增長 50%。
 
 
這就意味着,如果你有一個包含大量元素的 ArrayList 對象,那麼最終將有很大的空間會被浪費掉,這個浪費是由 ArrayList 的工作方式本身造成 的。
 
 
如果沒有足夠的空間來存放新的元素,數組將不得不被重新進行分配以便能夠增加新的元素。對數組進行重新分配,將會導致性能急劇下降。如果我們知道一個 ArrayList 將會有多少個元素,我們可以通過構造方法來指定容量。
 

我們還可以通過 trimToSize 方法在 ArrayList 分配完畢之後去掉浪 費掉的空間。

 
 
 

三.總結

ArrayList 和 LinkedList 在性能上各有優缺點,都有各自所適用的地方,總的說來可以描述如下:

 
 
1.對 ArrayList 和 LinkedList 而言,在列表末尾增加一個元素所花的開銷都是固定的。
 
對 ArrayList 而言,主要是在內部數組中增加一項,指向所添加的元素,偶 爾可能會導致對數組重新進行分配;而對 LinkedList 而言,這個開銷是統一的,分配一個內部 Entry 對象。
 
 
2.在 ArrayList 的中間插入或刪除一個元素意味着這個列表中剩餘的元素都會被移動;而在 LinkedList 的中間插入或刪除一個元素的開銷是固定的。
 
3.LinkedList 不支持高效的隨機元素訪問。
 
4.ArrayList 的空間浪費主要體現在在 list 列表的結尾預留一定的容量空間,而 LinkedList 的空間花費則體現在它的每一個元素都需要消耗相當的空間
 
可以這樣說:當操作是在一列數據的後面添加數據而不是在前面或中間,並且需要隨機地訪問其中的元素時,使用

 

ArrayList 會提供比較好的性能;當你的操作是在一列數據的前面或中間添加或刪除數據,並且按照順序訪問其中的元素時,就應該使用 LinkedList 了。

 
 

 

12. 請用兩個隊列模擬堆棧結構

 

兩個隊列模擬一個堆棧,隊列是先進先出,而堆棧是先進後出。模擬如下
 
隊列 a 和 b
 
(1)入棧:a 隊列爲空,b 爲空。例:則將”a,b,c,d,e”需要入棧的元素先放 a 中,a 進棧爲”a,b,c,d,e”
(2)出棧:a 隊列目前的元素爲”a,b,c,,d,e”。
將 a 隊列依次加入 Arraylist 集合 a 中。以倒序的方法,將 a 中的集合取出,放入 b 隊列中,再將 b 隊列出列。
 

 

代碼如下:

package com.hy.集合;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

/**
 * @Description: 請用兩個隊列模擬堆棧結構
 * @Param:
 * @return:
 * @Author: Hy
 * @Date: 2019/4/15
 */
public class Qu {

    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<String>(); //a 隊列
        Queue<String> queue2 = new LinkedList<String>(); //b 隊列
        ArrayList<String> a = new ArrayList<String>(); //arrylist 集合是中間參數
        //往 a 隊列添加元素
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        queue.offer("d");
        queue.offer("e");
        System.out.print("進棧:");
        //a 隊列依次加入 list 集合之中
        for (String q : queue) {
            a.add(q);
            System.out.print(q);
        }
        //以倒序的方法取出(a 隊列依次加入 list 集合)之中的值,加入 b 對列
        for (int i = a.size() - 1; i >= 0; i--) {
            queue2.offer(a.get(i));
        }
        //打印出棧隊列
        System.out.println("");
        System.out.print("出棧:");
        for (String q : queue2) {
            System.out.print(q);
        }

    }

}

 


 

 

13. Collection 和 Map 的集成體系

Map:

 


14. Map 中的 key 和 value 可以爲 null 麼?

 

HashMap 對象的 key、value 值均可爲 null。
HashTable 對象的 key、value 值均不可爲 null
 
且兩者的的 key 值均不能重複,若添加 key 相同的鍵值對,後面的 value 會自動覆蓋前面的 value,但不會報錯。
 
測試代碼如下:

package com.hy.集合;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * @Description: 問: Map 中的 key 和 value 可以爲 null 麼?
 * 答:
 * HashMap 對象的 key、value 值均可爲 null。
 * HashTable 對象的 key、value 值均不可爲 null。
 * 且兩者的的 key 值均不能重複,若添加 key 相同的鍵值對,後面的 value 會自動覆蓋前面的 value,但不會報錯。
 * @Param:
 * @return:
 * @Author: Hy
 * @Date: 2019/3/2
 */
public class HashTest {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();//HashMap 對象
        Map<String, String> tableMap = new Hashtable<String, String>();//HashTable 對象

        map.put(null, null);
        System.out.println("hashMap 的[key]和[value]均可以爲 null:" + map.get(null));

        try {
            tableMap.put(null, "3");
            System.out.println(tableMap.get(null));
        } catch (Exception e) {
            System.out.println("【ERROR】:hashTable 的[key]不能爲 null");
        }

        try {
            tableMap.put("3", null);
            System.out.println(tableMap.get("3"));
        } catch (Exception e) {
            System.out.println("【ERROR】:hashTable 的[value]不能爲 null");
        }
    }


}

 

 


 

 

 

 

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 

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