2015-05-13 2 views
17

Es ist mir gelungen, eine Lösung mit Java 8 Streams API zu schreiben, die zuerst eine Liste von Objekt Route nach ihrem Wert gruppiert und dann die Anzahl der Objekte zählt Gruppe. Es wird ein Mapping Route -> Long zurückgegeben. Hier ist der Code:Gruppierung nach Objektwert, Zählung und dann Einstellung des Gruppenschlüssels nach Objektattribut

Map<Route, Long> routesCounted = routes.stream() 
       .collect(Collectors.groupingBy(gr -> gr, Collectors.counting())); 

und die Route Klasse:

public class Route implements Comparable<Route> { 
    private long lastUpdated; 
    private Cell startCell; 
    private Cell endCell; 
    private int dropOffSize; 

    public Route(Cell startCell, Cell endCell, long lastUpdated) { 
     this.startCell = startCell; 
     this.endCell = endCell; 
     this.lastUpdated = lastUpdated; 
    } 

    public long getLastUpdated() { 
     return this.lastUpdated; 
    } 

    public void setLastUpdated(long lastUpdated) { 
     this.lastUpdated = lastUpdated; 
    } 

    public Cell getStartCell() { 
     return startCell; 
    } 

    public void setStartCell(Cell startCell) { 
     this.startCell = startCell; 
    } 

    public Cell getEndCell() { 
     return endCell; 
    } 

    public void setEndCell(Cell endCell) { 
     this.endCell = endCell; 
    } 

    public int getDropOffSize() { 
     return this.dropOffSize; 
    } 

    public void setDropOffSize(int dropOffSize) { 
     this.dropOffSize = dropOffSize; 
    } 

    @Override 
    /** 
    * Compute hash code by using Apache Commons Lang HashCodeBuilder. 
    */ 
    public int hashCode() { 
     return new HashCodeBuilder(43, 59) 
       .append(this.startCell) 
       .append(this.endCell) 
       .toHashCode(); 
    } 

    @Override 
    /** 
    * Compute equals by using Apache Commons Lang EqualsBuilder. 
    */ 
    public boolean equals(Object obj) { 
     if (!(obj instanceof Route)) 
      return false; 
     if (obj == this) 
      return true; 

     Route route = (Route) obj; 
     return new EqualsBuilder() 
       .append(this.startCell, route.startCell) 
       .append(this.endCell, route.endCell) 
       .isEquals(); 
    } 

    @Override 
    public int compareTo(Route route) { 
     if (this.dropOffSize < route.dropOffSize) 
      return -1; 
     else if (this.dropOffSize > route.dropOffSize) 
      return 1; 
     else { 
       // if contains drop off timestamps, order by last timestamp in drop off 
       // the highest timestamp has preceding 
      if (this.lastUpdated < route.lastUpdated) 
       return -1; 
      else if (this.lastUpdated > route.lastUpdated) 
       return 1; 
      else 
       return 0; 
     } 
    } 
} 

Was Ich mag würde zusätzlich zu erreichen, ist, dass der Schlüssel für jede Gruppe die mit dem größten Wert Lastupdated wäre. Ich schaute bereits auf this solution, aber ich weiß nicht, wie man die Zählung und die Gruppierung nach Wert und Route maximum lastUpdated kombiniert. Hier ist die Beispieldaten von dem, was ich erreichen möchte:

Beispiel:

List<Route> routes = new ArrayList<>(); 
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1200L)); 
routes.add(new Route(new Cell(3, 2), new Cell(2, 5), 1800L)); 
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1700L)); 

SOLLTEN zu konvertierenden:

Map<Route, Long> routesCounted = new HashMap<>(); 
routesCounted.put(new Route(new Cell(1, 2), new Cell(2, 1), 1700L), 2); 
routesCounted.put(new Route(new Cell(3, 2), new Cell(2, 5), 1800L), 1); 

Beachten Sie, dass der Schlüssel für die Zuordnung, die gezählt 2 Routen ist die mit dem größten Wert für die letzte Aktualisierung.

+1

In Beispiel Sie verwenden 'neu Route 'mit 3 Parametern, während der einzige Konstruktor 4 Parameter hat. Könnten Sie das bitte korrigieren? –

+0

Ups mein schlechtes. Es ist jetzt behoben. Grundsätzlich spielt die DropOffSize-Größe hier keine Rolle, aber ich habe sie im Code belassen, weil ich alle überschriebenen Methoden anzeigen wollte und die compareTo-Methode dropOffSize verwendet. –

Antwort

7

Hier ist ein Ansatz. Erste Gruppe in Listen und verarbeitet dann die Listen in die Werte, die Sie eigentlich wollen:

import static java.util.Comparator.comparingLong; 
import static java.util.stream.Collectors.groupingBy; 
import static java.util.stream.Collectors.toMap; 


Map<Route,Integer> routeCounts = routes.stream() 
     .collect(groupingBy(x -> x)) 
     .values().stream() 
     .collect(toMap(
      lst -> lst.stream().max(comparingLong(Route::getLastUpdated)).get(), 
      List::size 
     )); 
+0

Ich denke, diese Lösung ist leistungsschwer, weil sie Zwischenlisten erstellt. Wäre es möglich, das zu umgehen? Außerdem bekomme ich die folgenden zwei Beschwerden vom Compiler: "Methode kann nicht aufgelöst werden stream()" und "Methode kann nicht aufgelöst werden size()" –

+1

Wie groß ist Ihre Liste von Routen? Sie könnten dies sicherlich auf eine leistungsfähigere Weise tun, aber auf Kosten der Einfachheit. Wie für Compiler-Fehler, verwenden Sie Eclipse? Ich habe gerade versucht es mit jdk1.8.0_25 und es kompiliert gut. – Misha

+0

Ich verwende IntelliJ IDEA 14.1. Die Liste der Routen ändert sich in Bezug auf die Zeit. Es ist eigentlich keine Liste, sondern ein ArrayDeque, da ich bewegendes Fenster simuliere. Die Größe sollte ungefähr 1e4 bis 2e4 sein, denke ich. –

2

Changed equals und hashcode abhängig zu sein, nur auf Startzelle und Zielzelle.

@Override 
    public boolean equals(Object o) { 
     if (this == o) return true; 
     if (o == null || getClass() != o.getClass()) return false; 

     Cell cell = (Cell) o; 

     if (a != cell.a) return false; 
     if (b != cell.b) return false; 

     return true; 
    } 

    @Override 
    public int hashCode() { 
     int result = a; 
     result = 31 * result + b; 
     return result; 
    } 

Meine Lösung sieht wie folgt aus:

Map<Route, Long> routesCounted = routes.stream() 
      .sorted((r1,r2)-> (int)(r2.lastUpdated - r1.lastUpdated)) 
      .collect(Collectors.groupingBy(gr -> gr, Collectors.counting())); 

Natürlich Gießen mit etwas angeeignet mehr ersetzt int werden sollte.

+3

Beachten Sie, dass javadoc für 'groupingBy' dies nicht ausdrücklich garantiert wird funktionieren. – Misha

+1

Und es wird nicht wirklich für parallelen Strom arbeiten. Aber die Idee sieht interessant aus. –

7

Sie können eine abstrakte „Bibliothek“ Methode definieren, die zwei Kollektoren in einem vereint:

static <T, A1, A2, R1, R2, R> Collector<T, ?, R> pairing(Collector<T, A1, R1> c1, 
     Collector<T, A2, R2> c2, BiFunction<R1, R2, R> finisher) { 
    EnumSet<Characteristics> c = EnumSet.noneOf(Characteristics.class); 
    c.addAll(c1.characteristics()); 
    c.retainAll(c2.characteristics()); 
    c.remove(Characteristics.IDENTITY_FINISH); 
    return Collector.of(() -> new Object[] {c1.supplier().get(), c2.supplier().get()}, 
      (acc, v) -> { 
       c1.accumulator().accept((A1)acc[0], v); 
       c2.accumulator().accept((A2)acc[1], v); 
      }, 
      (acc1, acc2) -> { 
       acc1[0] = c1.combiner().apply((A1)acc1[0], (A1)acc2[0]); 
       acc1[1] = c2.combiner().apply((A2)acc1[1], (A2)acc2[1]); 
       return acc1; 
      }, 
      acc -> { 
       R1 r1 = c1.finisher().apply((A1)acc[0]); 
       R2 r2 = c2.finisher().apply((A2)acc[1]); 
       return finisher.apply(r1, r2); 
      }, c.toArray(new Characteristics[c.size()])); 
} 

Danach wird die eigentliche Operation kann wie folgt aussehen:

Map<Route, Long> result = routes.stream() 
     .collect(Collectors.groupingBy(Function.identity(), 
      pairing(Collectors.maxBy(Comparator.comparingLong(Route::getLastUpdated)), 
        Collectors.counting(), 
        (route, count) -> new AbstractMap.SimpleEntry<>(route.get(), count)) 
      )) 
     .values().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); 

Update: solche Sammler verfügbar in meiner StreamEx Bibliothek: MoreCollectors.pairing(). Auch ein ähnlicher Kollektor ist in jOOL Bibliothek implementiert, so dass Sie Tuple.collectors statt pairing verwenden können.

+0

Vielen Dank für die Lösung, aber ich bin auf der Suche nach etwas eleganter, wie @Misha Antwort. –

+1

Ich habe den "Bibliotheks" -Code von der Geschäftslogik getrennt. Bibliothekscode immer noch gruselig, aber Business-Code sieht jetzt viel besser aus. –

+1

Und nun bitte das ganze auf eine typsichere Art und Weise ;-) Ernsthaft sollte solch eine Lösung in 'Collectors' sein ... – Holger

2

Im Prinzip scheint es so, als ob dies in einem Durchgang machbar wäre. Die übliche Falte ist, dass dies ein Ad-hoc-Tupel oder -Paar erfordert, in diesem Fall mit einer Route und einer Zählung. Da Java diese nicht aufweist, verwenden wir am Ende ein Objekt-Array der Länge 2 (wie in Tagir Valeev's answer gezeigt) oder AbstractMap.SimpleImmutableEntry oder eine hypothetische Pair<A,B>-Klasse.

Die Alternative ist, eine kleine Wertklasse zu schreiben, die eine Route und eine Zählung enthält. Natürlich ist das ein bisschen schmerzhaft, aber in diesem Fall denke ich, dass es sich auszahlt, weil es einen Platz bietet, um die Kombinationslogik zu setzen. Dies wiederum vereinfacht den Stream-Betrieb.

Hier ist der Wert Klasse eine Route und eine Zählung enthält:

class RouteCount { 
    final Route route; 
    final long count; 

    private RouteCount(Route r, long c) { 
     this.route = r; 
     count = c; 
    } 

    public static RouteCount fromRoute(Route r) { 
     return new RouteCount(r, 1L); 
    } 

    public static RouteCount combine(RouteCount rc1, RouteCount rc2) { 
     Route recent; 
     if (rc1.route.getLastUpdated() > rc2.route.getLastUpdated()) { 
      recent = rc1.route; 
     } else { 
      recent = rc2.route; 
     } 
     return new RouteCount(recent, rc1.count + rc2.count); 
    } 
} 

Ziemlich einfach, aber die combine Methode bemerken. Es kombiniert zwei RouteCount Werte, indem es die Route wählt, die vor kurzem aktualisiert worden ist und die Summe der Zählungen verwendet. Nun, da wir diesen Wert Klasse haben, können wir einen One-Pass-Stream schreiben, um das Ergebnis zu erhalten wir wollen:

Map<Route, RouteCount> counted = routes.stream() 
     .collect(groupingBy(route -> route, 
        collectingAndThen(
         mapping(RouteCount::fromRoute, reducing(RouteCount::combine)), 
         Optional::get))); 

Wie andere Antworten, diese Gruppen die Routen in Äquivalenzklassen auf der Grundlage der Start- und End-Zelle. Die tatsächliche Route Instanz, die als Schlüssel verwendet wird, ist nicht signifikant; es ist nur ein Vertreter seiner Klasse. Der Wert ist ein einzelner RouteCount, der die Route Instanz enthält, die zuletzt aktualisiert wurde, zusammen mit der Anzahl der äquivalenten Route Instanzen.

Das funktioniert so, dass jede Route Instanz, die die gleichen Start- und Endzellen hat, in den nachgeschalteten Kollektor von groupingBy eingespeist wird. Dieser Kollektor bildet die Route Instanz in eine RouteCount Instanz ab und übergibt sie dann an einen reducing Kollektor, der die Instanzen unter Verwendung der oben beschriebenen Kombinierlogik reduziert. Der Und-Teil von collectingAndThen extrahiert den Wert aus dem Optional<RouteCount>, den der reducing Kollektor erzeugt.

(normalerweise ein nackter get ist gefährlich, aber wir bekommen gar nicht zu diesem Kollektor, es sei denn zur Verfügung mindestens ein Wert ist. So get in diesem Fall sicher ist.)

+0

Große Antwort und gut erklärt. Ich mag deine Lösung wirklich. Wenn ich das richtig verstehe, bedeutet dies, dass Ihre Lösung in O (n) Zeit läuft, während die @ Misha Lösung 2 * O (n) ist, oder? –

+0

@JernejJerin Streng genommen ist 2 * O (n) vom Standpunkt der Informatik aus dasselbe wie O (n). Aber ich denke, dass Sie meinen Ansatz als einen Durchlauf über die Daten gegenüber Misha's, der zwei Durchgänge macht. Das ist wahr, aber es folgt nicht notwendigerweise, dass mein Ansatz 2x die Geschwindigkeit von Mischa ist; Ein Zwei-Pass-Ansatz könnte dieselbe Geschwindigkeit wie ein One-Pass-Ansatz sein, der doppelt so viel Arbeit pro Element leistet. Ich weiß nicht, ob meine Vorgehensweise tatsächlich die doppelte Arbeit pro Element leistet, aber es scheint mehr pro Element zu sein als jeder von Mischas Pässen. Der Gesamtumfang der Arbeit scheint ähnlich zu sein. –

+0

@JernejJerin Ich vermute zwar, dass die realen Kosten in Speicherzuweisung, Speicherverbrauch und GC-Druck liegen würden. Ob dies signifikant ist, hängt von einer ganzen Reihe von Dingen ab. Wenn die Stream-Quelle nicht im Speicher gespeichert ist, z. B. wenn sie aus einer Datenbank oder irgendwo kommt, könnte der One-Pass-Ansatz einen großen Vorteil bei der Speichereinsparung haben. Aber nur wenn es viele doppelte Routen gibt. Wenn es keine Duplikate gibt, wird alles gespeichert. Wie auch immer, ich denke, du kannst die Nuancen sehen. Um herauszufinden, was besser ist, müssten Sie einen Benchmark erstellen. –

Verwandte Themen