Java 8 - השוואה עוצמתית עם Lambdas

1. סקירה כללית

במדריך זה, אנו נסתכל ראשונה על ה- תמיכה במבדה ב- Java 8 - באופן ספציפי כיצד למנף אותה לכתיבת ה- משווה ולמיין אוסף.

מאמר זה הוא חלק מסדרת "Java - Back to Basic" כאן בבלדונג.

ראשית, בואו נגדיר מחלקת ישות פשוטה:

מעמד ציבורי אנושי {שם מחרוזת פרטי; גיל פרטי פרטי; // בונים סטנדרטיים, גטרים / סטרים, שווים ו- hashcode} 

2. מיון בסיסי ללא למבדות

לפני Java 8, מיון אוסף יהיה כרוך יצירת מעמד פנימי אנונימי עבור משווה משמש במיון:

השוואה חדשה () {@Override public int השווה (אנושי h1, אנושי h2) {החזר h1.getName (). CompareTo (h2.getName ()); }}

זה פשוט ישמש למיון ה- רשימה שֶׁל בן אנוש ישויות:

@ מבחן בטל פומבי שניתן PreLambda_whenSortingEntitiesByName_thenCorrectlySorted () {רשימת בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); Collections.sort (בני אדם, משווה חדש () {@Override public int השווה (אנושי h1, אנושי h2) {להחזיר h1.getName (). CompareTo (h2.getName ());}}); Assert.assertThat (בני אדם. Get (0), שווה ל- (אנושי חדש ("ג'ק", 12))); }

3. מיון בסיסי עם תמיכת למבדה

עם כניסת Lambdas, אנו יכולים כעת לעקוף את המעמד הפנימי האנונימי ולהשיג את אותה תוצאה באמצעות סמנטיקה פשוטה ופונקציונלית:

(סופי אנושי h1, סופי אנושי h2) -> h1.getName (). CompareTo (h2.getName ());

באופן דומה - כעת אנו יכולים לבדוק את ההתנהגות בדיוק כמו בעבר:

@ מבחן פומבי בטל כאשרSortingEntitiesByName_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); בני אדם.סורט ((אנושי h1, אנושי h2) -> h1.getName (). CompareTo (h2.getName ())); assertThat (בני אדם. get (0), שווה ל- (אנושי חדש ("ג'ק", 12))); }

שימו לב שאנחנו משתמשים גם בזה החדש סוג API נוסף ל- java.util.List בג'אווה 8 - במקום הישן Collections.sort ממשק API.

4. מיון בסיסי ללא הגדרות סוג

אנו יכולים לפשט עוד יותר את הביטוי על ידי אי ציון הגדרות הסוג - המהדר מסוגל להסיק אלה בכוחות עצמו:

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

ושוב, המבחן נשאר דומה מאוד:

@ מבחן בטל פומבי שניתן LambdaShortForm_whenSortingEntitiesByName_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); בני אדם.סורט ((h1, h2) -> h1.getName (). CompareTo (h2.getName ())); assertThat (בני אדם. get (0), שווה ל- (אנושי חדש ("ג'ק", 12))); }

5. מיין לפי הפניה לשיטה סטטית

לאחר מכן, אנו נערוך את המיון באמצעות ביטוי למבדה עם התייחסות לשיטה סטטית.

ראשית, נגדיר את השיטה CompareByNameThenAge עם אותה חתימה בדיוק כמו לְהַשְׁווֹת שיטה בא משווה לְהִתְנַגֵד:

int static public CompareByNameThenAge (Human lhs, Human rhs) {if (lhs.name.equals (rhs.name)) {return Integer.compare (lhs.age, rhs.age); } אחר {להחזיר lhs.name.compareTo (rhs.name); }}

עכשיו, אנחנו הולכים להתקשר ל בני אדם שיטה עם התייחסות זו:

humans.sort (אנושי :: CompareByNameThenAge);

התוצאה הסופית היא מיון עבודה של האוסף בשיטה הסטטית כ- משווה:

@Test public void givenMethodDefinition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); humans.sort (אנושי :: CompareByNameThenAge); Assert.assertThat (בני אדם. Get (0), שווה ל- (אנושי חדש ("ג'ק", 12)); }

6. מיון השוואות מחולצות

אנו יכולים גם להימנע מהגדרת אפילו לוגיקת ההשוואה עצמה באמצעות הפניה לשיטת מופע וה Comparator. השוואה שיטה - המחלצת ויוצרת א ניתן להשוות בהתבסס על פונקציה זו.

אנחנו הולכים להשתמש בגטר getName () לבניית הביטוי למבדה ולמיין את הרשימה לפי שם:

@Test public void givenInstanceMethod_whenSortingEntitiesByName_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); Collections.sort (בני אדם, Comparator.comparing (אנושי :: getName)); assertThat (בני אדם. get (0), שווה ל- (אנושי חדש ("ג'ק", 12))); }

7. מיון הפוך

JDK 8 הציגה גם שיטת עוזר עבור היפוך השוואה אנו יכולים לעשות זאת במהירות כדי להפוך את המיון שלנו:

@Test הציבור בטל כאשרSortingEntitiesByNameReversed_thenCorrectlySorted () {רשימת בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); משווה השוואה = (h1, h2) -> h1.getName (). CompareTo (h2.getName ()); בני אדם. סוג (comparator.reversed ()); Assert.assertThat (בני אדם. Get (0), שווה ל- (אנושי חדש ("שרה", 10))); }

8. מיין לפי מספר תנאים

ההשוואה בין ביטויי למבדה אינה צריכה להיות כה פשוטה - אנו יכולים לכתוב ביטויים מורכבים יותר גם כן - למשל למיין את הישויות תחילה לפי שמות, ואז לפי גיל:

@Test הציבור בטל כאשרSortingEntitiesByNameThenAge_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 12), אנושי חדש ("שרה", 10), אנושי חדש ("זק", 12)); human.sort ((lhs, rhs) -> {if (lhs.getName (). שווה ל- (rhs.getName ())) {return Integer.compare (lhs.getAge (), rhs.getAge ());} אחר {החזר lhs.getName (). CompareTo (rhs.getName ());}}); Assert.assertThat (בני אדם. Get (0), שווה ל- (אנושי חדש ("שרה", 10))); }

9. מיין לפי תנאים מרובים - קומפוזיציה

את אותו לוגיקת ההשוואה - תחילה מיון לפי שם ואז, שנית, לפי גיל - ניתן ליישם גם על ידי התמיכה החדשה בהרכב משווה.

החל מ- JDK 8, כעת אנו יכולים לשרשר בין מספר משווים לבנות לוגיקת השוואה מורכבת יותר:

@ מבחן בטל פומבי שניתןComposition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 12), אנושי חדש ("שרה", 10), אנושי חדש ("זק", 12)); human.sort (Comparator.comparing (Human :: getName) .thenComparing (Human :: getAge)); Assert.assertThat (בני אדם. Get (0), שווה ל- (אנושי חדש ("שרה", 10))); }

10. מיון רשימה באמצעות Stream.sorted ()

אנחנו יכולים גם למיין אוסף באמצעות Java 8 זרםמְמוּיָן() ממשק API.

אנו יכולים למיין את הזרם באמצעות הזמנה טבעית כמו גם הזמנה הניתנת על ידי א משווה. לשם כך, יש לנו שתי גרסאות עמוסות מדי של ה- מְמוּיָן() ממשק API:

  • סוגed () ממיין את האלמנטים של א זרם שימוש בסדר טבעי; מחלקת האלמנטים חייבת ליישם את ניתן להשוות מִמְשָׁק.
  • ממוינת (Comparator סוּפֶּר T> קומפאהרטור) - ממיין את האלמנטים על בסיס a משווה למשל

בואו נראה דוגמה כיצד להשתמש ב מְמוּיָן() שיטה עם הזמנה טבעית:

@ מבחן סופי ציבורי בטל נתון StreamNaturalOrdering_whenSortingEntitiesByName_thenCorrectlySorted () {רשימה אותיות = Lists.newArrayList ("B", "A", "C"); List sortedLetters = letters.stream (). Sorted (). Collect (Collectors.toList ()); assertThat (sortedLetters.get (0), equalTo ("A")); }

עכשיו בואו נראה איך אנחנו יכולים השתמש בהתאמה אישית משווה עם ה מְמוּיָן() ממשק API:

@ מבחן סופי ציבורי בטל נתון StreamCustomOrdering_whenSortingEntitiesByName_thenCorrectlySorted () {רשימת בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); Comparator nameComparator = (h1, h2) -> h1.getName (). CompareTo (h2.getName ()); רשימה sortedHumans = בני אדם.זרם (). ממוינים (nameComparator) .collect (Collectors.toList ()); assertThat (sortedHumans.get (0), equalTo (אדם חדש ("ג'ק", 12))); }

אנו יכולים לפשט את הדוגמה לעיל עוד יותר אם אנו להשתמש ב Comparator.comparing () שיטה:

@ מבחן סופי ציבורי @StreamComparatorOrdering_whenSortingEntitiesByName_thenCorrectlySorted () {רשימת בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); רשימה sortedHumans = בני אדם.זרם () .ממוין (Comparator.comparing (אנושי :: getName)) .collect (Collectors.toList ()); assertThat (sortedHumans.get (0), equalTo (אדם חדש ("ג'ק", 12)); }

11. מיון רשימה בהפוך עם Stream.sorted ()

אנחנו יכולים גם להשתמש Stream.sorted () למיין אוסף הפוך.

ראשית, בואו נראה דוגמה כיצד לשלב את מְמוּיָן() שיטה עם Comparator.reverseOrder () למיין רשימה בסדר הטבעי ההפוך:

@ מבחן סופי ציבורי בטל נתון StreamNaturalOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted () {אותיות רשימה = Lists.newArrayList ("B", "A", "C"); רשימת reverseSortedLetters = letters.stream () .sorted (Comparator.reverseOrder ()) .collect (Collectors.toList ()); assertThat (reverseSortedLetters.get (0), equalTo ("C")); }

עכשיו בואו נראה איך אנחנו יכולים להשתמש ב מְמוּיָן() שיטה ומנהג משווה:

@Test הסופי הציבורי בטל שניתןStreamCustomOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); Comparator reverseName Comparator = (h1, h2) -> h2.getName (). CompareTo (h1.getName ()); רשימת reverseSortedHumans = בני אדם.זרם (). ממוינים (reverseNameComparator) .collect (Collectors.toList ()); assertThat (reverseSortedHumans.get (0), equalTo (אנושי חדש ("שרה", 10))); }

שים לב כי הקריאה של בהשוואה ל הוא התהפך, וזה מה שעושה את ההיפוך.

לבסוף, בואו נפשט את הדוגמה שלעיל על ידי משתמש ב Comparator.comparing () שיטה:

@ מבחן סופי ציבורי @StreamComparatorOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted () {רשימה בני אדם = Lists.newArrayList (אנושי חדש ("שרה", 10), אנושי חדש ("ג'ק", 12)); רשימת reverseSortedHumans = בני אדם.זרם () .מגוון (Comparator.comparing (אנושי :: getName, Comparator.reverseOrder ())) .collect (Collectors.toList ()); assertThat (reverseSortedHumans.get (0), equalTo (אנושי חדש ("שרה", 10))); }

12. ערכים אפסיים

עד כה יישמנו את שלנו משווהבאופן שהם לא יכולים למיין אוספים המכילים ריק ערכים. כלומר, אם האוסף מכיל לפחות אחד ריק אלמנט, ואז ה- סוג שיטה זורקת א NullPointerException:

@Test (צפוי = NullPointerException.class) חלל ציבורי שניתן ANullElement_whenSortingEntitiesByName_thenThrowsNPE () {רשימה בני אדם = Lists.newArrayList (null, אנושי חדש ("ג'ק", 12)); בני אדם.סורט ((h1, h2) -> h1.getName (). CompareTo (h2.getName ())); }

הפתרון הפשוט ביותר הוא טיפול ב- ריק ערכים באופן ידני שלנו משווה יישום:

@ מבחן בטל פומבי שניתן ANullElement_whenSortingEntitiesByNameManually_thenMovesTheNullToLast () {רשימה בני אדם = Lists.newArrayList (null, אנושי חדש ("ג'ק", 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 (בני אדם. Get (0)); Assert.assertNull (בני אדם. Get (1)); Assert.assertNull (בני אדם. Get (2)); }

הנה אנחנו דוחפים הכל ריק אלמנטים לקראת סוף האוסף. לשם כך, שוקל המשווה ריק להיות גדול מערכים שאינם אפסיים. כששניהם כן ריק, הם נחשבים שווים.

בנוסף, אנחנו יכולים לעבור כל אחד משווה זה לא בטוח ללא בטחון Comparator.nullsLast () שיטה ולהשיג את אותה תוצאה:

@ מבחן בטל פומבי שניתן ANullElement_whenSortingEntitiesByName_thenMovesTheNullToLast () {רשימה בני אדם = Lists.newArrayList (null, אנושי חדש ("ג'ק", 12), null); בני אדם.סורט (Comparator.nullsLast (Comparator.comparing (אנושי :: getName))); Assert.assertNotNull (בני אדם. Get (0)); Assert.assertNull (בני אדם. Get (1)); Assert.assertNull (בני אדם. Get (2)); }

באופן דומה, אנו יכולים להשתמש Comparator.nullsFirst () להזיז את ריק אלמנטים לקראת תחילת האוסף:

@ מבחן בטל פומבי שניתן ANullElement_whenSortingEntitiesByName_thenMovesTheNullToStart () {רשימה בני אדם = Lists.newArrayList (null, אנושי חדש ("ג'ק", 12), null); בני אדם.סורט (Comparator.nullsFirst (Comparator.comparing (אנושי :: getName))); Assert.assertNull (בני אדם. Get (0)); Assert.assertNull (בני אדם. Get (1)); Assert.assertNotNull (בני אדם. Get (2)); } 

מומלץ מאוד להשתמש ב- nullsFirst () אוֹ nullsLast () מעצבים, מכיוון שהם גמישים יותר ובעיקר קריאים יותר.

13. מסקנה

מאמר זה המחיש את הדרכים השונות והמרתקות בהן א ניתן למיין את הרשימה באמצעות Java 8 Lambda Expressions - לעבור ימינה מעבר לסוכר התחבירי ולסמנטיקה פונקציונאלית אמיתית וחזקה.

ניתן למצוא את היישום של כל הדוגמאות וקטעי הקוד ב- GitHub.