瞭解Java容器

容器深入

標籤(空格分隔): Java編程


高級語言中的容器實在是個很神奇的東西,有必要深入瞭解下。感覺Java虛擬機有必要找時間仔細看看。

## 容器的填充
容器的填充仍然像java.util.Arrays一樣面臨同樣的不足。Collections類的fill()方法也只是複製同一個對象引用來填充整個容器的。

class StringAddress{
    private String s;
    public StringAddress(String s){this.s=s;}
    public String toString(){
        return super.toString()+" "+s;
    }
}
public class FillingLists{
    public static void main(Stringp[] args){
        List<StringAddress> list=new ArrayList<StringAddress>(Collections.nCopies(4,new StringAddress("Hello")));
        System.out.println(list);
        Collections.fill(list,new StringAddress("World!"));
        System.out.println(list);
    }
}

這個示例展示了兩種用對單個對象的引用來填充Collection的方式,第一種是使用Collections.nCopies()創建傳遞給構造器的List,這裏填充的是ArrayList。fill()方法的用處更有限,因爲它只能替換已經在List中存在的元素,而不能添加新的元素。

事實上,所有的Collection子類型都有接收另一個Collection對象的構造器,用所接收的Collection對象中的元素來填充新的容器。構建接收Generator和quantity數值並將它們作爲構造器參數的類:

public class CollectionData<T>extends ArrayList<T>{
    public CollectionData(Generator<T> gen,int quantity){
        for(int i=0;i<quantity;i++)
            add(gen.next());
    }
    public static <T> CollectionsData<T> list(Generator<T> gen,int quantity){
        return  new CollectionData<T>(gen,quantity);
    }
}

## Map生成器
我們可以對Map也使用Generator的解決方法。但是這需要有一個Pari類,因爲爲了組裝Map,每次調用Generator。

## Collection的功能方法

項目 數量
boolean add(T) 確保容器持有具有泛型類型T的參數,如果沒有將此參數添加進容器,則返回false
boolean adAll(Collection<?extends T>) 添加參數中的所有元素。只要添加了任意元素就返回true
void clear() 移除容器中所有的元素
boolean contains(T) 如果容器已經持有具體泛型類型T此參數,則返回true
Boolean containsAll(Collection<?>) 如果容器持有此參數中的所有元素,則返回true
boolean isEmpty() 容器中沒有元素時返回true
Iterator iterator() 返回一個Iterator,可以用來遍歷容器中的元素
Boolean remove(Object) 如果參數在容器中,則移除此元素的一個實例。如果做了移除動作,則返回true
boolean removeAll(Collection<?>) 移除參數中的所有元素。只要有移除動作發生就返回true
boolean retainAll(Collection<?>) 只保存參數中的元素(應用集合類交集概念)。只要Collection發生了改變就返回true
int size() 返回容器中元素的數目
Object[] toArray() 返回一個數組,該數組包含容器中的所有元素
T[] toArray(T[]a) 返回一個數組,該數組包含容器中的所有元素

## Set和存儲順序
Set需要一種方式來維護存儲順序,而存儲順序如何維護,則是在Set的不同實現之間會有所變化。因此,不同的Set實現不僅具有不同的行爲,而且它們對於可以在特定的Set中放置的元素的類型也有不同的要求:

Set(interface) 存入Set的每個元素都必須是唯一的,因爲Set不保存重複元素。加入Set的元素必須定義equals()方法以確保對象的唯一性。Set與Collection有完全一樣的接口。Set接口不保護維護元素的次序
HashSet* 爲快速查找而設計的Set。存入HashSet的元素必須定義hashCode()
TreeSet 保持次序的Set,底層爲樹結構。使用它可以從Set中提取有序的序列。元素必須實現Comparable接口
LinkedHashSet 具有HashSet的查詢速度,且內部使用鏈表維護元素的順序(插入的次序)。於是在使用迭代器遍歷Set時,結果會按元素插入的次序顯示。元素也必須定義hashCode()方法

使用特定的Set實現類型必須定義的方法:

class SetType{
    int i;
    public SetType(int n){i=n;}
    public boolean equals(Object o){
        return o instanceof SetType&&(i==((SetType)o).i);
    }
    public String toString(){return Integer.toString(i);}
}
class HashType extends SetType{
    public HashType(int n){super(n);}
    public int hashCode(){return i;}
}
class TreeType extends SetType implements Comparable<TreeType>{
    public TreeType(int n){super(n);}
    public int compareTo(TreeType arg){
        return (arg.i<i?-1:(arg.i==i?0:1));
    }
}
public class TypeForSets{
    static<T> Set<T> fill(Set<T> set,Class<T> type){
        try{
            for(int i=0;i<10;i++){
                set.add(type.getConstructor(int.class).newInstance(i));
                }
        }catch(Exception e){
            throw new RuntimeException(e);
        }
        return set;
    }
    static <T> void test(Set<T> set,Class<T> type){
        fill(set,type);
        fill(set,type);
        fill(set,type);
        System.out.println(set);
    }
    public static void main(String[] args){
        test(new HashSet<HashType>(),HashType.class);
        test(new LinkedHashSet<HashType>(),HashType.class);
        test(new TreeSet<TreeType>(),TreeType.class);
        test(new HashSet<SetType>(),SetType.class);
        test(new HashSet<TreeType>(),TreeType.class);
        test(new LinkedHashSet<SetType>(),SetType.class);
        test(new LinkedHashSet<TreeType>(),TreeType.class);
        try{
            test(new TreeSet<SetType>(),SetType.class);
        }catch(Exception e){
            System.out.println(e.getMessage());
        }
        try{
            test(new TreeSet<HashType>(),HashType.class);
        }catch(Exception e){
            System.out.println(e.getMessage());
        }
    }
}

基類SetType只存儲一個int,並且通過toString()方法產生它的值。因爲所有在Set中存儲的類都必須具有equals()方法,因此在基類中也有該方法。其等價性是基於這個int類型的i的值確定。
HashType繼承自SetType,並且添加了hashCode()方法,該方法對於放置到Set的散列實現中的對象來說是必需的。

## 理解Map
映射表的基本思想是它維護的是鍵-值(對)關聯。Map的幾種實現:HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。它們擁有同樣的基本接口,但是行爲特性各不相同,這表現在效率,鍵值對的保存及呈現次序,對象的保存週期,映射表如何在多線程中工作和判定“鍵”等價的策略等方面。
Map的簡單實現:

public class AssociativeArray<K,V>{
    private Object[][] pairs;
    private int index;
    public AssociativeArray(int length){
        pairs=new Object[length][2];
    }
    public void put(K key,V value){
        if(index>=pairs.length)
            throw new ArrayIndexOutOfBoundException();
        pairs[index++]=new Object[]{key,value};
    }
    @SuppressWarnings("unchecked")
    public V get(K key){
        for(int i=0;i<index;i++)
            if(key.equals(pairs[i][0]))
                return (V)pairs[i][1];
        return null;
    }
    public String toString(){
        StringBuilder result=new StringBuilder();
        for(int i=0;i<index;i++){
            result.append(pairs[i][0].toString());
            result.append(" : ");
            result.append(pairs[i][1].toString());
            if(i<index-1)
                result.append("\n");
        }
        return result.toString();
    }
    public static void main(String[] args){
        AssociativeArray<String,String> map=new AssociativeArray<String,String>(6);
        map.put("sky","blue");
        map.put("grass","blue");
        try{
            map.put("extra","object");
        }catch(ArrayIndexOutOfBoundException e){
            print("Too many objects!");
        }
        print(map);
        print(map.get("ocean"));
    }
}

## 性能
性能是映射表中的一個重要問題,當在get()中使用線性搜索時,執行速度會相當的慢,而這正是HashMap提高速度的地方。HashMap使用了特殊的值,稱作散列碼,來取代對鍵的緩慢搜索。散列碼是“相對唯一”的,用以代表對象的int值,它是通過將該對象的某些信息進行轉換而生成的。

## SortedMap
使用SortedMap,可以確保鍵處於排序狀態。這使得它具有額外的功能。鍵值對是按鍵的次序排列的。TreeMap中的次序是有意義的,因此“位置”的概念纔有意義,所以才能取得第一個和最後一個元素,並且可以提取Map的子集。

## LinkedHashMap
爲了提高速度,LinkedHashMap散列化所有的元素,但是在遍歷鍵值對時,卻又以元素的插入順序返回鍵值對。此外,可以在構造器中設定LinkedHashMap,使之採用基於訪問的最近最少使用(LRU)算法,於是沒有被訪問過的元素就會出現在隊列的前面。對於需要定期清理元素以節省空間的程序來說,此功能使得程序很容易得以實現。

## 散列與散列碼
HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同。
正確的equals()方法必須滿足下列5個條件:

1.自反性。對任意x,x.equals(x)一定返回true。
2.對稱性。對任意x和y,如果y.equals(x)返回true,則x.equals(y)也返回true。
3.傳遞性。對任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定返回true。
4,一致性。對任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定要返回true。
5.對任何不是null的x,x.equals(null)一定返回false。

## 理解hashCode()
使用散列的目的在於:想要使用一個對象來查找另一個對象。
散列的價值在於速度:散列使得查詢得以快速進行。
存儲一組元素最快的數據結構是數組,所以使用它來表示鍵的信息(不是鍵本身)。但是數組並不保存鍵本身。而是通過鍵對象生成一個數字,將其作爲數組的下標。這個數字就是散列碼,由hashCode()方法生成。爲解決數組容量固定的問題,不同的鍵可以產生相同的下標。
通常,散列衝突由外部鏈接處理:數組並不直接保存值,而是保存值的list。然後對list中的值使用equals()方法進行線性的查詢。這部分的查詢自然會比較慢,但是如果散列函數好的話,數組的每個位置就只有較少的值。因此,不是查詢整個list,而是快速地跳到數組的某個位置,只對很少的元素進行比較。這便是HashMap會如此快的原因。

## 選擇接口的不同實現
從容器分類圖中可以看出,Hashtable,Vector和Stack的“特徵是”,它們是過去遺留下來的類,目的只是爲了支持老的程序。
LinkedList:經常性的需要在表中插入或刪除元素;
ArrayList:不需要經常插入刪除,就可以用速度更快的ArrayList;
HashSet:查詢速度最快,常用;
LinkedHashSet:保持元素的插入的次序;
TreeSet:生成一個總是處於排序狀態的Set;
有時某個特定容器的不同實現會擁有一些共同的操作,但是這些操作的性能卻並不相同。在這種情況下,你可以基於使用某個特定操作的頻率,以及你需要的執行速度來在它們中間進行選擇。

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