【问题标题】:Java 8 streams intermediary map/collect to a stream with 2 valuesJava 8 流中间映射/收集到具有 2 个值的流
【发布时间】:2016-03-17 10:22:55
【问题描述】:

假设我有以下工作 lambda 表达式:

        Map<Field, String> fields = Arrays.stream(resultClass.getDeclaredFields())
            .filter(f -> f.isAnnotationPresent(Column.class))
            .collect(toMap(f -> {
                f.setAccessible(true);
                return f;
            }, f -> f.getAnnotation(Column.class).name()));

我想在过滤器语句之前创建一个具有 2 个值的流。所以我想做一个映射,但仍然保留原始值。我想实现这样的目标:

        this.fields = Arrays.stream(resultClass.getDeclaredFields())
            //map to <Field, Annotation> stream
            .filter((f, a) -> a != null)
            .collect(toMap(f -> {
                f.setAccessible(true);
                return f;
            }, f -> a.name()));

这对 Java 8 流有可能吗?我看过 collect(groupingBy()) 但仍然没有成功。

【问题讨论】:

  • 您可以提供地图供应商。该供应商可以提供一个已经有几个条目的地图。顺便说一句,你可以做.peek(f -&gt; f.setAccessible(true))
  • 谢谢你,我不知道偷看。我有东西,我会在答案中发布,因为 cmets 确实不适用于代码。
  • 它是Stream&lt;T&gt;,因此必须随时在流中为T 提供1 个具体类型。 Java 中目前没有魔法可以通过神奇地捕获像(f, a) 这样的值来帮助动态创建此类类型。但这并不是一个不常见的问题,所以将来可能会发生变化。同时,有一些图书馆试图解决这个问题:jooq.org/products/jOO%CE%BB/javadoc/0.9.5/org/jooq/lambda/tuple/…
  • 是的,如果下一个 Java 版本提供类似的解决方案,那就太好了。我喜欢 jooq 框架,但目前我们没有足够的 lambda 函数来证明我们庞大的项目中还有另一个库。

标签: java java-stream


【解决方案1】:

您需要像 Pair 这样的东西,它包含两个值。您可以自己编写,但这里有一些代码可以重新利用AbstractMap.SimpleEntry

     Map<Field, String> fields = Arrays.stream(resultClass.getDeclaredFields())
            .map(f -> new AbstractMap.SimpleEntry<>(f, f.getAnnotation(Column.class)))
            .filter(entry -> entry.getValue() != null)
            .peek(entry -> entry.getKey().setAccessible(true))
            .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().name()));

【讨论】:

  • @GoGoris:是什么让您认为这会减少开销?
  • 因为我可以跳过整个hashMap,所以这将跳过HashMap每个条目的哈希生成。
  • @GoGoris:您问题中的第一个代码 sn-p 创建了一个 Map,就像这个答案的代码一样,但原始代码不需要每个声明的 AbstractMap.SimpleEntry 实例场地。那么为什么你认为这会减少开销呢?
  • 啊第一个,是的,你是对的。我只是想看看我是否可以避免 isAnnotationPresent 因为我总是听说反射很慢。但我现在意识到,可能是字符串方法查找会影响性能,而不是像这样。
【解决方案2】:

您可以在collect 操作期间一次性完成整个操作,而无需配对类型:

Map<Field, String> fields = Arrays.stream(resultClass.getDeclaredFields())
    .collect(HashMap::new, (m,f) -> {
        Column c=f.getAnnotation(Column.class);
        if(c!=null) {
            f.setAccessible(true);
            m.put(f, c.name());
        }
    }, Map::putAll);

不过,对我来说,将两个不同的操作分开看起来更干净:

Map<Field, String> fields = Arrays.stream(resultClass.getDeclaredFields())
    .collect(HashMap::new, (m,f) -> {
        Column c=f.getAnnotation(Column.class);
        if(c!=null) m.put(f,c.name());
    }, Map::putAll);

AccessibleObject.setAccessible(
    fields.keySet().stream().toArray(AccessibleObject[]::new), true);

此解决方案确实对具有注释的字段进行了两次迭代,但由于它只执行一次安全检查而不是每个字段进行一次检查,因此它可能仍然优于所有其他解决方案。

通常,除非确实存在性能问题,否则您不应该尝试优化,如果您这样做,您应该衡量而不是猜测操作的成本。结果可能会令人惊讶,并且对数据集进行多次迭代不一定是坏事。

【讨论】:

  • 我确实测量了性能:for 循环是最快的,然后是 Pair,但差异可以忽略不计。就我而言,这段代码的性能非常重要。这就是为什么如果循环不可读,我不想替换它。
  • 但是很抱歉,代码将在哪里使用并不重要,我的意图是“我如何在 Java 8 中做这样的事情?”。你的输入让我思考,它似乎确实有点清洁。有时间我会试试表演的!
  • 当然,性能特征取决于过滤后的剩余字段数。我只提到了性能,只是因为这推动了在一次迭代中进行操作的愿望。如果这个答案令人深思,它就完成了它的工作;^)
【解决方案3】:

@Peter Lawrey:我用中间地图尝试了你的建议。它现在可以工作,但它不是很漂亮。

this.fields = Arrays.stream(resultClass.getDeclaredFields())
            .collect(HashMap<Field, Column>::new, (map, f) -> map.put(f, f.getAnnotation(Column.class)), HashMap::putAll)
            .entrySet().stream()
            .filter(entry -> entry.getValue() != null)
            .peek(entry -> entry.getKey().setAccessible(true))
            .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().name()));

【讨论】:

    猜你喜欢
    • 2017-09-22
    • 1970-01-01
    • 2019-11-19
    • 2015-01-29
    • 2023-03-15
    • 1970-01-01
    • 1970-01-01
    • 2022-01-18
    • 1970-01-01
    相关资源
    最近更新 更多