【问题标题】:List<Object[]> to Map<K, V> in java 8在 Java 8 中列出<Object[]> 到 Map<K, V>
【发布时间】:2016-06-11 21:09:18
【问题描述】:

通常需要转换以下查询的结果:

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

映射到其中键是类别,值是属于同一类别的记录数。

许多持久性框架返回 List&lt;Object[]&gt; 这样的查询的结果,其中对象数组包含两个元素(类别和每个返回结果集行的计数)。

我正在尝试找到最易读的方法将此列表转换为相应的地图。

当然,传统方法会涉及创建地图并手动放置条目:

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

我想到的第一条线路是利用开箱即用的Collectors.toMapcollector:

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

但是,我发现这种e -&gt; (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());

由于类型推断,甚至不需要强制转换键和值。但是,对于代码的其他读者来说,这有点难以理解(util 方法签名中的Collector&lt;Object[], ?, Map&lt;K, V&gt;&gt; 等)。

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

【问题讨论】:

  • 您已经有一个单行的工作代码。我不确定您还需要什么“工具”。你对什么样的答案感兴趣?
  • 你正在做的事情看起来不错,除非我将 Class&lt;K&gt;Class&lt;V&gt; 传递给 toMap 以便检查演员表。
  • @Tunaki 真的。但我认为,看到任何可以进一步改进此功能的示例对我和其他人都是有益的,以便可以将其应用于此和类似的用例。
  • 为什么不使用允许将数据映射到 java 对象的持久性框架呢?像 JPA - 您可以将结果映射到一个简单的 持久性实体,其中包含一个字符串名称和一个整数计数。
  • 不,如果类型不匹配,您的实用程序方法不会获得类强制转换异常—您将获得损坏的Map(这被称为“堆污染”)。不可避免的异常可能会在很久以后发生在一个完全不相关的地方,与这个原因的联系很难追踪。 (请记住,当您使用Collection.addAll 等传输元素时,问题不会被检测到,污染可能会扩散)这就是为什么您应该在实用程序方法中收到有关“未经检查的操作”的编译器警告。

标签: java lambda java-8 java-stream collectors


【解决方案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));

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

虽然超出了您的问题范围,但理想情况下,您应该创建一个表示封装查询的行的类。类似于以下内容(为清楚起见跳过了吸气剂):

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

【讨论】:

  • 好方法。我觉得可以以某种方式利用方法引用而不是 e -&gt; (T) e[i] 语法。
  • 因此,您正在用更神奇的方式替换“魔法索引”,依赖于 enum 声明顺序,并在反射操作中更深地隐藏必要的类型转换。代码仍然依赖于关于数组内容的不成文约定,但只有 看起来 似乎还有更多内容。顺便说一句,Array.getInt 不执行拆箱转换,所以它甚至在这里都不起作用。
  • @Holger 我喜欢这个答案中的推理,它不必完全像这样。它可能是return (Integer) row[ordinal()],让它工作或完全不同但基于这个概念的东西。我发现构造 toMap(KEY::getStringValue, COUNT::getIntValue)e -&gt; (String) e[0], e -&gt; (Integer) e[1]) 更具可读性。
  • @Holger 这是一个非常小的实现细节,而不是对使用枚举封装列细节的一般设计的评论。我添加了一个索引字段并更改为标准强制转换,尽管我个人更喜欢按列顺序排列枚举并使用序数的设计。当我在实践中使用这种模式时,我还使用标准数据类型枚举来记录每一列的类型并确保请求正确的转换。
  • 好吧,如果你也使用enum 来创建数组,它确实更干净。但是,这意味着您可以控制创建和使用,因此,使用数组而不是在正确名称的变量中保存正确类型的值的专用对象根本难以理解……
【解决方案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()));
    }
}

【讨论】:

    猜你喜欢
    • 2013-12-20
    • 2020-04-08
    • 2014-02-09
    • 2019-01-23
    • 1970-01-01
    • 1970-01-01
    • 2014-05-09
    • 2017-05-20
    • 1970-01-01
    相关资源
    最近更新 更多