JavaRush /בלוג Java /Random-HE /גנריות לחתולים
Viacheslav
רָמָה

גנריות לחתולים

פורסם בקבוצה
גנריות לחתולים - 1

מבוא

היום הוא יום נהדר להיזכר במה שאנחנו יודעים על ג'אווה. לפי המסמך החשוב ביותר, כלומר. Java Language Specification (JLS - Java Language Specifiaction), Java היא שפת הקלדה חזקה, כמתואר בפרק " פרק 4. סוגים, ערכים ומשתנים ". מה זה אומר? נניח שיש לנו שיטה עיקרית:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
הקלדה חזקה מבטיחה שכאשר הקוד הזה מורכב, המהדר יבדוק שאם ציינו את סוג משתנה הטקסט בתור String, אז אנחנו לא מנסים להשתמש בו בשום מקום בתור משתנה מסוג אחר (לדוגמה, כ-Integer) . לדוגמה, אם ננסה לשמור ערך במקום טקסט 2L(כלומר ארוך במקום String), נקבל שגיאה בזמן ההידור:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
הָהֵן. הקלדה חזקה מאפשרת לך להבטיח שפעולות על אובייקטים מבוצעות רק כאשר פעולות אלו חוקיות עבור אותם אובייקטים. זה נקרא גם בטיחות סוג. כפי שנאמר ב-JLS, ישנן שתי קטגוריות של טיפוסים ב-Java: טיפוסים פרימיטיביים וסוגי התייחסות. אתה יכול לזכור לגבי טיפוסים פרימיטיביים ממאמר הביקורת: " טיפוסים פרימיטיביים בג'אווה: הם לא כל כך פרימיטיביים ." סוגי הפניות יכולים להיות מיוצגים על ידי מחלקה, ממשק או מערך. והיום נתעניין בסוגי התייחסות. ובואו נתחיל עם מערכים:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
קוד זה פועל ללא שגיאות. כפי שאנו יודעים (לדוגמה, מ" Oracle Java Tutorial: Arrays "), מערך הוא מיכל המאחסן נתונים מסוג אחד בלבד. במקרה זה - קווים בלבד. בואו ננסה להוסיף ארוך למערך במקום מחרוזת:
text[1] = 4L;
הבה נריץ את הקוד הזה (לדוגמה, ב- Repl.it Online Java Compiler ) ונקבל שגיאה:
error: incompatible types: long cannot be converted to String
המערך ובטיחות הסוג של השפה לא אפשרו לנו לשמור למערך את מה שלא התאים לסוג. זהו ביטוי של בטיחות סוג. נאמר לנו: "תקן את השגיאה, אבל עד אז אני לא אקמפל את הקוד." והדבר הכי חשוב בעניין זה שזה קורה בזמן הקומפילציה, ולא בזמן השקת התוכנית. כלומר, אנו רואים שגיאות מיד, ולא "יום אחד". ומכיוון שזכרנו מערכים, בואו נזכור גם לגבי מסגרת Java Collections . היו לנו שם מבנים שונים. למשל, רשימות. נכתוב מחדש את הדוגמה:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
בעת הקומפילציה, נקבל testשגיאה בשורת האתחול המשתנה:
incompatible types: Object cannot be converted to String
במקרה שלנו, List יכולה לאחסן כל אובייקט (כלומר אובייקט מסוג Object). לכן, המהדר אומר שהוא לא יכול לקחת על עצמו עול כזה של אחריות. לכן, עלינו לציין במפורש את הסוג שנקבל מהרשימה:
String test = (String) text.get(0);
אינדיקציה זו נקראת המרת סוג או יציקת סוג. והכל יעבוד בסדר עכשיו עד שננסה להשיג את האלמנט באינדקס 1, כי זה מסוג ארוך. ונקבל שגיאה הוגנת, אבל כבר בזמן שהתוכנית פועלת (בזמן ריצה):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
כפי שאנו יכולים לראות, יש כאן כמה חסרונות חשובים. ראשית, אנו נאלצים "להטיל" את הערך המתקבל מהרשימה למחלקה String. מסכים, זה מכוער. שנית, במקרה של שגיאה, נראה אותה רק כאשר התוכנית מבוצעת. אם הקוד שלנו היה מורכב יותר, ייתכן שלא נזהה מיד שגיאה כזו. ומפתחים התחילו לחשוב איך להפוך את העבודה במצבים כאלה לקלה יותר ואת הקוד ברור יותר. והם נולדו - גנריות.
גנריות לחתולים - 2

גנריות

אז, גנריות. מה זה? גנרי הוא דרך מיוחדת לתאר את הסוגים שבהם נעשה שימוש, שבה מהדר הקוד יכול להשתמש בעבודתו כדי להבטיח בטיחות סוג. זה נראה בערך כך:
גנריות לחתולים - 3
הנה דוגמה קצרה והסבר:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
בדוגמה זו, אנו אומרים שיש לנו לא רק List, אלא List, אשר עובד רק עם אובייקטים מסוג String. ולא אחרים. מה שרק מצוין בסוגריים, אנחנו יכולים לאחסן אותו. "סוגריים" כאלה נקראים "סוגריים זווית", כלומר. סוגרי זווית. המהדר יבדוק עבורנו בטובו אם עשינו טעויות בעבודה עם רשימת מחרוזות (הרשימה נקראת טקסט). המהדר יראה שאנחנו מנסים בחוצפה להכניס את Long לרשימת המחרוזות. ובזמן הקומפילציה זה ייתן שגיאה:
error: no suitable method found for add(long)
אולי זכרת ש-String הוא צאצא של CharSequence. ולהחליט לעשות משהו כמו:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
אבל זה לא אפשרי ונקבל את השגיאה: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> זה נראה מוזר, כי. השורה CharSequence sec = "test";אינה מכילה שגיאות. בוא נבין את זה. הם אומרים על התנהגות זו: "גנריות הן בלתי משתנה." מהו "אינוריאנט"? אני אוהב את האופן שבו נאמר על זה בויקיפדיה במאמר " שיתופיות וניגודיות ":
גנריות לחתולים - 4
לפיכך, Invariance היא היעדר ירושה בין טיפוסים נגזרים. אם חתול הוא תת-סוג של בעלי חיים, אז הסט<Cats> אינו תת-סוג של ה-Set<Animals> והסט<Animals> אינו תת-סוג של הסט<Cats>. אגב, כדאי לומר שהחל מ-Java SE 7, הופיע מה שנקרא " מפעיל יהלומים ". כי שני סוגרי הזווית <> הם כמו יהלום. זה מאפשר לנו להשתמש בגנריות כמו זה:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
על סמך הקוד הזה, הקומפיילר מבין שאם ציינו בצד שמאל שהוא Listיכיל אובייקטים מסוג String, אז בצד ימין אנחנו מתכוונים שאנחנו רוצים linesלשמור ArrayList חדש למשתנה, שגם יאחסן אובייקט. מהסוג המצוין בצד שמאל. אז המהדר מהצד השמאלי מבין או מסיק את הסוג של הצד הימני. זו הסיבה שהתנהגות זו נקראת סוג מסקנות או "Type Inference" באנגלית. דבר מעניין נוסף שכדאי לשים לב אליו הוא סוגי RAW או "טיפוסים גולמיים". כי גנריות לא תמיד היו בסביבה, וג'אווה מנסה לשמור על תאימות לאחור במידת האפשר, ואז גנריות נאלצות איכשהו לעבוד עם קוד שבו לא צוין גנרי. בוא נראה דוגמה:
List<CharSequence> lines = new ArrayList<String>();
כפי שאנו זוכרים, שורה כזו לא תתחבר בגלל השונות של גנריות.
List<Object> lines = new ArrayList<String>();
וגם זה לא יקמפל, מאותה סיבה.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
שורות כאלה יקומפלו ויעבדו. בהם משתמשים ב- Raw Types, כלומר. סוגים לא מוגדרים. שוב, כדאי לציין שאסור להשתמש ב- Raw Types בקוד מודרני.
גנריות לחתולים - 5

שיעורים מוקלדים

אז, שיעורי הקלדה. בואו נראה איך נוכל לכתוב מחלקה מוקלדת משלנו. לדוגמה, יש לנו היררכיית מחלקות:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
אנחנו רוצים ליצור מחלקה המיישמת מיכל של בעלי חיים. אפשר יהיה לכתוב מחלקה שתכיל כל Animal. זה פשוט, מובן, אבל... ערבוב כלבים וחתולים זה רע, הם לא חברים אחד של השני. בנוסף, אם מישהו יקבל מיכל כזה, הוא עלול לזרוק בטעות חתולים מהמיכל לתוך להקת כלבים... וזה לא יוביל לשום טוב. וכאן גנריות יעזרו לנו. לדוגמה, בוא נכתוב את היישום כך:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
המחלקה שלנו תעבוד עם אובייקטים מסוג שצוין על ידי גנרי בשם T. זהו סוג של כינוי. כי הגנרי מצוין בשם המחלקה, ואז נקבל אותו כאשר נכריז על המחלקה:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
כפי שאנו יכולים לראות, ציינו שיש לנו Box, שעובד רק עם Cat. המהדר הבין שבמקום catBoxגנרי, Tאתה צריך להחליף את הסוג Catבכל מקום שבו מצוין שם הגנרי T:
גנריות לחתולים - 6
הָהֵן. זה הודות למהדר Box<Cat>שהוא מבין מה slotsזה צריך להיות בעצם List<Cat>. כי Box<Dog>בפנים יהיה slots, מכיל List<Dog>. יכולות להיות מספר גנריות בהצהרת סוג, למשל:
public static class Box<T, V> {
השם הגנרי יכול להיות כל דבר, אם כי מומלץ להקפיד על כמה כללים שלא נאמרו - "מוסכמות מתן שמות של פרמטרים": סוג אלמנט - E, סוג מפתח - K, סוג מספר - N, T - לסוג, V - לסוג ערך . אגב, זכור שאמרנו שגנריות הן בלתי משתנות, כלומר. אין לשמר את היררכיית הירושה. למעשה, אנחנו יכולים להשפיע על זה. כלומר, יש לנו הזדמנות להפוך את הגנריות ל-COvariant, כלומר. שמירה על ירושות באותו סדר. התנהגות זו נקראת "סוג מוגבל", כלומר. סוגים מוגבלים. לדוגמה, הכיתה שלנו Boxיכולה להכיל את כל החיות, ואז נכריז על גנרי כזה:
public static class Box<T extends Animal> {
כלומר, אנו מגדירים את הגבול העליון למחלקה Animal. אנו יכולים גם לציין מספר סוגים לאחר מילת המפתח extends. זה אומר שהטיפוס איתו נעבוד חייב להיות צאצא של מחלקה כלשהי ובמקביל ליישם ממשק כלשהו. לדוגמה:
public static class Box<T extends Animal & Comparable> {
במקרה זה, אם ננסה להכניס Boxמשהו לכזה שאינו יורש Animalואינו מיישם Comparable, אז במהלך ההידור נקבל שגיאה:
error: type argument Cat is not within bounds of type-variable T
גנריות לחתולים - 7

הקלדת שיטה

גנריות משמשות לא רק בסוגים, אלא גם בשיטות בודדות. ניתן לראות את יישום השיטות במדריך הרשמי: " שיטות גנריות ".

רקע כללי:

גנריות לחתולים - 8
בואו נסתכל על התמונה הזו. כפי שאתה יכול לראות, המהדר מסתכל על חתימת השיטה ורואה שאנו לוקחים מחלקה לא מוגדרת כקלט. זה לא קובע בחתימה שאנחנו מחזירים חפץ כלשהו, ​​כלומר. לְהִתְנַגֵד. לכן, אם אנחנו רוצים ליצור, נניח, ArrayList, אז אנחנו צריכים לעשות את זה:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
אתה צריך לכתוב במפורש שהפלט יהיה ArrayList, וזה מכוער ומוסיף סיכוי לטעות. לדוגמה, אנחנו יכולים לכתוב שטויות כאלה וזה ירכיב:
ArrayList object = (ArrayList) createObject(LinkedList.class);
אנחנו יכולים לעזור למהדר? כן, תרופות גנריות מאפשרות לנו לעשות זאת. בואו נסתכל על אותה דוגמה:
גנריות לחתולים - 9
לאחר מכן, נוכל ליצור אובייקט פשוט כך:
ArrayList<String> object = createObject(ArrayList.class);
גנריות לחתולים - 10

WildCard

על פי המדריך של אורקל בנושא גנריות, במיוחד בקטע " תווים כלליים ", אנו יכולים לתאר "סוג לא ידוע" עם סימן שאלה. Wildcard הוא כלי שימושי למתן חלק מהמגבלות של תרופות גנריות. לדוגמה, כפי שדיברנו קודם לכן, תרופות גנריות הן בלתי משתנות. המשמעות היא שלמרות שכל המחלקות הן צאצאים (תתי סוגים) של סוג האובייקט, זה List<любой тип>לא תת-סוג List<Object>. אבל, List<любой тип>זה תת-סוג List<?>. אז נוכל לכתוב את הקוד הבא:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
כמו גנריות רגילות (כלומר ללא שימוש בתווים כלליים), ניתן להגביל את הגנריות עם תווים כלליים. התו הכללי עם הגבול העליון נראה מוכר:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
אבל אתה יכול גם להגביל אותו על ידי התו הכללי של הגבול התחתון:
public static void printCatList(List<? super Cat> list) {
לפיכך, השיטה תתחיל לקבל את כל החתולים, כמו גם גבוה יותר בהיררכיה (עד Object).
גנריות לחתולים - 11

הקלד מחיקה

אם כבר מדברים על תרופות גנריות, כדאי לדעת על "מחיקת סוג". למעשה, מחיקת סוג עוסקת בעובדה שגנריות הן מידע עבור המהדר. במהלך הפעלת התוכנית, אין יותר מידע על גנריות, זה נקרא "מחיקה". למחיקה זו יש את ההשפעה שהטיפוס הגנרי מוחלף בסוג הספציפי. אם לגנרי לא היה גבול, סוג האובייקט יוחלף. אם הגבול צוין (לדוגמה <T extends Comparable>), הוא יוחלף. הנה דוגמה מהמדריך של אורקל: " מחיקה של סוגים גנריים ":
גנריות לחתולים - 12
כפי שנאמר לעיל, בדוגמה זו הגנרי Tנמחק לגבול שלו, כלומר. לפני Comparable.
גנריות לחתולים - 13

סיכום

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