JavaRush /בלוג Java /Random-HE /תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל
Viacheslav
רָמָה

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

פורסם בקבוצה

מבוא

החל מ-JSE 5.0, נוספו גנריות לארסנל שפות Java.
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 1

מהן גנריות ב-Java?

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

ראשית, כדי להבין תרופות גנריות, אתה צריך להבין מדוע הם נחוצים בכלל ומה הם מספקים. במדריך בסעיף " מדוע להשתמש בגנריות ?" אומרים שאחת המטרות היא בדיקת סוגים חזקה יותר בזמן קומפילציה וביטול הצורך בליהוק מפורש.
תורת הגנריות בג'אווה או כיצד ליישם סוגריים בפועל - 2
בואו נכין מהדר ג'אווה מקוון של 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);
	}
}
הקוד הזה יפעל בסדר. אבל מה אם יבואו אלינו ואמרו את המשפט "שלום, עולם!" מוכה ואתה יכול רק להחזיר שלום? הבה נסיר את השרשור עם המחרוזת מהקוד ", world!". נראה שמה יכול להיות יותר לא מזיק? אבל למעשה, נקבל שגיאה DURING COMPILATION : error: incompatible types: Object cannot be converted to String העניין הוא שבמקרה שלנו List מאחסנת רשימה של אובייקטים מסוג Object. מכיוון ש-String הוא צאצא של Object (מאחר שכל המחלקות עוברות בירושה באופן מרומז מ-Object ב-Java), זה דורש cast מפורש, מה שלא עשינו. ובשרשור, המתודה הסטטית String.valueOf(obj) תיקרא על האובייקט, שבסופו של דבר תקרא למתודה toString על האובייקט. כלומר, הרשימה שלנו מכילה אובייקט. מסתבר שבמקום שבו אנחנו צריכים סוג מסוים, ולא 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);
		}
	}
}
עם זאת, במקרה זה, כי רשימה מקבלת רשימה של אובייקטים, היא מאחסנת לא רק מחרוזת, אלא גם מספר שלם. אבל הדבר הגרוע ביותר הוא שבמקרה הזה המהדר לא יראה שום דבר לא בסדר. וכאן נקבל שגיאה DURING EXECUTION (הם אומרים גם שהשגיאה התקבלה "בזמן ריצה"). השגיאה תהיה: 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);
		}
	}
}
כפי שאנו יכולים לראות, אנחנו כבר לא צריכים את הקאסט ל-String. בנוסף, יש לנו כעת סוגריים זווית הממסגרים גנריות. כעת המהדר לא יאפשר להרכיב את המחלקה עד שנסיר את התוספת של 123 לרשימה, כי זהו מספר שלם. הוא יגיד לנו את זה. אנשים רבים מכנים תרופות גנריות "סוכר תחבירי". והם צודקים, שכן תרופות גנריות אכן יהפכו לאותן קאסטות בעת הידור. בואו נסתכל על ה-bytecode של המחלקות הקומפיליות: עם ליהוק ידני ושימוש כללי:
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 3
לאחר הקומפילציה, כל מידע על גנריות נמחק. זה נקרא "מחיקת סוג" או " מחיקת סוג ". מחיקת סוגים וגנריות נועדו לספק תאימות לאחור עם גרסאות ישנות יותר של ה-JDK, ובמקביל לאפשר למהדר לסייע בהסקת סוג בגרסאות חדשות יותר של Java.
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 4

סוגי גלם או סוגי גלם

כשמדברים על גנריות, תמיד יש לנו שתי קטגוריות: טיפוסים מודפסים (Generic Types) וסוגים "raw" (Raw Types). סוגי גלם הם סוגים ללא ציון ה"הסמכה" בסוגריים זווית:
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 5
סוגים מוקלדים הם הפוכים, עם ציון "הבהרה":
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 6
כפי שאנו יכולים לראות, השתמשנו בעיצוב יוצא דופן, המסומן בחץ בצילום המסך. זהו תחביר מיוחד שהתווסף ב-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("unchecked") דיכוי מתורגם כ- suppress, כלומר, פשוטו כמשמעו, לדכא אזהרות. אבל תחשוב למה החלטת לציין את זה? זכור כלל מספר אחד ואולי אתה צריך להוסיף הקלדה.
תורת הגנריקה בג'אווה או כיצד ליישם סוגריים בפועל - 7

שיטות גנריות

גנריות מאפשרות לך להקליד שיטות. ישנו סעיף נפרד המוקדש לתכונה זו במדריך של אורקל: " שיטות כלליות ". ממדריך זה, חשוב לזכור את התחביר:
  • כולל רשימה של פרמטרים מוקלדים בתוך סוגריים זווית;
  • רשימת הפרמטרים המוקלדים עוברת לפני השיטה המוחזרת.
בואו נסתכל על דוגמה:
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, אנו רואים בה שתי שיטות מוקלדות. עם הסקת סוג, אנחנו יכולים לספק את הגדרת הסוג ישירות למהדר, או שנוכל לציין אותה בעצמנו. שתי האפשרויות מוצגות בדוגמה. אגב, התחביר די הגיוני אם חושבים על זה. בהקלדת שיטה, אנו מציינים את הגנרי לפני השיטה כי אם נשתמש בגנרי לאחר השיטה, Java לא תוכל להבין באיזה סוג להשתמש. לכן, תחילה אנו מודיעים שנשתמש ב-T גנרי, ולאחר מכן אנו אומרים שאנו הולכים להחזיר את הגנרי הזה. באופן טבעי, Util.<Integer>getValue(element, String.class)זה ייכשל עם שגיאה incompatible types: Class<String> cannot be converted to 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); ונקבל את השגיאה: טיפוסי אופרנד גרועים לאופרטור בינארי '+', סוג ראשון: Object , סוג שני: int כלומר, טיפוסים נמחקו. המהדר רואה שאף אחד לא ציין את הסוג, הטיפוס מצוין כ-Object וביצוע הקוד נכשל עם שגיאה.
Теория дженериков в Java or How на практике ставить скобки - 8

סוגים גנריים

אתה יכול להקליד לא רק שיטות, אלא גם מחלקות עצמן. לאורקל יש סעיף " סוגים כלליים " המוקדש לכך במדריך שלהם. בואו נסתכל על דוגמה:
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);
}
זה יעבוד טוב. המהדר רואה שיש רשימה של מספרים ואוסף מסוג String. אבל מה אם נמחק את הגנריות ונעשה כך:
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 הקלד מחיקה שוב. מכיוון שלמחלקה אין יותר גנרי, המהדר מחליט שמאחר שעברנו List, מתודה עם List<Integer> מתאימה יותר. ואנחנו נופלים עם טעות. לכן, כלל מס' 2: אם מחלקה מוקלדת, ציין תמיד את הסוג ב- Generic .

הגבלות

אנו יכולים להחיל הגבלה על סוגים המצוינים בגנריות. לדוגמה, אנו רוצים שהמכל יקבל רק Number כקלט. תכונה זו מתוארת במדריך של Oracle בסעיף פרמטרים מסוג מוגבל . בואו נסתכל על דוגמה:
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> { לגנריות יש גם את הרעיון של Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html הם, בתורם, מחולקים לשלושה סוגים: מה שנקרא עיקרון Get Put חל על Wildcards . הם יכולים לבוא לידי ביטוי בצורה הבאה:
Теория дженериков в Java or How на практике ставить скобки - 9
עיקרון זה נקרא גם עקרון PECS (Producer Extends Consumer Super). אתה יכול לקרוא עוד על Habré במאמר " שימוש בתווים כלליים כלליים לשיפור השימושיות של Java API ", וכן בדיון המצוין על stackoverflow: " שימוש בתווים כלליים ב-Java Generics ". הנה דוגמה קטנה ממקור Java - שיטת Collections.copy:
Теория дженериков в Java or How на практике ставить скобки - 10
ובכן, דוגמה קטנה לאופן שבו זה לא יעבוד:
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 or How на практике ставить скобки - 11
והנה עוד דוגמה טובה שתיכשל עם שגיאה:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
גם כאן הכל פשוט. רשימה<String> אינה צאצא של List<Object>, אם כי מחרוזת היא צאצא של אובייקט.

סופי

אז ריעננו את זיכרון הגנריות שלנו. אם הם משמשים רק לעתים רחוקות בכל כוחם, חלק מהפרטים נופלים מהזיכרון. אני מקווה שסקירה קצרה זו תעזור לרענן את הזיכרון שלך. וכדי לקבל תוצאות טובות יותר, אני ממליץ בחום לקרוא את החומרים הבאים: #ויאצ'סלב
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION