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}
}
}