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

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

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

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

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

  • מנגנונים פנימיים

  • טיפול בכפילויות

  • תמיכה בערכים null

  • סדר

  • סנכרון

  • ביצועים

  • שיטות מפתח

  • יישומים נפוצים

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

תוכן עניינים:

  1. הבנת מסגרת Collections של ג'אווה

  2. ממשקי קולקציות ב-Java

  3. מחלקת ייעוד לאוסף

  4. מסקנה

הבנת מתישב האוספים של ג'אווה

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

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

ממשקי אוספים

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

במסגרת אוספי Java, ממשקי אוסף שונים כמו Set, List ו-Queue מרחיבים את ממשק Collection, והם חייבים לעמוד בהסכם המוגדר על ידי ממשק Collection.

פיענוח היררכיית אוספי Java

בעברו של דיאגרמה מסודרת מתוך מאמר זה שממחיש את היררכיה של אוסף Java:

נبدأ מהצ顶 ונעבוד למטה כדי שתוכל להבין מה הדיאגרמה הזו מראה:

  1. בשורש של מסגרת אוספי Java נמצא הממשק Iterable, המאפשר לך לחזור על האלמנטים של אוסף.

  2. ממשק Collection מרחיב את הממשק Iterable. משמעות הדבר היא שהוא יורש את המאפיינים וההתנהגות של הממשק Iterable ומוסיף את ההתנהגות שלו להוספה, הסרה וקליטת אלמנטים.

  3. ממשקים ספציפיים כמו List, Set, ו־Queue מרחיבים עוד יותר את ממשק ה־Collection. לכל אחד מהממשקים הללו יש מחלקות אחרות שמיישמות את השיטות שלהם. לדוגמה, ArrayList היא מימוש פופולרי של ממשק ה־List, HashSet מיישם את ממשק ה־Set, וכו'.

  4. ממשק ה־Map הוא חלק ממסגרת ה־Java Collections Framework, אך הוא לא מרחיב את ממשק ה־Collection, בניגוד לאחרים שצוינו למעלה.

  5. כל הממשקים והמחלקות במסגרת זו הם חלק מחבילת ה־java.util.

שימו לב: מקור נפוץ לבלבול במסגרת האוספים של Java סובב סביב ההבדל בין Collection ל-Collections. Collection היא ממשק במסגרת, בעוד Collections היא מחלקת עזר. מחלקת Collections מספקת שיטות סטטיות שמבצעות פעולות על אלמנטים של אוסף.

ממשקי אוסף של Java

עד עכשיו, אתם מכירים את סוגי האוספים השונים שמבנים את הבסיס של מסגרת האוספים. עכשיו נבחן מקרוב את הממשקים List, Set, Queue, ו-Map.

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

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

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

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

רשימות

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

  1. מנגנון פנימי: א List נתמך פנימית על ידי מערך או רשימה מקושרת, בהתאם לסוג היישום. לדוגמה, ArrayList משתמש במערך, בעוד ש LinkedList משתמש ברשימה מקושרת פנימית. תוכלו לקרוא עוד על LinkedList כאן. א List משנה את גודלו באופן דינמי עם הוספה או הסרה של אלמנטים. המשיכה המבוססת על אינדקסים הופכת אותו לסוג אוסף מאוד יעיל.

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

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

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

  6. שיטות מפתח: הנה כמה שיטות מפתח של ממשק List: add(E element), get(int index), set(int index, E element), remove(int index), ו-size(). בואו נסתכל כיצד להשתמש בשיטות אלו עם תוכנית דוגמה.

     import java.util.ArrayList;
     import java.util.List;
    
     public class ListExample {
         public static void main(String[] args) {
             // צור רשימה
             List<String> list = new ArrayList<>();
    
             // add(E element)
             list.add("תפוח");
             list.add("בננה");
             list.add("דובדבן");
    
             // get(int index)
             String secondElement = list.get(1); // "בננה"
    
             // set(int index, E element)
             list.set(1, "אוכמנית");
    
             // remove(int index)
             list.remove(0); // מסיר "תפוח"
    
             // size()
             int size = list.size(); // 2
    
             // הדפס את הרשימה
             System.out.println(list); // פלט: [אוכמנית, דובדבן]
    
             // הדפס את גודל הרשימה
             System.out.println(size); // פלט: 2
         }
     }
    
  7. מימושים נפוצים: ArrayList, LinkedList, Vector, Stack

  8. ביצועים: בדרך כלל, פעולות הוספה ומחיקה מהירות הן בArrayList והן בLinkedList. אך קבלת רכיבים יכולה להיות איטית כי יש לעבור דרך הצמתים.

פעולה ArrayList LinkedList
הוספה מהירה בסוף – O(1) בממוצע, איטית בתחילה או באמצע – O(n) מהירה בתחילה או באמצע – O(1), איטית בסוף – O(n)
מחיקה מהירה בסוף – O(1) בממוצע, איטית בתחילה או באמצע – O(n) מהירה – O(1) אם המיקום ידוע
שליפה מהירה – O(1) לגישה אקראית איטית – O(n) לגישה אקראית, כי זה כרוך במעבר

אוספים

א Set הוא סוג של אוסף שאינו מאפשר אלמנטים כפולים ומייצג את המושג של אוסף מתמטי.

  1. מנגנון פנימי: א Set מגובה פנימית על ידי HashMap. בהתאם לסוג היישום, הוא נתמך על ידי HashMap, LinkedHashMap או TreeMap. כתבתי מאמר מפורט על איך HashMap עובד פנימית כאן. ודא לבדוק את זה.

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

  3. ערך ריק: מותר ערך ריק אחד לכל היותר בSet מכיוון שאין לאפשר כפילויות. אבל זה לא חל על יישום הTreeSet, שבו ערכים ריקים אינם מותרים בכלל.

  4. סדר: הסדר של האלמנטים בSet תלוי בסוג היישום.

    • HashSet: הסדר אינו מבוטח, ואלמנטים יכולים להיות ממוקמים בכל מיקום.

    • LinkedHashSet: יישום זה שומר על סדר ההכנסה, כך שתוכל לשחזר את האלמנטים באותו סדר שבו הם הוכנסו.

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

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

  6. שיטות עיקריות: הנה כמה שיטות עיקריות של ממשק Set: add(E element), remove(Object o), contains(Object o), ו־size(). בואו נסתכל על איך להשתמש בשיטות אלו עם דוגמה לתוכנית.

     import java.util.HashSet;
     import java.util.Set;
    
     public class SetExample {
         public static void main(String[] args) {
             // יצירת קבוצה
             Set<String> set = new HashSet<>();
    
             // הוספת איברים לקבוצה
             set.add("תפוח");
             set.add("בננה");
             set.add("דובדבן");
    
             // הסרת איבר מהקבוצה
             set.remove("בננה");
    
             // בדיקה אם הקבוצה מכילה איבר
             boolean containsApple = set.contains("תפוח");
             System.out.println("מכיל תפוח: " + containsApple);
    
             // קבלת גודל הקבוצה
             int size = set.size();
             System.out.println("גודל הקבוצה: " + size);
         }
     }
    
  7. יישומים נפוצים: HashSet, LinkedHashSet, TreeSet

  8. ביצועים: יישומי Set מציעים ביצועים מהירים עבור פעולות בסיסיות, למעט TreeSet, שם הביצועים יכולים להיות יחסית איטיים יותר מכיוון שהמבנה הנתוני הפנימי כרוך במיון האלמנטים במהלך פעולות אלו.

פעולה HashSet LinkedHashSet TreeSet
הכנסה מהירה – O(1) מהירה – O(1) איטית – O(log n)
מחיקה מהירה – O(1) מהירה – O(1) איטית – O(log n)
שחזור מהיר – O(1) מהיר – O(1) איטי – O(log n)

תורים

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

  1. מנגנון פנימי: הפעולה הפנימית של תור עשויה להשתנות בהתאם ליישום הספציפי שלו.

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

    • תור עדיפויות – מגובה פנימית על ידי ערימת בינארית, שהיא מאוד יעילה לפעולות שחזור.

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

  2. שכפולים: בתור, איברים כפולים מורשים, מאפשרים הכנסת מספר רב של הופעות של אותו ערך

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

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

  5. סנכרון: תור אינו מסונכרן כברירת מחדל. אך, ניתן להשתמש במימוש של ConcurrentLinkedQueue או BlockingQueue על מנת להשיג בטיחות תהליך.

  6. שיטות מפתח: כאן יש כמה שיטות מפתח של ממשק Queue: add(E element), offer(E element), poll(), ו-peek(). בואו נסתכל על איך להשתמש בשיטות אלה עם תוכנית דוגמה.

     import java.util.LinkedList;
     import java.util.Queue;
    
     public class QueueExample {
         public static void main(String[] args) {
             // צור תור באמצעות LinkedList
             Queue<String> queue = new LinkedList<>();
    
             // השתמש בשיטה add כדי להכניס אלמנטים, זורק חריגה אם ההכנסה נכשלת
             queue.add("Element1");
             queue.add("Element2");
             queue.add("Element3");
    
             // השתמש בשיטה offer כדי להכניס אלמנטים, מחזירה false אם ההכנסה נכשלת
             queue.offer("Element4");
    
             // הצג את התור
             System.out.println("Queue: " + queue);
    
             // הצג את האלמנט הראשון (לא מסיר אותו)
             String firstElement = queue.peek();
             System.out.println("Peek: " + firstElement); // מפיק "Element1"
    
             // קח את האלמנט הראשון (משיג ומסיר אותו)
             String polledElement = queue.poll();
             System.out.println("Poll: " + polledElement); // מפיק "Element1"
    
             // הצג את התור לאחר קבלת האלמנט
             System.out.println("Queue after poll: " + queue);
         }
     }
    
  7. יישומים נפוצים: LinkedList, PriorityQueue, ArrayDeque

  8. ביצועים: יישומים כמו LinkedList ו-ArrayDeque בדרך כלל מהירים להוספת והסרת פריטים. ה-PriorityQueue מעט יותר איטי מכיוון שהוא מכניס פריטים בהתאם לסדר עדיפויות שנקבע.

פעולה LinkedList PriorityQueue ArrayDeque
הכנסה מהיר בתחילה או באמצע – O(1), איטי בסופו – O(n) איטי – O(log n) מהיר – O(1), איטי – O(n), אם משתמש בשינוי גודל של מערך הפנימי
מחיקה מהיר – O(1) אם המיקום ידוע איטי – O(log n) מהיר – O(1), איטי – O(n), אם משתמש בשינוי גודל של מערך הפנימי
אחזור איטי – O(n) לגישה אקראית, מכיוון שכולל עבירה מהיר – O(1) מהיר – O(1)

מפות

מפה מייצגת אוסף של זוגות מפתח־ערך, עם כל מפתח ממפה לערך יחיד. אף על פי ש־Map הוא חלק ממתווך האוסף של ג'אווה, הוא אינו מרחיב את ממשק java.util.Collection.

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

  2. שכפולים: מבנה נתונים מסוג Map שומר נתונים כזוגות מפתח-ערך. כאן, כל מפתח הוא ייחודי, כך ששכפול מפתחות אינו מותר. אך שכפול ערכים מותר.

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

  4. סדר: סדר ההכנסה של Map משתנה בהתאם ליישום:

    • HashMap – סדר ההכנסה אינו מובטח מכיוון שהוא נקבע על פי רעיון ההאשינג.

    • LinkedHashMap – סדר ההכנסה נשמר ואתה יכול לשחזר את האלמנטים באותו סדר שבו הם נוספו לאוסף.

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

  5. סנכרון: מפתח אינו מסונכרן כברירת מחדל. אך ניתן להשתמש במימושים של Collections.synchronizedMap() או ConcurrentHashMap עבור קבלת בטיחות לקטע הנתונים.

  6. שיטות מפתח: הנה כמה שיטות מרכזיות של ממשק Map: put(K key, V value), get(Object key), remove(Object key), containsKey(Object key), ו־keySet(). בואו נסתכל על איך להשתמש בשיטות אלו עם תוכנית לדוגמה.

     import java.util.HashMap;
     import java.util.Map;
     import java.util.Set;
    
     public class MapMethodsExample {
         public static void main(String[] args) {
             // Create a new HashMap
             Map<String, Integer> map = new HashMap<>();
    
             // put(K key, V value) - Inserts key-value pairs into the map
             map.put("Apple", 1);
             map.put("Banana", 2);
             map.put("Orange", 3);
    
             // get(Object key) - Returns the value associated with the key
             Integer value = map.get("Banana");
             System.out.println("Value for 'Banana': " + value);
    
             // remove(Object key) - Removes the key-value pair for the specified key
             map.remove("Orange");
    
             // containsKey(Object key) - Checks if the map contains the specified key
             boolean hasApple = map.containsKey("Apple");
             System.out.println("Contains 'Apple': " + hasApple);
    
             // keySet() - Returns a set view of the keys contained in the map
             Set<String> keys = map.keySet();
             System.out.println("Keys in map: " + keys);
         }
     }
    
  7. יישומים נפוצים: HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap

  8. ביצועים: יישום HashMap בשימוש נרחב בעיקר בשל מאפייני הביצועים היעילים שלו המוצגים בטבלה למטה.

פעולה HashMap LinkedHashMap TreeMap
הכנסה מהירה – O(1) מהירה – O(1) איטית יותר – O(log n)
מחיקה מהירה – O(1) מהירה – O(1) איטית יותר – O(log n)
שליפה מהירה – O(1) מהירה – O(1) איטית יותר – O(log n)

מחלקת Utilities לאוספים

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

הנה כמה תכונות ומתודות מרכזיות, יחד עם מה שהן עושות, רשומות בקצרה:

  1. מיון: Collections.sort(List<T>) – מתודה זו משמשת למיון רכיבי רשימה בסדר עולה.

  2. חיפוש: Collections.binarySearch(List<T>, key) – שיטה זו משמשת לחיפוש אלמנט ספציפי ברשימה ממוינת והחזרת האינדקס שלו.

  3. סדר הפוך: Collections.reverse(List<T>) – שיטה זו משמשת להפוך את סדר האלמנטים ברשימה.

  4. פעולות מינ/מקס: Collections.min(Collection<T>) ו-Collections.max(Collection<T>) – שיטות אלו משמשות למציאת האלמנטים המינימליים והמקסימליים באוסף, בהתאמה.

  5. סינכרון: Collections.synchronizedList(List<T>) – שיטה זו משמשת כדי להפוך רשימה לבטוחה עבור חוטים על ידי סינכרון שלה.

  6. אוספים לא ניתנים לשינוי: Collections.unmodifiableList(List<T>) – שיטה זו משמשת ליצירת תצוגה לקריאה בלבד של רשימה, מונעת שינויים.

זהו תכנית Java דוגמה שמדגימה פונקציות שונות של מחלקת השימושיים Collections:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(8);
        numbers.add(1);

        // מיון
        Collections.sort(numbers);
        System.out.println("Sorted List: " + numbers);

        // חיפוש
        int index = Collections.binarySearch(numbers, 3);
        System.out.println("Index of 3: " + index);

        // סדר הפוך
        Collections.reverse(numbers);
        System.out.println("Reversed List: " + numbers);

        // פעולות מינימום/מקסימום
        int min = Collections.min(numbers);
        int max = Collections.max(numbers);
        System.out.println("Min: " + min + ", Max: " + max);

        // סנכרון
        List<Integer> synchronizedList = Collections.synchronizedList(numbers);
        System.out.println("Synchronized List: " + synchronizedList);

        // אוספים לא ניתנים לשינוי
        List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
        System.out.println("Unmodifiable List: " + unmodifiableList);
    }
}

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

מסקנה

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

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

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

אם מצאת את המאמר הזה מעניין, אל תהסס לבדוק את המאמרים האחרים שלי ב-freeCodeCamp ולהתחבר איתי ב-LinkedIn.