Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Example of an immutable class with hashmap

I have defined the following classes trying to make "IamImmutable.class" immutable. But when I change hashmap values in TestingImmutability.class after initialising IamImmutable, the changes are applicable to the Hashmap. Hashmap would refer to the same object even if we instantiate it with new HashMap(old). I need to make Hashmap in the instance immutable. I have tried iterating and copying the values as well but that doesn't work. Can anyone suggest on how to proceed?

package string;
import java.util.HashMap;
import java.util.Map.Entry;

public final class IamImmutable {
  private int i;
  private String s;
  private HashMap<String, String> h;

  public IamImmutable(int i, String s, HashMap<String, String> h) {
    this.i = i;
    this.s = s;

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

  public int getI() {
    return i;
  }

  public String getS() {
    return s;
  }

  public HashMap<String, String> getH() {
    return h;
  }
}

And a test:

package string;

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

public class TestingImmutability {

  public static void main(String[] args) {
    int i = 6;
    String s = "!am@John";
    HashMap<String, String> h = new HashMap<String, String>();

    h.put("Info1", "!am@John");
    h.put("Inf02", "!amCrazy6");


    IamImmutable imm = new IamImmutable(i, s, h);

    h.put("Inf02", "!amCraxy7");

    System.out.println(imm.getS() + imm.getI());
    for (Entry<String, String> entry: h.entrySet())
      System.out.println(entry.getKey() + " --- " + entry.getValue());
  }
}

Expected output:

  !am@ John6
Inf02---!amCrazy6
Info1---!am@ John

Actual output:

  !am@ John6
Inf02---!amCraxy7
Info1---!am@ John
like image 285
Atom Avatar asked Dec 14 '22 15:12

Atom


2 Answers

Your test is wrong, you're checking the contents of h, the map that you passed to the constructor and later modified, instead of imm.getH(). If you check the proper map

for (Entry<String, String> entry : imm.getH().entrySet())
        System.out.println(entry.getKey() + " --- " + entry.getValue());

things look just fine:

!am@John6
Info1 --- !am@John
Inf02 --- !amCrazy6

So your IamImmutable constructor is already fine, any later changes to the original map passed into the constructor won't affect the copy you made at construct time. You can also use the other HashMap constructor you mentioned, which is slightly more readable:

public IamImmutable(int i, String s, HashMap<String, String> h)
{
    this.i = i;
    this.s = s;
    this.h = new HashMap<String, String>(h);
}

And that will work fine too.


A different problem is that getH() passes a reference to the internal map out to the world, and if the world changes that reference, things will go wrong. An easy way to get around that is to apply the same copy trick that you already use in the constructor in getH() as well:

public HashMap < String, String > getH() {
    return new HashMap<String,String>(h);
}

Or to decorate the internal map before returning it:

public Map<String, String> getH() {
    return Collections.unmodifiableMap(h);
}

Consider using the ImmutableCollections in the guava library instead. They've done the same job that this code is doing, but with a bit more thought to efficiency and ease of use. The full copies at construct time and to get the map are unwieldy and the standard underlying HashMap will do modification checks that are pointless if we know it isn't going to get modified anyways.

like image 84
flup Avatar answered Dec 17 '22 04:12

flup


In order for your class to be immutable, getH() must return a copy of the HashMap. Otherwise, any caller of getH() can modify the state of the HashMap member of your IamImmutable class, which means it's not immutable.

An alternative would be to replace getH() with methods that access that internal HashMap without exposing it. For example, you can have a method String[] keys() that returns all the keys of the HashMap and a method String get(String key) that returns the value for a given key.

like image 30
Eran Avatar answered Dec 17 '22 05:12

Eran