Sorting through Lambda expressions

Keywords: Java Algorithm

Data sorting in memory

First, we define a basic class, and then we will demonstrate how to sort in memory according to this basic class.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Comparator based sorting

Before Java 8, we used to implement the Comparator interface to complete sorting, such as:

new Comparator<Student>() {
    @Override
    public int compare(Student h1, Student h2) {
        return h1.getName().compareTo(h2.getName());
    }
};

Here is the definition of anonymous internal class. If it is a general comparison logic, you can directly define an implementation class. It is also relatively simple to use, as follows:

@Test
void baseSortedOrigin() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    Collections.sort(students, new Comparator<Student>() {
        @Override
        public int compare(Student h1, Student h2) {
            return h1.getName().compareTo(h2.getName());
        }
    });
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

Junit5 is used to implement unit testing, which is very suitable for verifying logic.

Because the Comparator defined uses the name field to sort, in Java, the sorting of String type is determined by the ASCII code sequence of single character, and J is in front of T, so Jerry is in the first place.

Replace Comparator anonymous inner class with Lambda expression

Those who have used java 8's lambba should know that anonymous inner classes can be simplified to Lambda expressions as follows:

Collections.sort(students, (Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));

In Java 8, the sort method is added to the List class, so Collections.sort can be directly replaced by:

students.sort((Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));

According to the type inference of Lambda in Java 8, we can abbreviate the specified Student type:

students.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));

So far, our whole sorting logic can be simplified as follows:

@Test
void baseSortedLambdaWithInferring() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    students.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

Extract public Lambda expressions through static methods

We can define a static method in Student:

public static int compareByNameThenAge(Student s1, Student s2) {
    if (s1.name.equals(s2.name)) {
        return Integer.compare(s1.age, s2.age);
    } else {
        return s1.name.compareTo(s2.name);
    }
}

This method needs to return an int type parameter. In Java 8, we can use this method in Lambda:

@Test
void sortedUsingStaticMethod() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    students.sort(Student::compareByNameThenAge);
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

With the help of Comparator's comparing method

In Java 8, the Comparator class adds a comparing method, which can use the passed Function parameters as comparison elements, such as:

@Test
void sortedUsingComparator() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    students.sort(Comparator.comparing(Student::getName));
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

Multi condition sorting

We show multi conditional sorting in the static method section, and can also implement multi conditional logic in the Comparator anonymous inner class:

@Test
void sortedMultiCondition() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12),
            new Student("Jerry", 13)
    );
    students.sort((s1, s2) -> {
        if (s1.getName().equals(s2.getName())) {
            return Integer.compare(s1.getAge(), s2.getAge());
        } else {
            return s1.getName().compareTo(s2.getName());
        }
    });
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

Logically, multi condition sorting is to judge the first level conditions first, if they are equal, then judge the second level conditions, and so on. In Java 8, comparing and a series of thenmatching can be used to represent multi-level conditional judgment. The above logic can be simplified as follows:

@Test
void sortedMultiConditionUsingComparator() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12),
            new Student("Jerry", 13)
    );
    students.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge));
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

There can be multiple thenmatching methods here to represent multi-level conditional judgment, which is also the convenience of functional programming.

Sort in Stream

Java 8 not only introduces Lambda expressions, but also introduces a brand-new streaming API: Stream API. The sorted method is also used to sort elements in streaming calculation. You can pass it into Comparator to realize sorting logic:

@Test
void streamSorted() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final Comparator<Student> comparator = (h1, h2) -> h1.getName().compareTo(h2.getName());
    final List<Student> sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry", 12));
}

Similarly, we can simplify writing through Lambda:

Test
void streamSortedUsingComparator() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final Comparator<Student> comparator = Comparator.comparing(Student::getName);
    final List<Student> sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry", 12));
}

Transfer sort judgment

Sorting is to judge the order according to the value returned by the compareTo method. If you want to reverse the order, just return the returned value:

@Test
void sortedReverseUsingComparator2() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final Comparator<Student> comparator = (h1, h2) -> h2.getName().compareTo(h1.getName());
    students.sort(comparator);
    Assertions.assertEquals(students.get(0), new Student("Tom", 10));
}

It can be seen that when arranging in positive order, we are h1.getName().compareTo(h2.getName()), here we directly reverse it and use h2.getName().compareTo(h1.getName()), which achieves the effect of inversion. An internal private class of java.util.Collections.ReverseComparator is defined in Java Collections, which realizes element inversion in this way.

Reverse order with the help of Comparator's reversed method

The reversed method is added in Java8 to realize reverse order, which is also very simple to use:

@Test
void sortedReverseUsingComparator() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final Comparator<Student> comparator = (h1, h2) -> h1.getName().compareTo(h2.getName());
    students.sort(comparator.reversed());
    Assertions.assertEquals(students.get(0), new Student("Tom", 10));
}

Define sort inversion in Comparator.comparing

The comparing method also has an overloaded method, Java. Util. Comparator #comparing (Java. Util. Function <? Super T,? Extends U >, Java. Util. Comparator <? Super U >), and the second parameter can be passed into Comparator.reverseOrder(), which can realize reverse order:

@Test
void sortedUsingComparatorReverse() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    students.sort(Comparator.comparing(Student::getName, Comparator.reverseOrder()));
    Assertions.assertEquals(students.get(0), new Student("Jerry", 12));
}

Define sort reversal in Stream

The operation in Stream is similar to direct list sorting. You can reverse the Comparator definition or use Comparator.reverseOrder(). The implementation is as follows:

@Test
void streamReverseSorted() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final Comparator<Student> comparator = (h1, h2) -> h2.getName().compareTo(h1.getName());
    final List<Student> sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Tom", 10));
}

@Test
void streamReverseSortedUsingComparator() {
    final List<Student> students = Lists.newArrayList(
            new Student("Tom", 10),
            new Student("Jerry", 12)
    );
    final List<Student> sortedStudents = students.stream()
            .sorted(Comparator.comparing(Student::getName, Comparator.reverseOrder()))
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Tom", 10));
}

Judgment of null value

In the previous examples, the sorting of valued elements can cover most scenarios, but sometimes we still encounter null elements:

  1. The element in the list is null

  2. The field of the element in the list participating in the sorting condition is null

If we still use the previous implementations, we will encounter a NullPointException, that is, NPE. Let's briefly demonstrate:

@Test
void sortedNullGotNPE() {
    final List<Student> students = Lists.newArrayList(
            null,
            new Student("Snoopy", 12),
            null
    );
    Assertions.assertThrows(NullPointerException.class,
            () -> students.sort(Comparator.comparing(Student::getName)));
}

So we need to consider these scenarios.

The element is a clumsy implementation of null

The first thing I thought of was air judgment:

@Test
void sortedNullNoNPE() {
    final List<Student> students = Lists.newArrayList(
            null,
            new Student("Snoopy", 12),
            null
    );
    students.sort((s1, s2) -> {
        if (s1 == null) {
            return s2 == null ? 0 : 1;
        } else if (s2 == null) {
            return -1;
        }
        return s1.getName().compareTo(s2.getName());
    });

    Assertions.assertNotNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNull(students.get(2));
}

We can extract a Comparator from the empty judgment logic and realize it by combination:

class NullComparator<T> implements Comparator<T> {
    private final Comparator<T> real;

    NullComparator(Comparator<? super T> real) {
        this.real = (Comparator<T>) real;
    }

    @Override
    public int compare(T a, T b) {
        if (a == null) {
            return (b == null) ? 0 : 1;
        } else if (b == null) {
            return -1;
        } else {
            return (real == null) ? 0 : real.compare(a, b);
        }
    }
}

This implementation has been prepared for us in Java 8.

Use Comparator.nullsLast and Comparator.nullsFirst

Use Comparator.nullsLast to implement null at the end:

@Test
void sortedNullLast() {
    final List<Student> students = Lists.newArrayList(
            null,
            new Student("Snoopy", 12),
            null
    );
    students.sort(Comparator.nullsLast(Comparator.comparing(Student::getName)));
    Assertions.assertNotNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNull(students.get(2));
}

Use Comparator.nullsFirst to implement null at the beginning:

@Test
void sortedNullFirst() {
    final List<Student> students = Lists.newArrayList(
            null,
            new Student("Snoopy", 12),
            null
    );
    students.sort(Comparator.nullsFirst(Comparator.comparing(Student::getName)));
    Assertions.assertNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNotNull(students.get(2));
}

Is it very simple? Next, let's see how to realize the logic that the field of sorting condition is null.

The field of the sort condition is null

This is the combination with comparator, which is like a dolly. Comparator.nullsLast needs to be used twice. The implementation is listed here:

@Test
void sortedNullFieldLast() {
    final List<Student> students = Lists.newArrayList(
            new Student(null, 10),
            new Student("Snoopy", 12),
            null
    );
    final Comparator<Student> nullsLast = Comparator.nullsLast(
            Comparator.nullsLast( // 1
                    Comparator.comparing(
                            Student::getName,
                            Comparator.nullsLast( // 2
                                    Comparator.naturalOrder() // 3
                            )
                    )
            )
    );
    students.sort(nullsLast);
    Assertions.assertEquals(students.get(0), new Student("Snoopy", 12));
    Assertions.assertEquals(students.get(1), new Student(null, 10));
    Assertions.assertNull(students.get(2));
}

The code logic is as follows:

  1. Code 1 is the first layer of null safe logic, which is used to judge whether the element is null;

  2. Code 2 is the second layer of null safe logic, which is used to judge whether the condition field of the element is null;

  3. Code 3 is a conditional comparator. Comparator.naturalOrder() is used here because String sorting is used, which can also be written as String::compareTo. If it is a complex judgment, you can define a more complex comparator. The combination mode is so easy to use that one layer is not enough and another layer is set.

Summary at the end of the paper

This article demonstrates the use of Lambda expressions in Java 8 to implement various sorting logic, and the new syntax is really sweet.

This article is reproduced in WeChat official account macrozheng.

Posted by binit on Thu, 02 Dec 2021 22:22:46 -0800