Упрощение лямбда-выражений в потоке в Java

Я пытаюсь научить себя использовать лямбда-выражения в Java вместо обычных решений внешнего итерационного типа. Я сделал короткую программу, в которой я использую поток целых чисел и присваиваю им оценки. Вместо использования типичного оператора «switch» я использовал эти лямбда-выражения. Есть ли способ упростить это? Похоже, есть способ лучше. Работает, но выглядит не так аккуратно.

    grades.stream()
          .mapToInt(Integer::intValue)
          .filter(x -> x >= 90)
          .forEach(x -> System.out.println(x + " -> A"));

    grades.stream()
          .mapToInt(Integer::intValue)
          .filter(x -> x >= 80 && x <= 89)
          .forEach(x -> System.out.println(x + " -> B"));

    grades.stream()
          .mapToInt(Integer::intValue)
          .filter(x -> x >= 70 && x <= 79)
          .forEach(x -> System.out.println(x + " -> C"));

    grades.stream()
          .mapToInt(Integer::intValue)
          .filter(x -> x >= 60 && x <= 69)
          .forEach(x -> System.out.println(x + " -> D"));

    grades.stream()
          .mapToInt(Integer::intValue)
          .filter(x -> x >= 50 && x <= 59)
          .forEach(x -> System.out.println(x + " -> F"));

grades - это список массивов.

Есть ли способ объединить логику в одном операторе потока? Я пытался просто заставить их следовать друг за другом, но продолжал получать синтаксические ошибки. Что мне не хватает? Я попытался изучить документацию для IntStream и Stream и не смог найти то, что искал.

Лямбда или нет, старый добрый переключатель определенно лучший способ сделать это, как с точки зрения читаемости, так и с точки зрения реальной производительности. Так же, как Integer не заменяет int, stream не (всегда) заменяет for, а filter не заменяет switch. Рассмотрите codereview.stackexchange.com, если ваш код работает и не содержит ошибок.

Sheepy 22.03.2018 05:12

@Sheepy Спасибо за понимание! Я ценю его. Пойду проверю ссылку, которую ты дал. Радость!

dwagner6 22.03.2018 05:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
2
511
8

Ответы 8

Вы можете добавить эти условия в оператор forEach примерно так:

grades.stream()
.mapToInt(Integer::intValue)
.forEach(x -> {
    if (x >= 90) 
        System.out.println(x + " -> A");
    else if (x >= 80) 
        System.out.println(x + " -> B");
    else if (x >= 70)
        System.out.println(x + " -> C");
    ......
    // other conditions
});

Нет необходимости иметь сравнения <= 89 и <= 79.

Mike 22.03.2018 05:16
for (int x : grades)
{
    if (x >= 90)
        System.out.println(x + " -> A");
    else if (x >= 80)
        System.out.println(x + " -> B");
    else if (x >= 70)
        System.out.println(x + " -> C");
    else if (x >= 60)
        System.out.println(x + " -> D");
    else if (x >= 50)
        System.out.println(x + " -> F");
}

Легче читать и работает быстрее. пс. А как насчет оценки «Е»?

Проголосовали против, потому что OP уже знает об итерации и пытается изучить потоки на игрушечном примере, а не решить, как это лучше всего кодировать.

WW. 22.03.2018 05:25

Просто создайте отдельную функцию для сопоставления букв оценок и вызовите ее в одном потоке:

static char gradeLetter(int grade) {
    if (grade >= 90) return 'A';
    else if (grade >= 80) return 'B';
    else if (grade >= 70) return 'C';
    else if (grade >= 60) return 'D';
    else return 'F';
}

grades.stream()
      .mapToInt(Integer::intValue)
      .filter(x -> x >= 50)
      .forEach(x -> System.out.println(x + " -> " + gradeLetter(x)));

Если вы хотите упорядочить результаты по оценкам, вы можете добавить сортировку в начало потока:

.sorted(Comparator.comparing(x -> gradeLetter(x)))

IMHO, это намного лучше, чем включение встроенной логики в поток. Stream наиболее удобочитаем, когда выполняется несколько последовательных простых шагов.

WW. 22.03.2018 05:24

Если вы готовы использовать стороннюю библиотеку, это решение будет работать с Java Streams и Коллекции Eclipse.

List<Integer> grades = List.of(0, 40, 51, 61, 71, 81, 91, 100);
grades.stream()
        .mapToInt(Integer::intValue)
        .mapToObj(new IntCaseFunction<>(x -> x + " -> F")
                .addCase(x -> x >= 90, x -> x + " -> A")
                .addCase(x -> x >= 80 && x <= 89, x -> x + " -> B")
                .addCase(x -> x >= 70 && x <= 79, x -> x + " -> C")
                .addCase(x -> x >= 60 && x <= 69, x -> x + " -> D")
                .addCase(x -> x >= 50 && x <= 59, x -> x + " -> F"))
        .forEach(System.out::println);

Это выводит следующее:

0 -> F
40 -> F
51 -> F
61 -> D
71 -> C
81 -> B
91 -> A
100 -> A

Я использовал фабричный метод Java 9 для списка и IntCaseFunction из коллекций Eclipse. Лямбда, которую я передал конструктору, будет вариантом по умолчанию, если ни один из других случаев не совпадет.

Диапазоны номеров также могут быть представлены экземплярами IntInterval, которые можно протестировать, используя contains в качестве ссылки на метод. Следующее решение будет работать с использованием вывода типа локальной переменной в Java 10.

var mapGradeToLetter = new IntCaseFunction<>(x -> x + " -> F")
        .addCase(x -> x >= 90, x -> x + " -> A")
        .addCase(IntInterval.fromTo(80, 89)::contains, x -> x + " -> B")
        .addCase(IntInterval.fromTo(70, 79)::contains, x -> x + " -> C")
        .addCase(IntInterval.fromTo(60, 69)::contains, x -> x + " -> D")
        .addCase(IntInterval.fromTo(50, 59)::contains, x -> x + " -> F");

var grades = List.of(0, 40, 51, 61, 71, 81, 91, 100);
grades.stream()
        .mapToInt(Integer::intValue)
        .mapToObj(mapGradeToLetter)
        .forEach(System.out::println);

Если я отброшу все Predicates и Functions, удалю IntCaseFunction и заменю ссылочным вызовом метода экземпляра с использованием тернарного оператора, следующее будет работать.

@Test
public void grades()
{
    var grades = List.of(0, 40, 51, 61, 71, 81, 91, 100);
    grades.stream()
            .mapToInt(Integer::intValue)
            .mapToObj(this::gradeToLetter)
            .forEach(System.out::println);
}

private IntObjectPair<String> gradeToLetter(int grade)
{
    var letterGrade =
            grade >= 90 ? "A" :
            grade >= 80 ? "B" :
            grade >= 70 ? "C" :
            grade >= 60 ? "D" : "F";
    return PrimitiveTuples.pair(grade, letterGrade);
}

Я использую здесь реализацию пары toString(), поэтому на выходе будет следующее.

0:F
40:F
51:F
61:D
71:C
81:B
91:A
100:A

Вывод можно настроить в forEach, используя числовую и буквенную оценки, содержащиеся в паре.

Примечание: Я участник коллекций Eclipse.

Вот решение без переключателя / if, else.

Он имеет сопоставления между различными условиями и действием (логикой печати).

Map<Predicate<Integer>, Consumer<Integer>> actions = new LinkedHashMap<>();
//Beware of nulls in your list - It can result in a NullPointerException when unboxing
actions.put(x -> x >= 90, x -> System.out.println(x + " -> A"));
actions.put(x -> x >= 80 && x <= 89, x -> System.out.println(x + " -> B"));
actions.put(x -> x >= 70 && x <= 79, x -> System.out.println(x + " -> C"));
actions.put(x -> x >= 60 && x <= 69, x -> System.out.println(x + " -> D"));
actions.put(x -> x >= 50 && x <= 59, x -> System.out.println(x + " -> F"));


grades.forEach(x -> actions.entrySet().stream()
                    .filter(entry -> entry.getKey().apply(x))
                    .findFirst()
                    .ifPresent(entry -> entry.getValue().accept(x)));

Примечание:

  1. Это не изящно по сравнению с обычным переключателем / if, else.
  2. Это не так эффективно, поскольку для каждого класса он потенциально может проходить через каждую запись на карте (не большая проблема, поскольку карта имеет только несколько сопоставлений).

Резюме: Я бы предпочел придерживаться исходного кода с помощью switch / if, else.

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

Я думаю, что проще всего было бы просто написать логику для обычного создания строки. Если вы хотите представить себе это, вы можете использовать mapToObj, например:

    grades.stream()
        .mapToInt(Integer::intValue)
        .mapToObj(x -> {
            if (x >= 90) {
                return "A";
            }
            //etc
        })
        .forEach(System.out::println);

Вы бы хотели разделить логику перевода оценок в оценку (int -> String) и циклического прохождения вашего arrayylist.

Используйте потоки как механизм для циклического перебора ваших данных и метод логики перевода

Вот быстрый пример

private static void getGradeByMarks(Integer marks) {
    if (marks > 100 || marks < 0) {
        System.err.println("Invalid value " + marks);
        return;
    }
    // whatever the logic for your mapping is
    switch(marks/10) {
        case 9:
            System.out.println(marks + " --> " + "A");
            break;
        case 8:
            System.out.println(marks + " --> " + "B");
            break;
        case 7:
            System.out.println(marks + " --> " + "C");
            break;
        case 6:
            System.out.println(marks + " --> " + "D");
            break;
        default:
            System.out.println(marks + " --> " + "F");
            break;
    }
}

public static void main(String[] args) {
    List<Integer> grades = Arrays.asList(90,45,56,12,54,88,-6);

    grades.forEach(GradesStream::getGradeByMarks);

}

Следующий код дает точно такой же результат, что и исходный код, то есть в том же порядке.

grades.stream()
  .filter(x -> x >= 50)
  .collect(Collectors.groupingBy(x -> x<60? 'F': 'E'+5-Math.min(9, x/10)))
  .entrySet().stream()
  .sorted(Map.Entry.comparingByKey())
  .flatMap(e -> e.getValue().stream().map(i -> String.format("%d -> %c", i, e.getKey())))
  .forEach(System.out::println);

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

grades.stream()
  .filter(x -> x >= 50)
  .map(x -> String.format("%d -> %c", x, x<60? 'F': 'E'+5-Math.min(9, x/10)))
  .forEach(System.out::println);

или же

grades.stream()
  .filter(x -> x >= 50)
  .forEach(x -> System.out.printf("%d -> %c%n", x, x<60? 'F': 'E'+5-Math.min(9, x/10)));

или же

grades.forEach(x -> {
   if (x >= 50) System.out.printf("%d -> %c%n", x, x<60? 'F': 'E'+5-Math.min(9, x/10));
});

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