之前學習了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值以及進行類型的相等性判斷。因此要重寫類的hashCode
和equals
方法。
例如下面的例子
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()
: 獲取下一個元素并返回。調用這個方法后,迭代器指向的位置發生改變
使用迭代器的一般步驟如下:
- 使用集合的
iterator()
返回一個迭代器 - 循環調用迭代器的
hasNext
方法,判斷集合中是否還有元素需要遍歷 - 使用
next
方法,找到迭代器指向的下一個元素
//假設set是一個 HashSet<String>集合
Iterator<String> it = set.iterator();
while(it.hasNext()){
Stirng s = it.next();
}
Map遍歷
索引和迭代器的方式只能遍歷單列集合,像Map這樣的多列集合不能使用上述方式,它有額外的方法,主要有兩種方式
- 獲取key的一個集合,遍歷key集合并通過get方法獲取value
- 獲取鍵值對組成的一個集合,遍歷這個新集合來得到鍵值對的值
針對第一種方法,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 />