遍历中修改HashMap的Key

时间:2022-04-05 19:16:00

一、不可变对象

1.1 什么是不可变对象

immutable Objects就是那些一旦被创建,它们的状态就不能被改变的Objects,每次对他们的改变都是产生了新的immutable的对象,而mutable Objects就是那些创建后,状态可以被改变的Objects。

不可变的优势:

(1)提高效率。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址。复制地址需要很小的内存,效率也很高;对于同时引用这个“ABC”的其他变量也不会造成影响。

(2)安全也简化了程序的开发。在多线程应用中可以不使用锁机制就能被其他线程共享。


1.2 String类为什么是不可变的

字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域,当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。而对String进行修改操作,实际上是产生了另一个字符串常量,这个可能是新建的,也可能是已经存在字符串常量池中的。

也是因为这个特性,密码应该存放在字符数组中而不是String中。由于String在Java中是不可变的,如果你将密码以明文的形式保存成字符串,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符串被放在字符串缓冲池中以方便重复使用,所以它就可能在内存中被保留很长时间,而这将导致安全隐患,因为任何能够访问内存(memory dump内存转储)的人都能清晰的看到文本中的密码,这也是为什么你应该总是使用加密的形式而不是明文来保存密码。


1.3 如何创建不可变类

要写出这样的类,需要遵循以下几个原则:

1)immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。

2)所有属性都是private final。

3)确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。

5)如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)。

public final class ImmutableReminder{
    private final Date remindingDate;

    public ImmutableReminder (Date remindingDate) {
        if(remindingDate.getTime() < System.currentTimeMillis()){
            throw new IllegalArgumentException("Can not set reminder” + “ for past time: " + remindingDate);
        }
        this.remindingDate = new Date(remindingDate.getTime());
    }

    public Date getRemindingDate() {
        return (Date) remindingDate.clone();
    }
}

二、使用可变对象作为Key的问题

HashMap底层是使用Entry对象数组存储的,而Entry是一个单项的链表。当调用一个put()方法将一个键值对添加进来是,先使用hash()函数获取该对象的hash值,然后调用indexFor方法查找到该对象在数组中应该存储的下标,假如该位置为空,就将value值插入,如果该下标出不为空,则要遍历该下标上面的对象,使用equals()方法进行判断,如果遇到equals()方法返回真的则进行替换,否则将其插入。查找时只需要查询通过key值获取获取hash值,然后找到其下标,遍历该下标下面的Entry对象即可查找到value。

在HashMap中使用可变对象作为Key带来的问题:如果HashMap Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了。下面是证明的例子:

MutableKey.java

public class MutableKey {
    private int i;
    private int j;

    public MutableKey(int i, int j) {
        this.i = i;
        this.j = j;
    }

    public final int getI() {
        return i;
    }

    public final void setI(int i) {
        this.i = i;
    }

    public final int getJ() {
        return j;
    }

    public final void setJ(int j) {
        this.j = j;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + i;
        result = prime * result + j;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof MutableKey)) {
            return false;
        }
        MutableKey other = (MutableKey) obj;
        if (i != other.i) {
            return false;
        }
        if (j != other.j) {
            return false;
        }
        return true;
    }
}

MutableDemo1.java

import java.util.HashMap;
import java.util.Map;

public class MutableDemo1 {

    public static void main(String[] args) {

        // HashMap
        Map<MutableKey, String> map = new HashMap<>();

        // Object created
        MutableKey key = new MutableKey(10, 20);

        // Insert entry.
        map.put(key, "Robin");

        // This line will print 'Robin'
        System.out.println(map.get(key));

        // Object State is changed after object creation.
        // i.e. Object hash code will be changed.
        key.setI(30);

        // This line will print null as Map would be unable to retrieve the
        // entry.
        System.out.println(map.get(key));
    }
}

输出:

Robin
null

如何解决

  • 在HashMap中使用不可变对象。在HashMap中,使用String、Integer等不可变类型用作Key是非常明智的。
  • 我们也能定义属于自己的不可变类。
  • 如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。

三、Map的遍历

1.1 for each map.entrySet()

Map<String, String> map = new HashMap<String, String>();  
for (Entry<String, String> entry : map.entrySet()) {  
    entry.getKey();  
    entry.getValue();  
}

1.2 显示调用map.entrySet()的集合迭代器

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();  
while (iterator.hasNext()) {  
    Map.Entry<String, String> entry = iterator.next();  
    entry.getKey();  
    entry.getValue();  
}

1.3 for each map.keySet(),再调用get获取

Map<String, String> map = new HashMap<String, String>();  
for (String key : map.keySet()) {  
    map.get(key);  
}

1.4 通过Map.values()遍历所有的value,但不能遍历key

for (String v : map.values()) {    
   System.out.println("value= " + v);    
}

四、修改HashMap的Key

题目

一个已知存储了数据的HashMap中如何让所有的键修改成”prefix+原来的键”,意思是在所有的key中添加”prefix”这个前缀。


分析1

根据上面的分析,我们知道了如果直接修改一个Entry的Key是有问题,所以我们实际上需要put一个新的Entry,然后删除老的Entry。

代码1

    public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<>();
        for (int i = 0; i < 6; i++) {
            hashMap.put("key-"+i, "value-"+i);
        }

        printMap(hashMap);

        Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
        Map.Entry<String, String> entry;
        while (iterator.hasNext()){
            entry = iterator.next();
            // 放入新的Entry
            hashMap.put("prefix-"+ entry.getKey(), entry.getValue());
            // 删除老的Entry
            iterator.remove();
        }

        System.out.println("结果:");
        printMap(hashMap);
    }

输出:

Exception in thread "main" java.util.ConcurrentModificationException
( key-0, value-0 )
( key-1, value-1 )
    at java.util.HashMap$HashIterator.remove(HashMap.java:940)
( key-4, value-4 )
    at App.main(App.java:20)
( key-5, value-5 )
( key-2, value-2 )
( key-3, value-3 )

Process finished with exit code 1

原因是,HashMap的迭代器是快速失败的,在遍历中修改value是可以的,但是如果使用put方法增加Entry则modCount也会自增,expectedModCount值与modCount不相同就会抛出ConcurrentModificationException。


分析2

显然分析1的方法存在缺陷,改进的方法是新建一个newMap,新的Entry放到newMap,同时从hashMap中remove旧的Entry,这样内存不至于浪费。

代码2

    public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<>();
        for (int i = 0; i < 6; i++) {
            hashMap.put("key-"+i, "value-"+i);
        }
        printMap(hashMap);

        HashMap<String, String> newMap = new HashMap<>();
        Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
        Map.Entry<String, String> entry;
        while (iterator.hasNext()){
            entry = iterator.next();
            // 往newMap中放入新的Entry
            newMap.put("prefix-"+ entry.getKey(), entry.getValue());
            // 删除老的Entry
            iterator.remove();
        }

        System.out.println("结果:");
        printMap(newMap);
    }

输出

( key-0, value-0 )
( key-1, value-1 )
( key-4, value-4 )
( key-5, value-5 )
( key-2, value-2 )
( key-3, value-3 )
结果:
( prefix-key-5, value-5 )
( prefix-key-3, value-3 )
( prefix-key-4, value-4 )
( prefix-key-1, value-1 )
( prefix-key-2, value-2 )
( prefix-key-0, value-0 )