List<Object[]> 到 Java 8 中的 Map<K, V>

2022-09-04 02:46:38

通常需要转换查询的结果,例如:

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

但是,我发现这种语法的可读性比传统方法要差一些。为了克服这个问题,我可以创建一个util方法,我可以在所有此类情况下重用它:e -> (T) e[i]

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());

甚至不需要因为类型推断而强制转换键和值。但是,对于代码的其他读者来说,这有点难以掌握(在util方法签名等中)。Collector<Object[], ?, Map<K, V>>

我想知道,java 8工具箱中还有其他东西可以帮助以更易读/更优雅的方式实现这一目标吗?


答案 1

我认为你目前的“单行线”很好。但是,如果您不是特别喜欢命令中内置的魔术索引,则可以封装在枚举中:

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));

理想情况下,您将向列中添加一个类型字段,并检查调用了正确的方法。

虽然超出了问题的范围,但理想情况下,您将创建一个表示封装查询的行的类。如下所示(为清楚起见,跳过了 getters):

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));

答案 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()));
    }
}

推荐