在 Java2中,有一套設計優良的接口和類組成了Java集合框架Collection,使程序員操作成批的數據或對象元素極為方便。這些接口和類有很多對抽象數據類型操作的API,而這是我們常用的且在數據結構中熟知的。例如Map,Set,List等。並且Java用面向對象的設計對這些數據結構和算法進行了封裝,這就極大的減化了程序員編程時的負擔。程序員也可以以這個集合框架為基礎,定義更高級別的數據抽象,比如棧、隊列和線程安全的集合等,從而滿足自己的需要。
Java2的集合框架,抽其核心,主要有三種:List、Set和Map。如下圖所示:
需要注意的是,這里的 Collection、List、Set和Map都是接口(Interface),不是具體的類實現。 List lst = new ArrayList(); 這是我們平常經常使用的創建一個新的List的語句,在這里, List是接口,ArrayList才是具體的類。 注意:所謂只允許子類對象賦值給父類對象,而不准父類對象賦值給子類對象的規則說法------其本質上解釋:List lst=new ArrayList()是兩種變量做運算(或一個變量一個真實對象),lst是一個結構類型聲明變量,是變量當然具備存儲能力(接口是不能實例化的是沒錯,List lst=new List()只能說明等式右邊計算是一種語法錯誤,但是不能否定了List lst中lst變量不能存儲地址,因為所有的對象引用都是一種隱式指針,能夠存儲真實對象地址),但是在進行賦值運算的時候,編譯器總是檢查一下兩邊變量類型的從屬關系,如果滿足小於等於關系,則語法不報錯;接下來是lst會根據List類型屬性來匹配真實實例中的屬性,如果檢測合格滿足,則執行計算語句,如果不合格,則報錯。 比如這種情況就會報錯:
若父類中有protected int i=2;子類中有public int i=3; 子類對象賦值給父類對象tp時沒有報錯,是因為編譯器在賦值這一步時僅僅做了類型從屬關系判斷,而計算tp.i會報錯,這是因為,jvm編譯器無法根據結構聲明變量tp的i屬性找到被引用對象中有該屬性,因為它們不完全一致。(結構聲明變量會存儲子類對象的引用地址)
常用集合類的繼承結構如下:
Collection<--List<--Vector
Collection<--List<--ArrayList
Collection<--List<--LinkedList
Collection<--Set<--HashSet
Collection<--Set<--HashSet<--LinkedHashSet
Collection<--Set<--SortedSet<--TreeSet
Map<--SortedMap<--TreeMap
Map<--HashMap
-----------------------------------------------SB分割線------------------------------------------
List:
List是有序的Collection,使用此接口能夠精確的控制每個元素插入的位置。用戶能夠使用索引(元素在List中的位置,類似於數組下 >標)來訪問List中的元素,這類似於Java的數組。
Vector:
基於數組(Array)的List,其實就是封裝了數組所不具備的一些功能方便我們使用,所以它難易避免數組的限制,同時性能也不可能超越數組。所以,在可能的情況下,我們要多運用數組。另外很重要的一點就是Vector是線程同步的(sychronized)的,這也是Vector和ArrayList 的一個的重要區別。
ArrayList:
同Vector一樣是一個基於數組上的鏈表,但是不同的是ArrayList不是同步的。所以在性能上要比Vector好一些,但是當運行到多線程環境中時,可需要自己在管理線程的同步問題。
LinkedList:
LinkedList不同於前面兩種List,它不是基於數組的,所以不受數組性能的限制。
它每一個節點(Node)都包含兩方面的內容:
1.節點本身的數據(data);
2.下一個節點的信息(nextNode)。
所以當對LinkedList做添加,刪除動作的時候就不用像基於數組的ArrayList一樣,必須進行大量的數據移動。只要更改nextNode的相關信息就可以實現了,這是LinkedList的優勢。
List總結:
- 所有的List中只能容納單個不同類型的對象組成的表,而不是Key-Value鍵值對。例如:[ tom,1,c ]
- 所有的List中可以有相同的元素,例如Vector中可以有 [ tom,koo,too,koo ]
- 所有的List中可以有null元素,例如[ tom,null,1 ]
- 基於Array的List(Vector,ArrayList)適合查詢,而LinkedList 適合添加,刪除操作
--------------------------------------NB分割線------------------------------------
Set:
Set是一種不包含重復的元素的無序Collection。
HashSet:
雖然Set同List都實現了Collection接口,但是他們的實現方式卻大不一樣。List基本上都是以Array為基礎。但是Set則是在 HashMap的基礎上來實現的,這個就是Set和List的根本區別。HashSet的存儲方式是把HashMap中的Key作為Set的對應存儲項。看看 HashSet的add(Object obj)方法的實現就可以一目了然了。
- public boolean add(Object obj) {
- return map.put(obj, PRESENT) == null;
- }
這個也是為什么在Set中不能像在List中一樣有重復的項的根本原因,因為HashMap的key是不能有重復的。
LinkedHashSet:
HashSet的一個子類,一個鏈表。
TreeSet:
SortedSet的子類,它不同於HashSet的根本就是TreeSet是有序的。它是通過SortedMap來實現的。
Set總結:
- Set實現的基礎是Map(HashMap)
- Set中的元素是不能重復的,如果使用add(Object obj)方法添加已經存在的對象,則會覆蓋前面的對象
--------------------------------------2B分割線------------------------------------
Map:
Map 是一種把鍵對象和值對象進行關聯的容器,而一個值對象又可以是一個Map,依次類推,這樣就可形成一個多級映射。對於鍵對象來說,像Set一樣,一個 Map容器中的鍵對象不允許重復,這是為了保持查找結果的一致性;如果有兩個鍵對象一樣,那你想得到那個鍵對象所對應的值對象時就有問題了,可能你得到的並不是你想的那個值對象,結果會造成混亂,所以鍵的唯一性很重要,也是符合集合的性質的。當然在使用過程中,某個鍵所對應的值對象可能會發生變化,這時會按照最后一次修改的值對象與鍵對應。對於值對象則沒有唯一性的要求,你可以將任意多個鍵都映射到一個值對象上,這不會發生任何問題(不過對你的使用卻可能會造成不便,你不知道你得到的到底是那一個鍵所對應的值對象)。
Map有兩種比較常用的實現:HashMap和TreeMap。
HashMap也用到了哈希碼的算法,以便快速查找一個鍵,
TreeMap則是對鍵按序存放,因此它便有一些擴展的方法,比如firstKey(),lastKey()等,你還可以從TreeMap中指定一個范圍以取得其子Map。
鍵和值的關聯很簡單,用put(Object key,Object value)方法即可將一個鍵與一個值對象相關聯。用get(Object key)可得到與此key對象所對應的值對象。
--------------------------------------JB分割線------------------------------------
其它:
一、幾個常用類的區別
1.ArrayList: 元素單個,效率高,多用於查詢
2.Vector: 元素單個,線程安全,多用於查詢
3.LinkedList:元素單個,多用於插入和刪除
4.HashMap: 元素成對,元素可為空
5.HashTable: 元素成對,線程安全,元素不可為空
二、Vector、ArrayList和LinkedList
大多數情況下,從性能上來說ArrayList最好,但是當集合內的元素需要頻繁插入、刪除時LinkedList會有比較好的表現,但是它們三個性能都比不上數組,另外Vector是線程同步的。所以:
如果能用數組的時候(元素類型固定,數組長度固定),請盡量使用數組來代替List;
如果沒有頻繁的刪除插入操作,又不用考慮多線程問題,優先選擇ArrayList;
如果在多線程條件下使用,可以考慮Vector;
如果需要頻繁地刪除插入,LinkedList就有了用武之地;
如果你什么都不知道,用ArrayList沒錯。
三、Collections和Arrays
在 Java集合類框架里有兩個類叫做Collections(注意,不是Collection!)和Arrays,這是JCF里面功能強大的工具,但初學者往往會忽視。按JCF文檔的說法,這兩個類提供了封裝器實現(Wrapper Implementations)、數據結構算法和數組相關的應用。
想必大家不會忘記上面談到的“折半查找”、“排序”等經典算法吧,Collections類提供了豐富的靜態方法幫助我們輕松完成這些在數據結構課上煩人的工作:
binarySearch:折半查找。
sort:排序,這里是一種類似於快速排序的方法,效率仍然是O(n * log n),但卻是一種穩定的排序方法。
reverse:將線性表進行逆序操作,這個可是從前數據結構的經典考題哦!
rotate:以某個元素為軸心將線性表“旋轉”。
swap:交換一個線性表中兩個元素的位置。
……
Collections還有一個重要功能就是“封裝器”(Wrapper),它提供了一些方法可以把一個集合轉換成一個特殊的集合,如下:
unmodifiableXXX:轉換成只讀集合,這里XXX代表六種基本集合接口:Collection、List、Map、Set、SortedMap和SortedSet。如果你對只讀集合進行插入刪除操作,將會拋出UnsupportedOperationException異常。
synchronizedXXX:轉換成同步集合。
singleton:創建一個僅有一個元素的集合,這里singleton生成的是單元素Set,
singletonList和singletonMap分別生成單元素的List和Map。
空集:由Collections的靜態屬性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示。
這次關於Java集合類概述就到這里,下一次我們來講解Java集合類的具體應用,如List排序、刪除重復元素。
圖結構展示:
實現過程:
首先,我們來看看圖結構在代碼中的實現。有三塊邏輯:
1.圖中的節點:
- public class GraphNode {
- public List<GraphEdge> edgeList = null;
- private String label = "";
- public GraphNode(String label) {
- this.label = label;
- if (edgeList == null) {
- edgeList = new ArrayList<GraphEdge>();
- }
- }
- /**
- * 給當前節點添加一條邊
- * GraphNode
- * @param edge
- * 添加的邊
- */
- public void addEdgeList(GraphEdge edge) {
- edgeList.add(edge);
- }
- public String getLabel() {
- return label;
- }
- }
2.圖中的邊:
- public class GraphEdge {
- private GraphNode nodeLeft;
- private GraphNode nodeRight;
- /**
- * @param nodeLeft
- * 邊的左端
- * @param nodeRight
- * 邊的右端
- */
- public GraphEdge(GraphNode nodeLeft, GraphNode nodeRight) {
- this.nodeLeft = nodeLeft;
- this.nodeRight = nodeRight;
- }
- public GraphNode getNodeLeft() {
- return nodeLeft;
- }
- public GraphNode getNodeRight() {
- return nodeRight;
- }
- }
3.把節點和邊組合成一個圖結構:
- public class MyGraph {
- private List<GraphNode> nodes = null;
- public void initGraph(int n) {
- if (nodes == null) {
- nodes = new ArrayList<GraphNode>();
- }
- GraphNode node = null;
- for (int i = 0; i < n; i++) {
- node = new GraphNode(String.valueOf(i));
- nodes.add(node);
- }
- }
- public void initGraph(int n, boolean b) {
- initGraph(n);
- GraphEdge edge01 = new GraphEdge(nodes.get(0), nodes.get(1));
- GraphEdge edge02 = new GraphEdge(nodes.get(0), nodes.get(2));
- GraphEdge edge13 = new GraphEdge(nodes.get(1), nodes.get(3));
- GraphEdge edge14 = new GraphEdge(nodes.get(1), nodes.get(4));
- GraphEdge edge25 = new GraphEdge(nodes.get(2), nodes.get(5));
- GraphEdge edge26 = new GraphEdge(nodes.get(2), nodes.get(6));
- GraphEdge edge37 = new GraphEdge(nodes.get(3), nodes.get(7));
- GraphEdge edge47 = new GraphEdge(nodes.get(4), nodes.get(7));
- GraphEdge edge56 = new GraphEdge(nodes.get(5), nodes.get(6));
- nodes.get(0).addEdgeList(edge01);
- nodes.get(0).addEdgeList(edge02);
- nodes.get(1).addEdgeList(edge13);
- nodes.get(1).addEdgeList(edge14);
- nodes.get(2).addEdgeList(edge25);
- nodes.get(2).addEdgeList(edge26);
- nodes.get(3).addEdgeList(edge37);
- nodes.get(4).addEdgeList(edge47);
- nodes.get(5).addEdgeList(edge56);
- }
- public void initGraph() {
- initGraph(8, false);
- }
- public List<GraphNode> getGraphNodes() {
- return nodes;
- }
- }
有了圖的結構,我們就可以進行一些實際的操作了。
深度優先搜索:
- public class DFSearch {
- /**
- * 深度遍歷
- * DFSearch
- * @param node
- * 當前節點
- * @param visited
- * 被訪問過的節點列表
- */
- public void searchTraversing(GraphNode node, List<GraphNode> visited) {
- // 判斷是否遍歷過
- if (visited.contains(node)) {
- return;
- }
- visited.add(node);
- System.out.println("節點:" + node.getLabel());
- for (int i = 0; i < node.edgeList.size(); i++) {
- searchTraversing(node.edgeList.get(i).getNodeRight(), visited);
- }
- }
- }
廣度優先搜索:
- public class BFSearch {
- /**
- * 廣度優先搜索
- * BFSearch
- * @param node
- * 搜索的入口節點
- */
- public void searchTraversing(GraphNode node) {
- List<GraphNode> visited = new ArrayList<GraphNode>(); // 已經被訪問過的元素
- Queue<GraphNode> q = new LinkedList<GraphNode>(); // 用隊列存放依次要遍歷的元素
- q.offer(node);
- while (!q.isEmpty()) {
- GraphNode currNode = q.poll();
- if (!visited.contains(currNode)) {
- visited.add(currNode);
- System.out.println("節點:" + currNode.getLabel());
- for (int i = 0; i < currNode.edgeList.size(); i++) {
- q.offer(currNode.edgeList.get(i).getNodeRight());
- }
- }
- }
- }
- }