2

I have a below builder class which I am using from multithread application so I have made it thread safe. Just for simplicity, I am showing only few fields here to demonstrate the problem.

public final class ClientKey {
  private final long userId;
  private final int clientId;
  private final String processName;
  private final Map<String, String> parameterMap;

  private ClientKey(Builder builder) {
    this.userId = builder.userId;
    this.clientId = builder.clientId;
    this.processName = builder.processName;
    // initializing the required fields
    // and below line throws exception once I try to clone the `ClientKey` object
    builder.parameterMap.put("is_clientid", (clientId == 0) ? "false" : "true");
    this.parameterMap = builder.parameterMap.build();
  }

  public static class Builder {
    private final long userId;
    private final int clientId;
    private String processName;
    private ImmutableMap.Builder<String, String> parameterMap = ImmutableMap.builder();

    // this is for cloning
    public Builder(ClientKey key) {
      this.userId = key.userId;
      this.clientId = key.clientId;
      this.processName = key.processName;
      this.parameterMap =
          ImmutableMap.<String, String>builder().putAll(key.parameterMap);
    }

    public Builder(long userId, int clientId) {
      this.userId = userId;
      this.clientId = clientId;
    }

    public Builder parameterMap(Map<String, String> parameterMap) {
      this.parameterMap.putAll(parameterMap);
      return this;
    }

    public Builder processName(String processName) {
      this.processName = processName;
      return this;
    }

    public ClientKey build() {
      return new ClientKey(this);
    }
  }

  // getters
}

Below is how I make ClientKey and it works fine.

Map<String, String> testMap = new HashMap<String, String>();
testMap.put("hello", "world");
ClientKey keys = new ClientKey.Builder(12345L, 200).parameterMap(testMap).build();

Now when I try to clone the keys object as shown below, it throws exception.

ClientKey clonedKey = new ClientKey.Builder(keys).processName("hello").build();

It throws exception with error message as: java.lang.IllegalArgumentException: Multiple entries with same key: is_clientid=true and is_clientid=true

builder.parameterMap.put("is_clientid", (clientId == 0) ? "false" : "true");
// from below line exception is coming
this.parameterMap = builder.parameterMap.build();

How can I fix this problem? I want to make my map immutable but I also want to initialize with required fields as well and that I can only do it in the constructor of ClientKey class. And it throws exception while cloning the ClientKey object.

  • can you clarify where the error message is thrown? It doesn't seem clear from your code above that `ClientKey clonedKey = new ClientKey.Builder(keys).processName("hello").build();` would throw the exception since you don't even have the key `is_clientid`. Where is the next line in your code happening? – jamesw1234 Dec 17 '16 at 06:19
  • Are you sure this really is an instance of the builder pattern? -- I've never seen someone pass a "builder" to a private constructor just to set the fields of a new object. – errantlinguist Dec 17 '16 at 06:22
  • @jamesw1234 exception is thrown at this line `this.parameterMap = builder.parameterMap.build();` but only if I try to clone the `keys` object into a new one `clonedKey` –  Dec 17 '16 at 06:22
  • Since at the time of cloning, my `ClientKey` constructor will be called again and then it will try to insert same `is_clientid` again into the map and that's why it throws exception. –  Dec 17 '16 at 06:24
  • where is `builder` instantiated? – jamesw1234 Dec 17 '16 at 06:25
  • check this method `build()` in my builder pattern. I guess this error is bcoz you cannot insert same key twice in Guava ImmutableMap? –  Dec 17 '16 at 06:26
  • your `build()` method doesn't actually instantiate `builder`. In your code `builder.parameterMap.build();` where is `builder` first instantiated – jamesw1234 Dec 17 '16 at 06:29
  • @jamesw1234 I am not sure what you mean. If you copy paste the whole code and try it out, you should be able to reproduce this very easily. –  Dec 17 '16 at 06:32

2 Answers2

6

When you construct a ClientKey, the "is_clientid" key is put in the map. Therefore, if you call your ClientKey.Builder(ClientKey) constructor the putAll call will copy it to your new ImmutableMap.Builder instance. When you then build your cloned ClientKey, the ClientKey constructor will again try to add the same key to the map, which causes the exception.

The ImmutableMap.Builder could have been written in a different way, but it wasn't. If you want to use it, you'll have to live with it.

One solution is to not copy the entry with the "is_clientid" key to the new ImmutableMap.Builder in the constructor of your Builder. Instead of this.parameterMap = ImmutableMap.<String, String>builder().putAll(key.parameterMap); you write:

this.parameterMap = new ImmutableMap.Builder<>();
for (Map.Entry<String,String> entry : key.parameterMap.entrySet()) {
    if (!"is_clientid".equals(entry.getKey()) {
        this.parameterMap.put(entry.getKey(), entry.getValue());
    }
}

Another solution is to not use Guava's ImmutableMap.Builder, but a normal Java HashMap (it does not throw exception when you try to put a duplicate key in it, the old entry is simply overwritten). Then in your ClientKey constructor you write:

this.parameterMap = Collections.unmodifiableMap(builder.parameterMap);

You could also write:

this.parameterMap = ImmutableMap.copyOf(builder.parameterMap);

but this makes an entire copy of the map, which may take some time for very large maps.

A concluding remark: if all you want to do is copy a ClientKey, you do not need a builder; idiomatic Java would use a copy constructor or the clone() method (although the latter is discouraged by some).

Hoopje
  • 12,677
  • 8
  • 34
  • 50
  • Where I should use the first solution you mentioned? Is it also under `ClientKey` constructor or `Builder` constructor? –  Dec 17 '16 at 16:42
  • @user5447339 I edited the answer. The snippet would come in the constructor of your Builder class. – Hoopje Dec 17 '16 at 20:57
  • "not use Guava's `ImmutableMap`" - the problem is the builder, not the map. If the result should be immutable and key duplicates are to be handled, then it can be built using `ImmutableMap.copyOf(aHashMapUsedAsBuilder)`. – maaartinus Dec 17 '16 at 23:12
  • @maartinus True. I edited the answer. Still, `ImmutableMap.copyOf` makes a copy of the entire map, whereas as `Collections.unmodifiableMap` only creates a view of the original map. – Hoopje Dec 18 '16 at 07:01
0

You are getting an exception because you're trying to set a value for the key is_clientid in the same ImmutableMap.Builder used by your single ClientKey.Builder class:

builder.parameterMap.put("is_clientid", (clientId == 0) ? "false" : "true");

As seen in the documentation:

Associates key with value in the built map. Duplicate keys are not allowed, and will cause build() to fail.

Don't re-use the same instance of ImmutableMap.Builder.

You can clone an object sort of like this instead:

public ClientKey(ClientKey copyee) {
    // Copy fields here
    this.parameterMap = ImmutableMap.copyOf(copyee.parameterMap);
}

If you want to use some sort of builder object, you could do something like this:

public Builder(ClientKey copyee) {
    this.oldParameterMap = copyee.parameterMap;
}

public ClientKey build() {
    // Create new map here and pass it to new ClientKey somehow
    ImmutableMap.copyOf(oldParameterMap);
    return newKey;
}
errantlinguist
  • 3,658
  • 4
  • 18
  • 41
  • Yeah that I was able to figure it out why it throws exception but how to fix it in my case? –  Dec 17 '16 at 06:32
  • Again, **don't re-use the same instance of `ImmutableMap.Builder`**. – errantlinguist Dec 17 '16 at 06:34
  • Can you provide an example of not reusing same instance so that I can understand this? –  Dec 17 '16 at 06:35
  • I added a small example to help you understand a bit better. – errantlinguist Dec 17 '16 at 06:47
  • I used [this](http://stackoverflow.com/a/16217011/5447339) example which demonstrates how to clone the builder pattern from old to new. In that it was mentioned to pass the old reference in the Builder constructor. –  Dec 17 '16 at 06:58
  • I still don't understand why you need an additional "builder" class just to copy an object; Do you have a reason or are you just doing it because you saw it there? – errantlinguist Dec 17 '16 at 07:30
  • I added an example with some sort of "builder" object. – errantlinguist Dec 17 '16 at 07:43
  • 2
    Your `new ImmutableMap.Builder().putAll(...).build();` is in no way better than `ImmutableMap.copyOf(oldParameterMap)`. – maaartinus Dec 17 '16 at 23:14