具有相同密钥不可变映射错误的多个条目

时间:2016-12-17 06:11:57

标签: java multithreading guava builder builder-pattern

我有一个下面的构建器类,我从多线程应用程序使用,所以我已经使它线程安全。为简单起见,我在这里只显示几个字段来证明问题。

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
}

以下是我ClientKey的制作方式,并且工作正常。

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

现在,当我尝试克隆keys对象时,如下所示,它会抛出异常。

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

它抛出异常,错误消息为: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();

如何解决此问题?我想让我的地图不可变,但我也想用必填字段进行初始化,我只能在ClientKey类的构造函数中进行初始化。并且它在克隆ClientKey对象时抛出异常。

2 个答案:

答案 0 :(得分:3)

构建ClientKey时,"is_clientid"键将放入地图中。因此,如果您致电ClientKey.Builder(ClientKey)构造函数,则putAll调用会将其复制到新的ImmutableMap.Builder实例。然后,当您构建克隆的ClientKey时,ClientKey构造函数将再次尝试将相同的键添加到地图中,从而导致异常。

ImmutableMap.Builder本来可以用不同的方式编写,但它不是。如果你想使用它,你将不得不忍受它。

一种解决方案是不将带有"is_clientid"密钥的条目复制到ImmutableMap.Builder的构造函数中的新Builder。而不是this.parameterMap = ImmutableMap.<String, String>builder().putAll(key.parameterMap);你写的:

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());
    }
}

另一个解决方案是不使用Guava的ImmutableMap.Builder,而是使用普通的Java HashMap(当你尝试在其中放入一个重复的密钥时,它不会抛出异常,旧条目是简单地覆盖)。然后在你的ClientKey构造函数中写下:

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

你也可以写:

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

但是这会制作地图的完整副本,这可能需要一些时间来制作非常大的地图。

结束语:如果您只想复制ClientKey,则不需要建设者;惯用的Java会使用复制构造函数或clone()方法(尽管有些人不鼓励后者)。

答案 1 :(得分:0)

您收到例外情况是因为您尝试在单个is_clientid类使用的同一个ImmutableMap.Builder中设置密钥ClientKey.Builder的值:

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

the documentation中所示:

  

将键与构建映射中的值相关联。不允许使用重复密钥,这将导致build()失败。

请勿重复使用ImmutableMap.Builder的相同实例。

你可以克隆一个这样的对象:

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

如果你想使用某种类型的构建器对象,你可以这样做:

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