2017-07-27

קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ג': מחלקות

פוסט זה הוא המשך של:
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק א': הבסיס
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ב': פונקציות


הגדרת מחלקה פשוטה


בואו נתחיל עם המחלקה הפשוטה ביותר האפשרית בקטולין:


פשוט, לא?
האמת שזה דיי דומה למחלקה ריקה בג'אווה: פשוט אם אין body - אז אין צורך בסוגריים מסולסלים.

מה השימוש במחלקה ריקה?


לא הרבה.
אפשר לייצר ככה custom exception או Marker interface או Marker Annotation (בתחביר מקוצר הרבה מג'אווה).


טוב, עכשיו נתחיל יותר ברצינות:


  1. בקוטלין לא ניתן להגדיר שדות (fields) למחלקה. במקום זאת - מגדירים תכונות (properties). מה זה אומר?
    1. השורה שלנו תגרום לקומפיילר לייצר גם שדה, וגם accessor methods לשדה הזה.
      1. כאשר כותבים  בקוטלין - הגישה לתכונה (property) תעשה בעזרת person.age - כאילו זהו field שהוא פשוט public.
      2. כאשר כותבים בג'אווה - הגישה לתכונה של מחלקה הכתובה בקוטלין תעשה בעזרת ()person.getAge  ו ()person.setAge - כמקובל.
    2. מאחורי כל תכונה (property) באמת קיים שדה (field). לעתים - לא יהיה בשדה הזה בכלל שימוש. נראה דוגמה בהמשך.
    3. הסיבה מאחורי הוספת properties לשפה, היא כנראה Item 14 בספר "Effective Java" - המזהיר בפני חשיפת שדות שאז לא יהיה ניתן לשנות, לבקר, ולהגביל את הגישה אליהם. "אחרי שנחשפו - זהו!... התלות קיימת וזו יכולה להיות עבודה קשה להסיר אותה..."
    4. בקוטלין נתנו לנו את האפשרות להוסיף custom getter/setter בכל יום שנצטרך - אבל בלי הסרבול של לכתוב getters / setters בעצמנו, בכל פעם, כי "אולי בעתיד יהיה צורך".
      להיות עם - ולהרגיש בלי.
  2. בקוטלין יש primary constructor, וייתכן שיהיו secondary constructors.
    ה primary constructor מוגדר באותה שורה עם הגדרת המחלקה.
    1. מגדירים אותו בעזרת המילה השמורה constructor.
    2. מגדירים אלו פרמטרים הוא יקבל.
    3. אם לא מגדירים primary constructor אזי נוצר default primary constructor - ללא פרמטרים.
  3. ה init block נקרא בכל יצירה של instance של המחלקה, והוא בעצם משמש כגוף הפונקציה של הבנאי הראשי (primary constructor) - אם צריך כזה.
    1. כל התכונות של המחלקה צריכות להיות מאותחלות. אם נסיר את השורה "age = 0" - נקבל שגיאת קומפילציה.
      הקומפיילר דורש שאנו נאתחל את התכונות באותה השורה (מה שנקרא initializer, כמו ב name) - או שנאתחל אותם ב init block / בבנאי הראשי.
  4. הנה הגדרה של בנאי משני למחלקה.
    1. ההגדרה שלו נעשית בעזרת המילה השמורה constructor.
    2. מגדירים את הפרמטרים של הבנאי המשני.
    3. חובה להפעיל את הבנאי הראשי: 
      1. או ישירות - ע"י קריאה עם הפרמטרים המתאימים.
      2. או ע"י קריאה לבנאי משני אחר - שהוא יפעיל את הבנאי הראשי.
      3. כאשר קיים default primary constructor - אין צורך ב ()this :.
  5. הנה יצירת מופע של המחלקה בעזרת שימוש בבנאי הראשי.
  6. הנה יצירת מופע של המחלקה - בעזרת שימוש בבנאי המשני.
  7. הנה קריאה לתכונה - היא באמת נראית כמו קריאה לשדה בג'אווה - מה שהופך את הקוד למעט יותר "נקי בעין".

יש לא מעט מידע בפסקה הזו - אולי תרצו לעבור עליה שוב.
אתם בוודאי רואים שיש פה השפעה לא מעטה #C וגם קצת מסקאלה.


האמת, שיש דרך פשוטה יותר לכתוב את המחלקה Person. הנה היא לפניכם:


  1. הצלחנו לפשט את המחלקה דרמטית, בעזרת הגדרה "נבונה" יותר של הבנאי הראשי.
    1. המילה constructor בשורת המחלקה, לתיאור הבנאי הראשי - היא אופציונלית, ואפשר לוותר עליה.
    2. אם מגדירים val או var על הפרמטרים של הבנאי הראשי - הרי זה שקול להגדרת תכונות בגוף המחלקה. הארגומנטים שנשלחו לבנאי - יאתחלו את התכונות הללו. קצר ונוח.
      יכולת זו שמורה לבנאי הראשי בלבד.
    3. השימוש ב default value בחתימה של הבנאי הראשי - ייתרה את הצורך בבנאי משני.
      1. במקרים לא מעטים, השימוש ב default values - יכול לחסוך שימוש ב Builder Pattern, ולחסוך לא-מעט קוד.
  2. הנה ההרצה: אין שום שינוי, כי לא היה שום שינוי. הקוד שקול לחלוטין.

אמנם כך עדיף לכתוב את המחלקה, אך היה חשוב לי לעבור בדרך הארוכה - עבור הלמידה.


עוד קצת על תכונות (Properties)


בואו נחזור ונחדד עוד כמה דברים לגבי תכונות:



  1. הגדרנו למחלקה תכונה בשם age - ודרסנו את ה getter. ממש דומה לתחביר של #C.
    1. כעקרון, כאשר דורסים רק שלפן (accessor) אחד, במקרה שלנו: getter, השלפן השני - עדיין קיים במימוש ברירת המחדל שלו. כלומר: ל age עדיין יש default setter.
    2. ספציפית במקרה שלנו, מכיוון ש age הוגדר כ val (כלומר "immutable") - לא ייווצר setter.
  2. רק לצורך ההדגמה יצרתי תכונה מאוד דומה, אותה הגדרנו כ var. במקרה כזה, אנחנו מחויבים לאתחל את השדה של התכונה - גם אם לא יעשה בו שימוש לעולם (במקרה הזה: עם הערך אפס).
  3. כאשר ה getter שלי הוא פשוט - אני יכול להשתמש בתחביר shorthand, בעזרת הסימן =.
    1. יכולתי להשתמש בתחביר shorthand גם עבור age.
  4. אם אני לא רוצה שיגשו ל setter של התכונה - אני פשוט אגדיר אותה כ private.
    התחביר הזה אומר לקומפיילר: צור default setter - אך עשה אותו private.
  5. הנה - סתם בשביל ההדגמה: אני יכול להשתמש ב setter של התכונה בתוך המחלקה.
  6. אגדיר תכונה נוספת בשם nickname - אין לי בעיה לאתחל אותה בערך שנגזר מתכונה אחרת - name.
  7. כאן מימשתי setter לדוגמה. ה getter ה default-י עדיין קיים וזמין.
  8. מה קורה כאשר אנחנו רוצים, ב customer accessor שלנו לגשת לשדה שמאחורי התכונה?
    1. עושים את זה בעזרת המילה field.
    2. field היא מה שנקרא soft keyword, כלומר: keyword שקיים רק ב context מסוים. ממש כמו it - שראינו בפוסט הקודם.
  9. בניסיון ההרצה, שלושת השורות הללו יגרמו ל compilation error.
    1. את לשלושת התכונות הללו: age, name, ו ageNextYear - לא ניתן לקבוע ערך מחוץ למחלקה.

הנה תוצאת ההרצה:




Visibility Modifiers


בקוטלין, visibility ברירת המחדל היא תמיד public. יש שוני קטן בהגדרות כאשר ה visibility modifier מוגדר:
  • בתוך מחלקה.
  • ב top level, כלומר: עבור הגדרה של מחלקות, משתנים, או פונקציות שאינן שייכות למחלקה.

הנה ההגדרות:
  • private 
    • יתנהג כמו ג'אווה בתוך מחלקה
    • יגביל גישה לאותו קובץ בלבד, אם השתמשו בו ב top-level.
  • protected
    • יתנהג כמו ג'אווה בתוך מחלקה
    • לא ניתן להשתמש בו ב top-level.
  • internal
    • היא נראות חדשה לקוטלין, שמגדירה אפשרות לגשת לכל מי שנמצא באותו המודול.
      • כיצד מוגדר מודול? ע"י תהליך הקומפליציה: קבצים שקומפלו ביחד.
        זה יכול להיות מודול של Maven, של Gradle, או מודול ב IntelliJ - למשל.
    • היא מתנהגת אותו הדבר בתוך המחלקה, וב top-level


בואו נראה קצת קוד:


  1. הגדרתי את המשתנה כ private - לא יוכלו לגשת אליו מקובץ אחר.
  2. הגדרתי מחלקה שהיא internal - זמינה רק בתוך ה module.
    1. כל המחלקות בקוטלין הן final - לא ניתן לרשת מהן, אלא אם מרשים זאת במפורש בעזרת השימוש ב open. אהבתי!
  3. מה עושים כאשר רוצים להגדיר "שדה פנימי" בקוטלין? רק לשימוש המחלקה?
    - מגדירים תכונה שהיא private, ומשתמשים בה.
  4. פה נראה שאולי עשיתי איזה טריק: הגדרתי getter ו setter על התוכנה - כך שאי אפשר לגשת ל accessors שלה. האם הפכתי אותה ל private?
    1. בפועל: אין תחביר כזה:
    2. הקומפיילר מתעלם מ get ומגדיר רק את ה default setter כ private.
    3. תראו למטה - אמנם הקוד מתקמפל, אבל אני בהחלט יכול לגשת ל someHiddenField ממחלקה אחרת. אופס!
  5. הנה דוגמה תקינה ל default setter שהוא private. אפשר כמובן גם להגדיר custom setter באותו האופן.
  6. ניסיתי להגדיר internal getter - אך קיבלתי שגיאת קומפילציה: ה getter של תכונות חייב להיות באותה נראות כמו התכונה עצמה. חשבו אילו בעיות יכלו להיות אם לא...
  7. באופן הבא אני יכול להגדיר נראות (private) על primary constructor.
    כאשר אני משתמש ב visibility modifier על הבנאי - אני חייב להשתמש במילה constructor.
  8. על השורה הזו אני מקבל warning בקומפליציה: ה class לא הוגדר כ open - אז לא ניתן לרשת ממנו. אין טעם או משמעות להגדיר נראות של protected. צודק הקומפיילר.

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



Data Class


אחד הבזבוזים הגדולים של boilerplate code בג'אווה הוא ביצירה של data classes - מחלקות שכל תפקידן להחזיק כמות מסוימת של נתונים.

אני תמיד הייתי משתמש ב public fields, אבל יש כאלו שהקפידו על getters ו setters, וגם hashCode ו equals וכו'... הרבה קוד - עבור משהו מאוד סטנדרטי.

בקוטלין אפשר להגדיר data class, מחלקה שמספקת לנו:
  • תיאור כוונות ברור: המילה data מצביעה בבירור על הכוונה מאחורי המחלקה.
  • מימוש ל ()equals(), hashCode(), clone, ומימוש default-י וסביר לחלוטין של ()toString.
    בערך כל מה שמחלקה כזו צריכה.


  1. כך מגדירים data class, שהיא מחלקה לכל דבר. את התכונות של ה dataclass הגדרתי בתוך הבנאי הראשי - כמו שהראנו קודם. זה הכי נוח.
  2. מכיוון שזו מחלקה לכל דבר, אני יכול להגדיר לה member function. כמובן שבד"כ לא יהיו פונקציות ל data class.
  3. אמנם קיבלתי מימוש סטנדרטי של כמה מתודות, אבל אם צריך - אני יכול לדרוס אותן.
    בקוטלין override היא לא optional annotation, אלא mandatory keyword. טוב מאוד!
  4. הנה אני עושה השוואה בין אובייקטים של ה data class שלי. הם לא שווים, כי המימוש שמסופק ל equals - משווה את כל התכונות של המחלקה.
  5. לאחר שעשיתי השמה (שימוש ב ()clone) - האובייקטים שווים. זו אכן ההתנהגות הצפויה מ data class.

שני Data Classes שימושיים שמסופקים כחלק מהשפה הם Pair ו Triple, המאפשרים לנו להעביר זוגות או שלשות של פרמטרים.


אלו באמת רק Data Classes. למשל, הנה המימוש של Triple:




Enum Class


בדומה לג'אווה, לקוטלין יש enum... class. בואו נראה כיצד הוא נראה:


  1. הנה דוגמה ל enum פשוט. כל איבר הוא בעצם אובייקט - מופע של המחלקה.
    1. זהו אחד המקרים המעטים בהם צריך בקוטלין לכתוב יותר קוד מאשר בג'אווה: "enum class" במקום "enum"  😏
    2. מכיוון שהאיברים הם אובייקטים, אני יכול להרחיב אותם, למשל: לדרוס פונקציה של המחלקה (או להוסיף פונקציה חדשה). הפונקציות הללו שייכות לאובייקט, ולא למחלקה!
  2. אני יכול הוסיף גם פונקציה למחלקה, שתהיה זמינה לכל אחד מהאובייקטים ב enum.
  3. יש מקרה דיי נדיר בו עלי להשתמש בנקודה-פסיק בקוטלין: 
    1. אם הוספתי ל enum class פונקציות, הקומפיילר יבקש עזרה לדעת היכן נגמרה רשימת האיברים, והיכן מתחילה המחלקה. ההפרדה מסומנת בעזרת נקודה-פסיק.
    2. שימו לב שב TriColor לא היינו צריכים להוסיף נקודה-פסיק (אבל יכולנו - זה תקין).
  4. מכיוון ש BLUE הוא אובייקט - כאן תופעל, באופן טבעי, הפונקציה ()toString.
  5. לכל enum class יש את הפונקציה ()valueOf, המחפשת איבר ע"פ השם שלו. 
    1. ניתן להציץ בקוד המקור של ה Abstract Enum Class כאן.
  6. לכל אובייקט ב enum יש 2 תכונות מובנות: 
    1. name - (ששווה לשם האיבר), בחוסר תלות מ ()toString.
    2. ordinal - שהוא האינדקס של האיבר.
  7. תכונה אחרונה חשובה של enum היא הפונקציה ()values - המחזירה מערך של האיברים. לצורך הפלט - המרתי את המערך לרשימה.
  8. אני יכול לשנות את המחלקה ולהוסיף לה תכונות דרך הבנאי הראשי.
    הנה ל TwoColor הוספתי תכונה בשם value - שיש לכל אחד מהאיברים שלה.
הנה הפלט:




סיכום


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


שיהיה בהצלחה!





4 תגובות:

  1. תגובה זו הוסרה על ידי המחבר.

    השבמחק
  2. היי, יש טעות קטנה ב comment של הדוגמא על data class, הבדיקה האם a==b הראשון צריך להיות false והשני צריך להיות true.
    :-)

    השבמחק
  3. אנונימי1/3/21 10:25

    שאלה לגבי הסקשן של Visibility Modifiers
    ״8. על השורה הזו אני מקבל warning בקומפליציה: ה class לא הוגדר כ open - אז לא ניתן לרשת ממנו. אין טעם או משמעות להגדיר נראות של protected. צודק הקומפיילר.״

    האם הקומפיילר לא מתלונן בגלל שאנחנו מגדירים inner class כprotected בזמן השouter class הוא private? לא הבנתי מה המשמעות בקונטקסט הזה לזה שאי אפשר לרשת ממנה.

    השבמחק
    תשובות
    1. המחלקה החיצונית לא הוגדרה כ private - רק הבנאי שלה הוא כזה. לכאורה מישהו יכול לייצר מופע ולהעביר אלי לשימוש.

      העניין ש protected לא רלוונטי הוא מכיוון שמחלקות בקוטלין כברירת מחדל הן final - לא ניתן לרשת מהן. אם לא ניתן לרשת מהמחלקה שלי (לא הגדרתי אותה כ open) - אין משמעות ל protected והוא בעצם יתפקד כ private בפועל.

      אני מקווה שהצלחתי להסביר.

      מחק