דוגמה ל- Java Generics Tutorial – Generic Method, Class, Interface

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

נבחן את הנושאים הבאים של הג'נריקס ב-Java.

  1. ג'נריקס ב-Java

  2. מחלקת ג'נריק ב-Java

  3. ממשק ג'נריק ב-Java

  4. סוג ג'נרי ב-Java

  5. שיטת ג'נריק ב-Java

  6. פרמטרים מוגבלים לסוגים ב-Java Generics

  7. גנריקים ב-Java וירושה

  8. קבוצות גנריות ב-Java ותת-סוגיות

  9. סימני גנריקים ב-Java

  10. סימן גנרי עליון ב-Java

  11. סימן גנרי ללא הגבלה ב-Java

  12. סימן גנרי עליון ב-Java

  13. תת-סוגיות בשימוש בסימני גנריקים

  14. מחיקת סוגי גנריים ב-Java

  15. שאלות נפוצות על גנריקים

1. גנריקים ב־Java

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

List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK

for(Object obj : list){
	//המרה מסוג אובייקט שגורמת ל־ClassCastException בזמן ריצה
    String str=(String) obj; 
}

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

List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>(); 
list1.add("abc");
//list1.add(new Integer(5)); //שגיאת קומפילציה

for(String str : list1){
     //לא נדרשת המרת סוג, מונעת ClassCastException
}

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

2. מחלקת גנריק ב־Java

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

package com.journaldev.generics;

public class GenericsTypeOld {

	private Object t;

	public Object get() {
		return t;
	}

	public void set(Object t) {
		this.t = t;
	}

        public static void main(String args[]){
		GenericsTypeOld type = new GenericsTypeOld();
		type.set("Pankaj"); 
		String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
	}
}

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

package com.journaldev.generics;

public class GenericsType<T> {

	private T t;
	
	public T get(){
		return this.t;
	}
	
	public void set(T t1){
		this.t=t1;
	}
	
	public static void main(String args[]){
		GenericsType<String> type = new GenericsType<>();
		type.set("Pankaj"); //valid
		
		GenericsType type1 = new GenericsType(); //raw type
		type1.set("Pankaj"); //valid
		type1.set(10); //valid and autoboxing support
	}
}

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

טיפ: ניתן להשתמש באנוטציה @SuppressWarnings("rawtypes") כדי להתעלם מאזהרת המהדר, ראה מדריך האנוטציות של ג'אווה.

גם שים לב שהוא תומך באוטומטיזציה של ג'אווה.

ממשק גנרי של ג'אווה

ממשק Comparable הוא דוגמה נהדרת לגנריקה בממשקים והוא נכתב כך:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

בדומה לכך, אנו יכולים ליצור ממשקים גנריים ב-Java. ניתן גם להכיל מספר סוגים של פרמטרים כמו בממשק Map. שוב, ניתן לספק ערך פרמטריזטו לסוג שנפרמטר, לדוגמה new HashMap<String, List<String>>(); הוא תקף.

4. סוג גנרי של ג'אווה

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

  • E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
  • K – Key (Used in Map)
  • N – Number
  • T – Type
  • V – Value (Used in Map)
  • S,U,V etc. – 2nd, 3rd, 4th types

5. שיטת גנרי ג'אווה

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

package com.journaldev.generics;

public class GenericsMethods {

	//שיטת גנריקס ב-Java
	public static  boolean isEqual(GenericsType g1, GenericsType g2){
		return g1.get().equals(g2.get());
	}
	
	public static void main(String args[]){
		GenericsType g1 = new GenericsType<>();
		g1.set("Pankaj");
		
		GenericsType g2 = new GenericsType<>();
		g2.set("Pankaj");
		
		boolean isEqual = GenericsMethods.isEqual(g1, g2);
		//ההצהרה לעיל ניתנת לכתיבה פשוטה כך
		isEqual = GenericsMethods.isEqual(g1, g2);
		//תכונה זו, שנקראת הסקת סוג, מאפשרת לך לקרוא לשיטת גנריקס כמו שיטה רגילה, בלי לציין סוג בין סוגריים מזווית.
		//המהדר ישלים את הסוג שנדרש
	}
}

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

6. פרמטרים מגבליים של סוגי גנריקס ב-Java

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

public static <T extends Comparable<T>> int compare(T t1, T t2){
		return t1.compareTo(t2);
	}

קריאת השיטות האלו דומה לשיטת הבלתי מוגבלת רק שאם ננסה להשתמש באף מחלקה שאינה Comparable, זה יזרוק שגיאת זמן קומפילציה. פרמטרי סוג מוגבלים ניתן להשתמש בהם גם בשיטות וגם במחלקות וממשקים. גנריקס ב-Java תומך גם בגבולות מרובים, כלומר <T extends A & B & C>. במקרה זה, A יכולה להיות ממשק או מחלקה. אם A היא מחלקה, אז B ו-C צריכים להיות ממשקים. אי אפשר להכיל יותר ממחלקה אחת בגבולות מרובים.

7. גנריקס ב-Java וירושה

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

package com.journaldev.generics;

public class GenericsInheritance {

	public static void main(String[] args) {
		String str = "abc";
		Object obj = new Object();
		obj=str; // works because String is-a Object, inheritance in java
		
		MyClass myClass1 = new MyClass();
		MyClass myClass2 = new MyClass();
		//myClass2=myClass1; // שגיאת קומפילציה מאחר ו-MyClass אינו MyClass
		obj = myClass1; // MyClass parent is Object
	}
	
	public static class MyClass{}

}

אנחנו לא רשאים להקצות משתנה של MyClass למשתנה של MyClassמשום שהם אינם קשורים, למעשה ההורה של MyClass הוא Object.

8. כיתות גנריות ב-Java ותת סוגים

ניתן ליצור תת-סוג של מחלקה או ממשק גנרי על ידי הרחבתו או המימוש שלו. היחס בין פרמטרי הסוג של מחלקה או ממשק אחד לפרמטרי הסוג של מחלקה אחרת נקבע על ידי סעיפי extends ו-implements. לדוגמה, ArrayList מממש את List שהוא מרחיב את Collection, לכן ArrayList הוא תת-סוג של List ו-List הוא תת-סוג של Collection. היחס של תת-סוגים נשמר כל עוד אנחנו לא משנים את ארגומנט הסוג, להלן דוגמה לפרמטרי סוגים מרובים.

interface MyList<E,T> extends List<E>{
}

תתי הסוגים של List יכולים להיות MyList, MyList וכו'.

9. סימני גנריקס ב-Java

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

9.1) גנריקים של Java כרטיסיות גבוהות מוגבלות

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

public static double sum(List<Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}

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

package com.journaldev.generics;

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

public class GenericsWildcards {

	public static void main(String[] args) {
		List<Integer> ints = new ArrayList<>();
		ints.add(3); ints.add(5); ints.add(10);
		double sum = sum(ints);
		System.out.println("Sum of ints="+sum);
	}

	public static double sum(List<? extends Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}
}

זה דומה לכתיבת הקוד שלנו במונחי ממשק, בשיטה הנ"ל אנו יכולים להשתמש בכל השיטות של המחלקה גבול עליון Number. שימו לב שעם רשימה עם גבול עליון, אנו לא מורשים להוסיף כל אובייקט לרשימה חוץ מ- null. אם ננסה להוסיף אלמנט לרשימה בתוך שיטת ה-sum, התוכנית לא תהיה יכולה להתממש.

9.2) Java Generics Unbounded Wildcard

לעיתים יש לנו מצב שבו רוצים שהשיטה הגנרית שלנו תתעבוד עם כל סוגי המשתנים, במקרה כזה ניתן להשתמש בתווית רחבת המרחק. זה דומה לשימוש ב-<? extends Object>.

public static void printData(List<?> list){
		for(Object obj : list){
			System.out.print(obj + "::");
		}
	}

אנו יכולים לספק ארגומנט של רשימת String או רשימת Integer או כל סוג אחר של רשימת Object לשיטת ה-printData. דומה לרשימת גבול עליון, אנו לא מורשים להוסיף דברים לרשימה.

9.3) Java Generics Lower bounded Wildcard

נניח שנרצה להוסיף מספרים שלמים לרשימת מספרים שלמים בשיטה, אנו יכולים לשמור את סוג הארגומנט כ List<Integer>, אך זה יקשר אותו עם Integers בעוד ש־List<Number> ו־List<Object> יכולות גם להחזיק מספרים שלמים, לכן נוכל להשתמש ב־wildcard עם המילה השמורה super ומחלקת lower bound כדי להשיג זאת. נוכל להעביר lower bound או כל supertype של lower bound כארגומנט, במקרה זה, מהדפדפן של Java מאפשר להוסיף סוגי אובייקטים lower bound לרשימה.

public static void addIntegers(List<? super Integer> list){
		list.add(new Integer(50));
	}

10. Subtyping באמצעות Generics Wildcard

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

11. Java Generics Type Erasure

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

public class Test<T extends Comparable<T>> {

    private T data;
    private Test<T> next;

    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }

    public T getData() { return this.data; }
}

המהדר של Java מחליף את הפרמטר סוג הגבול T עם הממשק bound הראשון, Comparable, כמו בקוד למטה:

public class Test {

    private Comparable data;
    private Test next;

    public Node(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }

    public Comparable getData() { return data; }
}

12. שאלות נפוצות על Generics

12.1) למה אנו משתמשים ב-Generics ב-Java?

Generics מספק קוד מבדיקת סוגים חזקה בזמן הקידוד ומפחית את סיכון ה- ClassCastException וההמרה המפורשת של אובייקטים.

12.2) מהו T ב-Generics?

אנו משתמשים ב-<T> כדי ליצור מחלקה, ממשק, ושיטה גנרית. ה-T מוחלף עם הסוג הממשי כאשר אנו משתמשים בו.

12.3) איך Generics עובד ב-Java?

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

13. גנריקים ב-Java – קריאה נוספת

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

Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface