Java是如何存儲元素的(3)—Map集合存儲數據原理(爲什麼HashMap集合的key部分的特點是無序,不可重複)

(一)Map集合結構圖

(二)Map集合中常用的方法

 (1)往Map集合中添加元素

  • put(K key, V value)
  • 將指定的值與該映射中的指定鍵相關聯(可選操作)。

這個和Collection不一樣,Collection用的是add();

(2)清空

  • void

 

  • clear()
  • 從該地圖中刪除所有的映射(可選操作)。

 Collection中,也有這個方法。

(3)判斷包含key和value

  • boolean
  • containsKey(Object key)
  • 如果此映射包含指定鍵的映射,則返回 true 。

 

  • boolean
  • containsValue(Object value)
  • 如果此地圖將一個或多個鍵映射到指定的值,則返回 true 。

在Collection中是通過 Cotains,這和底層原理有關係了,因爲Map是key-value存儲的,Collection只是存儲一個value。

(4)通過Key獲取value值

  • get(Object key)
  • 返回到指定鍵所映射的值,或 null如果此映射包含該鍵的映射。

通過key值獲取value值,這個和Colleciton中的List很像,然後Set中是沒有下標的,所以Set沒有。

(5)判斷集合是否包含元素

  • boolean
  • isEmpty()
  • 如果此地圖不包含鍵值映射,則返回 true 。

這個和Collection的一樣。

(6) 獲得集合中的元素數量

 
  • int
  • size()
  • 返回此地圖中鍵值映射的數量。

這個和Colleciton中的一樣。

(7) 刪除集合中的某個元素

  • remove(Object key)
  • 如果存在(從可選的操作),從該地圖中刪除一個鍵的映射。

這個和Colleciton中的一模一樣。

(8)獲取所有的value

 Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"趙曉東");
        m1.put(2,"趙靜瑤");
        m1.put(3,"趙景康");
        Collection<String> c1 = m1.values();
        for(String s : c1){
            System.out.println(s);
        }
趙曉東
趙靜瑤
趙景康

(9) 獲取Map集合中的所有Set集合,這個也很重要,可以循環Map

  • keySet()
  • 返回此地圖中包含的鍵的Set視圖。

 (10)將Map集合轉換爲set集合。這個很重要,可以循環Map

  • entrySet()
  • 返回此地圖中包含的映射的Set視圖。

 

(三)Map集合循環

Map集合的循環一共有兩種方式

(1) 獲取所有的key,通過遍歷Key,來遍歷value。這時候就需要了KeySet()方法進行轉換了。

        Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"老師");
        m1.put(2,"校長");
        m1.put(3,"學生");
        /*通過將Map方法獲取Set*/
        Set<Integer> key =m1.keySet();
        /*轉換成Set方法後,通過獲取iterrator進行遍歷*/
        Iterator<Integer> I1=key.iterator();
        while (I1.hasNext()){
           Integer NumberKey = I1.next();
           String s1=m1.get(NumberKey);
            System.out.println(s1);
        }
老師
校長
學生

同時也可以通過增強for循環來獲取Key

 for(Integer key1 : key){
            System.out.println(m1.get(key1));
        }

(2)把Map集合直接全部轉化成Set集合。

這個時候需要調用的是Map的entrySet()方法了。

        Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"老師");
        m1.put(2,"校長");
        m1.put(3,"學生");
        /*將Map對象轉換爲Set*/
        Set<Map.Entry<Integer,String>> set = m1.entrySet();
        /*轉換爲Set之後,再調用Iterator*/
        Iterator<Map.Entry<Integer,String>> mset=set.iterator();
        /*接下來就是遍歷*/
        while (mset.hasNext()){
            Map.Entry<Integer,String> m2=mset.next();
            Integer I1 =m2.getKey();
            String S1 = m2.getValue();
            System.out.println(I1+"--->"+S1);
        }
1--->老師
2--->校長
3--->學生

我們來看一下轉換過程

其實這個時候,Map.Entry<Integer,String>爲一個結點。 這個結點就是Set的形式,然後再通過set的getkey和getvalue獲取元素即可。其實Map.Entry<k,v>爲節點的類型。

當然這個也能使用增強for循環。

for(Map.Entry<Integer,String > p1 :set){
            System.out.println(p1.getKey()+"---->"+p1.getValue());
        }

(四)HashMap(哈希表)

  (1)HashMap集合底層是哈希表/散列表的數據結構

(2)哈希表是一個怎樣的數據結構?

哈希表是一個數組和單向鏈表的結合體。數組:在查詢方面效率很高,在隨機增刪方面效率很低。單向鏈表:在隨機增刪方面效率很高,在查詢方面效率很低。哈希表將以上的兩種數據結構融合在一起,充分發揮他們的各自優點

(3)HashMap集合底層源代碼(哈希表在代碼的原理實現)

	Public class HashMap{
		//hashMap底層實際上就是一個數組(一維數組)
		Node<k,v> table;
		//靜態內部類
		static class Node<K,V> implements Map.Entry<K,V>{
		finalinthash;	//哈希值(哈希值是key()和hashCode()方法執行結果,hash值通過哈希函數/算法)
		finalKkey;	存儲到Map集合中的那個Key
		Vvalue;	存儲到map集合中的那個value
		Node<K,V> next;	下一個節點的內存地址
		
		Node(inthash,Kkey,Vvalue,Node<K,V>next){
		this.hash=hash;
		this.key=key;
		this.value=value;
		this.next=next;
		}
		
	}

從以上的源代碼我們可以看出,HashMap裏面有Node數組,數組裏面有hash,key,value,next。其中hash是哈希值(哈希值是key的hashCode()方法的執行結果,hash值通過哈希函數/算法。可以轉換存儲成數組的下標),key,value就是存儲的鍵值對,next就是下一個節點的內存地址。

綜上所述:哈希表/散列表,一維數組,這個數組中每一個元素是一個單向鏈表(數組和鏈表的結合體)。

很明顯的看出來,裏面是結點的方式。所以,它到底是怎麼樣子的呢?

(4)map.put(k,v)和v=map.get(k)的原理

這個是特別重要的,也就是說,存是如何存的,取是如何取的。

(I)先將k,v封裝到Node對象當中

(II)底層會調用k的hashCode()方法得到hash值,然後通過哈希函數/哈希算法,將hash值轉換成數組的下標,下標位置上如果沒有任何元素,就把Node添加到這個位置上,如果說下標對應的位置上有鏈表,此時就會拿着k和鏈表上每一個節點的k進行equals,如果所有的equals方法都返回false,那麼這個新節點將會被添加到鏈表的末尾。如果其中有一個equals返回了true,那麼會將這個節點的value將會被覆蓋。(hashCode是Object自帶的,如果不重寫,則返回地址。)其實這就是數據結構裏面學過的。

v=map.get(k)實現原理:

(III)先調用k的hashCode()方法得出哈希值,通過哈希算法轉換成數組下標,通過數組下標快速定位到某個位置上,如果這個位置上什麼也沒有,返回Null,如果這個位置上有單向鏈表,那麼會拿着參數k和單向鏈表上的每個結點中的k進行equals,如果所有equals方法返回false,那麼get方法返回null,只有其中有一個結點k和參數k的equals返回true,那麼此時這個節點的value就是我們要找的value。get方法最終返回這個要找的value。

(a)爲什麼哈希表的隨機增刪,以及查詢效率都很高?

因爲增刪是在鏈表上完成,查詢也不需要掃描,只需要部分掃描。

所以說HashMap上面的k會先後調用兩個方法,一個是hashCode()另一個是equals,所以這兩個都需要重寫。equals默認是比較內存地址,但是我們要比較的是裏面的內容。

這裏可以聯想爲什麼HashMap集合的key部分的特點是無序,不可重複?

無序:因爲調用了hashCode()方法,通過哈希算法得出來的值不一定是連續的,可能是隨便的到哪一個單向鏈表上的,學過數據結構的人都知道,計算位置的方法會有很多,其中有一個%取餘數。所以說,不可能是連續的,那隻能是無序的。

不可重複:不可重複是因爲,k值調用了equals方法。這個方法會對內容進行比較,如果內容相同的話,會覆蓋掉value,所以說HashMap存儲的數據時不可重複的。

放在HashMap集合key部分的元素其實就是放在了HashSet集合中了,所以HashSet集合中的元素也要需要重寫HashCode()和equals。

所以,同一個單向鏈表上的所有結點的Hash是相同的。同一個鏈表上一個key,equals一定是false。無序不可重複。

(b)如果HashCode()重寫的時候,寫成一個固定值,那麼就會成爲單向鏈表。就會發揮不出功能。這種情況稱爲散列分佈不均勻。

(c)如何HashCode()的返回值都不一樣,行嗎?不行那麼就稱爲一維數組了。就沒有鏈表的概念了,也是散列分佈不均勻。

所以說HashCode()重寫需要有一定的原理。

(5)同時重寫HashCode()和equals()

我們上面說了,爲什麼要重寫HashCode()和equals(),那麼equals方法有可能調用,也有可能不調用。

拿put(k,v)舉例,什麼時候equals不會調用?

哈希值經過哈希算法轉換爲數組下標,數組下標位置上如果是null,equals不需要執行。

拿get(k)舉例,什麼時候equals不會調用?

K.hashCode()方法返回哈希值,哈希值經過哈希算法轉換數組下標。

數組下標位置上如果是Null,equals不需要執行。

我們重寫了equals,但是沒有重寫hashCode()

        Student s1 = new Student("zhangsan");
        Student s2 = new Student("zhangsan");
        /*查看equals和hashCode*/
        /*沒有重寫equals方法之前*/
//        System.out.println(s1.equals(s2));//false
        /*重寫了equals方法之後*/
        System.out.println(s1.equals(s2));//true

        System.out.println(s1.hashCode());//-1432604525
        System.out.println(s2.hashCode());//-795136960

        Set<Student> students = new HashSet<>();
        students.add(s1);
        students.add(s2);
        System.out.println(students.size());
true
460141958
1163157884
2

按理說student.size()應該是1,但是結果是2,現在我們重寫hashCode方法,再進行測試。

true
-1432604525
-1432604525
1

student.size()的結果就變成了1.

所以說最終結論:放在HashMap集合中的key部分的,以及放在HashSet集合中的元素,需要同時重寫HashCode方法和equals方法。

但是在JDK8之後,如果哈希表單向鏈表中元素超過8個,單向鏈表這種結構會變成紅黑樹數據結構,當紅黑樹上的節點數量小於6時,會重新把紅黑樹變成單向鏈表數據結構。

(五)HashMap和HashTable的區別

(1)HashMap中可以存放Null,而HashTable中不可以。

HashMap存放Null值

    public static void main(String[] args) {
        Map m1 = new HashMap();
        m1.put(null,null);
        System.out.println(m1.size());
    }
1

HashTable存放 

        Map m2 = new Hashtable();
        m2.put(null,null);
Exception in thread "main" java.lang.NullPointerException
	at java.util.Hashtable.put(Hashtable.java:459)
	at com.javase.Map.MapTable.main(MapTable.java:13)

(六)Properties

Properties是一個Map集合,繼承了Hashtable,properties的key和value都是String類型。Properties被稱爲屬性類對象。並且是線程安全的。

         Properties pro = new Properties();
        /*需要掌握Properties的兩個方法,一個存,一個取*/
        pro.setProperty("url","zcdee");
        pro.setProperty("gdsa","dsarf");
        pro.setProperty("username","root");
        /*通過key值進行取*/
        String s1 = pro.getProperty("url");
        String s2 = pro.getProperty("username");

        System.out.println(s1);
        System.out.println(s2);
zcdee
root

(七)TreeSet

(1)TreeSet集合底層實際上是一個TreeMap

(2)TreeMap集合底層是一個二叉樹

(3)放到TreeSet集合中的元素,等同於放到了TreeMap集合Key部分了。

(4)TreeSet集合中的元素:無序不可重複,但是可以按照元素的大小順序自動排序。稱爲可排序集合。

(5)那麼什麼時候可能用到TreeSet呢?

編寫程序從數據庫當中取出數據,在頁面展示用戶信息的時候按照生日升序或者降序。這個時候可以使用TreeSet集合,因爲TreeSet集合放進去,拿出來就是有序的。

        TreeSet<Integer> t1 = new TreeSet<>();
        t1.add(10000);
        t1.add(100);
        t1.add(1000);
        for(Integer i1 : t1){
            System.out.println(i1);
        }

運行結果爲 

100
1000
10000

 (6)對自定義的類型來說,TreeSet可以排序嗎?

不可以,以下程序中對於Person類型來說,無法排序,

爲什麼不可以排序呢,因爲美音指定Customer對象之家你的比較規則。誰大誰小並沒有說明。當然出現這個異常的原因是因爲沒有實現Comparable接口。

(7)那麼怎麼進行排序呢?

(a)讓Customer實現這個Comparable接口

(b)重寫方法

之所以String,Integer能進行排序,是因爲他們實現了Comparable接口和方法。 

 所以說CompareTo方法的返回值很重要:

返回0表示相同,value會覆蓋,返回>0,會繼續在右子樹上找。返回<0,會繼續在左子樹上找。

(八)自平衡二叉樹

(1)遵循左小右大原則存放。

(2)遍歷二叉樹的時候三種方式:

前序遍歷:根左右。 中序遍歷:左根右 。 後序遍歷:左右根。

(3)TreeSet集合/TreeMap集合採用的是:中序遍歷方式Iterator迭代器採用的是中序遍歷方式,左根右。

(九)TreeSet的第二種比較方式

public static void main(String[] args) {
        /*創建TreeSet集合的時候,需要使用這個比較器*/
        TreeSet<WuGui> Wuguis = new TreeSet<>(new WuGuiComparator());
        WuGui w1 = new WuGui(800);
        WuGui w2 =new WuGui(521);
        WuGui w3 = new WuGui(888);
        Wuguis.add(w1);
        Wuguis.add(w2);
        Wuguis.add(w3);

        for(WuGui s : Wuguis){
            System.out.println(s);
        }

    }
}
class WuGui{
    private  int age;

    public WuGui(int age) {
        this.age = age;
    }

    public WuGui() {
    }

    @Override
    public String toString() {
        return "WuGui{" +
                "age=" + age +
                '}';
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
class WuGuiComparator implements Comparator<WuGui>{
    @Override
    public int compare(WuGui o1, WuGui o2) {
        return o1.getAge() - o2.getAge();
    }
WuGui{age=521}
WuGui{age=800}
WuGui{age=888}

 

最終結論:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括兩種方式:

第一種:放在集合中的元素實現java.lang.Comparable接口。

第二種:在構造TreeSet或者TreeMap集合的時候給它傳一個比較器對象。

Comparable和Comparator怎麼選擇呢?

當比較規則不會發生改變的時候,或者說當比較規則只有1個的時候,建議實現Comparable接口。

如果比較規則有多個,並且需要多個比較規則之間頻繁切換,建議使用Comparator接口。

(九)Collections工具類

(1)Collections實現對List進行排序

編程線程安全的和排序

        /*ArrayList集合不是線程安全的*/
        List<String > list1 = new ArrayList<>();

        /*變成線程安全的*/
        Collections.synchronizedList(list1);

        /*排序*/
        list1.add("abf");
        list1.add("abx");
        list1.add("abc");
        list1.add("abe");

        Collections.sort(list1);
        for (String l1 : list1){
            System.out.println(l1);
        }
abc
abe
abf
abx

當我們往list裏面傳入WuGui,並且沒有實現comparable方法的時候

就會報錯,所以使用Collections的時候必須要對Wugui2進行實現Comparable接口

 

class WuGui2 implements Comparable<WuGui2>{
    int age;

    public WuGui2(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(WuGui2 o) {
        return this.age -o.age;
    }

    @Override
    public String toString() {
        return "WuGui2{" +
                "age=" + age +
                '}';
    }

        List<WuGui2> WuGuis = new ArrayList<>();
        WuGuis.add(new WuGui2(1000));
        WuGuis.add(new WuGui2(2999));
        Collections.sort(WuGuis);
        for(WuGui2 wg : WuGuis){
            System.out.println(wg);
        }

對List集合中元素排序,需要保證List集合中的元素實現了Comparable接口。

(2)Collecitons實現對Set進行排序

Collections是不能直接對Set進行排序的,這時候需要將Set集合轉換成List集合。

        Set<String > s1 =new HashSet<>();
        s1.add("abd");
        s1.add("abc");
        s1.add("abe");
        List<String> l1 = new ArrayList(s1);
        Collections.sort(l1);
        for(String l2 :l1){
            System.out.println(l2);
        }

 

abc
abd
abe

總結:現在終於對集合有了大致的瞭解,首先是集合是幹什麼的,既然集合是存儲數據的,那麼和數組有什麼關係,有Map和Collection他們都有什麼特點,他們都是怎麼進行遍歷的,在Map集合 中HashMap爲什麼要重寫hashCode和equals方法,以及get和put方法存儲數據的原理是什麼等等,還有最大的感受是在學Java之前一定要把數據結構學好了。

 

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