一.概述
1. 對象存儲
在java最初版本中需要存儲多個對象可以使用數組實現,數組的特點是長度一旦初始化之后確定下來就不能進行改變,這使得它失去了擴展性;此外,數組中提供的方法較少,一些常用操作需要手動實現,效率較低,盡管它初始化的時候限制了其中元素的類型安全性較高。現在我們設想一個場景,如果需要存儲不重復、有序的數據,這應該怎么實現呢?數組遍歷?這效率顯然就很低下了。再進一步,如果我們需要存儲鍵值對數據呢?使用數組實現就有點捉襟見肘了。
2. 集合框架
在java1.2之后,集合框架橫空出世。簡單地說,集合也可以認為是一種容器,可以動態地存儲對象的引用,利用其中的方法有助于高效地訪問。與其他數據結構類庫一樣,集合類庫也將接口與實現進行分離,因此我們在使用集合框架時,實際上調用的是特定接口的實現類。在java.util包中提供了一個泛型集合框架,其中包括多個接口以及實現,包括List、Set、Map等接口以及ArrayList、HashMap的實現。
3. 接口分類
集合中有兩個基本的接口:Collection和Map。
3.1 Collection接口
Collection 接口是 List、Set 和 Queue 接口的父接口,繼承于Iterable接口。該接口中定義了一些基本的方法,既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合,對于該接口沒有任何直接實現,但提供了其具體的子接口的實現。
- Set接口:該接口擴展自Collection接口,是不包含重復元素的集合(根據equals方法來確定元素是否一致),并且元素沒有特定順序
- SortedSet接口:Set的擴展接口,與Set相似,不同的是其中的元素是有序的
- List接口:List是可重復的、有序的集合,每個元素都有對應的順序索引,實現類主要有ArrayList、LinkedList等
- Queue接口:在queue中元素具有隱含的順序,且每個queue都有一個head元素
3.2 Map接口
Map與Collection并列存在,用于保存具有映射關系的鍵值對數據,Map 中的 key 和 value 都可以是任何引用類型的數據,因為Map 中的 key 用Set來存放,所以不允許重復,因此key值的所對應的對象須重寫hashCode()和equals()方法。因為key是唯一的,所以總是可以通過key去找到對應的value。Map還有一個擴展接口為sortedMap,不同的是其中的key是有序的。
3.3 Iterator接口
這是一個迭代器接口,使用該接口可以從集合中每次返回其中的一個元素。還有一個List對象的迭代器接口—— Listiterator接口,與Iterator接口相比,其中增加了一些與List有關的方法。此外,java.lang包下的Iterable接口也是集合框架的一部分,它是一個可提供Iterator的對象,可用于增強for。
3.4 接口樹
二、迭代器
1. Iterator接口與Iterable接口
1.1 概述
實現了Iterator接口的類表示一個迭代器,而實現了Iterable接口的類表示的是這個類是可迭代的。
Collection接口繼承于Iterable接口,也就是說Collection接口是可迭代的。調用Collection接口的Iterator方法返回的是一個迭代器對象,該對象主要用于遍歷Collection集合中的元素,這也是前面說到的集合框架相較于數組的特點:提供了一種新的方式進行存儲元素的遍歷,因為創建的是一個新的對象,所以并不會對原來的集合對象產生影響,并且迭代器內部實現了遍歷過程的諸多細節。需要注意的是,集合對象每次調用iterator()方法都得到一個全新的迭代器對象,默認游標都在集合的第一個元素之前。此外還有一個ListIterator接口拓展了Iterator接口,在其中添加了一些方法使其可迭代操作一個經過排序的List對象,可以使用hasNext與next向前迭代,也可以使用hasPrevious與previous向后迭代。
1.2 迭代器對象
一個迭代器對象具有幾個用于遍歷元素的方法,游標默認在集合第一個元素之前。
- hasNext() 返回的是下一個位置是否存在元素,可以用來檢測是否到達集合末端
- next() 返回迭代下一個元素,如果下一位沒有元素會報NoSuchElementException錯誤
- remove() 刪除迭代最近返回的元素,需要先調用next()確保正確刪除元素,如果已經在調用next之后調用了remove,再次調用會拋出一個IllegalStateException異常
2. 使用迭代器遍歷操作
第一種方式:通過hasNext、next方法遍歷集合,以后面要講的ArrayList為例
@Test
public void test1(){
Collection coll1 = new ArrayList();
coll1.add("jack");
coll1.add(123);
//獲取迭代器對象
Iterator itColl1 = coll1.iterator();
//hasNext() 判斷下一位是否有元素
while (itColl1.hasNext()){
//next() 獲取下一位的元素
System.out.println("2)"+itColl1.next()); // output: jack
}
}
第二種方式:java5之后可以實現foreach循環迭代訪問集合與數組,底層使用iterator實現
public class ForEachText {
@Test
public void test1() {
Collection coll1 = new ArrayList();
coll1.add("Tom");
coll1.add(123);
// 使用增強for循環遍歷元素 這種方式遍歷無需下標
for (Object obj : coll1) {
System.out.println(obj); // output:Tom 123
}
}
}
三、Collection子接口的實現
1. Collection接口中定義的方法
在該接口中定義了一些用于操作集合數據的通用方法,但并無具體實現,而是在子接口中實現了具體的操作。
- coll1.add(object e) 將元素e添加到coll1中
- coll1.size() 查看coll1的長度
- coll1.addAll(Collection coll2) 把coll2的元素添加到coll1的后面
- coll1.clear() 清除coll1中的數據 但是該對象仍存在
- coll1.isEmpty() 判斷coll1是否數據為空 (并非空指針)
- coll1.contains(object obj) 判斷當前集合是否包含obj 注意:這里如果是對比的是String的內容是否一致的話將會返回true,因為String重寫了equals方法。如果其他類沒有重寫equals方法就會是false。因此裝入collection的數據最好都要重寫equals方法
- coll1.containsAll(Collection coll2)方法 判斷coll2的數據是否都在coll1中
- coll1.remove(object obj) 移除集合中的元素obj
- coll1.removeAll(Collection coll2) 從coll1中移除coll2的所有元素
- coll1.retainAll(Collection coll2) 處理后coll1的內容為coll1與coll2的交集
- coll1.equals(Collection coll2) 將coll1與coll2中所有元素進行比較
- coll1.hashCode() 計算coll1的哈希值
- coll1.toArray() 實現集合轉數組 注:數組到集合的轉換可以利用Arrays.asList() 方法實現
- coll1.iterator() 返回一個迭代器對象
2. Set
Set(集)是一種不包括重復元素的無序的集合。它擴展了Collection但是沒有增加新的方法。如果向set中連續添加同一個元素,第一次會返回true,之后都將返回false。那么如何判斷是否是同個元素呢?Set用到的是添加元素對象的equals方法以及hashcode方法,這些方法的具體細節下面會講到。在Java中實現了Set接口的類有LinkedHashSet、HashSet、TreeSet等。
2.1 HashSet
HashSet是一個用散列表實現的Set,是Set接口的典型實現,大多數時候使用 Set 集合時都使用這個實現類。HashSet按 Hash 算法來存儲集合中的元素,因此具有很好的存取、查找、刪除性能。HashSet不能保證元素的排列順序,它也不是線程安全的。對于HashSet來說,如果兩個元素相等,那么這兩個元素的hashcode必須相等,并且equals必須返回true。因此,存放在HashSet中的元素一定要重寫hashCode方法以及equals方法,保證散列碼的生成。
當我們向HashSet中添加元素時,會發生什么呢?首先,HashSet 會調用該對象的 hashCode() 方法來得到該對象的 hashCode值,然后根據 hashCode 值,通過某種散列函數決定該對象在 HashSet 底層數組中的存儲位置。例如:hashCode是1000,底層數組長度為15,那么1000%15即是該元素的索引,雖然這種計算很low... 元素越是散列分布,說明散列函數設計得越好,如果兩個元素的hashCode()值相等,會再繼續調用equals方法,如果equals方法結果 為true,添加失敗;如果為false,那么會保存該元素,因為該數組的位置已經有元素了(散列沖突),所以會通過鏈表的方式繼續鏈接。散列沖突發生后,元素a將與已存在該索引的數據以鏈表形式存儲:在jdk7中是元素a放到數組中,指向原來的鏈表;在jdk8中是原來的鏈表放在數組中,指向元素a;因此HashSet的底層為數組+鏈表。
@Test
public void test1() {
Set set = new HashSet();
set.add(123);
set.add("abc");
set.add(new User("tom",12));
set.add(123);
Iterator setIt = set.iterator();
while(setIt.hasNext()){
System.out.println(setIt.next());
}
}
//output:abc 123 User{name='tom', age=12}
//從結果可以看見重復的元素并沒有被存儲到HashSet中
此外,HashSet除了空參構造器之外還有一個帶初始容量和負載因子參數的構造器,這里的初始容量表示底層數組的大小,負載因子指明當HashSet達到多少占用率進行擴容,這兩個參數的具體細節會在HashMap處剖析。
2.2 LinkedHashSet
LinkedHashSet 是 HashSet 的子類,它根據元素的 hashCode 值來決定元素的存儲位置,但它同時使用雙向鏈表維護元素的次序,這使得元素看起來是以插入順序保存的。LinkedHashSet插入性能略低于 HashSet,但在迭代訪問 Set 里的全部元素時有很好的性能。LinkedHashSet 不允許集合元素重復。對其進行迭代所需的時間只與大小成比例,與容量無關。
2.3 TreeSet
TreeSet 是 SortedSet 接口的實現類,它會將內容存儲在一個樹形結構中,可以確保集合元素處于排序狀態。TreeSet底層使用紅黑樹結構存儲數據,對于樹的修改或者搜索的時間復雜度為O(logn)。對于TreeSet有兩種排序方法,一種是自然排序(默認),另一種是定制排序。他有四個構造器:
public TreeSet(){} //創建一個新的樹集 其中的元素必須實現Comparable接口
public TreeSet(Collection <? extends E> coll){} //生成樹集 將coll中的元素加入樹集
public TreeSet(Comparator <? super E> comp){} //創建一個樹集 根據comp指定順序排序(定制排序)
public TreeSet(SortedSet <E> set){} //將set的內容及排序方式遷移生成新的樹集
在自然排序中,TreeSet會調用元素的compareTo方法比較大小,之后按照升序排序,因此添加入TreeSet中的元素必須實現Comparable接口的compareTo方法。
import org.junit.Test;
import java.util.*;
//自定義類User
class User implements Comparable{
private String name;
private Integer age;
/******************省略構造器 toString方法等**********************/
// 在自定義類中實現Comparable接口的compareTo方法的雙層排序
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
if(this.age.compareTo(user.age)==0) return this.name.compareTo(user.name);
else return this.age.compareTo(user.age);
}
else{
throw new RuntimeException("類型不匹配");
}
}
}
//自然排序實例
class TreeSetTest{
@Test
public void test() {
TreeSet<Integer> set = new TreeSet<>();
set.add(123);
set.add(-5);
set.add(234);
Iterator<Integer> setIt = set.iterator();
while(setIt.hasNext()){
System.out.println(setIt.next());
}
//output: -5 123 234
TreeSet<Object> set2 = new TreeSet<>();
set2.add(new User("Tom",15));
set2.add(new User("Jack",20));
set2.add(new User("Oliver",12));
set2.add(new User("Adam",20));
Iterator<Object> set2It = set2.iterator();
while (set2It.hasNext()){
System.out.println(set2It.next());
}
//output: User{name='Oliver', age=12} User{name='Tom', age=15}
// User{name='Adam', age=20} User{name='Jack', age=20}
}
}
定制排序通過Comparator接口來實現,其中需要重寫compare(T o1,T o2)方法,如果方法返回正整數,則表示o1大于o2;如果返回0,表示相等;返回負整數,表示o1小于o2。要實現定制序,需要將實現Comparator接口的實例作為形參傳遞給TreeSet的構造器。使用定制排序判斷兩個元素相等的標準是:通過Comparator比較兩個元素返回了0。
public class TreeSetTest1 {
@Test
public void test3() {
// 定制排序 lambda表達式
TreeSet<User> set = new TreeSet<>((o1, o2) -> {
if(o1 != null && o2 != null){ return o1.getName().compareTo(o2.getName()); }
else{ throw new RuntimeException("類型不匹配"); }
});
set.add(new User("Tom",15));
set.add(new User("Jack",20));
set.add(new User("Oliver",12));
set.add(new User("Adam",20));
Iterator<User> setIt = set.iterator();
while(setIt.hasNext()){
System.out.println(setIt.next());
}
}
}
經典樣例:
class Test{
@Test
public void test2() {
HashSet set = new HashSet();
Person p1 = new Person("AA",1001);
Person p2 = new Person("BB",1002);
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set);
//output:[Person{name='CC', age=1001}, Person{name='BB', age=1002}]
//因為移除的是p1(AA 1001) 但實際上存儲在set中的p1已經修改為(CC 1001)
set.add(new Person("CC",1001));
System.out.println(set);
/*output:
[Person{name='CC',age=1001},Person{name='CC',age=1001},Person{name='BB',age=1002}]
其中一個(CC 1001)在set中的索引實際上的(AA 1001)的hashCode計算出來的索引 因此有兩個共存
*/
set.add(new Person("AA",1001));
System.out.println(set);
/*output:
[Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}, Person{name='BB', age=1002}]
新增加的(AA 1001)的hashCode和其中一個由p1轉變而來的(CC 1001)的hashCode是一樣,但如上面增加元素時的闡述,
當hashCode一致時,會調用元素的equals方法,比較他們的name和age是否一致,很明顯這里的AA和CC不一致,因此仍舊可以增加成功
*/
}
}
3. List
前面講到用數組存儲數據存在一些缺點,所以通常使用List替代數組,List接口拓展了Collection接口,定義了規定元素順序的集合。集合每個元素都有特定的位置(從0開始)。因此對List使用add方法時,會增加在尾部,移除元素時會將其后元素向前移動。也就是說,List集合類中元素有序、且可重復,集合中的每個元素都有其對應的順序索引,可以根據序號存取容器中的元素。List除了從Collection集合繼承的方法外,還添加了一些根據索引來操作集合元素的方法。
- void add(int index, Object ele):在index位置插入ele元素
- boolean addAll(int index, Collection eles):從index位置開始將eles中的所有元素添加進來
- Object get(int index):獲取指定index位置的元素
- int indexOf(Object obj):返回obj在集合中首次出現的位置
- int lastIndexOf(Object obj):返回obj在當前集合中末次出現的位置
- Object remove(int index):移除指定index位置的元素,并返回此元素
- Object set(int index, Object ele):設置指定index位置的元素為ele
- List subList(int fromIndex, int toIndex):返回從fromIndex到toIndex位置的子集合
JDK API中List接口的實現類常用的有:ArrayList、LinkedList和Vector。
3.1 ArrayList
ArrayList 是 List 接口的主要實現類,它將元素存在一個數組中。本質上,ArrayList是對象引用的一個”變長”數組。 關于ArrayList的初始容量,在 JDK1.7中,ArrayList像單例模式中的餓漢式,在底層直接創建一個初始容量為10的數組,在 JDK1.8中,ArrayList像單例模式中的懶漢式,底層創建一個長度為0的數組,當添加第一個元素時再創建一個始容量為10的數組。那么如果初始容量不足呢?這時候會開始ArrayList的擴容機制。在jdk7與jdk8中是將ArrayList擴充為原來的1.5倍。ArrayList有三個構造器:
public ArrayList(){} //使用默認容量創建一個ArrayList
public ArrayList(int initalCapacity){} //創建一個底層數組大小為initalCapacity的ArrayList
public ArrayList(Collection <? extends E> coll){} //創建一個包含coll所有元素的ArrayList 初始容量為coll的1.1倍
ArrayList的一波方法示例:
@Test
public void test3() {
ArrayList list = new ArrayList();
list.add("tom");
list.add(123);
list.add(new Person("jack",15));
list.add(123);
System.out.println(list); //output:[tom, 123, Person{name='jack', age=15}, 123]
// 1) list.add(index,Object obj) 向list中索引為index的位置插入obj
list.add(1,"jerry");
System.out.println(list);
//output:[tom, jerry, 123, Person{name='jack', age=15}, 123]
// 2) list.addAll(index,List ls) 向list中索引為index的位置插入ls的全部元素
List ls = Arrays.asList(1, 2, 3);
list.addAll(2,ls);
System.out.println(list);
//[tom, jerry, 1, 2, 3, 123, Person{name='jack', age=15}, 123]
// 3) list.get(index) 獲取list中索引為index的元素
System.out.println(list.get(0)); //oputput:tom
// 4) list.indexOf(Object obj) 返回obj在list中首次出現的索引 不存在即返回-1
System.out.println(list.indexOf(123)); //output:5
// 5) list.lastIndexOf(Object obj) 返回obj在list中末次出現的索引 不存在即返回-1
System.out.println(list.lastIndexOf(123)); //output:7
// 6) list.set(index,Object obj) 將list中索引為index的元素更換為obj
list.set(2,654);
System.out.println(list);
//output:[tom, jerry, 654, 2, 3, 123, Person{name='jack', age=15}, 123]
// 7) list.remove(index) 將list中索引為index的元素刪除
// 需要注意的是 如果輸入一個整型數據 默認情況下是按照索引刪除
// 如果要刪除其中的數據 需要new Integer(xx) 去進行刪除
Object obj = list.remove(2); // 這里返回的是被刪除的元素
System.out.println(obj); //output:654
System.out.println(list);
//output:[tom, jerry, 2, 3, 123, Person{name='jack', age=15}, 123]
// 8) list.subList(fromIndex,Index) 返回一個從fromIndex到Index(左閉右開)的list子集合
List list1 = list.subList(3, 6);
System.out.println(list1);
//output:[3, 123, Person{name='jack', age=15}]
}
3.2 LinkedList
LinkedList是一個雙向鏈表,它對于隨機讀取的效率低于ArrayList,但對于插入數據的效率高于ArrayList。LinkedList內部沒有聲明數組,而是定義了Node類型的first和last,用于記錄首末元素。同時,定義內部類Node,作為雙向鏈表中保存數據的基本結構。Node除了保存數據,還定義了兩個變量:prev變量記錄前一個元素的位置,next變量記錄下一個元素的位置。
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList提供了兩個構造器,一個為空參構造器,一個可以創建包含指定Collection元素的LinkedList,此外還實現了Collection的一些方法以及自定義的方法:
@Test
public void test() {
var list1 = new LinkedList<String>();
list1.add("test1");
list1.add("test2");
list1.add("test3");
var list2 = new LinkedList<String>();
list2.add("demo1");
list2.add("demo2");
list2.add("demo3");
list2.add("demo4");
// 1) addFirst 將元素加到鏈表頭部
// 2) addLast 將元素加到鏈表尾部
list1.addFirst("first");
list1.addLast("last");
System.out.println(list1); //output:[first, test1, test2, test3, last]
// 3) getFirst 獲取鏈表頭部元素
System.out.println(list2.getFirst()); //output:demo1
// 4) getLast 獲取鏈表尾部元素
System.out.println(list2.getLast()); //output:demo4
// 5) removeFirst 刪除鏈表頭部元素
// 6) removeLast 刪除鏈表尾部元素
list2.removeFirst();
list2.removeLast();
System.out.println(list2); //output:[demo2, demo3]
}
3.3 Vector
Vector是一個古老的集合,大多數操作與ArrayList相同,區別之處在于Vector是線程安全的。 在各種list中,最好把ArrayList作為缺省選擇。當插入、刪除頻繁時,使用LinkedList;Vector總是比ArrayList慢,所以盡量避免使用。
ps:ArrayList/LinkedList/Vector的異同?ArrayList底層是什么?擴容機制?Vector和ArrayList的最大區別?
1)ArrayList和LinkedList的異同 二者都線程不安全,相對線程安全的Vector,執行效率高。此外,ArrayList是實現了基于動態數組的數據結構,LinkedList基于鏈表的數據結構。對于隨機訪問get和set,ArrayList覺得優于LinkedList,因為LinkedList要移動指針。對于新增和刪除操作add(特指插入)和remove,LinkedList比較占優勢,因為ArrayList要移動數據。
2)ArrayList和Vector的區別 Vector和ArrayList幾乎是完全相同的,唯一的區別在于Vector是同步類(synchronized),屬于 強同步類。因此開銷就比ArrayList要大,訪問要慢。正常情況下,大多數的Java程序員使用 ArrayList而不是Vector,因為同步完全可以由程序員自己來控制。Vector每次擴容請求其大 小的2倍空間,而ArrayList是1.5倍。Vector還有一個子類Stack。
4. Queue
Queue接口拓展了Collection接口,隊列定義了一個head位置,它是下一個要被移除的元素。隊列通常是先進先出的操作(棧是后進先出),或者按照指定的順序進行。可以使用element方法獲取隊列頭,如果隊列為空則拋異常;使用peek方法可以返回隊列頭,隊列為空不拋異常;同時,最好使用offer方法添加元素,使用poll方法移除元素,因為使用這兩個方法添加/移除元素失敗時不會拋出異常,而Collection的add和remove方法失敗后拋異常。上面提到的LinkedList類提供了Queue的簡單實現,但應避免插入null。
4.1 Deque
Java6中引入了Deque接口(雙端隊列),它是Queue接口的子接口,雙端隊列允許在頭部和尾部增刪元素,不允許在中間添加元素。ArrayDeque和LinkedList類實現了這個接口,這兩個類都提供雙端隊列。
@Test
public void test1() {
var arr = new ArrayDeque<String>();
//1)添加元素到隊列頭部、尾部 有兩種方法
//1.1)第一種:addFirst() addLast() 如果隊列已滿添加失敗會拋出IllegalStateException異常
//1.2)第二種:offerFirst() offerLast() 如果隊列已滿添加失敗會返回false
arr.add("tom1");
arr.add("tom2");
System.out.println(arr); //output:[tom1, tom2]
arr.addFirst("tom3");
System.out.println(arr); //output: [tom3, tom1, tom2]
arr.offerLast("tom4");
System.out.println(arr); //output: [tom3, tom1, tom2, tom4]
//2)如果隊列不為空 刪除頭部元素、尾部元素的方法有兩種
//2.1)第一種:removeFirst() removeLast() 如果隊列為空會拋出NoSuchElementException異常
//2.2)第二種:pollFirst() pollLast() 如果隊列為空 刪除失敗會返回null
arr.removeFirst();
System.out.println(arr); //output: [tom1, tom2, tom4]
arr.pollLast();
System.out.println(arr); //output: [tom1, tom2]
//3)如果隊列不為空 獲取頭部元素、尾部元素的方法有兩種
//3.1)第一種:getFirst() getLast() 如果隊列為空會拋出一個NoSuchElementException異常
//3.2)第二種:peekFirst() peekLast() 如果隊列為空 獲取失敗會返回null
String first = arr.getFirst();
String last = arr.peekLast();
System.out.println(first+"\t"+last); //output:tom1 tom2
}
4.2 PriorityQueue
優先隊列采用堆這種數據結構,元素可以按照任意順序插, 但是刪除的時候總是會刪除最小的那個。優先隊列主要用于任務調度,當啟動新任務時會把優先級最高的任務從隊列中刪除(習慣上優先級最高的是1)。這里添加的對象(元素)需要實現compareTo方法。
@Test
public void test1() {
var pq = new PriorityQueue<LocalDate>();
pq.add(LocalDate.of(2019, 6, 1));
pq.add(LocalDate.of(2019, 3, 18));
pq.add(LocalDate.of(2020, 8, 20));
// 調用remove時會把元素中最小的那個刪除掉
pq.remove();
System.out.println(pq); //output:[2019-06-01, 2020-08-20]
}
4.3 阻塞隊列
還有一種隊列是阻塞式隊列,隊列滿了以后再插入元素則會拋出異常,主要實現類包括:ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。雖然接口并未定義阻塞方法,但是實現類擴展了父接口,實現了阻塞方法。[origin]
四、Map接口的實現
Map接口與Collection接口并列存在,用于保存具有映射關系的鍵值對數據,Map 中的 key 和 value 都可以是任何引用類型的數據。常用String類作為Map的“鍵”,key 和 value 之間存在單向一對一關系,即通過指定的 key 總能找到 唯一的、確定的 value。Map接口的常用實現類:HashMap、TreeMap、LinkedHashMap和Properties。
Map的實現類繼承結構如下:
1. HashMap
HashMap使用散列表實現Map,其根據每個鍵的hashCode方法確定位置,因此key元素對應類要重寫equals()和hashCode()。同時所有的value構成的集合是無序的、可以重復的。因此value元素對應類要重寫equals()方法。HashMap允許使用null鍵和null值,與HashSet一樣,不保證映射的順序。在HashMap中,一個key-value鍵值對構成一個entry,所有的entry構成的集合是無序的、不可重復的。HashMap 判斷兩個 key 相等的標準是:兩個 key 通過 equals() 方法返回 true,hashCode 值也相等。HashMap 判斷兩個 value相等的標準是:兩個 value 通過 equals() 方法返回 true。
1.1 構造器
public HashMap() 使用默認初始容量以及負載因子創建一個新的HashMap
public HashMap(int init) 使用給定init和默認負載因子創建一個HashMap
public HashMap(int init,float loadFactor) 使用init個散列位和給定的loadFactor(非負)創建一個HashMap
public HashMap(Map <? extends K, ? extends V> map) 創建一個HashMap并從map中復制內容,初始容量基于map大小,負載因子默認
1.2 常用方法
集合框架并沒有將映射表本身視為一個集合,所以它和Collection是平級的。但可以獲得映射表的視圖,這是一組實現了Collection接口或者它的子接口的視圖。有3個視圖,它們分別是:鍵集、值集和鍵值對集。
Set<K> keyset(); // 生成一個鍵集
Collection<K> values(); //生成一個值集
Set<Map.Entry<K,V>> entrySet(); //生成一個鍵值對集
keySet方法返回的是實現了Set接口的類的對象,這個類的方法對原映射表進行操作。Set接口擴展了Collection接口,因此可以與使用任何集合一樣使用keySet。對于entrySet同理。
// ********************HashMap常用方法**********************
@Test
public void test1() {
HashMap<String,Object> map = new HashMap<>();
HashMap<String,Object> mapCp = new HashMap<>();
//1) 添加鍵值對 put(key,value)
map.put("id","1");
map.put("id",2); // 對于相同的key執行put操作 實際上是進行value的替換
map.put("age",15);
mapCp.put("address","China");
//2) 批量添加鍵值對 putAll(HashMap xx)
map.putAll(mapCp);
//3) get(key) 獲取key對應的value 不做移除操作
Object id = map.get("id");
//4) remove(key) 對key對應的鍵值對作移除操作 返回的是key對應的value
map.remove("name");
//5) boolean containsKey(key) 查詢是否包含key
// boolean containsValue(value) 查詢是否包含value
map.containsKey("id");
map.containsValue(15);
//6) map.clear() 將map中的所有元素依次置為null 但map此時并非null 調用size()不會空指針
map.clear();
map.size();
}
@Test
public void test2() {
HashMap<String,Object> map = new HashMap<>();
map.put("name","tom");
map.put("age",18);
map.put("area","sz");
//1)keySet方法 取出所有的key 返回一個set
Set<String> mks = map.keySet();
// 接著可以使用迭代器迭代輸出
Iterator<String> mksIt = mks.iterator();
//2)values方法 取出所有的value 返回一個collection
Collection<Object> mvs = map.values();
// 接著可以使用迭代器迭代輸出
Iterator<Object> mvsIt = mvs.iterator();
//3)entrySet方法 取出所有的Entry數組 返回一個set
Set<Map.Entry<String, Object>> mes = map.entrySet();
// 接著可以使用迭代器迭代輸出
Iterator<Map.Entry<String, Object>> mesIt = mes.iterator();
while(mesIt.hasNext()){
Map.Entry<String, Object> entry = mesIt.next();
System.out.println("key is "+entry.getKey()+"|values is "+entry.getValue());
}
// 如果需要遍歷key-values 只需要知道其中一個即可自由組合遍歷出 需要靈活運用方法
}
1.3 底層實現原理
HashMap的底層實現原理Java7與Java8有所不同。在Java7及以前版本,HashMap是數組+鏈表結構(即為鏈地址法)。在Java8版本中HashMap是數組+鏈表+紅黑樹實現。
調用HashMap的空參構造器實例化時,底層創建了長度是16的一維數組Entry[] table(在Java8中只有當調用put方法后才會創建數組,并且類型為Node)。當HashMap開始添加元素,調用map.put(key1,value1)的時候,首先調用key1所在類的hashCode()方法,計算key1的哈希值,此值通過算法計算出 Entry數組在HashMap底層數組中的存放位置(索引位置),接著判斷數組此位置上是否有存在元素:
----①如果該位置為空,則key1-value1鍵值對插入成功;
----②如果該位置不為空(意味著存在一個或以鏈表形式存在的多個數據),則比較key的hash值:
--------①如果key1的hash值與已存在數據的hash值都不相同,則插入成功;
--------②如果key1的hash值與已存在的數據(key2-value2)的hash值相同,則調用key1所在類的equals方法:
----------------①equals返回true,則value1覆蓋value2;
----------------②equals返回false,則key1-value1添加成功;
需要注意的是,對于除第一種之外的添加成功的情況,key1-value1將與已存在該索引的數據以鏈表形式存儲。
1.4 擴容機制
當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數組進行擴容,而在HashMap數組擴容之后,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,并放進去,這就是resize(再次散列)。
當HashMap中的元素個數超過數組大小與loadFactor乘積時,就會進行數組擴容。 前面也講到了,loadFactor 的默認值為0.75,這是一個折中的取值,也就是說,默認情況下,當HashMap中元素個數超過12(16*0.75)的時候,就把數組的大小擴大一倍為32,然后重新計算每個元素在數組中的位置,因為這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,預設元素的個數能夠有效的提高HashMap的性能。在Java8中,當HashMap中的一個鏈的對象個數達到了8個,此時如果capacity沒有達到64,那么HashMap會先擴容解決,如果已經達到了64,那么這個鏈會變成樹,結點類型由Node變成TreeNode類型。當然,如果當映射關系被移除后,下次resize時判斷樹的結點個數低于6個,也會把樹再轉為鏈表。
負載因子值的大小,對HashMap有什么影響
- 負載因子的大小決定了HashMap的數據密度。
- 負載因子越大密度越大,發生碰撞的幾率越高,數組中的鏈表越容易長,造成查詢或插入時的比較次數增多,性能會下降。
- 負載因子越小,就越容易觸發擴容,數據密度也越小,意味著發生碰撞的幾率越小,數組中的鏈表也就越短,查詢和插入時比較的次數也越小,性能會更高。但是會浪費一定的內容空間。而且經常擴容也會影響性能,建議初始化預設大一點的空間。
- 按照其他語言的參考及研究經驗,會考慮將負載因子設置為0.7~0.75,此時平均檢索長度接近于常數。
2. LinkedHashMap
LinkedHashMap 是 HashMap 的子類,在HashMap存儲結構的基礎上,使用了一對雙向鏈表來記錄添加元素的順序。與LinkedHashSet類似,LinkedHashMap 可以維護 Map 的迭代順序:迭代順序與 Key-Value 對的插入順序一致。由于維護鏈表結構所產生的開銷,LinkedHashMap的性能可能會比HashMap差一點,但是其迭代所需時間只與LinkedHashMap的大小成比例,而與容量無關。
3. TreeMap
TreeMap存儲鍵值對時,需要根據鍵值對進行排序,它可以保證所有的鍵值對處于有序狀態。TreeSet底層使用紅黑樹結構存儲數據TreeMap 的 Key 的排序,這里同樣有兩種排序方法:如果使用自然排序,那么TreeMap 的所有的 Key 必須實現 Comparable 接口,而且所有的 Key 應該是同一個類的對象,否則將會拋出 ClasssCastException異常;如果使用定制排序,需要在創建 TreeMap 時傳入一個 Comparator 對象,該對象負責對TreeMap 中的所有 key 進行排序,此時不需要 Map 的 Key 實現Comparable 接口。關于排序的內容與TreeSet基本一致,這里不再贅述。TreeMap判斷兩個key相等的標準是兩個key通過compareTo()方法或者compare()方法返回0。一般來說只有在需要排序或者hashCode方法實現太差時才會使用TreeMap。
TreeMap有幾個構造器,分別為:
public TreeMap() 創建一個TreeMap,其中的鍵按照自然排序
public TreeMap(Map <? extends K, ? extends V> map) 等價于先調用TreeMap再將map中的鍵值對加進去
public TreeMap(Comparator <? super K> comp) 創建一個TreeMap,按照定制排序排列鍵
public TreeMap(SortedMap<K, ? extends V> map) 創建TreeMap,初始內容與排序方式皆與map相同
4. Hashtable
Hashtable是個古老的 Map 實現類,不同于HashMap,Hashtable是線程安全的。Hashtable實現原理和HashMap相同,功能相同。底層都使用哈希表結構,查詢速度快,很多情況下可以互用。但是Hashtable 不允許使用 null 作為 key 和 value。與HashMap一樣,Hashtable 也不能保證其中 Key-Value 對的順序。Hashtable判斷兩個key相等、兩個value相等的標準,與HashMap一致。
4.1 Properties
Properties 類是 Hashtable 的子類,該對象用于處理屬性文件。由于屬性文件里的 key、value 都是字符串類型,所以 Properties 里的 key 和 value 都是字符串類型。存取數據時,建議使用setProperty(String key,String value)方法和getProperty(String key)方法。
// 利用properties類讀取配置文件 需要用到io流
@Test
public void test1() throws Exception {
FileInputStream fis = null;
Properties pro = new Properties();
fis = new FileInputStream("top/jtszt/ReflectionTest/jdbc.properties");
pro.load(fis);
String name = pro.getProperty("name");
String password = pro.getProperty("password");
System.out.println("name is "+name+", password is "+password);
fis.close();
}
}
五. Collections與Arrays
1. Collections工具類
Collections 是一個操作 Set、List 和 Map 等集合的工具類。Collections 中提供了一系列靜態的方法對集合元素進行排序、查詢和修改等操作,還提供了對集合對象設置不可變、對集合對象實現同步控制等方法。Collections提供了多個synchronizedXx()方法,把線程不安全的list/Collection/map轉換為線程安全的。
@Test
public void test1() {
List<Integer> list = new ArrayList<>();
list.add(41);
list.add(13);
list.add(55);
list.add(55);
//1) reverse(list)方法 反轉list中的元素
Collections.reverse(list); // 對list做修改 沒有返回值
//2) shuffle(list)方法 對list進行隨機打亂排列
Collections.shuffle(list);
//3) sort(list) 對list進行排序 需要實現compareTo方法
Collections.sort(list);
//4) swap(list,i,j) 對list中的i處的元素和j處的進行交換
Collections.swap(list,0,3);
//5) max(Collection) 返回Collection中最大的元素(基于compareTo方法)
//6) min(Collection) 返回Collection中最小的元素(基于compareTo方法)
//7) frequency(Collection,i) 返回Collection中i出現的次數
int frequency = Collections.frequency(list, 55);
//8) copy(list dest,list src) 把src的內容復制到dest中
// 應該先新建一個帶src.size()個數null的一個list
List<Object> dest = Arrays.asList(new Object[list.size()]);
Collections.copy(dest,list);
// 9) 利用synchronizedList(list)方法 將list轉換為線程安全的
List<Integer> list1 = Collections.synchronizedList(list);
}
}
2. Arrays工具類
Arrays類提供了用于處理數組的靜態方法,其中大多數都有完備的重載形式:一個用于基本數據類型數組,一個用于Object數組。在實際開發中,有時候需要在數組與集合之間進行轉換,這時就可以利用Arrays.asList方法實現數組到集合的轉換,也可以使用toArray()方法實現集合到數組的轉換。集合在轉化為具體類型數組時需要強制類型轉換,并且要使用帶參數的toArray方法,參數為對象數組。此外還有其他的一些方法:
- sort:按升序排序數組
- binarySearch:在有序數組中查找給定的鍵。該方法將返回鍵的下標或對安全插人點進行編碼的負值
- fill:使用給定的值填充數組
- equals 和 deepEquals:如果傳入的兩個數組是同一個對象,或都是null,或大小相同且包含等價的內容,則返回 true。這兩個方法沒有子數組版本,用于 Object[ ]的equals方法將調用數組中每個元素的 Object.equals方法。因為該方法沒有對嵌套數組進行特殊處理,所以一般不能用它來比較包含數組的數組。deepEquals方法對兩個 object[ ]的等價性進行的是遞歸檢查,而且考慮了嵌套數組的等價性。
- hashCode和deepHashCode:基于給定數組的內容返回一個散列碼。因為用于 Object[ ] 的 deepHashcode方法考慮了嵌套數組的內容,所以 deepHashCode 方法將遞歸地計算 Object[ ] 的散列碼。
- toString 和 deepToString:返回數組內容的字符串表示。所返回的字符串由數組的元素列表組成,元素之間由逗號分隔,并且整個列表用[ ]括了起來。數組類型的內容會通過String.valueof轉換成字符串。用于 0bject[ ]的toString方法可以使用 object.toString方法把所有嵌套數組轉換成字符串。 deepToString方法可以通過遞歸地將嵌套數組轉換為由該方法定義的字符串來返回 object[ ] 的字符串表示。
參考資料:
《Java核心技術》
《Java程序設計語言》
《Java編程思想》