更好閱讀體驗:Java 集合詳解 | 一篇文章搞定Java 三大集合
好看的皮囊像是一個個容器,有趣的靈魂像是容器裡的資料。接下來講解Java集合資料容器。
文章篇幅有點長,還請耐心閱讀。如只是為了解決某個疑問,可以閱讀目錄來查詢你所需的內容。
開門見山:「Java集合框架圖」
1.Iterator(迭代器)
迭代器的基本功能就是遍歷集合中的所有元素。
Iterable 介面組合了迭代器Iterator,通過方法:Iterator iterator(); 來獲取迭代器。
Collection 和 Map是 Java集合框架的根介面,Iterable 介面提供了迭代的功能。
1.1.集合和迭代器的關係
Collection 介面通過繼承Iterable 介面來獲得迭代功能;
Map 介面的迭代功能是嫁接Collection 介面的迭代功能,看Map 介面的抽象方法就知道了:具體怎麼用在Map 集合的常用方法中講到。
// 通過鍵來遍歷,返回值是Set集合,Map的key是Set集合,特性也和Set一樣的 Set<K> keySet(); // 通過值來遍歷,返回值是Collection Collection<V> values(); // 通過鍵值對來遍歷,返回值是Set集合 Set<Map.Entry<K, V>> entrySet();
2.集合和集合特點
List集合:元素按進入先後有序排序有序排序儲存,可以儲存重複元素
Set集合:不可以儲存重複元素
Map集合:Key值不可以重複(key等於Set集合),Value值可以重複
資料結構:陣列、雙向連結串列、雜湊表、二叉樹;關於資料底層儲存結構,決定了集合絕大部分的特性,如:查詢和增刪快慢,是否有序,是否可以重複。
執行緒安全:就是集合中的方法使用了同步鎖關鍵字:synchronized;有鎖的效率低。
3.怎麼選擇集合
根據需求和集合特點來選擇所需的集合型別。
「集合的具體使用」?
4.List 集合
4.1.單集合操作
List<String> list = new ArrayList<String>(); // 1.1 新增元素 list.add("測試1"); list.add("測試2"); System.out.println("集合大小="+list.size()); // 1.2 指定下標新增,index 範圍:[0,list.size()] list.add(1,"測試3"); for (int i=0;i<list.size();i++){ // 2.1 獲取元素 String str = list.get(i); System.out.println("新增="+str); } // 3.1 修改元素,index 範圍:[0,list.size()) list.set(2,"測試?"); for (String str : list){ System.out.println("修改"+str); } // 4.1 刪除第3個元素,index 範圍:[0,list.size()) // list.remove(2); // 4.2 刪除指定物件值 list.remove(new String("測試?")); System.out.println("刪除="+list);//AbstractCollection重寫了toString()
4.2.集合間操作
//集合間操作 LinkedList<String> linkedList = new LinkedList<String>(); // 1 新增集合所有元素(合集),如果集合間無相同元素,則為並集 linkedList.addAll(list); linkedList.addAll(2,list); Iterator<String> listIterator = linkedList.listIterator(); while (listIterator.hasNext()){ String next = listIterator.next(); System.out.println("++"+next); } // 2 獲取兩者都有的元素(交集) list.add("存在和價值"); list.retainAll(linkedList); list.forEach(str -> { System.out.println("交集="+str); }); // 3 移除指定集合元素(差集) list.add("存在和價值"); list.removeAll(linkedList); for (String str : list){ System.out.println("差集="+str); } // 4 並集,如果集合見存在相同元素,則去除相同在合併就可以得到並集 list.removeAll(linkedList); list.addAll(linkedList); for (int i=0;i<list.size();i++){ // 2.1 獲取元素 String str = list.get(i); System.out.println("並集="+str); }
list 集合的「四種遍歷方式」都在程式碼裡面了?
還有很多其他方法,ArrayList 和 LinkedList 還有一些特有的方法,可自行學習。
5.Set 集合
Set 集合不能儲存重複元素。通過equals() 和 hashCode() 都相等來決定元素是否相等(重複元素)。
5.1.實現equals() 和hashCode()方法
AbstractSet 抽象類預設實現equals() 和 hashCode(),實現類會呼叫儲存物件的hashCode() 方法來得到hashCode值,然後根據該hashCode值決定該物件在的儲存位置;通過equal() 方法來判斷兩個物件的地址是否相等、型別是否相等、是否包含在集合內。所以兩個物件equals()相等,hashcode()一定相等;hashcode()不相等,則equals()肯定不相等就不用再判斷了提高效率。
重寫equals() 和hashCode()方法
hashCode 值的計算方式
一個實體類有多個成員變數,為了避免偶然出現雜湊值相等問題,常常將多列的雜湊值成語質數再相加:hashCode=(int)var*31+var.hashCode()*31;
「例子」
public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @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() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode())*prime; result = prime * result + age*prime; return result; } }
如果覺得這樣生成hashCode麻煩,可以用Objects型別的hash方法生成
@Override public int hashCode() { return Objects.hash(name,age); }
在使用HashSet 或 LinkedHashSet 集合時,實現hashCode和equals 方法即可。但在使用TreeSet 的時候,實體類還需要重寫比較器來實現定製排序。
5.2.比較器的實現
「比較器實現方式彙總」
- 實體類實現Comparable介面,重寫compareTo方法;
- 專門寫一個實現類實現Comparator介面,重寫compare方法;建立TreeSet的時候傳入該實體類的物件;
- 匿名實現Comparator;
- lambda實現Comparator;
第1種方式實現Comparable介面,重寫compareTo方法;例如Person類通過age 和 name 欄位進行排序
// 1.實現Comparable介面,重寫compareTo方法 @Override public int compareTo(Object obj) { if (!(obj instanceof Person)) throw new RuntimeException("不是正確物件"); Person p = (Person) obj; if (p.age > this.age) return -1; if (p.age == this.age) { return this.name.compareTo(p.name); } return 1; }
比較器有多種實現方式,這只是其中一種比較簡單的。
第2、3、4方式核心程式碼都一樣,只是寫法不同
// 2.單獨類實現 public class ComparatorImpl implements Comparator { @Override public int compare(Object o1, Object o2) { if (o1==o2) return 0; if (!(o1 instanceof Person) || !(o2 instanceof Person)) throw new RuntimeException("不是Person 型別"); Person p1=(Person)o1; Person p2=(Person)o2; int num=p1.getName().compareTo(p2.getName()); if (num==0){ return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge())); } return num; } } // 怎麼使用 Set<Person> treeSet = new TreeSet<Person>(new ComparatorImpl()); // 3.匿名方式 TreeSet ts = new TreeSet(new Comparator() { @Override public int compare(Object o1, Object o2) {內容一樣} }); // 4.lambda 方式 (匿名方式的簡寫) TreeSet ts = new TreeSet((o1, o2)->{內容一樣} });
1和2的實現方式的特點是寫死了,所有使用的定製排序都是一樣的,不能對不同的物件定製不同的排序規則,需要對每個物件定製不同排序則使用3或4的實現方式。
5.3.Set 集合通用方法
5.4.TreeSet 特有方法
- Comparator comparator():用於獲取定製排序Comparator 物件。如果實現了自定義定製排序,則該方法返回Comparator物件;反正返回null。
- Object first():返回集合中的第一個元素。
- Object last():返回集合中的最後一個元素。
- Object lower(Object e):返回集合中位於指定元素之前的元素(即小於指定元素的最大元素,參考元素不需要是TreeSet集合裡的元素)。
- Object higher(Object e):返回集合中位於指定元素之後的元素(即大於指定元素的最小元素,參考元素不需要是TreeSet集合裡的元素)。
- SortedSet subSet(Object fromElement, ObjecttoElement):返回此Set的子集合,範圍從fromElement(包含)到toElement(不包含)。
- SortedSet headSet(Object toElement):返回此Set的子集,由小於toElement的元素組成。
- SortedSet tailSet(Object fromElement) :返回此Set 的子集,由大於或等於fromElement的元素組成。
6.1.Map 集合
「建議:要想掌握好Map集合得先學會Collection集合」
Map集合儲存鍵值對資料,Key 擁有Set集合特點,Value 擁有著Collection的特點,Map 像是Set+Collection的集合。所以在使用map儲存、獲取物件時,用作Key的物件必須實現equals 和 hashCode 方法。不重寫這兩個方法就會出現,相同的Key的資料也可以儲存到Map集合中,就會出現**「key不唯一」**,如:當你使用map.get() 獲取到的是“null”。
6.2.為什麼必須重寫equals 和 hashCode 方法
「必須舉個例子說明,因為常常有人在這掉入陷阱」
public class Student { private String name; private int age; public Student(){} public Student(String name,int age){ this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
測試是否會儲存相同的key
// 測試 public class TestMap { public static void main(String[] args) { Student student1 = new Student("張三", 18); Student student2 = new Student("我是唯一", 28); Student student3 = new Student("張三", 18); HashMap<Student, Student> studentHashMap = new HashMap<>(); studentHashMap.put(student1,student1); studentHashMap.put(student2,student2); studentHashMap.put(student3,student3); Iterator<Map.Entry<Student, Student>> iterator = studentHashMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Student, Student> next = iterator.next(); System.out.println("key="+next.getKey()+"|value="+next.getValue()); } } }
結果:相同的Key也儲存成功了
測試通過key來獲取value:關鍵來了最容易理解錯的一步
錯誤示範:使用前面定義的物件引用來獲取
// 錯誤示範:使用前面定義的物件引用來獲取 Student student = studentHashMap.get(student3); System.out.println(student);
錯誤示範結果:在存有相同的key的map集合中,拿到了value值
正確示範:使用新定義的物件引用來獲取(定義的物件欄位值是相同的)
// 正確示範:使用新定義的物件引用來獲取:定義的物件欄位值是相同的 Student student4 = new Student("張三", 18); Student student = studentHashMap.get(student4); System.out.println(student);
測試結果
實現equals 和 hashCode方法後再來正確示範
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name,age); }
實現後的測試結果
之所以會測不出來null的問題是因為在同一個類中測試往往喜歡使用前面的儲存時的引用來操作map.get,那肯定是測不出來的,因為new的所有物件的地址肯定都是唯一的,所以用唯一的地址去獲取,那肯定是可以get到的。正確的是新建一個內容相同,地址不同的引用去獲取。
當使用TreeMap 時,作為key的物件還需要定製排序,這個操作和Set集合的定製排序一樣,具體看Set 集合定製排序。
6.3.Map集合常用方法
HashMap<String, String> hashMap = new HashMap<>(); // 新增 hashMap.put("test1","test1-value"); hashMap.put("test2","test2-value"); // map和其他集合都重寫了toString System.out.println(hashMap); System.out.println("集合大小"+hashMap.size()); System.out.println("集合大小是否=0:"+hashMap.isEmpty()); System.out.println("是否包含key=test1的元素:"+hashMap.containsKey("test1")); System.out.println("是否包含value=test1的元素:"+hashMap.containsValue("test1")); System.out.println("獲取Key=test1的值:"+hashMap.get("test1")); // 移除元素 hashMap.remove("test1"); System.out.println(hashMap); // 集合合併 HashMap<String, String> hashMap1 = new HashMap<>(); hashMap1.put("test1","test1"); // 相同key的會覆蓋原來的值 hashMap1.put("hashMap1","hashMap1-value"); hashMap.putAll(hashMap1); System.out.println(hashMap);
輸出結果
通過迭代器來遍歷:「key 集合」、「value集合」和「key-value集合(entry)」
看程式碼就知道key集合是Set集合,value集合是Collection集合,entry集合是Set集合
HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("testKey1","testValue1"); hashMap.put("testKey2","testValue2"); // 1.通過 鍵 來遍歷,返回值是Set集合,Map的key是Set集合,特性也和Set一樣的 //獲取集合中所有key的Set集合 Set<String> keySet = hashMap.keySet(); //通過Set集合的迭代器進行遍歷 (為了好理解分開兩步來獲取,開發時可以一步到位) Iterator<String> iteratorKey = keySet.iterator(); while (iteratorKey.hasNext()) { String next = iteratorKey.next(); System.out.println("key="+next); } // 2.通過 值 來遍歷,返回值是Collection // 獲取集合中所有value的集合 Collection<String> collection = hashMap.values(); //通過集合的迭代器進行遍歷 Iterator<String> iteratorValue = collection.iterator(); while (iteratorValue.hasNext()){ String next = iteratorValue.next(); System.out.println("value="+next); } // 3.通過 鍵值對 來遍歷,返回值是Set集合 //獲取Map元素物件的Set集合 Set<Map.Entry<String, String>> entrySet = hashMap.entrySet(); //通過Set集合的迭代器進行遍歷 Iterator<Map.Entry<String, String>> iteratorEntry = entrySet.iterator(); while (iteratorEntry.hasNext()){ //單個map元素物件 Map.Entry<String, String> entry = iteratorEntry.next(); System.out.println("key="+entry.getKey()+",value="+entry.getValue()); }
輸出結果
6.4.Map集合的三種遍歷方式
前面講了三種方式,前兩種只是準對key 和 value的遍歷。
在這裡彙總的都是針對整個Map的遍歷方式:「通用迭代器方式」,「forEach」和「forEach方法」。
// 通用遍歷方式 Iterator<Map.Entry<String, String>> iteratorEntry = hashMap.entrySet().iterator(); while (iteratorEntry.hasNext()){ Map.Entry<String, String> entry = iteratorEntry.next(); System.out.println("key="+entry.getKey()+",value="+entry.getValue()); } // forEach for (Map.Entry<String, String> entry : hashMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println("key=" + key + ",value=" + value); } // 呼叫forEach方法,lambda語法 hashMap.forEach((key,value)->{ System.out.println("key="+key+",value="+value); });
HashMap、LinkedHashMap、TressMap和ConcurrentHashMap……那麼多的方法,只能自己去學習了。
7.Collections 集合工具類
Collections 是用於操作List、Set和Map等集合的工具類。該工具類提供了大量方法(功能)對集合元素進行排序、查詢、修改等操作,對集合物件實現同步控制等方法。
7.1.排序問題
常用於對List集合元素排序的方法有
- 自然排序:void sort(List list);
- 比較器定製排序:void sort(List list,Comparator c);
- 反轉排序:void reverse(List list);
- 隨機排序:void shuffle(List list);
// 0.資料準備 List<String> arrayList = new ArrayList<>(); arrayList.add("z"); arrayList.add("a"); arrayList.add("c"); arrayList.add("b"); System.out.println("插入順序:"+arrayList); // 1.自然排序 Collections.sort(arrayList); System.out.println("自然排序:"+arrayList); // 2.比較器定製排序 Collections.sort(arrayList, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); } }); System.out.println("定製排序:"+arrayList); // 3.反轉排序 Collections.reverse(arrayList); System.out.println("反轉排序:"+arrayList); // 4.隨機排序 Collections.shuffle(arrayList); System.out.println("隨機排序:"+arrayList);
7.2.解決執行緒安全問題
ArrayList 和 Vector 的主要區別是執行緒安全,雖然Vector可以解決執行緒安全,但不建議使用。可以通過集合工具類(Collections類)來解決ArrayList 併發訪問執行緒安全問題。對於Set和Map集合執行緒安全問題,也可以通過Collections類來解決。
// 建立物件時使用 List<String> synchronizedList = Collections.synchronizedList(new ArrayList<String>());
Collections 工具類還有查詢操作、修改操作等等,更多知識自行學習。
原創不易,還請三聯。更多優質文章?