2017-01-30 18 views
8

Piszę funkcję konwertowania tablicy do Map przy użyciu Java 8 Stream.Jak przekonwertować Array na HashMap za pomocą Java 8 Stream

Oto co chciałem

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).???? 
} 

Prawidłowe zwyczaje

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

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

Nieprawidłowe zwyczajów

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

Wszelkie pomaga?

Dzięki!

+3

lepiej po prostu użyj dobrej starej pętli 'for' :) – ZhongYu

+2

Możesz znaleźć to, czego szukasz: [Zbierz kolejne pary ze strumienia] (http://stackoverflow.com/questions/20470010/collect-successive -para-z-strumienia). – MikaelF

+0

Odsyłacz: https://stackoverflow.com/questions/31693781/convert-string-array-to-map-using-java-8-lambda-expressions –

Odpowiedz

6

Można użyć

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

ale to daje (założony) niezaznaczone ostrzeżenie. Twoja metoda nie może spełnić obietnicy, aby zwrócić prawidłowo wpisaną Map<K, V> dla tablicy dowolnych obiektów, a co gorsza, nie zawiedzie z wyjątkiem, ale po cichu zwróci niespójną mapę, jeśli przekażesz obiekty niewłaściwego typu.

Czystsze, powszechnie stosowane rozwiązanie jest

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

To może być skompilowany bez ostrzeżenia, a poprawność będzie sprawdzana przy starcie. Kod wywołujący musi być dostosowana:

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

Poza tym należy określić rzeczywiste typy jak literały klasy, ma tę wadę, że nie wspiera rodzajowe klucza lub wartości typów (jako że nie mogą być wyrażone jako Class) i wciąż nie ma bezpieczeństwa podczas kompilacji, tylko kontrola czasu wykonywania.


Warto looking at Java 9. Tam będzie można zrobić:

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

To stworzy niezmienny mapę typu nieokreślonego, zamiast HashMap, ale interesującym punktem jest API.

Istnieje sposób <K,V> Map.Entry<K,V> entry(K k, V v), które mogą być połączone z
<K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries) utworzyć mapę o zmiennej długości (varargs wciąż ograniczone do 255 parametrów, chociaż).

można zaimplementować coś podobnego:

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

Sposób wygoda (s) of realizowane są jedynym sposobem, można to zrobić z bezpieczeństwem typu: jak przeciążone metody z różną liczbą argumentów, jak

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 wykonuje cięcie na dziesięć mapowań, jeśli masz więcej, musisz użyć wariantu ofEntries(entry(k1, v1), …)).

Jeśli zastosujemy się do tego wzorca, należy zachować swoje nazwisko toMap lub używać tylko map zamiast -zawijającego „of”, a nie piszesz interfejs Map.

Te przeciążenia mogą nie wyglądać bardzo elegancko, ale rozwiązują wszystkie problemy. Możesz napisać kod tak jak w pytaniu, bez określania obiektów Class, ale zyskujesz na bezpieczeństwie typu kompilacji, a nawet odrzucenia prób wywołania go z nieparzystą liczbą argumentów.

Musisz dokonać cięcia przy określonej liczbie parametrów, ale, jak już wspomniano, nawet warianty nie obsługują nieograniczonych parametrów. A forma ofEntries(entry(…), …) nie jest taka zła w przypadku większych map.


Kolektor Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue) zwraca nieokreśloną typ mapy, która może być nawet niezmienne (choć jest to HashMap w bieżącej wersji). Jeśli chcesz mieć gwarancję, że instancja HashMap zostanie zwrócona, musisz zamiast tego użyć Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1,v2)->{throw new IllegalArgumentException("duplicate key");}, HashMap::new).

+0

Dziękujemy za bardzo szczegółowe rozwiązanie! – Loc

1

Oto mój pomysł przez JDK 8 strumienia:

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

Jeśli nie przeszkadza bibliotekę użycie innej firmy AbacusUtil, kod można uprościć do:

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

I myślę najskuteczniejszym sposobem na zrobienie tego jest pętla for, jeśli nie będziesz szczególnie dążyć do używania 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); 
} 
+0

@Holger: Dziękujemy za bardzo dobre rozwiązanie! Rewizja! – Loc

2

Uzyskiwanie dokładnie tego, czego chcesz, prawdopodobnie nie będzie działać w przypadku map, których typ klucza różni się od ich typu wartości. Wynika to z faktu, że deklaracja arytmetyczna Java (część Object... entries) obsługuje tylko jeden typ.

Niektóre opcje przychodzą na myśl:

  1. Można zrobić kontrole dynamicznie i rzucać nielegalną wyjątek argumentu, jeśli wartości nie pasują. Ale stracisz sprawdzanie typu kompilatora.

  2. Można zdefiniować klasę Pair i grać nieco z importem statycznym dostać niemal co chcesz:

np .:

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

Dziękuję. Nadal wolę przekazywanie kluczy, wartości zamiast parowania. – Loc

0

Można użyć czegoś podobnego map literałów.
Dla osiągnięcia to może użyć metody fabryki:

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

Wreszcie, można zrobić coś jak następuje:

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

wyjściowa:

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

Mam nadzieję, że da ci to właściwą drogę.

+0

Dziękuję. Nadal wolę przekazywanie kluczy, wartości zamiast Map.Entry. – Loc

Powiązane problemy