(הווצא משימוש) המרה ל-Kotlin

1. שלום,

בקודלאב הזה תלמדו איך להמיר את הקוד מ-Java ל-Kotlin. בנוסף, נסביר מהן המוסכמות של שפת Kotlin ואיך לוודא שהקוד שאתם כותבים עומד בהן.

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

מה תלמדו

תלמדו איך להמיר קוד Java ל-Kotlin. במהלך העבודה תלמדו את התכונות והמושגים הבאים בשפת Kotlin:

  • טיפול באפשרות של ערך ריק
  • הטמעת אובייקטים ייחודיים
  • סיווגים של נתונים
  • טיפול במחרוזות
  • אופרטור Elvis
  • ניתוק המבנה
  • נכסים ונכסי תמיכה
  • ארגומנטים שמוגדרים כברירת מחדל ופרמטרים עם שם
  • עבודה עם קולקציות
  • פונקציות של תוספים
  • פונקציות ופרמטרים ברמה העליונה
  • מילות המפתח let,‏ apply,‏ with ו-run

הנחות

צריך להיות לכם כבר ניסיון ב-Java.

מה נדרש

2. תהליך ההגדרה

יצירת פרויקט חדש

אם אתם משתמשים ב-IntelliJ IDEA, יוצרים פרויקט Java חדש עם Kotlin/JVM.

אם אתם משתמשים ב-Android Studio, יוצרים פרויקט חדש באמצעות התבנית No Activity. בוחרים ב-Kotlin כשפת הפרויקט. הערך של SDK המינימלי יכול להיות כל ערך, הוא לא ישפיע על התוצאה.

הקוד

נוצר אובייקט מודל מסוג User וכיתה מסוג singleton‏ Repository שפועלת עם אובייקטים מסוג User ומציגה רשימות של משתמשים ושמות משתמשים בפורמט.

יוצרים קובץ חדש בשם User.java בקטע app/java/<yourpackagename> ומדביקים את הקוד הבא:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

תראו שה-IDE מודיע שהמשתנה @Nullable לא מוגדר. לכן, אם אתם משתמשים ב-Android Studio, עליכם לייבא את androidx.annotation.Nullable, ואם אתם משתמשים ב-IntelliJ, עליכם לייבא את org.jetbrains.annotations.Nullable.

יוצרים קובץ חדש בשם Repository.java ומדביקים את הקוד הבא:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

3. הצהרת תכונות nullability, ‏ val, ‏ var ו-data class

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

עוברים לקובץ User.java וממירים אותו ל-Kotlin: סרגל התפריטים -> קוד -> המרת קובץ Java לקובץ Kotlin.

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

e6f96eace5dabe5f.png

הקוד ב-Kotlin אמור להיראות כך:

class User(var firstName: String?, var lastName: String?)

הערה: השם של User.java השתנה ל-User.kt. קובצי Kotlin כוללים את הסיומת ‎ .kt.

בכיתה User ב-Java היו לנו שני מאפיינים: firstName ו-lastName. לכל אחד מהם הייתה שיטה לקבלת ערך (getter) ולקביעת ערך (setter), כך שהערך שלו היה ניתן לשינוי. מילת המפתח של Kotlin למשתנים שניתן לשינוי היא var, ולכן הממיר משתמש ב-var לכל אחד מהנכסים האלה. אם לנכסי ה-Java שלנו היו רק פונקציות getter, הם היו זמינים לקריאה בלבד והיו מוצהרים כמשתני val. val דומה למילת המפתח final ב-Java.

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

מכיוון שסימנו את firstName ואת lastName כאפשרויות nullable, הממיר האוטומטי סימן את המאפיינים כאפשרויות nullable באמצעות String?. אם תוסיפו הערות למשתני Java כערכים שאינם null (באמצעות org.jetbrains.annotations.NotNull או androidx.annotation.NonNull), הממיר יזהה זאת ויגדיר את השדות כערכים שאינם null גם ב-Kotlin.

ההמרה הבסיסית כבר בוצעה. אבל אפשר לכתוב את זה בצורה רגילה יותר. בואו נראה איך עושים את זה.

סיווג נתונים

הכיתה User שלנו מכילה רק נתונים. ב-Kotlin יש מילת מפתח לכיתות עם התפקיד הזה: data. סימון הכיתה הזו ככיתה מסוג data יגרום למהדר ליצור עבורנו באופן אוטומטי פונקציות getter ו-setter. הוא גם יניב את הפונקציות equals(),‏ hashCode() ו-toString().

נוסיף את מילת המפתח data לכיתה User:

data class User(var firstName: String?, var lastName: String?)

ב-Kotlin, כמו ב-Java, יכול להיות קונסטרוקטור ראשי וקונסטרוקטור משני אחד או יותר. המבנה בדוגמה שלמעלה הוא המבנה הראשי של הכיתה User. אם ממירים כיתה ב-Java שיש לה כמה קונסטרוקטורים, הממיר ייצור באופן אוטומטי כמה קונסטרוקטורים גם ב-Kotlin. הם מוגדרים באמצעות מילת המפתח constructor.

אם רוצים ליצור מופע של הכיתה הזו, אפשר לעשות זאת כך:

val user1 = User("Jane", "Doe")

שוויון

ב-Kotlin יש שני סוגים של שוויון:

  • כדי לבדוק אם שני מופעים זהים, משתמשים באופרטור == ובקריאה ל-equals().
  • בשוויון בין הפניות נעשה שימוש באופרטור ===, ובודקים אם שתי הפניות מפנות לאותו אובייקט.

המאפיינים שמוגדרים ב-constructor הראשי של סוג הנתונים ישמשו לבדיקות של שוויון מבני.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

4. ארגומנטים שמוגדרים כברירת מחדל, ארגומנטים עם שם

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

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

ב-Kotlin אפשר לתייג את הארגומנטים כשקוראים לפונקציות:

val john = User(firstName = "John", lastName = "Doe") 

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

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

ערכי ברירת מחדל הם מושג חשוב שנעשה בו שימוש לעיתים קרובות בקוד Kotlin. בקודלאב שלנו אנחנו רוצים תמיד לציין את השם הפרטי ואת שם המשפחה בהצהרה על אובייקט User, כך שלא נצטרך ערכים שמוגדרים כברירת מחדל.

5. התחלת אובייקטים, אובייקט נלווה ואובייקטים חד-משמעיים

לפני שממשיכים בקודלאב, צריך לוודא שהקלאס User הוא קלאס data. עכשיו נמיר את הכיתה Repository ל-Kotlin. תוצאת ההמרה האוטומטית אמורה להיראות כך:

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

בואו נראה מה הממיר האוטומטי עשה:

  • הרשימה של users היא nullable כי האובייקט לא נוצר בזמן ההצהרה
  • פונקציות ב-Kotlin כמו getUsers() מוצהרות באמצעות המאפיין fun
  • השיטה getFormattedUserNames() היא עכשיו נכס שנקרא formattedUserNames
  • לחזרה על רשימת המשתמשים (שהייתה בהתחלה חלק מ-getFormattedUserNames() יש תחביר שונה מזה של Java
  • השדה static הוא עכשיו חלק מבלוק companion object
  • נוסף בלוק init

לפני שנמשיך, ננסה לנקות קצת את הקוד. אם נסתכל ב-constructor, נראה שהממיר הפך את הרשימה users לרשימה שניתן לשינוי שמכילה אובייקטים שאפשר להקצות להם ערך null. אמנם הרשימה יכולה להיות null, אבל נניח שהיא לא יכולה להכיל משתמשים null. אז בואו נבצע את הפעולות הבאות:

  • מסירים את ? ב-User? בהצהרת הסוג users
  • מסירים את ? ב-User? עבור סוג ההחזרה של getUsers() כך שיוחזר List<User>?

בלוק init

ב-Kotlin, ה-constructor הראשי לא יכול להכיל קוד, ולכן קוד ההפעלה ממוקם בבלוק init. הפונקציונליות זהה.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

חלק גדול מהקוד של init מטפל בהפעלת מאפיינים. אפשר לעשות זאת גם בהצהרה על המאפיין. לדוגמה, בגרסה של Kotlin לכיתה Repository, אפשר לראות שמאפיין המשתמשים הותחל בזמן ההצהרה.

private var users: MutableList<User>? = null

המאפיינים והשיטות של static ב-Kotlin

ב-Java, אנחנו משתמשים במילות המפתח static לשדות או לפונקציות כדי לציין שהם שייכים לכיתה, אבל לא למופעים של הכיתה. לכן יצרנו את השדה הסטטי INSTANCE בכיתה Repository. המקבילה לכך ב-Kotlin היא הבלוק companion object. כאן גם מגדירים את השדות הסטטיים והפונקציות הסטטיות. הממיר יצר את הבלוק של אובייקט התווית הנלווית והעביר את השדה INSTANCE לכאן.

טיפול באובייקטים ייחודיים (singletons)

מכיוון שאנחנו צריכים רק מופע אחד של המחלקה Repository, השתמשנו בתבנית הסינגלון ב-Java. ב-Kotlin, אפשר לאכוף את התבנית הזו ברמת המהדר באמצעות החלפת מילת המפתח class ב-object.

מסירים את ה-constructor הפרטי ומחליפים את הגדרת הכיתה ב-object Repository. צריך להסיר גם את האובייקט הנלווה.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

כשמשתמשים בכיתה object, פשוט קוראים לפונקציות ולמאפיינים ישירות על האובייקט, כך:

val formattedUserNames = Repository.formattedUserNames

הערה: אם לא מוגדר בנכס משתנה של רמת חשיפה, הוא ציבורי כברירת מחדל, כמו במקרה של הנכס formattedUserNames באובייקט Repository.

6. טיפול באפשרות של ערך ריק

במהלך ההמרה של הכיתה Repository ל-Kotlin, הממיר האוטומטי הפך את רשימת המשתמשים ל-nullable, כי היא לא הוגדרה כאובייקט כשהצהירו עליה. כתוצאה מכך, בכל השימושים באובייקט users, צריך להשתמש באופרטור !! של טענת הנכוֹנוּת (assertion) של 'לא null'. (האותיות users!! ו-user!! יופיעו בקוד המומר). האופרטור !! ממיר כל משתנה לסוג שאינו null, כך שתוכלו לגשת לנכסים או לבצע קריאה לפונקציות באמצעותו. עם זאת, תופיע חריגה אם הערך של המשתנה אכן יהיה null. השימוש ב-!! עלול לגרום להשלכת חריגות בזמן הריצה.

במקום זאת, מומלץ לטפל באפשרות של ערך null באחת מהשיטות הבאות:

  • ביצוע בדיקת null ( if (users != null) {...} )
  • שימוש באופרטור elvis ?: (הסבר על האופרטור מופיע בהמשך ה-Codelab)
  • שימוש בחלק מהפונקציות הסטנדרטיות של Kotlin (הנושא הזה ייבדק בהמשך הסדנה)

במקרה שלנו, אנחנו יודעים שלרשימת המשתמשים אין צורך להיות nullable, כי היא מאותחלת מיד אחרי יצירת האובייקט (בבלוק init). כך אפשר ליצור ישירות מופע של האובייקט users כשמגדירים אותו.

כשאתם יוצרים מופעים של סוגי אוספים, Kotlin מספקת כמה פונקציות עזר שיעזרו לכם לכתוב קוד קריא וגמיש יותר. כאן אנחנו משתמשים ב-MutableList עבור users:

private var users: MutableList<User>? = null

כדי לפשט את העניין, אפשר להשתמש בפונקציה mutableListOf() ולציין את סוג רכיב הרשימה. mutableListOf<User>() יוצר רשימה ריקה שיכולה להכיל אובייקטים מסוג User. מאחר שהמידע על סוג הנתונים של המשתנה יכול להיכלל עכשיו על ידי המהדר, מסירים את הצהרת הסוג המפורשת של המאפיין users.

private val users = mutableListOf<User>()

שינינו גם את var ל-val כי המשתמשים יכללו הפניה לקריאה בלבד לרשימת המשתמשים. חשוב לזכור שההפניה היא לקריאה בלבד, ולכן היא אף פעם לא יכולה להצביע על רשימה חדשה, אבל הרשימה עצמה עדיין ניתנת לשינוי (אפשר להוסיף או להסיר אלמנטים).

מאחר שמשתנה users כבר מופעל, מסירים את ההפעלה הזו מהבלוק init:

users = ArrayList<Any?>()

לאחר מכן, הבלוק init אמור להיראות כך:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

בעקבות השינויים האלה, המאפיין users הוא עכשיו לא null, ואנחנו יכולים להסיר את כל המופעים הלא נחוצים של אופרטור !!. שימו לב שעדיין יופיעו שגיאות הידור ב-Android Studio, אבל עליכם להמשיך בשלבים הבאים של הקוד למעבדות כדי לפתור אותן.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

כמו כן, לגבי הערך userNames, אם מציינים את הסוג של ArrayList כמכיל את Strings, אפשר להסיר את הסוג המפורש בהצהרה כי הוא יוסק.

val userNames = ArrayList<String>(users.size)

פירוק מבנה

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

לדוגמה, כיתות data תומכות בפירוק מבנה, כך שאפשר לפרק את האובייקט User בלולאה for ל-(firstName, lastName). כך אנחנו יכולים לעבוד ישירות עם הערכים firstName ו-lastName. מעדכנים את הלולאה for כפי שמוצג בהמשך. מחליפים את כל המופעים של user.firstName ב-firstName ומחליפים את user.lastName ב-lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if expression

השמות ברשימת userNames עדיין לא בפורמט הרצוי. מכיוון שגם lastName וגם firstName יכולים להיות null, אנחנו צריכים לטפל באפשרות של ערך null כשאנחנו יוצרים את הרשימה של שמות המשתמשים בפורמט. אנחנו רוצים להציג את הערך "Unknown" אם אחד מהשמות חסר. מאחר שמשתנה name לא ישתנה אחרי שיוגדר פעם אחת, אפשר להשתמש ב-val במקום ב-var. קודם צריך לבצע את השינוי הזה.

val name: String

בודקים את הקוד שמגדיר את משתנה השם. יכול להיות שזה נראה לכם חדש: משתנה שמוגדר להיות שווה לבלוק קוד של if או else. מותר לעשות זאת כי ב-Kotlin, if ו-when הם ביטויים – הם מחזירים ערך. השורה האחרונה של משפט if תוקצה ל-name. המטרה היחידה של הבלוק הזה היא לאתחל את הערך של name.

בעיקרון, הלוגיקה הזו שמוצגת כאן היא שאם הערך של lastName הוא null, הערך של name מוגדר ל-firstName או ל-"Unknown".

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

אופרטור Elvis

אפשר לכתוב את הקוד הזה בצורה רגילה יותר באמצעות אופרטור ה-elvis ?:. אופרטור ה-elvis יחזיר את הביטוי בצד ימין אם הוא לא null, או את הביטוי בצד ימין אם הצד ימין הוא null.

לכן בקוד הבא, הערך firstName מוחזר אם הוא לא null. אם הערך של firstName הוא null, הביטוי מחזיר את הערך שמשמאל , "Unknown":

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

7. תבניות מחרוזות

ב-Kotlin קל לעבוד עם Strings באמצעות תבניות מחרוזות. תבניות מחרוזות מאפשרות להפנות למשתנים בתוך הצהרות על מחרוזות באמצעות הסמל $ לפני המשתנה. אפשר גם להוסיף ביטוי להצהרה על מחרוזת. לשם כך, מניחים את הביטוי בתוך { } ומשתמשים בסמל $ לפניו. דוגמה: ${user.firstName}.

הקוד שלך משתמש כרגע בשרשור מחרוזות כדי לשלב את firstName ו-lastName בשם המשתמש.

if (firstName != null) {
    firstName + " " + lastName
}

במקום זאת, מחליפים את שרשור המחרוזות ב:

if (firstName != null) {
    "$firstName $lastName"
}

שימוש בתבניות מחרוזות יכול לפשט את הקוד.

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

בשלב הזה, אמורה להופיע אזהרה על כך שאפשר לצרף את ההצהרה name למטלה. ניישם את זה. מכיוון שאפשר להסיק את הסוג של המשתנה name, אפשר להסיר את ההצהרה המפורשת על סוג String. עכשיו formattedUserNames נראה כך:

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

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

val formattedUserNames: List<String>

8. פעולות על קולקציות

נבחן מקרוב את ה-getter של formattedUserNames ונראה איך אפשר לשפר אותו. בשלב הזה, הקוד מבצע את הפעולות הבאות:

  • יצירת רשימה חדשה של מחרוזות
  • מעיינים ברשימת המשתמשים
  • יצירת השם המעוצב של כל משתמש על סמך השם הפרטי ושם המשפחה שלו
  • הפונקציה מחזירה את הרשימה החדשה שנוצרה
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

ב-Kotlin יש רשימה נרחבת של טרנספורמציות של אוספים שמרחיבות את היכולות של Java Collections API ומאפשרות לפתח מהר יותר ובצורה בטוחה יותר. אחת מהן היא הפונקציה map. הפונקציה הזו מחזירה רשימה חדשה שמכילה את התוצאות של החלת פונקציית הטרנספורמציה שצוינה על כל רכיב ברשימה המקורית. לכן, במקום ליצור רשימה חדשה ולעבור על רשימת המשתמשים באופן ידני, אפשר להשתמש בפונקציה map ולהעביר את הלוגיקה שהיתה בלולאה for לתוך גוף הפונקציה map. כברירת מחדל, השם של פריט הרשימה הנוכחי שמשמש ב-map הוא it, אבל כדי לשפר את הקריאוּת, אפשר להחליף את it בשם משתנה משלכם. במקרה שלנו, נקרא לו user:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

שימו לב שאנחנו משתמשים באופרטור Elvis כדי להחזיר את הערך "Unknown" אם הערך של user.lastName הוא null, כי user.lastName הוא מסוג String? ונדרש String ל-name.

...
else {
    user.lastName ?: "Unknown"
}
...

כדי לפשט את העניין עוד יותר, אפשר להסיר את המשתנה name לגמרי:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

9. נכסים ונכסי תמיכה

ראינו שהממיר האוטומטי החליף את הפונקציה getFormattedUserNames() במאפיין שנקרא formattedUserNames עם פונקציית getter בהתאמה אישית. מתחת לפני השטח, Kotlin עדיין יוצרת שיטה getFormattedUserNames() שמחזירה List.

ב-Java, אנחנו חושפים את מאפייני הכיתה באמצעות פונקציות getter ו-setter. ב-Kotlin אפשר להבחין טוב יותר בין מאפיינים של כיתה, שמתבטאים בשדות, לבין פונקציונליות, פעולות שכייתה יכולה לבצע, שמתבטאות בפונקציות. במקרה שלנו, הכיתה Repository פשוטה מאוד ולא מבצעת פעולות, ולכן יש בה רק שדות.

הלוגיקה שהופעל בפונקציה getFormattedUserNames() של Java מופעלת עכשיו כשקוראים ל-getter של מאפיין formattedUserNames ב-Kotlin.

אין לנו שדה מפורש שתואם למאפיין formattedUserNames, אבל Kotlin מספקת לנו שדה תמיכה אוטומטי בשם field, שאנחנו יכולים לגשת אליו במקרה הצורך באמצעות פונקציות getter ו-setter בהתאמה אישית.

עם זאת, לפעמים אנחנו רוצים פונקציונליות נוספת שלא קיימת בשדה הנגזר האוטומטי.

נסביר את זה בעזרת דוגמה.

בכיתה Repository יש לנו רשימה של משתמשים שניתנת לשינוי, שחשופה בפונקציה getUsers() שנוצרה מקוד ה-Java שלנו:

fun getUsers(): List<User>? {
    return users
}

לא רצינו שמבצעי הקריאה של הכיתה Repository ישנו את רשימת המשתמשים, לכן יצרנו את הפונקציה getUsers() שמחזירה List<User> לקריאה בלבד. במקרים כאלה, אנחנו מעדיפים להשתמש בנכסים במקום בפונקציות ב-Kotlin. באופן מדויק יותר, נחשוף List<User> לקריאה בלבד שמגובת על ידי mutableListOf<User>.

קודם כול, נשנה את השם של users ל-_users. מדגישים את שם המשתנה ולוחצים לחיצה ימנית כדי לשנות את הקוד > לשנות שם של המשתנה. לאחר מכן מוסיפים נכס ציבורי לקריאה בלבד שמחזיר רשימה של משתמשים. ניקרא לו users:

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

בשלב הזה אפשר למחוק את השיטה getUsers().

בעקבות השינוי שלמעלה, הנכס הפרטי _users הופך לנכס התומך של הנכס הציבורי users. מחוץ לכיתה Repository, אי אפשר לשנות את הרשימה _users, כי לצרכני הכיתה יש גישה לרשימה רק דרך users.

כשקוראים ל-users מקוד Kotlin, נעשה שימוש בהטמעה של List מ-Kotlin Standard Library, שבה אי אפשר לשנות את הרשימה. אם users נקרא מ-Java, נעשה שימוש בהטמעה של java.util.List, שבה אפשר לשנות את הרשימה וזמינות פעולות כמו add() ו-remove().

הקוד המלא:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

10. פונקציות ונכסים ברמה העליונה ובתוספים

בשלב הזה, הכיתה Repository יודעת לחשב את שם המשתמש בפורמט של אובייקט User. אבל אם רוצים לעשות שימוש חוזר באותה לוגיקה של עיצוב בכיתות אחרות, צריך להעתיק ולהדביק אותה או להעביר אותה לכיתה User.

ב-Kotlin אפשר להצהיר על פונקציות ומאפיינים מחוץ לכל כיתה, אובייקט או ממשק. לדוגמה, הפונקציה mutableListOf() שבה השתמשנו כדי ליצור מכונה חדשה של List כבר מוגדרת ב-Collections.kt מ-Kotlin Standard Library.

ב-Java, בכל פעם שצריך פונקציונליות של כלי שימושי, סביר להניח שתיצרו את הכיתה Util ותצהירו על הפונקציונליות הזו כפונקציה סטטית. ב-Kotlin אפשר להצהיר על פונקציות ברמה העליונה בלי שיהיה צורך בכיתה. עם זאת, ב-Kotlin יש גם אפשרות ליצור פונקציות הרחבה. אלה פונקציות שמרחיבות סוג מסוים, אבל הן מוצהרות מחוץ לסוג.

אפשר להגביל את החשיפה של פונקציות ותכונות של תוספים באמצעות משתני חשיפה. כך אפשר להגביל את השימוש רק לכיתות שזקוקות להרחבות, ולא לזהם את מרחב השמות.

עבור הכיתה User, אפשר להוסיף פונקציית תוסף שמחשבת את השם המעוצב, או לשמור את השם המעוצב בנכס תוסף. אפשר להוסיף אותו מחוץ לכיתה Repository, באותו קובץ:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

לאחר מכן נוכל להשתמש בפונקציות ובמאפיינים של התוסף כאילו הם חלק מהקלאס User.

מכיוון שהשם המעוצב הוא מאפיין של הכיתה User ולא פונקציונליות של הכיתה Repository, נשתמש במאפיין התוסף. קובץ Repository שלנו נראה עכשיו כך:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

ב-Kotlin Standard Library נעשה שימוש בפונקציות הרחבה כדי להרחיב את הפונקציונליות של כמה ממשקי API של Java. פונקציות רבות ב-Iterable וב-Collection מיושמות כפונקציות הרחבה. לדוגמה, הפונקציה map שבה השתמשנו בשלב קודם היא פונקציית תוסף ב-Iterable.

11. פונקציות היקף: let, ‏ apply, ‏ with, ‏ run, ‏ also

בקוד הכיתה Repository, מוסיפים כמה אובייקטים מסוג User לרשימה _users. אפשר לשפר את הקריאות האלה באמצעות פונקציות היקף של Kotlin.

כדי להריץ קוד רק בהקשר של אובייקט ספציפי, בלי צורך לגשת לאובייקט על סמך השם שלו, ב-Kotlin יש 5 פונקציות היקף: let, ‏ apply, ‏ with, ‏ run ו-also. הפונקציות האלה מקלות על הקריאה של הקוד ומקצרות אותו. לכל פונקציית היקף יש נמען (this), יכול להיות לה ארגומנט (it) והיא יכולה להחזיר ערך.

הנה טבלת עזרה שימושית שתעזור לכם לזכור מתי להשתמש בכל פונקציה:

6b9283d411fb6e7b.png

מכיוון שאנחנו מגדירים את האובייקט _users ב-Repository, אפשר לשפר את הסגנון של הקוד באמצעות הפונקציה apply:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

12. סיכום

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

כשמשתמשים ב-Kotlin באופן שתואם לשפה, אפשר לכתוב קוד קצר ופשוט. בעזרת כל התכונות של Kotlin, יש הרבה דרכים לשפר את הבטיחות, הקוהרנטיות והקריאוּת של הקוד. לדוגמה, אפשר גם לבצע אופטימיזציה של הכיתה Repository על ידי יצירה של רשימת _users עם משתמשים ישירות בהצהרה, וכך להיפטר מהבלוק init:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

התייחסנו למגוון רחב של נושאים, החל מהטיפול באפשרות של ערך null, מונולידים, מחרוזות ואוספים, ועד לנושאים כמו פונקציות תוסף, פונקציות ברמה העליונה, מאפיינים ופונקציות היקף. עברנו משתי כיתות Java לשתי כיתות Kotlin שנראות עכשיו כך:

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

ריכזנו כאן את הפונקציות של Java ואת המיפוי שלהן ל-Kotlin:

Java

Kotlin

אובייקט final

אובייקט val

equals()

==

==

===

כיתה שמכילה רק נתונים

הכיתה data

אתחול ב-constructor

אתחול בבלוק init

static שדות ופונקציות

שדות ופונקציות שהוגדרו ב-companion object

כיתה מסוג Singleton

object

למידע נוסף על Kotlin ועל האופן שבו משתמשים בה בפלטפורמה, אפשר לעיין במקורות המידע הבאים: