JavaRush /בלוג Java /Random-HE /השוואת חפצים: תרגול
articles
רָמָה

השוואת חפצים: תרגול

פורסם בקבוצה
זהו המאמר השני מבין המאמרים המוקדשים להשוואת חפצים. הראשון שבהם דן בבסיס התיאורטי של ההשוואה - כיצד היא נעשית, מדוע והיכן משתמשים בה. במאמר זה נדבר ישירות על השוואה בין מספרים, חפצים, מקרים מיוחדים, דקויות ונקודות לא מובנות מאליהן. ליתר דיוק, על מה נדבר:
השוואת חפצים: תרגול - 1
  • השוואת מחרוזות: ' ==' וequals
  • שיטהString.intern
  • השוואה בין פרימיטיבים אמיתיים
  • +0.0ו-0.0
  • מַשְׁמָעוּתNaN
  • Java 5.0. יצירת שיטות והשוואה באמצעות ' =='
  • Java 5.0. אגרוף אוטומטי/ביטול ארגז: ' ==', ' >=' ו' <=' עבור עטיפות אובייקטים.
  • Java 5.0. השוואה של אלמנטים מנויים (סוג enum)
אז בואו נתחיל!

השוואת מחרוזות: ' ==' וequals

אה, השורות האלה... אחד מהסוגים הנפוצים ביותר, שגורם להרבה בעיות. באופן עקרוני, יש מאמר נפרד עליהם . וכאן אגע בנושאי השוואה. כמובן, ניתן להשוות מחרוזות באמצעות equals. יתר על כן, יש להשוות ביניהם באמצעות equals. עם זאת, ישנן דקויות שכדאי להכיר. קודם כל, מחרוזות זהות הן למעשה אובייקט בודד. ניתן לאמת זאת בקלות על ידי הפעלת הקוד הבא:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
התוצאה תהיה "זהה" . מה שאומר שההפניות למחרוזות שווים. זה נעשה ברמת המהדר, כמובן כדי לחסוך בזיכרון. המהדר יוצר מופע אחד של המחרוזת, ומקצה str1הפניה str2למופע זה. עם זאת, זה חל רק על מחרוזות המוצהרות כמילוליות בקוד. אם אתה מחבר מחרוזת מקטעים, הקישור אליו יהיה שונה. אישור - דוגמה זו:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
התוצאה תהיה "לא זהה" . ניתן גם ליצור אובייקט חדש באמצעות בנאי ההעתקה:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
התוצאה תהיה גם "לא זהה" . לפיכך, לפעמים ניתן להשוות מחרוזות באמצעות השוואת הפניות. אבל עדיף לא להסתמך על זה. ברצוני לגעת בשיטה מעניינת אחת המאפשרת לך להשיג את מה שנקרא ייצוג קנוני של מחרוזת - String.intern. בואו נדבר על זה ביתר פירוט.

שיטת String.intern

נתחיל מזה שהכיתה Stringתומכת בבריכת מיתר. כל מילות המיתרים המוגדרות במחלקות, ולא רק הן, מתווספות למאגר הזה. אז, השיטה internמאפשרת לך לקבל מחרוזת מהמאגר הזה ששווה לקיים (זה שעליו נקראת השיטה intern) מנקודת המבט של equals. אם שורה כזו לא קיימת בבריכה, אז שמים שם את הקיימת ומוחזר קישור אליה. לפיכך, גם אם ההפניות לשתי מחרוזות שוות שונות (כמו בשתי הדוגמאות לעיל), אז קריאות למחרוזות אלו internיחזירו הפניה לאותו אובייקט:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
התוצאה של ביצוע קטע קוד זה תהיה "זהה" . אני לא יכול להגיד בדיוק למה זה נעשה ככה. השיטה internמקורית, ולמען האמת, אני לא רוצה להיכנס לפראיות של קוד C. סביר להניח שזה נעשה כדי לייעל את צריכת הזיכרון והביצועים. בכל מקרה, כדאי לדעת על תכונת היישום הזו. נעבור לחלק הבא.

השוואה בין פרימיטיבים אמיתיים

ראשית, אני רוצה לשאול שאלה. פשוט מאוד. מהו הסכום הבא - 0.3f + 0.4f? למה? 0.7f? בוא נבדוק:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
כתוצאה? כמו? גם אני. למי שלא השלים את הפרגמנט הזה, אגיד שהתוצאה תהיה...
f1==f2: false
למה זה קורה?.. בואו נבצע בדיקה נוספת:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
שימו לב להמרה ל double. זה נעשה על מנת להוציא יותר מקומות עשרוניים. תוֹצָאָה:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
באופן קפדני, התוצאה צפויה. הייצוג של החלק השברי מתבצע באמצעות סדרה סופית 2-n, ולכן אין צורך לדבר על הייצוג המדויק של מספר שנבחר באופן שרירותי. כפי שניתן לראות מהדוגמה, דיוק הייצוג floatהוא 7 מקומות עשרוניים. באופן קפדני, הייצוג float מקצה 24 ביטים למנטיסה. לפיכך, המספר המוחלט המינימלי שניתן לייצג באמצעות float (מבלי לקחת בחשבון את המידה, כי אנחנו מדברים על דיוק) הוא 2-24≈6*10-8. בשלב זה הערכים בייצוג הולכים למעשה float. ומכיוון שיש קוונטיזציה, יש גם שגיאה. floatמכאן המסקנה: ניתן להשוות מספרים בייצוג רק עם דיוק מסוים. הייתי ממליץ לעגל אותם למקום ה-6 העשרוני (10-6), או, רצוי, לבדוק את הערך המוחלט של ההפרש ביניהם:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
במקרה זה, התוצאה מעודדת:
|f3-f4|<1e-6: true
כמובן, התמונה זהה לחלוטין לסוג double. ההבדל היחיד הוא ש-53 סיביות מוקצות למנטיסה, ולכן דיוק הייצוג הוא 2-53≈10-16. כן, ערך הקוונטיזציה הרבה יותר קטן, אבל הוא קיים. וזה יכול לשחק בדיחה אכזרית. אגב, בספריית הבדיקות JUnit , בשיטות להשוואת מספרים ממשיים, הדיוק מצוין במפורש. הָהֵן. שיטת ההשוואה מכילה שלושה פרמטרים - המספר, למה הוא צריך להיות שווה ודיוק ההשוואה. אגב, אני רוצה להזכיר את הדקויות הקשורות לכתיבת מספרים בפורמט מדעי, תוך ציון התואר. שְׁאֵלָה. איך כותבים 10-6? התרגול מראה שיותר מ-80% עונים - 10e-6. בינתיים, התשובה הנכונה היא 1e-6! ו-10e-6 זה 10-5! דרכנו על המגרפה הזו באחד הפרויקטים, באופן די בלתי צפוי. הם חיפשו את השגיאה הרבה מאוד זמן, הסתכלו בקבועים 20 פעמים. ולאף אחד לא היה צל של ספק בנכונותם, עד שיום אחד, במידה רבה במקרה, הודפס הקבוע 10e-3 ומצאו שניים. ספרות אחרי הנקודה העשרונית במקום שלוש הצפויות. לכן, היזהר! בוא נמשיך הלאה.

+0.0 ו-0.0

בייצוג של מספרים ממשיים, הסיבית המשמעותית ביותר מסומנת. מה קורה אם כל שאר הביטים הם 0? בניגוד למספרים שלמים, כאשר במצב כזה התוצאה היא מספר שלילי הממוקם בגבול התחתון של טווח הייצוג, מספר ממשי שרק הביט המשמעותי ביותר מוגדר ל-1 פירושו גם 0, רק עם סימן מינוס. לפיכך, יש לנו שני אפסים - +0.0 ו-0.0. נשאלת שאלה הגיונית: האם צריך להתייחס למספרים אלו שווים? המכונה הוירטואלית חושבת בדיוק כך. עם זאת, מדובר בשני מספרים שונים , מכיוון שכתוצאה מפעולות איתם מתקבלים ערכים שונים:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
... והתוצאה:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
אז במקרים מסוימים הגיוני להתייחס ל-+0.0 ול-0.0 כשני מספרים שונים. ואם יש לנו שני אובייקטים, שבאחד מהם השדה הוא +0.0, ובשני -0.0, ניתן להתייחס גם לאובייקטים אלו כלא שווים. נשאלת השאלה - איך אתה יכול להבין שהמספרים אינם שווים אם ההשוואה הישירה שלהם למכונה וירטואלית נותנת true? התשובה היא זו. למרות שהמכונה הוירטואלית מחשיבה את המספרים האלה כשווים, הייצוגים שלהם עדיין שונים. לכן, הדבר היחיד שניתן לעשות הוא להשוות בין הדעות. וכדי לקבל את זה, ישנן שיטות int Float.floatToIntBits(float)ו- long Double.doubleToLongBits(double), שמחזירות מעט ייצוג בצורה intובהתאמה long(המשך הדוגמה הקודמת):
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
התוצאה תהיה
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
לפיכך, אם יש לך +0.0 ו -0.0 הם מספרים שונים, אז אתה צריך להשוות משתנים אמיתיים באמצעות ייצוג הסיביות שלהם. נראה שסידרנו את +0.0 ו-0.0. -0.0, לעומת זאת, אינו ההפתעה היחידה. יש גם דבר כזה...

ערך NaN

NaNמייצג Not-a-Number. ערך זה מופיע כתוצאה מפעולות מתמטיות שגויות, נניח, מחלקים 0.0 ב-0.0, אינסוף באינסוף וכו'. הייחודיות של ערך זה היא שהוא אינו שווה לעצמו. הָהֵן.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...יגרום...
x=NaN
x==x: false
איך זה יכול להתברר כאשר משווים בין חפצים? אם השדה של האובייקט שווה ל NaN, אז ההשוואה תיתן false, כלומר. מובטח שאובייקטים ייחשבו לא שווים. אם כי, באופן הגיוני, אולי נרצה בדיוק את ההיפך. אתה יכול להשיג את התוצאה הרצויה באמצעות השיטה Float.isNaN(float). הוא חוזר trueאם הטיעון הוא NaN. במקרה זה, לא הייתי מסתמך על השוואת ייצוגי סיביות, כי זה לא סטנדרטי. אולי זה מספיק לגבי פרימיטיבים. כעת נעבור לדקויות שהופיעו בג'אווה מאז גרסה 5.0. והנקודה הראשונה שהייתי רוצה לגעת בה היא

Java 5.0. יצירת שיטות והשוואה באמצעות ' =='

יש תבנית בעיצוב שנקראת שיטת הייצור. לפעמים השימוש בו הרבה יותר רווחי משימוש בקונסטרוקטור. תן לי לתת לך דוגמה. אני חושב שאני מכיר היטב את מעטפת החפץ Boolean. מחלקה זו אינה ניתנת לשינוי ויכולה להכיל שני ערכים בלבד. כלומר, למעשה, לכל צורך, מספיקים רק שני עותקים. ואם תיצור אותם מראש ואז פשוט תחזיר אותם, זה יהיה הרבה יותר מהיר מאשר שימוש בקונסטרוקטור. יש שיטה כזו Boolean: valueOf(boolean). זה הופיע בגרסה 1.4. שיטות ייצור דומות הוצגו בגרסה 5.0 במחלקות Byte, Character, Short, Integerומחלקות Long. כאשר המחלקות הללו נטענות, נוצרים מערכים של המופעים שלהם התואמים טווחים מסוימים של ערכים פרימיטיביים. טווחים אלה הם כדלקמן:
השוואת חפצים: תרגול - 2
המשמעות היא שבשימוש בשיטה, valueOf(...)אם הארגומנט נופל בטווח שצוין, אותו אובייקט תמיד יוחזר. אולי זה נותן עלייה מסוימת במהירות. אך יחד עם זאת, מתעוררות בעיות מסוג כזה שיכול להיות די קשה לרדת לעומקן. קרא עוד על זה. בתיאוריה, שיטת ההפקה valueOfנוספה גם למחלקות Floatוגם למחלקות Double. התיאור שלהם אומר שאם אתה לא צריך עותק חדש, אז עדיף להשתמש בשיטה זו, כי זה יכול לתת עלייה במהירות וכו'. וכולי. עם זאת, ביישום הנוכחי (Java 5.0), נוצר מופע חדש בשיטה זו, כלומר. השימוש בו אינו מובטח לתת עלייה במהירות. יתרה מכך, קשה לי לדמיין איך אפשר להאיץ את השיטה הזו, כי בגלל המשכיות הערכים לא ניתן לארגן שם מטמון. חוץ ממספרים שלמים. כלומר, בלי החלק השברירי.

Java 5.0. אגרוף אוטומטי/ביטול ארגז: ' ==', ' >=' ו' <=' עבור עטיפות אובייקטים.

אני חושד ששיטות הייצור ומטמון המופעים נוספו למעטפות עבור פרימיטיביות של מספרים שלמים כדי לייעל את הפעולות autoboxing/unboxing. תן לי להזכיר לך מה זה. אם אובייקט חייב להיות מעורב בפעולה, אבל פרימיטיבי מעורב, אז פרימיטיבי זה עטוף אוטומטית בעטיפת אובייקט. זה autoboxing. ולהיפך - אם חייב להיות מעורב פרימיטיבי בפעולה, אז אתה יכול להחליף שם מעטפת אובייקט, והערך יורחב ממנה אוטומטית. זה unboxing. כמובן, אתה צריך לשלם עבור נוחות כזו. פעולות המרה אוטומטיות מאטות מעט את האפליקציה. עם זאת, זה לא רלוונטי לנושא הנוכחי, אז בואו נעזוב את השאלה הזו. הכל בסדר כל עוד אנחנו עוסקים בפעולות שקשורות בבירור לפרימיטיבים או לקליפות. מה יקרה למבצע ' =='? נניח שיש לנו שני אובייקטים Integerבעלי אותו ערך בפנים. איך הם ישוו?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
תוֹצָאָה:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
תוֹצָאָה:
i1==i2: true
עכשיו זה יותר מעניין! אם autoboxing-e אותם אובייקטים מוחזרים! כאן טמונה המלכודת. ברגע שנגלה שאותם אובייקטים מוחזרים, נתחיל בניסויים כדי לראות אם זה תמיד המצב. וכמה ערכים נבדוק? אחד? עשר? מאה? סביר להניח שנגביל את עצמנו למאה בכל כיוון סביב האפס. ואנחנו מקבלים שוויון בכל מקום. נראה שהכל בסדר. עם זאת, הסתכל קצת אחורה, כאן . ניחשתם מה הקאץ'?.. כן, מקרים של קונכיות אובייקטים במהלך אוטובוקסינג נוצרים באמצעות שיטות ייצור. זה מומחש היטב על ידי המבחן הבא:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
התוצאה תהיה כזו:
number=-129: false
number=-128: true
number=127: true
number=128: false
עבור ערכים הנופלים בטווח המטמון , אובייקטים זהים מוחזרים, עבור אלה שמחוץ לו, אובייקטים שונים מוחזרים. ולכן, אם איפשהו בקליפות האפליקציה משווים במקום פרימיטיביים, יש סיכוי לקבל את השגיאה הנוראה ביותר: שגיאה צפה. מכיוון שככל הנראה הקוד ייבדק גם בטווח מוגבל של ערכים בהם השגיאה הזו לא תופיע. אבל בעבודה אמיתית, זה יופיע או ייעלם, בהתאם לתוצאות של כמה חישובים. קל יותר להשתגע מאשר למצוא טעות כזו. לכן, הייתי ממליץ לך להימנע מתאגרף אוטומטי בכל מקום אפשרי. וזה לא זה. בואו נזכור מתמטיקה, לא יותר מכיתה ה'. תן לאי השוויון A>=Bו А<=B. מה ניתן לומר על מערכת היחסים Aו B? יש רק דבר אחד - הם שווים. אתה מסכים? אני חושב שכן. בואו נריץ את הבדיקה:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
תוֹצָאָה:
i1>=i2: true
i1<=i2: true
i1==i2: false
וזה הדבר הכי מוזר בשבילי. אני לא מבין בכלל למה התכונה הזו הוכנסה לשפה אם היא מציגה סתירות כאלה. באופן כללי, אחזור שוב - אם אפשר להסתדר בלי autoboxing/unboxing, אז כדאי לנצל את ההזדמנות הזו עד הסוף. הנושא האחרון שאני רוצה לגעת בו הוא... Java 5.0. השוואה של רכיבי ספירה (סוג enum) כפי שאתה יודע, מאז גרסה 5.0 Java הציגה סוג כזה כמו enum - ספירה. המופעים שלו כברירת מחדל מכילים את השם ומספר הרצף בהצהרת המופע במחלקה. בהתאם לכך, כאשר סדר ההכרזה משתנה, המספרים משתנים. עם זאת, כפי שאמרתי במאמר 'הסדרה כפי שהיא' , זה לא גורם לבעיות. כל רכיבי הספירה קיימים בעותק בודד, זה נשלט ברמת המכונה הווירטואלית. לכן, ניתן להשוות ביניהם ישירות, באמצעות קישורים. * * * אולי זה הכל להיום לגבי הצד המעשי של יישום השוואת אובייקטים. אולי פספסתי משהו. כמו תמיד, אני מצפה לתגובות שלך! לעת עתה, תן לי לצאת לחופשה. תודה לכולכם על תשומת הלב! קישור למקור: השוואת אובייקטים: תרגול
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION