מדריך ל- HashSet בג'אווה

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

במאמר זה נצלול HashSet. זה אחד הפופולריים ביותר מַעֲרֶכֶת יישומים וכן חלק בלתי נפרד ממסגרת אוספי Java.

2. מבוא ל HashSet

HashSet הוא אחד ממבני הנתונים הבסיסיים בממשק ה- API של Java Collections.

בואו נזכיר את ההיבטים החשובים ביותר ביישום זה:

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

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

HashSet ציבורי () {map = HashMap חדש (); }

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

3. ממשק ה- API

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

3.1. לְהוֹסִיף()

ה לְהוֹסִיף() ניתן להשתמש בשיטה להוספת אלמנטים לסט. חוזה השיטה קובע כי אלמנט יתווסף רק כאשר הוא כבר לא קיים בערכה. אם נוסף אלמנט, השיטה חוזרת נָכוֹן, אחרת - שֶׁקֶר.

אנו יכולים להוסיף אלמנט ל- a HashSet כמו:

@Test ציבורי בטל כאשרAddingElement_shouldAddElement () {Set hashset = new HashSet (); assertTrue (hashset.add ("מחרוזת נוספה")); }

מנקודת מבט יישום, ה לְהוֹסִיף שיטה חשובה ביותר. פרטי היישום ממחישים כיצד HashSet עובד באופן פנימי וממנף את HashMap שללָשִׂים שיטה:

הוסף בוליאני ציבורי (E e) {return map.put (e, PRESENT) == null; }

ה מַפָּה משתנה הוא התייחסות לגיבוי הפנימי מפת גיבוב:

מפת HashMap פרטית חולפת;

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

תִמצוּת:

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

3.2. מכיל ()

מטרת ה מכיל השיטה היא לבדוק אם אלמנט קיים בנתון HashSet. זה חוזר נָכוֹן אם האלמנט נמצא, אחרת שֶׁקֶר.

אנו יכולים לחפש אלמנט ב HashSet:

@Test ציבורי בטל כאשרCheckingForElement_shouldSearchForElement () {Set hashsetContains = hashset חדש (); hashsetContains.add ("מחרוזת נוספה"); assertTrue (hashsetContains.contains ("מחרוזת נוספה")); }

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

3.3. לְהַסִיר()

השיטה מסירה מהרכיב את האלמנט שצוין אם הוא קיים. שיטה זו חוזרת נָכוֹן אם סט הכיל את האלמנט שצוין.

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

@ מבחן ציבורי בטל כאשר RemovingElement_shouldRemoveElement () {Set removeFromHashSet = HashSet new (); removeFromHashSet.add ("מחרוזת נוספה"); assertTrue (removeFromHashSet.remove ("מחרוזת נוספה")); }

3.4. ברור()

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

בואו נראה את זה בפעולה:

@Test ציבורי בטל כאשרClearingHashSet_shouldClearHashSet () {Set clearHashSet = HashSet new (); clearHashSet.add ("מחרוזת נוספה"); clearHashSet.clear (); assertTrue (clearHashSet.isEmpty ()); }

3.5. גודל()

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

בואו נראה את זה בפעולה:

@Test ציבורי בטל כאשרCheckingTheSizeOfHashSet_shouldReturnThesize () {Set hashSetSize = new HashSet (); hashSetSize.add ("מחרוזת נוספה"); assertEquals (1, hashSetSize.size ()); }

3.6. זה ריק()

אנו יכולים להשתמש בשיטה זו כדי להבין אם מופע נתון של a HashSet ריק או לא. שיטה זו חוזרת נָכוֹן אם הסט אינו מכיל אלמנטים:

@Test ציבורי בטל כאשרCheckingForEmptyHashSet_shouldCheckForEmpty () {Set emptyHashSet = HashSet new (); assertTrue (emptyHashSet.isEmpty ()); }

3.7. איטרטור ()

השיטה מחזירה איטרטור על פני האלמנטים ב- מַעֲרֶכֶת. האלמנטים מבקרים ללא סדר מסוים ואיטרטים אינם מהירים.

אנו יכולים לצפות בסדר החיזור האקראי כאן:

@ מבחן ציבורי בטל כאשרIteratingHashSet_shouldIterateHashSet () {הגדר hashset = חדש HashSet (); hashset.add ("ראשון"); hashset.add ("שני"); hashset.add ("שלישי"); Iterator itr = hashset.iterator (); בעוד (itr.hasNext ()) {System.out.println (itr.next ()); }}

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

בואו נראה את זה בפעולה:

@Test (צפוי = ConcurrentModificationException.class) בטל ציבורי כאשרModifyingHashSetWhileIterating_shouldThrowException () {הגדר hashset = חדש HashSet (); hashset.add ("ראשון"); hashset.add ("שני"); hashset.add ("שלישי"); Iterator itr = hashset.iterator (); בעוד (itr.hasNext ()) {itr.next (); hashset.remove ("שני"); }} 

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

@Test ציבורי בטל כאשר RemovingElementUsingIterator_shouldRemoveElement () {Set hashset = new HashSet (); hashset.add ("ראשון"); hashset.add ("שני"); hashset.add ("שלישי"); Iterator itr = hashset.iterator (); בעוד (itr.hasNext ()) {אלמנט מחרוזת = itr.next (); if (element.equals ("Second")) itr.remove (); } assertEquals (2, hashset.size ()); }

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

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

4. איך HashSet שומר על ייחודיות?

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

כל ערך קוד hash תואם למיקום דלי מסוים שיכול להכיל אלמנטים שונים, שעבורם ערך ה- hash המחושב זהה. אבל שני אובייקטים עם אותו דבר hashCode יכול להיות שלא יהיה שווה.

לכן, יש להשוות בין אובייקטים בתוך אותה דלי באמצעות ה- שווים() שיטה.

5. ביצוע של HashSet

הביצועים של א HashSet מושפע בעיקר משני פרמטרים - שלה קיבולת ראשונית וה פקטור עומס.

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

הערה חשובה: מאז JDK 8, מורכבות הזמן הגרועה ביותר היא O (log * n).

גורם העומס מתאר מהי רמת המילוי המקסימלית, שמעליה יש לשנות את גודל הסט.

אנחנו יכולים גם ליצור HashSet עם ערכים מותאמים אישית עבור יכולת ראשונית ו גורם עומס:

הגדר hashset = HashSet חדש (); הגדר hashset = חדש HashSet (20); הגדר hashset = HashSet חדש (20, 0.5f); 

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

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

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

ככלל אצבע:

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

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

6. מסקנה

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

למדנו כמה מהשיטות החשובות מממשק ה- API, כיצד הן יכולות לעזור לנו כמפתח להשתמש ב- HashSet לפוטנציאל שלו.

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