מתבוסס על אוספים ב- Java

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

דפוס האיטרטור

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

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

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

הדפסת מבני נתונים מורכבים

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

איטרטורים וכנופיית ארבע דפוסי העיצוב

על פי כנופיית הארבעה (ראה להלן), דפוס העיצוב של איטרטור הוא דפוס התנהגותי, שרעיון המפתח שלו הוא "להוציא את האחריות על הגישה והמעבר מחוץ לאובייקט הרשימה [ עריכת אוסף חושב ]] ולהכניס אותו לאיטרטור. לְהִתְנַגֵד." מאמר זה לא עוסק באותה מידה בתבנית האיטרטור כמו באופן השימוש באיטרטים בפועל. כדי לכסות את התבנית באופן מלא, נדרש דיון כיצד יתוכנן איטרטור, משתתפים (חפצים ושיעורים) בתכנון, עיצובים חלופיים אפשריים ופיזורים של חלופות עיצוב שונות. אני מעדיף להתמקד באופן השימוש באיטרטים בפועל, אך אצביע על כמה משאבים לחקירת דפוס האיטרטור ודפוסי העיצוב בדרך כלל:

  • דפוסי עיצוב: אלמנטים של תוכנה מוכוונת עצמים לשימוש חוזר (אדיסון-ווסלי פרופסיונל, 1994) שנכתבו על ידי אריך גאמה, ריצ'רד הלם, ראלף ג'ונסון וג'ון פליסידס (הידוע גם בכנופיית ארבע או פשוט GoF) הוא המשאב הסופי ללימוד. על דפוסי עיצוב. למרות שהספר פורסם לראשונה בשנת 1994, הוא נותר קלאסי, כפי שמעידה העובדה שהיו יותר מ -40 הדפסים.
  • לבוב טאר, מרצה באוניברסיטת מרילנד במחוז בולטימור, יש שקופיות מצוינות לקורס שלו בנושא דפוסי עיצוב, כולל היכרותו עם דפוס האיטרטור.
  • סדרת JavaWorld של דייויד גירי Java Design Patterns מציגה רבים מתבניות העיצוב של כנופיית ארבע, כולל הדפוסים Singleton, Observer ו- Composite. גם ב- JavaWorld, הסקירה העדכנית יותר של ג'ף פרייזן על דפוסי העיצוב כוללת שלושה מדריכים לדפוסי GoF.

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

ישנן שתי גישות כלליות ליישום איטרטור בהתאם למי ששולט באיטרציה. עבור איטרטור פעיל (המכונה גם איטרטור מפורש או איטרטור חיצוני ), הלקוח שולט באיטרציה במובן שהלקוח יוצר את האיטרטור, אומר לו מתי להתקדם לאלמנט הבא, בודק אם כל אלמנט ביקר, וכן הלאה. גישה זו נפוצה בשפות כמו C ++, והיא הגישה שמקבלת את מירב תשומת הלב בספר GoF. למרות שאיטרטים בג'אווה קיבלו צורות שונות, השימוש באיטרטור פעיל היה למעשה האופציה הניתנת לביצוע לפני Java 8.

עבור איטרטור פסיבי (הידוע גם בשם איטרטר משתמע , איטרטור פנימי או איטרטור החזרה), האיטרטור עצמו שולט באיטרציה. הלקוח בעצם אומר לאיטרטור, "בצע פעולה זו על האלמנטים באוסף." גישה זו נפוצה בשפות כמו LISP המספקות פונקציות או סגירות אנונימיות. עם שחרורו של Java 8, גישה זו לאיטרציה מהווה כעת חלופה סבירה עבור מתכנתי Java.

תוכניות שמות ל- Java 8

אמנם לא ממש גרוע כמו Windows (NT, 2000, XP, VISTA, 7, 8, ...) היסטוריית הגרסאות של Java כוללת מספר תוכניות שמות. כדי להתחיל, עלינו להתייחס למהדורה הסטנדרטית של Java כ- "JDK", "J2SE" או "Java SE"? מספרי הגרסאות של Java התחילו די פשוטים - 1.0, 1.1 וכו '- אבל הכל השתנה עם גרסה 1.5, שהייתה ממותגת Java (או JDK) 5. כאשר אני מתייחס לגירסאות מוקדמות של Java אני משתמש בביטויים כמו "Java 1.0" או "Java" 1.1, "אבל אחרי הגרסה החמישית של Java אני משתמש בביטויים כמו" Java 5 "או" Java 8. "

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

צורות אחרות של איטרציה ב- Java 8

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

איטרציה עם כיתת המניין

ב- Java 1.0 ו- 1.1, שתי כיתות האוסף הראשוניות היו Vectorו- Hashtable, ודפוס העיצוב של Iterator יושם בכיתה שנקראה Enumeration. בדיעבד זה היה שם רע לכיתה. אל תבלבל את הכיתה Enumerationעם הרעיון של סוגי enum , אשר לא הופיעו עד Java 5. היום הן Vectorו Hashtableהן כיתות גנריות, אבל אז הגנריקה לא היו חלק מן השפה Java. הקוד לעיבוד וקטור מחרוזות באמצעות Enumerationייראה בערך כמו רישום 1.

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

 Vector names = new Vector(); // ... add some names to the collection Enumeration e = names.elements(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); System.out.println(name); } 

איטרציה עם כיתת איטרטור

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

רישום 2. באמצעות איטרטור כדי לחזור על רשימת מחרוזות

 List names = new LinkedList(); // ... add some names to the collection Iterator i = names.iterator(); while (i.hasNext()) { String name = (String) i.next(); System.out.println(name); } 

איטרציה עם גנריות והפרופ-לופ המשופר

Java 5 העניקה לנו גנריות, את הממשק Iterableואת ה- for-loop המשופר. ה- for-loop המשופר הוא אחת התוספות הקטנות האהובות עליי בכל הזמנים ל- Java. בריאת iterator והשיחות שלה hasNext()ואת next()שיטות אינן הביעו במפורש את הקוד, אבל הם עדיין מתרחשים מאחורי הקלעים. לפיכך, למרות שהקוד הוא קומפקטי יותר, אנו עדיין משתמשים באיטרט פעיל. באמצעות Java 5, הדוגמה שלנו תיראה דומה למה שאתה רואה ברשימה 3.

רישום 3. שימוש בגנריות וב- for-loop המשופר כדי לחזור על רשימת מחרוזות

 List names = new LinkedList(); // ... add some names to the collection for (String name : names) System.out.println(name); 

Java 7 gave us the diamond operator, which reduces the verbosity of generics. Gone were the days of having to repeat the type used to instantiate the generic class after invoking the new operator! In Java 7 we could simplify the first line in Listing 3 above to the following:

 List names = new LinkedList(); 

A mild rant against generics

The design of a programming language involves tradeoffs between the benefits of language features versus the complexity they impose on the syntax and semantics of the language. For generics, I am not convinced that the benefits outweigh the complexity. Generics solved a problem that I did not have with Java. I generally agree with Ken Arnold's opinion when he states: "Generics are a mistake. This is not a problem based on technical disagreements. It's a fundamental language design problem [...] The complexity of Java has been turbocharged to what seems to me relatively small benefit."

Fortunately, while designing and implementing generic classes can sometimes be overly complicated, I have found that using generic classes in practice is usually straightforward.

Iteration with the forEach() method

Before delving into Java 8 iteration features, let's reflect on what's wrong with the code shown in the previous listings–which is, well, nothing really. There are millions of lines of Java code in currently deployed applications that use active iterators similar to those shown in my listings. Java 8 simply provides additional capabilities and new ways of performing iteration. For some scenarios, the new ways can be better.

The major new features in Java 8 center on lambda expressions, along with related features such as streams, method references, and functional interfaces. These new features in Java 8 allow us to seriously consider using passive iterators instead of the more conventional active iterators. In particular, the Iterable interface provides a passive iterator in the form of a default method called forEach().

A default method, another new feature in Java 8, is a method in an interface with a default implementation. In this case, the forEach() method is actually implemented using an active iterator in a manner similar to what you saw in Listing 3.

Collection classes that implement Iterable (for example, all list and set classes) now have a forEach() method. This method takes a single parameter that is a functional interface. Therefore the actual parameter passed to the forEach() method is a candidate for a lambda expression. Using the features of Java 8, our running example would evolve to the form shown in Listing 4.

Listing 4. Iteration in Java 8 using the forEach() method

 List names = new LinkedList(); // ... add some names to the collection names.forEach(name -> System.out.println(name)); 

Note the difference between the passive iterator in Listing 4 and the active iterator in the previous three listings. In the first three listings, the loop structure controls the iteration, and during each pass through the loop, an object is retrieved from the list and then printed. In Listing 4, there is no explicit loop. We simply tell the forEach() method what to do with the objects in the list — in this case we simply print the object. Control of the iteration resides within the forEach() method.

Iteration with Java streams

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