1. 程式人生 > >Java基礎 -- 持有對象(容器)

Java基礎 -- 持有對象(容器)

treeset ont 有序性 light ext 相同 元素 move 兩種

一 容器的用途

如果對象的數量與生命周期都是固定的,自然我們也就不需要很復雜的數據結構。

我們可以通過創建引用來持有對象,如

Class clazz;

也可以通過數組來持有多個對象,如

Class[] clazs = new Class[10];

然而,一般情況下,我們並不知道要創建多少對象,或者以何種方式創建對象。數組顯然只能創建固定長度的對象,為了使程序變得更加靈活與高效,Java類庫提供了一套完整的容器類,具備完善的方法來解決上述問題。

技術分享圖片

從上圖可以看到容器中有七大接口:

  • Collection接口
  • Map接口
  • Set接口
  • List接口
  • Queue接口
  • Iterator接口
  • Comparable接口

其中List, Queue和Set接口繼承了Collection接口,剩下的接口之間都是相互獨立的,無繼承關系。Iterater叠代器則是為了更靈活的叠代集合,與foreach一起使用。Comparable接口則用於比較。

除了接口之外,容器中還包含了List接口的實現類LinkedList,Set接口的實現類HashSet等;容器中還包含兩個工具類/幫助類:Collections和Arrays,其中提供了一系列靜態方法,用於對集合中元素進行排序、搜索以及線程安全等各種操作。

容器主要分為兩種類型:

  • Collection:用來保存單一的元素序列,這些元素都服從一條或者多條規則。List必須按照插入的順序保存元素,而Set不能有重復的元素,Queue按照排隊規則來確定對象產生的順序(通常與它們被插入的順序相同)。
  • Map:用來保存關聯鍵值對,允許使用鍵來查找值。ArrayList允許你使用數字來查找值,因此在某種意義上講,它將數字與對象關聯在了一起。映射表允許我們使用另一個對象來查找某個對象,它也被稱為“關聯數組”,因為它將某些對象與另外一些對象關聯在一起;或者稱為"字典",因此可以使用鍵對象來查找值對象,就像在字典中使用單詞來定義一樣。

二 Collection中添加元素

在java.util包中的Collections和Arrays類中包含了很多使用方法,可以再一個Collection中添加一組元素。

  • Arrays.asList()方法接受一個數組或是一個用逗號分隔的元素列表(使用可變參數),並將其轉換為一個Lsit對象。
  • Collections.addAll()方法接受一個Collection對象,以及一個數組或是一個用逗號分隔的列表,將元素添加到Collection中。

如下演示一個例子:

///添加一組元素
import java.util.*;

public class AddingGoups {
	public static void main(String[] args) {
		//方式一
		List<Integer> list = Arrays.asList(1,2,3,4,5);
		list.set(1, 1);
		//list.add(21);  無法改變長度,因為Arrays.asList()的輸出,其底層表示的是數組
		Collection<Integer> collection = new ArrayList<Integer>(list);
		
		//方式二
		Integer[] moreInts = {6,7,8,9,10};
		Collections.addAll(collection,moreInts);
		Collections.addAll(collection,11,12,13,14,15);
		
		//輸出
		System.out.println(collection);
				
	}
}

輸出結果:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

註意:可以直接使用Arrays.asList()的輸出,將其作為List,但是這種情況下,其底層表示的是數組,因此不能調整尺寸所以使用add()、delete()會出現錯誤。此外Arrays.asList()它對其所產生的List類型如做出了最理想的假設,即由傳入的元素類型所決定,如List<Integer> list = Arrays.asList(1,2,3,4,5),因此如果將其賦值給不同的List類型可能會引發問題。

三 容器的打印

容器的輸出,默認調用的是容器.toString()方法,如下:

///打印容器
import java.util.*;

public class PrintingContainers {
	static Collection fill(Collection<String> collection) {
		collection.add("rat");
		collection.add("cat");
		collection.add("dog");
		collection.add("pig");
		return collection;
	}
	
	static Map fill(Map<String,String> map) {
		map.put("rat","Fuzzy");
		map.put("cat","Rags");
		map.put("dog","Bosco");
		map.put("pig","Spot");
		return map;
	}
	
	public static void main(String[] args) {
		System.out.println(fill(new ArrayList<String>()));
		System.out.println(fill(new LinkedList<String>()));
		System.out.println(fill(new HashSet<String>()));
		System.out.println(fill(new TreeSet<String>()));
		System.out.println(fill(new LinkedHashSet<String>()));
		
		System.out.println(fill(new HashMap<String,String>()));
		System.out.println(fill(new TreeMap<String,String>()));
		System.out.println(fill(new LinkedHashMap<String,String>()));
	}
}

輸出如下:

[rat, cat, dog, pig]
[rat, cat, dog, pig]
[rat, cat, dog, pig]
[cat, dog, pig, rat]
[rat, cat, dog, pig]
{rat=Fuzzy, cat=Rags, dog=Bosco, pig=Spot}
{cat=Rags, dog=Bosco, pig=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Bosco, pig=Spot}

四 List接口

相比於Collection接口,添加了新的方法,使得可以再List中間插入和移除元素。其實現類有以下兩個:

  • ArrayList:類似於動態數組,適用於大量隨機訪問的情況,但是插入和刪除的代價很高;
  • LinkedList:LinkedList除了實現了List接口,類似於鏈表,提供了優化的順序訪問,在插入和刪除訪問方面代價低廉,但是隨即訪問代價較高;由於LinkedList還實現了Queue接口,可以使其用作棧、隊列或雙端隊列。

1、ArrayList

下面演示一個ArrayList的例子,將ArrayList向上轉型為List:

import  java.util.*;

class Person{
	public String name;
	public int age;
	public boolean sex;
	
	public Person(String name, int age, boolean sex) {
		super();
		this.name = name;
		this.age = age;
		this.sex = sex;
	}	
	
	public String toString(){
		return name + "-" + age + "-" + sex;
	}
		
}

public class ListFeatures {
	public static void main(String[] args) {
		List<Person> pers = new ArrayList<Person>();
	
		Person p1 = new Person("吳定會",50,true);
		pers.add(p1);
		
		Person p2 = new Person("沈艷霞",46,false);
		pers.add(p2);
		
		Person p3 = new Person("張三",17,true);
		pers.add(p3);
		
		Person p4 = new Person("李四",34,false);
		pers.add(p4);
		
		System.out.println("1:"+pers);
		
		//成員函數1
		System.out.println("2:"+pers.contains(p1));
		
		//成員函數2
		Person p = pers.get(2);
		
		//成員函數3
		System.out.println("3:" + p +" " + pers.indexOf(p));
		
		//成員函數4
		System.out.println("4:" + pers.remove(p));

		System.out.println("5:"+pers);
		
		//成員函數5
		List<Person> sub = pers.subList(0, 2);    //不包含2
		System.out.println("6:"+sub);
		
		//成員函數6
		System.out.println("7:"+pers.containsAll(sub));
		
		Collections.shuffle(sub);
		System.out.println("shuffled sublist:" + sub);
		
		//成員函數7
		pers.removeAll(sub);
		System.out.println("8:"+pers);
		
		//成員函數8
		System.out.println("9:"+pers.isEmpty());
		
	}
}

輸出如下:

1:[吳定會-50-true, 沈艷霞-46-false, 張三-17-true, 李四-34-false]
2:true
3:張三-17-true 2
4:true
5:[吳定會-50-true, 沈艷霞-46-false, 李四-34-false]
6:[吳定會-50-true, 沈艷霞-46-false]
7:true
shuffled sublist:[沈艷霞-46-false, 吳定會-50-true]
8:[李四-34-false]
9:false

在上面例子中我們使用了List的部分方法:

  • contains():判斷一個對象是否在列表中。
  • remove():刪除一個對象,如果你想移除一個對象,則可以將這個對象的引用傳遞給這個方法;
  • indexOf();用來判斷一個對象在List中所處位置的索引編號,不存在返回-1;
  • subList():從列表中創建一個子集;
  • containsAll():判斷是否存在該子集;
  • removeAll():移除一個子集;
  • isEmpty():判斷是否為空;

除了以上方法,List還有很多方法,比如set()、replace()、clear()、addAll()、toArray()等。

2、LinkedList

下面演示一個LinkedList的例子

import java.util.*;


public class LinkedListFeatures {
	public static void main(String[] args) {
		LinkedList<Person> pers = new LinkedList<Person>();
	
		Person p1 = new Person("吳定會",50,true);
		pers.add(p1);
		
		Person p2 = new Person("沈艷霞",46,false);
		pers.add(p2);
		
		Person p3 = new Person("張三",17,true);
		pers.add(p3);
		
		Person p4 = new Person("李四",34,false);
		pers.add(p4);
		
		System.out.println(pers);
		
		//成員函數1 返回第一個元素
		System.out.println("getFirst():"+pers.getFirst());
		
		//成員函數2 返回第一個元素
		System.out.println("element():" + pers.element());
		
		//成員函數3 返回第一個元素
		System.out.println("peek():" + pers.peek());
		
		//成員函數4 移除並返回第一個元素
		System.out.println("remove():" + pers.remove());
		
		//成員函數5  刪除並返回第一個元素
		System.out.println("removeFirst():" + pers.removeFirst());
		
		//成員函數6 刪除並返回第一個元素
		System.out.println("poll():" + pers.poll());
		
		System.out.println(pers);
		
		//成員函數7 追加元素到List頭部
		pers.addFirst(new Person("鄭洋",25,true));
		
		System.out.println("After addFirst():" + pers);
		
		
		//成員函數8 追加元素到List尾部
		pers.offer(new Person("楊德亮",25,true));		
		System.out.println("After offer():" + pers);
		
		
		//成員函數9 追加元素到List尾部
		pers.addLast(new Person("黃旭",25,true));		
		System.out.println("After addLast():" + pers);
		
		//成員函數10 刪除並返回最後一個元素
		System.out.println("After removeLast():" + pers.removeLast());
				 
	}
}

輸出:

[吳定會-50-true, 沈艷霞-46-false, 張三-17-true, 李四-34-false]
getFirst():吳定會-50-true
element():吳定會-50-true
peek():吳定會-50-true
remove():吳定會-50-true
removeFirst():沈艷霞-46-false
poll():張三-17-true
[李四-34-false]
After addFirst():[鄭洋-25-true, 李四-34-false]
After offer():[鄭洋-25-true, 李四-34-false, 楊德亮-25-true]
After addLast():[鄭洋-25-true, 李四-34-false, 楊德亮-25-true, 黃旭-25-true]
After removeLast():黃旭-25-true

註意:上面程序中的element()、offer()、peek()、poll()、等方法都是Queue接口的方法,具體內容在Queue接口中將會詳細介紹。

五 Set接口

Set不保存重復的元素,如果試圖將相同對象的多個實例添加到Set中,那麽它會組織這種重復現象。Set可以很容易查詢某個元素是否在某個Set中,正因為如此,查找就成為了Set中最重要的操作。

具有與Collection相同的方法。實際上Set就是Collection,只是行為不同。其實現類有以下兩個:

  • HashSet:使用了散列函數實現元素的存儲,極大的提高了訪問速度,存入HashSet的對象必須定義hashCode()。註意:HashSet元素存儲的順序看起來沒有實際意義。
  • TreeSet:使用紅黑樹來實現存儲,好處就是可以在插入之後維持集合的有序性,按照比較結果升序保存元素。

此外還有一個類型LinkedHashSet,繼承自HashSet:

  • LinkedHashSet:使用鏈表保存元素插入順序,但是它也使用了散列,保留了HashSet的訪問速度。

1、HashSet

下面是使用存放Integer對象的HashSet的示例:

import java.util.*; 

public class SetOfInteger {
	public static void main(String[] args) {
		Random rand = new Random(47);
		
		Set<Integer> intset = new HashSet<Integer>();
		for(int i=0;i<10000;i++) {
			intset.add(rand.nextInt(30));
		}
		System.out.println(intset);
	}
}

輸出結果:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

在本例中,intset中插入10000次,但是由於不保存重復元素,最終輸出結果數目<=30。

2、TreeSet

如果想對插入的元素進行排序,可以使用TreeSet。

import java.util.*;

public class SortedSetOfInteger {
	public static void main(String[] args) {
		Random rand = new Random(47);
		
		Set<Integer> intset = new TreeSet<Integer>();
		for(int i=0;i<10000;i++) {
			intset.add(rand.nextInt(30));
		}
		System.out.println(intset);
	}
}

輸出如下:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

六 Queue接口

隊列是一個典型的先進先出(FIFO)的容器,即從容器的一端放入事物,從另一端取出,並且事物放入容器的順序與取出的順序是相同的。隊列常被當做一種可靠的將對象從程序的某個區域傳輸到另一個區域的途徑。隊列在並發編程中特別重要。

Queue的實現有以下兩個:

  • LinkedList:LinkedList實現了Queue接口,提供了支持隊列行為的方法;
  • PriorityQueue:與普通隊列不同,優先隊列每次彈出的是優先級最高的元素,可以通過提供自己的Comparator來修改默認的優先級順序。

1、LinkedList

下面的示例使用了Queue接口中與Queue相關的方法:

import java.util.*;

public class QueueDemo {
	public static void printQ(Queue queue){
		while(queue.peek() != null) {
			System.out.print(queue.remove() + " ");
		}
		System.out.println();
	}
	
	public static void main(String[] args) {
		Queue<Integer> queue = new LinkedList<Integer>();
		Random rand = new Random(47);
		
		for(int i=0;i<10;i++) {
			queue.offer(rand.nextInt(i+10));					
		}
		
		printQ(queue);
		
		Queue<Character> qc = new LinkedList<Character>();
		for(char c:"LaJiShiPin".toCharArray()) {
			qc.offer(c);
		}
		printQ(qc);
	}
	
}

輸出如下:

8 1 1 1 5 14 3 1 0 1 
L a J i S h i P i n 

下面介紹一下上面用到的方法:

  • offer():它在允許的情況下,將一個元素插入到隊尾,或者返回false;
  • peek():在不移除的情況下返回隊頭,在隊列為空時返回null;
  • element():在不移除的情況下返回隊頭,在隊列為空時拋出NoSuchElementException異常;
  • poll():移除並返回隊頭,在隊列為空時返回null;
  • remove():移除並返回隊頭,在隊列為空時拋出NoSuchElementException異常;

自動裝包機制會自動將nextInt()方法的int結果轉換為Integer對象,將char c轉換成qc所需的Character對象。Queue接口窄化了對LinkedList的方法的訪問權限,以使得只有恰當的方法可以使用,因此能夠訪問的LinkedList方法會變少(這裏我們可以將queue轉換回LinkedList,但是最好不要這麽做)。

2、PriorityQueue

先進先出描述的是最典型的隊列規則。隊列規則是指在給定一組隊列中的所有元素的情況下,確定下一個彈出隊列的元素的規則。先進先出聲明的是下一個元素應該等待時間最長的元素。

優先級隊列聲明下一個彈出元素是最重要的元素(具有最高的優先級)。例如,在飛機場,當飛機臨近起飛時,這架飛機的乘客可以再辦理登記手續時排到隊頭。

當調用PriorityQueue中的offer()方法:會插入一個對象到隊列中,並且這個對象會在隊列中被排序。

PriorityQueue可以確保調用peek(),poll()和remove()方法時,獲取的元素將是隊列中優先級最高的元素。

下面演示一個示例:

import java.util.*;

public class PriorityQueueDemo {
	public static void main(String[] args) {
		//默認從小到大排序
		PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();		
		Random rand = new Random(47);
		for(int i=0;i<10;i++) {
			//將一個元素插入到隊尾
			priorityQueue.offer(rand.nextInt(i+10));
		}
		QueueDemo.printQ(priorityQueue);
				
		//默認從小到大排序
		List<Integer> ints = Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25);
		priorityQueue = new PriorityQueue<Integer>(ints);
		QueueDemo.printQ(priorityQueue);
		
		//從大到小排序
		priorityQueue = new PriorityQueue<Integer>(ints.size(),Collections.reverseOrder());
		priorityQueue.addAll(ints);
		QueueDemo.printQ(priorityQueue);
		
		
		//按字母從小到大排序
		String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION";
		List<String> strings = Arrays.asList(fact.split(" "));
		PriorityQueue<String> stringPQ = new PriorityQueue<String>(strings);
		QueueDemo.printQ(stringPQ);
		
		//按字母從大到小排序
		stringPQ = new PriorityQueue<String>(strings.size(),Collections.reverseOrder());
		stringPQ.addAll(strings);
		QueueDemo.printQ(stringPQ);
		
		//按字母從小到大排序
		Set<Character> charSet = new HashSet<Character>();
		for(char c:fact.toCharArray()) {
			charSet.add(c);
		}
		PriorityQueue<Character> characterPQ = new PriorityQueue<Character>(charSet);
		QueueDemo.printQ(characterPQ);
	}

}

輸出如下:

0 1 1 1 1 1 3 5 8 14 
1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 
25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 
EDUCATION ESCHEW OBFUSCATION SHOULD 
SHOULD OBFUSCATION ESCHEW EDUCATION 
  A B C D E F H I L N O S T U W 

從輸出結果可以看到,重復是允許的,在默認排序規則下最小的值擁有最高的優先級。此外,還演示了如何使用自己的Comparator對象來改變排序,我們在PriorityQueue<類型>構造器調用的時候使用了由Collections.reverseOrder()產生的反序的Comparator。

Integer、String、Character可以與PriorityQueu一起工作,因為這些類已經內建了自然排序,如果想在PriorityQueu中使用自己的類,就必須包含額外的功能以產生自然排序,或者提供自己的Comparator。

七 Map接口

Map的實現有以下兩個:

  • HashMap:通過散列函數來實現,可以用來快速訪問;
  • TreeMap:使用紅黑樹來實現,保持鍵值處於排序狀態,訪問速度沒有HashMap快;

此外還有一個類型LinkedHashMap,繼承自HashMap:

  • LinkedHashMap:使用鏈表保存元素插入順序,但是它也使用了散列,保留了HashMap的訪問速度。

1、HashMap

下面演示一個HashMap的例子:

import java.util.*;

public class HashMapDemo {
	public static void main(String[] args) {
		Random rand = new Random(47);
		
		Map<Integer,Integer> m = new HashMap<Integer,Integer>();
		for(int i=0;i<10000;i++) {
			int r = rand.nextInt(20);
			//獲取出現次數  如果不存在返回null
			Integer freq = m.get(r);
			m.put(r, freq == null?1:freq + 1);				
		}
		
		System.out.println(m);
	}

}

輸出如下:

{0=481, 1=502, 2=489, 3=508, 4=481, 5=503, 6=519, 7=471, 8=468, 9=549, 10=513, 11=531, 12=521, 13=506, 14=477, 15=497, 16=533, 17=509, 18=478, 19=464}

下面的示例將使用String描述來查Person:,還通過containsKey()和containsValue()來測試一個Map,以便看它是否包含某個鍵或某個值:

import java.util.*;

public class PersonHashMap {
	public static void main(String[] args) {
		Map<String,Person> perMap = new HashMap<String,Person>();
		perMap.put("吳定會", new Person("吳定會",50,true));
		perMap.put("沈艷霞", new Person("沈艷霞",46,false));
		System.out.println(perMap);
		
		Person p = perMap.get("沈艷霞");
		System.out.println(p);
		System.out.println(perMap.containsKey("吳定會"));
		System.out.println(perMap.containsValue(p));
		
		System.out.println(perMap.keySet());
		System.out.println(perMap.values());
	}
}

輸出如下:

{吳定會=吳定會-50-true, 沈艷霞=沈艷霞-46-false}
沈艷霞-46-false
true
true
[吳定會, 沈艷霞]
[吳定會-50-true, 沈艷霞-46-false]

參考文獻:

[1] Java編程思想

[2] 走進Java中的持有對象(容器類)之一 容器分類

Java基礎 -- 持有對象(容器)