1

我得到了一个遗留应用程序,它使用以下玩具片段中的数据结构,我无法轻易更改这些数据结构。

我使用 Java 8(仅)流来进行一些统计,但我未能使用收集器获得所需的类型。

package myIssueWithCollector;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

public class MyIssueWithCollector {

    public static Double latitude(Map<String, String> map) {
    String latitude = map.get("LATITUDE");
    return Double.valueOf(latitude);
    }

    private static int latitudeComparator(double d1, double d2) {
    // get around the fact that NaN > +Infinity in Double.compare()
    if (Double.isNaN(d1) && !Double.isNaN(d2)) {
        return -1;
    }
    if (!Double.isNaN(d1) && Double.isNaN(d2)) {
        return 1;
    }
    return Double.compare(Math.abs(d1), Math.abs(d2));
    }

    public static Map<String, String> createMap(String city, String country, String continent, String latitude) {
    Map<String, String> map = new HashMap<>();
    map.put("CITY", city);
    map.put("COUNTRY", country);
    map.put("CONTINENT", continent);
    map.put("LATITUDE", latitude);
    return map;
    }

    public static void main(String[] args) {

    // Cities with dummies latitudes
    // I can not change easily these legacy data structures
    Map<String, String> map1 = createMap("London", "UK", "Europa", "48.1");
    Map<String, String> map2 = createMap("New York", "USA", "America", "42.4");
    Map<String, String> map3 = createMap("Miami", "USA", "America", "39.1");
    Map<String, String> map4 = createMap("Glasgow", "UK", "Europa", "49.2");
    Map<String, String> map5 = createMap("Camelot", "UK", "Europa", "NaN");

    List<Map<String, String>> maps = new ArrayList<>(4);
    maps.add(map1);
    maps.add(map2);
    maps.add(map3);
    maps.add(map4);
    maps.add(map5);

    //////////////////////////////////////////////////////////////////
    // My issue starts here:
    //////////////////////////////////////////////////////////////////
    Map<String, Map<String, Double>> result = maps.stream()
        .collect(Collectors.groupingBy(m -> m.get("CONTINENT"),
            Collectors.groupingBy(m -> m.get("COUNTRY"), Collectors.reducing(Double.NaN, m -> latitude(m),
                BinaryOperator.maxBy((d1, d2) -> latitudeComparator(d1, d2))))));

    System.out.println(result);
}
}

我需要结果类型, Map<String, Map<String, String>>而不是Map<String, Map<String, Double>> 通过将“纬度”从转换回DoubleString使用自定义格式,而不是Double.toString())。

我未能使用收集器方法(如 andThen 或 collectAndThen,...

我目前坚持使用 Java 8。

有没有办法Map<String, Map<String, String>>使用相同的流获得结果?

4

3 回答 3

4

除了使用Collectors.reducing(…)withBinaryOperator.maxBy(…)你也可以使用Collectors.maxBy. 由于此收集器不支持标识值,因此它需要一个完成器函数来从 中提取值Optional,但是您的任务无论如何都需要一个完成器来格式化值。

Map<String, Map<String,String>> result = maps.stream()
    .collect(Collectors.groupingBy(m -> m.get("CONTINENT"),
        Collectors.groupingBy(m -> m.get("COUNTRY"),
            Collectors.mapping(MyIssueWithCollector::latitude,
                Collectors.collectingAndThen(
                    Collectors.maxBy(MyIssueWithCollector::latitudeComparator),
                    o -> format(o.get()))))));

这假设format是您的自定义格式功能,例如

private static String format(double d) {
    return String.format("%.2f", d);
}

但有时,实现自己的收集器而不是组合多个内置收集器可能是值得的。

Map<String, Map<String,String>> result = maps.stream()
    .collect(Collectors.groupingBy(m -> m.get("CONTINENT"),
        Collectors.groupingBy(m -> m.get("COUNTRY"),
            Collector.of(
                () -> new double[]{Double.NEGATIVE_INFINITY},
                (a, m) -> {
                    double d = latitude(m);
                    if(!Double.isNaN(d)) a[0] = Double.max(a[0], d);
                },
                (a, b) -> a[0] >= b[0]? a: b,
                a -> format(a[0])))));

收集器使用可变容器维护其状态,此自定义收集器使用长度为 1 的数组来保存double值(这消除了将其装箱到Double对象的需要)。它没有实现一个特殊的比较器来专门处理 NaN,而是使用一个条件,从一开始就永远不要让 NaN 进入数组。这就是组合器不需要关心 NaN 的原因;它可以简单地返回两个值中较大的一个。

Finisher 函数只是format使用该double值调用自定义函数。

于 2022-01-26T13:13:05.627 回答
2

您可以使用Collectors.collectingAndThen将减少的double值转换为相应的String

    Map<String, Map<String, String>> result = maps.stream().collect(
        Collectors.groupingBy(
            m -> m.get("CONTINENT"),
            Collectors.groupingBy(
                m -> m.get("COUNTRY"),
                Collectors.collectingAndThen(
                    Collectors.reducing(
                        Double.NaN,
                        m -> latitude(m),
                        BinaryOperator.maxBy(
                            (d1, d2) -> latitudeComparator(d1, d2)
                        )
                    ),
                    MyIssueWithCollector::myToString
                )
            )
        )
    );

这里,myToString是类中的一些方法,可以使用您的自定义格式MyIssueWithCollector返回,例如,Stringdouble

    public static String myToString(double d) {
        return "[latitude=" + d + "]";
    }
于 2022-01-26T11:22:47.207 回答
1

使用 Collectors reduction,您可以在标识中维护纬度的 String 类型,以便下游收集器返回 String。

Map < String, Map < String, String >> result = maps.stream()
  .collect(
    Collectors.groupingBy(m - > m.get("CONTINENT"),
      Collectors.groupingBy(m - > m.get("COUNTRY"),
        Collectors.reducing("NaN", m - > m.get("LATITUDE"),
          BinaryOperator.maxBy((s1, s2) - > latitudeComparator(Double.valueOf(s1), Double.valueOf(s2)))))));
于 2022-01-26T15:16:02.827 回答