Java EE 第四篇 核心類庫(一)集合

一、泛型

泛型,即“參數化類型”。就是將類型由原來的具體的類型參數化,類似于方法中的變量參數,此時類型也定 義成參數形式(可以稱之為類型形參),然后在使用/調用時傳入具體的類型(類型實參)。

(1)使用:

1、泛型類:

public class ClassName<T>{
    private T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

2、泛型接口:

public interface IntercaceName<T>{
    T getData();
}

//實現接口時,可以選擇指定泛型類型,也可以選擇不指定, 如下:
//指定類型:
public class Interface1 implements IntercaceName<String> {
    private String text;
    
    @Override
    public String getData() {
        return text;
    }
}

//不指定類型:
public class Interface1<T> implements IntercaceName<T> {
    private T data;
    @Override
    public T getData() {
        return data;
    }
}

3、泛型方法:

private static <T> T function(T a, T b) {}

(2)泛型限制類型:

在使用泛型時,可以指定泛型的限定區域
例如: 必須是某某類的子類或 某某接口的實現類,格式:
<T extends 類或接口1 & 接口2>

類型通配符是使用?代替方法具體的類型實參。
1 <? extends Parent> 指定了泛型類型的上界
2 <? super Child> 指定了泛型類型的下界
3 <?> 指定了沒有限制的泛型類型

(3)好處:

1、 提高代碼復用率
2、 泛型中的類型在使用時指定,不需要強制類型轉換(類型安全,編譯器會檢查類型)

注意:

  • 在編譯之后程序會采取去泛型化的措施。
  • 也就是說Java中的泛型,只在編譯階段有效。
  • 在編譯過程中,正確檢驗泛型結果后,會將泛型的相關信息擦出,并且在對象進入和離開方法的邊界處添加 類型檢查和類型轉換的方法。也就是說,泛型信息不會進入到運行時階段。

二、Java SE 類集

Java 中為了方便用戶操作各個數據結構, 所以引入了類集的概念,有時候就可以把類集稱為 java 對數據結構的實現。類集中最大的幾個操作接口:Collection、Map、Iterator,這三個接口為以后要使用的最重點的接口。 所有的類集操作的接口或類都在 java.util 包中。

1、Collection接口

Collection 接口是在整個 Java 類集中保存單值的最大操作父接口,里面每次操作的時候都只能保存一個對象的數據。在開發中不會直接使用 Collection 接口。而使用其操作的子接口:List、Set。

//定義
public interface Collection<E> extends Iterable<E>
方法:

2、List接口

在整個集合中 List 是 Collection 的子接口,里面的所有內容都是允許重復的。

//定義
public interface List<E> extends Collection<E>
方法:此接口對于 Collection 接口來講有如下的擴充方法:
常用的實現類有如下幾個:

使用頻率:ArrayList(95%)、Vector(4%)、LinkedList(1%)

ArrayList、Vector采用動態數組實現,前者線程不安全,后者安全。LinkedList采用鏈表實現。

2.1、ArrayList

ArrayList是List接口的子類,此類的定義如下:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

此類繼承了AbstractList 類。AbstractList是List接口的子類。AbstractList是個抽象類,適配器設計模式。ArrayLIst增加刪除比較慢,查找比較快。創建時必須使用引用類型或包裝類構造。

ArrayList();//創建初始容量為10的空列表。
ArrayList(int initialCapacity);
ArrayList(Collection<? extends E> e)

注意,注意對空列表進行空間為10的賦值是在空列表添加元素調用add方法的時候,內部擴容算法將新長度賦值為10,add方法的返回值永遠為true。

2.2、Vector

定義:
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

Vector 屬于 Java 元老級的操作類,是最早的提供了動態對象數組的操作類,在 JDK 1.0 的時候就已經推出了此類的使用,只是后來在 JDK 1.2 之后引入了 Java 類集合框架。但是為了照顧很多已經習慣于使用 Vector 的用戶,所以在 JDK 1.2 之后將 Vector 類進行了升級了,讓其多實現了一個 List 接口,這樣才將這個類繼續保留了下來。

Vector為可增長對象數組,區別與ArrayList在于線程安全,增加慢,查找快。

構造方法:
Vector();
Vector(int initialCapacity);
Vector(int initialCapacity, int capacityIncrement); //額外賦值擴容增量,無參構造方法默認增量為0,此時的擴容方法為翻一番。
Vector(Collection<? extends E> e)
與ArrayLIst的區別:

2.3、LinkedList

使用場景很少,定義:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable

此類繼承了 AbstractList,所以是 List 的子類。但是此類也是 Queue 接口的子類,Queue 接口定義了如下的方法:

3、Set接口

重點:
  1. Set 接口也是 Collection 的子接口,與 List 接口最大的不同在于,Set 接口里面的內容是不允許重復的。
  2. Set 接口并沒有對 Collection 接口進行擴充,基本上還是與 Collection 接口保持一致。因為此接口沒有 List 接口中定義 的 get(int index)方法,所以無法使用循環進行輸出。
  3. 此接口中有兩個常用的子類:HashSet、TreeSet

3.1 HashSet

ava.util.HashSet 是 Set 接口的一個實現類,它所存儲的元素是不可重復的,并且元素都是無序的 (即存取順序不一致)。 java.util.HashSet 底層的實現其實是一個 java.util.HashMap 支持,利用鍵值部分不可重復將其用作HashSet,value部分都對應一個默認元素,鍵值部分存儲時無序。

HashSet 是根據對象的哈希值來確定元素在集合中的存儲位置,因此具有良好的存取和查找性能。保證 元素唯一性的方式依賴于: hashCode 與 equals 方法。

當一個對象被存進 HashSet 集合后,就不能修改這個對象中的那些參與計算的哈希值的字段了,否則,對象被修改后的哈希值與最初存儲進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當前引用作為 的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從 HashSet 集合中刪除當前對象,從而 造成內存泄露。

雖然HashSet無序,但是仍然可以通過遍歷訪問數據,借用Collection接口的方法,轉換為數組即可:

Set<String> all = new HashSet<String>(); // 實例化Set接口對象
String[] str = all.toArray(new String[] {});// 變為指定的泛型類型數組
for (int x = 0; x < str.length; x++) {
    System.out.print(str[x] + "、");
}
存儲流程:

給HashSet中存放自定義類型元素時,需要重寫對象中的hashCode和equals方法,建立自己的比較方 式,才能保證HashSet集合中的對象唯一。如果想用HashSet存儲并保證數據存儲的順序,可以使用在HashSet下面的一個子類 java.util.LinkedHashSet ,它是鏈表和哈希表組合的一個數據存儲結構。

3.2、TreeSet

與 HashSet 不同的是,TreeSet 本身屬于排序的子類。

定義:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, Serializable

添加到TreeSet中的對象必須實現Comparable接口,才能實現排序。實現這個接口,需要實現里面的compareTo方法。

TreeSet有序采用二叉樹進行存儲,內部通過TreeMap進行實現。

什么是迭代器快速失敗:

迭代器遍歷集合時,遍歷集合本身。如果創建迭代器后的任何時間修改集合,除了通過remove方式,迭代器將拋出ConcurrentModificationException異常。因此在并發修改的情況下,迭代器快速失敗,減少未來的不確定性風險。

什么是迭代器安全失敗:

遍歷的是集合的備份,不會出現快速失敗(一般默認)。

如果使用TreeSet添加自定義的對象,必須實現Comparable接口,提供compareTo方法。注意compareTo的實現,當兩個元素相等時返回0,此時TreeSet對于后添加的元素拒絕,因為不接收兩個同樣的元素。

補充:Comparator接口

Comparable:強行對實現它的每個類的對象進行整體排序。這種排序被稱為類的自然排序,類的 compareTo方法被稱為它的自然比較方法。只能在類中實現compareTo()一次,不能經常修改類的代碼 實現自己想要的排序。實現此接口的對象列表(和數組)可以通過Collections.sort(和Arrays.sort)進行自動排序,對象可以用作有序映射中的鍵或有序集合中的元素,無需指定比較器。

Comparator強行對某個對象進行整體排序。可以將Comparator 傳遞給sort方法(如Collections.sort 或 Arrays.sort),從而允許在排序順序上實現精確控制。還可以使用Comparator來控制某些數據結構 (如有序set或有序映射)的順序,或者為那些沒有自然順序的對象collection提供排序,需要實現一個compare方法。

3.3關于重復元素判斷

Set 接口定義的時候本身就是不允許重復元素的,按照這個思路,如果現在真的是有重復元素的話,使用 HashSet 也同樣可以進行區分。

Set<Person> all = new HashSet<Person>();
all.add(new Person("張三", 10));
all.add(new Person("李四", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("趙六", 12));
all.add(new Person("孫七", 13));
System.out.println(all);

此時發現,并沒有去掉所謂的重復元素,也就是說之前的操作并不是真正的重復元素的判斷,而是通過 Comparable 接口間接完成的。 如果要想判斷兩個對象是否相等,則必須使用 Object 類中的 equals()方法。 從最正規的來講,如果要想判斷兩個對象是否相等,則有兩種方法可以完成:

  1. 第一步判斷兩個對象的編碼是否一致,這個方法需要通過 hashCode()完成,即:每個對象有唯一的編碼
  2. 第二步驗證對象中的每個屬性是否相等,需要通過 equals()完成。

所以此時需要覆寫 Object 類中的 hashCode()方法,此方法表示一個唯一的編碼,一般是通過公式計算出來的。

4、Iterator

Iterator 屬于迭代輸出,基本的操作原理:是不斷的判斷是否有下一個元素,有的話,則直接輸出。

定義:
public interface Iterator<E>

要想使用此接口,則必須使用 Collection 接口,在 Collection 接口中規定了一個 iterator()方法,可以用于為 Iterator 接口進行實例化操作。

方法:

通過 Collection 接口為其進行實例化之后,一定要記住,Iterator 中的操作指針是在第一條元素之上,當調用 next()方 法的時候,獲取當前指針指向的值并向下移動,使用 hasNext()可以檢查序列中是否還有元素。

應用:
Collection<String> all = new ArrayList<String>();
    all.add("A");
    all.add("B");
    all.add("C");
    all.add("D");
    all.add("E");
    Iterator<String> iter = all.iterator();
    while (iter.hasNext()) {// 判斷是否有下一個元素
        String str = iter.next(); // 取出當前元素
        System.out.print(str + "、");
    }
}

在使用 Iterator 輸出的時候有一點必須注意,在進行迭代輸出的時候如果要想刪除當前元素,則只能使用 Iterator 接口中的 remove()方法,而不能使用集合中的 remove()方法。否則將出現未知的錯誤。

Iterator 接口本身可以完成輸出的功能,但是此接口只能進行由前向后的單向輸出。如果要想進行雙向輸出,則必須 使用其子接口 —— ListIterator。

五、ListIterator

ListIterator 是可以進行雙向輸出的迭代接口,此接口定義如下:

public interface ListIterator<E>
extends Iterator<E>
方法:

但是如果要想使用 ListIterator 接口,則必須依靠 List 接口進行實例化。List 接口中定義了以下的方法:ListIterator listIterator()。

此處有一點需要注意的是,如果要想進行由后向前的輸出,則首先必須先進行由前向后的輸出。因為要將迭代器的位置后移。

六、Map接口

多值接口,里面的所有內容都按照 key?value 的形式保存,也稱為二元偶對象。與collection根接口同一級別,多值集合的根接口,存儲的是一個個鍵值對,通過鍵來訪問值,key不可以重復。

定義:
public interface Map<K,V>
方法:

Map 本身是一個接口,所以一般會使用以下的幾個子類:HashMap、TreeMap、Hashtable

6.1、HashMap

定義:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
應用:
map.put(1, "張三A");
map.put(1, "張三B"); // 新的內容替換掉舊的內容
map.put(2, "李四");
map.put(3, "王五");
String val = map.get(6);

get方法根據指定的 key 找到內容,如果沒有找到,則返回 null,找到 了則返回具體的內容。

Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "張三A");
map.put(2, "李四");
map.put(3, "王五");
Set<Integer> set = map.keySet(); // 得到全部的key
Collection<String> value = map.values(); // 得到全部的value
Iterator<Integer> iter1 = set.iterator();
Iterator<String> iter2 = value.iterator();
System.out.print("全部的key:");
while (iter1.hasNext()) {
    System.out.print(iter1.next() + "、");
}
System.out.print("\n全部的value:");
while (iter2.hasNext()) {
    System.out.print(iter2.next() + "、");
}

在JDK1.8之前,哈希表底層采用數組+鏈表實現,即使用鏈表處理沖突,同一hash值的鏈表都存儲在一 個鏈表里。但是當位于一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率 較低。而JDK1.8中,哈希表存儲采用數組+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉 換為紅黑樹,這樣大大減少了查找時間。 簡單的來說,哈希表是由數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的,如下圖所示。

初始桶數量是16,裝載因子為0.75,超過裝載因子時擴容,容量翻倍。

構造方法:
HashMap() 使用默認初始容量(16)和默認加載因子(0.75)構造一個空 HashMap
HashMap(int initialCapacity) 使用指定的初始容量和默認加載因子(0.75)構造一個空 HashMap
HashMap(int initialCapacity, float loadFactor) 使用指定的初始容量和加載因子構造一個空 HashMap
HashMap(Map<? extends K,? extends V> m) 構造一個新的 HashMap ,其映射與指定的 Map相同。
添加元素過程:

裝載因子過小,查找效率高,但是存儲空間利用率低。裝載因子過大,查找效率慢,但是存儲空間利用率高。一般要選取平衡,選取好初始容量,裝載因子默認0.75。

已經存儲在Map中的自定義對象如果作為鍵值存儲不要修改它,否則由于hashcode方法和equals方法的限制,在修改鍵值和再散列后的場景下,都無法找到原有對象。

6.2 HashTable與HashMap的區別

6.3 關于Map集合的輸出

1、 使用 Map 接口中的 entrySet()方法將 Map 接口的全部內容變為 Set 集合
2、 可以使用 Set 接口中定義的 iterator()方法為 Iterator 接口進行實例化
3、 之后使用 Iterator 接口進行迭代輸出,每一次的迭代都可以取得一個 Map.Entry 的實例
4、 通過 Map.Entry 進行 key 和 value 的分離

Map.Entry 本身是一個接口。此接口是定義在 Map 接口內部的,是 Map 的內部接口。此內部接口使用 static 進行定義, 所以此接口將成為外部接口。 實際上來講,對于每一個存放到 Map 集合中的 key 和 value 都是將其變為了 Map.Entry 并且將 Map.Entry 保存在了 Map 集合之中。

Map.Entry接口:
方法:
應用:
Set<Map.Entry<String, String>> set = map.entrySet();// 變為Set實例
Iterator<Map.Entry<String, String>> iter = set.iterator();
while (iter.hasNext()) {
    Map.Entry<String, String> me = iter.next();
    System.out.println(me.getKey() + " --> " + me.getValue());
}

Map 集合中每一個元素都是 Map.Entry 的實例,只有通過 Map.Entry 才能進行 key 和 value 的分離操作。

或使用foreach:
Map<String, String> map = new HashMap<String, String>();
map.put("ZS", "張三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "趙六");
map.put("SQ", "孫七");
for (Map.Entry<String, String> me : map.entrySet()) {
System.out.println(me.getKey() + " --> " + me.getValue());
}

七、Collections類

Collections 實際上是一個集合的操作類,此類的定義如下:

public class Collections extends Object

這個類與 Collection 接口沒有任何的關系。是一個單獨存在的類。

方法:
//很多,這里僅舉例
Collections.emptyList();// 空的集合

List<String> all = new ArrayList<String>();
Collections.addAll(all, "A", "B", "C");// 向集合增加元素

但是,從實際考慮,使用此類操作并不是很方便,最好的做法就是使用各個接口的直接操作的方法完成。此類只是 一個集合的操作類。

八、分析 equals、hashCode 與內存泄露

在 java 的集合中,判斷兩個對象是否相等的規則是:

(1)判斷兩個對象的 hashCode 是否相等 
        如果不相等,認為兩個對象也不相等,完畢 
        如果相等,轉入 2 (這一點只是為了提高存儲效率而要求的,其實理論上沒有也可以,但        如果沒有,實際使用時效率會大大降低,所以我們 這里將其做為必需的。后面會重點講        到這個問題。) 
        
(2)判斷兩個對象用 equals 運算是否相等 如果不相等,認為兩個對象也不相等 如果相等,認為兩個對象相等(equals()是判斷兩個對象是否相等的關鍵)

當一個對象被存進 HashSet 集合后,就不能修改這個對象中的那些參與計算的哈希值的字段了,否則,對象被修改后的哈 希值與最初存儲進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當前引用作為 的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從 HashSet 集合中刪除當前對象,從而 造成內存泄露。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。