2016-08-16 2 views
3

Angenommen, eine Klasse MyClass:Stream: auf Kinder filtern, gibt die Eltern

public class MyClass { 

    private final Integer myId; 
    private final String myCSVListOfThings; 

    public MyClass(Integer myId, String myCSVListOfThings) { 
     this.myId = myId; 
     this.myCSVListOfThings = myCSVListOfThings; 
    } 

    // Getters, Setters, etc 
} 

Und diesen Stream:

final Stream<MyClass> streamOfObjects = Stream.of(
     new MyClass(1, "thing1;thing2;thing3"), 
     new MyClass(2, "thing2;thing3;thing4"), 
     new MyClass(3, "thingX;thingY;thingZ")); 

Ich mag jede Instanz MyClass zurückzukehren, die einen Eintrag "thing2" in myCSVListOfThings enthalten .

Wenn ich wollte ein List<String>myCSVListOfThings mit diesem leicht getan werden könnte:

List<String> filteredThings = streamOfObjects 
     .flatMap(o -> Arrays.stream(o.getMyCSVListOfThings().split(";"))) 
     .filter("thing2"::equals) 
     .collect(Collectors.toList()); 

Aber was ich wirklich brauchen, ist ein List<MyClass>.

Das ist, was ich jetzt haben:

List<MyClass> filteredClasses = streamOfObjects.filter(o -> { 
    Stream<String> things = Arrays.stream(o.getMyCSVListOfThings().split(";")); 
    return things.anyMatch(s -> s.equals("thing2")); 
}).collect(Collectors.toList()); 

Aber irgendwie funktioniert es nicht richtig an. Jede sauberere Lösung als das Öffnen einer neuen Stream innerhalb einer Predicate?

Antwort

7

Zunächst empfehle ich Ihnen zusätzliche Methode MyClasspublic boolean containsThing(String str), hinzufügen, damit Sie können Sie wie dieser Code umwandeln:

List<MyClass> filteredClasses = streamOfObjects 
    .filter(o -> o.containsThing("thing2")) 
    .collect(Collectors.toList()); 

Jetzt können Sie diese Methode implementieren können, wie Sie wollen, hängt von Eingangsdaten: Aufspaltung in Stream , spalten in Set, sogar Suche von substring (wenn es möglich ist und Sinn hat), Caching Ergebnis, wenn Sie brauchen.

Sie wissen viel mehr über die Verwendung dieser Klasse, damit Sie die richtige Wahl treffen können.

+0

Sieht sauberer aus. Am Ende wird "containsThing" in meinem Fall wahrscheinlich immer noch etwas wie der Code in der Frage sein, aber zumindest wird die Trennung von Bedenken erzwungen. Wie auch immer, sind Sie sich einer generischen Technik bewusst, um diese Art von Szenario zu vereinfachen? Angenommen, ich habe einen 'Stream' von' MyClass1', der eine Liste von 'MyClass2' enthält, die eine Liste von' MyClass3' enthält, die eine 'String' mit Dingen enthält. Wenn ich basierend auf dem String der Dinge filtern und eine 'List ' zurückgeben wollte, wie würdest du es tun? –

+0

In der Tat. Ihre ursprüngliche Lösung ist zu eng gekoppelt. Das Hinzufügen einer Inspektions-Methode ('contains()') entkoppelt die Oberfläche (äußere Erscheinung) von den inneren Details der 'MyClass'. –

+0

Es ist besser, 'filter (o -> o.containsThing (" thing2 "))' zu 'filter (o -> o.containsThing ("; thing2; "))' 'zu ändern. – walsh

1

Wie ich sehe, haben Sie drei Möglichkeiten.

1) suchen bestimmten Eintrag im String ohne es zu spliting - immer noch chaotisch aussieht

List<MyClass> filteredClasses = streamOfObjects 
       .filter(o -> o.getMyCSVListOfThings().contains(";thing2;")) 
       .collect(Collectors.toList()); 

2) Karte zweimal - nach wie vor chaotisch

List<MyClass> filteredClasses = streamOfObjects 
       .map(o -> Pair<MyClass, List<String>>.of(o, toList(o.getMyCSVListOfThings())) 
       .filter(pair -> pair.getRight().contains("thing2")) 
       .map(pair -> pair.getLeft()) 
       .collect(Collectors.toList()); 

wo ToList eine Methode, den String konvertiert zur Liste

3) zusätzliches Feld erstellen - Methode würde ich vorschlagen

Extend Klasse MyClass - fügen Feld der Klasse

List<String> values; 

Und es im Konstruktor initialisieren:

public MyClass(Integer myId, String myCSVListOfThings) { 
    this.myId = myId; 
    this.myCSVListOfThings = myCSVListOfThings; 
    this.values = toList(myCSVListOfThings); 
} 

Und dann in den Strom einfach:

List<MyClass> filteredClasses = streamOfObjects 
      .filter(o -> o.getValues().contains("thing2")) 
      .collect(Collectors.toList()); 

Natürlich Feldwerte können während des ersten getValues-Methodenaufrufs im LAZY-Modus initialisiert werden, wenn Sie möchten.

+0

'; thing2;' kann in einigen Fällen fehlschlagen (z. B. 'ding2' ist die erste oder letzte Zeichenfolge), aber ich habe Ihren Standpunkt verstanden. Andere zwei Optionen funktionieren gut und sogar Option 1 ist mit einigen Regex-Magie möglich. Wie auch immer, können Sie einen Blick auf meine [Kommentar] (http://stackoverflow.com/questions/38973410/stream-filter-on-children-return-the-parent/38973554#comment65300747_38973554) oben? Ich mache mir Sorgen darüber, wie diese Techniken in einem realen Szenario skalieren, in dem die zu filternden Informationen unter dem Objekt, das ich zurückgeben möchte, mehrere Objekte verschachteln können (z. B. Urenkel). –

+1

Sind Sie mit Besucher- und Strategieprofil vertraut? Um eine generische Lösung zu erstellen, würde ich beides kombinieren. Basierend auf dem, was Sie im Objekt und in der Klasse des Startobjekts (Strategie) suchen, würde ich ein anderes Besucherobjekt erstellen, das ich dann als Argument an Ihre Klassenmethode übergeben würde. Besucher würde feststellen, ob Ihr Objekt die Kriterien erfüllt. Innerhalb des Besucher-Objekts können Sie die ganze Logik der Überprüfung der Objekte Ihrer Klassen und ihrer Kinder/Enkel usw. einschließen. – jchmiel

2

Eine Lösung ist es, eine Musterübereinstimmung zu verwenden, die den Split-und-Stream-Betrieb vermeidet:

Pattern p=Pattern.compile("(^|;)thing2($|;)"); 
List<MyClass> filteredClasses = streamOfObjects 
    .filter(o -> p.matcher(o.getMyCSVListOfThings()).find()) 
    .collect(Collectors.toList()); 

Da das Argument String.split als RegexMuster definiert ist, hat das Muster über die gleiche semantischen wie Suche ein Treffer innerhalb des Ergebnisses split; Sie suchen nach dem Wort thing2 zwischen zwei Grenzen, die erste ist entweder der Anfang der Zeile oder ein Semikolon, die zweite ist entweder das Ende der Zeile oder ein Semikolon.

Außerdem ist nichts falsch daran, eine andere Stream-Operation innerhalb eines Prädikats zu verwenden. Aber es gibt einige Möglichkeiten, es zu verbessern. Der Lambda-Ausdruck wird prägnanter, wenn Sie die veraltete lokale Variable, die den Stream enthält, weglassen. Im Allgemeinen sollten Sie es vermeiden, Stream-Instanzen in lokalen Variablen zu belassen, da die Verkettung der Operationen das Risiko, einen Stream mehr als einmal zu verwenden, reduziert. Zweitens können Sie die Pattern Klasse verwenden, um die resultierenden Elemente eines split Betrieb zu streamen, ohne sie alle in ein Array zu sammeln:

Pattern p=Pattern.compile(";"); 
List<MyClass> filteredClasses = streamOfObjects 
    .filter(o -> p.splitAsStream(o.getMyCSVListOfThings()).anyMatch("thing2"::equals)) 
    .collect(Collectors.toList()); 

oder

Pattern p=Pattern.compile(";"); 
List<MyClass> filteredClasses = streamOfObjects 
    .filter(o -> p.splitAsStream(o.getMyCSVListOfThings()).anyMatch(s->s.equals("thing2"))) 
    .collect(Collectors.toList()); 

Beachten Sie, dass auch Ihre ursprünglichen umschreiben könnte Code zu

Jetzt ist die Operation innerhalb des Prädikats kein Stream, sondern eine Collection-Operation, aber das ändert sich nicht e die Semantik noch die Korrektheit des Codes ...

1

Dies ist ähnlich dem Problem, Getting only required objects from a list using Java 8 Streams, ein Jahr zuvor veröffentlicht. Ich denke, dass die Lösung, die ich dort gelassen habe, hier anwendbar ist.

Es gibt eine Bibliothek namens com.coopstools.cachemonads. Es erweitert die Klassen Java-Stream (und Optional), um das Zwischenspeichern von Entitäten für die spätere Verwendung zu ermöglichen.

List<Parent> goodParents = CacheStream.of(parents) 
      .cache() 
      .map(Parent::getChildren) 
      .flatMap(Collection::stream) 
      .map(Child::getAttrib1) 
      .filter(att -> att > 10) 
      .load() 
      .distinct() 
      .collect(Collectors.toList()); 

wo die Eltern ein Array oder Strom ist:

Die Lösung kann mit finden.

Aus Gründen der Klarheit speichert die Cache-Methode die Eltern; und die Lademethode zieht die Eltern zurück. Und wenn ein Elternteil keine Kinder hat, wird nach der ersten Karte ein Filter benötigt, um die Nulllisten zu entfernen.

Insbesondere für Ihr Problem:

List<Parent> goodParents = CacheStream.of(streamOfObjects) 
    .cache() 
    .map(o -> o.getMyCSVListOfThings().split(";")) 
    .flatMap(Collection::stream) 
    .filter("thing2"::equals) 
    .load() 
    .collect(Collectors.toList()) 

Diese Bibliothek kann in jeder Situation verwendet werden, in denen Operationen an Kindern durchgeführt werden müssen, einschließlich der Karte/Art/Filter/etc, aber wo eine ältere Person ist immernoch gebraucht. Es kann mehr Linien als einige der anderen Antworten geben, aber jede Linie ist sehr sauber und geradlinig.

Bitte lassen Sie mich wissen, wenn diese Antwort hilfreich ist.

<dependency> 
    <groupId>com.coopstools</groupId> 
    <artifactId>cachemonads</artifactId> 
    <version>0.2.0</version> 
</dependency> 

(oder, gradle, com.coopstools: cachemonads: 0.2

Der Code kann auf https://github.com/coopstools/cachemonads oder kann heruntergeladen werden von Maven zu finden.0)

Verwandte Themen