Java 8 - Perbandingan Kuat dengan Lambdas

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat terlebih dahulu sokongan Lambda di Java 8 - khususnya bagaimana memanfaatkannya untuk menulis Comparator dan menyusun Koleksi .

Artikel ini adalah sebahagian daripada siri "Java - Back to Basic" di Baeldung.

Pertama, mari tentukan kelas entiti mudah:

public class Human { private String name; private int age; // standard constructors, getters/setters, equals and hashcode } 

2. Susun Asas Tanpa Lambdas

Sebelum Java 8, menyusun koleksi akan melibatkan membuat kelas dalaman tanpa nama untuk Perbandingan yang digunakan dalam urutan:

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

Ini hanya akan digunakan untuk menyusun Senarai of Human entiti:

@Test public void givenPreLambda_whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); Collections.sort(humans, new Comparator() { @Override public int compare(Human h1, Human h2) { return h1.getName().compareTo(h2.getName()); } }); Assert.assertThat(humans.get(0), equalTo(new Human("Jack", 12))); }

3. Susun Asas Dengan Sokongan Lambda

Dengan pengenalan Lambdas, kita sekarang dapat melewati kelas dalaman tanpa nama dan mencapai hasil yang sama dengan semantik yang mudah dan berfungsi :

(final Human h1, final Human h2) -> h1.getName().compareTo(h2.getName());

Begitu juga - kita sekarang boleh menguji tingkah laku seperti sebelumnya:

@Test public void whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); humans.sort( (Human h1, Human h2) -> h1.getName().compareTo(h2.getName())); assertThat(humans.get(0), equalTo(new Human("Jack", 12))); }

Notis yang kami juga menggunakan baru jenis API ditambah kepada java.util.List di Jawa 8 - bukan lama Collections.sort API.

4. Pengelasan Asas Tanpa Definisi Jenis

Kami dapat mempermudah ungkapan dengan tidak menentukan definisi jenis - penyusun mampu menyimpulkannya sendiri:

(h1, h2) -> h1.getName().compareTo(h2.getName())

Dan sekali lagi, ujiannya tetap serupa:

@Test public void givenLambdaShortForm_whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); humans.sort((h1, h2) -> h1.getName().compareTo(h2.getName())); assertThat(humans.get(0), equalTo(new Human("Jack", 12))); }

5. Susun Menggunakan Rujukan kepada Kaedah Statik

Seterusnya, kita akan melakukan semacam menggunakan Lambda Expression dengan merujuk kepada kaedah statik.

Pertama, kita akan menentukan kaedah membandingkanByNameThenAge - dengan tandatangan yang sama dengan kaedah membandingkan dalam objek Perbandingan :

public static int compareByNameThenAge(Human lhs, Human rhs) { if (lhs.name.equals(rhs.name)) { return Integer.compare(lhs.age, rhs.age); } else { return lhs.name.compareTo(rhs.name); } }

Sekarang, kita akan memanggil kaedah human.sort dengan rujukan ini:

humans.sort(Human::compareByNameThenAge);

Hasil akhirnya adalah pengumpulan koleksi yang berfungsi menggunakan kaedah statik sebagai Pembanding :

@Test public void givenMethodDefinition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); humans.sort(Human::compareByNameThenAge); Assert.assertThat(humans.get(0), equalTo(new Human("Jack", 12))); }

6. Susun Pembanding yang Diekstrak

Kita juga boleh mengelakkan menentukan logik perbandingan itu sendiri dengan menggunakan rujukan kaedah contoh dan kaedah perbandingan Comparator.com - yang mengekstrak dan membuat Perbandingan berdasarkan fungsi tersebut.

Kami akan menggunakan getter getName () untuk membina ungkapan Lambda dan menyusun senarai mengikut nama:

@Test public void givenInstanceMethod_whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); Collections.sort( humans, Comparator.comparing(Human::getName)); assertThat(humans.get(0), equalTo(new Human("Jack", 12))); }

7. Susun Balik

JDK 8 juga telah memperkenalkan kaedah pembantu untuk membalikkan pembanding - kami dapat menggunakannya dengan cepat untuk membalikkan jenis kami:

@Test public void whenSortingEntitiesByNameReversed_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 10), new Human("Jack", 12) ); Comparator comparator = (h1, h2) -> h1.getName().compareTo(h2.getName()); humans.sort(comparator.reversed()); Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10))); }

8. Susun Dengan Pelbagai Keadaan

Perbandingan ungkapan lambda tidak semudah ini - kita juga boleh menulis ungkapan yang lebih kompleks - misalnya menyusun entiti dengan nama, dan kemudian mengikut usia:

@Test public void whenSortingEntitiesByNameThenAge_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 12), new Human("Sarah", 10), new Human("Zack", 12) ); humans.sort((lhs, rhs) -> { if (lhs.getName().equals(rhs.getName())) { return Integer.compare(lhs.getAge(), rhs.getAge()); } else { return lhs.getName().compareTo(rhs.getName()); } }); Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10))); }

9. Susun Dengan Pelbagai Keadaan - Komposisi

Logik perbandingan yang sama - menyusun pertama berdasarkan nama dan kemudian, kedua mengikut usia - juga dapat dilaksanakan oleh sokongan komposisi baru untuk Perbandingan .

Bermula dengan JDK 8, kita kini dapat menyatukan pelbagai pembanding untuk membina logik perbandingan yang lebih kompleks:

@Test public void givenComposition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted() { List humans = Lists.newArrayList( new Human("Sarah", 12), new Human("Sarah", 10), new Human("Zack", 12) ); humans.sort( Comparator.comparing(Human::getName).thenComparing(Human::getAge) ); Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10))); }

10. Menyusun Senarai Dengan Aliran. Disusun ()

Kita juga dapat menyusun koleksi menggunakan Java 8's Stream sorted () API.

Kami dapat menyusun aliran menggunakan susunan semula jadi dan juga pesanan yang diberikan oleh seorang Pembanding. Untuk ini, kami mempunyai dua varian kelebihan API yang disusun :

  • sort ed () - menyusun elemen Aliran menggunakan susunan semula jadi; kelas elemen mesti melaksanakan antara muka yang boleh dibandingkan .
  • sorted(Comparator super T> comparator) – sorts the elements based on a Comparator instance

Let's see an example of how to use the sorted() method with natural ordering:

@Test public final void givenStreamNaturalOrdering_whenSortingEntitiesByName_thenCorrectlySorted() { List letters = Lists.newArrayList("B", "A", "C"); List sortedLetters = letters.stream().sorted().collect(Collectors.toList()); assertThat(sortedLetters.get(0), equalTo("A")); }

Now let's see how we can use a custom Comparator with the sorted() API:

@Test public final void givenStreamCustomOrdering_whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12)); Comparator nameComparator = (h1, h2) -> h1.getName().compareTo(h2.getName()); List sortedHumans = humans.stream().sorted(nameComparator).collect(Collectors.toList()); assertThat(sortedHumans.get(0), equalTo(new Human("Jack", 12))); }

We can simplify the above example even further if we use the Comparator.comparing() method:

@Test public final void givenStreamComparatorOrdering_whenSortingEntitiesByName_thenCorrectlySorted() { List humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12)); List sortedHumans = humans.stream() .sorted(Comparator.comparing(Human::getName)) .collect(Collectors.toList()); assertThat(sortedHumans.get(0), equalTo(new Human("Jack", 12))); }

11. Sorting a List in Reverse With Stream.sorted()

We can also use Stream.sorted() to sort a collection in reverse.

First, let's see an example of how to combine the sorted() method with Comparator.reverseOrder() to sort a list in the reverse natural order:

@Test public final void givenStreamNaturalOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() { List letters = Lists.newArrayList("B", "A", "C"); List reverseSortedLetters = letters.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); assertThat(reverseSortedLetters.get(0), equalTo("C")); }

Now, let's see how we can use the sorted() method and a custom Comparator:

@Test public final void givenStreamCustomOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() { List humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12)); Comparator reverseNameComparator = (h1, h2) -> h2.getName().compareTo(h1.getName()); List reverseSortedHumans = humans.stream().sorted(reverseNameComparator) .collect(Collectors.toList()); assertThat(reverseSortedHumans.get(0), equalTo(new Human("Sarah", 10))); }

Note that the invocation of compareTo is flipped, which is what is doing the reversing.

Finally, let's simplify the above example by using the Comparator.comparing() method:

@Test public final void givenStreamComparatorOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() { List humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12)); List reverseSortedHumans = humans.stream() .sorted(Comparator.comparing(Human::getName, Comparator.reverseOrder())) .collect(Collectors.toList()); assertThat(reverseSortedHumans.get(0), equalTo(new Human("Sarah", 10))); }

12. Null Values

So far, we implemented our Comparators in a way that they can't sort collections containing null values. That is, if the collection contains at least one null element, then the sort method throws a NullPointerException:

@Test(expected = NullPointerException.class) public void givenANullElement_whenSortingEntitiesByName_thenThrowsNPE() { List humans = Lists.newArrayList(null, new Human("Jack", 12)); humans.sort((h1, h2) -> h1.getName().compareTo(h2.getName())); }

The simplest solution is to handle the null values manually in our Comparator implementation:

@Test public void givenANullElement_whenSortingEntitiesByNameManually_thenMovesTheNullToLast() { List humans = Lists.newArrayList(null, new Human("Jack", 12), null); humans.sort((h1, h2) -> { if (h1 == null) { return h2 == null ? 0 : 1; } else if (h2 == null) { return -1; } return h1.getName().compareTo(h2.getName()); }); Assert.assertNotNull(humans.get(0)); Assert.assertNull(humans.get(1)); Assert.assertNull(humans.get(2)); }

Here we're pushing all null elements towards the end of the collection. To do that, the comparator considers null to be greater than non-null values. When both are null, they are considered equal.

Additionally, we can pass any Comparator that is not null-safe into the Comparator.nullsLast() method and achieve the same result:

@Test public void givenANullElement_whenSortingEntitiesByName_thenMovesTheNullToLast() { List humans = Lists.newArrayList(null, new Human("Jack", 12), null); humans.sort(Comparator.nullsLast(Comparator.comparing(Human::getName))); Assert.assertNotNull(humans.get(0)); Assert.assertNull(humans.get(1)); Assert.assertNull(humans.get(2)); }

Similarly, we can use Comparator.nullsFirst() to move the null elements towards the start of the collection:

@Test public void givenANullElement_whenSortingEntitiesByName_thenMovesTheNullToStart() { List humans = Lists.newArrayList(null, new Human("Jack", 12), null); humans.sort(Comparator.nullsFirst(Comparator.comparing(Human::getName))); Assert.assertNull(humans.get(0)); Assert.assertNull(humans.get(1)); Assert.assertNotNull(humans.get(2)); } 

It's highly recommended to use the nullsFirst() or nullsLast() decorators, as they're more flexible and, above all, more readable.

13. Conclusion

This article illustrated the various and exciting ways that a List can be sorted using Java 8 Lambda Expressions – moving right past syntactic sugar and into real and powerful functional semantics.

The implementation of all these examples and code snippets can be found over on GitHub.