學習Java集合?看我這一篇就夠了!

1、Java集合體系

  Java集合體系主要由兩個接口派生而出:Collection和Map。Collection及其實現類存儲的數據均是單列的,而Map及其實現類均是雙列的k-v鍵值對形式。Collection和Map的實現類很多,在不同的應用場景均有應用,學習Java集合一定要會對這個體系進行分類學習,纔能有效地掌握它們!

2、Collection體系

  Collection是一個接口,定義了一系列的集合操作方法,其實現類主要是重寫抽象方法和定義自己特定的方法。該體系的實現類都是直接或間接實現Collection的三個子接口:List、Queue、Set。我們常用的實現類主要有ArrayList、LinkedList、TreeSet、HashSet。
Collection體系繼承樹

2.1、List接口及其實現類

2.1.1、List

List接口的主要方法如下:

public void add(int index, E element) :將指定的元素,添加到該集合中的指定位置上。
public E get(int index) :返回集合中指定位置的元素。
public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element) :用指定元素替換集合中指定位置的元素,返回值的更新前的元素。

        //1.使用多態思想,創建List集合對象
        List<String> list = new ArrayList<>();
        //2.往尾部添加制定元素
        boolean a = list.add("小明");
        boolean b = list.add("小紅");
        boolean c = list.add("小剛");
        //3.輸出集合
        System.out.println("list = " + list);
        //4.刪除指定索引的元素,並返回被刪除的元素
        String removeElement = list.remove(2);
        System.out.println("removeElement = " + removeElement);
        //5.獲取指定位置的元素
        String getElement = list.get(1);
        System.out.println("getElement = " + getElement);
        //6.修改指定位置的元素
        String setElement = list.set(1, "小嚴");
        System.out.println("setElement = " + setElement);

2.1.2、ArrayList

  ArrayList 集合數據存儲的結構是數組(關於數組與鏈表的區別,請查詢數據結構)。元素增刪慢,查找快,由於日常開發中使用最多的功能爲查詢數據、遍歷數據,所以ArrayList是最常用的集合。想對ArrayList深入瞭解的,應該查看它的源碼。

  查看源碼可知,類中定義了private static final int DEFAULT_CAPACITY = 10;與private int size;其中DEFAULT_CAPACITY表示初始容量爲10,size表示當前列表的存儲的數據數量。類中有多個構造方法,若是指定容量,則使用指定的數字創建ArrayList,否則使用默認數量10爲初始化容量。

  其他的一些常用方法均可根據方法名與形參列表瞭解用法,若是想深入瞭解就查看源碼。

2.1.3、LinkedList

  LinkedList 集合數據存儲的結構是鏈表。元素增刪快,查找慢。
在這裏插入圖片描述
  實際開發中對一個集合元素的添加與刪除經常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。

public void addFirst(E e) :將指定元素插入此列表的開頭。
public void addLast(E e) :將指定元素添加到此列表的結尾。
public E getFirst() :返回此列表的第一個元素。
public E getLast() :返回此列表的最後一個元素。
public E removeFirst() :移除並返回此列表的第一個元素。
public E removeLast() :移除並返回此列表的最後一個元素。
public E pop() :從此列表所表示的堆棧處彈出一個元素。
public void push(E e) :將元素推入此列表所表示的堆棧。
public boolean isEmpty() :如果列表不包含元素,則返回true。

  由於LinkedList特殊的結構和方法,我們可以利用它來定義棧和隊列,具體實現代碼如下

public class Stack<E> {
    //定義一個linkedlist集合
    private LinkedList<E> list = new LinkedList<>();

    //入棧,鏈表的頭爲棧頂
    public void push(E e) {
        list.addFirst(e);
    }

    //出棧,並返回被出棧的元素
    public Object pop() {
        E e = list.removeFirst();
        return e;
    }

    //棧的大小
    public int size() {
        return list.size();
    }

    //查看棧頂元素
    public E peak(){
        return list.getFirst();
    }

	//集合是否爲空
    public boolean isEmpty(){
        int size = this.size();
        if (size == 0){
            return true;
        }
        return false;
    }

    public String toString(){
        return list.toString();
    }
}
public class Quene<E> {
    private LinkedList<E> list = new LinkedList<>();

    //入隊,鏈表的頭爲隊頭
    public void push(E e) {
        list.addLast(e);
    }

    //出隊
    public E pop() {
        return list.removeFirst();
    }

    //隊列的大小
    public int size() {
        return list.size();
    }

    //查看隊頭元素
    public E peak(){
        return list.getFirst();
    }

    //查看隊尾元素
    public E rear(){
        return list.getLast();
    }

    //隊列是否爲空
    public boolean isEmpty(){
        int size = this.size();
        if (size == 0){
            return true;
        }
        return false;
    }

    public String toString(){
        String s = list.toString();
        return s;
    }
}

2.2、Set接口及其實現類

2.2.1、Set

   Set 接口和 List 接口一樣,同樣繼承自 Collection 接口,它與 Collection 接口中的方法基本一致,並沒有對 Collection 接口進行功能上的擴充,只是比 Collection 接口更加嚴格了。與 List 接口不同的是,Set 接口中元素無序,並且都會以某種規則保證存入的元素不出現重複。Set 集合有多個子類,這裏我們介紹其中的 HashSet 、LinkedHashSet 這兩個集合。

2.2.2、HashSet

   java.util.HashSet 是 Set 接口的一個實現類,它所存儲的元素是不可重複的,並且元素都是無序的(即存取順序不一致)。查看源碼可知, HashSet 底層的實現其實是一個 HashMap 支持,只是將其中value固定了,把key作爲HashSet的值。HashSet 是根據對象的哈希算法(詳情查看數據結構)來確定元素在集合中的存儲位置,因此具有良好的存取和查找性能。保證元素唯一性 的方式依賴於:hashCode 與 equals 方法。要想了解HashSet,應該使用HashSet分別儲存Integer類型、String類型、自定義對象類型來研究HashSet。

public static void main(String[] args) {
        HashSet<Integer> set = new HashSet<Integer>();
        set.add(1);
        set.add(3);
        set.add(2);
        set.add(1);
        System.out.println("set = " + set);
    }

結果輸出爲set = [1, 2, 3],說明元素添加的無序且不重複的。

public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        String s1 = new String("aaa");
        String s2 = new String("aaa");
        System.out.println(s1 == s2);//false
        String s3 = new String("ccc");
        String s4 = new String("ddd");
        set.add(s1);
        set.add(s2);
        set.add(s3);
        set.add(s4);
        System.out.println("set = " + set);
    }

結果輸出爲set = [aaa, ccc, ddd]。由於String類型是引用類型,所以s1與s2的地址不同,但由於String重寫了equals方法,所以HashSet認爲二者是相同的。

public class Student {
    //未重寫equals和hashCode方法
    private int age;     //
    private String name;      //

    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

	public static void main(String[] args) {
        HashSet<Student> set = new HashSet<>();
        Student stu1 = new Student(23, "小明");
        Student stu2 = new Student(23, "小明");
        Student stu3 = new Student(24, "小紅");
        set.add(stu1);
        set.add(stu2);
        set.add(stu3);
        System.out.println("set = " + set);
    }
}

  定義的Student沒有重寫equals方法,所以HashSet判斷兩個student對象是否相同是根據地址值是否相同來判定的(Object類的原始equals方法)。所以結果輸出爲set = [Student{age=23, name=‘小明’}, Student{age=23, name=‘小明’}, Student{age=24, name=‘小紅’}]。

  利用IDEA生成equals和hashCode方法,得出的結果爲set = [Student{age=23, name=‘小明’}, Student{age=24, name=‘小紅’}]。

2.2.3、LinkedHashSet

  我們知道HashSet保證元素唯一,可是元素存放進去是沒有順序的,那麼我們要保證有序,怎麼辦呢? 在HashSet下面有一個子類LinkedHashSet ,它是鏈表和哈希表組合的一個數據存儲結構,保證了存取一致。其它的用法與HashSet幾乎一致。

2.2.4、TreeSet

  TreeSet底層是二叉樹,可以對對象元素進行排序,但是自定義類需要實現comparable接口,重寫comparaTo() 方法。TreeSet 可以保證對象元素的唯一性(並不是一定保證唯一性,需要根據重寫的compaaTo方法來確定)。下面演示存儲integer類型和自定義對象類型。

public static void main(String[] args) {
        TreeSet<Integer> set = new TreeSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        set.add(5);
        set.add(1);
        set.add(4);
        System.out.println("set = " + set);
    }

結果輸出set = [1, 2, 3, 4, 5]

public class Student implements Comparable{
    private int age;     //
    private String name;      //

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

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        Student student = (Student)o;//向下轉型
        return this.age - student.age;//按年齡排序
    }

    public static void main(String[] args) {
        TreeSet<Student> set = new TreeSet<>();
        set.add(new Student(23, "小明"));
        set.add(new Student(24, "小明"));
        set.add(new Student(25, "小明"));
        System.out.println("set = " + set);
    }
}

2.3、Queue

Queue是實現隊列的類,但現在使用很少了。重點還是在List和Set上

3、Map體系

3.1、Map

   Map用於保存具有映射關係的數據,因此Map集合裏保存着兩組值,一組值用於保存Map裏的key,一組保存Map裏的value,key和value都可以是任何引用類型的數據。Map的key不允許重複,即同一個Map對象的任何兩個key通過equals方法比較總會返回false。

  key和value之間存在着單向的一對一的關係,通過指定的key,能夠找到對應的value。Map中存在一個keySet()方法,能夠獲得所有的key,由於key不能重複且無序,所以本質上key集合就是一個set集合,而value看起來則像是一個list集合。
在這裏插入圖片描述

Map接口常用方法:

void clear():刪除該Map對象的所有k-v對
boolean containsKey(Object key):查詢該Map對象是否包含指定的Key
boolean containsValue(Object value):查詢該Map對象是否包含指定的value
Set entrySet():返回該Map對象包含的k-v對所組成的Set集合,Set集合元素都是Map.Entry(Entry是Map的內部類)對象
Object get(Object key):返回指定key對應的value;如果該Map對象不包含該key,則返回null
boolean isEmpty():查詢該Map對象是否爲空
Set keySet():返回該Map對象所有key組成的Set集合
Object put(Object key,Object value):向該Map對象添加一對k-v對
Object remove(Object key):刪除指定key所對應k-v對
boolean remove(Object key, Object value):刪除指定key和value所對應k-v對,成功刪除則返回True
int size():返回該Map對象的k-v對個數

3.2、HashMap和Hashtable

  HashMap和Hashtable都是Map接口的實現類,類似於ArrayList和Vector的關係。在實際開發中,HashMap使用率要遠遠大於Hashtable。從命名規則來看,可知道Hashtable是個古老的類(沒有遵從如今的命名規範)。

  想要在HashMap和Hashtable中存儲數據,用作key的對象必須實現hashCode()方法和equals()方法。

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:01
 */
public class Person {
    //使用Person類所爲key
    private String name;
    private int age;
    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return getAge() == person.getAge() &&
                Objects.equals(getName(), person.getName());
    }
    @Override
    public int hashCode() {
        return Objects.hash(getName(), getAge());
    }

    public static void main(String[] args) {
        HashMap<Person, String> hmap = new HashMap<>();
        hmap.put(new Person("小明", 18), "是個壞學生");
        hmap.put(new Person("小紅", 17), "是個好學生");
        hmap.put(new Person("小剛", 19), "是個學霸");
        Set<Map.Entry<Person, String>> entrySet = hmap.entrySet();
        for (Map.Entry<Person, String> entry : entrySet) {
            Person person = entry.getKey();
            String string = entry.getValue();
            System.out.println("person = " + person + "..." + "string = " + string);
        }
    }
}

3.3、LinkedHashMap

  LinkedHashMap是HashMap的一個子類,用法幾乎一樣,只是存儲順序的實現方法不同。LinkedHashMap的底層是使用哈希表與雙向鏈表來保存所有元素,所以LinkedHashMap的迭代順序和插入順序一致。

import java.security.Key;
        import java.util.LinkedHashMap;
        import java.util.Map;
        import java.util.Set;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:14
 */
public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap<String,Integer> lsmap = new LinkedHashMap();
        lsmap.put("小明", 18);
        lsmap.put("小紅", 19);
        lsmap.put("小剛", 20);
        Set<Map.Entry<String, Integer>> entrySet = lsmap.entrySet();
        for (Map.Entry<String, Integer> entry : entrySet) {
            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println("key = " + key + "..." + "value = " + value);
        }
    }
}

3.4、SortedMap和TreeMap

  SortedMap和TreeMap的原理與上面的SortedSet和TreeSet一樣,不瞭解的請回去翻翻看。

4、集合遍歷

4.1、List遍歷方式一

import java.util.ArrayList;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:38
 */
public class Demo2 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        //遍歷方式一:普通for循環
        for(int i = 0;i < list.size(); i++) {
            System.out.print(list.get(i) + " ");
        }
        System.out.println();
        //使用遍歷方式一進行刪除所有爲2的元素
        for(int i=0; i<list.size(); i++) {
            if (2 == list.get(i)) {
                list.remove(i);
            }
        }
        System.out.println("list.toString() = " + list.toString());
    }
}

結果爲:

1 2 2 3
list.toString() = [1, 2, 3]

  此時便出現了一個錯誤:有一個2未被刪除。原因是當刪除一個元素時,後面元素的索引就會被修改(index = index - 1),如刪除第一個2時候,此時下一個元素2的索引由原來的索引爲2變爲1,而此時for循環執行i++,對應索引的位置爲2而新的1索引上的值就不會在讀取了,所以結果爲【1,2,3】。要解決這個問題很容易,需要把 list.remove(i)改爲list.remove(i - -)。

4.2、List遍歷方式二

import java.util.ArrayList;
import java.util.Iterator;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:38
 */
public class Demo2 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        //遍歷方式二:使用迭代器
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
            System.out.println("next = " + next);
            if (next == 2){
                //務必使用Iterator的remove()
                iterator.remove();
            }
        }

        System.out.println("list.toString() = " + list.toString());
    }
}

  刪除元素過程中務必使用Iterator的remove(),若是使用集合的remove(),迭代器便會檢測到集合的內容發生改變,便會報出併發修改異常。

4.3、List遍歷方式三

import java.util.ArrayList;
import java.util.Iterator;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:38
 */
public class Demo2 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        //遍歷方式三:使用增強for循環
        for (Integer integer : list) {
            System.out.println("integer = " + integer);
        }
        System.out.println("list.toString() = " + list.toString());
    }
}

   使用增強for循環無法刪除元素,因爲集合刪除元素需要提供索引。

4.4、Map遍歷方式一

import java.util.LinkedHashMap;
import java.util.Set;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:14
 */
public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap<String,Integer> lsmap = new LinkedHashMap();
        lsmap.put("小明", 18);
        lsmap.put("小紅", 19);
        lsmap.put("小剛", 20);
        Set<String> keySet = lsmap.keySet();
        for (String key : keySet) {
            Integer integer = lsmap.get(key);
            System.out.println("integer = " + integer);
        }
    }
}

  使用Map集合的keySet()獲取所有的key,再通過key獲取value

4.5、Map遍歷方式二

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @author RuiMing Lin
 * @date 2020-03-05 16:01
 */
public class Person {
    //使用Person類所爲key
    private String name;
    private int age;
    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return getAge() == person.getAge() &&
                Objects.equals(getName(), person.getName());
    }
    @Override
    public int hashCode() {
        return Objects.hash(getName(), getAge());
    }

    public static void main(String[] args) {
        HashMap<Person, String> hmap = new HashMap<>();
        hmap.put(new Person("小明", 18), "是個壞學生");
        hmap.put(new Person("小紅", 17), "是個好學生");
        hmap.put(new Person("小剛", 19), "是個學霸");
        Set<Map.Entry<Person, String>> entrySet = hmap.entrySet();
        for (Map.Entry<Person, String> entry : entrySet) {
            Person person = entry.getKey();
            String string = entry.getValue();
            System.out.println("person = " + person + "..." + "string = " + string);
        }
    }
}

  通過獲取Map的內部類Entry獲取所有的k-v對,再通過Entry的getKey()和getValue()獲取key和value。

4.6、Map遍歷兩種方式的比較

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author RuiMing Lin
 * @date 2020-02-29 16:03
 */
public class Demo1 {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        for (int i = 0; i < 100000000; i++) {
            // 向map集合添加100w條數據
            map.put(i, "num" + i);
        }
        for (int i = 1; i <= 5; i++) {
            long time = entryTime(map);
            System.out.println("使用entrySet()第" + i + "次花費時間爲" + time + "ms");
        }
        for (int i = 1; i <= 5; i++) {
            long time = keyTime(map);
            System.out.println("使用keySet()第" + i + "次花費時間爲" + time + "ms");
        }

    }

    public static long entryTime(HashMap<Integer, String> map) {
        long start = System.currentTimeMillis();
        Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
        for (Map.Entry<Integer, String> entry : entrySet) {
            Integer key = entry.getKey();
            String value = entry.getValue();
            //System.out.println("key = " + key +"..." + "value = " + value);
        }
        long end = System.currentTimeMillis();
        return end - start;
    }

    public static long keyTime(HashMap<Integer, String> map) {
        long start = System.currentTimeMillis();
        Set<Integer> keySet = map.keySet();
        for (Integer key : keySet) {
            String value = map.get(key);//keySet()花費時間的原因,需要遍歷2次map集合
        }
        long end = System.currentTimeMillis();
        return end - start;
    }
}

結果輸出爲:

使用entrySet()第1次花費時間爲158ms
使用entrySet()第2次花費時間爲136ms
使用entrySet()第3次花費時間爲153ms
使用entrySet()第4次花費時間爲149ms
使用entrySet()第5次花費時間爲152ms
使用keySet()第1次花費時間爲200ms
使用keySet()第2次花費時間爲190ms
使用keySet()第3次花費時間爲257ms
使用keySet()第4次花費時間爲238ms
使用keySet()第5次花費時間爲175ms

分析:結果可以看出使用keySet()獲取值所需要的時間較長,原因在於它需要遍歷兩次map集合

5、總結

  Java的集合體系幾乎是面試必考的,同時也是Java基礎中最重要的一部分。正如前言所說,Java體系龐大,一定要學會分類總結。同時,也要學會尋找內在聯繫,比如set和map的關係。最後,要多思考什麼場景用什麼容器合適,到底是使用最簡單的數組就可以了?還是選擇可變長度的List?或是選擇不會有重複元素的Set?亦或是雙列的Map?

有錯誤的地方敬請指出,歡迎大家評論區或者私信交流!每日持續更新Java、Python、大數據技術,請大家多多關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章