Java 學習筆記(10)——容器

之前學習了java中從語法到常用類的部分。在編程中有這樣一類需求,就是要保存批量的相同數據類型。針對這種需求一般都是使用容器來存儲。之前說過Java中的數組,但是數組不能改變長度。Java中提供了另一種存儲方式,就是用容器類來處理這種需要動態添加或者刪除元素的情況

概述

Java中最常見的容器有一維和多維。單維容器主要是一個節點上存儲一個數據。比如列表和Set。而多維是一個節點有多個數據,例如Map,每個節點上有鍵和值。
單維容器的上層接口是Collection,它根據存儲的元素是否為線性又分為兩大類 List與Set。它們根據實現不同,List又分為ArrayList和LinkedList;Set下面主要的實現類有TreeSet、HashSet。
它們的結構大致如下圖:


Collection 接口

Collection 是單列容器的最上層的抽象接口,它里面定義了所有單列容器都共有的一些方法:

  • boolean add(E e):向容器中添加元素
  • void clear(): 清空容器
  • boolean contains(Object o): 判斷容器中是否存在對應元素
  • boolean isEmpty(): 容器是否為空
  • boolean remove(Object o): 移除指定元素
  • <T> T[] toArray(T[] a): 轉化為指定類型的數組

List

list是Collection 中的一個有序容器,它里面存儲的元素都是按照一定順序排序的,可以使用索引進行遍歷。允許元素重復出現,它的實現中有 ArrayList和 LinkedList

  • ArrayList 底層是一個可變長度的數組,它具有數組的查詢快,增刪慢的特點
  • LinkedList 底層是一個鏈表,它具有鏈表的增刪快而查詢慢的特點

Set

Set集合是Collection下的另一個抽象結構,Set類似于數學概念上的集合,不關心元素的順序,不能存儲重復元素。

  • TreeSet是一顆樹,它擁有樹形結構的相關特定
  • HashSet: 為了加快查詢速度,它的底層是一個hash表和鏈表。但是從JDK1.8以后,為了進一步加快具有相同hash值的元素的查詢,底層改為hash表 + 鏈表 + 紅黑樹的結構。相同hash值的元素個數不超過8個的采用鏈表存儲,超過8個之后采用紅黑樹存儲。它的結構類似于下圖的結構


在存儲元素的時候,首先計算它的hash值,根據hash值,在數組中查找,如果沒有,則在數組對應位置存儲hash值,并在數組對應位置添加元素的節點。如果有,則先判斷對應位置是否有相同的元素,如果有則直接拋棄否則在數組對應位置下方的鏈表或者紅黑樹中添加節點。

從上面的描述看,想要在HashSet中添加元素,需要首先計算hash值,在判斷集合中是否存在元素。這樣在存儲自定義類型的元素的時候,需要保證類能夠正確計算hash值以及進行類型的相等性判斷。因此要重寫類的hashCodeequals 方法。
例如下面的例子

class Person{
    private String name;
    private int age;

    Person(){

    } 

    Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    public int getAge(){
        return this.age;
    }

    public String getName(){
        return this.name;
    }

    public void setAge(int age){
        this.age = age;
    }

    public void setName(String name){
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.name, this.age);
    }
}

上面說到HashSet是無序的結構,如果我們想要使用hashSet,但是又想它有序,該怎么辦?在Set中提供了另一個實現,LinkedHashMap。它的底層是一個Hash表和一個鏈表,Hash表用來存儲真正的數據,而鏈表用來存儲元素的順序,這樣就結合了二者的優先。

Map

Map是一個雙列的容器,一個節點存儲了兩個值,一個是元素的鍵,另一個是值。其中Key 和 Value既可以是相同類型的值,也可以是不同類型的值。Key和Value是一一對應的關系。一個key只能對應一個值,但是多個key可以指向同一個value,有點像數學中函數的自變量和值的關系。

Map常用的實現類有: HashMap和LinkedHashMap。

常用的方法有:

  • void clear(): 清空集合
  • boolean containsKey(Object key): map中是否包含對應的鍵
  • V get(Object key): 根據鍵返回對應的值
  • V put(K key, V value): 添加鍵值對
  • boolean isEmpty(): 集合是否為空
  • int size(): 包含鍵值對的個數

遍歷

針對列表類型的,元素順序固定,我們可以使用循環依據索引進行遍歷,比如

for(int i = 0; i < list.size(); i++){
    String s = list.get(i);
}

而對于Set這種不關心元素的順序的集合來說,不能再使用索引了。針對單列集合,有一個迭代器接口,使用迭代器可以實現遍歷

迭代器

迭代器可以理解為指向集合中某一個元素的指針。使用迭代器可以操作元素本身,也可以根據當前元素尋找到下一個元素,它的常用方法有:

  • boolean hasNext() : 當前迭代器指向的位置是否有下一個元素
  • E next(): 獲取下一個元素并返回。調用這個方法后,迭代器指向的位置發生改變

使用迭代器的一般步驟如下:

  1. 使用集合的 iterator() 返回一個迭代器
  2. 循環調用迭代器的 hasNext方法,判斷集合中是否還有元素需要遍歷
  3. 使用 next方法,找到迭代器指向的下一個元素
//假設set是一個 HashSet<String>集合
Iterator<String> it = set.iterator();
while(it.hasNext()){
    Stirng s = it.next();
}

Map遍歷

索引和迭代器的方式只能遍歷單列集合,像Map這樣的多列集合不能使用上述方式,它有額外的方法,主要有兩種方式

  1. 獲取key的一個集合,遍歷key集合并通過get方法獲取value
  2. 獲取鍵值對組成的一個集合,遍歷這個新集合來得到鍵值對的值

針對第一種方法,Map中有一個 keySet() 方法。這個方法會獲取到所有的key值并保存將這些值保存為一個新的Set返回,我們只要遍歷這個Set并調用 Map的get方法即可獲取到對應的Value, 例如:

// 假設map 是一個 HashMap<String, String> 集合
Set<String> kSet = map.keySet();
Iterator<String> key = kSet.iterator();
while(it.hasNext()){
    String key = it.next();
    String value = map.get(key);
}

針對第二種方法,可以先調用 Map的 entrySet() 獲取一個Entry結構的Set集合。Entry 中保存了一個鍵和它對應的值。使用結構中的 getKey()getValue() 分別獲取key和value。這個結構是定義在Map中的內部類,因此在使用的時候需要使用Map這個類名調用

// 假設map 是一個 HashMap<String, String> 集合
Set<Map.Entry<String,String>> entry = map.entrySet();
Iterator<Map.Entry<String, String>> it = entry.iterator();
while(it.hasNext()){
    Map.Entry<String, String> me = it.next();
    String key = me.getKey();
    String value = me.getValue();
}

for each 循環

在上述遍歷的代碼中,不管是使用for或者while都顯得比較麻煩,我們能像 Python 等腳本語言那樣,直接在 for 中使用迭代嗎?從JDK1.5 以后引入了for each寫法,使Java能夠直接使用for迭代,而不用手工使用迭代器來進行迭代。

for (T t: set); 

上述是它的簡單寫法。
例如我們對遍歷Set的寫法進行簡化

//假設set是一個 HashSet<String>集合
for(String s: set){
    //TODO:do some thing
}

我們說使用 for each寫法主要是為了簡化迭代的寫法,它在底層仍然采用的是迭代器的方式來遍歷,針對向Map這樣無法直接使用迭代的結構來說,自然無法使用這種簡化的寫法,針對Map來說需要使用上述的兩種遍歷方式中的一種,先轉化為可迭代的結構,然后使用for each循環

// 假設map 是一個 HashMap<String, String> 集合
Set<Map.Entry<String, String>> set = map.entrySet();
for(Map.Entry<String, String> entry: set){
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + "-->" + value);
}

泛型

在上述的集合中,我們已經使用了泛型。

泛型與C++ 中的模板基本類似,都是為了重復使用代碼而產生的一種語法。由于這些集合在創建,增刪改查上代碼基本類似,只是事先不知道要存儲的數據的類型。如果沒有泛型,我們需要將所有類型對應的這些結構的代碼都重復寫一遍。有了泛型我們就能更加專注于算法的實現,而不用考慮具體的數據類型。
在定義泛型的時候,只需要使用 <>中包含表示泛型的字母即可。常見的泛型有:

  • T 表示Type
  • E 表示 Element

<> 中可以使用任意標識符來表示泛型,只要符合Java的命名規則即可。使用 T 或者 E 只是為了方便而已,比如下面的例子

public static <Element> void print(Element e){
    System.out.println(e);
}

當然也可以使用Object 對象來實現泛型的重用代碼的功效,在對元素進行操作的時候主要使用java的多態來實現。但是使用多態的一個缺點是無法使用元素對象的特有方法。

泛型的使用

泛型可以在類、接口、方法中使用

在定義類時定義的泛型可以在類的任意位置使用

class DataCollection<T>{
    private T data;
    public T getData(){
        return this.data;
    }

    public void SetData(T data){
        this.data = data;
    }
}

在定義類的時候定義的泛型在創建對象的時候指定具體的類型.

也可以在定義接口的時候定義泛型

public interface DataCollection<T>{
    public abstract T getData();
    public abstract void setData(T data);
}

定義接口時定義的泛型可以在定義實現類的時候指定泛型,或者在創建實現類的對象時指定泛型

public class StringDataCollectionImpl implements DataCollection<String>{
    private String data;
    public String getData(){
        return this.data;
    }

    public void SetData(String data){
        this.data = data;
    }
}

public interface DataCollection<T> implements DataCollection<T>{
    private T data;
    public T getData(){
        return this.data;
    }

    public void SetData(T data){
        this.data = data;
    }
}

除了在定義類和接口時使用外,還可以在定義方法的時候使用,針對這種情況,不需要顯示的指定使用哪種類型,由于接收返回數據和傳入參數的時候已經知道了

public static <Element> Element print(Element e){
    System.out.println(e);
    return e;
}
String s = print("hello world");

泛型的通配符

在使用通配符的時候可能有這樣的需求:我想要使用泛型,但是不希望它傳入任意類型的值,我只想要處理繼承自某一個類的類型,就比如說我只想保存那些實現了某個接口的類。我們當然可以將數據類型定義為某個接口,但是由于多態的這一個缺陷,實現起來總不是那么完美。這個時候可以使用泛型的通配符。
泛型中使用 ? 作為統配符。在通配符中可以使用 super 或者 extends 表示泛型必須是某個類型的父類或者是某個類型的實現類

class Fruit{

}

class Apple extends Fruit{

}

class Bananal extends Fruit{

}

static void putFruit(<? extends Fruit> data){

}

上述代碼中 putFruit 函數中只允許 傳遞 Fruit 類的子類或者它本身作為參數。

當然也可以使用 <? super T> 表示只能取 T類型的父類或者T類型本身。
<hr />

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容