How to convert Array to HashMap using Java 8 Stream
Here is my idea by JDK 8 stream:
public static <K, V> Map<K, V> toMap(final Object... entries) {
// Requirements:
// entries must be K1, V1, K2, V2, .... ( even length )
if (entries.length % 2 == 1) {
throw new IllegalArgumentException("Invalid entries");
}
final Map<K, V> map = new HashMap<>((int) (entries.length / 2 * 1.25 + 1));
IntStream.range(0, entries.length / 2).forEach(i -> map.put((K) entries[i * 2], (V) entries[i * 2 + 1]));
return map;
// OR:
// return IntStream.range(0, entries.length / 2).boxed().reduce(new HashMap<K, V>(), (m, i) -> {
// m.put((K) entries[i * 2], (V) entries[i * 2 + 1]);
// return m;
// }, (a, b) -> {
// a.putAll(b);
// return b;
// });
}
If you don't mind use third-party library AbacusUtil, the code could be simplified to:
public static <K, V> Map<K, V> toMap2(final Object... entries) {
// Requirements:
// entries must be K1, V1, K2, V2, .... ( even length )
if (entries.length % 2 == 1) {
throw new IllegalArgumentException("Invalid entries");
}
return Stream.of(entries).split0(2).toMap(e -> (K) e.get(0), e -> (V) e.get(1));
}
And I think the most efficient way to do it is by for loop, if you not particularly pursue using Stream API
public static <K, V> Map<K, V> toMap3(final Object... entries) {
// Requirements:
// entries must be K1, V1, K2, V2, .... ( even length )
if (entries.length % 2 == 1) {
throw new IllegalArgumentException("Invalid entries");
}
final Map<K, V> map = new HashMap<>((int) (entries.length / 2 * 1.25 + 1));
for (int i = 0, len = entries.length; i < len; i++) {
map.put((K) entries[i], (V) entries[++i]);
}
return map;
// OR just call the method in AbacusUtil.
// return N.asMap(entries);
}
Getting exactly what you want will probably not work for maps whose key type differs from their value type. This is because Java's variable arity declaration (the Object... entries
part) supports only one type.
Some options come to mind:
You could do the checks dynamically and throw an illegal argument exception if the values don't match. But you'll lose the compiler type-checking.
You could define a
Pair
class, and play a bit with static import to get almost what you want:
e.g.:
class Pair<K,V> {
final K k;
final V v;
Pair( K ak, V av) {
k=ak;
v=av;
}
static <A,B> Pair<A,B> p(A a, B b) {
return new Pair(a,b);
}
}
public class JavaTest8 {
<K,V> Map<K,V> toMap( Pair<K,V>... pairs ) {
return Arrays.stream(pairs).collect(Collectors.toMap(p->p.k, p->p.v));
}
public static void main(String[] args) {
// Usage
Map<String,Integer> sti = toMap( p("A",1), p("B",2) );
Map<Integer,Boolean> itb = toMap( p(1,true), p(42,false) );
}
}
You may use
public static <K, V> Map<K, V> toMap(Object... entries) {
if(entries.length % 2 == 1)
throw new IllegalArgumentException("Invalid entries");
return (Map<K, V>)IntStream.range(0, entries.length/2).map(i -> i*2)
.collect(HashMap::new, (m,i)->m.put(entries[i], entries[i+1]), Map::putAll);
}
but it will give you a (founded) unchecked warning. Your method can’t hold the promise to return a correctly typed Map<K, V>
for an array of arbitrary objects and, even worse, it will not fail with an exception, but silently return an inconsistent map if you pass in objects of the wrong type.
A cleaner, commonly used, solution is
public static <K, V> Map<K, V> toMap(
Class<K> keyType, Class<V> valueType, Object... entries) {
if(entries.length % 2 == 1)
throw new IllegalArgumentException("Invalid entries");
return IntStream.range(0, entries.length/2).map(i -> i*2)
.collect(HashMap::new,
(m,i)->m.put(keyType.cast(entries[i]), valueType.cast(entries[i+1])),
Map::putAll);
}
This can be compiled without a warning, as the correctness will be checked at runtime. The calling code has to be adapted:
Map<String, Integer> map1 = toMap(String.class, Integer.class, "k1", 1, "k2", 2);
Map<String, String> map2 = toMap(
String.class, String.class, "k1", "v1", "k2", "v2", "k3", "v3");
Besides the need to specify the actual types as class literals, it has the disadvantage of not supporting generic key or value types (as they can’t be expressed as Class
) and still having no compile-time safety, only a runtime check.
It’s worth looking at Java 9. There, you will be able to do:
Map<String, Integer> map1 = Map.of("k1", 1, "k2", 2);
Map<String, String> map2 = Map.of("k1", "v1", "k2", "v2", "k3", "v3");
This will create an immutable map of an unspecified type, rather than a HashMap
, but the interesting point is the API.
There is a method <K,V> Map.Entry<K,V> entry(K k, V v)
which can be combined with<K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
to create a map of a variable length (varargs are still limited to 255 parameters, though).
You can implement a similar thing:
public static <K,V> Map.Entry<K,V> entry(K k, V v) {
return new AbstractMap.SimpleImmutableEntry<>(k, v);
}
public static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries) {
return Arrays.stream(entries)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
The convenience method(s) of
are implemented the only way, this can be done with type safety: as overloaded methods with different numbers of arguments, like
public static <K,V> Map<K,V> of() {
return new HashMap<>();// or Collections.emptyMap() to create immutable maps
}
static <K,V> Map<K,V> of(K k1, V v1) {
return ofEntries(entry(k1, v1));
}
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2) {
return ofEntries(entry(k1, v1), entry(k2, v2));
}
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
return ofEntries(entry(k1, v1), entry(k2, v2), entry(k3, v3));
}
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
return ofEntries(entry(k1, v1), entry(k2, v2), entry(k3, v3), entry(k4, v4));
}
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {
return ofEntries(entry(k1, v1), entry(k2, v2), entry(k3, v3), entry(k4, v4));
}
(Java 9 makes the cut at ten mappings, if you have more, you have to use the ofEntries(entry(k1, v1), …)
variant).
If you follow this pattern, you should keep your toMap
name or use just map
, rather than calling at “of
”, as you are not writing the Map
interface.
These overloads might not look very elegant, but they solve all problems. You can write the code just as in your question, without specifying Class
objects, but gain compile-time type safety and even rejection of attempts to call it with an odd number of arguments.
You have to make a cut at a certain number of parameters, but, as already noted, even varargs do not support unlimited parameters. And the ofEntries(entry(…), …)
form isn’t so bad for larger maps.
The collector Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)
returns an unspecified map type, which might even be immutable (though it’s a HashMap
in the current version). If you want to have a guaranty that a HashMap
instance is returned, you have to use Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1,v2)->{throw new IllegalArgumentException("duplicate key");}, HashMap::new)
instead.