Java 7:Java集合從不懂到更不懂,不信來看
集合
1 集合關係網
關係網:
- 陣列 Arrays.asList就成了List
介面Collection
- 3個分支:List,Set,Queue
- List兩個分支:Sequential(代表作是LinkedList)和RandomAccess(ArrayList,Vector,Stack)
- 用時要考慮是頻繁插入,還是頻繁訪問
- LinkedList可以快速插入刪除,因為只是節點的操作,但下標訪問(隨機訪問)比較慢,它的方法也更多
- 提供了支援棧,佇列,雙端佇列的方法
- RandomAccess分支可以快速根據下標訪問,但長度變化代價比較昂貴,往中間插入,移除也比較費時
Set兩個分支:排序的(代表作TreeSet,LinkedHashSet)和無序的(HashSet)
- 不允許重複
- 能夠迅速判斷是否包含某元素
- 元素型別必須考慮hash,雖然可能不需要你寫什麼
- HashSet底層是HashMap(android-23),value全是HashSet本身
- LinkedHashSet:
按照被新增的順序儲存物件
,繼承自HashSet,底層是LinkedHashMap,而LinkedHashMap也是繼承自HashMap,並配合一個雙端連結串列解決有序問題,所以Hash相關的操作其實都在HashMap裡 - TreeSet:
按照比較結果來升序儲存物件,所以元素必須實現了Comparable
,底層是TreeMap
Queue兩個分支:LinkedList實現了一般化的佇列介面,先進者先出,PriorityQueue基於Comparator等進行優先順序排序,優先順序高者先出
- CopyOnWrite, Concurrent支援
- 介面Map
- 兩個分支:SortedMap(代表作TreeMap,LinkedHashMap)和無序的(HashMap,IdentityHashMap,WeakHashMap)
- 鍵不能重複,鍵還得能算出hash值
- Tree相關是基於二叉查詢樹,TreeMap基於紅黑樹
- Hash相關是基於雜湊,包括LinkedHashMap,其實雜湊操作也是繼承自HashMap,順序或者LRU序由雙端連結串列來實現
- LinkedHashMap有點特殊,可以用來實現Lru快取
- 同步支援:
- CopyOnWrite的List和Set
- ConcurrentMap
- 阻塞佇列
- 涉及到的其他東西
- 泛型
- 同步的和非同步的
- 排序,Comparable和Comparator
- Arrays, Arrays2, Collections, Collections2
- Map的底層實現,Hash的原理
- 擴充套件:
- Guava的集合庫
綜合:
- LinkedList,ArrayList,Vector
- Stack,Queue,Dequeue
- HashSet,LinkedHashSet,TreeSet
- HashMap,LinkedHashMap,TreeMap,IdentityHashMao,WeakHashMap
- Vector和HashTable是執行緒同步的
- 新程式碼中不應該使用過時的Vector,HashTable,Stack
2 關於ArrayList
構造Collection:
Collection<Integer> collection = new ArrayList<>(10); //引數是capacity,不是size
Collection<Integer> collection2 = new ArrayList<>(); //引數預設值就是10
Collection<Integer> collection3 = new ArrayList<>(collection); //型別是java.util.ArrayList
Collection<Integer> collection4 = Arrays.asList(1,2,3,4,5);
Collection<Integer> collection5 = Arrays.<Integer>asList(1,2,3,4,5);
Collection<Integer> collection6 = Arrays.asList(new Integer[]{5,6,7,8}); //型別是:java.util.Arrays$ArrayList,所以一般是將這個返回作為new ArrayList的引數
List<Integer> list = new ArrayList<Integer>(10);
List<Integer> list = new LinkedList<Integer>(); //沒法帶capacity引數,因為根本不用事先分配長度
Set<Integer> set = new HashSet<Integer>();
Set<Integer> set = new TreeSet<Integer>();
Set<Integer> set = new LinkedHashSet<Integer>();
Map<String, String> map = new HashMap<String, String>();
Map<String, String> map = new TreeMap<String, String>();
Map<String, String> map = new LinkedHashMap<String, String>();
- 需要注意的是:Arrays.asList
- 返回的其實是java.util.Arrays$ArrayList型別,是一個受限的型別,底層是陣列,不支援delete等操作,所以儘量別用
- Arrays.asList(1,2,3,4,5); 這種語法叫顯式型別說明
長度:
size()
isEmpty()
clear()
capacity和size
新增:
add
addAll
查詢:
contains
containsAll
indexOf
刪除
remove(T),基於equals
remove(index)
removeAll(),基於equals
擷取
subList(from, to_exclude),截取出來的list,肯定滿足containsAll,並且不受sort和shuffle影響
排序,打亂
Collections.sort(List, Comparator)
Collection.shuffle()
交集
retainAll(list1, list2),取交集,依賴於equals方法
遍歷
//Iterator只能前向移動
Iterator<Integer> it = arraylist.iterator();
while(it.hasNext()){
Integer i = it.next();
System.out.println(i);
}
//ListIterator可以雙向移動
ListIterator<Integer> it2 = arraylist.listIterator();
while(it2.hasNext()){
Integer i = it2.next();
int prevIndex = it2.previousIndex();
Integer prev = it2.previous();
int nextIndex = it2.nextIndex();
Integer next = it2.next();
it2.hasPrevious();
it2.hasNext();
it2.remove();
System.out.println(i);
}
//遍歷A
for(Integer i: arraylist){
System.out.println(i);
}
//遍歷B
for(int i = 0; i < arraylist.size(); i++){
System.out.println(arraylist.get(i));
}
A:需要耗費一個迭代器 的開銷,但獲取元素的時間複雜度是O(1),速度快,但耗記憶體
B:不需要額外的空間,但get(i)時間複雜度是O(n),速度慢(除非是ArrayList,get(i)就是O(1)
所以:
ArrayList:推薦方式B
LinkedList:推薦方式A
3 關於LinkedList
提供了支援棧,佇列,雙端佇列的方法
把這些方法分類列出來吧,好找
getFirst
remove
removeFirst
removeLast
peek()
poll()
offer()
element()
add
addLast
4 關於Stack
有個java.util.Stack,除了提供基本的:
push,peek,pop,empty等操作,還提供了隨機訪問
這裡自己基於LinkdedList寫一個,只提供棧的最小可用子集:
按照程式設計思想裡所說的,java.util.Stack設計欠缺,基於LinkedList能產生更好的Stack,所以我們這個更好
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<>();
public void push(T v){
storage.addFirst(v);
}
public T peek(){
return storage.getFirst();
}
public T pop(){
return storage.removeFirst();
}
public boolean empty(){
return storage.isEmpty();
}
public String toString(){
return storage.toString();
}
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
for(String s: "My dog has fileas".split(" ")){
stack.push(s);
}
while(!stack.empty()){
System.out.println(stack.pop() + " ");
}
}
}
5 關於Set
- 特性:
- 不允許重複物件
- 可以快速查詢某個物件是否存在,專門對快速查詢做了優化,set.contains(t)
- 常用HashSet,HashSet沒順序,TreeSet有順序,按照物件的compare來
- TreeSet元素型別必須實現了Comparable,構造方法也可以傳入個Comparator,如元素型別是String,但不想用預設的字典序來排序
- Set wors = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
set.contains(t)
set.containsAll(another set)
set.remove(t)
set.removeAl(another set)
可以用foreach遍歷
6 關於Map
- 概論
- HashMap:基於hashCode方法,這個是最純的map,不考慮其他特性,所以最快
- 插入和查詢鍵值對的開銷都是固定的
- 優化措施:可以通過構造器設定容量和負載因子
- 一個HashMap 例項有兩個影響它效能的因素:初始大小和載入因子(load factor)
- 當雜湊表的大小達到初始大小和載入因子的乘積的時候,雜湊表會進行 rehash操作
- 如果在一個HashMap 例項裡面要儲存多個對映關係時,我們需要設定足夠大的初始化大小以便更有效地儲存對映關係而不是讓雜湊表自動增長讓後rehash,造成效能瓶頸
- 如果你知道有N個鍵值對,則可以:map = new HashMap
- HashMap:基於hashCode方法,這個是最純的map,不考慮其他特性,所以最快
import org.ayo.lang.JsonUtilsUseFast;
public class Dict<K, V> {
public static class Entry<K, V>{
public K k;
public V v;
public Entry(){
}
public Entry(K kk, V vv){
k = kk;
v = vv;
}
}
private Entry<K, V>[] pairs; //鍵值對作為一個object[2]儲存,所有鍵值對就是個
private int index;
public Dict(int length){
pairs = new Entry[length];
}
public void put(K key, V value){
if(index >= pairs.length){
throw new ArrayIndexOutOfBoundsException();
}
pairs[index++] = new Entry(key, value);
}
public V get(K key){
for(int i = 0; i < index; i++){
if(key.equals(pairs[i].k)){
return pairs[i].v;
}
}
return null;
}
public int size(){
return index;
}
@Override
public String toString() {
return JsonUtilsUseFast.toJson(pairs, true);
}
public static void main(String[] args) {
Dict<String, String> map = new Dict<String, String>(10);
map.put("1", "一");
map.put("2", "貳");
map.put("3", "叄");
map.put("4", "肆");
map.put("5", "伍");
System.out.println(map.size());
System.out.println(map.toString());
System.out.println(map.get("4"));
}
}
這裡只做個說明,完全沒考慮查詢效率,長度擴充套件,鍵重複問題
雜湊
Dict中,get方法是在陣列中遍歷,線性查詢,HashMap用的是雜湊碼,雜湊
- 關於雜湊
- 雜湊碼是相對唯一的,用來代表物件的int值
- 是根據物件的某些資訊進行轉換生成的
- java中的支援就是hashCode()方法
- 所以Dict需要考慮給key生成雜湊碼之後,怎麼根據雜湊儲存和快速訪問
一個可以作為key的型別定義,需要考慮hashCode()和equals()兩個方法
參考hash.Groundhog這個類
先得考慮equals,然後equals相等的兩個物件,hashCode()也應該相等
Map裡會根據equals來確保鍵不重複
Object預設的equals比較的是物件地址
Object預設的hashCode也是基於物件地址
equals方法要滿足的5個條件:
- 自反性:x.equals(x),一定返回true
- 對稱性:x.equals(y)的結果一定等於y.eqauls(x)
- 傳遞性:x.equals(y)為true,y.equals(z)為true,則x.equals(z)一定為true
- 一致性:多次呼叫,只要等價資訊沒變,返回結果一致
- 有一個是null,結果就是false
hashCode方法:
- 並不需要總是能夠返回唯一的標識碼,這個可能不好理解
- 首先要知道,雜湊值應該依賴於物件的標識性欄位,一般情況下,不能依賴於可變欄位
- 這裡你要考慮的問題就是:如果一個欄位值變了,你希望雜湊值也跟著變嗎
- 生成雜湊只並不追求唯一性,應該更追求速度
- 雜湊值如果不唯一,注意,有個桶位的概念,即雜湊值對應的下標處,存的其實是個List,這個就是桶位
- 至於是什麼型別的List或者陣列,後面看原始碼
- List的元素型別,其實是entry
- 桶位在這就暫時叫做BucketList吧,對應的是一個雜湊作為下標取出的幾個值
- 即使雜湊值唯一,放到Map裡時,Map的底層陣列大小也是有限定的,所以需要對雜湊再處理
- 例如Map底層陣列設定為100個,你的雜湊值是200,就得把200對映到100範圍之內,
- 最直觀的方法是取餘,但取餘也是個耗時操作
- 通過雜湊值,拿到BucketList之後,還會通過key的equals方法找到最終確定的Value
結論:
- hashCode要的是速度,不是唯一性,不過hashCode的值應該是均勻的,避免值都集中在一個區域內
- 當然既有速度,又有唯一性,是很完美的,如果值的個數固定,map底層陣列大小也就固定,完美雜湊是有可能的
- EnumSet和EnumMap就實現了完美雜湊
- Key的型別必須仔細定義hashCode和equals方法,使二者能唯一確定一個物件
- 所以這又回到那個話題:空間換時間
- 如果知道Value就是100個,怎麼辦呢,更一般的,要把一個List放到Map裡,map可以怎麼優化?
- 參考linkedIn的優化:new HashMap(Math.ceiling(list.size() 乘 0.7))
- 0.7在這裡是負載因子
經驗
- equals中用到欄位,一般也應該用於生成hashCode
- 生成hashCode的一個公式,引自effecttive java
- 定義個int result = 17
- 對於每一個有意義的欄位,計算出一個int c
- 對result和每一個c:result = 37 x result + c
- 各種欄位f對應的c怎麼計算:
- boolean:c = f ? 1 : 0
- byte char short int:c = (int)f
- long:c = (int)(f^(f>>>32))
- float:c = Float.floatToIntBits(f)
- double:long l = Double.doubleToLongBits(f),回到long
- Object:c = f.hashCode()
- 陣列:對每個元素應用此規則
public class Student{
public int id;
public String name;
public int hashCode(){
int result = 17;
result = 37*result + name.hashCode();
result = 37*result + id;
return result;
}
}
7 關於Queue
7.1 先進先出:典型佇列
Queue介面:LinkedList實現了Queue介面,主要是offer,peek,poll,element
offer:插入隊尾,失敗返回false,好像還和capacity-restrick有關,這個不會改變capacity大小
add:插入隊尾,但是可以改變capacity
element:拿到隊頭,但不remove,無則拋異常
remove:拿到隊頭,同時remove,無則拋異常 NoSuchElementException
peek:拿到隊頭,但不remove,無則返回null
poll:拿到隊頭,同時 remove,無則null
常用:
Queue queue = new LinkedList();
將LinkedList窄化為Queue介面
7.2 優先順序佇列:基於排序
- 簡介:
- 優先順序最高先出,PriorityQueue的排序基於Comparator,或者預設的自然排序
- 內部維護了一個堆,在插入時,會排序
- 允許重複
7.3 Deque(雙向佇列)
LinkedList支援雙端佇列的方法,但java裡沒有顯式的定義這麼個介面,因為不太常用,一般不會在兩端都放元素,然後又需要從兩端獲取元素
自己定義:
public class Deque<T> {
private LinkedList<T> deque = new LinkedList<T>();
public void addFirst(T e){
deque.addFirst(e);
}
public void addLast(T e){
deque.addLast(e);
}
public T getFirst(){
return deque.getFirst();
}
public T getLast(){
return deque.getLast();
}
public T removeFirst(){
return deque.removeFirst();
}
public T removeLast(){
return deque.removeLast();
}
public int size(){
return deque.size();
}
public String toString(){
return deque.toString();
}
}
8 Iterable介面:foreach基於此介面
所有Collection都可以foreach
Map怎麼foreach才最合適??
for(Map.Entry entry: map.entrySet()){
entry.getKey(), entry.getValue();
}
9 Collections提供的便利方法
//Empty系列:內部其實都對應一個private static final的實現類,無法插入資料,因為都是immutalble
List<String> list = Collections.EMPTY_LIST;
Set<String> set = Collections.EMPTY_SET;
Map<String, String> map = Collections.EMPTY_MAP;
list = Collections.emptyList();
set = Collections.emptySet();
map = Collections.emptyMap();
Enumeration<String> enumeration = Collections.emptyEnumeration();
Collections.emptyIterator();
Collections.emptyListIterator();
Collections.emptyNavigableMap();
Collections.emptyNavigableSet();
Collections.emptySortedMap();
Collections.emptySortedSet();
//unmodifiable系列:不可變集合
Collection<String> c = new LinkedList<String>();
Collection<String> c1 = Collections.unmodifiableCollection(c);
list = Collections.unmodifiableList(List);
map = Collections.unmodifiableMap(Map);
NavigableMap<String, String> nmap = Collections.unmodifiableNavigableMap(NavigableMap<K, V>);
SortedMap<String, String> smap = Collections.unmodifiableSortedMap(SortedMap<K, V>);
set = Collections.unmodifiableSet(Set<String>);
SortedSet<String> sset = Collections.unmodifiableSortedSet(SortedSet<T>);
NavigableSet<String> nset = Collections.unmodifiableNavigableSet(NavigableSet<T>);
//synchronized系列:效能沒有CopyOnWrite和ConcurrentMap好
c1 = Collections.synchronizedCollection(c);
List<String> list = Collections.synchronizedList(list);
map = Collections.synchronizedMap(Map);
SortedMap<String, String> smap = Collections.synchronizedSortedMap(SortedMap<K, V>);
NavigableMap<String, String> nmap = Collections.synchronizedNavigableMap(NavigableMap<K, V>);
set = Collections.synchronizedSet(Set<T>);
SortedSet<String> sset = Collections.synchronizedSortedSet(SortedSet);
NavigableSet<String> nset = Collections.synchronizedNavigableSet(NavigableSet<T>);
//singleton系列
Set<T> set = Collections.singleton(T t);
List<T> list = Collections.singletonList(T t);
Map<K, V> map = Collections.singletonMap(key, value);
//checked系列,避免List list = new ArrayList<String>(); list.add(1);這種不規範使用
Collections.checkedList(list, type);
排序
Collections.sort(list);
混排
Collections.shuffle(list)
反轉
Collections.reverse(list)
用指定元素替換
Collections.fill(li,"aaa")
拷貝
如果目標list長度大,則剩餘元素不會被覆蓋
Collections.copy(list_src, list_dest)
最大最小
Collections.min(list)
Collections.max(list)
查詢子列表: 這裡
int locations = Collections.indexOfSubList(list, subList)
int locations = Collections.lastIndexOfSubList (list, subList);
根據指定的距離迴圈移動指定列表中的元素
Collections.rotate(list,-1);
{112, 111, 23, 456, 231 }按-1旋轉之後是:111,23,456,231,112
10 陣列
java有三種類型:
基本資料型別8種
普通物件
陣列物件
初始化時必須知道大小
Arrays裡的操作,個人認為Arrays最重要的作用是提供了對陣列的反射
//填充陣列
Arrays.fill(array, 5);
//將陣列的第2和第3個元素賦值為8
Arrays.fill(array, 2, 4, 8);
//對陣列的第2個到第6個進行排序進行排序
Arrays.sort(array,2,7);
//比較陣列元素是否相等
Arrays.equals(array1, array2)
//查詢
Arrays.binarySearch(array1, 9)
11 多執行緒支援
List支援:CopyOnWriteArrayList
Vector被廢棄了,你應該使用CopyOnWriteArrayList
CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡新增元素,新增完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
add時,會加鎖,即一次就拷貝出一份,寫完下一個寫
read時,不用加鎖,如果此時有人正寫,讀到的就是舊資料
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index);
}
Set支援:CopuOnWriteArraySet
和CopyOnWriteArrayList一個道理
Map支援:
HashTable被廢棄了
現在應該使用ConcurrentHashMap和ConcurrentSkipListMap
ConcurrentHashMap是HashMap的執行緒安全版本,ConcurrentSkipListMap是TreeMap的執行緒安全版本
為什麼HashMap執行緒不安全
因為多執行緒環境下,使用HashMap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap,如以下程式碼
final HashMap<String, String> map = new HashMap<String, String>(2);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
map.put(UUID.randomUUID().toString(), "");
}
}, "ftf" + i).start();
}
}
}, "ftf");
t.start();
t.join();
HashMap使用連結串列解決衝突,put時也會遍歷這個連結串列,這個執行緒遍歷著,那個執行緒
往裡插入,可能造成死迴圈,個人理解
為什麼HashTable不行了?
HashTable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪問HashTable的同步方法時,其他執行緒訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
ConcurrentHashMap用的什麼技術?鎖分段
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。
用法
V putIfAbsent(K key,V value)
如果不存在key對應的值,則將value以key加入Map,否則返回key對應的舊值
等價於:
if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
boolean remove(Object key,Object value)
只有目前將鍵的條目對映到給定值時,才移除該鍵的條目。這等價於清單3 的操作
等價於:
if (map.containsKey(key) && map.get(key).equals(value)) {
map.remove(key);
return true;
}
return false;
boolean replace(K key,V oldValue,V newValue)
只有目前將鍵的條目對映到給定值時,才替換該鍵的條目
等價於:
if (map.containsKey(key) && map.get(key).equals(oldValue)) {
map.put(key, newValue);
return true;
}
return false;
V replace(K key,V value)
等價於:
if (map.containsKey(key)) {
return map.put(key, value);
}
return null;
2 我們也可以自己嘗試實現一個CopyOnWriteMap
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
private volatile Map<K, V> internalMap;
public CopyOnWriteMap() {
internalMap = new HashMap<K, V>();
}
public V put(K key, V value) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
V val = newMap.put(key, value);
internalMap = newMap;
return val;
}
}
public V get(Object key) {
return internalMap.get(key);
}
public void putAll(Map<? extends K, ? extends V> newData) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
newMap.putAll(newData);
internalMap = newMap;
}
}
}
寫時,加鎖,並新建底層容器,然後重新指向這個新容器
讀時,不加鎖,如果正在寫,讀到的就是舊資料
懂了這個道理,實現CopyOnWriteLinkedList也就簡單了
Queue支援:BlockingQueue
插入:
add(e): 丟擲異常 IllegalStateException("Queue full")
offer(e): 不拋異常,返回false
put(e) : 阻塞
offer(e,time,unit):超時則退出
移除:
remove() 丟擲異常 NoSuchElementException
poll() 不拋異常,返回null
take() 阻塞
poll(time,unit) 超時則退出
檢查方法:
element() 丟擲異常
peek() 不拋異常,返回特殊值
- 七種阻塞佇列
- ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列
- LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列
- PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列
- DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列
- SynchronousQueue:一個不儲存元素的阻塞佇列
- LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列
- LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列