简体   繁体   中英

Simplifying Lambda expressions on stream in Java

I'm trying to teach myself how to use lambda expressions in Java instead of regular external iteration type solutions. I've made a short program where I use a stream of integers and assign them grade letters. Instead of using the typical "switch" statement, I've used these lambda expressions. Is there any way I can simplify it? It seems like there's a better way to do it. It works, but doesn't look as tidy.

    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 is an ArrayList.

Is there any way I can combine the logic all in one stream statement? I tried just having them follow eachother but kept getting syntax errors. What am I missing? I tried looking into the documentation for IntStream and Stream and couldn't really find what I was looking for.

Just create a separate function to map grade letters and call that in a single stream:

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)));

If you're particular about ordering the results by grade, you can add a sort to the beginning of your stream:

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

If you're open to using a third-party library, this solution will work with Java Streams and Eclipse Collections .

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);

This outputs the following:

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

I used the Java 9 factory method for the List and IntCaseFunction from Eclipse Collections. The lambda I passed to the constructor will be the default case if none of the other cases match.

The number ranges could also be represented by instances of IntInterval , which can be tested using contains as a method reference. The following solution will work using Local-variable type inference in 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);

If I drop all of the Predicates and Functions and remove the IntCaseFunction and replace with a method reference call to an instance method using a ternary operator, the following will work.

@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);
}

I am using the toString() implementation of the pair here, so the following will be the output.

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

The output could be customized in the forEach using the number grade and letter grade contained in the pair.

Note : I am a committer for Eclipse Collections.

Here's a solution without switch/if,else.

It has mappings between the various conditions and the action (printing logic)

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)));

Note:

  1. It is not elegant when compared to the conventional switch/if,else.
  2. It is not that efficient as for each grade it can potentially go through each entry in the map (not a big problem since the map has only a few mappings).

Summary: I would prefer to stick with your original code using switch/if,else.

You would want separate the logic of translating the marks to a grade (int -> String) and looping through your arraylist.

Use streams as a mechanism to loop through your data and a method for translation logic

Here is a quick example

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);

}

You can add these conditions inside forEach statement something like

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
});

There's no way to a "fork" stream into multiple streams, if that's what you're after. The way you've currently written it is the best you can do if you want all of the logic written in the stream manipulation.

I think the simplest thing to do here would be to just write the logic for producing the string normally. If you wanted to be fancy about it you could use mapToObj , like so:

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

The following code produces exactly the same output as you original code, ie in the same order.

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);

If you don't need that order, you can omit the grouping and sorting step:

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);

or

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)));

or

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

Easier to read and runs faster. ps. What about the "E" grade?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM