Java中HashMap和synchronized的更多认识

主要内容

HashMap.keySet()

这个方法返回一个内部类 HashMap.KeySet的实例, 多次调用这个方法将获取同一个对象。

  • 调用这个对象的 remove 方法 将删除HashMap 里面的元素。
  • 调用这个对象的 clear 方法将清空 HashMap

可见源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super K> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next)
                    action.accept(e.key);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

这个方法的返回值一般用于遍历 keys, 如果对这个Set做一些操作,但是并不想修改HashMap的数据的话, 可以这么做:

1
2
3
4
5
6
7
8
Map<String, Long> map = new HashMap<>();
Set<String> keys = new HashSet<>(map.keySet());
// 这么做之后, 修改 `keys` 里面的元素就不会影响到 `map` 了
// 但是如果修改keys的元素的值, 那么就会影响到 map里面的key, 这个自然不必多说。
// 在本例里面 元素的类型是 String, 我们都知道String是不可变的, 所以无法修改它的内部属性
// 这里在额外说一些内容 
// 如果 Map<K,V> 中的 K是一个 自定义的复合类型, 在放入map中之后就尽量不要修改它的值
// 详情看下面的代码

如果 Map<K,V> 中的 K是一个 自定义的复合类型, 那么就尽量不要修改它的值, 因为如果你修改了, 则可能会发生一些预期之外的行为。 其实最好的还是不使用复合类型作为KEY, 仅仅使用简单类型和String,或者使用一些 不可变的对象 。 下面是笔者测试使用的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

public class HashMapTest {
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    @ToString
    public static class Point{
        private int x;
        private int y;
    }

    public static void main(String[] args) {
        
        Map<Point, Integer> map = new HashMap<>(8);
        Point point1 = new Point(1,1);
        map.put(point1, 1);
        System.out.println(map.get(new Point(1, 1)));    // 1
        point1.setX(2);
        point1.setY(2);
        System.out.println(map.get(point1));     // null
        System.out.println(map.get(new Point(2, 2)));    // null
        point1.setX(1);
        point1.setY(1);
        System.out.println(map.get(point1));    // 1

        point1.setX(2);
        point1.setY(2);
        System.out.println(map.get(point1));    // null

        Point point3 = new Point(3, 3);
        map.put(point3, 3);
        for (Entry<Point, Integer> entry : map.entrySet()) {
            System.out.println(entry);  
            // HashMapTest.Point(x=2, y=2)=1
            // HashMapTest.Point(x=3, y=3)=3
        }
        for (Point point : map.keySet()) {
            System.out.println(point.toString() + "=" + map.get(point));  
            // HashMapTest.Point(x=2, y=2)=null
            // HashMapTest.Point(x=3, y=3)=3
        }   

        System.out.println(map.get(point1));    // null
        for (int i = 0; i < 160; i++) {
            Point point = new Point(i,i*2);
            map.put(point, i+1);
        }
        System.out.println(map.size());         // 162
        System.out.println(map.get(point1));    // null

        map.put(new Point(1, 1), 10);
        System.out.println(map.size());         // 163
        System.out.println(map.get(point1));    // null
        System.out.println(map.get(new Point(1, 1)));    // 10
        System.out.println(new Point(1, 1).equals(point1));    // false
        System.out.println(new Point(1, 1).hashCode() == point1.hashCode());  // false

        System.out.println(map.get(new Point(2, 2)));   // null
        System.out.println(new Point(2, 2).equals(point1));   // true
        System.out.println(new Point(2, 2).hashCode() == point1.hashCode());   // true

        point1.setX(1);
        point1.setY(1);
        System.out.println(map.get(point1));       // 1
        System.out.println(map.size());            // 163
        System.out.println(new Point(1, 1).equals(point1));   // true
        System.out.println(new Point(1, 1).hashCode() == point1.hashCode());   // true
        
        System.out.println(map.get(new Point(1, 1)));    // 1
    }

}

前面可以看出, 假如 map.put(b,c), map.get(a). 如果 a.hashCode()和b.hashCode() 相等,并且a.equals(b) 返回true, 即使是两个不一样的对象, 也可以获取到 c.

从代码中可以看到 假如 point1的值修改了, 那么你就永远的失去了它,除非你把它修改回去。

  • map.put(point1, 1);
  • 修改 point1的值
  • map.get(point1); // null
  • map.get(new Point(2, 2)); // null

而修改回去又会产生新的问题, 假如在修改之前, put了一个使用原来key相同的值, 那么修改回去之后, 后面put的那个值将会永远丢失。

  • map.put(new Point(1, 1), 10);
  • 修改回 point1的值
  • System.out.println(map.get(point1)); // 1
  • System.out.println(map.get(new Point(1, 1))); // 1
  • 可以看到, 我们的10 丢失了, 但是实际上 map里面的元素数量并没有减少。

原因可能和 HashMap.Node 类的实现有关系。 可以看下面的代码 节选自 java.util.HashMap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V 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 final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

这里的 Node 会缓存 put时的key的 hashCode。 其实是一个间接值,即并不是原来的hashCode,但产生自原来的hashCode.

也许还有更多的测试值得我们去做, 但是笔者认为现在已经拿出足够的理由使我们不使用可变的自定义复合类型作为Map.KEY . 下面的相关内容是没有做测试的部分

  • containsKey
  • remove

synchronized

TODO 待补充

0%