Неизменяемое изменение данных с помощью потока Java

Рассмотрим этот код:

Function<BigDecimal,BigDecimal> func1 = x -> x;//This could be anything
Function<BigDecimal,BigDecimal> func2 = y -> y;//This could be anything
Map<Integer,BigDecimal> data = new HashMap<>();

Map<Integer,BigDecimal> newData = 
    data.entrySet().stream().
        collect(Collectors.toMap(Entry::getKey,i -> 
            func1.apply(i.getValue())));

List<BigDecimal> list = 
    newData.entrySet().stream().map(i -> 
        func2.apply(i.getValue())).collect(Collectors.toList());

В основном я обновляю HashMap с помощью func1, чтобы применить вторую трансформацию с помощью func2 и сохранить значение, обновленное во второй раз, в списке. Я СДЕЛАЛ все неизменным способом, создавая новые объекты newData и list.

МОЙ ВОПРОС: Можно ли выполнить потоковую передачу исходной HashMap (данных) один раз?

Я пробовал это:

Function<BigDecimal,BigDecimal> func1 = x -> x;
Function<BigDecimal,BigDecimal> func2 = y -> y;
Map<Integer,BigDecimal> data = new HashMap<>();
List<BigDecimal> list = new ArrayList<>();

Map<Integer,BigDecimal> newData = 
    data.entrySet().stream().collect(Collectors.toMap(
        Entry::getKey,i -> 
        {
            BigDecimal newValue = func1.apply(i.getValue());
            //SIDE EFFECT!!!!!!!
            list.add(func2.apply(newValue));
            return newValue;
    }));    

но при этом у меня есть побочный эффект при обновлении списка, поэтому я потерял требование «неизменяемости».

Значит, вам нужно правильно получить два объекта из одного потока, один список и одну карту?

Nonika 01.11.2018 12:50
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
4
1
440
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ваш код можно упростить следующим образом:

    List<BigDecimal> list = data.values().stream()
            .map(func1)
            .map(func2)
            .collect(Collectors.toList());

Ваша цель - применить эти функции ко всем значениям BigDecimal в Map. Вы можете получить все эти значения из карты, используя Map::values, который возвращает List. Затем примените поток только к списку. Считайте, что data уже содержит несколько записей:

List<BigDecimal> list = data.values().stream()
                                     .map(func1)
                                     .map(func2)
                                     .collect(Collectors.toList()); 

Я не рекомендую вам повторять все записи (Set<Entry<Integer, BigDecimal>>), поскольку вам нужно работать только со значениями.

Вместе со списком, который мне нужен на будущее, карта обновлялась только с func1.

giuseppe maugeri 01.11.2018 10:29

То же, что этот ответ, а не то, что ищет OP. Проверьте этот ответ для более чистого решения.

Naman 01.11.2018 14:57

@giuseppemaugeri: Не могли бы вы лучше описать проблему в вопросе? Не уверен, что правильно понял.

Nikolas Charalambidis 01.11.2018 17:46

@nullpointer: Похоже, мы поняли вопрос точно так же.

Nikolas Charalambidis 01.11.2018 17:47

Попробуйте таким образом, чтобы он вернул массив объекта [2], первый - это карта, а второй - список.

Map<Integer, BigDecimal> data = new HashMap<>();

        data.put(1, BigDecimal.valueOf(30));
        data.put(2, BigDecimal.valueOf(40));
        data.put(3, BigDecimal.valueOf(50));

        Function<BigDecimal, BigDecimal> func1 = x -> x.add(BigDecimal.valueOf(10));//This could be anything
        Function<BigDecimal, BigDecimal> func2 = y -> y.add(BigDecimal.valueOf(-20));//This could be anything


        Object[] o = data.entrySet().stream()
                .map(AbstractMap.SimpleEntry::new)
                .map(entry -> {
                    entry.setValue(func1.apply(entry.getValue()));
                    return entry;
                })
                .collect(Collectors.collectingAndThen(toMap(Map.Entry::getKey, Map.Entry::getValue), a -> {
                    List<BigDecimal> bigDecimals = a.values().stream().map(func2).collect(Collectors.toList());
                    return new Object[]{a,bigDecimals};
                }));
        System.out.println(data);
        System.out.println((Map<Integer, BigDecimal>)o[0]);
        System.out.println((List<BigDecimal>)o[1]);

Выход:

 Original Map: {1=30, 2=40, 3=50}
 func1 map: {1=40, 2=50, 3=60}
 func1+func2 list: [20, 30, 40]
Ответ принят как подходящий

Это кажется идеальным вариантом использования метода Collectors.teeing в JDK 12. Вот webrev, а здесь CSR. Вы можете использовать его следующим образом:

Map.Entry<Map<Integer, BigDecimal>, List<BigDecimal>> result = data.entrySet().stream()
    .collect(Collectors.teeing(
             Collectors.toMap(
                     Map.Entry::getKey, 
                     i -> func1.apply(i.getValue())),
             Collectors.mapping(
                     i -> func1.andThen(func2).apply(i.getValue()),
                     Collectors.toList()),
             Map::entry));

Collectors.teeing собирает данные в два разных коллектора, а затем объединяет оба частичных результата в окончательный результат. На этом последнем шаге я использую статический метод Map.entry(K k, V v) JDK 9, но я мог бы использовать любой другой контейнер, то есть Pair или Tuple2 и т. д.

Для первого сборщика я использую ваш точный код для сбора данных в Map, а для второго сборщика я использую Collectors.mapping вместе с Collectors.toList, используя Function.andThen для создания ваших функций func1 и func2 для этапа сопоставления.


Обновлено: Если вы не можете дождаться выпуска JDK 12, вы можете пока использовать этот код:

public static <T, A1, A2, R1, R2, R> Collector<T, ?, R> teeing(
        Collector<? super T, A1, R1> downstream1,
        Collector<? super T, A2, R2> downstream2,
        BiFunction<? super R1, ? super R2, R> merger) {

    class Acc {
        A1 acc1 = downstream1.supplier().get();
        A2 acc2 = downstream2.supplier().get();

        void accumulate(T t) {
            downstream1.accumulator().accept(acc1, t);
            downstream2.accumulator().accept(acc2, t);
        }

        Acc combine(Acc other) {
            acc1 = downstream1.combiner().apply(acc1, other.acc1);
            acc2 = downstream2.combiner().apply(acc2, other.acc2);
            return this;
        }

        R applyMerger() {
            R1 r1 = downstream1.finisher().apply(acc1);
            R2 r2 = downstream2.finisher().apply(acc2);
            return merger.apply(r1, r2);
        }
    }

    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::applyMerger);
}

Примечание. Характеристики нижележащих коллекторов не учитываются при создании возвращенного коллектора (оставлено в качестве упражнения).


Обновлено еще раз: Ваше решение абсолютно нормально, даже если оно использует два потока. Мое решение, приведенное выше, передает исходную карту только один раз, но дважды применяет func1 ко всем значениям. Если func1 стоит дорого, вы можете рассмотреть его как запоминание (т.е. кэширование его результатов, чтобы при повторном вызове с тем же вводом вы возвращали результат из кеша, а не вычисляли его снова). Или вы также можете сначала применить func1 к значениям исходной карты, а затем собрать с помощью Collectors.teeing.

Запоминать легко. Просто объявите этот служебный метод:

public <T, R> Function<T, R> memoize(Function<T, R> f) {
    Map<T, R> cache = new HashMap<>(); // or ConcurrentHashMap
    return k -> cache.computeIfAbsent(k, f);
}

А затем используйте его следующим образом:

Function<BigDecimal, BigDecimal> func1 = memoize(x -> x); //This could be anything

Теперь вы можете использовать этот мемоизированный func1, и он будет работать точно так же, как и раньше, за исключением того, что он будет возвращать результаты из кеша, когда его метод apply вызывается с аргументом, который использовался ранее.

Другое решение - сначала применить func1, а затем собрать:

Map.Entry<Map<Integer, BigDecimal>, List<BigDecimal>> result = data.entrySet().stream()
    .map(i -> Map.entry(i.getKey(), func1.apply(i.getValue())))
    .collect(Collectors.teeing(
             Collectors.toMap(
                     Map.Entry::getKey, 
                     Map.Entry::getValue),
             Collectors.mapping(
                     i -> func2.apply(i.getValue()),
                     Collectors.toList()),
             Map::entry));

Опять же, я использую статический метод jdk9 Map.entry(K k, V v).

Спасибо, что поделились обновлением. Просто не мог сравнить внесенную здесь сложность с другое решение. ... может вопрос там неправильно интерпретируется?

Naman 01.11.2018 14:22

@nullpointer Дело в том, что OP хочет оба новую карту с только func1, примененным к значениям, а также список с func2 ∘ func1, примененным к ним.

fps 01.11.2018 14:31

Хм, теперь я могу выбрать функцию слияния и тип возврата вашего решения. Интересно.

Naman 01.11.2018 14:55

Другие вопросы по теме