java集合入門和深入學習(詳解),看這篇就差不多了

java集合入門和深入學習(詳解),看這篇就差不多了

一、集合入門總結

集合框架:

Java中的集合框架大類可分為Collection和Map;兩者的區別:

1、Collection是單列集合;Map是雙列集合

2、Collection中只有Set系列要求元素唯一;Map中鍵需要唯一,值可以重複

3、Collection的數據結構是針對元素的;Map的數據結構是針對鍵的。

泛型:

在說兩大集合體系之前先說說泛型,因為在後面的集合中都會用到;

所謂的泛型就是:類型的參數化

泛型是類型的一部分,類名+泛型是一個整體

如果有泛型,不使用時,參數的類型會自動提升成Object類型,如果再取出來的話就需要向下強轉,就可能發生類型轉化異常(ClassCaseException);不加泛型就不能在編譯期限定向集合中添加元素的類型,導致後期的處理麻煩。

下面就來對比加了泛型和不加泛型的區別:

package 好好學java;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
 public static void main(String[] args) {
 // 不加泛型,添加和遍歷
 List list = new ArrayList<>();
 list.add(1);
 list.add("123");
 list.add("hello");
 
 Iterator it = list.iterator();
 while(it.hasNext()){
 // 沒有添加泛型,這裡只能使用Object接收
 Object obj = it.next();
 System.out.println(obj);
 }
 }
}
package 好好學java;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
 public static void main(String[] args) {
 // 加泛型,添加和遍歷
 List list = new ArrayList();
 list.add("123");
 list.add("hello");
 
 Iterator it = list.iterator();
 while(it.hasNext()){
 // 因為添加了泛型,就說明集合中裝的全部都是String類型的數據
 // 所以這裡用String類型接收,就不會發生異常,並且可以使用String的方法
 String str = it.next();
 System.out.println(str.length());
 }
 }
}

自定義帶泛型的類:

package 好好學java;
public class Test {
 // 自定義一個帶有一個參數的泛型類,可以向傳入什麼類型就傳入什麼類型
 public static void main(String[] args) {
 // 進行測試, 傳入一個String對象
 Person perStr = new Person();
 perStr.setT("我是字符串");
 String str = perStr.getT();
 System.out.println(str);
 
 // 進行測試,傳入一個Integer對象
 Person perInt = new Person();
 perInt.setT(100);
 Integer intVal = perInt.getT();
 System.out.println(intVal);
 
 }
}
//自定義一個帶有一個參數的泛型類
class Person{
 private T t;
 
 void setT(T t){
 this.t = t;
 }
 
 T getT(){
 return t;
 }
}

實現帶有泛型的接口類型:

實現接口的同時, 指定了接口中的泛型類型. (定義類時確定);

public class GenericImpl1 implements GenericInter {}

實現接口時, 沒有指定接口中的泛型類型.此時, 需要將該接口的實現類定義為泛型類.接口的類型需要在創建實現類對象時才能真正確定其類型. (始終不確定類型, 直到創建對象時確定類型);

public class GenericImpl2 implements GenericInter {}

泛型的通配符(?):

上限限定:比如定義方法的時候出現,public void getFunc(List extends Animal> an),

那麼表示這裡的參數可以傳入Animal,或者 Animal的子類

下限限定: 比如定義方法的時候出現,public void getFunc(Set super Animal> an ),

那麼表示這裡的參數可以傳入Animal,或者Animal的父類

使用泛型的注意點:

1、泛型不支持基本數據類型

2、泛型不支持繼承,必須保持前後一致(比如這樣是錯誤的:List list = new ArrayList();

Collection體系:

ollection包括兩大體系,List和Set

List的特點:

存取有序,有索引,可以根據索引來進行取值,元素可以重複

Set的特點:

存取無序,元素不可以重複

List:

下面有ArrayList,LinkedList,Vector(已過時)

集合的的最大目的就是為了存取;List集合的特點就是存取有序,可以存儲重複的元素,可以用下標進行元素的操作

ArrayList: 底層是使用數組實現,所以查詢速度快,增刪速度慢

package 好好學java;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
 // 使用ArrayList進行添加和遍歷
 public static void main(String[] args) {
 List list = new ArrayList();
 
 list.add("接口1");
 list.add("接口2");
 list.add("接口3");
 
 // 第一種遍歷方式,使用迭代器
 Iterator it = list.iterator();
 while(it.hasNext()){
 String next = it.next();
 System.out.println(next);
 }
 System.out.println("-------------------");
 // 第二種遍歷方式,使用foreach
 for (String str : list){
 System.err.println(str);
 }
 }
}

LinkedList:是基於鏈表結構實現的,所以查詢速度慢,增刪速度快,提供了特殊的方法,對頭尾的元素操作(進行增刪查)。

使用LinkedList來實現棧和隊列;棧是先進後出,而隊列是先進先出

package com.xiaoshitou.classtest;
import java.util.LinkedList;
/**
 * 利用LinkedList來模擬棧
 * 棧的特點:先進後出
 * @author Beck
 *
 */
public class MyStack {
 private LinkedList linkList = new LinkedList();
 
 // 壓棧
 public void push(String str){
 linkList.addFirst(str);
 }
 
 // 出棧
 public String pop(){
 return linkList.removeFirst();
 }
 
 // 查看
 public String peek(){
 return linkList.peek();
 }
 
 // 判斷是否為空
 public boolean isEmpty(){
 return linkList.isEmpty();
 }
}
package 好好學java;
public class Test {
 public static void main(String[] args) {
 // 測試棧
 StackTest stack = new StackTest();
 stack.push("我是第1個進去的");
 stack.push("我是第2個進去的");
 stack.push("我是第3個進去的");
 stack.push("我是第4個進去的");
 stack.push("我是第5個進去的");
 // 取出
 while (!stack.isEmpty()){
 String pop = stack.pop();
 System.out.println(pop);
 }
 // 打印結果
 /*我是第5個進去的
 我是第4個進去的
 我是第3個進去的
 我是第2個進去的
 我是第1個進去的*/
 }
 
}
 

LinkedList實現Queue:

package 好好學java;
import java.util.LinkedList;
/**
 * 利用linkedList來實現隊列
 * 隊列: 先進先出
 * @author Beck
 *
 */
public class QueueTest {
 private LinkedList link = new LinkedList();
 
 // 放入
 public void put(String str){
 link.addFirst(str);
 }
 
 // 獲取
 public String get(){
 return link.removeLast();
 }
 
 // 判斷是否為空
 public boolean isEmpty(){
 return link.isEmpty();
 }
}
package 好好學java;
public class Test {
 public static void main(String[] args) {
 // 測試隊列
 QueueTest queue = new QueueTest();
 
 queue.put("我是第1個進入隊列的");
 queue.put("我是第2個進入隊列的");
 queue.put("我是第3個進入隊列的");
 queue.put("我是第4個進入隊列的");
 
 // 遍歷隊列
 while (!queue.isEmpty()){
 String str = queue.get();
 System.out.println(str);
 }
 // 打印結果
 /*我是第1個進入隊列的
 我是第2個進入隊列的
 我是第3個進入隊列的
 我是第4個進入隊列的*/
 }
 
}

Vector:因為已經過時,被ArrayList取代了;它還有一種迭代器通過vector.elements()獲取,判斷是否有元素和取元素的方法為:hasMoreElements(),nextElement()。

package 好好學java;
import java.util.Enumeration;
import java.util.Vector;
public class Test {
 public static void main(String[] args) {
 Vector vector = new Vector();
 
 vector.add("搜索");
 vector.add("vector");
 vector.add("list");
 
 Enumeration elements = vector.elements();
 while (elements.hasMoreElements()){
 String nextElement = elements.nextElement();
 System.out.println(nextElement);
 }
 }
 
}

Set:

Set集合的特點:元素不重複,存取無序,無下標

Set集合下面有:HashSet,LinkedHashSet,TreeSet

HashSet存儲字符串:

package 好好學java;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Test {
 public static void main(String[] args) {
 // 利用HashSet來存取
 Set set = new HashSet();
 
 set.add("我的天");
 set.add("我是重複的");
 set.add("我是重複的");
 set.add("welcome");
 
 // 遍歷 第一種方式 迭代器
 Iterator it = set.iterator();
 while(it.hasNext()){
 String str = it.next();
 System.out.println(str);
 }
 
 System.out.println("--------------");
 for (String str : set){
 System.out.println(str);
 }
 // 打印結果,重複的已經去掉了
 /*我的天
 welcome
 我是重複的
 --------------
 我的天
 welcome
 我是重複的*/
 }
 

那哈希表是怎麼來保證元素的唯一性的呢,哈希表是通過hashCode和equals方法來共同保證的。

哈希表的存儲數據過程(哈希表底層也維護了一個數組):

根據存儲的元素計算出hashCode值,然後根據計算得出的hashCode值和數組的長度進行計算出存儲的下標;如果下標的位置無元素,那麼直接存儲;如果有元素,那麼使用要存入的元素和該元素進行equals方法,如果結果為真,則已經有相同的元素了,所以直接不存;如果結果假,那麼進行存儲,以鏈表的形式存儲。

演示HashSet來存儲自定義對象:

package 好好學java;
public class Person {
 // 屬性
 private String name;
 private int age;
 
 // 構造方法
 public Person() {
 super();
 
 }
 public Person(String name, int age) {
 super();
 this.name = name;
 this.age = age;
 }
 
 // 要讓哈希表存儲不重複的元素,就必須重寫hasCode和equals方法
 @Override
 public int hashCode() {
 final int prime = 31;
 int result = 1;
 result = prime * result + age;
 result = prime * result + ((name == null) ? 0 : name.hashCode());
 return result;
 }
 @Override
 public boolean equals(Object obj) {
 if (this == obj)
 return true;
 if (obj == null)
 return false;
 if (getClass() != obj.getClass())
 return false;
 Person other = (Person) obj;
 if (age != other.age)
 return false;
 if (name == null) {
 if (other.name != null)
 return false;
 } else if (!name.equals(other.name))
 return false;
 return true;
 }
 
 
 @Override
 public String toString() {
 return "Person [name=" + name + ", age=" + age + "]";
 }
 // getter & setter
 
 ...
 
}
package 好好學java;
import java.util.HashSet;
import java.util.Set;
public class Test {
 public static void main(String[] args) {
 // 利用HashSet來存取自定義對象 Person
 Set set = new HashSet();
 
 set.add(new Person("張三", 12));
 set.add(new Person("李四", 13));
 set.add(new Person("王五", 22));
 set.add(new Person("張三", 12));
 
 // 遍歷
 for (Person p : set){
 System.out.println(p);
 }
 // 結果:向集合中存儲兩個張三對象,但是集合中就成功存儲了一個
 /*Person [name=王五, age=22]
 Person [name=李四, age=13]
 Person [name=張三, age=12]*/
 }
 
}

所以在向HashSet集合中存儲自定義對象時,為了保證set集合的唯一性,那麼必須重寫hashCode和equals方法。

LinkedHashSet:

是基於鏈表和哈希表共同實現的,所以具有存取有序,元素唯一

package 好好學java;
import java.util.LinkedHashSet;
public class Test {
 public static void main(String[] args) {
 // 利用LinkedHashSet來存取自定義對象 Person
 LinkedHashSet set = new LinkedHashSet();
 
 set.add(new Person("張三", 12));
 set.add(new Person("李四", 13));
 set.add(new Person("王五", 22));
 set.add(new Person("張三", 12));
 
 // 遍歷
 for (Person p : set){
 System.out.println(p);
 }
 // 結果:向集合中存儲兩個張三對象,但是集合中就成功存儲了一個,
 // 並且存進的順序,和取出來的順序是一致的
 /*Person [name=張三, age=12]
 Person [name=李四, age=13]
 Person [name=王五, age=22]*/
 }
 
}
 

TreeSet:

特點:存取無序,元素唯一,可以進行排序(排序是在添加的時候進行排序)。

TreeSet是基於二叉樹的數據結構,二叉樹的:一個節點下不能多餘兩個節點。

二叉樹的存儲過程:

如果是第一個元素,那麼直接存入,作為根節點,下一個元素進來是會跟節點比較,如果大於節點放右邊的,小於節點放左邊;等於節點就不存儲。後面的元素進來會依次比較,直到有位置存儲為止

TreeSet集合存儲String對象

package 好好學java;
import java.util.TreeSet;
public class Test {
 public static void main(String[] args) {
 TreeSet treeSet = new TreeSet();
 treeSet.add("abc");
 treeSet.add("zbc");
 treeSet.add("cbc");
 treeSet.add("xbc");
 
 for (String str : treeSet){
 System.out.println(str);
 }
 // 結果:取出來的結果是經過排序的
 /*
 abc
 cbc
 xbc
 zbc*/
 }
 
}

TreeSet保證元素的唯一性是有兩種方式:

1、自定義對象實現Comparable接口,重寫comparaTo方法,該方法返回0表示相等,小於0表示準備存入的元素比被比較的元素小,否則大於0;

2、在創建TreeSet的時候向構造器中傳入比較器Comparator接口實現類對象,實現Comparator接口重寫compara方法。

如果向TreeSet存入自定義對象時,自定義類沒有實現Comparable接口,或者沒有傳入Comparator比較器時,會出現ClassCastException異常

下面就是演示用兩種方式來存儲自定義對象

package 好好學java;
public class Person implements Comparable{
 // 屬性
 private String name;
 private int age;
 
 // 構造方法
 public Person() {
 super();
 
 }
 public Person(String name, int age) {
 super();
 this.name = name;
 this.age = age;
 }
 
 // 要讓哈希表存儲不重複的元素,就必須重寫hasCode和equals方法
 @Override
 public int hashCode() {
 final int prime = 31;
 int result = 1;
 result = prime * result + age;
 result = prime * result + ((name == null) ? 0 : name.hashCode());
 return result;
 }
 @Override
 public boolean equals(Object obj) {
 if (this == obj)
 return true;
 if (obj == null)
 return false;
 if (getClass() != obj.getClass())
 return false;
 Person other = (Person) obj;
 if (age != other.age)
 return false;
 if (name == null) {
 if (other.name != null)
 return false;
 } else if (!name.equals(other.name))
 return false;
 return true;
 }
 
 
 @Override
 public String toString() {
 return "Person [name=" + name + ", age=" + age + "]";
 }
 // getter & setter
 ...
 
 @Override
 public int compareTo(Person o) {
 int result = this.age - o.age;
 if (result == 0){
 return this.name.compareTo(o.name);
 }
 return result;
 }
 
 
}
package 好好學java;
import java.util.TreeSet;
public class Test {
 public static void main(String[] args) {
 // 利用TreeSet來存儲自定義類Person對象
 TreeSet treeSet = new TreeSet();
 // Person類實現了Comparable接口,並且重寫comparaTo方法
 // 比較規則是先按照 年齡排序,年齡相等的情況按照年齡排序
 treeSet.add(new Person("張山1", 20));
 treeSet.add(new Person("張山2", 16));
 treeSet.add(new Person("張山3", 13));
 treeSet.add(new Person("張山4", 17));
 treeSet.add(new Person("張山5", 20));
 
 for (Person p : treeSet){
 System.out.println(p);
 }
 // 結果:按照comparaTo方法內的邏輯來排序的
 /*
 Person [name=張山3, age=13]
 Person [name=張山2, age=16]
 Person [name=張山4, age=17]
 Person [name=張山1, age=20]
 Person [name=張山5, age=20]
 */
 
 }
 
}

另一種方式:使用比較器Comparator

package 好好學java;
public class Person{
 // 屬性
 private String name;
 private int age;
 
 // 構造方法
 public Person() {
 super();
 
 }
 public Person(String name, int age) {
 super();
 this.name = name;
 this.age = age;
 }
 
 // 要讓哈希表存儲不重複的元素,就必須重寫hasCode和equals方法
 @Override
 public int hashCode() {
 final int prime = 31;
 int result = 1;
 result = prime * result + age;
 result = prime * result + ((name == null) ? 0 : name.hashCode());
 return result;
 }
 @Override
 public boolean equals(Object obj) {
 if (this == obj)
 return true;
 if (obj == null)
 return false;
 if (getClass() != obj.getClass())
 return false;
 Person other = (Person) obj;
 if (age != other.age)
 return false;
 if (name == null) {
 if (other.name != null)
 return false;
 } else if (!name.equals(other.name))
 return false;
 return true;
 }
 
 
 @Override
 public String toString() {
 return "Person [name=" + name + ", age=" + age + "]";
 }
 // getter & setter
 ...
 
}
package 好好學java;
import java.util.Comparator;
import java.util.TreeSet;
public class Test {
 public static void main(String[] args) {
 // 利用TreeSet來存儲自定義類Person對象
 // 創建TreeSet對象的時候傳入Comparator比較器,使用匿名內部類的方式
 // 比較規則是先按照 年齡排序,年齡相等的情況按照年齡排序
 TreeSet treeSet = new TreeSet(new Comparator() {
 @Override
 public int compare(Person o1, Person o2) {
 if (o1 == o2){
 return 0;
 }
 int result = o1.getAge() - o2.getAge();
 if (result == 0){
 return o1.getName().compareTo(o2.getName());
 }
 return result;
 }
 
 });
 treeSet.add(new Person("張山1", 20));
 treeSet.add(new Person("張山2", 16));
 treeSet.add(new Person("張山3", 13));
 treeSet.add(new Person("張山4", 17));
 treeSet.add(new Person("張山5", 20));
 
 for (Person p : treeSet){
 System.out.println(p);
 }
 // 結果:按照compara方法內的邏輯來排序的
 /*
 Person [name=張山3, age=13]
 Person [name=張山2, age=16]
 Person [name=張山4, age=17]
 Person [name=張山1, age=20]
 Person [name=張山5, age=20]
 */
 
 }
 
}
 

比較器總結:

Collection體系總結:

  • List : "特點 :" 存取有序,元素有索引,元素可以重複.
  • ArrayList : 數組結構,查詢快,增刪慢,線程不安全,因此效率高.
  • Vector : 數組結構,查詢快,增刪慢,線程安全,因此效率低.
  • LinkedList : 鏈表結構,查詢慢,增刪快,線程不安全,因此效率高.
 addFirst() removeFirst() getFirst()
  • Set :"特點 :" 存取無序,元素無索引,元素不可以重複.
  • HashSet : 存儲無序,元素無索引,元素不可以重複.底層是哈希表.

請問 : 哈希表如何保證元素唯一呢 ? 底層是依賴 hashCode 和 equals 方法.

當存儲元素的時候,先根據 hashCode + 數組長度 計算出一個索引,判斷索引位置是否有元素.

如果沒有元素,直接存儲,如果有元素,先判斷 equals 方法,比較兩個元素是否相同,不同則存儲,相同則捨棄.

我們自定義對象存儲的元素一定要實現 hashCode 和 equals.

  • LinkedHashSet : 存儲有序,元素不可以重複.
  • TreeSet : 存取無序, 但是可以排序 (自然排序), 元素不可以重複.

有兩種排序方式 :

  • 自然排序 :

我們的元素必須實現 Comparable 接口.可比較的.實現 CompareTo 方法.

  • 比較器排序 :

我們需要自定義類,實現Comparetor接口,這個類就是比較器實現 compare 方法.

然後在創建 TreeSet 的時候,把比較器對象作為參數傳遞給 TreeSet.

Map:

Map是一個雙列集合,其中保存的是鍵值對,鍵要求保持唯一性,值可以重複

鍵值是一一對應的,一個鍵只能對應一個值

Map的特點:是存取無序,鍵不可重複

Map在存儲的時候,將鍵值傳入Entry,然後存儲Entry對象

其中下面有HashMap,LinkedHashMap和TreeMap

HashMap:

是基於哈希表結構實現的,所以存儲自定義對象作為鍵時,必須重寫hasCode和equals方法。存取無序的

下面演示HashMap以自定義對象作為鍵:

package 好好學java;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
public class Test {
 public static void main(String[] args) {
 // 利用HashMap存儲,自定義對象Person作為鍵
 // 為了保證鍵的唯一性,必須重寫hashCode和equals方法
 HashMap map = new HashMap();
 
 map.put(new Person("張三", 12), "JAVA");
 map.put(new Person("李四", 13), "IOS");
 map.put(new Person("小花", 22), "JS");
 map.put(new Person("小黑", 32), "PHP");
 map.put(new Person("張三", 12), "C++");
 
 Set> entrySet = map.entrySet();
 Iterator> it = entrySet.iterator();
 while (it.hasNext()){
 Entry entry = it.next();
 System.out.println(entry.getKey() + "---" + entry.getValue());
 }
 // 結果:存入的時候添加了兩個張三,如果Map中鍵相同的時候,當後面的值會覆蓋掉前面的值
 /*
 Person [name=李四, age=13]---IOS
 Person [name=張三, age=12]---C++
 Person [name=小黑, age=32]---PHP
 Person [name=小花, age=22]---JS
 */
 
 }
 
}
 

LinkedHashMap:

用法跟HashMap基本一致,它是基於鏈表和哈希表結構的所以具有存取有序,鍵不重複的特性

下面演示利用LinkedHashMap存儲,注意存的順序和遍歷出來的順序是一致的:

package 好好學java;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
public class Test {
 public static void main(String[] args) {
 // 利用LinkedHashMap存儲,自定義對象Person作為鍵
 // 為了保證鍵的唯一性,必須重寫hashCode和equals方法
 LinkedHashMap map = new LinkedHashMap();
 
 map.put(new Person("張三", 12), "JAVA");
 map.put(new Person("李四", 13), "IOS");
 map.put(new Person("小花", 22), "JS");
 map.put(new Person("小黑", 32), "PHP");
 map.put(new Person("張三", 12), "C++");
 
 // foreach遍歷
 for (Entry entry : map.entrySet()){
 System.out.println(entry.getKey()+"==="+entry.getValue());
 }
 // 結果:存入的時候添加了兩個張三,如果Map中鍵相同的時候,當後面的值會覆蓋掉前面的值
 // 注意:LinkedHashMap的特點就是存取有序,取出來的順序就是和存入的順序保持一致
 /*
 Person [name=張三, age=12]===C++
 Person [name=李四, age=13]===IOS
 Person [name=小花, age=22]===JS
 Person [name=小黑, age=32]===PHP
 */
 }
}
 
 
java集合入門和深入學習(詳解),看這篇就差不多了

TreeMap:

給TreeMap集合中保存自定義對象,自定義對象作為TreeMap集合的key值。由於TreeMap底層使用的二叉樹,其中存放進去的所有數據都需要排序,要排序,就要求對象具備比較功能。對象所屬的類需要實現Comparable接口。或者給TreeMap集合傳遞一個Comparator接口對象。

利用TreeMap存入自定義對象作為鍵:

package 好好學java;
import java.util.Comparator;
import java.util.Map.Entry;
import java.util.TreeMap;
public class Test {
 public static void main(String[] args) {
 // 利用TreeMap存儲,自定義對象Person作為鍵
 // 自定義對象實現Comparable接口或者傳入Comparator比較器
 TreeMap map = new TreeMap(new Comparator() {
 @Override
 public int compare(Person o1, Person o2) {
 if (o1 == o2){
 return 0;
 }
 int result = o1.getAge() - o2.getAge();
 if (result == 0){
 return o1.getName().compareTo(o2.getName());
 }
 return result;
 }
 });
 
 map.put(new Person("張三", 12), "JAVA");
 map.put(new Person("李四", 50), "IOS");
 map.put(new Person("小花", 32), "JS");
 map.put(new Person("小黑", 32), "PHP");
 map.put(new Person("張三", 12), "C++");
 
 // foreach遍歷
 for (Entry entry : map.entrySet()){
 System.out.println(entry.getKey()+"==="+entry.getValue());
 }
 // 結果:存入的時候添加了兩個張三,如果Map中鍵相同的時候,當後面的值會覆蓋掉前面的值
 // 注意:TreeMap 取出來的順序是經過排序的,是根據compara方法排序的
 /*
 Person [name=張三, age=12]===C++
 Person [name=小花, age=32]===JS
 Person [name=小黑, age=32]===PHP
 Person [name=李四, age=50]===IOS
 */
 }
}

二、集合進階總結

數組和第一類對象

無論使用的數組屬於什麼類型,數組標識符實際都是指向真實對象的一個句柄。那些對象本身是在內存

“堆”裡創建的。堆對象既可“隱式”創建(即默認產生),亦可“顯式”創建(即明確指定,用一個 new

表達式)。堆對象的一部分(實際是我們能訪問的唯一字段或方法)是隻讀的length(長度)成員,它告訴

我們那個數組對象裡最多能容納多少元素。對於數組對象,“ []”語法是我們能採用的唯一另類訪問方法。

對象數組和基本數據類型數組在使用方法上幾乎是完全一致的。唯一的差別在於對象數組容納的是句柄,而基本數據類型數組容納的是具體的數值

public class ArraySize {
 public static void main(String[] args) {
 // Arrays of objects:
 Weeble[] a; // Null handle
 Weeble[] b = new Weeble[5]; // Null handles
 Weeble[] c = new Weeble[4];
 for (int i = 0; i < c.length; i++)
 c[i] = new Weeble();
 Weeble[] d = { new Weeble(), new Weeble(), new Weeble() };
 // Compile error: variable a not initialized:
 // !System.out.println("a.length=" + a.length);
 System.out.println("b.length = " + b.length);
 // The handles inside the array are
 // automatically initialized to null:
 for (int i = 0; i < b.length; i++)
 System.out.println("b[" + i + "]=" + b[i]);
 System.out.println("c.length = " + c.length);
 System.out.println("d.length = " + d.length);
 a = d;
 System.out.println("a.length = " + a.length);
 // Java 1.1 initialization syntax:
 a = new Weeble[] { new Weeble(), new Weeble() };
 System.out.println("a.length = " + a.length);
 // Arrays of primitives:
 int[] e; // Null handle
 int[] f = new int[5];
 int[] g = new int[4];
 for (int i = 0; i < g.length; i++)
 g[i] = i * i;
 int[] h = { 11, 47, 93 };
 // Compile error: variable e not initialized:
 // !System.out.println("e.length=" + e.length);
 System.out.println("f.length = " + f.length);
 // The primitives inside the array are
 // automatically initialized to zero:
 for (int i = 0; i < f.length; i++)
 System.out.println("f[" + i + "]=" + f[i]);
 System.out.println("g.length = " + g.length);
 System.out.println("h.length = " + h.length);
 e = h;
 System.out.println("e.length = " + e.length);
 // Java 1.1 initialization syntax:
 e = new int[] { 1, 2 };
 System.out.println("e.length = " + e.length);
 }
}

輸出如下:

b.length = 5

b[0]=null

b[1]=null

b[2]=null

b[3]=null

b[4]=null

c.length = 4

d.length = 3

a.length = 3

a.length = 2

f.length = 5

f[0]=0

f[1]=0

f[2]=0

f[3]=0

f[4]=0

g.length = 4

h.length = 3

e.length = 3

e.length = 2

其中,數組 a 只是初始化成一個 null 句柄。此時,編譯器會禁止我們對這個句柄作任何實際操作,除非已正

確地初始化了它。數組 b 被初始化成指向由 Weeble 句柄構成的一個數組,但那個數組裡實際並未放置任何

Weeble 對象。然而,我們仍然可以查詢那個數組的大小,因為 b 指向的是一個合法對象。

換言之,我們只知道數組對象的大小或容量,不知其實際容納了多少個元素。

儘管如此,由於數組對象在創建之初會自動初始化成 null,所以可檢查它是否為 null,判斷一個特定的數組“空位”是否容納一個對象。類似地,

由基本數據類型構成的數組會自動初始化成零(針對數值類型)、 null(字符類型)或者false(布爾類型)

數組 c 顯示出我們首先創建一個數組對象,再將 Weeble 對象賦給那個數組的所有“空位”。數組 d 揭示出

“集合初始化”語法,從而創建數組對象(用 new 命令明確進行,類似於數組 c),然後用 Weeble 對象進行

初始化,全部工作在一條語句裡完成。

下面這個表達式:

a = d;

向我們展示瞭如何取得同一個數組對象連接的句柄,然後將其賦給另一個數組對象,向我們展示瞭如何取得同一個數組對象連接的句柄,然後將其賦給另一個數組對象

1.基本數據類型集合

集合類只能容納對象句柄。但對一個數組,卻既可令其直接容納基本類型的數據,亦可容納指向對象的句

柄。利用象 Integer、 Double 之類的“ 封裝器”類,可將基本數據類型的值置入一個集合裡。

無論將基本類型的數據置入數組,還是將其封裝進入位於集合的一個類內,都涉及到執行效率的問題。顯

然,若能創建和訪問一個基本數據類型數組,那麼比起訪問一個封裝數據的集合,前者的效率會高出許多。

數組的返回

假定我們現在想寫一個方法,同時不希望它僅僅返回一樣東西,而是想返回一系列東西。此時,象C 和 C++這樣的語言會使問題複雜化,因為我們不能返回一個數組,只能返回指向數組的一個指針。這樣就非常麻煩,因為很難控制數組的“存在時間”,它很容易造成內存“漏洞”的出現。

Java 採用的是類似的方法,但我們能“返回一個數組”。當然,此時返回的實際仍是指向數組的指針。但在Java 裡,我們永遠不必擔心那個數組的是否可用—— 只要需要,它就會自動存在。而且垃圾收集器會在我們完成後自動將其清除

public class IceCream {
 static String[] flav = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl",
 "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream",
 "Mud Pie" };
 static String[] flavorSet(int n) {
 // Force it to be positive & within bounds:
 n = Math.abs(n) % (flav.length + 1);
 String[] results = new String[n];
 int[] picks = new int[n];
 for(int i = 0; i < picks.length; i++)
 picks[i] = -1;
 for(int i = 0; i < picks.length; i++) {
 retry:
 while(true) {
 int t =(int)(Math.random() * flav.length);
 for(int j = 0; j < i; j++)213
 if(picks[j] == t) continue retry;
 picks[i] = t;
 results[i] = flav[t];
 break;
 }
 }
 return results;
 }
 public static void main(String[] args) {
 for (int i = 0; i < 20; i++) {
 System.out.println("flavorSet(" + i + ") = ");
 String[] fl = flavorSet(flav.length);
 for (int j = 0; j < fl.length; j++)
 System.out.println("\t" + fl[j]);
 }
 }
}
 

flavorSet()方法創建了一個名為 results 的 String 數組。該數組的大小為 n—— 具體數值取決於我們傳遞給方法的自變量。隨後,它從數組 flav 裡隨機挑選一些“香料”( Flavor),並將它們置入 results 裡,並最終返回 results。返回數組與返回其他任何對象沒什麼區別—— 最終返回的都是一個句柄。

另一方面,注意當 flavorSet()隨機挑選香料的時候,它需要保證以前出現過的一次隨機選擇不會再次出現。為達到這個目的,它使用了一個無限 while 循環,不斷地作出隨機選擇,直到發現未在 picks 數組裡出現過的一個元素為止(當然,也可以進行字串比較,檢查隨機選擇是否在 results 數組裡出現過,但字串比較的效率比較低)。若成功,就添加這個元素,並中斷循環( break),再查找下一個( i 值會遞增)。但假若 t 是一個已在 picks 裡出現過的數組,就用標籤式的 continue 往回跳兩級,強制選擇一個新 t。 用一個調試程序可以很清楚地看到這個過程。

集合

為容納一組對象,最適宜的選擇應當是數組。而且假如容納的是一系列基本數據類型,更是必須採用數組。

缺點:

類型未知

使用 Java 集合的“缺點”是在將對象置入一個集合時丟失了類型信息。之所以會發生這種情況,是由於當初編寫集合時,那個集合的程序員根本不知道用戶到底想把什麼類型置入集合。若指示某個集合只允許特定的類型,會妨礙它成為一個“常規用途”的工具,為用戶帶來麻煩。為解決這個問題,集合實際容納的是類型為 Object 的一些對象的句柄。

當然,也要注意集合並不包括基本數據類型,因為它們並不是從“任何東西”繼承來的。

Java 不允許人們濫用置入集合的對象。假如將一條狗扔進一個貓的集合,那麼仍會將集合內的所有東西都看作貓,所以在使用那條狗時會得到一個“違例”錯誤。在同樣的意義上,假若試圖將一條狗的句柄“造型”到一隻貓,那麼運行期間仍會得到一個“違例”錯誤

class Cat {
 private int catNumber;
 Cat(int i) {
 catNumber = i;
 }
 void print() {
 System.out.println("Cat #" + catNumber);
 }
}
class Dog {
 private int dogNumber;
 Dog(int i) {
 dogNumber = i;
 }
 void print() {
 System.out.println("Dog #" + dogNumber);
 }
}
public class CatsAndDogs {
 public static void main(String[] args) {
 Vector cats = new Vector();
 for (int i = 0; i < 7; i++)
 cats.addElement(new Cat(i));
 // Not a problem to add a dog to cats:
 cats.addElement(new Dog(7));
 for (int i = 0; i < cats.size(); i++)
 ((Cat) cats.elementAt(i)).print();
 // Dog is detected only at run-time
 }
}
  • 錯誤有時並不顯露出來
  • 在某些情況下,程序似乎正確地工作,不造型回我們原來的類型。第一種情況是相當特殊的: String 類從編譯器獲得了額外的幫助,使其能夠正常工作。只要編譯器期待的是一個String 對象,但它沒有得到一個,就會自動調用在 Object 裡定義、並且能夠由任何 Java 類覆蓋的 toString()方法。這個方法能生成滿足要求的String 對象,然後在我們需要的時候使用。因此,為了讓自己類的對象能顯示出來,要做的全部事情就是覆蓋toString()方法。
class Mouse {
 private int mouseNumber;
 Mouse(int i) {
 mouseNumber = i;
 }
 // Magic method:
 public String toString() {
 return "This is Mouse #" + mouseNumber;
 }
 void print(String msg) {
 if (msg != null)
 System.out.println(msg);
 System.out.println("Mouse number " + mouseNumber);
 }
}
class MouseTrap {
 static void caughtYa(Object m) {
 Mouse mouse = (Mouse) m; // Cast from Object
 mouse.print("Caught one!");
 }
}
public class WorksAnyway {
 public static void main(String[] args) {
 Vector mice = new Vector();
 for(int i = 0; i < 3; i++)
 mice.addElement(new Mouse(i));
 for(int i = 0; i < mice.size(); i++) {
 // No cast necessary, automatic call
 // to Object.toString():
 System.out.println(
 "Free mouse: " + mice.elementAt(i));
 MouseTrap.caughtYa(mice.elementAt(i));
 }
 }
}

可在 Mouse 裡看到對 toString()的重定義代碼。在 main()的第二個 for 循環中,可發現下述語句:

System.out.println("Free mouse: " +
mice.elementAt(i));

在“ +”後,編譯器預期看到的是一個 String 對象。 elementAt()生成了一個 Object,所以為獲得希望的String,編譯器會默認調用 toString()。但不幸的是,只有針對 String 才能得到象這樣的結果;其他任何類型都不會進行這樣的轉換。

隱藏造型的第二種方法已在 Mousetrap 裡得到了應用。 caughtYa()方法接收的不是一個 Mouse,而是一個Object。隨後再將其造型為一個 Mouse。當然,這樣做是非常冒失的,因為通過接收一個 Object,任何東西都可以傳遞給方法。然而,假若造型不正確—— 如果我們傳遞了錯誤的類型—— 就會在運行期間得到一個違例錯誤。這當然沒有在編譯期進行檢查好,但仍然能防止問題的發生。注意在使用這個方法時毋需進行造型:

MouseTrap.caughtYa(mice.elementAt(i));

  • 生成能自動判別類型的 Vector
  • 一個更“健壯”的方案是用 Vector 創建一個新類,使其只接收我們指定的
  • 類型,也只生成我們希望的類型。
class Gopher {
 private int gopherNumber;
 Gopher(int i) {
 gopherNumber = i;
 }
 void print(String msg) {
 if (msg != null)
 System.out.println(msg);
 System.out.println("Gopher number " + gopherNumber);
 }
}
class GopherTrap {
 static void caughtYa(Gopher g) {
 g.print("Caught one!");
 }
}
class GopherVector {
 private Vector v = new Vector();
 public void addElement(Gopher m) {
 v.addElement(m);
 }
 public Gopher elementAt(int index) {
 return (Gopher) v.elementAt(index);
 }
 public int size() {
 return v.size();
 }
 public static void main(String[] args) {
 GopherVector gophers = new GopherVector();
 for (int i = 0; i < 3; i++)
 gophers.addElement(new Gopher(i));
 for (int i = 0; i < gophers.size(); i++)
 GopherTrap.caughtYa(gophers.elementAt(i));
 }
}

新的 GopherVector 類有一個類型為 Vector 的 private 成員(從 Vector 繼承有些麻煩,理由稍後便知),而且方法也和 Vector 類似。然而,它不會接收和產生普通 Object,只對 Gopher 對象

感興趣。

由於 GopherVector 只接收一個 Gopher(地鼠),所以假如我們使用:

gophers.addElement(new Pigeon());

就會在編譯期間獲得一條出錯消息。採用這種方式,儘管從編碼的角度看顯得更令人沉悶,但可以立即判斷出是否使用了正確的類型。注意在使用 elementAt()時不必進行造型—— 它肯定是一個 Gopher

枚舉器

容納各種各樣的對象正是集合的首要任務。在 Vector 中, addElement()便是我們插入對象採用的方法,而 elementAt()是

提取對象的唯一方法。 Vector 非常靈活,我們可在任何時候選擇任何東西,並可使用不同的索引選擇多個元素。

若從更高的角度看這個問題,就會發現它的一個缺陷:需要事先知道集合的準確類型,否則無法使用。乍看來,這一點似乎沒什麼關係。但假若最開始決定使用Vector,後來在程序中又決定(考慮執行效率的原因)改變成一個 List(屬於 Java1.2 集合庫的一部分),這時又該如何做呢?

我們通常認為反覆器是一種“輕量級”對象;也就是說,創建它只需付出極少的代價。但也正是由於這個原因,我們常發現反覆器存在一些似乎很奇怪的限制。例如,有些反覆器只能朝一個方向移動。

Java 的 Enumeration(枚舉,註釋②)便是具有這些限制的一個反覆器的例子。除下面這些外,不可再用它

做其他任何事情:

(1) 用一個名為 elements()的方法要求集合為我們提供一個 Enumeration。我們首次調用它的 nextElement()

時,這個 Enumeration 會返回序列中的第一個元素。

(2) 用 nextElement() 獲得下一個對象。

(3) 用 hasMoreElements()檢查序列中是否還有更多的對象

class Hamster {
 private int hamsterNumber;
 Hamster(int i) {
 hamsterNumber = i;
 }
 public String toString() {
 return "This is Hamster #" + hamsterNumber;
 }
}
class Printer {
 static void printAll(Enumeration e) {
 while (e.hasMoreElements())
 System.out.println(e.nextElement().toString());
 }
}
public class HamsterMaze {
 public static void main(String[] args) {
 Vector v = new Vector();
 for (int i = 0; i < 3; i++)
 v.addElement(new Hamster(i));
 Printer.printAll(v.elements());
 }
}
 

仔細研究一下打印方法:

static void printAll(Enumeration e) {
while(e.hasMoreElements())
System.out.println(
e.nextElement().toString());
}

注意其中沒有與序列類型有關的信息。我們擁有的全部東西便是Enumeration。為了解有關序列的情況,一個 Enumeration 便足夠了:可取得下一個對象,亦可知道是否已抵達了末尾。取得一系列對象,然後在其中遍歷,從而執行一個特定的操作—— 這是一個頗有價值的編程概念

集合的類型

V e c t o r

崩潰 Java

Java 標準集合裡包含了 toString()方法,所以它們能生成自己的 String 表達方式,包括它們容納的對象。

例如在 Vector 中, toString()會在 Vector 的各個元素中步進和遍歷,併為每個元素調用 toString()。假定我們現在想打印出自己類的地址。看起來似乎簡單地引用 this 即可(特別是 C++程序員有這樣做的傾向):

public class CrashJava {
 public String toString() {
 return "CrashJava address: " + this + "\n";
 }
 public static void main(String[] args) {
 Vector v = new Vector();
 for (int i = 0; i < 10; i++)
 v.addElement(new CrashJava());
 System.out.println(v);
 }
}

此時發生的是字串的自動類型轉換。當我們使用下述語句時:

“CrashJava address: ” + this

編譯器就在一個字串後面發現了一個“ +”以及好象並非字串的其他東西,所以它會試圖將 this 轉換成一個字串。轉換時調用的是 toString(),後者會產生一個遞歸調用。若在一個 Vector 內出現這種事情,看起來堆棧就會溢出,同時違例控制機制根本沒有機會作出響應。

若確實想在這種情況下打印出對象的地址,解決方案就是調用 Object 的 toString 方法。此時就不必加入this,只需使用 super.toString()。當然,採取這種做法也有一個前提:我們必須從 Object 直接繼承,或者沒有一個父類覆蓋了 toString 方法。

B i t S e t

BitSet 實際是由“ 二進制位”構成的一個 Vector。如果希望高效率地保存大量“開-關”信息,就應使用BitSet。它只有從尺寸的角度看才有意義;如果希望的高效率的訪問,那麼它的速度會比使用一些固有類型的數組慢一些。

BitSet 的最小長度是一個長整數( Long)的長度: 64 位。這意味著假如我們準備保存比這更小的數據,如 8 位數據,那麼 BitSet 就顯得浪費了。所以最好創建自己的類,用它容納自己的標誌位。

S t a c k

Stack 有時也可以稱為“後入先出”( LIFO)集合。換言之,我們在堆棧裡最後“壓入”的東西將是以後第

一個“彈出”的。和其他所有 Java 集合一樣,我們壓入和彈出的都是“對象”,所以必須對自己彈出的東西

進行“造型”。

下面是一個簡單的堆棧示例,它能讀入數組的每一行,同時將其作為字串壓入堆棧。

public class Stacks {
 static String[] months = { "January", "February", "March", "April", "May",
 "June", "July", "August", "September", "October", "November",
 "December" };
 public static void main(String[] args) {
 Stack stk = new Stack();
 for (int i = 0; i < months.length; i++)
 stk.push(months[i] + " ");
 System.out.println("stk = " + stk);
 // Treating a stack as a Vector:
 stk.addElement("The last line");
 System.out.println("element 5 = " + stk.elementAt(5));
 System.out.println("popping elements:");
 while (!stk.empty())
 System.out.println(stk.pop());
 }
}

months 數組的每一行都通過 push()繼承進入堆棧,稍後用 pop()從堆棧的頂部將其取出。要聲明的一點是,Vector 操作亦可針對 Stack 對象進行。這可能是由繼承的特質決定的—— Stack“屬於”一種 Vector。因此,能對 Vector 進行的操作亦可針對 Stack 進行,例如 elementAt()方法

H a s h t a b l e

Vector 允許我們用一個數字從一系列對象中作出選擇,所以它實際是將數字同對象關聯起來了。

但假如我們想根據其他標準選擇一系列對象呢?堆棧就是這樣的一個例子:它的選擇標準是“最後壓入堆棧的東西”。

這種“從一系列對象中選擇”的概念亦可叫作一個“映射”、“字典”或者“關聯數組”。從概念上講,它看起來象一個 Vector,但卻不是通過數字來查找對象,而是用另一個對象來查找它們!這通常都屬於一個程序中的重要進程。

在 Java 中,這個概念具體反映到抽象類 Dictionary 身上。該類的接口是非常直觀的 size()告訴我們其中包含了多少元素; isEmpty()判斷是否包含了元素(是則為 true); put(Object key, Object value)添加一個值(我們希望的東西),並將其同一個鍵關聯起來(想用於搜索它的東西); get(Object key)獲得與某個鍵對應的值;而 remove(Object Key)用於從列表中刪除“鍵-值”對。還可以使用枚舉技術: keys()產生對鍵的一個枚舉( Enumeration);而 elements()產生對所有值的一個枚舉。這便是一個 Dict ionary(字典)的全部。

public class AssocArray extends Dictionary {
 private Vector keys = new Vector();
 private Vector values = new Vector();
 public int size() {
 return keys.size();
 }
 public boolean isEmpty() {
 return keys.isEmpty();
 }
 public Object put(Object key, Object value) {
 keys.addElement(key);
 values.addElement(value);
 return key;
 }
 public Object get(Object key) {
 int index = keys.indexOf(key);
 // indexOf() Returns -1 if key not found:
 if (index == -1)
 return null;
 return values.elementAt(index);
 }
 public Object remove(Object key) {
 int index = keys.indexOf(key);
 if (index == -1)
 return null;
 keys.removeElementAt(index);
 Object returnval = values.elementAt(index);
 values.removeElementAt(index);
 return returnval;
 }
 public Enumeration keys() {
 return keys.elements();
 }
 public Enumeration elements() {
 return values.elements();
 }
 // Test it:
 public static void main(String[] args) {
 AssocArray aa = new AssocArray();
 for (char c = 'a'; c <= 'z'; c++)
 aa.put(String.valueOf(c), String.valueOf(c).toUpperCase());
 char[] ca = { 'a', 'e', 'i', 'o', 'u' };
 for (int i = 0; i < ca.length; i++)
 System.out.println("Uppercase: " + aa.get(String.valueOf(ca[i])));
 }
}

在對 AssocArray 的定義中,我們注意到的第一個問題是它“擴展”了字典。這意味著 AssocArray 屬於Dictionary 的一種類型,所以可對其發出與 Dictionary 一樣的請求。如果想生成自己的 Dictionary,而且就在這裡進行,那麼要做的全部事情只是填充位於 Dictionary 內的所有方法(而且必須覆蓋所有方法,因為

它們—— 除構建器外—— 都是抽象的)。

標準 Java 庫只包含 Dictionary 的一個變種,名為 Hashtable(散列表,註釋③)。 Java 的散列表具有與AssocArray 相同的接口(因為兩者都是從 Dictionary 繼承來的)。但有一個方面卻反映出了差別:執行效率。若仔細想想必須為一個 get()做的事情,就會發現在一個 Vector 裡搜索鍵的速度要慢得多。但此時用散列表卻可以加快不少速度。不必用冗長的線性搜索技術來查找一個鍵,而是用一個特殊的值,名為“散列碼”。散列碼可以獲取對象中的信息,然後將其轉換成那個對象“相對唯一”的整數( int)。所有對象都有一個散列碼,而 hashCode()是根類 Object 的一個方法。 Hashtable 獲取對象的 hashCode(),然後用它快速查找鍵。

class Counter {
 int i = 1;
 public String toString() {
 return Integer.toString(i);
 }
}
class Statistics {
 public static void main(String[] args) {
 Hashtable ht = new Hashtable();
 for (int i = 0; i < 10000; i++) {
 // Produce a number between 0 and 20:
 Integer r = new Integer((int) (Math.random() * 20));
 if (ht.containsKey(r))
 ((Counter) ht.get(r)).i++;
 else
 ht.put(r, new Counter());
 }
 System.out.println(ht);
 }
}
  • 創建“關鍵”類
  • 但在使用散列表的時候,一旦我們創建自己的類作為鍵使
  • 用,就會遇到一個很常見的問題。例如,假設一套天氣預報系統將Groundhog(土拔鼠)對象匹配成Prediction(預報) 。這看起來非常直觀:我們創建兩個類,然後將Groundhog 作為鍵使用,而將Prediction 作為值使用。如下所示:
class Groundhog {
 int ghNumber;
 Groundhog(int n) {
 ghNumber = n;
 }
}
class Prediction {
 boolean shadow = Math.random() > 0.5;
 public String toString() {
 if (shadow)
 return "Six more weeks of Winter!";
 else
 return "Early Spring!";
 }
}
public class SpringDetector {
 public static void main(String[] args) {
 Hashtable ht = new Hashtable();
 for (int i = 0; i < 10; i++)
 ht.put(new Groundhog(i), new Prediction());
 System.out.println("ht = " + ht + "\n");
 System.out.println("Looking up prediction for groundhog #3:");
 Groundhog gh = new Groundhog(3);
 if (ht.containsKey(gh))
 System.out.println((Prediction) ht.get(gh));
 }
}

問題在於Groundhog 是從通用的 Object 根類繼承的(若當初未指

定基礎類,則所有類最終都是從 Object 繼承的)。事實上是用 Object 的 hashCode()方法生成每個對象的散列碼,而且默認情況下只使用它的對象的地址。所以, Groundhog(3)的第一個實例並不會產生與Groundhog(3)第二個實例相等的散列碼,而我們用第二個實例進行檢索

或許認為此時要做的全部事情就是正確地覆蓋 hashCode()。但這樣做依然行不能,除非再做另一件事情:覆蓋也屬於 Object 一部分的 equals()。當散列表試圖判斷我們的鍵是否等於表內的某個鍵時,就會用到這個方法。同樣地,默認的 Object.equals()只是簡單地比較對象地址,所以一個 Groundhog(3)並不等於

另一個 Groundhog(3)。

因此,為了在散列表中將自己的類作為鍵使用,必須同時覆蓋 hashCode()和 equals(),就象下面展示的那樣:

class Groundhog {
 int ghNumber;
 Groundhog(int n) {
 ghNumber = n;
 }
}
class Prediction {
 boolean shadow = Math.random() > 0.5;
 public String toString() {
 if (shadow)
 return "Six more weeks of Winter!";
 else
 return "Early Spring!";
 }
}
public class SpringDetector {
 public static void main(String[] args) {
 Hashtable ht = new Hashtable();
 for (int i = 0; i < 10; i++)
 ht.put(new Groundhog(i), new Prediction());
 System.out.println("ht = " + ht + "\n");
 System.out.println("Looking up prediction for groundhog #3:");
 Groundhog gh = new Groundhog(3);
 if (ht.containsKey(gh))
 System.out.println((Prediction) ht.get(gh));
 }
}

Groundhog2.hashCode()將土拔鼠號碼作為一個標識符返回(在這個例子中,程序員需要保證沒有兩個土拔鼠用同樣的 ID 號碼並存)。為了返回一個獨一無二的標識符,並不需要 hashCode(), equals()方法必須能夠嚴格判斷兩個對象是否相等。

equals()方法要進行兩種檢查:檢查對象是否為 null;若不為 null ,則繼續檢查是否為 Groundhog2 的一個實例(要用到 instanceof 關鍵字)。即使為了繼續執行 equals(),它也應該是一個Groundhog2。正如大家看到的那樣,這種比較建立在實際 ghNumber 的基礎上。這一次一旦我們運行程序,就會看到它終於產生了正確的輸出(許多 Java 庫的類都覆蓋了 hashcode() 和 equals()方法,以便與自己提供的內容適應)。

再論枚舉器

將穿越一個序列的操作與那個序列的基礎結構分隔開。在下面的例子裡, PrintData 類用一個 Enumeration 在一個序列中移動,併為每個對象都調用toString()方法。此時創建了兩個不同類型的集合:一個 Vector 和一個 Hashtable。並且在它們裡面分別填

充 Mouse 和 Hamster 對象,由於 Enumeration 隱藏了基層集合的結構,所以PrintData 不知道或者不關心 Enumeration 來自於什麼類型的集合:

class PrintData {
 static void print(Enumeration e) {
 while (e.hasMoreElements())
 System.out.println(e.nextElement().toString());
 }
}
class Enumerators2 {
 public static void main(String[] args) {
 Vector v = new Vector();
 for (int i = 0; i < 5; i++)
 v.addElement(new Mouse(i));
 Hashtable h = new Hashtable();
 for (int i = 0; i < 5; i++)
 h.put(new Integer(i), new Hamster(i));
 System.out.println("Vector");
 PrintData.print(v.elements());
 System.out.println("Hashtable");
 PrintData.print(h.elements());
 }
}

注意 PrintData.print()利用了這些集合中的對象屬於 Object 類這一事實,所以它調用了 toString()。但在

解決自己的實際問題時,經常都要保證自己的 Enumeration 穿越某種特定類型的集合。例如,可能要求集合

中的所有元素都是一個 Shape(幾何形狀),並含有 draw()方法。若出現這種情況,必須從

Enumeration.nextElement()返回的 Object 進行下溯造型,以便產生一個 Shape。

排序

編寫通用的排序代碼時,面臨的一個問題是必須根據對象的實際類型來執行比較運算,從而實現正確的排序。當然,一個辦法是為每種不同的類型都寫一個不同的排序方法。然而,應認識到假若這樣做,以後增加新類型時便不易實現代碼的重複利用。

程序設計一個主要的目標就是“將發生變化的東西同保持不變的東西分隔開”。在這裡,保持不變的代碼是通用的排序算法,而每次使用時都要變化的是對象的實際比較方法。因此,我們不可將比較代碼“硬編碼”到多個不同的排序例程內,而是採用“回調”技術。

利用回調,經常發生變化的那部分代碼會封裝到它自己的類內,而總是保持相同的代碼則“回調”發生變化的代碼。這樣一來,不同的對象就可以表達不同的比較方式,同時向它們傳遞相同的排序代碼。

下面這個“接口”( Interface)展示瞭如何比較兩個對象,它將那些“要發生變化的東西”封裝在內:

interface Compare {
boolean lessThan(Object lhs, Object rhs);
boolean lessThanOrEqual(Object lhs, Object rhs);
} 

對這兩種方法來說, lhs 代表本次比較中的“左手”對象,而 rhs 代表“右手”對象。

可創建 Vector 的一個子類,通過 Compare 實現“快速排序”。對於這種算法,包括它的速度以及原理等等

public class SortVector extends Vector {
 private Compare compare; // To hold the callback
 public SortVector(Compare comp) {
 compare = comp;
 }
 public void sort() {
 quickSort(0, size() - 1);
 }
 private void quickSort(int left, int right) {
 if (right > left) {
 Object o1 = elementAt(right);
 int i = left - 1;
 int j = right;
 while (true) {
 while (compare.lessThan(elementAt(++i), o1))
 ;
 while (j > 0)
 if (compare.lessThanOrEqual(elementAt(--j), o1))
 break; // out of while
 if (i >= j)
 break;
 swap(i, j);
 }
 swap(i, right);
 quickSort(left, i - 1);
 quickSort(i + 1, right);
 }
 }
 private void swap(int loc1, int loc2) {
 Object tmp = elementAt(loc1);
 setElementAt(elementAt(loc2), loc1);
 setElementAt(tmp, loc2);
 }
}

為使用 SortVector,必須創建一個類,令其為我們準備排序的對象實現 Compare。此時內部類並不顯得特別重要,但對於代碼的組織卻是有益的。下面是針對 String 對象的一個例子

public class StringSortTest {
 static class StringCompare implements Compare {
 public boolean lessThan(Object l, Object r) {
 return ((String) l).toLowerCase().compareTo(
 ((String) r).toLowerCase()) < 0;
 }
 public boolean lessThanOrEqual(Object l, Object r) {
 return ((String) l).toLowerCase().compareTo(
 ((String) r).toLowerCase()) <= 0;
 }
 }
 public static void main(String[] args) {
 SortVector sv = new SortVector(new StringCompare());
 sv.addElement("d");
 sv.addElement("A");
 sv.addElement("C");
 sv.addElement("c");
 sv.addElement("b");
 sv.addElement("B");
 sv.addElement("D");
 sv.addElement("a");
 sv.sort();
 Enumeration e = sv.elements();
 while (e.hasMoreElements())
 System.out.println(e.nextElement());
 }
}

一旦設置好框架,就可以非常方便地重複使用象這樣的一個設計—— 只需簡單地寫一個類,將“需要發生變化”的東西封裝進去,然後將一個對象傳給SortVector 即可

繼承( extends)在這兒用於創建一種新類型的 Vector—— 也就是說, SortVector 屬於一種 Vector,並帶有一些附加的功能。繼承在這裡可發揮很大的作用,但了帶來了問題。它使一些方法具有了final 屬性,所以不能覆蓋它們。如果想創建一個排好序的 Vector,令其只接收和生成 String 對象,就會遇到麻煩。因為 addElement()和 elementAt()都具有 final 屬性,而且它們都是我們必須覆蓋的方法,否則便無法實現只能接收和產生 String 對象。

但在另一方面,請考慮採用“合成”方法:將一個對象置入一個新類的內部。此時,不是改寫上述代碼來達到這個目的,而是在新類裡簡單地使用一個 SortVector。在這種情況下,用於實現 Compare 接口的內部類就可以“匿名”地創建

import java.util.*;
public class StrSortVector {
 private SortVector v = new SortVector(
 // Anonymous inner class:
 new Compare() {
 public boolean lessThan(Object l, Object r) {
 return ((String) l).toLowerCase().compareTo(
 ((String) r).toLowerCase()) < 0;
 }
 public boolean lessThanOrEqual(Object l, Object r) {
 return ((String) l).toLowerCase().compareTo(
 ((String) r).toLowerCase()) <= 0;
 }
 });
 private boolean sorted = false;
 public void addElement(String s) {
 v.addElement(s);
 sorted = false;
 }
 public String elementAt(int index) {
if(!sorted) {
v.sort();232
sorted = true;
}
return (String)v.elementAt(index);
}
 public Enumeration elements() {
 if (!sorted) {
 v.sort();
 sorted = true;
 }
 return v.elements();
 }
 // Test it:
 public static void main(String[] args) {
 StrSortVector sv = new StrSortVector();
 sv.addElement("d");
 sv.addElement("A");
 sv.addElement("C");
 sv.addElement("c");
 sv.addElement("b");
 sv.addElement("B");
 sv.addElement("D");
 sv.addElement("a");
 Enumeration e = sv.elements();
 while (e.hasMoreElements())
 System.out.println(e.nextElement());
 }
}

新集合

java集合入門和深入學習(詳解),看這篇就差不多了

這裡寫圖片描述

這張圖剛開始的時候可能讓人有點兒摸不著頭腦,相信大家會真正理解它實際只有三個集合組件: Map, List 和 Set。而且每個組件實際只有兩、三種實現方式

虛線框代表“接口”,點線框代表“抽象”類,而實線框代表普通(實際)類。點線箭頭表示一個特定的類準備實現一個接口(在抽象類的情況下,則是“部分”實現一個接口)。雙線箭頭表示一個類可生成箭頭指向的那個類的對象。

致力於容納對象的接口是 Collection, List, Set 和 Map。在傳統情況下,我們需要寫大量代碼才能同這些接口打交道。而且為了指定自己想使用的準確類型,必須在創建之初進行設置。所以可能創建下面這樣的一

個 List:

List x = new LinkedList();

當然,也可以決定將 x 作為一個 LinkedList 使用(而不是一個普通的 List),並用 x 負載準確的類型信息。使用接口的好處就是一旦決定改變自己的實施細節,要做的全部事情就是在創建的時候改變它,就象下面這樣:

List x = new ArrayList();

在類的分級結構中,可看到大量以“ Abstract ”(抽象)開頭的類,這剛開始可能會使人感覺迷惑。它們實際上是一些工具,用於“部分”實現一個特定的接口。舉個例子來說,假如想生成自己的Set,就不是從 Set接口開始,然後自行實現所有方法。相反,我們可以從 AbstractSet 繼承,只需極少的工作即可得到自己的新類。儘管如此,新集合庫仍然包含了足夠的功能,可滿足我們的幾乎所有需求。所以考慮到我們的目的,可忽略所有以“ Abstract”開頭的類。

因此,在觀看這張示意圖時,真正需要關心的只有位於最頂部的“接口”以及普通(實際)類—— 均用實線方框包圍。通常需要生成實際類的一個對象,將其上溯造型為對應的接口。以後即可在代碼的任何地方使用那個接口。下面是一個簡單的例子,它用 String 對象填充一個集合,然後打印出集合內的每一個元素:

public class SimpleCollection {
 public static void main(String[] args) {
 Collection c = new ArrayList();
 for (int i = 0; i < 10; i++)
 c.add(Integer.toString(i));
 Iterator it = c.iterator();
 while (it.hasNext())
 System.out.println(it.next());
 }
}

main()的第一行創建了一個 ArrayList 對象,然後將其上溯造型成為一個集合。由於這個例子只使用了Collection 方法,所以從 Collection 繼承的一個類的任何對象都可以正常工作。但 ArrayList 是一個典型的 Collection,它代替了 Vector 的位置。

add()方法的作用是將一個新元素置入集合裡。然而,用戶文檔謹慎地指出 add()“保證這個集合包含了指定的元素”。這一點是為 Set 作鋪墊的,後者只有在元素不存在的前提下才會真的加入那個元素。對於ArrayList 以及其他任何形式的 List, add()肯定意味著“直接加入”。

利用 iterator()方法,所有集合都能生成一個“反覆器”( Iterator)。反覆器其實就象一個“枚舉”( Enumeration),是後者的一個替代物,只是:

(1) 它採用了一個歷史上默認、而且早在 OOP 中得到廣泛採納的名字(反覆器)。

(2) 採用了比 Enumeration 更短的名字: hasNext()代替了 hasMoreElement(),而 next()代替了nextElement()。

(3) 添加了一個名為 remove()的新方法,可刪除由 Iterator 生成的上一個元素。所以每次調用 next()的時候,只需調用 remove()一次

使用 C o l l e c t i o n s

下面這張表格總結了用一個集合能做的所有事情(亦可對 Set 和 List 做同樣的事情,儘管 List 還提供了一

些額外的功能)。 Map 不是從 Collection 繼承的,所以要單獨對待

boolean add(Object) *保證集合內包含了自變量。如果它沒有添加自變量,就返回 false(假)

boolean addAll(Collection) *添加自變量內的所有元素。如果沒有添加元素,則返回 true(真)

void clear() *刪除集合內的所有元素

boolean contains(Object) 若集合包含自變量,就返回“真”

boolean containsAll(Collection) 若集合包含了自變量內的所有元素,就返回“真”

boolean isEmpty() 若集合內沒有元素,就返回“真”

Iterator iterator() 返回一個反覆器,以用它遍歷集合的各元素

boolean remove(Object) *如自變量在集合裡,就刪除那個元素的一個實例。如果已進行了刪除,就返回

“真”

boolean removeAll(Collection) *刪除自變量裡的所有元素。如果已進行了任何刪除,就返回“真”

boolean retainAll(Collection) *只保留包含在一個自變量裡的元素(一個理論的“交集”)。如果已進

行了任何改變,就返回“真”

int size() 返回集合內的元素數量

Object[] toArray() 返回包含了集合內所有元素的一個數組

*這是一個“可選的”方法,有的集合可能並未實現它。若確實如此,該方法就會遇到一個

UnsupportedOperatiionException,即一個“操作不支持”違例。

下面這個例子向大家演示了所有方法。同樣地,它們只對從集合繼承的東西有效,一個ArrayList 作為一種“不常用的分母”使用

public class Collection1 {
 // Fill with 'size' elements, start
 // counting at 'start':
 public static Collection fill(Collection c, int start, int size) {
 for (int i = start; i < start + size; i++)
 c.add(Integer.toString(i));
 return c;
 }
 // Default to a "start" of 0:
 public static Collection fill(Collection c, int size) {
 return fill(c, 0, size);
 }
 // Default to 10 elements:
 public static Collection fill(Collection c) {
 return fill(c, 0, 10);
 }
 // Create & upcast to Collection:
 public static Collection newCollection() {
 return fill(new ArrayList());
 // ArrayList is used for simplicity, but it's
 // only seen as a generic Collection
 // everywhere else in the program.
 }
 // Fill a Collection with a range of values:
 public static Collection newCollection(int start, int size) {
 return fill(new ArrayList(), start, size);
 }
 // Moving through a List with an iterator:
 public static void print(Collection c) {
 for (Iterator x = c.iterator(); x.hasNext();)
 System.out.print(x.next() + " ");
 System.out.println();
 }
 public static void main(String[] args) {
 Collection c = newCollection();
 c.add("ten");
 c.add("eleven");
 print(c);
 // Make an array from the List:
 Object[] array = c.toArray();
 // Make a String array from the List:
 String[] str = (String[]) c.toArray(new String[1]);
 // Find max and min elements; this means
 // different things depending on the way
 // the Comparable interface is implemented:
 System.out.println("Collections.max(c) = " + Collections.max(c));
 System.out.println("Collections.min(c) = " + Collections.min(c));
 // Add a Collection to another Collection
 c.addAll(newCollection());
 print(c);
 c.remove("3"); // Removes the first one
 print(c);
 c.remove("3"); // Removes the second one
 print(c);
 // Remove all components that are in the
 // argument collection:
 c.removeAll(newCollection());
 print(c);
 c.addAll(newCollection());
 print(c);
 // Is an element in this Collection?
 System.out.println("c.contains("4") = " + c.contains("4"));
 // Is a Collection in this Collection?
 System.out.println("c.containsAll(newCollection()) = "
 + c.containsAll(newCollection()));
 Collection c2 = newCollection(5, 3);
 // Keep all the elements that are in both
 // c and c2 (an intersection of sets):
 c.retainAll(c2);
 print(c);
 // Throw away all the elements in c that
 // also appear in c2:
 c.removeAll(c2);
 System.out.println("c.isEmpty() = " + c.isEmpty());
 c = newCollection();
 print(c);
 c.clear(); // Remove all elements
 System.out.println("after c.clear():");
 print(c);
 }
}

newCollection()的兩個版本都創建了 ArrayList,用於包含不同的數據集,並將它們作為集合對象返回。所以很明顯,除了 Collection 接口之外,不會再用到其他什麼。

現在私信我可以免費獲得各種Java編程的從入門到深入的學習資料


分享到:


相關文章: