2018-02-17

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

פוסט זה הוא המשך של:
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק א': הבסיס
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ב': פונקציות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ג': מחלקות
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ד': עוד על מחלקות, אובייקטים, ועוד...
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ה': DSLs
קוטלין (Kotlin) למפתחי ג'אווה ותיקים - חלק ו': Collections ו Generics


הפעם אני רוצה לדבר על Interoperability בין קוטלין וג'אווה.

באופן כללי, ה interoperability בין השתיים הוא מצוין - וייתכן ותוכלו לעבוד זמן ארוך מבלי להיתקל בבעיות.

מתישהו... במקרי הקצה - זה יגיע.
משהו בג'אווה לא יאהב משהו בקוטלין (או אולי ההיפך - אבל זה פחות נפוץ).


מקור


כיצד null יכול לצוץ לו בקוטלין ללא הודעה מוקדמת?


כבר כמה פעמים נשאלתי את השאלה: "האם אפשר לרשת מחלקת ג'אווה בקוטלין? קוטלין בג'אווה?"

בוודאי שאפשר! אחרת לא הייתי אומר ש interoperability ביניהן כ"כ מוצלח.

מיד נראה שזה אכן המצב, ועל הדרך נדגיש פינה חשובה לגבי nullability (שעלולה לקרות בהורשה, אך לא רק):

ערבוב של קוד קוטלין וג'אווה לצורך רצף הקריאות. במציאות כמובן שהקוד ישב בקבצים נפרדים.
  1. יצרנו מחלקה מופשטת A בשפת ג'אווה.
  2. הרחבנו את המחלקה בג'אווה A - בעזרת מחלקה בקוטלין B.
    1. מכיוון שברירת המחדל בקוטלין היא מחלקה final - עלינו להגדיר אותה כ open ע"מ שקוד הג'אווה יוכל לרשת את המחלקה C.
  3. ואכן הרחבנו את המחלקה בקוטלין B בג'אווה, ללא בעיה. כל שפה שומרת על הקונבנציות שלה (במידת האפשר)
  4. הממ... ה IDE מעיר לי פה משהו: 
Not annotated method overrides method annotated with @NotNull 

מה זה?
אני לא רואה Annotation בשם NotNull@ בקוד.



מה? java.lang.NullPointerException? - אבל אני כותב בקוטלין!?



בכדי להבין מה קורה, נחזור שלב אחר אחורה - למחלקה KotlinB.

במחלקה הזו דרסנו את המתודה ()getHelloMessage שהוגדרה בג'אווה.
ערך ההחזרה של המתודה שהוגדרה בג'אווה הוא String, אבל מה זה אומר עבור קוטלין: String או ?String, אולי?



נעשה ניסוי קטן, ללא קשר להורשה. הנה מחלקה פשוטה בג'אווה המכילה מתודה המחזירה אובייקט (String):


והנה השימוש שלה בקוטלין:


ה IDE מסמן לי שערך ההחזרה של המתודה הזו הוא !String.

אין טיפוס כזה בקוטלין, וטעות נפוצה היא להניח ש !String הוא ההיפך מ ?String - כלומר: String שהוא בהכרח לא null.

מה שבאמת ה IDE מנסה לומר לנו הוא שהוא לא יודע אם ה String הוא null או לא. אני חושב שתחביר כמו [?]String היה יכול להיות יותר אינטואיטיבי.


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

other?.java?.object?.always?.might?.be?.null()


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

זה נוח, אבל גם יכול לגרום לשגיאות בלתי צפויות.


הנה דוגמה מהחיים:


jdbi הוא פריימוק רזה (וחביב) הכתוב בג'אווה, ומאפשר גישה לבסיס הנתונים.
אופן השימוש בו הוא להגדיר interface או abstract class עם מתודות ומוסיף להן annotation עם השאילתה שיש לממש.
jdbi, בעזרת reflection, מג'נרט (בזמן ריצה) אובייקט DAO שמממש את הממשק שהגדרתי. התוצאה היא אובייקט שמבצע את השאילות שהגדרתי ב annotations. קוד עובד.

בדוגמאת הקוד למעלה ה abstract class כתוב בקוטלין. יש הגדרה של פונקציה שתבצע שאילתת SELECT, וערך ההחזרה הוא מסוג String. הכל מתקמפל ועובד.

...עד הרגע שאני מפעיל את השאילתה עם job_id שלא קיים - וחוזר לי null.
מתישהו אני "חוטף" NullPointerException.

"מאיפה הגיע לפה null? הגדרתי במפורש שערך ההחזרה של הפונקציה הוא מסוג String - לא ?String.
"וואו, נראה לי שמצאתי באג בקוטלין" הוא שלב הכחשה אנושי...

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

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



עלי לשים לב שמדובר בשאילתת SELECT, ויותר מכך: עם תנאי WHERE - ולכן יכול לחזור null.
עליו להנחות את הקופיילר בהתאם - ואז הכל יתנהג כצפוי.


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

JebBrains (החברה מאוחרי קוטלין ו IntelliJ) סיפקה annotations לג'אווה שיכולים להנחות את ה IDE, מתי צפוי null ומתי לא ייתכן null. הנה דוגמה:



השימוש ב annotation מסיר מה IDE את הספק:


ואז הוא יכול להגן עלי.

אין כנראה פתרון טוב יותר: ואישהו בתפר בין ג'אווה וקוטלין עלולים "לזלוג" nulls מג'אווה לקוטלין.
סימון ה Nullability בעזרת annotations הוא לא תמיד אפשרי (למשל: ספריית צד-שלישי) וגם אותו אפשר לשכוח.


באופן דומה, אגב:

(Mutable)List<String>

הוא סימן שה IDE מספק שמשמעו: רשימה שייתכן שהיא mutable, וייתכן immutable. הקומפיילר לא מסוגל להגיע למסקנה בעצמו.

הנה דוגמה לביטוי מורכב:

  • הרשימה ו/או האיברים בה עלולים להכיל ערך null.
  • הרשימה עשויה להיות mutable או לא.

קוד הג'אווה מאחורי המתודה ()getStrings הוא זה:


מה שמוביל אותנו לעניין נוסף שכדאי להכיר:
כאשר מתודה בג'אווה נקראת ב naming של JavaBeans, כלומר: ()getXxxx או ()setXxxx - קוטלין מתייחסת אליהם כתכונה בשם xxxx.

הנה הקוד בקוטלין שקורא לקוד הג'אווה הנ"ל:


אתם רואים שהשלפנים (getters) שכתובים בג'אווה נראים בקוטלין כמו תכונות לכל דבר.
מכיוון ש true היא מילה שמורה בקוטלין, יש לעטוף (escaping) אותה בגרש מוטה.

באופן סימטרי, תכונות (properties) שהוגדרו בקוטלין כ yyyy יופיעו בקוד הג'אווה כמתודות ()getYyyy ו/או ()setYyyy.


כדרך אגב, יכולת ה escaping של שמות בקוטלין - מאפשר לתת שמות קריאים יותר לפונקציות של בדיקות:


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




חשיפה מתוכננת


העיקרון המנחה ב interoperability בין קוטלין וג'אווה הוא שכל שפה תדבוק בקונבנציות שלה.

אם יש לי תכונה בשם yyyy בקוטלין (מה שטבעי בקוטלין), הגישה אליה תהיה בעזרת getYyyy ו setYyyy - מה שטבעי בג'אווה.

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


אאוץ. אאוץ!!

הנה רשימת בעיות:
  • כאשר אני קורא לתכונה now מג'אווה - שם הפונקציה מופיע כ ()getNow, ומסיבה כזו או אחרת אני רוצה להשתמש בשם now כ field.
  • המילה transient היא מילה שמורה בג'אווה - אך לא בקוטלין. אי אפשר לקרוא לפונקציה הזו מתוך ג'אווה, ואין escaping בג'אווה המאפשר להשתמש בשמות שאינם תקינים בשפה.
  • אני לא יכול ליהנות מהערך ברירת המחדל של המתודה repeat. אין קונספט של default value בג'אווה - ולכן אני נדרש לשלוח את שני הפרמטרים בכל קריאה. בריבוי קריאות - זה יכול להיות מעצבן!
  • יצרתי companion object על מנת "לחקות" מתודות סטטיות בג'אווה - אבל הדרך לקרוא ל foo היא באופן: ()KotlinProducer.Companion.foo. מסורבל!


מה עושים?

הנה הפתרון, מבוסס annotations - אך עובד:


JvmOverloads היא הנחיה להשתמש ב default values על מנת לג'נרט מופעים שונים של פונקציות בקומבינציות השונות, מה שנקרא בג'אווה Method Overloading. אני מניח ששאר ה annotations הן self-explanatory.

הוספתי גם דוגמה לשימוש ב extension function. איך משתמשים ב extension functions מתוך ג'אווה?!


הנה קוד הג'אווה שמשתמש בקוד הקוטלין, "בהנאה":


דיי אלגנטי, מלבד ה Extension Function שבאמת לא משתווה לשימוש בקוטלין.
יש לציין את שם המחלקה (כל קובץ של קוטלין מתתרגם למחלקה עם Kt בסופה) נקודה שם המתודה - כאשר הפרמטר הראשון הוא האובייקט עליו אנו רוצים לפעול.

לא מקסים, אבל בג'אווה יש מבנים של הספריות הסטנדרטיות שהם לא פחות "כבדים".


סיכום


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

כאשר תגיעו למקרה קיצון הקשורים למחלקות מקוננות או ל reflection של הורשות מסובכות - חפשו בגוגל מה לעשות. חבל ללמוד דברים שרוב הסיכויים שלעולם לא תשתמשו בהם.

ה interoperability בין ג'אווה וקוטלין פשוט עובד!

יצא לראות לא מעט קוד קוטלין (צד-שרת) שעובד:
  • בצורה אינטנסיבית עם ספריות של ג'אווה.
  • ספריות ותיקות, שנכתבו לג'אווה - עוד לפני שקוטלין הייתה מעניינת.
  • ספריות שמבצעות reflection והורשה לקוד הקוטלין שנכתב (למשל: JDBI, Guice, או Jackson שמקודד עשרות רבות של מחלקות ל json ובחזרה לקוטלין)
  • והעבודה הייתה בסה"כ חלקה מאוד!
    • במקרים מעטים היה צורך / או היה יפה יותר להשתמש בכלים שסיפקתי בפוסט הזה.
    • במקרים מעטים נאלצנו לכתוב קוד "java-like" בקוטלין, על מנת שדברים יעבדו. עם הזמן צצו wrappers לקוטלין שהקלו על הדברים, ואפשרו להשתמש בסגנון "קוטליני" בחופשיות.



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



3 תגובות:

  1. אנונימי18/2/18 01:03

    אתה צרפתי? וגם משתמש בwindows!
    :)

    השבמחק
  2. לקח לי כמה שניות ארוכות להבין את הקטע של ״הצרפתי״ :-)

    Motif הוא שם של Theme (לדייק: Look & Feel) של Java Swing - לעתיקים בינינו שחוו מה זה...
    http://www.java2s.com/Tutorial/Java/0240__Swing/ChangingtheLookandFeeltoMotifLookAndFeel.htm

    את צילום המסך מצאתי בגוגל - אבל באמת מתוך מחשב שמריץ ווינדווס.

    השבמחק