1. שלום,
ב-codelab הזה נסביר איך להמיר את הקוד מ-Java ל-Kotlin. בנוסף, נסביר מהן המוסכמות של שפת Kotlin ואיך לוודא שהקוד שאתם כותבים עומד בהן.
ה-codelab הזה מתאים לכל מפתח שמשתמש ב-Java ושוקל להעביר את הפרויקט שלו ל-Kotlin. נתחיל עם כמה מחלקות Java שתמירו ל-Kotlin באמצעות סביבת הפיתוח המשולבת (IDE). לאחר מכן נבחן את הקוד שהומר ונראה איך אפשר לשפר אותו כדי שיהיה אידיומטי יותר, ואיך אפשר להימנע מטעויות נפוצות.
מה תלמדו
תלמדו איך להמיר Java ל-Kotlin. במהלך התרגול תלמדו על התכונות והמושגים הבאים בשפת Kotlin:
- טיפול בערכים ריקים
- הטמעה של סינגלטונים
- סיווגי נתונים
- טיפול במחרוזות
- אופרטור אלביס
- פירוק מבנה
- מאפיינים ומאפייני גיבוי
- ארגומנטים שמוגדרים כברירת מחדל ופרמטרים עם שמות
- עבודה עם קולקציות
- פונקציות של תוספים
- פונקציות ופרמטרים ברמה העליונה
- מילות המפתח
let,apply,withו-run
הנחות
כבר אמורה להיות לכם היכרות עם Java.
הדרישות
2. תהליך ההגדרה
יצירת פרויקט חדש
אם אתם משתמשים ב-IntelliJ IDEA, אתם יכולים ליצור פרויקט Java חדש עם Kotlin/JVM.
אם אתם משתמשים ב-Android Studio, אתם יכולים ליצור פרויקט חדש באמצעות התבנית No Activity (ללא פעילות). בוחרים באפשרות Kotlin כשפת הפרויקט. הערך של גרסת ה-SDK המינימלית יכול להיות כל ערך, והוא לא ישפיע על התוצאה.
הקוד
ניצור אובייקט מודל 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;
}
}
תראו שה-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. הצהרה על אפשרות קבלת ערך null, על val, על var ועל מחלקות נתונים
סביבת הפיתוח המשולבת שלנו יכולה להמיר קוד Java לקוד Kotlin באופן אוטומטי, אבל לפעמים היא זקוקה לעזרה קלה. נשתמש בסביבת הפיתוח המשולבת כדי לבצע המרה ראשונית. לאחר מכן נבדוק את הקוד שנוצר כדי להבין איך ולמה הוא הומר בצורה הזו.
עוברים לקובץ User.java וממירים אותו ל-Kotlin: סרגל התפריטים -> קוד -> המרת קובץ 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 היו רק מתודות get, הם היו לקריאה בלבד והיו מוצהרים כמשתני 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")
שוויון
ב-Kotlin יש שני סוגים של שוויון:
- בשוויון מבני נעשה שימוש באופרטור
==ומתבצעת קריאה ל-equals()כדי לקבוע אם שני מקרים שווים. - שוויון רפרנציאלי משתמש באופרטור
===ובודק אם שתי הפניות מצביעות על אותו אובייקט.
המאפיינים שמוגדרים בבונה הראשי של מחלקת הנתונים ישמשו לבדיקות של שוויון מבני.
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. ב-codelab שלנו אנחנו רוצים לציין תמיד את השם הפרטי ואת שם המשפחה בהצהרה של אובייקט User, ולכן אנחנו לא צריכים ערכי ברירת מחדל.
5. אתחול אובייקט, אובייקט נלווה וסינגלטונים
לפני שממשיכים ב-codelab, צריך לוודא שהסיווג 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 - ה-method
getFormattedUserNames()הוא עכשיו מאפיין שנקראformattedUserNames - האיטרציה על רשימת המשתמשים (שהייתה במקור חלק מ-
getFormattedUserNames() ) כוללת תחביר שונה מזה של Java - השדה
staticהוא עכשיו חלק מהבלוקcompanion object - נוסף בלוק
init
לפני שנמשיך, ננקה קצת את הקוד. אם נסתכל על ה-constructor, נראה שהממיר הפך את הרשימה users לרשימה שניתנת לשינוי ומכילה אובייקטים שניתנים ל-nullable. אמנם הרשימה יכולה להיות ריקה, אבל נניח שהיא לא יכולה להכיל משתמשים ריקים. לכן, צריך לבצע את הפעולות הבאות:
- הסרת
?ב-User?בהצהרת הסוגusers - מסירים את
?ב-User?עבור סוג ההחזרה שלgetUsers()כדי שהפונקציה תחזירList<User>?
בלוק 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>? = null
static מאפיינים ושיטות ב-Kotlin
ב-Java, אנחנו משתמשים במילת המפתח static בשדות או בפונקציות כדי לציין שהם שייכים למחלקה אבל לא למופע של המחלקה. לכן יצרנו את השדה הסטטי INSTANCE במחלקה Repository שלנו. המקבילה ב-Kotlin היא הבלוק companion object. כאן גם מצהירים על שדות סטטיים ופונקציות סטטיות. הממיר יצר את בלוק האובייקט הנלווה והעביר את השדה INSTANCE לכאן.
טיפול בסינגלטונים
מכיוון שאנחנו צריכים רק מופע אחד של המחלקה Repository, השתמשנו ב-singleton pattern ב-Java. ב-Kotlin, אפשר לאכוף את התבנית הזו ברמת הקומפיילר על ידי החלפת מילת המפתח class במילת המפתח object.
מסירים את הבונה הפרטי ומחליפים את הגדרת המחלקה ב-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, הממיר האוטומטי הופך את רשימת המשתמשים לניתנת לאיפוס, כי היא לא אותחלה לאובייקט כשהיא הוכרזה. לכן, בכל מקום שבו נעשה שימוש באובייקט users, צריך להשתמש באופרטור !! של טענת אי-null. (הסמלים users!! ו-user!! יופיעו לאורך הקוד שהומר). האופרטור !! ממיר כל משתנה לסוג שאינו null, כך שתוכלו לגשת למאפיינים או לקרוא לפונקציות שלו. עם זאת, אם ערך המשתנה הוא null, תופעל חריגה. השימוש ב-!! עלול לגרום לחריגות בזמן הריצה.
במקום זאת, מומלץ לטפל בערכי null באמצעות אחת מהשיטות הבאות:
- ביצוע בדיקת ערך null (
if (users != null) {...}) - שימוש באופרטור אלביס
?:(יוסבר בהמשך ה-Codelab) - שימוש בחלק מהפונקציות הסטנדרטיות של Kotlin (שיוסברו בהמשך ב-codelab)
במקרה שלנו, אנחנו יודעים שרשימת המשתמשים לא צריכה להיות 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, אבל צריך להמשיך לשלבים הבאים ב-codelab כדי לפתור אותן.
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)
}
בנוסף, אם מציינים שהסוג של ArrayList הוא Strings, אפשר להסיר את הסוג המפורש בהצהרה כי הוא יוסק.userNames
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
השמות ברשימת שמות המשתמשים עדיין לא בפורמט הרצוי. מכיוון שגם lastName וגם firstName יכולים להיות null, אנחנו צריכים לטפל במאפיין המציין אם ערך יכול להיות ריק (nullability) כשאנחנו יוצרים את רשימת שמות המשתמשים בפורמט. אנחנו רוצים להציג "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"
}
אופרטור אלביס
אפשר לכתוב את הקוד הזה בצורה יותר אידיומטית באמצעות אופרטור אלביס ?:. אופרטור אלביס יחזיר את הביטוי בצד שמאל שלו אם הוא לא null, או את הביטוי בצד ימין שלו אם הצד השמאלי הוא null.
לכן בקוד הבא, הפונקציה מחזירה את firstName אם הוא לא null. אם firstName הוא null, הביטוי מחזיר את הערך בצד שמאל , "Unknown":
name = if (lastName != null) {
...
} else {
firstName ?: "Unknown"
}
7. תבניות של מחרוזות
קל לעבוד עם Strings ב-Kotlin באמצעות תבניות מחרוזות. תבניות מחרוזות מאפשרות לכם להפנות למשתנים בתוך הצהרות מחרוזות באמצעות הסימן $ לפני המשתנה. אפשר גם להוסיף ביטוי להצהרה על מחרוזת, על ידי הוספת הביטוי בתוך { } ושימוש בסמל $ לפניו. לדוגמה: ${user.firstName}.
הקוד שלכם משתמש כרגע בשרשור מחרוזות כדי לשלב את firstName ואת lastName בשם המשתמש.
if (firstName != null) {
firstName + " " + lastName
}
במקום זאת, מחליפים את שרשור המחרוזות בקוד הבא:
if (firstName != null) {
"$firstName $lastName"
}
שימוש בתבניות מחרוזת יכול לפשט את הקוד.
סביבת הפיתוח המשולבת (IDE) תציג לכם אזהרות אם יש דרך יותר אידיומטית לכתוב את הקוד. תראו קו תחתון גלי בקוד, וכשתעבירו את העכבר מעליו תראו הצעה לשינוי המבנה של הקוד.
בשלב הזה, אמורה להופיע אזהרה שאפשר לצרף את ההצהרה 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. פעולות באוספים
בואו נבחן מקרוב את 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
}
}
שימו לב שאנחנו משתמשים באופרטור אלביס כדי להחזיר "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. מסמנים את שם המשתנה, לוחצים לחיצה ימנית על Refactor > Rename כדי לשנות את שם המשתנה. לאחר מכן מוסיפים נכס ציבורי לקריאה בלבד שמחזיר רשימה של משתמשים. נקרא לו users:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
בשלב הזה, אפשר למחוק את השיטה getUsers().
בעקבות השינוי הזה, הנכס הפרטי _users הופך לנכס הבסיס של הנכס הציבורי users. מחוץ למחלקה Repository, אי אפשר לשנות את הרשימה _users, כי צרכני המחלקה יכולים לגשת לרשימה רק דרך users.
כשקוראים ל-users מקוד Kotlin, נעשה שימוש בהטמעה של List מהספרייה הסטנדרטית של Kotlin, שבה אי אפשר לשנות את הרשימה. אם קוראים ל-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.
ב-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, ולכן נשתמש במאפיין 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 Standard Library נעשה שימוש בפונקציות הרחבה כדי להרחיב את הפונקציונליות של כמה ממשקי Java API. הרבה מהפונקציות ב-Iterable וב-Collection מיושמות כפונקציות הרחבה. לדוגמה, הפונקציה map שבה השתמשנו בשלב הקודם היא פונקציית הרחבה של Iterable.
11. פונקציות היקף: let, apply, with, run, also
בקוד הכיתה Repository, אנחנו מוסיפים כמה אובייקטים User לרשימה _users. אפשר להפוך את הקריאות האלה לאידיומטיות יותר בעזרת פונקציות היקף של Kotlin.
כדי להריץ קוד רק בהקשר של אובייקט ספציפי, בלי צורך לגשת לאובייקט לפי השם שלו, 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)
}
}
12. סיכום
ב-codelab הזה הסברנו את היסודות שצריך לדעת כדי להתחיל להמיר את הקוד מ-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 |
אובייקט | אובייקט |
|
|
|
|
מחלקות שמכילות רק נתונים |
|
אתחול בבונה | אתחול בבלוק |
| שדות ופונקציות שהוגדרו ב- |
Singleton class |
|
כדי לקבל מידע נוסף על Kotlin ועל אופן השימוש בה בפלטפורמה שלכם, אפשר לעיין במקורות המידע הבאים:
- Kotlin Koans
- מדריכים ל-Kotlin
- Android Kotlin Fundamentals
- Kotlin Bootcamp for Programmers
- Kotlin for Java developers – קורס חינמי במצב ביקורת