Как преобразовать массив в HashMap с помощью Java 8 Stream

Я пишу функцию для преобразования массива в карту с помощью Java 8 Stream.

вот что я хотел

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

помогает?

спасибо!

4 ответов


вы можете использовать

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

но это даст вам (основана) unchecked предупреждение. Ваш метод не может содержать обещание вернуть правильно набранный 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 реализованы единственным способом, это можно сделать с помощью типа safety: as overloaded методы с различным количеством аргументов, например

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 объекты, но получить безопасность типа времени компиляции и даже отклонение попыток вызвать его с нечетным числом аргументов.

вы должны сделать разрез при определенном количестве параметров, но, как уже отмечалось, даже varargs не поддерживают неограниченные параметры. И ofEntries(entry(…), …) форма не так уж и плохо для больших карт.


сборщик Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue) возвращает неопределенный тип карты, который может быть даже неизменяемым (хотя это HashMapв текущей версии). Если вы хотите иметь гарантию, что HashMap экземпляр возвращается, вы должны использовать .


получение именно того, что вы хотите, вероятно, не будет работать для карт, тип ключа которых отличается от их типа значения. Это потому, что объявление переменной arity Java (Object... entries part) поддерживает только один тип.

какие варианты приходят на ум:

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

вот моя идея от 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;
    //    });
}

Если вы не возражаете использовать стороннюю библиотеку AbacusUtil, код может быть упрощен до:

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 AbacusUtil.       
    // return N.asMap(entries);
}

Вы можете использовать что-то вроде карты литералы.
Для достижения этого вы можете использовать фабричный метод:

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

Я надеюсь, что это дает вам правильный путь.