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

以前學習了java中從語法到經常使用類的部分。在編程中有這樣一類需求,就是要保存批量的相同數據類型。針對這種需求通常都是使用容器來存儲。以前說過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相似於數學概念上的集合,不關心元素的順序,不能存儲重複元素。ide

  • 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,有點像數學中函數的自變量和值的關係。ui

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

經常使用的方法有:指針

  • 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類型自己。

相關文章
相關標籤/搜索