Tái cấu trúc sangKotlin

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách chuyển đổi mã từ Java sang Kotlin. Bạn cũng sẽ tìm hiểu về quy ước ngôn ngữ Kotlin và cách đảm bảo mã bạn đang viết tuân theo các quy ước đó.

Lớp học lập trình này phù hợp với mọi nhà phát triển sử dụng Java và đang cân nhắc việc di chuyển dự án của họ sang Kotlin. Chúng ta sẽ bắt đầu với một vài lớp Java mà bạn sẽ chuyển đổi sang Kotlin bằng IDE. Sau đó, chúng ta sẽ xem xét mã đã chuyển đổi và xem cách cải thiện mã đó bằng cách làm cho mã trở nên thành ngữ hơn và tránh các lỗi thường gặp.

Kiến thức bạn sẽ học được

Bạn sẽ tìm hiểu cách chuyển đổi Java sang Kotlin. Khi làm như vậy, bạn sẽ tìm hiểu các tính năng và khái niệm sau đây của ngôn ngữ Kotlin:

  • Xử lý tính chất rỗng
  • Triển khai singleton
  • Lớp dữ liệu
  • Xử lý chuỗi
  • Toán tử Elvis
  • Huỷ cấu trúc
  • Thuộc tính và thuộc tính dự phòng
  • Đối số mặc định và tham số được đặt tên
  • Làm việc với tập hợp
  • Hàm mở rộng
  • Hàm và tham số cấp cao nhất
  • let, apply, withrun từ khoá

Giả định

Bạn cần thông thạo Java.

Bạn cần có

Tạo dự án mới

Nếu bạn đang sử dụng IntelliJ IDEA, hãy tạo một dự án Java mới bằng Kotlin/JVM.

Nếu bạn đang dùng Android Studio, hãy tạo một dự án mới mà không có Hoạt động.

Chúng ta sẽ tạo một đối tượng mô hình User và một lớp singleton Repository hoạt động với các đối tượng User, đồng thời hiển thị danh sách người dùng và tên người dùng được định dạng.

Tạo một tệp mới có tên là User.java trong app/java/<yourpackagename> rồi dán đoạn mã sau vào:

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;
    }

}

Tuỳ thuộc vào loại dự án, hãy nhập androidx.annotation.Nullable nếu bạn sử dụng dự án Android hoặc org.jetbrains.annotations.Nullable nếu không.

Tạo một tệp mới có tên là Repository.java rồi dán mã sau vào:

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;
    }
}

IDE của chúng tôi có thể tự động tái cấu trúc mã Java thành mã Kotlin một cách khá hiệu quả, nhưng đôi khi bạn cần hỗ trợ một chút. Chúng ta sẽ thực hiện việc này trước, sau đó xem xét mã đã tái cấu trúc để hiểu cách thức và lý do mã được tái cấu trúc theo cách này.

Chuyển đến tệp User.java rồi chuyển đổi tệp đó sang Kotlin: Thanh trình đơn -> Mã -> Chuyển đổi tệp Java sang tệp Kotlin.

Nếu IDE của bạn nhắc bạn sửa sau khi chuyển đổi, hãy nhấn vào Yes (Có).

Bạn sẽ thấy mã Kotlin sau: :

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

Xin lưu ý rằng User.java đã được đổi tên thành User.kt. Tệp Kotlin có đuôi .kt.

Trong lớp User Java, chúng ta có 2 thuộc tính: firstNamelastName. Mỗi đối tượng đều có một phương thức getter và setter, giúp giá trị của đối tượng có thể thay đổi. Từ khoá của Kotlin cho các biến có thể thay đổi là var, vì vậy, trình chuyển đổi sẽ dùng var cho từng thuộc tính này. Nếu các thuộc tính Java của chúng ta chỉ có phương thức getter, thì chúng sẽ là bất biến và được khai báo là các biến val. val tương tự như từ khoá final trong Java.

Một trong những điểm khác biệt chính giữa Kotlin và Java là Kotlin chỉ định rõ ràng liệu một biến có thể chấp nhận giá trị rỗng hay không. Thao tác này được thực hiện bằng cách thêm một ? vào khai báo kiểu.

Vì chúng ta đã đánh dấu firstNamelastName là có thể nhận giá trị rỗng, nên trình chuyển đổi tự động đã tự động đánh dấu các thuộc tính là có thể nhận giá trị rỗng bằng String?. Nếu bạn chú giải các thành phần Java là không có giá trị rỗng (bằng cách sử dụng org.jetbrains.annotations.NotNull hoặc androidx.annotation.NonNull), thì trình chuyển đổi sẽ nhận ra điều này và cũng làm cho các trường không có giá trị rỗng trong Kotlin.

Đã hoàn tất quá trình tái cấu trúc cơ bản. Nhưng chúng ta có thể viết theo cách đặc trưng hơn. Hãy cùng xem cách thực hiện.

Lớp dữ liệu

Lớp User của chúng tôi chỉ lưu giữ dữ liệu. Kotlin có một từ khoá cho các lớp có vai trò này: data. Bằng cách đánh dấu lớp này là lớp data, trình biên dịch sẽ tự động tạo các phương thức getter và setter cho chúng ta. Thao tác này cũng sẽ lấy các hàm equals(), hashCode()toString().

Hãy thêm từ khoá data vào lớp User:

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

Tương tự như Java, Kotlin có thể có một hàm khởi tạo chính và một hoặc nhiều hàm khởi tạo phụ. Hàm dựng trong ví dụ trên là hàm dựng chính của lớp User. Nếu bạn đang chuyển đổi một lớp Java có nhiều hàm khởi tạo, thì trình chuyển đổi cũng sẽ tự động tạo nhiều hàm khởi tạo trong Kotlin. Các lớp này được xác định bằng từ khoá constructor.

Nếu muốn tạo một thực thể của lớp này, chúng ta có thể làm như sau:

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

Equality

Kotlin có hai loại đẳng thức:

  • Đẳng thức cấu trúc sử dụng toán tử == và gọi equals() để xác định xem hai thực thể có bằng nhau hay không.
  • Đẳng thức tham chiếu sử dụng toán tử === và kiểm tra xem hai tham chiếu có trỏ tới cùng một đối tượng hay không.

Các thuộc tính được xác định trong hàm khởi tạo chính của lớp dữ liệu sẽ được dùng để kiểm tra đẳng thức cấu trúc.

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

Trong Kotlin, chúng ta có thể chỉ định giá trị mặc định cho các đối số trong lệnh gọi hàm. Giá trị mặc định được dùng khi đối số bị bỏ qua. Trong Kotlin, hàm khởi tạo cũng là hàm, vì vậy, chúng ta có thể dùng các đối số mặc định để chỉ định rằng giá trị mặc định của lastNamenull. Để làm việc này, chúng ta chỉ cần chỉ định null cho 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")

Bạn có thể đặt tên cho các tham số của hàm khi gọi hàm:

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

Trong một trường hợp sử dụng khác, giả sử firstNamenull làm giá trị mặc định và lastName thì không. Trong trường hợp này, vì tham số mặc định sẽ đứng trước một tham số không có giá trị mặc định, nên bạn sẽ phải gọi hàm bằng các đối số có tên:

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")

Trước khi tiếp tục, hãy đảm bảo rằng lớp User của bạn là một lớp data. Hãy chuyển đổi lớp Repository sang Kotlin. Kết quả chuyển đổi tự động sẽ có dạng như sau:

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)
   }
}

Hãy xem bộ chuyển đổi tự động đã làm gì:

  • Đã thêm một khối init (Repository.kt#L50)
  • Trường static hiện là một phần của khối companion object (Repository.kt#L33)
  • Danh sách users có thể rỗng vì đối tượng không được khởi tạo tại thời điểm khai báo (Repository.kt#L7)
  • Phương thức getFormattedUserNames() hiện là một thuộc tính có tên là formattedUserNames (Repository.kt#L11)
  • Quá trình lặp lại danh sách người dùng (ban đầu là một phần của getFormattedUserNames() có cú pháp khác với cú pháp Java (Repository.kt#L15)

Trước khi đi xa hơn, hãy dọn dẹp mã một chút. Chúng ta có thể thấy rằng trình chuyển đổi đã biến danh sách users của chúng ta thành một danh sách có thể thay đổi, chứa các đối tượng có thể rỗng. Mặc dù danh sách có thể có giá trị rỗng, nhưng giả sử danh sách không thể chứa người dùng có giá trị rỗng. Vì vậy, hãy làm như sau:

  • Xoá ? trong User? trong khai báo kiểu users
  • getUsers phải trả về List<User>?

Trình chuyển đổi tự động cũng chia không cần thiết thành 2 dòng các khai báo biến của biến người dùng và của các biến được xác định trong khối init. Hãy đặt mỗi khai báo biến trên một dòng. Mã của chúng ta sẽ có dạng như sau:

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)
    }
}

Khối khởi tạo

Trong Kotlin, hàm khởi tạo chính không thể chứa bất kỳ mã nào, vì vậy, mã khởi tạo được đặt trong các khối init. Chức năng này vẫn giữ nguyên.

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)
    }
}

Phần lớn mã init xử lý việc khởi tạo các thuộc tính. Bạn cũng có thể thực hiện việc này trong phần khai báo thuộc tính. Ví dụ: trong phiên bản Kotlin của lớp Repository, chúng ta thấy rằng thuộc tính users đã được khởi tạo trong khai báo.

private var users: MutableList<User>? = null

Các thuộc tính và phương thức static của Kotlin

Trong Java, chúng ta sử dụng từ khoá static cho các trường hoặc hàm để cho biết rằng chúng thuộc về một lớp chứ không phải một thực thể của lớp. Đó là lý do chúng ta tạo trường tĩnh INSTANCE trong lớp Repository. Tương đương với khối companion object trong Kotlin. Tại đây, bạn cũng sẽ khai báo các trường tĩnh và hàm tĩnh. Trình chuyển đổi đã tạo và di chuyển trường INSTANCE đến đây.

Xử lý singleton

Vì chỉ cần một thực thể của lớp Repository, nên chúng ta đã sử dụng mẫu singleton trong Java. Với Kotlin, bạn có thể thực thi mẫu này ở cấp trình biên dịch bằng cách thay thế từ khoá class bằng object.

Xoá hàm khởi tạo riêng tư và đối tượng đồng hành, đồng thời thay thế định nghĩa lớp bằng 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)
    }
}

Khi sử dụng lớp object, chúng ta chỉ cần gọi trực tiếp các hàm và thuộc tính trên đối tượng, như sau:

val users = Repository.users

Destructuring

Kotlin cho phép trích xuất một đối tượng thành một số biến bằng cách sử dụng một cú pháp gọi là khai báo trích xuất. Chúng ta tạo nhiều biến và có thể sử dụng chúng một cách độc lập.

Ví dụ: các lớp dữ liệu hỗ trợ việc huỷ cấu trúc để chúng ta có thể huỷ cấu trúc đối tượng User trong vòng lặp for thành (firstName, lastName). Điều này cho phép chúng ta làm việc trực tiếp với các giá trị firstNamelastName. Hãy cập nhật vòng lặp for như sau:

 
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)
}

Khi chuyển đổi lớp Repository sang Kotlin, trình chuyển đổi tự động đã tạo danh sách người dùng có thể rỗng, vì danh sách này không được khởi tạo thành một đối tượng khi được khai báo. Đối với tất cả các cách sử dụng đối tượng users, toán tử xác nhận không rỗng !! sẽ được dùng. Toán tử này chuyển đổi mọi biến thành loại không có giá trị rỗng và gửi một ngoại lệ nếu giá trị đó là rỗng. Khi sử dụng !!, bạn có nguy cơ gặp phải các trường hợp ngoại lệ trong thời gian chạy.

Thay vào đó, hãy ưu tiên xử lý khả năng có giá trị rỗng bằng một trong các phương thức sau:

  • Kiểm tra giá trị rỗng ( if (users != null) {...} )
  • Sử dụng toán tử elvis ?: (sẽ được đề cập ở phần sau trong lớp học lập trình này)
  • Sử dụng một số hàm chuẩn của Kotlin (sẽ được đề cập sau trong lớp học lập trình)

Trong trường hợp này, chúng ta biết rằng danh sách người dùng không cần phải có giá trị rỗng, vì danh sách này được khởi chạy ngay sau khi đối tượng được tạo. Vì vậy, chúng ta có thể trực tiếp tạo thực thể cho đối tượng khi khai báo.

Khi tạo các thực thể của loại tập hợp, Kotlin cung cấp một số hàm trợ giúp để giúp mã của bạn dễ đọc và linh hoạt hơn. Ở đây, chúng ta đang dùng MutableList cho users:

private var users: MutableList<User>? = null

Để đơn giản, chúng ta có thể sử dụng hàm mutableListOf(), cung cấp loại phần tử danh sách, xoá lệnh gọi hàm khởi tạo ArrayList khỏi khối init và xoá khai báo kiểu rõ ràng của thuộc tính users.

private val users = mutableListOf<User>()

Chúng ta cũng thay đổi var thành val vì users sẽ chứa một tham chiếu bất biến đến danh sách người dùng. Xin lưu ý rằng giá trị tham chiếu là bất biến, nhưng bản thân danh sách có thể thay đổi (bạn có thể thêm hoặc xoá các phần tử).

Với những thay đổi này, thuộc tính users của chúng ta hiện không có giá trị rỗng và chúng ta có thể xoá tất cả các lần xuất hiện toán tử !! không cần thiết.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Ngoài ra, vì biến users đã được khởi tạo, nên chúng ta phải xoá quá trình khởi tạo khỏi khối 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)
}

Vì cả lastNamefirstName đều có thể là null, nên chúng ta cần xử lý khả năng có giá trị rỗng khi tạo danh sách tên người dùng được định dạng. Vì muốn hiển thị "Unknown" nếu thiếu một trong hai tên, nên chúng ta có thể đặt tên là không có giá trị rỗng bằng cách xoá ? khỏi khai báo kiểu.

val name: String

Nếu lastName là giá trị rỗng, thì name sẽ là firstName hoặc "Unknown":

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

Bạn có thể viết mã này theo cách đặc trưng hơn bằng cách sử dụng toán tử elvis ?:. Toán tử elvis sẽ trả về biểu thức ở phía bên trái nếu biểu thức đó không phải là giá trị rỗng, hoặc biểu thức ở phía bên phải nếu phía bên trái là giá trị rỗng.

Vì vậy, trong đoạn mã sau, user.firstName sẽ được trả về nếu không có giá trị rỗng. Nếu user.firstName là giá trị rỗng, biểu thức sẽ trả về giá trị ở bên phải , "Unknown":

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

Kotlin giúp bạn dễ dàng làm việc với String bằng mẫu chuỗi. Mẫu chuỗi cho phép bạn tham chiếu đến các biến bên trong phần khai báo chuỗi.

Trình chuyển đổi tự động đã cập nhật việc nối tên và họ để tham chiếu trực tiếp tên biến trong chuỗi bằng cách sử dụng ký hiệu $ và đặt biểu thức giữa { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

Trong mã, hãy thay thế việc nối chuỗi bằng:

name = "$firstName $lastName"

Trong Kotlin, if, when, forwhile là các biểu thức, chúng trả về một giá trị. IDE của bạn thậm chí còn cho thấy một cảnh báo rằng bạn nên đưa câu lệnh gán ra khỏi if:

Hãy làm theo đề xuất của IDE và loại bỏ câu lệnh gán cho cả hai câu lệnh if. Dòng cuối cùng của câu lệnh if sẽ được chỉ định. Như vậy, rõ ràng hơn rằng mục đích duy nhất của khối này là khởi tạo giá trị tên:

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

Tiếp theo, chúng ta sẽ nhận được cảnh báo rằng bạn có thể kết hợp khai báo name với việc chỉ định. Hãy áp dụng cả điều này. Vì có thể suy luận kiểu của biến name, nên chúng ta có thể xoá khai báo kiểu rõ ràng. formattedUserNames của chúng ta hiện sẽ có dạng như sau:

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
   }

Hãy xem xét kỹ hơn về phương thức truy xuất formattedUserNames và xem cách chúng ta có thể làm cho phương thức này trở nên đặc trưng hơn. Hiện tại, mã này thực hiện những việc sau:

  • Tạo danh sách chuỗi mới
  • Lặp lại theo danh sách người dùng
  • Tạo tên đã định dạng cho từng người dùng, dựa trên họ và tên của người dùng
  • Trả về danh sách mới tạo
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 cung cấp một danh sách mở rộng về các phép biến đổi tập hợp giúp quá trình phát triển diễn ra nhanh hơn và an toàn hơn bằng cách mở rộng các chức năng của Java Collections API. Một trong số đó là hàm map. Hàm này trả về một danh sách mới chứa kết quả của việc áp dụng hàm biến đổi đã cho cho từng phần tử trong danh sách ban đầu. Vì vậy, thay vì tạo một danh sách mới và lặp lại danh sách người dùng theo cách thủ công, chúng ta có thể sử dụng hàm map và di chuyển logic mà chúng ta có trong vòng lặp for vào bên trong phần nội dung map. Theo mặc định, tên của mục danh sách hiện tại được dùng trong mapit, nhưng để dễ đọc, bạn có thể thay thế it bằng tên biến của riêng mình. Trong trường hợp này, hãy đặt tên là 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
            }
        }

Để đơn giản hoá hơn nữa, chúng ta có thể xoá hoàn toàn biến 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"
                }
            }
        }

Chúng ta thấy rằng trình chuyển đổi tự động đã thay thế hàm getFormattedUserNames() bằng một thuộc tính có tên là formattedUserNames có một getter tuỳ chỉnh. Về cơ bản, Kotlin vẫn tạo ra một phương thức getFormattedUserNames() trả về List.

Trong Java, chúng ta sẽ hiển thị các thuộc tính lớp thông qua các hàm getter và setter. Kotlin cho phép chúng ta phân biệt rõ hơn giữa các thuộc tính của một lớp (được biểu thị bằng các trường) và các chức năng (những hành động mà một lớp có thể thực hiện, được biểu thị bằng các hàm). Trong trường hợp này, lớp Repository rất đơn giản và không thực hiện bất kỳ thao tác nào nên chỉ có các trường.

Logic được kích hoạt trong hàm getFormattedUserNames() của Java hiện được kích hoạt khi gọi getter của thuộc tính formattedUserNames Kotlin.

Mặc dù chúng ta không có trường nào tương ứng một cách rõ ràng với thuộc tính formattedUserNames, nhưng Kotlin cung cấp cho chúng ta một trường hỗ trợ tự động có tên là field mà chúng ta có thể truy cập nếu cần từ các phương thức truy xuất và thiết lập tuỳ chỉnh.

Tuy nhiên, đôi khi chúng ta muốn có thêm một số chức năng mà trường hỗ trợ tự động không cung cấp. Hãy xem ví dụ dưới đây.

Trong lớp Repository, chúng ta có một danh sách người dùng có thể thay đổi đang được hiển thị trong hàm getUsers() được tạo từ mã Java của chúng ta:

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

Vấn đề ở đây là bằng cách trả về users, mọi đối tượng sử dụng lớp Repository đều có thể sửa đổi danh sách người dùng của chúng ta – đây không phải là một ý tưởng hay! Hãy khắc phục vấn đề này bằng cách sử dụng một thuộc tính hỗ trợ.

Trước tiên, hãy đổi tên users thành _users. Bây giờ, hãy thêm một thuộc tính công khai không thể thay đổi để trả về danh sách người dùng. Hãy gọi tệp này là users:

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

Với thay đổi này, thuộc tính riêng tư _users sẽ trở thành thuộc tính sao lưu cho thuộc tính công khai users. Bên ngoài lớp Repository, danh sách _users không thể sửa đổi vì người dùng của lớp chỉ có thể truy cập vào danh sách thông qua users.

Mã đầy đủ:

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)
    }
}

Hiện tại, lớp Repository biết cách tính tên người dùng được định dạng cho một đối tượng User. Nhưng nếu muốn sử dụng lại cùng một logic định dạng trong các lớp khác, chúng ta cần sao chép và dán logic đó hoặc di chuyển logic đó sang lớp User.

Kotlin cho phép khai báo các hàm và thuộc tính bên ngoài mọi lớp, đối tượng hoặc giao diện. Ví dụ: hàm mutableListOf() mà chúng ta dùng để tạo một thực thể mới của List được xác định trực tiếp trong Collections.kt của Thư viện chuẩn.

Trong Java, bất cứ khi nào cần một số chức năng tiện ích, bạn rất có thể sẽ tạo một lớp Util và khai báo chức năng đó dưới dạng một hàm tĩnh. Trong Kotlin, bạn có thể khai báo các hàm cấp cao nhất mà không cần có lớp. Tuy nhiên, Kotlin cũng cung cấp khả năng tạo các hàm mở rộng. Đây là những hàm mở rộng một loại nhất định nhưng được khai báo bên ngoài loại đó. Do đó, họ có mối quan tâm đến loại nội dung đó.

Bạn có thể hạn chế khả năng hiển thị của các hàm và thuộc tính mở rộng bằng cách sử dụng từ khoá xác định mức độ hiển thị. Những lớp này chỉ giới hạn việc sử dụng cho các lớp cần tiện ích và không làm ảnh hưởng đến không gian tên.

Đối với lớp User, chúng ta có thể thêm một hàm tiện ích tính toán tên được định dạng hoặc có thể giữ tên được định dạng trong một thuộc tính tiện ích. Bạn có thể thêm mã này bên ngoài lớp Repository, trong cùng một tệp:

// 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

Sau đó, chúng ta có thể sử dụng các hàm và thuộc tính mở rộng như thể chúng là một phần của lớp User.

Vì tên đã định dạng là một thuộc tính của người dùng chứ không phải là chức năng của lớp Repository, nên hãy sử dụng thuộc tính tiện ích. Tệp Repository của chúng ta hiện có dạng như sau:

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)
    }
}

Thư viện chuẩn Kotlin dùng các hàm mở rộng để mở rộng chức năng của một số API Java; nhiều chức năng trên IterableCollection được triển khai dưới dạng các hàm mở rộng. Ví dụ: hàm map mà chúng ta đã dùng ở bước trước là một hàm mở rộng trên Iterable.

Trong mã lớp Repository, chúng ta sẽ thêm một số đối tượng người dùng vào danh sách _users. Bạn có thể thực hiện các lệnh gọi này theo cách thành ngữ hơn nhờ các hàm phạm vi.

Để chỉ thực thi mã trong ngữ cảnh của một đối tượng cụ thể mà không cần truy cập vào đối tượng dựa trên tên của đối tượng đó, Kotlin đã tạo 5 hàm phạm vi: let, apply, with, runalso. Ngắn gọn và mạnh mẽ, tất cả các hàm này đều có một đối tượng nhận (this), có thể có một đối số (it) và có thể trả về một giá trị. Bạn sẽ quyết định sử dụng loại nào tuỳ thuộc vào mục tiêu bạn muốn đạt được.

Sau đây là một bảng thông tin hữu ích giúp bạn ghi nhớ điều này:

Vì đang định cấu hình đối tượng _users trong Repository, nên chúng ta có thể làm cho mã trở nên thành ngữ hơn bằng cách sử dụng hàm 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)
    }
 }

Trong lớp học lập trình này, chúng ta đã đề cập đến những kiến thức cơ bản mà bạn cần để bắt đầu tái cấu trúc mã từ Java sang Kotlin. Hoạt động tái cấu trúc này không phụ thuộc vào nền tảng phát triển của bạn và giúp đảm bảo rằng mã bạn viết là mã đặc trưng.

Kotlin tự nhiên giúp bạn viết mã ngắn gọn và dễ hiểu. Với tất cả các tính năng mà Kotlin cung cấp, có rất nhiều cách để giúp mã của bạn an toàn hơn, ngắn gọn hơn và dễ đọc hơn. Ví dụ: chúng ta thậm chí có thể tối ưu hoá lớp Repository bằng cách khởi tạo danh sách _users với người dùng ngay trong khai báo, loại bỏ khối init:

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

Chúng ta đã đề cập đến nhiều chủ đề, từ cách xử lý khả năng rỗng, singleton, chuỗi và bộ sưu tập cho đến các chủ đề như hàm mở rộng, hàm cấp cao nhất, thuộc tính và hàm phạm vi. Chúng ta đã chuyển từ 2 lớp Java sang 2 lớp Kotlin. Giờ đây, các lớp này có dạng như sau:

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 }
}

Dưới đây là nội dung tóm tắt về các chức năng của Java và cách ánh xạ các chức năng đó sang Kotlin:

Java

Kotlin

Đối tượng final

Đối tượng val

equals()

==

==

===

Lớp chỉ lưu giữ dữ liệu

Lớp data

Khởi chạy trong hàm khởi tạo

Khởi chạy trong khối init

static trường và hàm

các trường và hàm được khai báo trong một companion object

Lớp Singleton

object

Để tìm hiểu thêm về Kotlin và cách sử dụng ngôn ngữ này trên nền tảng của bạn, hãy xem các tài nguyên sau: