如何使用Java 8 Stream将数组转换为HashMap

13

我正在编写一个Java 8 Stream函数,用于将数组转换为Map。

这是我想要的:

public static <K, V> Map<K, V> toMap(Object... entries) {
    // Requirements:
    // entries must be K1, V1, K2, V2, .... ( even length )
    if (entries.length % 2 == 1) {
        throw new IllegalArgumentException("Invalid entries");
    }

    // TODO
    Arrays.stream(entries).????
}

有效用法

Map<String, Integer> map1 = toMap("k1", 1, "k2", 2);

Map<String, String> map2 = toMap("k1", "v1", "k2", "v2", "k3", "v3");

无效用法

Map<String, Integer> map1 = toMap("k1", 1, "k2", 2, "k3");

有什么需要帮忙的吗?

谢谢!


3
最好还是使用老式的 for 循环 :) - ZhongYu
2
你可能会在这里找到你要找的东西:从流中收集连续的一对 - MikaelF
将字符串数组转换为Map使用Java 8 Lambda表达式 - Yang Hailong
6个回答

14

您可以使用

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

但是它会给你一个(有根据的)未经检查警告。你的方法无法保证返回一个正确类型的Map<K, V>,即使你传入任意对象的数组,并且更糟糕的是,如果你传入错误类型的对象,它不会抛出异常,而是悄悄地返回一个不一致的映射。

一个更清晰、常用的解决方案是

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

这可以在没有警告的情况下编译,因为正确性将在运行时进行检查。调用代码必须进行适应:
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");

除了需要使用类字面量来指定实际类型之外,它的缺点是不支持通用的键或值类型(因为它们不能表示为Class),并且仍然没有编译时安全性,只有运行时检查。

值得关注Java 9。在那里,您将能够做到:

Map<String, Integer> map1 = Map.of("k1", 1, "k2", 2);
Map<String, String>  map2 = Map.of("k1", "v1", "k2", "v2", "k3", "v3");

这将创建一个未指定类型的不可变映射,而不是HashMap,但有趣的是API。
有一个方法<K,V> Map.Entry<K,V> entry(K k, V v),它可以与
<K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)结合使用,创建一个可变长度的映射(varargs仍然限制为255个参数)。
您可以实现类似的功能:
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));
}

方便的方法 of 只有一种实现方式,这可以通过类型安全实现:使用不同数量参数的重载方法,例如

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 将只保留前十个映射关系,您需要使用 ofEntries(entry(k1, v1), …) 变体。

如果您遵循这种模式,应该保留您的 toMap 名称或仅使用 map,而不是调用“of”,因为您没有编写 Map 接口。

这些重载可能看起来不太优雅,但它们解决了所有问题。您可以像在问题中那样编写代码,无需指定 Class 对象,但可以获得编译时类型安全性,甚至拒绝尝试使用奇数个参数调用它。

您必须在一定数量的参数处进行截断,但是,正如已经注意到的那样,即使是可变参数也不支持无限参数。对于更大的映射,ofEntries(entry(…), …) 形式并不那么糟糕。


收集器Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)返回一个未指定的地图类型,这甚至可能是不可变的(尽管在当前版本中它是HashMap)。如果您想要保证返回一个HashMap实例,您必须使用Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1,v2)->{throw new IllegalArgumentException("duplicate key");}, HashMap::new)


2

如果键类型与值类型不同,那么准确获取您想要的内容可能不起作用。这是因为Java的可变参数声明(Object... entries部分)仅支持一种类型。

有一些选项:

  1. 您可以动态执行检查,如果值不匹配,则抛出非法参数异常。但您将失去编译器类型检查。

  2. 您可以定义一个Pair类,并使用静态导入来实现几乎满足您的要求:

例如:

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

谢谢。我仍然更喜欢传递键、值而不是Pair。 - LHA

1

这是我的JDK 8流的想法:

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

如果您不介意使用第三方库abacus-common,则代码可以简化为:
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));
}

我认为最有效的方法是使用for循环,如果你不特别追求使用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 abacus-common.       
    // return N.asMap(entries);
}

0

你可以使用类似于映射字面量的东西。
为了实现这个,你可以使用一个工厂方法:

// Creates a map from a list of entries
@SafeVarargs
public static <K, V> Map<K, V> mapOf(Map.Entry<K, V>... entries) {
    LinkedHashMap<K, V> map = new LinkedHashMap<>();
    for (Map.Entry<K, V> entry : entries) {
        map.put(entry.getKey(), entry.getValue());
    }
    return map;
}

// Creates a map entry
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
    return new AbstractMap.SimpleEntry<>(key, value);
}

最后,你可以像下面这样做:

public static void main(String[] args) {
    Map<String, Integer> map = mapOf(entry("a", 1), entry("b", 2), entry("c", 3));
    System.out.println(map);
}

输出:

{a=1,b=2,c=3}

希望这能给你指明正确的方向。


谢谢。我仍然更喜欢传递键、值而不是Map.Entry。 - LHA

0
public static <K, V, E> Map<K, V> toMap(Function<E, K> toKey, Function<E, V> toValue, E[][] e){

        final Map<K, V> newMap = new HashMap<>();

        Arrays
                .stream(e, 0, e.length - 1)
                .forEach(s ->
                {
                    if (s[0] != null || s[1] != null)
                        newMap.put(toKey.apply(s[0]), toValue.apply(s[1]));
                }
                );

        return newMap;

}




public static void main(String[] args) {

        Object[][] objects = new Object[10][2];
        objects[0][0] ="Ahmet";
        objects[0][1] =28;
        objects[1][0] ="Mehmet";
        objects[1][1] =18;
        objects[2][0] ="Kemal";
        objects[2][1] =55;

Map<String, Integer> newMap = toMap((Object::toString), (Object v) -> Integer.parseInt(v.toString()), objects);

System.out.println(newMap.get("Ahmet") + " " + newMap.get("Kemal"));


}

0
我用listIterator解决了类似的问题 参数:[-arg1 val1 -arg2 val2 -arg3 -arg4 val4]
Map<String, String> argsMap = new HashMap<>();
        ListIterator<String> listIterator = Arrays.stream(args).toList().listIterator();
        while(listIterator.hasNext()) {
            String current = listIterator.next();;
            if (current.startsWith("-")) {
                if (listIterator.hasNext()) {
                    String next = listIterator.next();
                    if (!next.startsWith("-")) {
                        argsMap.put(current, next);
                    } else {
                        argsMap.put(current, "");
                        listIterator.previous();
                    }
                    
                }
            }
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接