2016-02-18 19 views
19

Mam mapę Java, którą chcę przekształcić i przefiltrować. Jako trywialny przykład załóżmy, że chcę przekonwertować wszystkie wartości na liczby całkowite, a następnie usunąć nieparzyste wpisy.Przekształcanie i filtrowanie mapy Java ze strumieniami

Map<String, String> input = new HashMap<>(); 
input.put("a", "1234"); 
input.put("b", "2345"); 
input.put("c", "3456"); 
input.put("d", "4567"); 

Map<String, Integer> output = input.entrySet().stream() 
     .collect(Collectors.toMap(
       Map.Entry::getKey, 
       e -> Integer.parseInt(e.getValue()) 
     )) 
     .entrySet().stream() 
     .filter(e -> e.getValue() % 2 == 0) 
     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 


System.out.println(output.toString()); 

ta jest poprawna, a rentowności: {a=1234, c=3456}

Niestety, nie mogę pomóc, ale zastanawiam się, czy istnieje sposób, aby uniknąć wywoływania .entrySet().stream() dwukrotnie.

Czy jest sposób, w jaki mogę wykonać operacje transformacji i filtrowania i zadzwonić pod numer .collect() tylko raz na końcu?

+0

Nie sądzę, że to możliwe. Na podstawie javadoc "Strumień powinien być obsługiwany (wywołując operację pośrednią lub strumień końcowy) tylko jeden raz, co wyklucza na przykład" rozwidlone "strumienie, w których to samo źródło podaje dwa lub więcej potoków lub wiele przejazdów tego samego Strumień. Implementacja strumienia może rzucić IllegalStateException, jeśli wykryje, że strumień jest ponownie wykorzystywany. " – kosa

+0

@Namban To nie jest pytanie. – immibis

Odpowiedz

21

Tak, możesz odwzorować każdy wpis na inny tymczasowy wpis, w którym będzie przechowywany klucz i przeanalizowana liczba całkowita. Następnie możesz filtrować każdy wpis na podstawie jego wartości.

Map<String, Integer> output = 
    input.entrySet() 
     .stream() 
     .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue()))) 
     .filter(e -> e.getValue() % 2 == 0) 
     .collect(Collectors.toMap(
      Map.Entry::getKey, 
      Map.Entry::getValue 
     )); 

Zauważ, że użyłem Integer.valueOf zamiast parseInt ponieważ rzeczywiście chcemy pudełkowej int.


Jeśli masz luksus korzystania z biblioteki StreamEx, można to zrobić bardzo prosto:

Map<String, Integer> output = 
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap(); 
+1

Choć jest to jeden z możliwych sposobów na zjadliwość, wydaje mi się, że rezygnujemy z wydajności i czytelności dla kilku linii kodu, prawda? – kosa

+3

@Nambari Nie rozumiem, dlaczego jest to "hacky". To tylko filtr mapy. Jeśli jest to wyraźne użycie 'AbstractMap.SimpleEntry', możesz utworzyć kolejną' Pair', ale uważam, że jest to odpowiednie, ponieważ mamy już do czynienia z mapami. – Tunaki

+1

Użyłem "hacky" tylko ze względu na "tymczasowe wejście", które śledzimy, może nie być poprawnym terminem. Podobało mi się rozwiązanie StreamEx. – kosa

3

Można użyć metody Stream.collect(supplier, accumulator, combiner) przekształcać wpisy i warunkowo gromadzić je:

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new, 
    (m, e) -> Optional.ofNullable(e) 
      .map(Map.Entry::getValue) 
      .map(Integer::valueOf) 
      .filter(i -> i % 2 == 0) 
      .ifPresent(i -> m.put(e.getKey(), i)), 
    Map::putAll); 

System.out.println(even); // {a=1234, c=3456} 

Tutaj, wewnątrz akumulatora, używam metod Optional do zastosowania zarówno transformacji, jak i predykatu, a także opcjonalnego v alue jest nadal obecny, dodaję go do zebranej mapy.

+1

Bardzo blisko [mojego pierwszego wariantu] (http://stackoverflow.com/a/35490546/2711488), ale nie sądzę, że użycie opcji "Opcjonalnie" jest tutaj wygraną ... – Holger

+0

@Holer to po prostu wszystko w jednej linii. W każdym razie, nie widziałem twojej odpowiedzi. Wygrana (jeśli staniemy się bardzo subtelną), może polegać na tym, że 'Opcjonalna.zakładka()' pozwala na puste klucze. –

+1

Pisaliśmy w tym samym czasie. Łańcuch 'Opcjonalny' w rzeczywistości nie jest jedną linią (lub bardzo dużą), ale pojedynczym wyrażeniem. Ale zaczynając od pewnego rozmiaru wyrażenia, dwa nawiasy klamrowe potrzebne do stwierdzenia lambda nie wydają się już tak kosztowne ... – Holger

3

Innym sposobem, aby to zrobić, to usunąć wartości, których nie chcesz z przekształconej Map:

Map<String, Integer> output = input.entrySet().stream() 
     .collect(Collectors.toMap(
       Map.Entry::getKey, 
       e -> Integer.parseInt(e.getValue()), 
       (a, b) -> { throw new AssertionError(); }, 
       HashMap::new 
     )); 
output.values().removeIf(v -> v % 2 != 0); 

Zakłada chcesz zmienny Map jako wynik, jeśli nie prawdopodobnie można utworzyć niezmienna jeden z output.


Jeśli przekształcenia wartości do tego samego typu i chcesz zmodyfikować Map w miejscu, to może być dużo krótsza z replaceAll:

input.replaceAll((k, v) -> v + " example"); 
input.values().removeIf(v -> v.length() > 10); 

zakłada to również input jest zmienny.


Nie polecam ten sposób, ponieważ nie będzie działać na wszystkich ważnych Map wdrożeń i może przestać działać na HashMap w przyszłości, ale obecnie można używać replaceAll i rzucić HashMap zmienić typ wartości:

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v)); 
Map<String, Integer> output = (Map)input; 
output.values().removeIf(v -> v % 2 != 0); 

będzie to również dać wpisać zasadami bezpieczeństwa i jeśli starają się odzyskać wartość z Map poprzez odniesienie starego typu jak ten:

String ex = input.get("a"); 

Spowoduje to wyświetlenie ClassCastException.


Można przesunąć pierwszy przekształcić część do metody, aby uniknąć boilerplate jeśli można oczekiwać, aby go używać dużo:

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
     Map<? extends K, ? extends VO> old, 
     Function<? super VO, ? extends VN> f, 
     Supplier<? extends M> mapFactory){ 
    return old.entrySet().stream().collect(Collectors.toMap(
      Entry::getKey, 
      e -> f.apply(e.getValue()), 
      (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); }, 
      mapFactory)); 
} 

i używać go tak:

Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new); 
    output.values().removeIf(v -> v % 2 != 0); 

Uwaga że wyjątek klucza duplikatu może zostać zgłoszony, jeśli na przykład oldMap jest IdentityHashMap i mapFactory tworzy HashMap.

+1

Twój komunikat "Duplicate keys" + a + "" + b' jest mylący: 'a' i' b' są w rzeczywistości wartościami, a nie kluczami. –

+0

@TagirValeev tak, zauważyłem, że tak, ale to jest tak samo jak w 'Collectors' dla dwóch wersji argumentów' toMap' i zmiana tego na to, co myślałem, umieści pasek przewijania na polu kodu, więc zdecydowałem się opuścić to. Zmienię to teraz, ponieważ myślisz, że to wprowadzanie w błąd. – Alex

+1

Tak, jest to znany problem, który został już naprawiony w [Java-9] (https://bugs.openjdk.java.net/browse/JDK-8040892) (ale nie został przeniesiony do wersji Java-8). Java-9 wyświetla klucz kolizyjny, a także obie wartości w komunikacie wyjątku. –

11

Jednym ze sposobów rozwiązania problemu z dużo mniejszym obciążeniem jest przesunięcie mapowania i filtrowanie do kolektora.

Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new, 
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); }, 
    Map::putAll); 

nie wymaga tworzenia pośrednich Map.Entry przypadkach, a nawet lepiej, będzie odłożyć boks z int wartości do momentu, gdy wartości te są faktycznie dodanej do Map, co oznacza, że ​​wartości odrzucone przez filtr są w ogóle nie zapakowane.

W porównaniu do tego, co robi Collectors.toMap(…), operacja jest również uproszczona przez użycie Map.put zamiast Map.merge jak wiemy z góry, że nie mamy do obsługi kluczowych kolizji tutaj.

Jednak tak długo, jak nie chcesz wykorzystać wykonanie równoległego można również rozważyć zwykłą pętlę

HashMap<String,Integer> output=new HashMap<>(); 
for(Map.Entry<String, String> e: input.entrySet()) { 
    int i = Integer.parseInt(e.getValue()); 
    if(i%2==0) output.put(e.getKey(), i); 
} 

lub wewnętrzny wariant iteracji:

HashMap<String,Integer> output=new HashMap<>(); 
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); }); 

ten ostatni jest dość zwarta i co najmniej na równi ze wszystkimi innymi wariantami dotyczącymi wydajności z pojedynczym gwintem.

+0

Przegłosowałem dla twojej rekomendacji zwykłej pętli. Tylko dlatego, że możesz używać strumieni, nie znaczy, że powinieneś. –

+2

@Jeffrey Bosboom: tak, stara, dobra pętla "for" wciąż żyje. Chociaż w przypadku map, lubię używać metody 'Map.forEach' dla mniejszych pętli tylko dlatego, że' (k, v) -> 'jest o wiele ładniejsze niż deklarowanie zmiennych' Map.Entry 'i ewentualnie kolejne dwie zmienne dla rzeczywistego klucza i wartości ... – Holger

1

Guava jest twoim przyjacielem:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0); 

Należy pamiętać, że output jest przekształcona, sączy widok od input. Musisz wykonać kopię, jeśli chcesz działać na nich niezależnie.

0

Oto kod przez AbacusUtil

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567"); 

Map<String, Integer> output = Stream.of(input) 
          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue())) 
          .filter(e -> e.getValue() % 2 == 0) 
          .toMap(Map.Entry::getKey, Map.Entry::getValue); 

N.println(output.toString()); 

Deklaracji: Jestem deweloperem AbacusUtil.

Powiązane problemy