CodeGym /בלוג Java /Random-HE /Java Generics: כיצד להשתמש בסוגריים זוויתיים בפועל
John Squirrels
רָמָה
San Francisco

Java Generics: כיצד להשתמש בסוגריים זוויתיים בפועל

פורסם בקבוצה

מבוא

החל מ-JSE 5.0, נוספו גנריות לארסנל של שפת Java.

מה זה גנרי ב-Java?

גנריות הן המנגנון המיוחד של Java ליישום תכנות גנרי - דרך לתאר נתונים ואלגוריתמים המאפשרת לך לעבוד עם סוגי נתונים שונים מבלי לשנות את תיאור האלגוריתמים. באתר אורקל יש מדריך נפרד המוקדש לגנריות: " שיעור ". כדי להבין גנריות, תחילה עליך להבין מדוע יש צורך בהן ומה הן נותנות. הסעיף " מדוע להשתמש בגנריות? " במדריך אומר שכמה מטרות הן בדיקת סוגים חזקה יותר בזמן הקומפילציה וביטול הצורך בגבס מפורש. גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 1בואו להתכונן לכמה מבחנים במהדר ה-Java המקוון Tutorialspoint האהוב שלנו . נניח שיש לך את הקוד הבא:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
הקוד הזה יפעל בצורה מושלמת. אבל מה אם הבוס יבוא אלינו ויגיד ש"שלום עולם!" האם ביטוי בשימוש יתר ושצריך להחזיר רק "הלו"? נסיר את הקוד שמשרשר "עולם!" זה נראה לא מזיק מספיק, נכון? אבל למעשה, אנו מקבלים שגיאה בזמן קומפילציה:

error: incompatible types: Object cannot be converted to String
הבעיה היא שברשימה שלנו מאחסנים אובייקטים. String הוא צאצא של Object (מאחר שכל מחלקות Java יורשים באופן מרומז את Object ), מה שאומר שאנחנו צריכים קאסט מפורש, אבל לא הוספנו אחד. במהלך פעולת השרשור, שיטת String.valueOf(obj) הסטטית תיקרא באמצעות האובייקט. בסופו של דבר, הוא יקרא לשיטת toString של המחלקה Object . במילים אחרות, הרשימה שלנו מכילה אובייקט . זה אומר שבכל מקום בו אנו צריכים סוג מסוים (לא Object ), נצטרך לבצע את המרת הסוג בעצמנו:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
עם זאת, במקרה זה, מכיוון שרשימה לוקחת אובייקטים, היא יכולה לאחסן לא רק String s, אלא גם Integer s. אבל הדבר הגרוע ביותר הוא שהמהדר לא רואה כאן שום דבר לא בסדר. ועכשיו נקבל שגיאה AT RUN TIME (המכונה "שגיאת זמן ריצה"). השגיאה תהיה:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
אתה חייב להסכים שזה לא כל כך טוב. וכל זה בגלל שהמהדר אינו בינה מלאכותית שמסוגלת לנחש נכון תמיד את כוונת המתכנת. Java SE 5 הציג גנריות כדי לאפשר לנו לספר למהדר על הכוונות שלנו - על אילו סוגים אנו הולכים להשתמש. אנחנו מתקנים את הקוד שלנו על ידי כך שאומרים למהדר מה אנחנו רוצים:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
כפי שאתה יכול לראות, אין לנו עוד צורך בקאסט למחרוזת . בנוסף, יש לנו סוגריים של זווית המקיפים את ארגומנט הסוג. כעת המהדר לא יאפשר לנו להדר את המחלקה עד שנסיר את השורה שמוסיפה 123 לרשימה, מכיוון שמדובר ב- Integer . וזה יגיד לנו את זה. אנשים רבים מכנים תרופות גנריות "סוכר תחבירי". והם צודקים, שכן לאחר הידור הגנרי, הם באמת הופכים לאותו סוג המרות. בואו נסתכל על ה-bytecode של המחלקות הקומפיליות: אחד שמשתמש בקאסט מפורש ואחד שמשתמש בגנריות: גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 2לאחר הקומפילציה, כל הגנריות נמחקות. זה נקרא " מחיקת סוג ". מחיקת סוגים וגנריות מתוכננות להיות תואמות לאחור עם גרסאות ישנות יותר של ה-JDK ובו זמנית לאפשר למהדר לעזור עם הגדרות סוג בגרסאות חדשות של Java.

סוגי גלם

אם כבר מדברים על תרופות גנריות, תמיד יש לנו שתי קטגוריות: סוגים עם פרמטרים וסוגים גולמיים. טיפוסים גולמיים הם טיפוסים המשמיטים את "הבהרת הסוג" בסוגריים של זווית: גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 3טיפוסים עם פרמטרים, מצדם, כוללים "הבהרה": גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 4כפי שניתן לראות, השתמשנו במבנה יוצא דופן, המסומן בחץ בצילום המסך. זהו תחביר מיוחד שהתווסף ל-Java SE 7. הוא נקרא " יהלום ". למה? סוגרי הזווית יוצרים יהלום: <> . כדאי גם לדעת שתחביר היהלום קשור למושג " הסקת סוג ". אחרי הכל, המהדר, כשהוא רואה את <> בצד ימין, מסתכל בצד השמאלי של אופרטור ההקצאה, שם הוא מוצא את סוג המשתנה שהערך שלו מוקצה. בהתבסס על מה שהוא מוצא בחלק זה, הוא מבין את סוג הערך בצד ימין. למעשה, אם ניתן סוג גנרי משמאל, אך לא מימין, המהדר יכול להסיק את הסוג:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
אבל זה מערבב את הסגנון החדש עם הגנריות ואת הסגנון הישן בלעדיהם. וזה מאוד לא רצוי. בעת קומפילציה של הקוד למעלה, אנו מקבלים את ההודעה הבאה:

Note: HelloWorld.java uses unchecked or unsafe operations
למעשה, הסיבה שבגללה אתה בכלל צריך להוסיף כאן יהלום נראית לא מובנת. אבל הנה דוגמה:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
תזכרו של ArrayList יש בנאי שני שלוקח אוסף כארגומנט. וכאן מסתתר משהו מרושע. ללא תחביר היהלום, המהדר לא מבין שמרמה אותו. עם תחביר היהלום, זה כן. לכן, כלל מס' 1 הוא: השתמש תמיד בתחביר היהלום עם טיפוסים עם פרמטרים. אחרת, אנו מסתכנים בפספוס היכן שאנו משתמשים בסוגים גולמיים. כדי לבטל אזהרות "משתמש בפעולות לא מסומנות או לא בטוחות", אנו יכולים להשתמש בהערה @SuppressWarnings("לא מסומנת") על שיטה או מחלקה. אבל תחשוב למה החלטת להשתמש בזה. זכרו כלל מספר אחד. אולי אתה צריך להוסיף ארגומנט סוג.

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

גנריות מאפשרות לך ליצור שיטות שסוגי הפרמטרים וסוג ההחזר שלהן מותאמים לפרמטרים. חלק נפרד מוקדש ליכולת זו במדריך של אורקל: " שיטות כלליות ". חשוב לזכור את התחביר הנלמד במדריך זה:
  • הוא כולל רשימה של פרמטרי סוג בתוך סוגריים זווית;
  • רשימת פרמטרי הסוג קודמת לסוג ההחזרה של השיטה.
בואו נסתכל על דוגמה:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
אם תסתכל על המחלקה Util , תראה שיש לה שתי שיטות גנריות. הודות לאפשרות להסקת סוג, נוכל לציין את הסוג ישירות למהדר, או שנוכל לציין אותו בעצמנו. שתי האפשרויות מוצגות בדוגמה. אגב, התחביר הגיוני מאוד אם חושבים על זה. כאשר מצהירים על שיטה גנרית, אנו מציינים את פרמטר ה-type לפני השיטה, כי אם נכריז על פרמטר ה-type לאחר השיטה, ה-JVM לא יוכל להבין באיזה סוג להשתמש. בהתאם לכך, תחילה אנו מצהירים שנשתמש בפרמטר T type, ולאחר מכן אנו אומרים שאנו הולכים להחזיר את הסוג הזה. באופן טבעי, Util.<Integer>getValue(element, String.class) ייכשל עם שגיאה: סוגים לא תואמים: לא ניתן להמיר Class<String> ל-Class<Integer> . בעת שימוש בשיטות גנריות, עליך תמיד לזכור את מחיקת הסוג. בואו נסתכל על דוגמה:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
זה יפעל בסדר גמור. אבל רק כל עוד המהדר מבין שסוג ההחזר של השיטה הנקראת הוא Integer . החלף את הצהרת הפלט של המסוף בשורה הבאה:
System.out.println(Util.getValue(element) + 1);
אנו מקבלים שגיאה:

bad operand types for binary operator '+', first type: Object, second type: int.
במילים אחרות, התרחשה מחיקת סוג. המהדר רואה שאף אחד לא ציין את הסוג, אז הטיפוס מצוין כ- Object והמתודה נכשלת עם שגיאה.

שיעורים כלליים

לא רק שיטות ניתנות לפרמטרים. שיעורים יכולים גם כן. הקטע "סוגים כלליים" במדריך של אורקל מוקדש לכך. הבה נשקול דוגמה:
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
הכל פשוט כאן. אם נשתמש במחלקה הגנרית, פרמטר הסוג מצוין אחרי שם המחלקה. עכשיו בואו ניצור מופע של המחלקה הזו בשיטה הראשית :
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
הקוד הזה יפעל היטב. המהדר רואה שיש רשימה של מספרים ואוסף מחרוזות . אבל מה אם נבטל את פרמטר הסוג ונעשה כך:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
אנו מקבלים שגיאה:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
שוב, זוהי מחיקת סוג. מכיוון שהמחלקה כבר לא משתמשת בפרמטר type, המהדר מחליט שמאחר שהעברנו List , השיטה עם List<Integer> היא המתאימה ביותר. ואנחנו נכשלים עם שגיאה. לכן, יש לנו כלל מס' 2: אם יש לך מחלקה גנרית, ציין תמיד את פרמטרי הסוג.

הגבלות

אנו יכולים להגביל את הסוגים המצוינים בשיטות גנריות ובמחלקות. לדוגמה, נניח שאנו רוצים שמיכל יקבל רק Number כארגומנט הסוג. תכונה זו מתוארת בסעיף Bounded Type Parameters של המדריך של אורקל. בואו נסתכל על דוגמה:
import java.util.*;
public class HelloWorld {

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number) { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
כפי שאתה יכול לראות, הגבלנו את פרמטר הסוג למחלקה /ממשק Number או לצאצאיו. שים לב שאתה יכול לציין לא רק מחלקה, אלא גם ממשקים. לדוגמה:
public static class NumberContainer<T extends Number & Comparable> {
גנריות תומכות גם בתווים כלליים . הם מחולקים לשלושה סוגים: השימוש שלך בתווים כלליים צריך לעמוד בעקרון Get-Put . זה יכול להתבטא כך:
  • השתמש בתו כללי להרחבה כאשר אתה מקבל רק ערכים ממבנה.
  • השתמש בתו כללי על כאשר אתה מכניס רק ערכים למבנה.
  • ואל תשתמש בתו כללי כששניכם רוצים להגיע ולהעביר מ/אל מבנה.
עקרון זה נקרא גם עקרון ה-Pecs (Producer Extends Consumer Super). הנה דוגמה קטנה מקוד המקור לשיטת Collections.copy של Java : גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 5והנה דוגמה קטנה למה שלא יעבוד:
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
אבל אם אתה מחליף מרחיב בסופר , אז הכל בסדר. מכיוון שאנו מאכלסים את הרשימה בערך לפני הצגת התוכן שלה, היא צרכן . בהתאם לכך, אנו משתמשים בסופר.

יְרוּשָׁה

לגנריות יש תכונה מעניינת נוספת: ירושה. הדרך שבה הורשה פועלת עבור גנריות מתוארת תחת " גנריות, ירושה ותתי סוגים " במדריך של אורקל. הדבר החשוב הוא לזכור ולזהות את הדברים הבאים. אנחנו לא יכולים לעשות את זה:
List<CharSequence> list1 = new ArrayList<String>();
כי ירושה עובדת אחרת עם גנריות: גנריות ב-Java: כיצד להשתמש בסוגריים זוויתיים בפועל - 6והנה עוד דוגמה טובה שתיכשל עם שגיאה:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
שוב, הכל פשוט כאן. רשימה<String> אינה צאצא של List<Object> , למרות שהמחרוזת היא צאצא של Object . כדי לחזק את מה שלמדת, אנו מציעים לך לצפות בשיעור וידאו מקורס Java שלנו
אז ריעננו את הזיכרון שלנו לגבי תרופות גנריות. אם אתה מנצל רק לעתים רחוקות את מלוא היכולות שלהם, חלק מהפרטים מטושטשים. אני מקווה שהסקירה הקצרה הזו עזרה להרים את הזיכרון שלך. לתוצאות טובות עוד יותר, אני ממליץ לך בחום להכיר את החומר הבא:
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION