20

Much like how an ImmutableList could be extended as such:

ImmutableList<Long> originalList = ImmutableList.of(1, 2, 3);
ImmutableList<Long> extendedList = Iterables.concat(originalList, ImmutableList.of(4, 5));

If I have an existing map, how could I extend it (or create a new copy with replaced values)?

ImmutableMap<String, Long> oldPrices = ImmutableMap.of("banana", 4, "apple", 7);
ImmutableMap<String, Long> newPrices = … // Increase apple prices, leave others.
                                         //  => { "banana": 4, "apple": 9 }

(Let's not seek an efficient solution, as apparently that doesn't exist by design. This question rather seeks the most idiomatic solution.)

Fjolnir Dvorak
  • 316
  • 3
  • 13
Andres Jaan Tack
  • 22,566
  • 11
  • 59
  • 78

5 Answers5

29

You could explicitly create a builder:

ImmutableMap<String, Long> oldPrices = ImmutableMap.of("banana", 4, "apple", 7);
ImmutableMap<String, Long> newPrices =
    new ImmutableMap.Builder()
    .putAll(oldPrices)
    .put("orange", 9)
    .build();

EDIT:
As noted in the comments, this won't allow overriding existing values. This can be done by going through an initializer block of a different Map (e.g., a HashMap). It's anything but elegant, but it should work:

ImmutableMap<String, Long> oldPrices = ImmutableMap.of("banana", 4, "apple", 7);
ImmutableMap<String, Long> newPrices =
    new ImmutableMap.Builder()
    .putAll(new HashMap<>() {{
        putAll(oldPrices);
        put("orange", 9); // new value
        put("apple", 12); // override an old value
     }})
    .build();
Yang
  • 7,712
  • 9
  • 48
  • 65
Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • 1
    The restriction here is that duplicates are not allowed -- it doesn't allow me to increase the price of apples. – Andres Jaan Tack Apr 24 '15 at 06:49
  • @AndresJaanTack see my edited answer. Far from elegant, but it should do the trick. – Mureinik Apr 24 '15 at 07:38
  • 5
    Please don't recommend "double brace initialization". See http://stackoverflow.com/a/9108655/95725 and http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/ – NamshubWriter Apr 25 '15 at 03:08
10

Just copy the ImmutableMap into a new HashMap, add the items, and convert to a new ImmutableMap

ImmutableMap<String, Long> oldPrices = ImmutableMap.of("banana", 4, "apple", 7);
Map<String, Long> copy = new HashMap<>(oldPrices);
copy.put("orange", 9); // add a new entry
copy.put("apple", 12); // replace the value of an existing entry

ImmutableMap<String, Long> newPrices = ImmutableMap.copyOf(copy);
NamshubWriter
  • 23,549
  • 2
  • 41
  • 59
3

Staying with Guava, you can create a utility method that skips duplicates when building the new map. This is done with the help of Maps.filterKeys().

This is the utility function:

private <T, U> ImmutableMap<T, U> extendMap(ImmutableMap<T, U> original, ImmutableMap<T, U> changes) {
    return ImmutableMap.<T, U>builder()
            .putAll(changes)
            .putAll(Maps.filterKeys(original, key -> !changes.containsKey(key)))
            .build();
}

and here is a unit test with your data (based on AssertJ).

@Test
public void extendMap() {
    ImmutableMap<String, Integer> oldPrices = ImmutableMap.of("banana", 4, "apple", 7);
    ImmutableMap<String, Integer> changes = ImmutableMap.of("orange", 9, "apple", 12);
    ImmutableMap<String, Integer> newPrices = extendMap(oldPrices, changes);
    assertThat(newPrices).contains(
            entry("banana", 4),
            entry("apple", 12),
            entry("orange", 9));

}

UPDATE: Here is a slightly more elegant alternative for the utility function based on Maps.difference().

private <T, U> ImmutableMap<T, U> extendMap(ImmutableMap<T, U> original, ImmutableMap<T, U> changes) {
    return ImmutableMap.<T, U>builder()
            .putAll(Maps.difference(original, changes).entriesOnlyOnLeft())
            .putAll(changes)
            .build();
}
Bogdan Calmac
  • 7,993
  • 6
  • 51
  • 64
0

Well I've done this with streams but it isn't perfect:

public static <K,V> Map<K,V> update(final Map<K,V> map, final Map.Entry<K,V> replace)
{
    return Stream.concat(
        Stream.of(replace),
        map.entrySet().stream()
            .filter(kv -> ! replace.getKey().equals(kv.getKey()))
    .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue));
}

and this only inserts or updates a single entry. Note that ImmutableMap & the associated collector can be dropped in (which is what I actually used)

0

Not a terribly performant code, but the below would work

private <K, V> ImmutableMap.Builder<K, V> update(final ImmutableMap.Builder<K, V> builder, final List<ImmutablePair<K, V>> replace) {
    Set<K> keys = replace.stream().map(entry -> entry.getKey()).collect(toSet());
    Map<K, V> map = new HashMap<>();
    builder.build().forEach((key, val) -> {
        if (!keys.contains(key)) {
            map.put(key, val);
        }
    });
    ImmutableMap.Builder<K, V> newBuilder = ImmutableMap.builder();
    newBuilder.putAll(map);
    replace.stream().forEach(kvEntry -> newBuilder.put(kvEntry.getKey(), kvEntry.getValue()));
    return newBuilder;
}