1. Chào mừng bạn!
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 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à tìm hiểu cách cải thiện mã đó bằng cách làm cho mã trở nên tự nhiên 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. Trong quá trình nà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 sao lưu
- Đố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
- Từ khoá
let
,apply
,with
vàrun
Giả định
Bạn phải đã quen thuộc với Java.
Bạn cần có
2. Thiết lập
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 sử dụng Android Studio, hãy tạo một dự án mới bằng mẫu No Activity (Không có hoạt động). Chọn Kotlin làm ngôn ngữ dự án. SDK tối thiểu có thể có bất kỳ giá trị nào, điều này sẽ không ảnh hưởng đến kết quả.
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> và dá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;
}
}
Bạn sẽ thấy IDE thông báo rằng @Nullable
chưa được xác định. Vì vậy, hãy nhập androidx.annotation.Nullable
nếu bạn sử dụng Android Studio hoặc org.jetbrains.annotations.Nullable
nếu bạn đang sử dụng IntelliJ.
Tạo một tệp mới có tên 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;
}
}
3. Khai báo tính chất rỗng, val, var và lớp dữ liệu
IDE của chúng ta có thể tự động chuyển đổi mã Java thành mã Kotlin khá tốt, nhưng đôi khi cần một chút trợ giúp. Hãy để IDE thực hiện lượt chuyển đổi đầu tiên. Sau đó, chúng ta sẽ xem xét mã thu được để hiểu cách thức và lý do mã được chuyển đổi theo cách này.
Chuyển đến tệp User.java
và 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 nhắc bạn sửa lỗi 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?)
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ó hai thuộc tính: firstName
và lastName
. Mỗi biến đều có phương thức getter và setter, giúp giá trị của biến 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ì các thuộc tính đó sẽ chỉ có thể đọc và được khai báo dưới dạng 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. Để thực hiện việc này, bạn cần thêm ?
vào phần khai báo loại.
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 sẽ tự động đánh dấu các thuộc tính này là có thể nhận giá trị rỗng bằng String?
. Nếu bạn chú thích các thành phần Java là không rỗng (sử dụng org.jetbrains.annotations.NotNull
hoặc androidx.annotation.NonNull
), trình chuyển đổi sẽ nhận ra điều này và cũng đặt các trường thành không rỗng trong Kotlin.
Bạn đã thực hiện xong lượt chuyển đổi cơ bản. Tuy nhiên, chúng ta có thể viết mã này 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 ta chỉ chứa dữ liệu. Kotlin có một từ khoá dành 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 phương thức getter và setter cho chúng ta. Hàm 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?)
Kotlin, giống như Java, 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 trong ví dụ trên là hàm khởi tạo 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ể thực hiện như sau:
val user1 = User("Jane", "Doe")
Bằng nhau
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
4. Đối số mặc định, đối số được đặt tên
Trong Kotlin, chúng ta có thể gán giá trị mặc định cho các đối số trong lệnh gọi hàm. Giá trị mặc định được sử dụng khi bạn bỏ qua đối số. Trong Kotlin, hàm khởi tạo cũng là hàm, vì vậy, chúng ta có thể sử dụng đối số mặc định để chỉ định rằng giá trị mặc định của lastName
là null
. Để thực hiện việc này, chúng ta chỉ cần gán 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("Joe", "Doe")
Kotlin cho phép bạn gắn nhãn cho đối số khi hàm được gọi:
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
không có. Trong trường hợp này, vì tham số mặc định sẽ đứng trước tham số không có giá trị mặc định, nên bạn phải gọi hàm bằng đối số được đặt 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")
Giá trị mặc định là một khái niệm quan trọng và thường được sử dụng trong mã Kotlin. Trong lớp học lập trình, chúng ta luôn muốn chỉ định tên và họ trong phần khai báo đối tượng User
, vì vậy chúng ta không cần giá trị mặc định.
5. Khởi tạo đối tượng, đối tượng đồng hành và singleton
Trước khi tiếp tục lớp học lập trình, hãy đảm bảo rằng lớp User
của bạn là lớp data
. Bây giờ, 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 trình chuyển đổi tự động đã làm gì:
- Danh sách
users
có thể nhận giá trị rỗng vì đối tượng không được tạo thực thể tại thời điểm khai báo - Các hàm trong Kotlin như
getUsers()
được khai báo bằng đối tượng sửa đổifun
- Phương thức
getFormattedUserNames()
hiện là một thuộc tính có tên làformattedUserNames
- 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 - Trường
static
hiện là một phần của khốicompanion object
- Thêm một khối
init
Trước khi đi tiếp, hãy dọn dẹp mã một chút. Nếu xem trong hàm khởi tạo, chúng ta sẽ thấy trình chuyển đổi đã tạo danh sách users
thành một danh sách có thể thay đổi chứa các đối tượng rỗng. Mặc dù danh sách này thực sự có thể có giá trị rỗng, nhưng giả sử danh sách này không thể chứa người dùng rỗng. Vì vậy, hãy làm như sau:
- Xoá
?
trongUser?
trong phần khai báo loạiusers
- Xoá
?
trongUser?
cho loại dữ liệu trả về củagetUsers()
để trả vềList<User>?
Khối khởi tạo
Trong Kotlin, hàm khởi tạo chính không được chứa bất kỳ mã nào, do đó, mã khởi tạo được đặt trong các khối init
. Chức năng vẫn như cũ.
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 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 thuộc tính người dùng đã được khởi tạo trong phần khai báo.
private var users: MutableList<User>? = null
Các thuộc tính và phương thức của static
trong 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 các trường hoặc hàm đó 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
. Phần tương đương trong Kotlin cho việc này là khối companion object
. 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 khối đối tượng đồng hành và di chuyển trường INSTANCE
tại đâ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à thay thế định nghĩa lớp bằng object Repository
. Đồng thời, hãy xoá đối tượng đồng hành.
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 các hàm và thuộc tính trực tiếp trên đối tượng, như sau:
val formattedUserNames = Repository.formattedUserNames
Xin lưu ý rằng nếu một thuộc tính không có đối tượng sửa đổi chế độ hiển thị, thì thuộc tính đó sẽ ở chế độ công khai theo mặc định, như trong trường hợp thuộc tính formattedUserNames
trong đối tượng Repository
.
6. Xử lý tính chất rỗng
Khi chuyển đổi lớp Repository
sang Kotlin, trình chuyển đổi tự động đã đặt danh sách người dùng thành 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. Do đó, đối với tất cả các trường hợp sử dụng đối tượng users
, bạn cần sử dụng toán tử xác nhận không rỗng !!
. (Bạn sẽ thấy users!!
và user!!
trong toàn bộ mã đã chuyển đổi.) Toán tử !!
chuyển đổi mọi biến thành loại không rỗng, vì vậy, bạn có thể truy cập vào các thuộc tính hoặc gọi hàm trên biến đó. Tuy nhiên, một ngoại lệ sẽ được gửi nếu giá trị biến thực sự là rỗng. Khi sử dụng !!
, bạn có nguy cơ bị gửi ngoại lệ trong thời gian chạy.
Thay vào đó, hãy ưu tiên xử lý tính chất 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 (đượ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 tạo ngay sau khi đối tượng được tạo (trong khối init
). Do đó, chúng ta có thể trực tiếp tạo thực thể cho đối tượng users
khi khai báo đối tượng đó.
Khi tạo các thực thể của loại bộ sưu tậ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 sử 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()
và cung cấp loại phần tử danh sách. mutableListOf<User>()
tạo một danh sách trống có thể chứa các đối tượng User
. Vì trình biên dịch hiện có thể suy ra loại dữ liệu của biến, nên hãy xoá phần khai báo loại rõ ràng của thuộc tính users
.
private val users = mutableListOf<User>()
Chúng tôi cũng đã thay đổi var
thành val
vì người dùng sẽ chứa một tệp tham chiếu chỉ có thể đọc đến danh sách người dùng. Xin lưu ý rằng tham chiếu này chỉ có thể đọc, do đó, tham chiếu này không bao giờ có thể trỏ đến một danh sách mới, nhưng bản thân danh sách vẫn có thể thay đổi (bạn có thể thêm hoặc xoá các phần tử).
Vì biến users
đã được khởi tạo, hãy xoá quá trình khởi tạo này khỏi khối init
:
users = ArrayList<Any?>()
Sau đó, khối init
sẽ có dạng như sau:
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ới những thay đổi này, thuộc tính users
của chúng ta hiện không 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. Xin lưu ý rằng bạn vẫn sẽ thấy lỗi biên dịch trong Android Studio, nhưng hãy tiếp tục thực hiện vài bước tiếp theo của lớp học lập trình để khắc phục các lỗi đó.
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)
}
Ngoài ra, đối với giá trị userNames
, nếu chỉ định loại ArrayList
là chứa Strings
, thì bạn có thể xoá loại rõ ràng trong phần khai báo vì loại này sẽ được suy ra.
val userNames = ArrayList<String>(users.size)
Huỷ cấu trúc
Kotlin cho phép phân ly một đối tượng thành một số biến bằng cách sử dụng cú pháp có tên là khai báo phân ly. Chúng ta tạo nhiều biến và có thể sử dụng các biến đó một cách độc lập.
Ví dụ: các lớp data
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
. Cập nhật vòng lặp for
như minh hoạ dưới đây. Thay thế tất cả các thực thể của user.firstName
bằng firstName
và thay thế user.lastName
bằng 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)
}
biểu thức if
Tên trong danh sách userNames chưa hoàn toàn ở định dạng mà chúng ta muốn. Vì cả lastName
và firstName
đều có thể là null
, nên chúng ta cần xử lý tính chất rỗng khi tạo danh sách tên người dùng được định dạng. Chúng ta muốn hiển thị "Unknown"
nếu thiếu một trong hai tên. Vì biến name
sẽ không thay đổi sau khi được đặt một lần, nên chúng ta có thể sử dụng val
thay vì var
. Hãy thực hiện thay đổi này trước.
val name: String
Hãy xem mã đặt biến name. Bạn có thể thấy mới mẻ khi thấy một biến được đặt bằng một khối mã if
/ else
. Điều này được cho phép vì trong Kotlin, if
và when
là các biểu thức – trả về một giá trị. Dòng cuối cùng của câu lệnh if
sẽ được gán cho name
. Mục đích duy nhất của khối này là khởi tạo giá trị name
.
Về cơ bản, logic được trình bày ở đây là nếu lastName
rỗng, name
sẽ được đặt thành firstName
hoặc "Unknown"
.
name = if (lastName != null) {
if (firstName != null) {
firstName + " " + lastName
} else {
lastName
}
} else if (firstName != null) {
firstName
} else {
"Unknown"
}
Toán tử Elvis
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 ở bên trái nếu biểu thức đó không rỗng hoặc biểu thức ở bên phải nếu biểu thức bên trái là rỗng.
Vì vậy, trong mã sau, firstName
sẽ được trả về nếu không phải là giá trị rỗng. Nếu firstName
là giá trị rỗng, thì biểu thức sẽ trả về giá trị ở bên phải , "Unknown"
:
name = if (lastName != null) {
...
} else {
firstName ?: "Unknown"
}
7. Mẫu chuỗi
Kotlin giúp bạn dễ dàng làm việc với String
thông qua 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 bằng cách sử dụng ký hiệu $ trước biến. Bạn cũng có thể đặt một biểu thức trong phần khai báo chuỗi bằng cách đặt biểu thức đó trong { } và sử dụng ký hiệu $ trước biểu thức. Ví dụ: ${user.firstName}
.
Mã của bạn hiện sử dụng tính năng nối chuỗi để kết hợp firstName
và lastName
vào tên người dùng.
if (firstName != null) {
firstName + " " + lastName
}
Thay vào đó, hãy thay thế việc nối Chuỗi bằng:
if (firstName != null) {
"$firstName $lastName"
}
Việc sử dụng mẫu chuỗi có thể giúp đơn giản hoá mã của bạn.
IDE sẽ hiển thị cảnh báo nếu có cách viết mã phù hợp hơn. Bạn sẽ thấy một đường gạch dưới ngoằn ngoèo trong mã và khi di chuột qua đường gạch dưới đó, bạn sẽ thấy một đề xuất về cách tái cấu trúc mã.
Hiện tại, bạn sẽ thấy một cảnh báo cho biết bạn có thể kết hợp phần khai báo name
với phần gán. Hãy áp dụng đ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á phần khai báo kiểu String
rõ ràng. Bây giờ, formattedUserNames
của chúng ta sẽ có dạng như sau:
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
}
Chúng ta có thể điều chỉnh thêm một chút. Logic giao diện người dùng của chúng ta hiển thị "Unknown"
trong trường hợp thiếu tên và họ, vì vậy, chúng tôi không hỗ trợ các đối tượng rỗng. Do đó, đối với loại dữ liệu formattedUserNames
, hãy thay thế List<String?>
bằng List<String>
.
val formattedUserNames: List<String>
8. Thao tác trên tập hợp
Hãy xem xét kỹ hơn phương thức getter formattedUserNames
và tìm hiểu cách chúng ta có thể làm cho phương thức này trở nên phù hợp 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 qua danh sách người dùng
- Tạo tên được định dạng cho mỗi người dùng, dựa trên tên và họ 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 phong phú các biến đổi tập hợp giúp quá trình phát triển nhanh hơn và an toàn hơn bằng cách mở rộng chức năng của API Tập hợp Java. 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
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 sử 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
}
}
Lưu ý rằng chúng ta sử dụng toán tử Elvis để trả về "Unknown"
nếu user.lastName
là giá trị rỗng, vì user.lastName
thuộc kiểu String?
và cần có String
cho name
.
...
else {
user.lastName ?: "Unknown"
}
...
Để đơ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"
}
}
}
9. Thuộc tính và thuộc tính sao lưu
Chúng ta đã thấy 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ó phương thức getter tuỳ chỉnh. Trong phần nội dung, Kotlin vẫn tạo 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 hàm getter và setter. Kotlin cho phép chúng ta phân biệt rõ ràng 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à chức nă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 của chúng ta, lớp Repository
rất đơn giản và không thực hiện bất kỳ hành động 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 phương thức getter của thuộc tính Kotlin formattedUserNames
.
Mặc dù chúng ta không có trường tương ứng rõ ràng với thuộc tính formattedUserNames
, nhưng Kotlin cung cấp cho chúng ta một trường sao lưu tự động có tên là field
. Chúng ta có thể truy cập vào trường này nếu cần từ các phương thức getter và setter 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 sao lưu tự động không cung cấp.
Hãy xem xét một ví dụ.
Bên trong lớp Repository
, chúng ta có một danh sách người dùng có thể thay đổi được hiển thị trong hàm getUsers()
được tạo từ mã Java:
fun getUsers(): List<User>? {
return users
}
Vì không muốn phương thức gọi của lớp Repository
sửa đổi danh sách người dùng, nên chúng ta đã tạo hàm getUsers()
trả về một List<User>
chỉ có thể đọc. Với Kotlin, chúng ta nên sử dụng thuộc tính thay vì hàm cho những trường hợp như vậy. Chính xác hơn, chúng ta sẽ hiển thị một List<User>
chỉ có thể đọc được hỗ trợ bởi mutableListOf<User>
.
Trước tiên, hãy đổi tên users
thành _users
. Đánh dấu tên biến, nhấp chuột phải để Refactor > Rename (Tái cấu trúc > Đổi tên) biến. Sau đó, hãy thêm một thuộc tính chỉ đọc công khai trả về danh sách người dùng. Hãy gọi lớp này là users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
Tại thời điểm này, bạn có thể xoá phương thức getUsers()
.
Với thay đổi ở trên, thuộc tính _users
riêng tư sẽ trở thành thuộc tính sao lưu cho thuộc tính users
công khai. Bên ngoài lớp Repository
, danh sách _users
không thể sửa đổi được vì người dùng của lớp này chỉ có thể truy cập vào danh sách thông qua users
.
Khi users
được gọi từ mã Kotlin, phương thức triển khai List
từ Thư viện chuẩn Kotlin sẽ được sử dụng, trong đó danh sách không thể sửa đổi được. Nếu users
được gọi từ Java, thì cách triển khai java.util.List
sẽ được sử dụng, trong đó danh sách có thể sửa đổi và có các thao tác như add() và remove().
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)
}
}
10. Các hàm và thuộc tính cấp cao nhất và mở rộng
Hiện tại, lớp Repository
biết cách tính tên người dùng được định dạng cho đối tượng User
. Tuy nhiên, 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 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 bất kỳ lớp, đối tượng hoặc giao diện nào. 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 trong Collections.kt
của Thư viện chuẩn Kotlin.
Trong Java, mỗi khi cần một số chức năng tiện ích, bạn có nhiều khả năng 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 hàm mở rộng. Đây là các hàm mở rộng một loại nhất định nhưng được khai báo bên ngoài loại đó.
Bạn có thể hạn chế chế độ hiển thị của các hàm và thuộc tính mở rộng bằng cách sử dụng các đối tượng sửa đổi chế độ hiển thị. Các lớp này chỉ hạn chế việc sử dụng cho các lớp cần tiện ích và không làm bẩn không gian tên.
Đối với lớp User
, chúng ta có thể thêm một hàm mở rộng để tính toán tên được định dạng hoặc giữ tên được định dạng trong một thuộc tính mở rộng. Bạn có thể thêm thuộc tính 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 được định dạng là thuộc tính của lớp User
chứ không phải chức năng của lớp Repository
, 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 tiêu chuẩn Kotlin sử 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 hàm mở rộng. Ví dụ: hàm map
mà chúng ta đã sử dụng ở bước trước là một hàm mở rộng trên Iterable
.
11. Hàm phạm vi: let, apply, with, run, also
Trong mã lớp Repository
, chúng ta sẽ thêm một số đối tượng User
vào danh sách _users
. Bạn có thể thực hiện các lệnh gọi này một cách tự nhiên hơn nhờ các hàm phạm vi Kotlin.
Để 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 cung cấp 5 hàm phạm vi: let
, apply
, with
, run
và also
. Các hàm này giúp mã của bạn dễ đọc và ngắn gọn hơn. Tất cả các hàm phạm vi đều có một trình nhận (this
), có thể có một đối số (it
) và có thể trả về một giá trị.
Dưới đây là một bảng tra cứu hữu ích để giúp bạn nhớ thời điểm sử dụng từng hàm:
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 phù hợp 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)
}
}
12. Tóm tắt
Trong lớp học lập trình này, chúng tôi đã đề cập đến những kiến thức cơ bản mà bạn cần để bắt đầu chuyển đổi mã từ Java sang Kotlin. Quá trình chuyển đổi này độc lập với 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ã Kotlin đặc trưng.
Thành ngữ Kotlin giúp việc viết mã ngắn gọn và súc tích. 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 tạo bản sao danh sách _users
với người dùng ngay trong phần 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ừ việc xử lý tính chất rỗng, singleton, Chuỗi và bộ sưu tập đế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ừ hai lớp Java sang hai lớp Kotlin có dạng như sau:
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 }
}
Dưới đây là thông tin 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ỉ chứa 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ác trường và hàm được khai báo trong |
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
- Kiến thức cơ bản về Kotlin cho Android
- 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