1. 程式人生 > 實用技巧 >JavaSE學習筆記 - Map集合

JavaSE學習筆記 - Map集合

Map 繼承結構圖

Map

  • Map 集合為雙列集合,集合中不能包含重複的鍵,但是值可以重複,並且每一個鍵只能對應一個值。

常用方法

public class Main {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "a");
        map.put(3, "b");
        map.put(2, "c");
        System.out.println(map);//{1=a, 2=c, 3=b}
        System.out.println(map.get(3));//b
        //這裡需要注意,如果是自定義型別我們需要重寫hashCode和equals方法
        //equals方法在Collection中已經闡述過原因
        //hashCode方法在HashMap中作出解釋
        //底層呼叫的equals
        System.out.println(map.containsKey(1));//true
        System.out.println(map.containsValue("b"));//true
        map.remove(1);
        System.out.println(map);//{2=c, 3=b}
    }
}

Map 集合的遍歷

public class Main {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "a");
        map.put(3, "b");
        map.put(2, "c");

        //通過keySet方法拿到鍵值
        Set<Integer> set = map.keySet();
        for (Integer key : set) {
            System.out.print(key + ":" + map.get(key) + "   ");//1:a   2:c   3:b
        }
        System.out.println();

        //Set套自定義型別
        Set<Map.Entry<Integer, String>> set1 = map.entrySet();
        for (Map.Entry<Integer, String> entry : set1) {//這種方式效率要高一些
            System.out.print(entry.getKey() + ":" + entry.getValue() + " ");//1:a   2:c   3:b 
        }
        System.out.println();
    }
}

HashMap

  • 底層是一個雜湊表,雜湊表是陣列和單向連結串列的結合體。雜湊表實際上就是一個數組,陣列中存放的元素是 Node
transient Node<K,V>[] table; //Node陣列

static class Node<K,V> implements Map.Entry<K,V> { //結點
	//通過hash演算法,可以將hash值轉換為陣列下標
    final int hash; //hash值,hashCode方法的結果
    final K key; //key
    V value; //value
    Node<K,V> next; //下一個結點
 
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
} 
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
               e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap存取記憶體分析

  • 通過上面的圖我們看的出來,在存入和取出的時候,我們都將 key 通過 hashCode 得到和 hash 值,然後在通過 equals 方法來進行比較,所以我們在使用自定義的型別的時候,需要重寫這兩個方法,如果不進行重寫,那麼就會出現問題(見程式碼)。(Java 8 以後,如果連結串列上面的結點超過了 8 個,那麼連結串列將會轉換為紅黑樹(平衡二叉樹),當樹上的結點小於 6 個時,會在變回單向連結串列。)

  • 未重寫 HashCode 和 equals 方法

public class Person {
    String name;
    public Person() {
    }

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

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person("a");
        Person person1 = new Person("a");

        //未重寫hashCode和equals
        System.out.println(person.equals(person1));//false
        System.out.println(person.hashCode());//460141958
        System.out.println(person1.hashCode());//1163157884

        Map<Person, String> map = new HashMap<>();
        map.put(person, "a");
        map.put(person1, "b");
        System.out.println(map);//{Person{name='a'}=b, Person{name='a'}=a}
    }
}
  • 按照 HashMap 的儲存方式的分析,我們可以發現,首先 key 轉換成 hash 值,在我們沒有寫hashCode方法的時候,兩個new Person("a")都被存入,但是我們的目的是存入一個,這是因為我們在自定義類的時候沒有重寫 hashCode 方法,在我們 new 物件的時候即便 name 是一樣的,那麼也會因為 hashCode 的不同被存入到雜湊表中。下面我們就在類中重寫 hashCode 方法(equals 方法先不寫)
public class Person {
    String name;
    public Person() {
    }

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

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

//    @Override
//    public boolean equals(Object o) {
//        if (this == o) return true;
//        if (o == null || getClass() != o.getClass()) return false;
//        Person person = (Person) o;
//        return Objects.equals(name, person.name);
//    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person("a");
        Person person1 = new Person("a");

        //未重寫hashCode和equals
        System.out.println(person.equals(person1));//false
        System.out.println(person.hashCode());//128
        System.out.println(person1.hashCode());//128

        Map<Person, String> map = new HashMap<>();
        map.put(person, "a");
        map.put(person1, "b");
        System.out.println(map);//{Person{name='a'}=a, Person{name='a'}=b}
    }
}
  • 此時我們發現雜湊表中仍然存入了兩個元素,雖然我們重寫了 hashCode 方法,但是在第二個物件存入雜湊表的時候發現這個某一個位置上已經有元素,那麼就拿當前結點的 key 和單向連結串列中的 key 進行 equals,由於我們沒有重寫 equals 方法,所以預設比較的是地址,所以當前的這個結點就被加入到了雜湊表中。只有重寫了 hashCode 和 equals 方法才能達到我們的目的。此時雜湊表中的主鍵值就唯一了。
public class Person {
    String name;
    public Person() {
    }

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person("a");
        Person person1 = new Person("a");

        //未重寫hashCode和equals
        System.out.println(person.equals(person1));//true
        System.out.println(person.hashCode());//128
        System.out.println(person1.hashCode());//128

        Map<Person, String> map = new HashMap<>();
        map.put(person, "a");
        map.put(person1, "b");
        System.out.println(map);//{Person{name='a'}=b}
    }
}
  • HashMap 的 key 和 value 可以為 null,初始化容量為 16,預設載入因子為 0.75,預設載入因子指的是當 HashMap 底層的 Node 陣列容量達到 75% 的時候就對雜湊表進行擴容。擴容時新的容量是舊的容量的 2 倍。
  • HashMap 的初始化容量必須是 2 的倍數,這樣才能提高 HashMap 的存取效率。
/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
 public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

static final float DEFAULT_LOAD_FACTOR = 0.75f;

Hashtable

  • 和 HashMap 一樣,但是 Hashtable 是執行緒安全的,執行效率比較低。Hashtable 的初始化容量為 11,預設載入因子為 0.75,在進行擴容的時候 int newCapacity = (oldCapacity << 1) + 1;,新容量 = 舊容量 << 1 + 1
  • Hashtable 的 key 值 和 value 值 不能為空,當 key 值為空時,會丟擲異常
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }
}

Properties

public class Main {
    public static void main(String[] args) {
        Properties pro = new Properties();
        pro.setProperty("1", "a");
        pro.setProperty("2", "b");
        System.out.println(pro.getProperty("1"));//a
        System.out.println(pro.getProperty("2"));//b
    }
}

TreeMap

  • 無序不可重複,但是可以對 key 進行排序,底層是紅黑樹
public class Main {
    public static void main(String[] args) {
        TreeMap<String, String> map = new TreeMap<>();
        map.put("a1", "b");
        map.put("a2", "b");
        map.put("a6", "b");
        map.put("a4", "b");
        map.put("a5", "b");
        System.out.println(map);//{a1=b, a2=b, a4=b, a5=b, a6=b}
    }
}
  • 由於 String 類實現了 Comparable 介面,所以 key 值是按照 String 類中的排序方式來對 key 進行排序的。第一種方式是自定義比較器,第二種方式是自定義類實現 Comparable 介面。

自定義比較器

public class Person {
    String name;
    public Person() {
    }

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

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

}
public class Main {
    public static void main(String[] args) {
        TreeMap<Person, String> map = new TreeMap<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.name.compareTo(o2.name);
            }
        });
        map.put(new Person("a1"), "a");
        map.put(new Person("a5"), "a");
        map.put(new Person("a3"), "a");
        System.out.println(map);//{Person{name='a1'}=a, Person{name='a3'}=a, Person{name='a5'}=a}

    }
}

自定義類實現 Comparable 介面

public class Person implements Comparable<Person>{
    String name;
    public Person() {
    }

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

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

    @Override
    public int compareTo(Person o) {
        return this.name.compareTo(o.name);
    }
}
public class Main {
    public static void main(String[] args) {
        TreeMap<Person, String> map = new TreeMap<>();
        map.put(new Person("a1"), "a");
        map.put(new Person("a5"), "a");
        map.put(new Person("a3"), "a");
        System.out.println(map);//{Person{name='a1'}=a, Person{name='a3'}=a, Person{name='a5'}=a}
    }
}