2015-12-04 4 views
15

Lassen Sie uns sagen, dass wir diesen Strom habenJava Stream: Gibt es eine Möglichkeit, iterativ zwei Elemente gleichzeitig zu verwenden?

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j"); 

und ich möchte, dass die Paare von benachbarten Saiten in einer Karte speichern, in der die erste beginnt mit „irren“.

Was ich dachte, der ist so etwas wie dies

Map<String, String> map = new HashMap<>(); 

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j") 
.reduce((acc, next) -> { 
    if (acc.startsWith("err")) 
     map.put(acc,next); 
    if (next.startsWith("err")) 
     return next; 
    else 
     return ""; 
}); 

Aber ich bin nicht ganz zufrieden mit ihm aus zwei Gründen

  1. ich „missbräuchlich“ reduce Funktion. In der Stream API hat jede Funktion ihren klaren, wohldefinierten Zweck: max soll den maximalen Wert berechnen, filter soll basierend auf einer Bedingung filtern, reduce soll einen inkrementell akkumulierten Wert erzeugen und so weiter.
  2. Das hindert mich daran, Streams mächtige Mechanismen zu verwenden: Was, wenn ich meine Suche auf die ersten beiden Ergebnisse beschränken wollte?

Hier verwendete ich reduce weil (soweit ich weiß) es ist die einzige Funktion ist, die Sie vergleichen paar Werte können, die Sie irgendwie zu etwas führen wieder ähnlich wie bei „Stromwert“ und „nächste Wert“ Konzepte.

Gibt es einen einfacheren Weg? Etwas, das es Ihnen ermöglicht, den Stream zu iterieren, wobei für jede Iteration mehr als ein Wert berücksichtigt wird?

EDIT

Was ich denke, ist ein Mechanismus, der das aktuelle Element gegeben, Sie ein „Fenster der Elemente“ definieren können für jede Iteration zu berücksichtigen.

So etwas wie

<R> Stream<R> mapMoreThanOne(
    int elementsBeforeCurrent, 
    int elementsAfterCurrent, 
    Function<List<? super T>, ? extends R> mapper); 

statt

<R> Stream<R> map(Function<? super T, ? extends R> mapper); 

, die eine leistungsstarke "Upgrade" zu aktuellen API wäre.

EDIT2

Ich schätze die Bemühungen der Menschen, ihre Lösung vorschlagen, aber das Problem ist nicht, den Algorithmus per se. Es gibt verschiedene Möglichkeiten, um mein Ziel zu erreichen, indem ich Streams, Indizes und temporäre Variablen zusammenfasse, um vorherige Werte zu speichern ... aber ich habe mich gefragt, ob es eine Methode in der Stream-API gibt, die für den Umgang mit anderen Elementen als dem aktuellen entwickelt wurde ohne das "Strom-Paradigma" zu brechen. So etwas wie dieses

List<String> list = 
     Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j") 
     .filterFunctionImWonderingIfExist(/*filters couples of elements*/) 
     .limit(2) 
     .collect(Collectors.toList()); 

die Antworten gegeben, ich glaube, es gibt keine „klare und schnelle“ Lösung, es sei denn mit StreamEx Bibliothek

+1

Sieht so aus, als ob Sie einen Collector brauchen – Bohemian

+0

Ich dachte darüber nach, aber ich konnte keine (nette und saubere) Lösung finden –

Antwort

9

Sie können einen benutzerdefinierten Collector für diese Aufgabe erstellen.

Map<String, String> map = 
    Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j") 
      .collect(MappingErrors.collector()); 

mit:

private static final class MappingErrors { 

    private Map<String, String> map = new HashMap<>(); 

    private String first, second; 

    public void accept(String str) { 
     first = second; 
     second = str; 
     if (first != null && first.startsWith("err")) { 
      map.put(first, second); 
     } 
    } 

    public MappingErrors combine(MappingErrors other) { 
     throw new UnsupportedOperationException("Parallel Stream not supported"); 
    } 

    public Map<String, String> finish() { 
     return map; 
    } 

    public static Collector<String, ?, Map<String, String>> collector() { 
     return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish); 
    } 

} 

In diesem Kollektor sind zwei Laufelemente gehalten. Jedes Mal, wenn eine String akzeptiert wird, werden sie aktualisiert, und wenn die erste mit "err" beginnt, werden die zwei Elemente zu einer Karte hinzugefügt.


Eine andere Lösung ist es, die StreamEx Bibliothek zu verwenden, die eine pairMap Verfahren liefert, das eine gegebene Funktion auf die jedem benachbarten Paar von Elementen dieses Stroms gilt. Im folgenden Code gibt die Operation ein String-Array zurück, das aus dem ersten und zweiten Element des Paares besteht, wenn das erste Element andernfalls mit "err", null beginnt. null Elemente werden dann herausgefiltert und der Stream wird in einer Karte gesammelt.

Map<String, String> map = 
    StreamEx.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j") 
      .pairMap((s1, s2) -> s1.startsWith("err") ? new String[] { s1, s2 } : null) 
      .nonNull() 
      .toMap(a -> a[0], a -> a[1]); 

System.out.println(map); 
+0

Also, nun, es gibt keinen einfacheren Weg. +1 für StreamEx-Bibliothek, sehr interessantes Projekt! 'PairMap' passt perfekt auf meine Bedürfnisse –

+1

@LuigiCortese Ich denke nicht, nein. Das ist bedauerlich. Scala hat eine schöne ['sliding'] (http://www.scala-lang.org/api/current/index.html#[email protected]%28size:Int,step:Int%29:Iterator [ Repr]) Methode für Stream, die hier angebracht wäre: Sie gruppiert Elemente in Blöcken fester Größe. – Tunaki

+0

Ich habe davon gehört. IMHO, das ist ein großes, tiefes Loch dort in der Mitte.Es könnte viel mehr synthetisch und leicht verständlich sein –

5

Sie können einen benutzerdefinierten Sammler schreiben, oder verwenden Sie den viel einfacheren Ansatz über die von Streaming Liste der Indizes:

Map<String, String> result = IntStream.range(0, data.size() - 1) 
     .filter(i -> data.get(i).startsWith("err")) 
     .boxed() 
     .collect(toMap(data::get, i -> data.get(i+1))); 

Dies setzt voraus, dass Ihre Daten in einem Direktzugriffsfreundliche Liste ist oder dass sie vorübergehend in einem Dump kann.

Wenn Sie nicht zufällig auf die Daten zugreifen können oder es in eine Liste oder ein Array für die Verarbeitung laden, können Sie immer einen benutzerdefinierten pairing Sammler, so dass Sie

Map<String, String> result = data.stream() 
     .collect(pairing(
       (a, b) -> a.startsWith("err"), 
       AbstractMap.SimpleImmutableEntry::new, 
       toMap(Map.Entry::getKey, Map.Entry::getValue) 
     )); 

Hier sind die Quelle für den Sammler schreiben können. Es ist parallel-freundlich und könnte in anderen Situationen nützlich sein:

public static <T, V, A, R> Collector<T, ?, R> pairing(BiPredicate<T, T> filter, BiFunction<T, T, V> map, Collector<? super V, A, R> downstream) { 

    class Pairing { 
     T left, right; 
     A middle = downstream.supplier().get(); 
     boolean empty = true; 

     void add(T t) { 
      if (empty) { 
       left = t; 
       empty = false; 
      } else if (filter.test(right, t)) { 
       downstream.accumulator().accept(middle, map.apply(right, t)); 
      } 
      right = t; 
     } 

     Pairing combine(Pairing other) { 
      if (!other.empty) { 
       this.add(other.left); 
       this.middle = downstream.combiner().apply(this.middle, other.middle); 
       this.right = other.right; 
      } 
      return this; 
     } 

     R finish() { 
      return downstream.finisher().apply(middle); 
     } 
    } 

    return Collector.of(Pairing::new, Pairing::add, Pairing::combine, Pairing::finish); 
} 
+0

Nun, im ursprünglichen Problem Source Stream ist eine Protokolldatei, so dass Sie nicht wirklich Zugriff haben zu Indizes. –

4

Die Dinge wären einfacher, wenn sich Ihre Eingabe in der Direktzugriffsliste befindet. Auf diese Weise können gute alte List.subList Methode wie folgt nutzen können:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
    "f", "g", "h", "err3", "i", "j"); 

Map<String, String> map = IntStream.range(0, list.size()-1) 
    .mapToObj(i -> list.subList(i, i+2)) 
    .filter(l -> l.get(0).startsWith("err")) 
    .collect(Collectors.toMap(l -> l.get(0), l -> l.get(1))); 

Dasselbe könnte mit bereits erwähnt StreamEx Bibliothek (geschrieben von mir) in etwas kürzer Art und Weise erfolgen:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
    "f", "g", "h", "err3", "i", "j"); 

Map<String, String> map = StreamEx.ofSubLists(list, 2, 1) 
    .mapToEntry(l -> l.get(0), l -> l.get(1)) 
    .filterKeys(key -> key.startsWith("err")) 
    .toMap(); 

Obwohl Wenn Sie keine Abhängigkeit von Drittanbietern wünschen, sieht die schlechte Stream API-Lösung auch nicht sehr schlecht aus.

+1

Ich mag die Idee hinter StreamEx, ich hoffe, mit einigen interessanten PR bald =) –

+0

@LuigiCortese kommen, bitte beachten Sie jedoch, dass ich nicht eifrig jede einzelne Erweiterung akzeptieren. Jede Verbesserung hat viele Vor- und Nachteile zu evaluieren. Zum Beispiel werde ich wahrscheinlich jede neue Funktion ablehnen, die eine schlechte parallele Leistung zeigt. Wahrscheinlich wäre es besser, deine Idee zuerst im [issue tracker] zu diskutieren (https://github.com/amaembo/streamex/issues) –

-1

Hier ist ein einfacher Einzeiler mit einem off-the-shelf Sammlern:

Stream<String> stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j"); 

Map<String, String> map = Arrays.stream(stream 
     .collect(Collectors.joining(",")).split(",(?=(([^,]*,){2})*[^,]*$)")) 
    .filter(s -> s.startsWith("err")) 
    .map(s -> s.split(",")) 
    .collect(Collectors.toMap(a -> a[0], a -> a[1])); 

Der „Trick“ hier ist es, zunächst alle Begriffe verbindet sie zu einem einzigen String, spaltete es dann in Strings von Paaren, zB "a,b", "err1,c", usw. Sobald Sie einen Strom von Paaren haben, ist die Verarbeitung unkompliziert.

Verwandte Themen