2016-02-28 24 views
14

Spesso v'è la necessità di trasformare i risultati per una query del tipo:Lista <Object[]> a Map <K, V> in Java 8

select category, count(*) 
from table 
group by category 

a una mappa in cui le chiavi sono categorie e valori sono conteggio di record appartenenti alla stessa categoria .

Molti framework di persistenza restituiscono i risultati di tale query come List<Object[]>, in cui gli array di oggetti contengono due elementi (categoria e il conteggio per ciascuna riga di serie di risultati restituita).

Sto cercando di trovare il modo più leggibile per convertire questa lista nella mappa corrispondente.

Naturalmente, tradizionale approccio comporterebbe la creazione della mappa e mettere manualmente le voci:

Map<String, Integer> map = new HashMap<>(); 
list.stream().forEach(e -> map.put((String) e[0], (Integer) e[1])); 

Il primo one-liner che è venuto in mente è stato quello di utilizzare l'out of the box disponibili Collectors.toMap collezionista:

Map<String, Integer> map = list.stream().collect(toMap(e -> (String) e[0], e -> (Integer) e[1])); 

Tuttavia, trovo questa sintassi e -> (T) e[i] un po 'meno leggibile rispetto all'approccio tradizionale. Per ovviare a questo, ho potuto creare un metodo util che posso riutilizzare in tutte queste situazioni:

public static <K, V> Collector<Object[], ?, Map<K, V>> toMap() { 
    return Collectors.toMap(e -> (K) e[0], e -> (V) e[1]); 
} 

Poi ho un perfetto one-liner:

Map<String, Integer> map = list.stream().collect(Utils.toMap()); 

C'è anche bisogno di cast chiave e valore a causa dell'inferenza di tipo. Tuttavia, questo è un po 'più difficile da comprendere per altri lettori del codice (Collector<Object[], ?, Map<K, V>> nella firma del metodo util, ecc.).

Mi chiedo, c'è qualcos'altro nella cassetta degli attrezzi di java 8 che potrebbe aiutare questo risultato in un modo più leggibile/elegante?

+3

Hai già un codice funzionante che è una singola riga. Non sono sicuro di quali siano più "strumenti" di cui hai bisogno. A che tipo di risposte sei interessato? – Tunaki

+2

Quello che stai facendo sembra andare bene, tranne che passarei un 'Classe ' e 'Classe ' a 'toMap' in modo che i cast possano essere controllati. – Radiodef

+2

@ Trueki True. Ma penso che sarebbe utile per me e per gli altri vedere esempi di come questo possa essere ulteriormente migliorato, in modo che possa essere applicato in questo e in simili casi d'uso. –

risposta

14

Penso che il tuo attuale "one-liner" sia perfetto. Ma se non piace particolarmente gli indici magici costruiti nel comando allora si potrebbe incapsulare in un enum:

enum Column { 
    CATEGORY(0), 
    COUNT(1); 

    private final int index; 

    Column(int index) { 
     this.index = index; 
    } 

    public int getIntValue(Object[] row) { 
     return (int)row[index]); 
    } 

    public String getStringValue(Object[] row) { 
     return (String)row[index]; 
    } 
} 

Allora sei il codice di estrazione diventa un po 'più chiaro:

list.stream().collect(Collectors.toMap(CATEGORY::getStringValue, COUNT::getIntValue)); 

Idealmente si 'd aggiungere un campo tipo alla colonna e verificare che venga chiamato il metodo corretto.

Mentre al di fuori della portata della domanda, idealmente si dovrebbe creare una classe che rappresenta le righe che incapsula la query. Qualcosa come il seguente (saltato i getter per chiarezza):

class CategoryCount { 
    private static final String QUERY = " 
     select category, count(*) 
     from table 
     group by category"; 

    private final String category; 
    private final int count; 

    public static Stream<CategoryCount> getAllCategoryCounts() { 
     list<Object[]> results = runQuery(QUERY); 
     return Arrays.stream(results).map(CategoryCount::new); 
    } 

    private CategoryCount(Object[] row) { 
     category = (String)row[0]; 
     count = (int)row[1]; 
    } 
} 

che mette la dipendenza tra la query e la decodifica delle righe nella stessa classe e nasconde tutti i dettagli inutili dall'utente.

Quindi creare la mappa diventa:

Map<String,Integer> categoryCountMap = CategoryCount.getAllCategoryCounts() 
    .collect(Collectors.toMap(CategoryCount::getCategory, CategoryCount::getCount)); 
+0

Buon approccio. Ho sentito che i riferimenti al metodo potevano essere sfruttati in qualche modo invece della sintassi 'e -> (T) e [i]'. –

+1

Quindi stai sostituendo gli "indici magici" con ancora più magia, facendo affidamento sull'ordine di dichiarazione "enum" e nascondendo i getti di tipo necessari ancora più in profondità nelle operazioni riflessive. Il codice si basa ancora su convenzioni non scritte sui contenuti dell'array, ma solo * sembra * come se ci fosse dell'altro. A proposito, 'Array.getInt' non esegue conversioni unboxing, quindi non funziona nemmeno qui. – Holger

+1

@Holger Mi piace il ragionamento in questa risposta, non deve essere esattamente così. Potrebbe essere 'return (Integer) row [ordinal()]', per farlo funzionare o qualcosa di completamente diverso ma basato su questo concetto. Trovo costrutto 'toMap (KEY :: getStringValue, COUNT :: getIntValue)' più leggibile di 'e -> (String) e [0], e -> (Integer) e [1])'. –

2

Invece di nascondere il cast di classe, mi renderebbe paio di funzioni per aiutare con la leggibilità:

Map<String, Integer> map = results.stream() 
     .collect(toMap(
       columnToObject(0, String.class), 
       columnToObject(1, Integer.class) 
     )); 

esempio completa:

package com.bluecatcode.learning.so; 

import com.google.common.collect.ImmutableList; 

import java.util.List; 
import java.util.Map; 
import java.util.function.Function; 

import static java.lang.String.format; 
import static java.util.stream.Collectors.toMap; 

public class Q35689206 { 

    public static void main(String[] args) { 
     List<Object[]> results = ImmutableList.of(
       new Object[]{"test", 1} 
     ); 

     Map<String, Integer> map = results.stream() 
       .collect(toMap(
         columnToObject(0, String.class), 
         columnToObject(1, Integer.class) 
       )); 

     System.out.println("map = " + map); 
    } 

    private static <T> Function<Object[], T> columnToObject(int index, Class<T> type) { 
     return e -> asInstanceOf(type, e[index]); 
    } 

    private static <T> T asInstanceOf(Class<T> type, Object object) throws ClassCastException { 
     if (type.isAssignableFrom(type)) { 
      return type.cast(object); 
     } 
     throw new ClassCastException(format("Cannot cast object of type '%s' to '%s'", 
       object.getClass().getCanonicalName(), type.getCanonicalName())); 
    } 
} 
+2

Buono. Semplice e legge come il dominio del problema. –

Problemi correlati