Set שומר ערכים ייחודיים
HashSet (מ-java.util) הוא אוסף שמחזיק כל ערך לכל היותר פעם אחת. אין בו מפתחות וערכים בזוגות כמו ב-HashMap: רק שק של איברים שונים זה מזה. כל התפקיד שלו הוא לענות מהר על שאלה אחת: "האם הדבר הזה נמצא כאן?"
יש פרמטר טיפוס אחד, <ElementType>. כמו עם ArrayList ו-HashMap, בדרך כלל מגדירים את המשתנה כממשק Set ויוצרים HashSet.
add מחזירה אם הערך היה חדש
add לא רק שומרת את הערך: היא מחזירה boolean שאומר אם הקבוצה באמת השתנתה. הוספה של ערך שכבר קיים מחזירה false ומשאירה את הקבוצה בלי שינוי.
ערך ההחזרה הזה שימושי באמת: if (!seen.add(x)) { /* x is a repeat */ } מאפשר לזהות כפילויות בשורה אחת תוך כדי תנועה.
הסרת כפילויות מרשימה
מכיוון שקבוצה דוחה חזרות, הדרך המהירה ביותר להסיר כפילויות מאוסף היא לשפוך אותו לתוך קבוצה. הבנאי של HashSet מקבל כל אוסף אחר:
זו הסיבה הנפוצה ביותר שמתחילים פונים לקבוצה. רק דעו שבדרך הלוך ושוב מאבדים את הסדר המקורי: השתמשו ב-LinkedHashSet אם הסדר חשוב (על זה בהמשך).
contains, remove ו-size
הפעולות היומיומיות דומות לאלה של האוספים האחרים:
היתרון הגדול על פני ArrayList הוא contains. רשימה צריכה לעבור על כל איבר כדי לענות (O(n)); HashSet קופץ כמעט ישר לתשובה (בערך O(1)). כשאתם מוצאים את עצמכם קוראים ל-list.contains(...) בתוך לולאה, זה בדרך כלל הסימן לעבור לקבוצה.
פעולות על קבוצות: איחוד, חיתוך, הפרש
קבוצות זורחות כשמשלבים אותן. המתודות נקראות בפשטות ברגע שיודעים מי זו מי:
המלכודת העיקרית: addAll, retainAll ו-removeAll משנות את הקבוצה שעליה הן נקראות. בגלל זה כל דוגמה מעתיקה קודם את a ל-HashSet חדש: אחרת הייתם הורסים את המקור. בנו קבוצה חדשה לכל תוצאה.
HashSet לא שומר על סדר
כמו HashMap, גם HashSet לא מבטיח שום סדר מעבר, והסדר יכול להשתנות בין הרצות. אם צריך סדר צפוי:
LinkedHashSetשומר על סדר ההכנסה: הסדר שבו הוספתם את האיברים.TreeSetשומר את האיברים ממוינים לפי הסדר הטבעי (או לפיComparatorשתספקו).
שלושתם מממשים את הממשק Set, כך שהחלפה ביניהם היא שינוי של שורה אחת בבנאי.
איברים חייבים לתמוך ב-hashing
מתחת לפני השטח HashSet מבוסס על HashMap, ולכן אותו כלל חל: הוא מאתר איברים על ידי hashing, כלומר hashCode() ו-equals() של איבר חייבים להסכים ביניהם. טיפוסים מובנים כמו String ו-Integer כבר עושים את זה נכון, ולכן מחרוזות "java" כפולות מתמזגות כמו שצריך בדוגמאות הקודמות. אם אתם שומרים מופעים של מחלקה משלכם, דרסו גם את equals וגם את hashCode: אחרת שני אובייקטים ש"שווים" במשמעות שלהם ייחשבו שונים, ו-contains והסרת הכפילויות ייכשלו בשקט.
הבא בתור: מעבר על אוספים
הכרתם עכשיו את שלושת האוספים העיקריים: ArrayList, HashMap ו-HashSet. על כל אחד עוברים קצת אחרת, ויש מלכודות עדינות (כמו שינוי אוסף בזמן שעוברים עליו בלולאה). בהמשך נחבר את הכול יחד ונסביר איך לעבור על אוספים בצורה נקייה עם לולאת for-each, איטרטורים ו-forEach.
שאלות נפוצות
איך יוצרים HashSet ב-Java?
מצהירים עליו עם פרמטר טיפוס אחד, טיפוס האיבר, וקוראים לבנאי: Set<String> tags = new HashSet<>();. מוסיפים ערכים עם tags.add("java"); ובודקים שייכות עם tags.contains("java");. ייבאו את java.util.HashSet ואת java.util.Set.
מה ההבדל בין HashSet ל-ArrayList ב-Java?
ArrayList שומרת כל איבר שמוסיפים (כולל כפילויות) לפי סדר ההכנסה, עם גישה לפי מיקום. HashSet שומר רק ערכים ייחודיים, לא מבטיח שום סדר, אין לו אינדקס, ובדיקת ה-contains שלו לוקחת בערך זמן קבוע במקום לסרוק את כל הרשימה. בחרו ב-HashSet כשחשובים לכם ייחודיות או בדיקת שייכות מהירה, לא מיקום.
איך מסירים כפילויות מרשימה ב-Java?
העבירו את הרשימה לבנאי של HashSet: Set<String> unique = new HashSet<>(list);. הקבוצה משמיטה ערכים חוזרים אוטומטית. אם צריך בחזרה רשימה (ולא אכפת לכם לאבד את הסדר), עטפו שוב: new ArrayList<>(unique). השתמשו ב-LinkedHashSet אם רוצים לשמור על הסדר המקורי.