Java TreeMap לעומת HashMap

1. הקדמה

במאמר זה נשווה שניים מַפָּה יישומים: TreeMap ו מפת גיבוב.

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

2. הבדלים

2.1. יישום

ראשית נדבר על מפת גיבוב שהוא יישום מבוסס hashtable. זה מרחיב את AbstractMap בכיתה ומיישם את מַפָּה מִמְשָׁק. א מפת גיבוב עובד על פי העיקרון של hashing.

זֶה מַפָּה היישום בדרך כלל משמש כדלי טבלת גיבוב, אבל כאשר הדליים גדולים מדי הם הופכים לצמתים של TreeNodes, כל אחד מהם בנוי באופן דומה לאלה שב java.util.TreeMap.

אתה יכול למצוא עוד על HashMap של הפנימיות במאמר התמקדו בו.

מצד שני, TreeMap מרחיב AbstractMap כיתה וכלים NavigableMap מִמְשָׁק. א TreeMap מאחסן אלמנטים במפה א אדום שחור עץ שהוא א איזון עצמי עץ חיפוש בינארי.

ותוכלו למצוא עוד על ה- TreeMap's הפנימיות במאמר התמקדו בזה כאן.

2.2. להזמין

מפת גיבוב אינו מעניק שום ערבות לאופן שבו האלמנטים מסודרים ב מַפָּה.

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

@ מבחן ציבורי בטל כאשרInsertObjectsHashMap_thenRandomOrder () {Map hashmap = HashMap חדש (); hashmap.put (3, "TreeMap"); hashmap.put (2, "לעומת"); hashmap.put (1, "HashMap"); assertThat (hashmap.keySet (), containInAnyOrder (1, 2, 3)); }

עם זאת, פריטים ב TreeMap הם ממוינים לפי הסדר הטבעי שלהם.

אם TreeMap לא ניתן למיין חפצים לפי הסדר הטבעי ואז אנו עשויים להשתמש ב- משווה אוֹ ניתן להשוות כדי להגדיר את סדר סדר היסודות בתוך ה- מַפָּה:

@ מבחן ציבורי בטל כאשרInsertObjectsTreeMap_thenNaturalOrder () {Map treemap = new TreeMap (); treemap.put (3, "TreeMap"); treemap.put (2, "לעומת"); treemap.put (1, "HashMap"); assertThat (treemap.keySet (), מכיל (1, 2, 3)); }

2.3. ריק ערכים

מפת גיבוב מאפשר אחסון לכל היותר אחד ריקמַפְתֵחַ ורבים ריק ערכים.

בואו נראה דוגמה:

@ מבחן ציבורי בטל כאשרInsertNullInHashMap_thenInsertsNull () {Map hashmap = HashMap חדש (); hashmap.put (null, null); assertNull (hashmap.get (null)); }

למרות זאת, TreeMap אינו מאפשר א ריקמַפְתֵחַ אך עשוי להכיל רבים ריק ערכים.

א ריק המפתח אינו מורשה מכיוון ש- בהשוואה ל() או ה לְהַשְׁווֹת() שיטה זורקת א NullPointerException:

@Test (צפוי = NullPointerException.class) בטל בציבור כאשרInsertNullInTreeMap_thenException () {מפת מפת מפה = מפת עץ חדשה (); treemap.put (null, "NullPointerException"); }

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

3. ניתוח ביצועים

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

בחלק זה נספק ניתוח מקיף של הביצועים עבור מפת גיבוב ו TreeMap.

3.1. מפת גיבוב

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

מפת גיבוב מספק ביצועים צפויים בזמן קבוע O (1) עבור רוב הפעולות כמו לְהוֹסִיף(), לְהַסִיר() ו מכיל (). לכן, זה מהיר משמעותית מ- TreeMap.

הזמן הממוצע לחיפוש אלמנט בהנחה הסבירה בטבלת חשיש הוא O (1). אבל, יישום לא תקין של ה- פונקציית חשיש עלול להוביל לפיזור ערכים גרוע בדליים וכתוצאה מכך:

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

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

לכן, חיפוש אחר אלמנט ב- מפת גיבוב, במקרה הגרוע יכול היה להימשך זמן רב ככל שחיפש אלמנט ברשימה מקושרת כְּלוֹמַרעַל) זְמַן.

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

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

לפיכך, במקרה של התנגשויות חשיש גבוהות, הביצועים הגרועים ביותר ישתפרו מ- עַל) ל O (יומן n).

הקוד שמבצע שינוי זה יואר בהמשך:

אם (binCount> = TREEIFY_THRESHOLD - 1) {treeifyBin (כרטיסייה, hash); }

הערך עבור TREEIFY_THRESHOLD הוא שמונה המציין למעשה את ספירת הסף לשימוש בעץ ולא ברשימה מקושרת לדלי.

ניכר כי:

  • א מפת גיבוב דורש זיכרון רב יותר ממה שנדרש כדי להחזיק את הנתונים שלו
  • א מפת גיבוב לא אמור להיות יותר מ- 70% - 75% מלא. אם הוא מתקרב, גודל זה משתנה וערכים מחדש
  • חימום מחדש דורש נ פעולות שעלותן יקרה בה הכנסת הזמן הקבוע שלנו הופכת לסדר עַל)
  • זה אלגוריתם הגיבוב הקובע את סדר הכנסת האובייקטים ל- מפת גיבוב

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

עם זאת, עלינו לבחור א מפת גיבוב אם:

  • אנו יודעים בערך כמה פריטים יש לשמור באוסף שלנו
  • אנחנו לא רוצים לחלץ פריטים בסדר טבעי

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

3.2. TreeMap

א TreeMap מאחסן את הנתונים שלה בעץ היררכי עם יכולת למיין את האלמנטים בעזרת מנהג משווה.

סיכום ביצועיו:

  • TreeMap מספק ביצועים של O (יומן (n)) עבור רוב הפעולות כמו לְהוֹסִיף(), לְהַסִיר() ו מכיל ()
  • א Treemap יכול לחסוך זיכרון (בהשוואה ל- מפת גיבוב) מכיוון שהוא משתמש רק בכמות הזיכרון הדרושה להחזקת פריטיו, בניגוד ל- מפת גיבוב המשתמשת באזור רציף של זיכרון
  • עץ צריך לשמור על שיווי משקלו על מנת לשמור על ביצועיו המיועדים, הדבר דורש מאמץ ניכר, ולכן מסבך את היישום

אנחנו צריכים ללכת על TreeMap בְּכָל פַּעַם:

  • יש לקחת בחשבון מגבלות זיכרון
  • איננו יודעים כמה פריטים צריכים להיות מאוחסנים בזיכרון
  • אנו רוצים לחלץ חפצים בסדר טבעי
  • אם פריטים יתווספו ויוסרו באופן עקבי
  • אנחנו מוכנים לקבל O (יומן n) זמן חיפוש

4. קווי דמיון

4.1. אלמנטים ייחודיים

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

@Test הציבור בטל givenHashMapAndTreeMap_whenputDuplicates_thenOnlyUnique () {Map treeMap = חדש HashMap (); treeMap.put (1, "Baeldung"); treeMap.put (1, "Baeldung"); assertTrue (treeMap.size () == 1); מפה treeMap2 = מפת עץ חדשה (); treeMap2.put (1, "Baeldung"); treeMap2.put (1, "Baeldung"); assertTrue (treeMap2.size () == 1); }

4.2. גישה במקביל

שניהם מַפָּה יישומים אינם מסונכרן ועלינו לנהל גישה מקבילה לבד.

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

עלינו להשתמש במפורש Collections.synchronizedMap (mapName) כדי לקבל תצוגה מסונכרנת של המפה המסופקת.

4.3. איטרטורים מהירים

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

בנוסף, אנו יכולים להשתמש בשיטת ההסרה של איטרטר כדי לשנות את ה- מַפָּה במהלך איטרציה.

בואו נראה דוגמה:

@Test ציבורי בטל כאשרModifyMapDuringIteration_thenThrowExecption () {Map hashmap = HashMap חדש (); hashmap.put (1, "אחד"); hashmap.put (2, "שניים"); הפעלה הפעלה = () -> hashmap .forEach ((מפתח, ערך) -> hashmap.remove (1)); assertThrows (ConcurrentModificationException.class, הפעלה); }

5. באיזה מימוש להשתמש?

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

תִמצוּת:

  • עלינו להשתמש ב- TreeMap אם אנחנו רוצים לשמור על הערכים שלנו ממוינים
  • עלינו להשתמש ב- מפת גיבוב אם אנו נותנים עדיפות לביצועים על פני צריכת זיכרון
  • מאז TreeMap יש יישוב משמעותי יותר, אנו עשויים לשקול זאת אם אנו רוצים לגשת לאובייקטים קרובים יחסית זה לזה על פי הסדר הטבעי שלהם
  • מפת גיבוב ניתן לכוון באמצעות initialCapacity ו loadFactor, מה שאינו אפשרי עבור ה- TreeMap
  • אנחנו יכולים להשתמש ב- LinkedHashMap אם אנו רוצים לשמור על צו הכניסה תוך נהנה מגישה מתמדת לזמן

6. מסקנה

במאמר זה הראינו את ההבדלים והדמיון בין TreeMap ו מפת גיבוב.

כמו תמיד, דוגמאות הקוד למאמר זה זמינות ב- GitHub.