קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ב': פונקציות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ג': מחלקות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ד': עוד על מחלקות, אובייקטים, ועוד…
קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ה': DSLs
קוטלין (Kotlin) למפתחי ג'אווה ותיקים – חלק ו': Collections ו Generics
הגדרת מחלקה פשוטה
בואו נתחיל עם המחלקה הפשוטה ביותר האפשרית בקטולין:
פשוט, לא?
האמת שזה דיי דומה למחלקה ריקה בג'אווה: פשוט אם אין body – אז אין צורך בסוגריים מסולסלים.
מה השימוש במחלקה ריקה?
לא הרבה.
אפשר לייצר ככה custom exception או Marker interface או Marker Annotation (בתחביר מקוצר הרבה מג'אווה).
טוב, עכשיו נתחיל יותר ברצינות:
- בקוטלין לא ניתן להגדיר שדות (fields) למחלקה. במקום זאת – מגדירים תכונות (properties). מה זה אומר?
- השורה שלנו תגרום לקומפיילר לייצר גם שדה, וגם accessor methods לשדה הזה.
- כאשר כותבים בקוטלין – הגישה לתכונה (property) תעשה בעזרת person.age – כאילו זהו field שהוא פשוט public.
- כאשר כותבים בג'אווה – הגישה לתכונה של מחלקה הכתובה בקוטלין תעשה בעזרת ()person.getAge ו ()person.setAge – כמקובל.
- מאחורי כל תכונה (property) באמת קיים שדה (field). לעתים – לא יהיה בשדה הזה בכלל שימוש. נראה דוגמה בהמשך.
- הסיבה מאחורי הוספת properties לשפה, היא כנראה Item 14 בספר "Effective Java" – המזהיר בפני חשיפת שדות שאז לא יהיה ניתן לשנות, לבקר, ולהגביל את הגישה אליהם. "אחרי שנחשפו – זהו!… התלות קיימת וזו יכולה להיות עבודה קשה להסיר אותה…"
- בקוטלין נתנו לנו את האפשרות להוסיף custom getter/setter בכל יום שנצטרך – אבל בלי הסרבול של לכתוב getters / setters בעצמנו, בכל פעם, כי "אולי בעתיד יהיה צורך".
להיות עם – ולהרגיש בלי.
- השורה שלנו תגרום לקומפיילר לייצר גם שדה, וגם accessor methods לשדה הזה.
- בקוטלין יש primary constructor, וייתכן שיהיו secondary constructors.
ה primary constructor מוגדר באותה שורה עם הגדרת המחלקה.- מגדירים אותו בעזרת המילה השמורה constructor.
- מגדירים אלו פרמטרים הוא יקבל.
- אם לא מגדירים primary constructor אזי נוצר default primary constructor – ללא פרמטרים.
- ה init block נקרא בכל יצירה של instance של המחלקה, והוא בעצם משמש כגוף הפונקציה של הבנאי הראשי (primary constructor) – אם צריך כזה.
- כל התכונות של המחלקה צריכות להיות מאותחלות. אם נסיר את השורה "age = 0" – נקבל שגיאת קומפילציה.
הקומפיילר דורש שאנו נאתחל את התכונות באותה השורה (מה שנקרא initializer, כמו ב name) – או שנאתחל אותם ב init block / בבנאי הראשי.
- כל התכונות של המחלקה צריכות להיות מאותחלות. אם נסיר את השורה "age = 0" – נקבל שגיאת קומפילציה.
- הנה הגדרה של בנאי משני למחלקה.
- ההגדרה שלו נעשית בעזרת המילה השמורה constructor.
- מגדירים את הפרמטרים של הבנאי המשני.
- חובה להפעיל את הבנאי הראשי:
- או ישירות – ע"י קריאה עם הפרמטרים המתאימים.
- או ע"י קריאה לבנאי משני אחר – שהוא יפעיל את הבנאי הראשי.
- כאשר קיים default primary constructor – אין צורך ב ()this :.
- הנה יצירת מופע של המחלקה בעזרת שימוש בבנאי הראשי.
- הנה יצירת מופע של המחלקה – בעזרת שימוש בבנאי המשני.
- הנה קריאה לתכונה – היא באמת נראית כמו קריאה לשדה בג'אווה – מה שהופך את הקוד למעט יותר "נקי בעין".
אתם בוודאי רואים שיש פה השפעה לא מעטה #C וגם קצת מסקאלה.
- הצלחנו לפשט את המחלקה דרמטית, בעזרת הגדרה "נבונה" יותר של הבנאי הראשי.
- המילה constructor בשורת המחלקה, לתיאור הבנאי הראשי – היא אופציונלית, ואפשר לוותר עליה.
- אם מגדירים val או var על הפרמטרים של הבנאי הראשי – הרי זה שקול להגדרת תכונות בגוף המחלקה. הארגומנטים שנשלחו לבנאי – יאתחלו את התכונות הללו. קצר ונוח.
יכולת זו שמורה לבנאי הראשי בלבד. - השימוש ב default value בחתימה של הבנאי הראשי – ייתרה את הצורך בבנאי משני.
- במקרים לא מעטים, השימוש ב default values – יכול לחסוך שימוש ב Builder Pattern, ולחסוך לא-מעט קוד.
- הנה ההרצה: אין שום שינוי, כי לא היה שום שינוי. הקוד שקול לחלוטין.
אמנם כך עדיף לכתוב את המחלקה, אך היה חשוב לי לעבור בדרך הארוכה – עבור הלמידה.
עוד קצת על תכונות (Properties)
בואו נחזור ונחדד עוד כמה דברים לגבי תכונות:
- הגדרנו למחלקה תכונה בשם age – ודרסנו את ה getter. ממש דומה לתחביר של #C.
- כעקרון, כאשר דורסים רק שלפן (accessor) אחד, במקרה שלנו: getter, השלפן השני – עדיין קיים במימוש ברירת המחדל שלו. כלומר: ל age עדיין יש default setter.
- ספציפית במקרה שלנו, מכיוון ש age הוגדר כ val (כלומר "immutable") – לא ייווצר setter.
- רק לצורך ההדגמה יצרתי תכונה מאוד דומה, אותה הגדרנו כ var. במקרה כזה, אנחנו מחויבים לאתחל את השדה של התכונה – גם אם לא יעשה בו שימוש לעולם (במקרה הזה: עם הערך אפס).
- כאשר ה getter שלי הוא פשוט – אני יכול להשתמש בתחביר shorthand, בעזרת הסימן =.
- יכולתי להשתמש בתחביר shorthand גם עבור age.
- אם אני לא רוצה שיגשו ל setter של התכונה – אני פשוט אגדיר אותה כ private.
התחביר הזה אומר לקומפיילר: צור default setter – אך עשה אותו private. - הנה – סתם בשביל ההדגמה: אני יכול להשתמש ב setter של התכונה בתוך המחלקה.
- אגדיר תכונה נוספת בשם nickname – אין לי בעיה לאתחל אותה בערך שנגזר מתכונה אחרת – name.
- כאן מימשתי setter לדוגמה. ה getter ה default-י עדיין קיים וזמין.
- מה קורה כאשר אנחנו רוצים, ב customer accessor שלנו לגשת לשדה שמאחורי התכונה?
- עושים את זה בעזרת המילה field.
- field היא מה שנקרא soft keyword, כלומר: keyword שקיים רק ב context מסוים. ממש כמו it – שראינו בפוסט הקודם.
- בניסיון ההרצה, שלושת השורות הללו יגרמו ל compilation error.
- את לשלושת התכונות הללו: age, name, ו ageNextYear – לא ניתן לקבוע ערך מחוץ למחלקה.
Visibility Modifiers
בקוטלין, visibility ברירת המחדל היא תמיד public. יש שוני קטן בהגדרות כאשר ה visibility modifier מוגדר:
- בתוך מחלקה.
- ב top level, כלומר: עבור הגדרה של מחלקות, משתנים, או פונקציות שאינן שייכות למחלקה.
הנה ההגדרות:
- private
- יתנהג כמו ג'אווה בתוך מחלקה
- יגביל גישה לאותו קובץ בלבד, אם השתמשו בו ב top-level.
- protected
- יתנהג כמו ג'אווה בתוך מחלקה
- לא ניתן להשתמש בו ב top-level.
- internal
- היא נראות חדשה לקוטלין, שמגדירה אפשרות לגשת לכל מי שנמצא באותו המודול.
- כיצד מוגדר מודול? ע"י תהליך הקומפליציה: קבצים שקומפלו ביחד.
זה יכול להיות מודול של Maven, של Gradle, או מודול ב IntelliJ – למשל.
- כיצד מוגדר מודול? ע"י תהליך הקומפליציה: קבצים שקומפלו ביחד.
- היא מתנהגת אותו הדבר בתוך המחלקה, וב top-level
- היא נראות חדשה לקוטלין, שמגדירה אפשרות לגשת לכל מי שנמצא באותו המודול.
בואו נראה קצת קוד:
- הגדרתי את המשתנה כ private – לא יוכלו לגשת אליו מקובץ אחר.
- הגדרתי מחלקה שהיא internal – זמינה רק בתוך ה module.
- כל המחלקות בקוטלין הן final – לא ניתן לרשת מהן, אלא אם מרשים זאת במפורש בעזרת השימוש ב open. אהבתי!
- מה עושים כאשר רוצים להגדיר "שדה פנימי" בקוטלין? רק לשימוש המחלקה?
– מגדירים תכונה שהיא private, ומשתמשים בה. - פה נראה שאולי עשיתי איזה טריק: הגדרתי getter ו setter על התוכנה – כך שאי אפשר לגשת ל accessors שלה. האם הפכתי אותה ל private?
- בפועל: אין תחביר כזה:
- הקומפיילר מתעלם מ get ומגדיר רק את ה default setter כ private.
- תראו למטה – אמנם הקוד מתקמפל, אבל אני בהחלט יכול לגשת ל someHiddenField ממחלקה אחרת. אופס!
- הנה דוגמה תקינה ל default setter שהוא private. אפשר כמובן גם להגדיר custom setter באותו האופן.
- ניסיתי להגדיר internal getter – אך קיבלתי שגיאת קומפילציה: ה getter של תכונות חייב להיות באותה נראות כמו התכונה עצמה. חשבו אילו בעיות יכלו להיות אם לא…
- באופן הבא אני יכול להגדיר נראות (private) על primary constructor.
כאשר אני משתמש ב visibility modifier על הבנאי – אני חייב להשתמש במילה constructor. - על השורה הזו אני מקבל warning בקומפליציה: ה class לא הוגדר כ open – אז לא ניתן לרשת ממנו. אין טעם או משמעות להגדיר נראות של protected. צודק הקומפיילר.
Data Class
אחד הבזבוזים הגדולים של boilerplate code בג'אווה הוא ביצירה של data classes – מחלקות שכל תפקידן להחזיק כמות מסוימת של נתונים.
אני תמיד הייתי משתמש ב public fields, אבל יש כאלו שהקפידו על getters ו setters, וגם hashCode ו equals וכו'… הרבה קוד – עבור משהו מאוד סטנדרטי.
בקוטלין אפשר להגדיר data class, מחלקה שמספקת לנו:
- תיאור כוונות ברור: המילה data מצביעה בבירור על הכוונה מאחורי המחלקה.
- מימוש ל ()equals(), hashCode(), clone, ומימוש default-י וסביר לחלוטין של ()toString.
בערך כל מה שמחלקה כזו צריכה.
- כך מגדירים data class, שהיא מחלקה לכל דבר. את התכונות של ה dataclass הגדרתי בתוך הבנאי הראשי – כמו שהראנו קודם. זה הכי נוח.
- מכיוון שזו מחלקה לכל דבר, אני יכול להגדיר לה member function. כמובן שבד"כ לא יהיו פונקציות ל data class.
- אמנם קיבלתי מימוש סטנדרטי של כמה מתודות, אבל אם צריך – אני יכול לדרוס אותן.
בקוטלין override היא לא optional annotation, אלא mandatory keyword. טוב מאוד! - הנה אני עושה השוואה בין אובייקטים של ה data class שלי. הם לא שווים, כי המימוש שמסופק ל equals – משווה את כל התכונות של המחלקה.
- לאחר שעשיתי השמה (שימוש ב ()clone) – האובייקטים שווים. זו אכן ההתנהגות הצפויה מ data class.
שני Data Classes שימושיים שמסופקים כחלק מהשפה הם Pair ו Triple, המאפשרים לנו להעביר זוגות או שלשות של פרמטרים.
אלו באמת רק Data Classes. למשל, הנה המימוש של Triple:
Enum Class
בדומה לג'אווה, לקוטלין יש enum… class. בואו נראה כיצד הוא נראה:
- הנה דוגמה ל enum פשוט. כל איבר הוא בעצם אובייקט – מופע של המחלקה.
- זהו אחד המקרים המעטים בהם צריך בקוטלין לכתוב יותר קוד מאשר בג'אווה: "enum class" במקום "enum" 😏
- מכיוון שהאיברים הם אובייקטים, אני יכול להרחיב אותם, למשל: לדרוס פונקציה של המחלקה (או להוסיף פונקציה חדשה). הפונקציות הללו שייכות לאובייקט, ולא למחלקה!
- אני יכול הוסיף גם פונקציה למחלקה, שתהיה זמינה לכל אחד מהאובייקטים ב enum.
- יש מקרה דיי נדיר בו עלי להשתמש בנקודה-פסיק בקוטלין:
- אם הוספתי ל enum class פונקציות, הקומפיילר יבקש עזרה לדעת היכן נגמרה רשימת האיברים, והיכן מתחילה המחלקה. ההפרדה מסומנת בעזרת נקודה-פסיק.
- שימו לב שב TriColor לא היינו צריכים להוסיף נקודה-פסיק (אבל יכולנו – זה תקין).
- מכיוון ש BLUE הוא אובייקט – כאן תופעל, באופן טבעי, הפונקציה ()toString.
- לכל enum class יש את הפונקציה ()valueOf, המחפשת איבר ע"פ השם שלו.
- ניתן להציץ בקוד המקור של ה Abstract Enum Class כאן.
- לכל אובייקט ב enum יש 2 תכונות מובנות:
- name – (ששווה לשם האיבר), בחוסר תלות מ ()toString.
- ordinal – שהוא האינדקס של האיבר.
- תכונה אחרונה חשובה של enum היא הפונקציה ()values – המחזירה מערך של האיברים. לצורך הפלט – המרתי את המערך לרשימה.
- אני יכול לשנות את המחלקה ולהוסיף לה תכונות דרך הבנאי הראשי.
הנה ל TwoColor הוספתי תכונה בשם value – שיש לכל אחד מהאיברים שלה.
סיכום
עשינו כברת דרך בעולם המחלקות של קוטלין, לי זה הרגיש פוסט אינטנסיבי – אבל עדיין לא דיברנו על ממשקים, הורשה, וכו'.
את זה נשמור לפוסט הבא.
שיהיה בהצלחה!
