פרימיטיבי Java לעומת אובייקטים

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

במדריך זה אנו מראים את היתרונות והחסרונות של השימוש בסוגים פרימיטיביים של Java ובמקביליהם העטופים.

2. מערכת Java סוג

לג'אווה יש מערכת מסוג כפול המורכבת מפרימיטיבים כמו int, בוליאני וסוגי התייחסות כגון מספר שלם,בוליאני. כל סוג פרימיטיבי תואם לסוג ייחוס.

כל אובייקט מכיל ערך יחיד מהסוג הפרימיטיבי המתאים. ה שיעורי עטיפה אינם ניתנים לשינוי (כדי שמצבם לא יכול להשתנות לאחר בניית האובייקט) והם סופיים (כך שלא נוכל לרשת מהם).

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

מספר שלם j = 1; // תיבה אוטומטית int i = מספר שלם חדש (1); // ביטול איגרוף 

תהליך ההמרה של סוג פרימיטיבי להפניה נקרא אוטומציה אוטומטית, התהליך ההפוך נקרא unboxing.

3. יתרונות וחסרונות

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

אם אנחנו לא מתמודדים עם אף אחד מאלה, אנו עשויים להתעלם משיקולים אלה אם כי כדאי להכיר אותם.

3.1. טביעת רגל זיכרון של פריט יחיד

רק לצורך ההפניה, למשתני הסוג הפרימיטיבי יש את ההשפעה הבאה על הזיכרון:

  • בוליאני - ביט אחד
  • בתים - 8 ביטים
  • קצר, char - 16 ביטים
  • int, float - 32 ביטים
  • ארוך, כפול - 64 ביט

בפועל, ערכים אלה יכולים להשתנות בהתאם ליישום המכונה הווירטואלית. ב- VM של אורקל, הסוג הבוליאני, למשל, ממופה לערכי int 0 ו- 1, ולכן נדרשים 32 סיביות, כמתואר כאן: סוגים וערכים פרימיטיביים.

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

סוגי הייחוס הם עצמים, הם חיים על הערמה ואינם נגישים יחסית. יש להם תקורה מסוימת הנוגעת לעמיתיהם הפרימיטיביים.

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

java 10.0.1 2018-04-17 Java (TM) SE Runtime Environment 18.3 (build 10.0.1 + 10) Java HotSpot (TM) 64-Bit Server VM 18.3 (build 10.0.1 + 10, מצב מעורב)

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

מתברר שמופע יחיד מסוג התייחסות ב- JVM זה תופס 128 ביטים למעט ארוך ו לְהַכפִּיל אשר תופסים 192 ביטים:

  • בוליאני - 128 ביט
  • בתים - 128 ביט
  • קצר, אופי - 128 ביט
  • מספר שלם, צף - 128 ביט
  • ארוך, כפול - 192 ביט

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

3.2. טביעת רגל זיכרון למערכים

המצב נעשה מעניין יותר אם נשווה כמה זיכרון תופס מערכים מהסוגים הנחשבים.

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

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

  • ארוך, כפול: m (s) = 128 + 64 s
  • קצר, char: m (s) = 128 + 64 [s / 4]
  • בתים, בוליאניים: m (s) = 128 + 64 [s / 8]
  • השאר: m (s) = 128 + 64 [s / 2]

כאשר הסוגריים המרובעים מציינים את פונקציית התקרה הסטנדרטית.

באופן מפתיע, מערכים מהסוגים הפרימיטיביים הארוכים והכפולים צורכים יותר זיכרון מאשר שיעורי העטיפה שלהם ארוך ו לְהַכפִּיל.

אנחנו יכולים לראות את זה מערכים של אלמנטים בודדים מסוגים פרימיטיביים הם כמעט תמיד יקרים יותר (למעט ארוך וכפול) מאשר סוג הייחוס המקביל.

3.3. ביצועים

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

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

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

בעוד (! pivot.equals (יסודות [אינדקס])) {אינדקס ++; }

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

אנו משתמשים בכלי ה- benchmarking הידוע של JMH (עיין במדריך שלנו כיצד להשתמש בו), וניתן לתמצת את תוצאות פעולת החיפוש בתרשים זה:

אפילו עבור פעולה כה פשוטה אנו יכולים לראות כי נדרש זמן רב יותר לביצוע הפעולה לשיעורי עטיפה.

במקרה של פעולות מסובכות יותר כמו סיכום, כפל או חלוקה, ההבדל במהירות עשוי לרקוד שחקים.

3.4. ערכי ברירת מחדל

ערכי ברירת המחדל של הסוגים הפרימיטיביים הם 0 (בייצוג המקביל, כלומר 0, 0.0d וכו ') עבור סוגים מספריים, שֶׁקֶר לסוג בוליאני, \ u0000 לסוג החרבן. עבור מחלקות העטיפה, ערך ברירת המחדל הוא ריק.

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

למרות שזה לא נחשב למקובל להשאיר משתנים ללא איגום, לפעמים אנו עשויים להקצות ערך לאחר יצירתו.

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

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

4. שימוש

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

מצד שני, המפרט הנוכחי של שפת Java אינו מאפשר שימוש בסוגים פרימיטיביים בסוגים פרמטרים (גנריים), באוספי Java או ב- Reflection API.

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

5. מסקנה

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

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