2016-02-28 9 views
14

Oft besteht die Notwendigkeit, Ergebnisse für eine Abfrage wie zu transformieren:Liste <Object[]> zur Karte <K, V> in Java 8

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

auf eine Karte, in der Schlüssel-Kategorien und Werte Anzahl der Datensätze zu der gleichen Kategorie gehören, .

Viele Persistenz-Frameworks geben die Ergebnisse einer solchen Abfrage als List<Object[]> zurück, wobei Objektarrays zwei Elemente enthalten (Kategorie und Anzahl für jede zurückgegebene Ergebnismengenzeile).

Ich versuche, die beste Möglichkeit zu finden, diese Liste in die entsprechende Karte zu konvertieren.

Natürlich wäre traditioneller Ansatz beinhaltet die Karte erstellen und die Einträge manuell setzen:

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

Der erste Einzeiler, die ich in dem Sinn kam, war es, die aus der Box verfügbar Collectors.toMap Kollektor zu nutzen:

Allerdings finde ich diese e -> (T) e[i] Syntax ein bisschen weniger lesbar als der traditionelle Ansatz. Um dies zu überwinden, könnte ich eine util Methode erstellen, die ich in allen solchen Situationen wiederverwenden können:

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

Dann habe ich ein perfektes Einzeiler bekam:

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

Es gibt sogar keine Notwendigkeit, Cast-Schlüssel und Wert wegen Typinferenz. Dies ist jedoch für andere Leser des Codes (Collector<Object[], ?, Map<K, V>> in der util-Methodensignatur usw.) etwas schwieriger zu verstehen.

Ich frage mich, gibt es noch etwas in der Java 8 Toolbox, die dazu beitragen könnte, dass dies in einer lesbareren/eleganteren Weise erreicht wird?

+3

Sie haben bereits einen Arbeitscode, der eine einzelne Zeile ist. Ich bin nicht sicher, was mehr "Werkzeuge" Sie brauchen. Für welche Art von Antworten interessieren Sie sich? – Tunaki

+2

Was Sie tun, scheint in Ordnung zu sein, außer dass ich eine 'Klasse ' und 'Klasse ' an 'toMap' übergeben würde, damit die Umwandlungen überprüft werden können. – Radiodef

+2

@Tunaki Wahr. Aber ich denke, dass es für mich und für die anderen vorteilhaft wäre, Beispiele zu sehen, wie dies weiter verbessert werden kann, damit es in diesem und ähnlichen Anwendungsfällen angewendet werden kann. –

Antwort

14

Ich denke, Ihre aktuelle 'One-Liner' ist in Ordnung, wie es ist. Aber wenn Sie nicht besonders, wie die Magie in den Befehl gebaut Indizes dann könnte man in einer Enum kapseln:

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]; 
    } 
} 

Dann bist du Extraktionscode wird es ein wenig klarer:

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

Idealer Sie würde der Spalte ein Typfeld hinzufügen und prüfen, ob die korrekte Methode aufgerufen wurde.

Außerhalb des Bereichs Ihrer Frage würden Sie idealerweise eine Klasse erstellen, die die Zeilen darstellt, die die Abfrage kapseln. So etwas wie die folgende (übersprungen die Getter für Klarheit):

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]; 
    } 
} 

, dass die Abhängigkeit zwischen der Abfrage und der Decodierung der Zeilen in der gleichen Klasse versetzt und blendet alle unnötigen Details vom Benutzer.

dann Ihre Karte zu schaffen wird:

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

Guter Ansatz. Ich hatte das Gefühl, dass Methodenverweise irgendwie ausgenutzt werden könnten, anstatt auf die Syntax von e -> (T) e [i]. –

+1

Sie ersetzen also die "magischen Indizes" durch noch mehr Magie, verlassen sich auf die Deklarationsreihenfolge "enum" und verstecken die notwendigen Typumwandlungen noch tiefer in den reflektierenden Operationen. Der Code basiert immer noch auf ungeschriebenen Konventionen über den Array-Inhalt, aber nur * sieht aus, als wäre mehr dahinter. By the way, 'Array.getInt' führt keine Unboxing-Konvertierungen durch, so dass es hier nicht funktioniert. – Holger

+1

@Holger Ich mag die Argumentation in dieser Antwort, es muss nicht genau so sein. Es könnte 'return (Integer) row [ordinal()]' sein, damit es funktioniert oder etwas völlig anderes, aber basierend auf diesem Konzept. Ich finde Konstrukt 'toMap (KEY :: getStringValue, COUNT :: getIntValue)' besser lesbar als 'e -> (String) e [0], e -> (Integer) e [1])'. –

2

Statt die Klasse Besetzung von versteckt, würde ich einige Funktionen machen mit Lesbarkeit helfen:

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

Voll Beispiel:

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

Gute Eins. Einfach und liest wie die Problemdomäne. –