ב-codelab הזה נסביר איך להמיר את הקוד מ-Java ל-Kotlin. בנוסף, נסביר מהן המוסכמות של שפת Kotlin ואיך לוודא שהקוד שאתם כותבים עומד בהן.
ה-codelab הזה מתאים לכל מפתח שמשתמש ב-Java ושוקל להעביר את הפרויקט שלו ל-Kotlin. נתחיל עם כמה מחלקות Java שתמירו ל-Kotlin באמצעות סביבת הפיתוח המשולבת (IDE). לאחר מכן נבדוק את הקוד שהומר ונראה איך אפשר לשפר אותו על ידי הפיכתו לאידיומטי יותר, ואיך אפשר להימנע מטעויות נפוצות.
מה תלמדו
במאמר הזה נסביר איך להמיר Java ל-Kotlin. במהלך התרגול תלמדו על התכונות והמושגים הבאים בשפת Kotlin:
- טיפול בערכים ריקים
- הטמעה של singleton
- סיווגי נתונים
- טיפול במחרוזות
- Elvis operator
- פירוק מבנה
- מאפיינים ומאפייני גיבוי
- ארגומנטים שמוגדרים כברירת מחדל ופרמטרים עם שמות
- עבודה עם קולקציות
- פונקציות של תוספים
- פונקציות ופרמטרים ברמה העליונה
- מילות המפתח
let,apply,withו-run
הנחות
כדאי שתהיה לכם כבר היכרות עם Java.
מה נדרש
יצירת פרויקט חדש
אם משתמשים ב-IntelliJ IDEA, יוצרים פרויקט Java חדש עם Kotlin/JVM.
אם משתמשים ב-Android Studio, יוצרים פרויקט חדש ללא פעילות.
הקוד
ניצור אובייקט מודל User ומחלקה סינגלטונית 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;
}
}בהתאם לסוג הפרויקט, מייבאים androidx.annotation.Nullable אם משתמשים בפרויקט Android, או 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;
}
}סביבת הפיתוח המשולבת שלנו יכולה לבצע עבודה טובה למדי של שינוי מבנה קוד Java לקוד Kotlin באופן אוטומטי, אבל לפעמים היא זקוקה לעזרה קלה. קודם נסביר איך זה עובד, ואז נסקור את הקוד שעבר רפקטורינג כדי להבין איך ולמה הוא עבר רפקטורינג בצורה הזו.
עוברים לקובץ User.java וממירים אותו ל-Kotlin: סרגל התפריטים -> Code (קוד) -> Convert Java File to Kotlin File (המרת קובץ Java לקובץ Kotlin).
אם סביבת הפיתוח המשולבת (IDE) מציעה לתקן אחרי ההמרה, לוחצים על Yes (כן).

אמור להופיע קוד Kotlin הבא: :
class User(var firstName: String?, var lastName: String?)הערה: השם של User.java שונה ל-User.kt. קבצי Kotlin הם עם הסיומת .kt.
במחלקת Java User היו לנו שני מאפיינים: firstName ו-lastName. לכל אחד מהם הייתה שיטת getter ושיטת setter, ולכן הערך שלו היה ניתן לשינוי. מילת המפתח של Kotlin למשתנים שניתנים לשינוי היא var, ולכן הכלי להמרה משתמש ב-var לכל אחת מהמאפיינים האלה. אם למאפייני Java היו רק שיטות getter, הם לא היו ניתנים לשינוי והיו מוצהרים כמשתני val. val דומה למילת המפתח final ב-Java.
אחד ההבדלים העיקריים בין Kotlin לבין Java הוא שב-Kotlin מציינים במפורש אם משתנה יכול לקבל ערך null. הוא עושה את זה על ידי הוספת `?` להצהרת הסוג.
מכיוון שסימנו את firstName ו-lastName כמאפיינים שאפשר להגדיר להם ערך null, הכלי להמרה אוטומטית סימן את המאפיינים האלה כמאפיינים שאפשר להגדיר להם ערך null באמצעות String?. אם מוסיפים הערות לחברי Java כ-non-null (באמצעות org.jetbrains.annotations.NotNull או androidx.annotation.NonNull), הכלי להמרה יזהה את זה ויגדיר את השדות כ-non-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")Equality
ב-Kotlin יש שני סוגים של שוויון:
- כדי לקבוע אם שני מקרים שווים, השוויון המבני משתמש באופרטור
==וקורא ל-equals(). - שוויון רפרנציאלי משתמש באופרטור
===ובודק אם שתי הפניות מצביעות על אותו אובייקט.
המאפיינים שמוגדרים בבונה הראשי של מחלקת הנתונים ישמשו לבדיקות של שוויון מבני.
val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // falseב-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("John", "Doe")אפשר לתת שמות לפרמטרים של פונקציות כשקוראים לפונקציות:
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")לפני שממשיכים, צריך לוודא שהכיתה שלכם ב-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)
}
}בואו נראה מה עשה הכלי האוטומטי להמרת מטבעות:
- נוסף בלוק
init(Repository.kt#L50) - השדה
staticהוא עכשיו חלק מהבלוקcompanion object(Repository.kt#L33) - הרשימה של
usersיכולה להיות null כי האובייקט לא נוצר בזמן ההצהרה (Repository.kt#L7) - השיטה
getFormattedUserNames()היא עכשיו מאפיין שנקראformattedUserNames(Repository.kt#L11) - האיטרציה על רשימת המשתמשים (שהייתה במקור חלק מ-
getFormattedUserNames() ) כוללת תחביר שונה מזה של Java (Repository.kt#L15)
לפני שנמשיך, ננקה קצת את הקוד. אפשר לראות שהממיר הפך את הרשימה users לרשימה שניתנת לשינוי ומכילה אובייקטים שניתנים לאיפוס. הרשימה יכולה להיות ריקה, אבל נניח שהיא לא יכולה להכיל משתמשים ריקים. לכן, צריך לבצע את הפעולות הבאות:
- הסרת
?ב-User?בהצהרה מסוגusers getUsersאמור להחזירList<User>?
בנוסף, הכלי להמרה אוטומטית פיצל שלא לצורך לשתי שורות את הצהרות המשתנים של משתני המשתמשים ושל המשתנים שמוגדרים בבלוק init. בואו נשים כל הצהרה על משתנה בשורה נפרדת. כך הקוד צריך להיראות:
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)
}
}בלוק init
ב-Kotlin, הקונסטרוקטור הראשי לא יכול להכיל קוד, ולכן קוד ההגדרה מוצב בבלוקים של 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, אפשר לראות שהמאפיין users אותחל בהצהרה.
private var users: MutableList<User>? = nullstatic מאפיינים ושיטות ב-Kotlin
ב-Java, משתמשים במילת המפתח static בשדות או בפונקציות כדי לציין שהם שייכים למחלקה אבל לא למופע של המחלקה. לכן יצרנו את השדה הסטטי INSTANCE במחלקה Repository. המקבילה ב-Kotlin היא הבלוק companion object. כאן גם מצהירים על שדות סטטיים ופונקציות סטטיות. הממיר נוצר והעביר את השדה INSTANCE לכאן.
טיפול בסינגלטונים
מכיוון שאנחנו צריכים רק מופע אחד של המחלקה 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 users = Repository.usersפירוק מבנה
ב-Kotlin אפשר לפרק אובייקט למספר משתנים באמצעות תחביר שנקרא הצהרת פירוק. אנחנו יוצרים כמה משתנים ואפשר להשתמש בהם בנפרד.
לדוגמה, מחלקות נתונים תומכות בפירוק מבנה, כך שאפשר לפרק את האובייקט User בלולאה for ל-(firstName, lastName). כך אנחנו יכולים לעבוד ישירות עם הערכים firstName ו-lastName. כך מעדכנים את לולאת for:
for ((firstName, lastName) in users!!) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
כשממירים את המחלקה Repository ל-Kotlin, הממיר האוטומטי הופך את רשימת המשתמשים לניתנת לאיפוס, כי היא לא אותחלה לאובייקט כשהיא הוכרזה. בכל השימושים באובייקט users, נעשה שימוש באופרטור !! של טענת אי-null. היא ממירה כל משתנה לסוג שאינו null, ומציגה חריגה אם הערך הוא null. השימוש ב-!! עלול לגרום להפעלת חריגים בזמן ריצה.
במקום זאת, מומלץ לטפל בערכי null באחת מהשיטות הבאות:
- ביצוע בדיקת ערך null (
if (users != null) {...}) - שימוש באופרטור אלביס
?:(יוסבר בהמשך ה-Codelab) - שימוש בחלק מהפונקציות הסטנדרטיות של Kotlin (הסבר על כך בהמשך במעבדת הקוד)
במקרה שלנו, אנחנו יודעים שרשימת המשתמשים לא צריכה להיות nullable, כי היא מאותחלת מיד אחרי בניית האובייקט, ולכן אנחנו יכולים ליצור מופע של האובייקט ישירות כשאנחנו מכריזים עליו.
כשיוצרים מופעים של סוגי אוסף, Kotlin מספקת כמה פונקציות עזר כדי להפוך את הקוד לקריא וגמיש יותר. בדוגמה הזו אנחנו משתמשים ב-MutableList בשביל users:
private var users: MutableList<User>? = nullכדי לפשט את התהליך, אפשר להשתמש בפונקציה mutableListOf(), לספק את סוג הרכיב של הרשימה, להסיר את הקריאה לבונה ArrayList מהבלוק init ולהסיר את הצהרת הסוג המפורשת של המאפיין users.
private val users = mutableListOf<User>()בנוסף, שינינו את var ל-val כי המשתנה users יכיל הפניה קבועה לרשימת המשתמשים. חשוב לשים לב שההפניה היא קבועה, אבל הרשימה עצמה ניתנת לשינוי (אפשר להוסיף או להסיר אלמנטים).
בעקבות השינויים האלה, המאפיין users כבר לא יכול להיות null, ואפשר להסיר את כל המקרים המיותרים של האופרטור !!.
val userNames: MutableList<String?> = ArrayList(users.size)for ((firstName, lastName) in users) {
...
}בנוסף, מכיוון שהמשתנה users כבר אותחל, צריך להסיר את האתחול מהבלוק 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)
}מכיוון שגם lastName וגם firstName יכולים להיות null, אנחנו צריכים לטפל באפשרות של ערך null כשיוצרים את רשימת שמות המשתמשים המעוצבים. מכיוון שאנחנו רוצים להציג את "Unknown" אם אחד מהשמות חסר, אנחנו יכולים להסיר את ? מהצהרת הסוג כדי שהשם לא יהיה null.
val name: Stringאם lastName הוא null, name הוא firstName או "Unknown":
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}אפשר לכתוב את זה בצורה יותר אידיומטית באמצעות האופרטור אלביס ?:. האופרטור של אלביס יחזיר את הביטוי בצד שמאל שלו אם הוא לא null, או את הביטוי בצד ימין שלו אם הצד השמאלי הוא null.
לכן בקוד הבא, אם הוא לא null, מוחזר user.firstName. אם user.firstName הוא null, הביטוי מחזיר את הערך בצד ימין , "Unknown":
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}ב-Kotlin קל לעבוד עם Strings באמצעות תבניות מחרוזות. תבניות מחרוזת מאפשרות לכם להפנות למשתנים בתוך הצהרות מחרוזת.
הכלי האוטומטי להמרה עדכן את השרשור של השם הפרטי והשם המשפחה כך שיפנה ישירות לשם המשתנה במחרוזת באמצעות הסמל $, והציב את הביטוי בין { } .
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"בקוד, מחליפים את שרשור המחרוזת ב:
name = "$firstName $lastName"ב-Kotlin if, when, for ו-while הם ביטויים – הם מחזירים ערך. בסביבת הפיתוח המשולבת (IDE) מוצגת אזהרה שלפיה צריך להוציא את ההקצאה מתוך if:

נפעל לפי ההצעה של סביבת הפיתוח המשולבת ונבטל את ההקצאה בשתי ההצהרות של if. השורה האחרונה של משפט התנאי if תוקצה. כך ברור יותר שהמטרה היחידה של הבלוק הזה היא לאתחל את ערך השם:
name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}בשלב הבא, תופיע אזהרה שאפשר לצרף את ההצהרה name להקצאה. נחיל את זה גם. אפשר להסיק את סוג המשתנה של השם, ולכן אפשר להסיר את הצהרת הסוג המפורשת. עכשיו formattedUserNames נראה כך:
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> = ArrayList(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
}בואו נבחן מקרוב את formattedUserNames getter ונראה איך אפשר לשפר אותו. בשלב הזה הקוד מבצע את הפעולות הבאות:
- יצירה של רשימה חדשה של מחרוזות
- הפונקציה חוזרת על עצמה ברשימת המשתמשים
- יוצר את השם המעוצב של כל משתמש, על סמך השם הפרטי ושם המשפחה של המשתמש
- מחזירה את הרשימה החדשה שנוצרה
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
}
}כדי לפשט את זה עוד יותר, אפשר להסיר לגמרי את המשתנה 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"
}
}
}ראינו שהכלי להמרה אוטומטית החליף את הפונקציה 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 יכול לשנות את רשימת המשתמשים שלנו – וזה לא רעיון טוב!users כדי לפתור את הבעיה הזו, נשתמש במאפיין גיבוי.
קודם כול, נשנה את השם של users ל-_users. עכשיו מוסיפים נכס ציבורי שלא ניתן לשינוי שמחזיר רשימה של משתמשים. נקרא לו users:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _usersבעקבות השינוי הזה, מאפיין פרטי _users הופך למאפיין הבסיס של מאפיין ציבורי users. מחוץ למחלקה Repository, אי אפשר לשנות את הרשימה _users, כי צרכני המחלקה יכולים לגשת לרשימה רק דרך users.
קוד מלא:
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)
}
}בשלב הזה, המחלקה Repository יודעת איך לחשב את שם המשתמש בפורמט של אובייקט User. אבל אם רוצים להשתמש שוב באותה לוגיקת עיצוב בכיתות אחרות, צריך להעתיק ולהדביק אותה או להעביר אותה לכיתה User.
ב-Kotlin אפשר להצהיר על פונקציות ומאפיינים מחוץ לכל מחלקה, אובייקט או ממשק. לדוגמה, הפונקציה mutableListOf() שבה השתמשנו כדי ליצור מופע חדש של List מוגדרת ישירות ב-Collections.kt מהספרייה הרגילה.
ב-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.
מכיוון שהשם המעוצב הוא מאפיין של המשתמש ולא פונקציונליות של המחלקה Repository, נשתמש במאפיין extension. קובץ 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 נעשה שימוש בפונקציות הרחבה כדי להרחיב את הפונקציונליות של כמה ממשקי API של Java. הרבה מהפונקציות ב-Iterable וב-Collection מיושמות כפונקציות הרחבה. לדוגמה, הפונקציה map שבה השתמשנו בשלב הקודם היא פונקציית הרחבה של Iterable.
בקטע Repository class code, אנחנו מוסיפים כמה אובייקטים של משתמשים לרשימה _users. אפשר להשתמש בפונקציות של היקף כדי להפוך את הקריאות האלה ליותר אידיומטיות.
כדי להריץ קוד רק בהקשר של אובייקט ספציפי, בלי צורך לגשת לאובייקט לפי השם שלו, ב-Kotlin נוצרו 5 פונקציות היקף: let, apply, with, run ו-also. הפונקציות האלה קצרות ושימושיות, לכולן יש מקבל (this), יכול להיות להן ארגומנט (it) והן יכולות להחזיר ערך. אתם מחליטים באיזו שיטה להשתמש בהתאם למטרה שאתם רוצים להשיג.
הנה דף עזר שיעזור לכם לזכור את זה:

מכיוון שאנחנו מגדירים את האובייקט _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)
}
}ב-codelab הזה הסברנו את היסודות שצריך לדעת כדי להתחיל לשכתב את הקוד מ-Java ל-Kotlin. השינוי הזה לא תלוי בפלטפורמת הפיתוח שלכם, ועוזר לוודא שהקוד שאתם כותבים הוא אידיומטי.
השימוש ב-Kotlin אידיומטי מאפשר לכתוב קוד קצר ופשוט. עם כל התכונות ש-Kotlin מספקת, יש כל כך הרבה דרכים להפוך את הקוד לבטוח יותר, לתמציתי יותר ולקריא יותר. לדוגמה, אפשר אפילו לבצע אופטימיזציה של המחלקה Repository על ידי יצירת מופע של הרשימה _users עם משתמשים ישירות בהצהרה, וכך להיפטר מהבלוק init:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))הסברנו על מגוון רחב של נושאים, החל מטיפול בערכי null, בסינגלטונים, במחרוזות ובאוספים, ועד לנושאים כמו פונקציות הרחבה, פונקציות ברמה העליונה, מאפיינים ופונקציות היקף. עברנו משני מחלקות Java לשני מחלקות Kotlin, שהן עכשיו כאלה:
User.kt
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 |
|
|
|
|
|
|
מחלקות שמכילות רק נתונים |
|
אתחול בבונה | אתחול בקטע |
| שדות ופונקציות שהוגדרו ב- |
Singleton class |
|
כדי לקבל מידע נוסף על Kotlin ועל אופן השימוש בה בפלטפורמה שלכם, אפשר לעיין במקורות המידע הבאים:
- Kotlin Koans
- מדריכים ל-Kotlin
- פיתוח אפליקציות ל-Android באמצעות Kotlin – קורס חינמי
- Kotlin Bootcamp for Programmers
- Kotlin for Java developers – קורס חינמי במצב ביקורת