框架是個好東西,可早晚有一天會過時,這世界上就沒有亙古不變的東西,來學下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,然後再根據實現這些接口的集合的特點來選用。