JavaRush /בלוג Java /Random-HE /משווה בג'אווה
Viacheslav
רָמָה

משווה בג'אווה

פורסם בקבוצה
רק עצלנים לא כתבו על Comparator והשוואה בג'אווה. אני לא עצלן - אז אני מבקש מכם לאהוב ולהעדיף עוד וריאציה אחת. אני מקווה שזה לא יהיה מיותר. וכן, מאמר זה הוא התשובה לשאלה: "האם אתה יכול לכתוב משווה מהזיכרון?" אני מקווה שאחרי קריאת המאמר הזה כולם יוכלו לכתוב משווה מהזיכרון.
משווה בג'אווה - 1
מבוא Java ידועה כשפה מונחה עצמים. כתוצאה מכך, ב-Java מקובל לפעול עם אובייקטים. אבל במוקדם או במאוחר מתעוררת המשימה של השוואת אובייקטים לפי עיקרון כלשהו. אז, בהינתן: יש לנו הודעה כלשהי, שמתוארת על ידי המחלקה Message:
public static class Message {
    private String message;
    private int id;

    public Message(String message) {
        this.message = message;
        this.id = new Random().nextInt(1000);
    }
    public String getMessage() {
        return message;
    }
    public Integer getId() {
        return id;
    }
    public String toString() {
        return "[" + id + "] " + message;
    }
}
בואו נוסיף את המחלקה הזו למהדר Java של Tutorialspoint . בואו נזכור גם להוסיף יבוא:
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
בשיטה הראשית ניצור מספר הודעות:
public static void main(String[] args){
    List<Message> messages = new ArrayList();
    messages.add(new Message("Hello, World!"));
    messages.add(new Message("Hello, Sun!"));
    System.out.println(messages);
}
בואו נחשוב מה עלינו לעשות אם אנחנו רוצים להשוות ביניהם? לדוגמה, אנו רוצים למיין לפי id. וכדי ליצור סדר, צריך איכשהו להשוות בין אובייקטים כדי להבין איזה אובייקט קודם (כלומר קטן יותר) ואיזה הבא (כלומר גדול יותר). נתחיל עם מחלקה כמו java.lang.Object . כפי שאנו יודעים, כל המחלקות יורשות באופן מרומז ממחלקת האובייקט הזו. וזה הגיוני, כי זה בעצם מבטא את המושג: "הכל הוא אובייקט" ומספק התנהגות משותפת לכל הכיתות. והמחלקה הזו מגדירה שלכל מחלקה שתי שיטות: → hashCode שיטת hashCode מחזירה ייצוג מספרי (int) כלשהו של האובייקט כמופע של המחלקה. מה זה אומר? זה אומר שאם יצרת שני מופעים שונים של מחלקה, אז מכיוון שהמופעים שונים, ה-hashCode שלהם צריך להיות שונה. זה מה שכתוב בתיאור השיטה: "ככל שהיא מעשית באופן סביר, שיטת ה-hashCode המוגדרת על ידי המחלקה Object מחזירה מספרים שלמים נפרדים עבור אובייקטים נפרדים" כלומר, אם אלו שני מופעים שונים, אז הם צריכים להיות שונים hashCodes. כלומר, שיטה זו אינה מתאימה להשוואה שלנו. → שווה שיטת שווה עונה על השאלה "האם אובייקטים שווים" ומחזירה ערך בוליאני. לשיטה זו יש את קוד ברירת המחדל:
public boolean equals(Object obj) {
    return (this == obj);
}
כלומר, מבלי לעקוף שיטה זו על אובייקט, שיטה זו בעצם אומרת אם ההפניות לאובייקט תואמות או לא. זה לא מתאים להודעות שלנו, כי אנחנו לא מעוניינים בקישורים לאובייקט, אנחנו מעוניינים במזהה ההודעה. וגם אם נעקוף את שיטת השווים, המקסימום שנקבל הוא: "הם שווים" או "הם לא שווים". אבל זה לא מספיק לנו כדי לקבוע את הסדר.

Comparator והשוואה ב-Java

מה מתאים לנו? אם נתרגם את המילה "השוואה" לאנגלית במתרגם, נקבל את התרגום "השוואה". מצוין, אז אנחנו צריכים מישהו שישווה. אם אתה משווה את ההשוואה הזו, אז מי שמשווה הוא המשווה. בואו נפתח את Java Api ונמצא שם Comparator . ואכן, יש ממשק כזה - java.util.Comparator java.util.Comparator ו-java.lang.Comparable כפי שאתה יכול לראות, יש ממשק כזה. המחלקה המיישמת את זה אומרת ש"אני מיישם פונקציה להשוואת אובייקטים." הדבר היחיד שצריך לזכור באמת הוא חוזה ההשוואה, שמתבטא כך:

Comparator возвращает int по следующей схеме: 
  • отрицательный int (первый an object отрицательный, то есть меньше)
  • положительный int (первый an object положительный, хороший, то есть больший)
  • ноль = an objectы равны
עכשיו בואו נכתוב משווה. נצטרך לייבא את java.util.Comparator . לאחר הייבוא, הוסף שיטה ל-main: Comparator<Message> comparator = new Comparator<Message>(); באופן טבעי, זה לא יעבוד, כי Comparator הוא ממשק. לכן, אחרי הסוגריים נוסיף מתולתלים { }. בסוגריים אלו נכתוב את השיטה:
public int compare(Message o1, Message o2) {
    return o1.getId().compareTo(o2.getId());
}
אתה אפילו לא צריך לזכור לכתוב את זה. משווה הוא מי שמבצע השוואה, כלומר עושה השוואה. כדי לענות על השאלה באיזה סדר נמצאים האובייקטים בהשוואה, נחזיר int. זה הכל, בעצם. פשוט ובקלות. כפי שאנו יכולים לראות מהדוגמה, בנוסף ל-Comparator, יש ממשק נוסף - java.lang.Comparable , המיישם אותו עלינו להגדיר את שיטת compareTo . ממשק זה אומר ש"מחלקה המיישמת ממשק מאפשרת השוואה בין מופעים של המחלקה." לדוגמה, היישום של CompareTo של Integer נראה כך:
(x < y) ? -1 : ((x == y) ? 0 : 1)
איך לזכור את כל הממשקים האלה? בשביל מה? הכל בא מאנגלית. השווה - להשוואה, מי שמשווה הוא Comparator (כרשם, למשל. כלומר, מי שנרשם), ושם התואר "השוו" הוא Comparable. ובכן, "השווה עם" מתורגם לא רק כהשווה עם, אלא גם כהשוואה ל. זה פשוט. שפת ג'אווה נכתבה על ידי אנשים דוברי אנגלית, ובמתן שם לכל דבר בג'אווה הם הודרכו פשוט על ידי אנגלית והיה איזשהו היגיון במתן השם. ומתודה compareTo מתארת ​​כיצד יש להשוות מופע של מחלקה עם מופעים אחרים. לדוגמה, מחרוזות מושווים בצורה לקסיקוגרפית ומספרים מושווים לפי ערך.
Comparator ב-Java - 2
Java 8 הביאה כמה שינויים נחמדים. אם נסתכל מקרוב על ממשק Comparator, נראה שיש הערה מעליו @FunctionalInterface. למעשה, ההערה הזו מיועדת למידע ומשמעותה שהממשק הזה פונקציונלי. המשמעות היא שלממשק הזה יש רק שיטה מופשטת אחת ללא יישום. מה זה נותן לנו? אנו יכולים לכתוב את קוד ההשוואה כעת כך:
Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
בסוגריים זה איך אנחנו קוראים למשתנים. ג'אווה עצמה תראה את זה כי... אם יש רק שיטה אחת, אז ברור אילו פרמטרים קלט נדרשים, כמה, ואיזה סוגים. לאחר מכן, אנחנו אומרים עם חץ שאנחנו רוצים להעביר אותם לקטע הזה של הקוד. בנוסף, הודות ל-Java 8, הופיעו שיטות ברירת מחדל בממשקים – אלו שיטות המופיעות כברירת מחדל (ברירת מחדל) כאשר אנו מיישמים ממשק. יש כמה כאלה בממשק Comparator. לדוגמה:
Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
יש שיטה אחרת שתהפוך את הקוד שלך לנקי יותר. הבה נסתכל על הדוגמה לעיל, שבה תיארנו את המשווה שלנו. מה הוא עושה? זה די פרימיטיבי. זה פשוט לוקח אובייקט ומוציא ממנו ערך שהוא בר השוואה. לדוגמה, Integer מיישמת דומה, כך שהצלחנו לבצע compareTo על ערכי מזהה הודעה. ניתן לכתוב את פונקציית ההשוואה הפשוטה הזו גם כך:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
כלומר, פשוטו כמשמעו, "יש לנו Comparator שמשווה כך: הוא לוקח אובייקטים, מקבל מהם Comparable באמצעות שיטת getId() ומשווה באמצעות compareTo." ואין עוד עיצובים נוראיים. ולבסוף, אני רוצה לציין עוד תכונה אחת. ניתן לשרשר בין משווים. לדוגמה:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());

יישום

הצהרת המשווה התבררה כהגיונית למדי, לא? כעת עלינו לראות כיצד להשתמש בו ובאילו מקומות. ← Collections.sort (java.util.Collections) כמובן, אנו יכולים למיין אוספים בצורה זו. אבל לא הכל, רק רשימות. ואין כאן שום דבר יוצא דופן, כי... זוהי הרשימה שדורשת גישה לרכיב לפי אינדקס. וזה מאפשר להחליף אלמנט מספר שתיים עם אלמנט מספר שלוש. לכן, מיון בדרך זו אפשרי רק עבור רשימות:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
Arrays.sort (java.util.Arrays) מערכים נוחים גם למיון. שוב, מאותה סיבה של גישה לאלמנטים לפי אינדקס. ← צאצאים של java.util.SortedSet ו-java.util.SortedMap כזכור, סט ומפה אינם מבטיחים את סדר אחסון הרשומות. אבל יש לנו יישומים מיוחדים שמבטיחים סדר. ואם רכיבי האוסף אינם מיישמים java.lang.Comparable, אז נוכל להעביר Comparator לבונה של אוספים כאלה:
Set<Message> msgSet = new TreeSet(comparator);
Stream API ב- Stream Api, שהופיע ב-Java 8, השוואת מאפשרת לך לפשט את העבודה על רכיבי זרם. לדוגמה, אנו זקוקים לרצף של מספרים אקראיים מ-0 עד 999 כולל:
Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
    .limit(10)
    .sorted(Comparator.naturalOrder())
    .forEach(e -> System.out.println(e));
יכולנו לעצור, אבל יש בעיות מעניינות יותר. לדוגמה, עליך להכין מפה, כאשר המפתח הוא מזהה ההודעה. יחד עם זאת, אנו רוצים למיין מפתחות אלו כך שהמפתחות יהיו מסודרים, מהקטנים לגדולים ביותר. נתחיל עם הקוד הזה:
Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
מה שנקבל כאן הוא למעשה HashMap. וכידוע, זה לא מבטיח שום סדר. לכן, הרישומים שלנו ממוינים לפי תעודת זהות פשוט יצאו מכלל תקינות. לא טוב. נצטרך לשנות מעט את האספן שלנו:
Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg, (oldValue, newValue) -> oldValue, TreeMap::new));
הקוד נראה קצת יותר מצמרר, אבל הבעיה נפתרה כעת בצורה נכונה הודות ליישום המפורש של TreeMap. עוד על הקבוצות השונות תוכלו לקרוא כאן: אתה יכול ליצור את האספן בעצמך. אתה יכול לקרוא עוד כאן: "יצירת אספן מותאם אישית ב-Java 8" . וזה שימושי לקרוא את הדיון כאן: "רשימת Java 8 למיפוי עם זרם" .
משווה בג'אווה - 3
מגרפות משווה ומגרפות דומות הן טובות. אבל יש ניואנס אחד הקשור בהם שכדאי לזכור. כאשר מחלקה מבצעת מיון, היא מחשבת שהיא יכולה להעביר את המחלקה שלך ל- Comparable. אם זה לא המקרה, תקבל שגיאה בזמן הביצוע. בואו נסתכל על דוגמה:
SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
נראה שאין כאן שום דבר רע. אבל למעשה, בדוגמה שלנו, הוא יקרוס עם השגיאה: java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable והכל בגלל שהוא ניסה למיין את האלמנטים (זה בסופו של דבר SortedSet). ולא יכולתי. עליך לזכור זאת בעת עבודה עם SortedMap ו- SortedSet. בנוסף מומלץ לצפייה: יורי טקך: HashSet ו-TreeSet - אוספים #1 - Java מתקדם
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION