Выполнение операции между двумя хэш-картами с помощью STREAM API

#java #hashmap #java-8 #java-stream

#java #hashmap #java-8 #java-stream

Вопрос:

У меня есть две хэш-карты в форме

 Map<Integer, Map<String,Double>> MAP_1
Map<Integer, Map<String,Double>> MAP_2
  

Пусть INNER будет внутренней картой, т.е.

 Map<String,Double>
  

Для каждой пары соответствующих ключей я хочу выполнить операцию (например, вычитание) между двумя внутренними картами. В свою очередь, эта операция должна выполняться между двумя соответствующими ключами внутренних карт.
Результирующая карта должна быть

 Map<Integer,List<Double> RESULT
  

Примеры карт

 MAP_1
------------------------------
|      |  2016-10-02   10.0   |
| ID1  |  2016-10-03   20.0   |
|      |  2016-10-04   30.0   |
------------------------------
|      |  2016-10-02   1.00   | 
| ID2  |  2016-10-03   2.00   |
|      |  2016-10-04   3.00   |
------------------------------

MAP_2
------------------------------
|      |  2016-10-02   2.00   |
| ID1  |  2016-10-03   3.00   |
|      |                      |
------------------------------
|      |  2016-10-02   1.00   | 
| ID3  |  2016-10-03   2.00   |
|      |  2016-10-04   3.00   |
------------------------------

RESULT
---------------
|      |  8.00 |
| ID1  |  17.0 |
|      |       |
---------------
  

Итак, что касается внешних карт, необходимо учитывать только ID1; в свою очередь, операция должна включать внутренние (общие) ключи «2016-10-02» и «2016-10-03».

Вот мой текущий код

  Set<Integer> common_keys = new LinkedHashSet<Integer>(map1.keySet());
    common_keys.retainAll(map2.keySet());
    System.out.println("Common keys: " common_keys.toString());

    map1.forEach((k,v) -> {

        Map<String,Double> submap_1 = new LinkedHashMap<String,Double>(v);
        Map<String,Double> submap_2 = new LinkedHashMap<String,Double>(map2.get(k));

        Set<String> commons = new LinkedHashSet<String>(submap_1.keySet());
        commons.retainAll(submap_2.keySet());

        System.out.println(k " : common days: " commons);


        List<Double> val1 = submap_1.keySet()
                .stream()
                .filter(c -> commons.contains(c))
                .map(c -> submap_1.get(c))
                .collect(Collectors.toList());

        List<Double> val2 = submap_2.keySet()
                .stream()
                .filter(c -> commons.contains(c))
                .map(c -> submap_2.get(c))
                .collect(Collectors.toList());

        List<Double> ABS = IntStream.range(0, val1.size())
                .mapToObj(i -> val1.get(i) - val2.get(i))
                .collect(Collectors.toList());

        diff_abs.put(k, ABS);
        });
  

Есть ли более простой и разумный способ сделать это, используя потоковый API JAVA 8?

Заранее спасибо

Комментарии:

1. Почему так хочется использовать stream API? Если вам подходит обычный код, просто используйте обычный код.

2. Я уже использую stream API в своем коде. Мне было интересно, существует ли более компактный способ достижения того же результата

Ответ №1:

Создание Set не требуется. Замените это проверкой null in для того же ключа в другой карте. Кроме того, создание копий карт не требуется, поскольку карты никогда не изменяются в методе.

 public static <T, U> Map<T, List<Double>> merge(Map<T, Map<U, Double>> m1, Map<T, Map<U, Double>> m2) {
    Map<T, List<Double>> result = new HashMap<>();
    m1.forEach((k, v) -> {
        Map<U, Double> v2 = m2.get(k);
        if (v2 != null) {
            // both outer maps contain the same key
            ArrayList<Double> list = new ArrayList<>();
            v.forEach((innerK, innerV) -> {
                Double d = v2.get(innerK);
                if (d != null) {
                    // matching key in both inner maps
                    list.add(innerV - d);
                }
            });
            // list.trimToSize();
            result.put(k, list);
        }
    });
    return resu<
}
  

Ответ №2:

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

 public static <K,V,R> Map<K, R> merge(Map<K,V> map1, Map<K,V> map2, BiFunction<V,V,R> f) {

    boolean hasOrder=map1.entrySet().spliterator().hasCharacteristics(Spliterator.ORDERED);
    return map1.entrySet().stream()
        .collect(hasOrder? LinkedHashMap<K,R>::new: HashMap<K,R>::new, (m,e)-> {
            V v2 = map2.get(e.getKey());
            if(v2!=null) m.put(e.getKey(), f.apply(e.getValue(), v2));
        }, Map::putAll);
}
  

Затем вы можете легко реализовать слияние вложенных карт, используя метод дважды:

 public static <K1, K2> Map<K1, List<Double>> merge(
    Map<K1, Map<K2, Double>> map1, Map<K1, Map<K2, Double>> map2) {

    return merge(map1, map2,
        (a,b) -> new ArrayList<>(merge(a, b, (x,y) -> x-y).values()));
}
  

Обратите внимание, что приведенный выше код достаточно умен, чтобы создать LinkedHashMap , если и только если, первая карта имеет внутренний порядок, например, сама по себе является LinkedHashMap или SortedMap , и простое HashMap иначе. Порядок второй карты в любом случае не сохраняется, что не является проблемой, если, как, по-видимому, предполагает ваш исходный код, обе карты имеют одинаковый порядок. Если их порядок отличается, очевидно, что нет способа сохранить оба порядка.

Ответ №3:

Всего несколько моментов в вашем примере выше. Мои переходные реализации для List , Set , и Map являются ArrayList , HashSet , и HashMap , если мне не нужно что-то специально предоставленное их Linked реализациями, например, согласованный порядок (но это случается чрезвычайно редко).

Вот как я бы подошел к этому:

 public static <T, S> Map<T, List<Double>> method(
        Map<T, Map<S, Double>> map1,
        Map<T, Map<S, Double>> map2)
{
    Set<T> commonKeys = intersection(map1.keySet(), map2.keySet());

    return commonKeys.stream().collect(Collectors.toMap(
            Function.identity(),
            key -> {
                Map<S, Double> inner1 = map1.get(key);
                Map<S, Double> inner2 = map2.get(key);

                Set<S> innerCommonKeys = intersection(
                        inner1.keySet(),
                        inner2.keySet());

                        return innerCommonKeys.stream().map(innerKey ->
                                inner1.get(innerKey) - inner2.get(innerKey))
                                .collect(Collectors.toList());
            }));
}

private static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
    Set<T> intersection = new HashSet<T>(set1);
    intersection.retainAll(set2);
    return intersection;
}
  

И вот «тест», чтобы показать, что он работает:

 public static void main(String[] args)
{
    Map<String, Map<String, Double>> map1 = new HashMap<>();
    Map<String, Double> inner11 = new HashMap<>();
    inner11.put("2016-10-02", 10.0);
    inner11.put("2016-10-03", 20.0);
    inner11.put("2016-10-04", 30.0);
    map1.put("ID1", inner11);
    Map<String, Double> inner12 = new HashMap<>();
    inner12.put("2016-10-02", 1.00);
    inner12.put("2016-10-03", 2.00);
    inner12.put("2016-10-04", 3.00);
    map1.put("ID2", inner12);

    Map<String, Map<String, Double>> map2 = new HashMap<>();
    Map<String, Double> inner21 = new HashMap<>();
    inner21.put("2016-10-02", 2.00);
    inner21.put("2016-10-03", 3.00);
    map2.put("ID1", inner21);
    Map<String, Double> inner22 = new HashMap<>();
    inner22.put("2016-10-02", 1.00);
    inner22.put("2016-10-03", 2.00);
    inner22.put("2016-10-04", 3.00);
    map2.put("ID3", inner22);

    System.out.println(method(map1, map2));
}
  

Вывод: {ID1=[8.0, 17.0]}

Комментарии:

1. Иногда в моем приложении мне нужно, чтобы обработка не нарушала порядок ключей; более того, добавление пар (ключ, значение) в хэш-карту должно приводить к добавлению новых ключей. По этой причине я использовал Linked реализацию.