容器深入
標籤(空格分隔): 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;
有時某個特定容器的不同實現會擁有一些共同的操作,但是這些操作的性能卻並不相同。在這種情況下,你可以基於使用某個特定操作的頻率,以及你需要的執行速度來在它們中間進行選擇。