2016-02-28 2 views
14

Часто возникает необходимость преобразовать результаты поиска по запросу:Список <Object[]> к карте <K, V> в Java 8

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

на карте, в которой ключи являются категории и значения количества записей, принадлежащих к одной и той же категории ,

Многие основы сохранения возвращают результаты такого запроса, как List<Object[]>, где массивы объектов содержат два элемента (категория и счетчик для каждой строки возвращаемого набора результатов).

Я пытаюсь найти наиболее читаемый способ преобразования этого списка в соответствующую карту.

Конечно, традиционный подход предполагает создание карты и ввода записей вручную:

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

Первый лайнер, который пришел мне на ум, чтобы использовать из коробки доступного Collectors.toMap коллектора:

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

Однако, я нахожу это e -> (T) e[i] синтаксисом, менее читаемым, чем традиционный подход. Чтобы преодолеть это, я мог бы создать метод Util, который я могу повторно использовать во всех таких ситуациях:

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

Тогда у меня есть идеальный Однострочник:

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

Там даже нет необходимости cast key и value из-за вывода типа. Однако это немного сложнее понять для других читателей кода (Collector<Object[], ?, Map<K, V>> в сигнатуре метода утилиты и т. Д.).

Мне интересно, есть ли что-нибудь еще в панели инструментов java 8, которая могла бы помочь этому достичь в более читаемом/элегантном стиле?

+3

У вас уже есть рабочий код, который представляет собой одну строку. Я не уверен, какие еще «инструменты» вам нужны. Какие ответы вас интересуют? – Tunaki

+2

То, что вы делаете, кажется прекрасным, за исключением того, что я передавал бы «Class » и «Class » на 'toMap', чтобы проверки могли быть проверены. – Radiodef

+2

@ Tunaki True. Но я думаю, что для меня и для других было бы полезно увидеть примеры того, как это можно улучшить, чтобы оно могло применяться в этом и подобных случаях использования. –

ответ

14

Я думаю, что ваш текущий «один лайнер» в порядке, как есть. Но если вы не особенно нравятся магические индексы, встроенные в команду, то вы можете инкапсулировать в перечислении:

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

Тогда вы код экстракции становится немного понятнее:

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

В идеале 'd добавить поле типа в столбец и проверить правильный метод.

Невзирая на сферу вашего вопроса, в идеале вы должны создать класс, представляющий строки, которые инкапсулируют запрос. Что-то вроде следующего (пропустил геттеры для ясности):

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

Это ставит зависимость между запросом и декодирование строк в том же классе, и скрывает все ненужные детали от пользователя.

Затем создать карту становится:

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

Хороший подход. Я чувствовал, что ссылки на методы могут быть использованы каким-то образом вместо синтаксиса 'e -> (T) e [i]'. –

+1

Итак, вы заменяете «магические индексы» еще более магии, полагаясь на порядок объявления «enum» и скрывая требуемые типы отбрасываний даже глубже в рефлексивных операциях. Код по-прежнему полагается на неписаные соглашения о содержимом массива, но только * выглядит так, как будто это было больше. Кстати, 'Array.getInt' не выполняет распаковки конверсий, поэтому он даже не работает. – Holger

+1

@Holger Мне нравится рассуждение в этом ответе, оно не обязательно должно быть таким. Это может быть строка return (Integer) [порядковый номер()], чтобы заставить ее работать или что-то совершенно другое, но основанное на этой концепции. Я нахожу конструкцию 'toMap (KEY :: getStringValue, COUNT :: getIntValue)' более читаемой, чем 'e -> (String) e [0], e -> (Integer) e [1])'. –

2

Вместо того, чтобы прятать класс актеров, я хотел бы сделать несколько функций, чтобы помочь с дискретностью:

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

Полный пример:

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

Хорошо. Простой и читается как проблемная область. –

Смежные вопросы