简体   繁体   中英

How to create a Map with immutable keys and no duplicates using Google Guava?

I'd like to create a key/value map structure using Google Guava where the keys cannot be modified but the values can. I also want the ability to use a predicate (or something similar) to iterate the Map and only retrieve those entries that have values.

For example, conceptually:

// start
Map data =
{Constants.KEY_NAME_1, Optional.absent()},
{Constants.KEY_NAME_2, Optional.absent()};

// succeeds
data.put(Constants.KEY_NAME_2, Optional.of("new_data"));

// finish
Map data =
{Constants.KEY_NAME_1, Optional.absent()},
{Constants.KEY_NAME_2, Optional("new_data")};

// fails
data.put(Constants.KEY_NAME_3, Optional.of("more_new_data"));

Any idea how to accomplish this?

-------- Solution --------

As per the comments below, I went with the ForwardingMap. The Implementation is straightforward

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableList;
import java.util.Map;

Map<String, String> labelMap = ImmutableMap.<String, String> builder()
    .put("KEY_1", "data1")
    .put("KEY_2", "data2")
    .build();

MyCustomMap<String> map = new MyCustomMap(labelMap);

public class MyCustomMap<String> extends ForwardingMap<String, String> {

    private final Map<String, String> delegate;
    private final ImmutableMap<String, String> immutableMap;

    public MyCustomMap(Map<String, String> labelMap) {

        /*
            Check for duplicate values in the map here.  The construction of 
            the ImmutableMap above ensures that there are no duplicate
            keys.  Otherwise it will throw
            "IllegalArgumentException: Multiple entries with same key".
        */

        delegate = labelMap;
        immutableMap = ImmutableMap.<String, String>builder().putAll(delegate).build();
    }

    @Override
    protected Map<String, String> delegate() {
        return immutableMap;
    }
}

Guava cannot do anything for you if your keys are not immutable; this is something you have to ensure yourself (by making sure that the class of all keys is an immutable class).

Even an ImmutableMap is not immune to this kind of mishap:

// Modify the key
victim.keySet().iterator().next().alterMe();

If what you wish to do is customize the behavior upon insertion/retrieval then you can use a ForwardingMap to wrap another Map instance.

Beware however that this class leaves you a lot of freedom, including that of breaking the Map contract, which you should obviously refrain from!

I'd use an EnumMap that overwrites the put() method:

public enum Constants {
    KEY_NAME_1, KEY_NAME_2, KEY_NAME_3;

    @SuppressWarnings("serial")
    public static <T> EnumMap<Constants, Optional<T>> asMap(
        final Constants... validKeys) {

        return new EnumMap<Constants, Optional<T>>(Constants.class) {
            {
                for (Constants c : validKeys) {
                    super.put(c, Optional.absent());
                }
            }

            @Override
            public Optional<T> put(Constants key, Optional<T> value) {
                if (!this.containsKey(key)) {
                    throw new IllegalArgumentException("Invalid key");
                }
                return super.put(key, value);
            }
        };
    }

    public static <T> Map<Constants, Optional<T>> withValues(
        EnumMap<Constants, Optional<T>> map) {

        return Maps.filterValues(map, new Predicate<Optional<T>>() {

            @Override
            public boolean apply(Optional<T> input) {
                return input.isPresent();
            }
        });
    }
}

This is an enum with a static method that creates an anonymous EnumMap initialized with the provided keys. It uses the anonymous class' initializer block to map the provided keys to Optional.absent() and overrides the put method to disallow putting keys not provided as arguments.

It also has a helper method that returns a view of the map containing the entries that have a value different than Optional.absent() .

Sample usage:

// Create map with KEY_NAME_1 and KEY_NAME_2 only
EnumMap<Constants, Optional<String>> map = 
    Constants.asMap(Constants.KEY_NAME_1, Constants.KEY_NAME_2);

System.out.println(map); // {KEY_NAME_1=Optional.absent(), KEY_NAME_2=Optional.absent()}

map.put(Constants.KEY_NAME_2, Optional.of("two"));

System.out.println(map); // {KEY_NAME_1=Optional.absent(), KEY_NAME_2=Optional.of(two)}

Map<Constants, Optional<String>> withValues = Constants.withValues(map);

System.out.println(withValues); // {KEY_NAME_2=Optional.of(two)}

map.put(Constants.KEY_NAME_3, Optional.of("three")); // throws IllegalArgumentException

// TODO Override remove() in returned map so that instead of removing the entry it sets an Optional.absent() . Same with other methods that might affect the map.

I don't think Guava can do that for you. Guava defines ImmutableMap which means neither the keys nor the values can be modified. What you are describing is more like a static array than a map, where the array positions map to fixed keys. You might be better off writing your own Map implementation. You could store an ImmutableMap<Key,Integer> for the keys, where the values are the positions in an array of the actual map values, say Value[] , initialized with the size of the set of keys. Then you can implement your custom put that throws an exception if the provided key is not in the ImmutableMap .

Or you can just define a wrapper for some Map implementation that implements put .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM