How can I create a map with immutable keys and no duplicates using Google Guava?
I would like to create a key / value map structure using Google Guava where keys cannot be changed, but values ββcan. I also want to be able to use a predicate (or something similar) to iterate over the map and only get the records that matter.
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 do this?
-------- Decision --------
As per the comments below, I went with ForwardingMap. The implementation is simple.
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;
}
}
source to share
Guava can do nothing for you if your keys are not immutable; this is something you should be sure of yourself (making sure the class of all keys is an immutable class).
Not ImmutableMap
even immune to this kind of failure:
// Modify the key
victim.keySet().iterator().next().alterMe();
If you want to customize the insert / extract behavior, you can use ForwardingMap
another Map
instance to transfer .
Beware, however, that this class leaves you a lot of freedom, including breaking the contract Map
, which you obviously should refrain from!
source to share
I would use EnumMap
which overwrites the method put()
:
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 enum
with a static method that creates an anonymous one EnumMap
, initialized with the supplied keys. It uses the anonymous class initializer block to map the supplied keys to Optional.absent()
and overrides the put
method to deny entry of keys not supplied as arguments.
It also has a helper method that returns a view of the map containing records whose values ββdiffer from 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 the returned map so that instead of deleting the entry, it installs Optional.absent()
. It's the same with other methods that can affect the map.
source to share
I don't think Guava can do this for you. Guava defines ImmutableMap
which means that neither keys nor values ββcan be changed. What you are describing is more like a static array than a map where array positions are mapped to fixed keys. You might be better off writing your own implementation Map
. You can store ImmutableMap<Key,Integer>
for keys where the values ββare positions in an array of actual map values, for example Value[]
, initialized with the size of the keyset. Then you can implement your own put
, which will throw an exception if the provided key is not in ImmutableMap
.
Or, you can simply define a wrapper for some implementation Map
that implements put
.
source to share