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
,with
vàrun
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.
Mã
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: firstName
và lastName
. 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 firstName
và lastName
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()
và 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ọiequals()
để 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 lastName
là null
. Để 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ử firstName
có null
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ốicompanion 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á
?
trongUser?
trong khai báo kiểuusers
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ị firstName
và lastName
. 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ả lastName
và firstName
đề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
, for
và while
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 map
là it
, 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 Iterable
và Collection
đượ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
, run
và also
. 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 | Đối tượng |
|
|
|
|
Lớp chỉ lưu giữ dữ liệu | Lớp |
Khởi chạy trong hàm khởi tạo | Khởi chạy trong khối |
| các trường và hàm được khai báo trong một |
Lớp Singleton |
|
Để 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:
- Kotlin Koans
- Hướng dẫn về Kotlin
- Phát triển ứng dụng Android bằng Kotlin – khoá học miễn phí
- Chương trình đào tạo về Kotlin dành cho lập trình viên
- Kotlin dành cho nhà phát triển Java – khoá học miễn phí ở chế độ Kiểm tra