一篇喫透Java集合

集合

1.Collection接口

來看一張Collectiong的結構圖:
[外鏈圖片轉存失敗(img-qgQLaxne-1563270831936)(C:\Users\Laptop\AppData\Local\Temp\1563270794413.png)]

public interface Collection<E> extends Iterable<E>

在JDK1.5之前Iterable接口中的iterator()方法是直接在Collection接口中定義的

collection接口中定義了大量的方法:(摘自API)

    • boolean add(E e) 確保此集合包含指定的元素(可選操作)。
      boolean addAll(Collection<? extends E> c) 將指定集合中的所有元素添加到此集合(可選操作)。
      void clear() 從此集合中刪除所有元素(可選操作)。
      boolean contains(Object o) 如果此集合包含指定的元素,則返回 true
      boolean containsAll(Collection<?> c) 如果此集合包含指定 集合中的所有元素,則返回true。
      boolean equals(Object o) 將指定的對象與此集合進行比較以獲得相等性。
      int hashCode() 返回此集合的哈希碼值。
      boolean isEmpty() 如果此集合不包含元素,則返回 true
      Iterator<E> iterator() 返回此集合中的元素的迭代器。
      default Stream<E> parallelStream() 返回可能並行的 Stream與此集合作爲其來源。
      boolean remove(Object o) 從該集合中刪除指定元素的單個實例(如果存在)(可選操作)。
      boolean removeAll(Collection<?> c) 刪除指定集合中包含的所有此集合的元素(可選操作)。
      default boolean removeIf(Predicate<? super E> filter) 刪除滿足給定謂詞的此集合的所有元素。
      boolean retainAll(Collection<?> c) 僅保留此集合中包含在指定集合中的元素(可選操作)。
      int size() 返回此集合中的元素數。
      default Spliterator<E> spliterator() 創建一個Spliterator在這個集合中的元素。
      default Stream<E> stream() 返回以此集合作爲源的順序 Stream
      Object[] toArray() 返回一個包含此集合中所有元素的數組。
      <T> T[] toArray(T[] a) 返回包含此集合中所有元素的數組; 返回的數組的運行時類型是指定數組的運行時類型。

1.Collecttion接口的子接口-------List接口(允許保存重複數據)(⭐)

List接口在Collection接口中擴充了兩個重要方法(⭐)

E get(int index);
E set(int index, E element);

1 .List接口的子類-------------ArrayList

這些子類所使用的方法基本都是父接口的,所以我們更加關心它們的父接口所實現的方法;

ArrayList是一個針對於List接口的數組實現。 看下面簡單實例:

        //面向接口編程⭐
        //ArrayList有三個構造方法,第一個無參,初始化容量爲:10⭐
        // 第二個有參  int類型,表示初始化容量
        //第三個有參,爲一個Collection
        List<Integer> list = new ArrayList<>();   //面向接口編程的寫法
        list.add(2);
        list.add(3);
        list.add(1,10);
        List<Integer> list1 = new ArrayList<>();
        list1.add(9);
        list.addAll(list1);
        System.out.println(list);
        System.out.println("是否爲空:"+list.isEmpty());
        System.out.println(list.toArray());   //[Ljava.lang.Object;@154617c
        System.out.println("獲取下標爲一的元素:"+list.get(1));  //10,下標是從0開始的喲
        System.out.println("修改下標爲2的元素:"+list.set(2,40));  //返回的是被改前的值

[2, 10, 3, 3, 9]
是否爲空:false
[Ljava.lang.Object;@154617c
獲取下標爲一的元素:10
修改下標爲2的元素:3
[2, 10, 40, 3, 9]

2. List接口的子類-------------Vector (老版)

Vector在JDK1就提出來了,而ArrayList在JDK2提出來的;

其實Vector和ArrayList並沒有本質上的差別,只是在線程安全和性能方面有差距,現在基本都優先使用ArrayList;

Vector和ArrayList的區別
  • 歷史時間: ArrayList是從JDK1.2提供的,而Vector是從JDK1.0就提供了。
  • 處理形式:ArrayList是異步處理,性能更高;Vector是同步處理,性能較低。
  • 數據安全:ArrayList是非線程安全;Vector是線程安全。
  • 輸出形式:ArrayList支持Iterator、ListIterator、foreach;Vector支持Iterator、ListIterator、foreach

Enumeration。

在以後使用的時候優先考慮ArrayList,因爲其性能更高,實際開發時很多時候也是每個線程擁有自己獨立的集合資源。如果需要考慮同步也可以使用concurrent包提供的工具將ArrayList變爲線程安全的集合(瞭解)。

3.集合與簡單Java類

class Person {
    private String name;
    private int age;
    public Person() {

    }
    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 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(name, age);
    }
}
public class TestArrayList {
  
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        Person per = new Person();
        per.setAge(10);
        list.add(per);
        list.add(new Person("yy",18));
        list.add(new Person("yyg",20));
        list.add(new Person("ygz",25));
        System.out.println(list.contains(per));  //true
        //未在Person類中覆寫equals和hashCode方法時,下面輸出false
        System.out.println(list.contains(new Person("yy",18)));
    }
}

從上面代碼可以看到,自己寫的類型要覆寫equals和hashcode方法,使用contain方法才能行
Integer啥的JDK自帶的類型都已經覆寫了,不需要考慮;

總結:集合操作簡單java類時,對於remove()、contains()方法需要equals()方法支持。

4.List接口的子類-------------LinkedList (面試題:ArrayList、LinkedList區別)

面試題:請解釋ArrayList與LinkedList區別

  1. 觀察ArrayList源碼,可以發現ArrayList裏面存放的是一個數組,如果實例化此類對象時傳入了數組大小,則面保存的數組就會開闢一個定長的數組,但是後面再進行數據保存的時候發現數組個數不夠了會進行數組動態擴充。 所以在實際開發之中,使用ArrayList最好的做法就是設置初始化大小。
  2. LinkedList:是一個純粹的鏈表實現,與之前編寫的鏈表程序的實現基本一樣(人家性能高)。
    總結:ArrayList封裝的是數組;LinkedList封裝的是鏈表。ArrayList時間複雜度爲1,而LinkedList的複雜度爲n。

2.Collecttion接口的子接口-------Set集合接口(不允許保存重複數據)(⭐)

Set接口與List接口最大的不同在於Set接口中的內容是不允許重複的。同時需要注意的是,Set接口並沒有對
Collection接口進行擴充,而List對Collection進行了擴充。因此,在Set接口中沒有get()方法。
在Set子接口中有兩個常用子類:HashSet(無序存儲)、TreeSet(有序存儲)

1.Set接口的子類-------------HashSet(無序存儲)

        Set<String> set = new HashSet<>();
        set.add("C++");
        set.add("Java");
        set.add("PHP");
        set.add("Java");  //不會報錯,但size還是三
        System.out.println(set);
        System.out.println(set.size());   //  3
        System.out.println("是否存在Java:"+set.contains("Java"));   //true

輸出:
[Java, C++, PHP]
3
是否存在Java:true

2.Set接口的子類-------------TreeSet(有序存儲)

既然TreeSet子類可以進行排序,所以我們可以利用TreeSet實現數據的排列處理操作。此時要想進行排序實際上是針對於對象數組進行的排序處理,而如果要進行對象數組的排序,對象所在的類一定要實現Comparable接口並且覆寫compareTo()方法,只有通過此方法才能知道大小關係。
需要提醒的是如果現在是用Comparable接口進行大小關係匹配,所有屬性必須全部進行比較操作

class Person implements Comparable{
    private String name;
    private int age;
    public Person() {

    }
    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 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(name, age);
    }

    @Override
    public int compareTo(Object o) {
        if(this.age < ((Person)o).age) {
            return -1;
        }else if(this.age > ((Person)o).age) {
            return 1;
        }else {
            return this.name.compareTo(((Person)o).name) ;
        }
    }

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

    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        set.add(new Person("A",10));
        set.add(new Person("B",5));
        set.add(new Person("D",20));
        System.out.println(set);

    }
}

輸出:
[Person{name=‘D’, age=20}, Person{name=‘B’, age=5}, Person{name=‘A’, age=10}]

在實際使用之中,使用TreeSet過於麻煩了。項目開發之中,簡單java類是根據數據表設計得來的,如果一個類的
性很多,那麼比較起來就很麻煩了。所以我們一般使用的是HashSet。

3.Comparable接口和Comparator接口(實現對象的比較)

對於比較大小的概念,我們之前都是比較的一些已有的數據類型,比如Integer數據類型、String數據類型等等…
那麼有沒有思考它們是如何比較的呢,遇到我們自定義的對象又該如何進行比較呢?

我們可以查看Integer的源碼:

public final class Integer extends Number implements Comparable<Integer>

可以看到這裏實現了Comparable類,這是一個內部排序接口,實現這個結構的類必須覆寫compareTo這個方法,這個方法用來定義你要比較的規則,String等等其他類也都實現了Comparable接口,所以對於那些常用的基本數據類型,都可以用 .compareTo方法來直接進行比較;

1.Comparable接口(內部排序接口)是一個排序接口

源碼:

public interface Comparable<T> {
    public int compareTo(T o);
}

關於返回值:
可以看出compareTo方法返回一個int值,該值有三種返回值:

  1. 返回負數:表示當前對象小於比較對象
  2. 返回0:表示當前對象等於目標對象
  3. 返回正數:表示當前對象大於目標對象

若一個類實現了Comparable接口,就意味着“該類支持排序”。 即然實現Comparable接口的類支持排序,假設現在存在“實現Comparable接口的類的對象的List列表(或數組)”,則該List列表(或數組)可以通過Collections.sort(或
Arrays.sort)進行排序。此外,“實現Comparable接口的類的對象”可以用作“有序映射(如TreeMap)”中的鍵或“有序集合(TreeSet)”中的元素,而不需要指定比較器。

實現自己的內部比較器:

class Person implements Comparable<Person>{
    private String name;
    private int age;

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

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Person o) {
        return this.getAge() - o.getAge();
    }
}
public class CompareDemo {
    public static void main(String[] args) {
        Person per1 = new Person("a",12);
        Person per2 = new Person("b", 23);
        System.out.println(per1.compareTo(per2));   //返回一個小於0的數
    }
}

2.Comparator接口(外部排序接口)是一個比較器接口

實現兩個對象的比較還有一種方法,那就是實現外部比較

一般情況下,實現外部比較更爲常用 ,因爲實現內部比較只能實現一種方式的比較,對於上面所舉的例子,假如你又要以姓名爲標準來評定比較規則,那麼你得修改comparable裏面的代碼,這樣修改之後之前的比較結果就出現不同了;

實例:(外部比較可以實現多個比較標準)

class Person {
    private String name;
    private Integer 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 Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

}
//用年齡來判定大小
class PersonCompareAge implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getAge().compareTo(o2.getAge());   //Integer類型本來就實現了內部排序,這裏也可以寫成:o1.getAge - o2.getAge();
    }
}
//用姓名來比較大小
class PersonCompareName implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return    o1.getName().compareTo(o2.getName());

    }
}
public class CompareDemo {
    public static void main(String[] args) {
        Person per1 = new Person("Alice",12);
        Person per2 = new Person("Bob", 23);
        PersonCompareAge personCompareAge = new PersonCompareAge();
        PersonCompareName personCompareName = new PersonCompareName();
        System.out.println(personCompareAge.compare(per1, per2));  //-1
        System.out.println(personCompareName.compare(per1, per2));   //-1
    }
}

2.集合輸出(優先使用Iterator)

注意:

在集合遍歷的時候,不要修改元素 (⭐),比如在你用Iterator遍歷元素的時候,用remove方法刪除元素時,會產生ConcurrentModificationException異常(併發修改異常)

分析:
邏輯上講,迭代時可以添加元素,但是一旦開放這個功能,很有可能造成很多意想不到的情況。 比如你在迭代一個ArrayList,迭代器的工作方式是依次返回給你第0個元素,第1個元素,等等,假設當你迭代到第5個元素的時候,你突然在ArrayList的頭部插入了一個元素,使得你所有的元素都往後移動,於是你當前訪問的第5個元素就會被重複訪問。 java認爲在迭代過程中,容器應當保持不變。因此,java容器中通常保留了一個域稱爲modCount,每次你對容器修改,這個值就會加1。當你調用iterator方法時,返回的迭代器會記住當前的modCount,隨後迭代過程中會檢查這個值,一旦發現這個值發生變化,就說明你對容器做了修改,就會拋異常。

1.迭代輸出:Iterator接口 (⭐)

在JDK1.5之前,在Collection接口中就定義有iterator()方法,通過此方法可以取得Iterator接口的實例化對象;而在JDK1.5之後,將此方法提升爲Iterable接口中的方法 。無論你如何提升,只要Collection有這個方法,那麼List、Set也一定有此方法。
對於Iterator接口最初的設計裏面實際有三個抽象方法:

  1. 判斷是否有下一個元素: public boolean hasNext();
  2. 取得當前元素: public E next();
  3. 刪除元素: public default void remove(); 此方法從JDK1.8開始變爲default完整方法。
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("C++");
        list.add("PHP");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()) {
            String str = iterator.next();
            System.out.println(str);
        }

2.雙向迭代輸出:ListIterator接口

Iterator接口只能由前往後遍歷,而ListIterator可以雙向遍歷
注意:要從後往前遍歷,必須先從前往後遍歷⭐

listIterator是Iterator的子接口 ,此接口定義的方法有:

  1. 判斷是否有上一個元素:public boolean hasPrevious();
  2. 取得上一個元素:public E previous();

Iterator接口對象是由Collection接口支持的,但是ListIterator是由List接口支持的,List接口提供有如下方法:
取得ListIterator接口對象:public ListIterator listIterator();

用法:

        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("C++");
        list.add("PHP");
        list.add("JS");
        ListIterator<String> listIterator = list.listIterator();
        System.out.println("從前往後:");
        while(listIterator.hasNext()) {
            System.out.println(listIterator.next());
        }
        //從後往前的前提是必須先從前往後遍歷一遍⭐
        System.out.println("從後往前:");
        while(listIterator.hasPrevious()) {
            System.out.println(listIterator.previous());
        }

3.foreach輸出

這裏就不再多說了,jdk1.5後foreach就可以輸出數組和集合;

        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("C++");
        list.add("PHP");
        list.add("JS");
        ListIterator<String> listIterator = list.listIterator();
        for(String s:list) {
            System.out.println(s);
        }

3.Map接口

先看一下這個接口提供的比較重要的方法:

序號 方法 類型 描述
1 V put(K key,V value) 普通 向Map中添加數據
2 V get(Object key) 普通 根據key獲取對應的value,如果沒有返回null
3 Set KeySet() 普通 取得所有key信息,key不能重複
4 Collection values() 普通 取得所有value信息,可以重複
5 Set<Map.Entry<K, V>> entrySet() 普通 將Map集合變爲Set集合

Map本身是一個接口,要使用Map需要通過子類進行對象實例化。Map接口的常用子類有如下四個: HashMap、
Hashtable、TreeMap、ConcurrentHashMap。

1.Map的最常用子類-----------HashMap(⭐)

例碼:

        Map<Integer,String> map = new HashMap<>();
        map.put(1,"C++");
        map.put(2,"Java");
        map.put(3,"PHP");
        map.put(2,"JS");
        //由於key不能重複,所以size還是爲3
        System.out.println("當前size爲:"+map.size());   // 3
        System.out.println("key爲2的元素值爲:"+map.get(2));   //JS,以後面添加的爲準

        /**
         * 取得Map中的所有key信息 ⭐⭐
         */
        Set<Integer> set = map.keySet();
        Iterator iterator = set.iterator();
        while(iterator.hasNext()) {
            System.out.println(iterator.next());
        }

重點:HashMap源碼分析

先看看HashMap的定義:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

這裏有我寫的單獨的一篇博客來分析HashMap源碼:
https://blog.csdn.net/eternal_yangyun/article/details/89278871

2.HashTable

JDK1.0提供有三大主要類:Vector、Enumeration、Hashtable。Hashtable是最早實現這種二元偶對象數據結構,後期的設計也讓其與Vector一樣多實現了Map接口而已

HashTable用法與HashMap差異不大,所以這裏不再講解,這裏着重看一下他們的區別;

1.HashMap與HashTable的區別(⭐)

NO 區別 HashMap HashTable
1 版本 jdk1.2 jdk1.0
2 性能 異步處理,性能高 同步處理,性能低
3 安全性 非線程安全 線程安全
4 null操作 允許存放null,但只能有一個 key與value都不允許爲空,否則拋出空指針異常

3.HashMap的輸出

HashMap不直接支持迭代器Iterator的輸出,但可以將HashMap轉化爲Set集合再調用迭代器輸出;

        Map<Integer,String> map = new HashMap<>();
        map.put(1,"java");
        map.put(2,"c++");
        map.put(3,"PHP");
        System.out.println("Map的數據:"+map);
        System.out.println(map.get(1));

        System.out.println("-------------------");
        System.out.println("利用foreach輸出:");
        //1.key -->value
        //2.values(無法獲取到key)
        //3.entryset
        for(Integer k:map.keySet()) {
            System.out.printf("%d = %s \n",k,map.get(k));
        }

        System.out.println("-------------------");
        System.out.println("利用迭代器輸出:");
        Set<Map.Entry<Integer,String>> set = map.entrySet();
        Iterator<Map.Entry<Integer,String>> iterator = set.iterator();
        while(iterator.hasNext()) {
            Map.Entry<Integer,String> entry = iterator.next();
            Integer key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+" = "+value);

            //下面這將會錯誤輸出,因爲包含了兩個next調用了
//            Integer key = iterator.next().getKey();
//            String value = iterator.next().getValue();
//            System.out.println(key+"="+value);
        }

4.ConcurrentHashMap 子類

前面已經瞭解過HashTable和HashMap這兩個子類,他們最大的區別就是線程安全與否,需要線程安全就用HashTable,否則就使用HashMap,那麼我們爲什麼還要實現ConcurrentHashMap這個類呢?
前面也提到,HashTable實現線程安全的辦法是直接在需要同步的地方加上synchronized關鍵字來實現同步 ,而我們學過多線程的知道,synchronized同步的實現,在一個線程進行訪問的時候,其他線程只能夠等待這個線程,所以效率十分低下, 而且我們只需要在修改值得時候同步,在取值的時候並不需要同步,HashTable卻是在各種方法都加上了synchronized關鍵字;

5.自定義對象作爲Map集合的key

其實這個跟在List集合中用自定義對象作爲元素的道理是一樣的,這裏就不再多多敘述,總之就一句話,自定義的key需要覆寫equals方法和hashcode方法

4.棧和隊列

1.棧----Stack

棧是一種常見的數據結構,它最大的特點就是先進後出,在Java中,實現了這樣一種集合來實現這一數據結構

先來看它的定義:(Stack類在jdk1就有了)

public
class Stack<E> extends Vector<E>

可以看到Stack類繼承了Vector類,但值得我們注意的是,對棧的操作(push、pop、peek等)都是在Stack類裏直接實現的方法,而不是用的Vector裏面的方法,Vector只是用來作爲棧的存儲結構,因爲棧其實本來就是個數組;

簡單應用:

        Stack<Integer> stack = new Stack<>();
        stack.push(3);
        stack.push(4);
        System.out.println(stack.peek());  //4
        System.out.println(stack.pop());    //4

2.隊列----Queue

隊列的最大特點就是先進先出,他在我們實際開發中經常用到

先來看它的定義:

public interface Queue<E> extends Collection<E>

可以看到,隊列Queue是一個接口,上面的Stack是一個類,Queue接口繼承了Collection接口;
繼承Queue接口的有這樣一個接口Deque,而LinkedList類又實現了Deque這個接口,所以LinkedList間接實現了Queue ,所以實現一個隊列需要LinkedList來實現;

簡單應用:

        Queue<Integer> queue = new LinkedList<>();
        queue.add(3);
        queue.add(4);
        queue.add(5);
        System.out.println(queue.poll());  //3
        System.out.println(queue.poll());  //4
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章