2015-11-22 20 views
14

Come posso limitare groupBy ad ogni voce?Limit groupBy in Java 8

Per esempio (basato su questo esempio: stream groupBy):

studentClasses.add(new StudentClass("Kumar", 101, "Intro to Web")); 
studentClasses.add(new StudentClass("White", 102, "Advanced Java")); 
studentClasses.add(new StudentClass("Kumar", 101, "Intro to Cobol")); 
studentClasses.add(new StudentClass("White", 101, "Intro to Web")); 
studentClasses.add(new StudentClass("White", 102, "Advanced Web")); 
studentClasses.add(new StudentClass("Sargent", 106, "Advanced Web")); 
studentClasses.add(new StudentClass("Sargent", 103, "Advanced Web")); 
studentClasses.add(new StudentClass("Sargent", 104, "Advanced Web")); 
studentClasses.add(new StudentClass("Sargent", 105, "Advanced Web")); 

Questo metodo di ritorno semplice gruppo:

Map<String, List<StudentClass>> groupByTeachers = studentClasses 
      .stream().collect(
        Collectors.groupingBy(StudentClass::getTeacher)); 

Che cosa devo fare per limitare le collezioni restituiti? Supponiamo che voglio solo le prime classi N per ogni insegnante. Come può essere fatto?

+3

Che cosa si intende per prima ?, Ti, le classi con il più basso numero della classe, i nomi più bassi ASCIIBetically, o qualsiasi selezione casuale di N classi. Nota: l'insieme di classi può essere non ordinato. –

+0

@PeterLawrey Hai ragione, non ho detto questo, per me l'ordine è irrilevante, ma se vogliamo una soluzione più completa e generale - sarei felice se aggiungessi un esempio di ordinamento (con uno dei campi) – yossico

risposta

15

sarebbe possibile introdurre un nuovo collettore che limita il numero di elementi nella lista risultante.

Questo collettore manterrà gli elementi di testa dell'elenco (in encounter order). L'accumulatore e il combinatore gettano via ogni elemento quando il limite è stato raggiunto durante la raccolta. Il codice del combinatore è un po 'complicato, ma questo ha il vantaggio che non vengono aggiunti elementi aggiuntivi solo per essere gettati via più tardi.

private static <T> Collector<T, ?, List<T>> limitingList(int limit) { 
    return Collector.of(
       ArrayList::new, 
       (l, e) -> { if (l.size() < limit) l.add(e); }, 
       (l1, l2) -> { 
        l1.addAll(l2.subList(0, Math.min(l2.size(), Math.max(0, limit - l1.size())))); 
        return l1; 
       } 
      ); 
} 

e quindi utilizzarlo in questo modo:

Map<String, List<StudentClass>> groupByTeachers = 
     studentClasses.stream() 
        .collect(groupingBy(
          StudentClass::getTeacher, 
          limitingList(2) 
        )); 
4

Per questo è necessario .stream() il risultato della mappa. È possibile farlo facendo:

// Part that comes from your example 
Map<String, List<StudentClass>> groupByTeachers = studentClasses 
      .stream().collect(
        Collectors.groupingBy(StudentClass::getTeacher)); 

// Create a new stream and limit the result 
groupByTeachers = 
    groupByTeachers.entrySet().stream() 
     .limit(N) // The actual limit 
     .collect(Collectors.toMap(
      e -> e.getKey(), 
      e -> e.getValue() 
     )); 

Questo non è un modo molto ottimale per farlo. Ma se si .limit() nell'elenco iniziale, i risultati del raggruppamento non sarebbero corretti. Questo è il modo più sicuro per garantire il limite.

EDIT:

Come indicato nei commenti questo limita l'insegnante, non la classe per insegnante. In questo caso si può fare:

groupByTeachers = 
     groupByTeachers.entrySet().stream() 
      .collect(Collectors.toMap(
       e -> e.getKey(), 
       e -> e.getValue().stream().limit(N).collect(Collectors.toList()) // Limit the classes PER teacher 
      )); 
+0

Non molto ottimale, presumo che intendesse fare ciò nel gruppo iniziale. –

+0

Questo limita il numero di insegnanti restituiti, non il numero di classi per insegnante. – siegi

+3

L'uso di 'Map.replaceAll' sarebbe meglio nella fase di post-elaborazione piuttosto che eseguire un flusso separato per ciascun elemento. Ma la risposta di @ Tunaki è comunque migliore. –

3

Questo darebbe il risultato desiderato, ma categorizza ancora tutti gli elementi del flusso:

final int N = 10; 
final HashMap<String, List<StudentClass>> groupByTeachers = 
     studentClasses.stream().collect(
      groupingBy(StudentClass::getTeacher, HashMap::new, 
       collectingAndThen(toList(), list -> list.subList(0, Math.min(list.size(), N))))); 
4

Si potrebbe utilizzare collectingAndThen per definire un'operazione di finitura nella lista risultante. In questo modo è possibile limitare, filtrare, ordinare, ... le liste:

int limit = 2; 

Map<String, List<StudentClass>> groupByTeachers = 
    studentClasses.stream() 
        .collect(
         groupingBy(
          StudentClass::getTeacher, 
          collectingAndThen(
           toList(), 
           l -> l.stream().limit(limit).collect(toList())))); 
+0

Continuerebbe a filtrare i valori dopo che sono già stati aggiunti alla mappa, ma la risposta migliore finora. –

+2

L'idea di un finitore è buona ma non è necessario un costo O (n) nel finisher. Puoi fare qualcosa come 'list -> list.size() <= limit?lista: list.subList (0, limite)) 'invece. Ma preferisco ancora di gran lunga la soluzione di Tunaki, che non richiede assolutamente di inserire gli elementi extra nella lista. –

+0

Chi codice come questo ?? –