Với mục đích giúp đỡ bạn học Lập trình Java căn bản tốt. Hôm nay mình sẽ giúp bạn tìm hiểu kỹ hơn về Class và Object.
Classes và Object trong Java | Học Lập trình Java căn bản
Tìm hiểu về cách làm cho các class, field, method, constructor và các object hoạt động cùng nhau trong các ứng dụng Java.
Các class, field, method, constructor và các object là các mảnh ghép của các ứng dụng Java hướng đối tượng.
Bài hướng dẫn này giúp bạn hiểu:
-
Cách khai báo các class
-
Mô tả các thuộc tính thông qua các trường
-
Mô tả các hành vi thông qua các phương thức
-
Khởi tạo các đối tượng thông qua các hàm tạo
-
Khởi tạo các đối tượng từ các class và truy cập các thành viên của chúng.
Đồng thời, bạn cũng sẽ tìm hiểu về setters và getters, phương thức nạp chồng (overloading), thiết lập accsess level cho các trường, constructor và method, v.v.
Lưu ý: Các ví dụ trong hướng dẫn này biên dịch và chạy với phiên bản thấp hơn Java 12.
1. Cách Khai báo Class trong Java | Java căn bản
Một Class là một khuôn mẫu cho các đối tượng. Bạn khai báo một class bằng cách chỉ định từ khóa class theo sau là một định danh (indentifier).
tiếp theo là một cặp ký tự dấu ngoặc nhọn mở và đóng phù hợp {
và }
và phân định phần body của class.
Cú pháp khai báo class như sau:
class identifier
{
// class body
}
Theo quy ước:
-
Tên class có chữ cái đầu tiên viết Hoa
-
Các ký tự tiếp theo viết thương (ví dụ: Staff).
-
Nếu một tên bao gồm nhiều từ, chữ cái đầu tiên của mỗi từ được viết hoa (chẳng hạn như SavingsAccount)
=> Quy ước đặt tên này được gọi là CamelCasing (Hoặc PascalCase).
> Xem thêm: Quy ước đặt tên trong Java
Ví dụ bên dưới đây khai báo một class có tên là Book:
class Book
{
// Phần thân của class
}
Phần body của một class được điền với các trường, method và constructor. Kết hợp các tính năng ngôn ngữ Java vào các class như là đóng gói trong Java.
> Tìm hiểu kỹ hơn về Đóng gói trong Java qua ví dụ Hack tài khoản ngân hàng.
Khả năng này cho phép chúng ta lập trình ở mức độ trừu tượng cao hơn (các class và object) thay vì tập trung riêng vào cấu trúc dữ liệu và chức năng.
Ghi chú: Object-based application là gì?
Object-based application là một ứng dụng có thiết kế dựa trên việc khai báo các class, tạo các object và thiết kế các tương tác giữa các đối tượng.
Ghi chú: Utility Class là gì?
Một class có thể được thiết kế để không liên quan gì đến object làm việc cụ thể. Thay vào đó, nó tồn tại như một trình giữ chỗ cho các class field hoặc các class method.
=> Một class như vậy được gọi là một utility class.
Một ví dụ về utility class là class Math trong thư viện tiêu chuẩn của Java.
Tìm hiểu về Multi-class applications và main()
Một ứng dụng Java được thực hiện bởi một hoặc nhiều class.
Các ứng dụng nhỏ có thể được cung cấp bởi một class duy nhất, nhưng các ứng dụng lớn hơn thường yêu cầu nhiều class.
Trong trường hợp đó, một trong các class được chỉ định là main class và chứa phương thức main()
.
Ví dụ 1: Một ứng dụng Java có nhiều class
class A
{
}
class B
{
}
class C
{
public static void main(String[] args)
{
System.out.println("Ứng dụng bắt đầu từ C");
}
}
Bạn có thể khai báo ba class này trong một tệp mã duy nhất, chẳng hạn như D.java
. Sau đó, bạn sẽ biên dịch tệp nguồn này như sau:
javac D.java
Trình biên dịch tạo ra ba tệp: A.class
, B.class
và C.class
. Chạy ứng dụng này thông qua lệnh sau:
java C
Bạn có output như sau:
Ứng dụng bắt đầu từ C
Ngoài ra, bạn có thể khai báo mỗi class trong tệp nguồn riêng của nó. Theo quy ước, tên của tệp khớp với tên class.
Ví dụ, bạn sẽ khai báo class A
trong A.java
. Sau đó, bạn có thể biên dịch các tệp nguồn này một cách riêng biệt:
javac A.java
javac B.java
javac C.java
Để tiết kiệm thời gian, bạn cũng có thể biên dịch cả ba tệp này cùng một lúc bằng cách thay thế tên tệp bằng dấu hoa thị (*
) (nhưng vẫn giữ phần mở rộng tệp .java
):
javac *.java
Dù bằng cách nào, bạn sẽ chạy ứng dụng thông qua lệnh sau:
java C
Ghi chú: Tìm hiểu một chút về Public class
Java cho phép bạn khai báo một class có quyền truy cập công khai thông qua từ khóa public.
Khi bạn khai báo một puclic class, bạn phải lưu trữ nó trong một tệp có cùng tên.
Ví dụ: Bạn sẽ lưu trữ public class C {}
trong tệp C.java
. Bạn chỉ có thể khai báo một public class trong một tệp lệnh.
Khi thiết kế các ứng dụng dạng multi-class, bạn sẽ chỉ định một trong các class này là main class và định vị phương thức main()
ở trong đó.
Tuy nhiên, không có gì ngăn bạn khai báo các phương thức main()
trong các class khác.
Chúng ta sẽ thử xem điều này ở ví dụ tiếp theo.
Ví dụ 2: Khai báo nhiều hơn một phương thức main() trong ứng dụng Java
class A
{
public static void main(String[] args)
{
System.out.println("Test class A");
}
}
class B
{
public static void main(String[] args)
{
System.out.println("Test class B");
}
}
class C
{
public static void main(String[] args)
{
System.out.println("Ứng dụng bắt đầu từ C");
}
}
Sau khi biên dịch mã nguồn, bạn sẽ thực hiện các lệnh sau để kiểm tra các helper classes A và B và để chạy ứng dụng class C:
java A
java B
java C
Sau đó, bạn sẽ quan sát các dòng output sau, một dòng trên mỗi lệnh java:
Test class A
Test class B
Ứng dụng bắt đầu từ C
Hãy cẩn thận khi sử dụng hàm main() trong Java
Việc đặt một phương thức main()
trong mỗi class có thể gây nhầm lẫn, đặc biệt nếu bạn quên ghi lại main class.
Ngoài ra, bạn có thể quên xóa các phương thức này trước khi đưa ứng dụng vào trong sản phẩm, trong trường hợp đó, sự hiện diện của chúng sẽ làm ứng dụng phình to hơn.
Hơn nữa, ai đấy có thể chạy một trong các class hỗ trợ dẫn đến có khả năng phá vỡ môi trường của ứng dụng Java.
2. Tìm hiểu về Fields: Mô tả thuộc tính
Việc mô hình hóa một thực thể trong thế giới thực về mặt trạng thái gọi là thuộc tính (attributes)
Ví dụ: Một chiếc xe có màu và kiểm tra tài khoản có số dư.
Một class cũng có thể bao gồm trạng thái phi thực thể. Bất kể, trạng thái được lưu trữ trong các biến được gọi là các trường (field).
Cú pháp khai báo một trường (field) như sau:
[static] type identifier [ = expression ] ;
Khai báo trường tùy ý bắt đầu bằng từ khóa static (đối với thuộc tính không phải là thực thể) và tiếp tục với type được theo sau bởi một định danh (indentifier) là tên của trường.
Trường (Field) có thể được khởi tạo một cách rõ ràng bằng cách chỉ định = theo sau là một biểu thức (expression) với kiểu tương thích. Một dấu chấm phẩy để chấm dứt khai báo.
Ví dụ khai báo một trường có tên là Book:
class Book
{
String title;
int pubYear; // Năm xuất bản
}
Các khai báo trường title
và pubYear
giống hệt với các khai báo biến mà chúng ta đã biết.
Các trường này được gọi là instance fields vì mỗi đối tượng chứa bản sao riêng của chúng.
Các trường tiêu đề và pubYear lưu trữ giá trị cho một cuốn sách cụ thể. Tuy nhiên, thường là bạn muốn lưu trữ trạng thái độc lập với bất kỳ cuốn sách cụ thể nào.
Ví dụ: Bạn có thể muốn ghi lại tổng số đối tượng Book được tạo. Đây là cách bạn sẽ làm điều đó:
class Book
{
// ...
static int count;
}
Ví dụ này khai báo trường số nguyên (int) lưu trữ số lượng đối tượng Book được tạo.
Khai báo bắt đầu bằng từ khóa static để chỉ ra rằng chỉ có một bản sao của trường này trong bộ nhớ.
Mỗi đối tượng Book có thể truy cập bản sao này và không có đối tượng nào có bản sao riêng. Vì lý do này, count được gọi là một class field.
Khởi tạo: Initialization
Các trường trước đó sẽ không được gán giá trị khi bạn không khởi tạo một cách rõ ràng, nó sẽ được khởi tạo hoàn toàn với tất cả các bit của nó được đặt thành không.
Bạn diễn giải giá trị mặc định này là:
-
false (đối với boolean)
-
'\u0000' (đối với char)
-
0 (đối với int)
-
0L (đối với long)
-
0.0F (đối với float)
-
0.0 (đối với double)
-
hoặc null (đối với một kiểu tham chiếu).
Tuy nhiên, cũng có thể khởi tạo một cách rõ ràng một trường khi trường được khai báo.
Ví dụ: bạn có thể chỉ định
-
int int Count = 0;
(không cần thiết vì số mặc định là 0)
-
String logfile = 'log.txt';
-
hoặc thậm chí
double sinPIDiv2 = Math.sin (Math.PI / 2);
.
Mặc dù bạn có thể khởi tạo instance field thông qua gán trực tiếp, nhưng việc thực hiện khởi tạo này trong một constructor sẽ phổ biến hơn, điều này mình sẽ trình bày sau.
Ngược lại, một class field (đặc biệt là class constant) thường được khởi tạo thông qua việc gán trực tiếp một biểu thức cho trường.
Hiểu sơ qua một chút về Lifetime và Scope trong lập trình Java
Một instance field được sinh ra khi đối tượng của nó được tạo và chết khi đối tượng được garbage collected (thu gom rác).
Một class field được sinh ra khi class được tải và chết khi class không được tải hoặc khi ứng dụng kết thúc.
=> Đây được gọi là Lifetime
Instance field và class field có thể truy cập được từ các khai báo của chúng cho đến cuối các class khai báo của chúng.
Các instance field chỉ có thể truy cập được vào mã bên ngoài trong ngữ cảnh đối tượng (object context)
Các class field chỉ có thể truy cập được vào mã bên ngoài trong ngữ cảnh đối tượng (object context) và class (class context) khi được cấp các mức truy cập phù hợp.
=> Đây được gọi là Scope
> Bạn cũng sẽ được tìm hiểu kỹ hơn về Lifetime và Scope trong KHÓA HỌC LẬP TRÌNH JAVA tại NIIT - ICT Hà Nội. Nhận lịch học ngay!
3. Tìm hiểu về Method: Mô tả hành vi của đối tượng
Ngoài việc mô hình hóa trạng thái của một thực thể trong thế giới thực, một class còn mô hình hóa các hành vi của nó => đây được gọi là method (Phương thức)
Ví dụ: Xe ô tô có thể di chuyển và tài khoản tín dụng có thể gửi tiền và rút tiền.
Một class cũng có thể bao gồm các hành vi phi thực thể. Một khai báo phương thức có cú pháp sau:
Cú pháp khai báo phương thức (method)
[static] returnType identifier ( [parameterList] )
{
// method body
}
Một khai báo phương thức tùy ý bắt đầu bằng từ khóa static (đối với hành vi không phải là thực thể) và tiếp tục với returnType, theo sau là một định danh (indentifier) không dành riêng ddeerr đặt tên cho phương thức.
Tên sau đó được theo sau bởi một tham số tùy chọn được phân định bằng dấu ngoặc tròn.
Phần body được phân tách bằng dấu ngoặc nhọn chứa mã để thực thi khi phương thức được gọi.
Return Type (hay còn gọi là kiểu trả về) xác định loại giá trị được trả về từ phương thức thông qua câu lệnh return (phần này chúng ta sẽ được tìm hiểu sau).
Ví dụ, nếu một phương thức trả về các chuỗi, kiểu trả về của nó sẽ được đặt thành String. Khi một phương thức không trả về một giá trị, kiểu trả về của nó được đặt thành void.
Parameter list (danh sách tham số) là danh sách khai báo tham số được phân tách bằng dấu phẩy.
Mỗi khai báo bao gồm một loại được theo sau bởi một định danh không dành riêng đặt tên tham số.
Tham số là một biến nhận một đối số (argument) (một giá trị biểu thức có kiểu tương thích với tham số tương ứng của nó) khi một phương thức hoặc hàm tạo được gọi.
Một tham số là cục bộ đối với phương thức hoặc constructor của nó. Nó xuất hiện khi phương thức hoặc hàm tạo được gọi và biến mất khi phương thức hoặc constructor trả về cho người gọi nó.
Nói cách khác, thời gian tồn tại của nó là phụ thuộc vào phương thức thực thi. Một tham số có thể được truy cập bởi bất kỳ mã nào trong phương thức. Phạm vi (Scope) của nó là toàn bộ phương thức.
Ví dụ khai báo bốn phương thức trong class Book:
class Book
{
// ...
String getTitle()
{
return title;
}
int getPubYear()
{
return pubYear;
}
void setTitle(String _title)
{
title = _title;
}
void setPubYear(int _pubYear)
{
pubYear = _pubYear;
}
}
Các phương thức getTitle()
và getPubYear()
trả về các giá trị của các trường tương ứng của chúng.
Chúng sử dụng câu lệnh return để trả về các giá trị này cho người gọi. Lưu ý rằng kiểu biểu thức của câu lệnh này phải tương thích với kiểu trả về của phương thức.
Các phương thức setTitle()
và setPubYear()
cho phép bạn đặt các giá trị của các trường title và pubYear.
Các loại trả về của họ được đặt là void để chỉ ra rằng chúng không trả lại bất kỳ giá trị nào cho khi được gọi đến.
Tất cả bốn phương thức được gọi là phương thức cá thể (instance method) vì chúng chỉ ảnh hưởng đến các đối tượng mà chúng được gọi.
Ghi chú: Cùng hiểu sơ qua một chút về Setter và Getter:
Tiền tố 'set' xác định setTitle()
và setPubYear()
là các phương thức setter (setter methods), nghĩa là chúng sẽ thiết lập các giá trị.
Tương tự, tiền tố 'get' xác định getTitle()
và getPubYear()
là các phương thức getter (getter methods), có nghĩa là chúng sẽ lấy các giá trị.
Nếu bạn đang thắc mắc về lý do sử dụng các phương thức setter / getter thay vì truy cập trực tiếp vào title và pubYear, thì bạn có thể đọc kỹ hơn về chủ đề này tại Stackoverflow.
Quay trở lại với ví dụ ở trên.
Các phương thức getTitle()
, getPubYear()
, setTitle()
và setPubYear()
ảnh hưởng đến các bản sao của một đối tượng của các trường title và pubYear.
Tuy nhiên, có thể bạn sẽ muốn muốn khai báo một phương thức độc lập với bất kỳ cuốn sách cụ thể nào.
Ví dụ: Bạn có thể muốn giới thiệu một phương thức xuất ra số lượng đối tượng Sách, như sau:
class Book
{
// ...
static void showCount()
{
System.out.println("count = " + count);
}
}
Ví dụ này khai báo một phương thức showCount()
sẽ xuất giá trị của trường count
.
Khai báo bắt đầu bằng từ khóa static để chỉ ra rằng phương thức này thuộc về class và không thể truy cập ở trạng thái đối tượng riêng lẻ, không có đối tượng cần phải được tạo ra.
Vì lý do này, showCount()
được gọi là một class method.
Tìm hiểu về biến cục bộ: Local Variables
Trong một phương thức hoặc constructor, bạn có thể khai báo các biến bổ sung như là một phần của việc triển khai (implementation) nó.
Các biến này được gọi là biến cục bộ (Local variables).
Chúng chỉ tồn tại trong khi phương thức hoặc hàm tạo đang thực thi và không thể được truy cập từ bên ngoài phương thức / constructor đó. Hãy xem xét ví dụ sau:
static void average(double[] values)
{
double sum = 0.0;
for (int i = 0; i < values.length; i++)
sum += values[i];
System.out.println("Average: " + (sum / values.length));
}
Trong ví dụ này khai báo một phương thức có tên là average
để tính toán và xuất ra mức trung bình của một mảng double.
Ngoài các giá trị tham số, nó khai báo biến cục bộ sum
và i
để giúp tính toán.
-
Lifetime của biến
sum
là từ điểm khai báo đến khi phương thức thực thi xong.
-
Phạm vi của biến
sum
là từ đầu đến cuối phương thức Average.
-
Lifetime của
i
và phạm vi bị giới hạn ở trong vòng lặp for.
Lifetime và Scope của một biến cục bộ được giới hạn trong block mà nó được khai báo, cũng như các block con.
Đây là lý do tại sao biến i
không thể truy cập bên ngoài vòng lặp for.
Hãy xem xét ví dụ sau đây:
{
int i;
{
i = 1; // OK: Vẫn thấy i
}
}
i = 2; // Lỗi i không tồn tại
Phương thức nạp chồng: Method Overloading
Java cho phép bạn khai báo các phương thức có cùng tên nhưng với các danh sách tham số khác nhau trong cùng một class. Tính năng này được gọi là method overloading.
Khi trình biên dịch gặp một biểu thức gọi đến method, nó so sánh danh sách các đối số được phân tách bằng dấu phẩy của method với mỗi danh sách tham số của method overloading vì nó cần tìm phương thức đúng để gọi.
Hai method cùng tên bị overload khi danh sách tham số của chúng khác nhau về số lượng hoặc thứ tự tham số.
Ngoài ra, hai phương thức cùng tên cũng bị overload khi có ít nhất một tham số khác nhau về kiểu.
Ví dụ, hãy xem xét bốn phương thức draw()
sau đây, method vẽ một hình dạng hoặc chuỗi tại vị trí vẽ hiện tại hoặc được chỉ định:
void draw(Shape shape)
{
// drawing code
}
void draw(Shape shape, double x, double y)
{
// drawing code
}
void draw(String string)
{
// drawing code
}
void draw(String string, double x, double y)
{
// drawing code
}
Khi trình biên dịch gặp draw('abc');
, nó sẽ chọn phương thức thứ ba vì nó cung cấp một danh sách tham số phù hợp.
Tuy nhiên, trình biên dịch sẽ làm gì khi nó gặp draw(null, 10, 20);
?
Nó sẽ báo cáo một thông báo lỗi 'reference to draw is ambiguous'
bởi vì có hai method để chọn.
Bạn không thể overload một method bằng cách chỉ thay đổi kiểu trả về.
Ví dụ:
Bạn không thể chỉ định int add(int x, int y)
và double add(int x, int y)
Vì trình biên dịch không có đủ thông tin để phân biệt giữa các method này khi nó gặp add(4, 5);
trong tập lệnh. Trình biên dịch sẽ báo cáo lỗi 'redefinition
' (Định nghĩa lại).
Tìm hiểu về câu lệnh Return trong Java
Đôi khi, một method phải chấm dứt thực hiện trước khi kết thúc.
Ví dụ:
Method vẽ một pixel có thể phát hiện tọa độ âm. Các method khác cần trả về một giá trị cho người gọi chúng.
Đối với cả hai tình huống, Java cung cấp câu lệnh return để chấm dứt thực thi method và điều khiển trả về cho người gọi method. Câu lệnh này có cú pháp sau:
return [ expression ] ;
Bạn có thể chỉ định trả về mà không cần biểu thức để thoát sớm một method hoặc một constructor.
Ví dụ:
Một method tên là copy()
sao chép các byte từ luồng đầu vào tiêu chuẩn (thu được thông qua các cuộc gọi method System.in.read()
) đến luồng đầu ra tiêu chuẩn (thông qua các cuộc gọi method System.out.print ()
). Phương thứ này được hiển thị dưới đây:
static void copy() throws java.io.IOException // Chúng ta sẽ
{ // thảo luận sau.
while (true)
{
int _byte = System.in.read();
if (_byte == -1)
return;
System.out.print((char) _byte);
}
}
System.in.read()
đọc byte từ luồng đầu vào tiêu chuẩn, mặc định cho bàn phím nhưng có thể được chuyển hướng đến một tệp.
Khi luồng được chuyển hướng đến một tệp, -1
được trả về khi không còn byte nào để đọc.
Khi copy()
phát hiện tình huống này, nó sẽ thực hiện quay trở lại từ vòng lặp vô hạn cho người gọi của nó.
Bạn có thể chỉ định return với một biểu thức để trả về một giá trị cho người gọi method. (Constructor không hỗ trợ phiên bản return này vì chúng không có kiểu trả về.)
Ví dụ: Bạn có thể quay lại sớm từ một phương thức đang tìm kiếm một giá trị mảng cụ thể khi tìm thấy giá trị.
Kịch bản này được thể hiện trong ví dụ sau:
static int search(int[] values, int srchValue)
{
for (int i = 0; i < values.length; i++)
if (values[i] == srchValue)
return i; // Trả về index giá trị tìm thấy
return -1; // Không tìm thấy giá trị
}
4. Tìm hiểu về Constructor: Initializing Objects
Cũng như việc gán giá trị rõ ràng cho các trường, một class có thể khai báo một hoặc nhiều block code để khởi tạo đối tượng rộng hơn.
Mỗi block code là một constructor.
Khai báo constructor bao gồm:
-
Một tiêu đề (Bao gồm tên class (Constructor thì không có tên riêng), sau đó là danh sách tham số tùy chọn)
-
Một body được phân định bằng dấu ngoặc nhọn.
Cú pháp khai báo một constructor như sau:
className ( [parameterList] )
{
// Phần thân constructor
}
ClassName phải khớp với tên của class trong đó constructor được khai báo. parameterList là một danh sách các tham số được phân tách bằng dấu phẩy.
Một body được phân tách bằng dấu ngoặc nhọn chứa mã để thực thi khi hàm constructor được gọi.
Không giống như một method, hàm constructor không có kiểu trả về vì nó không trả về bất kỳ giá trị nào.
Ghi chú: Hàm Constructor mặc định không đối số
Khi một class không khai báo bất kỳ hàm constructor nào, trình biên dịch sẽ tạo một hàm constructor mặc định không có đối số. Constructor mặc định này không làm gì cả.
Ví dụ sau khai báo một hàm constuctor trong class Book
. Hàm constructor khởi tạo các trường title
và pubYear
của đối tượng Book cho các đối số được truyền cho các tham số _title
và _pubYear
của hàm constructor khi đối tượng được tạo. Hàm tạo cũng tăng trường count
:
class Book
{
// ...
Book(String _title, int _pubYear)
{
title = _title;
pubYear = _pubYear;
++count;
}
// ...
}
Các tên tham số có dấu gạch dưới hàng đầu để ngăn chặn sự cố với các bài tập.
Ví dụ: nếu bạn đổi tên _title
thành title
và chỉ định title = title;
, bạn sẽ chỉ gán giá trị của tham số cho tham số, không thực hiện được gì. Tuy nhiên, bạn có thể tránh vấn đề này bằng cách thêm tiền tố this
trước tên của trường này.
class Book
{
// ...
Book(String title, int pubYear)
{
this.title = title;
this.pubYear = pubYear;
++count;
}
// ...
void setTitle(String title)
{
this.title = title;
}
void setPubYear(int pubYear)
{
this.pubYear = pubYear;
}
// ...
}
Từ khóa this
đại diện cho đối tượng hiện tại (thực sự, tham chiếu của nó). Thêm this
đến tên trường sẽ truy cập tên trường thay vì tham số cùng tên.
Ghi chú: Shadowing fields
Một tham số hoặc tên biến cục bộ có thể phủ bóng một tên class field. Bạn có thể sử dụng this trước tên class field để truy cập vào trường này.
Mặc dù bạn có thể khởi tạo các trường như title và pubYear thông qua các ví dụ được hiển thị ở trên, nhưng tốt nhất là thực hiện các ví dụ thông qua các phương thức setter như setTitle()
và setPubYear()
, như được trình bày dưới đây:
class Book
{
// ...
Book(String title, int pubYear)
{
setTitle(title);
setPubYear(pubYear);
++count;
}
// ...
}
Lưu ý rằng trong tương lai các method này có thể thực hiện các tác vụ khởi tạo bổ sung. Tại sao duplicate mã này trong hàm tạo?
Tìm hiểu về Constructor Calling
Các lớp có thể khai báo nhiều constructor.
Ví dụ: Một constructor Book
chỉ chấp nhận đối số title
và pubYear
là -1
để chỉ ra rằng năm xuất bản không xác định.
Hàm constructor bổ sung này cùng với hàm constructor ban đầu được hiển thị bên dưới:
class Book
{
// ...
Book(String title)
{
setTitle(title);
setPubYear(-1);
++count;
}
Book(String title, int pubYear)
{
setTitle(title);
setPubYear(pubYear);
++count;
}
// ...
}
Nhưng có một vấn đề với hàm constructor mới này: nó sao chép mã (setTitle (title);
) nằm trong hàm constructor hiện có.
Code trùng lặp là không cần thiết cho class.
Java cung cấp một cách để tránh sự trùng lặp này bằng cách cung cấp cú pháp this()
để có một hàm constructor gọi một hàm khác:
class Book
{
// ...
Book(String title)
{
this(title, -1);
// Không cần thêm ++count;
// vì nó đã có sẵn
}
Book(String title, int pubYear)
{
setTitle(title);
setPubYear(pubYear);
++count;
}
// ...
}
Hàm constructor đầu tiên sử dụng từ khóa this theo sau là danh sách đối số được đặt dấu ngoặc để gọi hàm constructor thứ hai.
Giá trị tham số duy nhất được truyền không thay đổi làm đối số thứ nhất và -1
được truyền dưới dạng đối số thứ hai.
Khi sử dụng this()
, hãy nhớ rằng nó phải là đoạn mã đầu tiên trong hàm constructor. Nếu không trình biên dịch sẽ báo lỗi.
5. Tìm hiểu về Object: Làm việc với class Instance
Một khi bạn đã khai báo một class, bạn có thể tạo các đối tượng (object) từ nó. Một đối tượng chính là thể hiện của một class.
Ví dụ, class Book đã được khai báo, bạn có thể tạo một hoặc nhiều đối tượng Book khác.
Hoàn thành nhiệm vụ này bằng cách chỉ định toán tử new theo sau bởi hàm constructor Book, như sau:
Book book = new Book("A Tale of Two Cities", 1859);
new sẽ load Book vào bộ nhớ và sau đó gọi hàm tạo của nó với các đối số 'A Tale of Two Cities'
và 1859
.
Đối tượng được khởi tạo cho các giá trị này. Khi hàm constructor trả về từ thực thi của nó, new
sẽ trả về một tham chiếu (một loại con trỏ tới một đối tượng) cho đối tượng Book
mới được khởi tạo.
Tham chiếu này sau đó được gán cho biến book
(thể hiện)
Ghi chú: Kiểu trả về của constructor
Nếu bạn đã tự hỏi tại sao một hàm constructor không có kiểu trả về, thì câu trả lời là không có cách nào để trả về giá trị của hàm constructor.
Sau tất cả, toán tử new đã trả về một tham chiếu đến đối tượng vừa được tạo.
Sau khi tạo một đối tượng Book, bạn có thể gọi các phương thức getTitle()
và getPubYear()
của nó để trả về các giá trị instance field.
Ngoài ra, bạn có thể gọi setTitle()
và setPubYear()
để đặt giá trị mới.
Trong cả hai trường hợp, bạn sử dụng toán tử dot truy cập (.
) Với tham chiếu book
để hoàn thành nhiệm vụ này.
System.out.println(book.getTitle()); // Kết quả: A Tale of Two Cities
System.out.println(book.getPubYear()); // Kết quả: 1859
book.setTitle("Moby Dick");
book.setPubYear(1851);
System.out.println(book.getTitle()); // Két quả: Moby Dick
System.out.println(book.getPubYear()); // Kết quả: 1851
Ghi chú: Messaging objects
Gọi một method trên một đối tượng tương đương với việc gửi một thông điệp đến đối tượng. Tên của method và các đối số của nó được khái niệm hóa như một thông điệp đang được gửi đến đối tượng mà method được gọi.
Bạn không phải tạo bất kỳ đối tượng Book nào để gọi các class method.
Thay vào đó, bạn gọi bằng cách như sau:
Book.showCount(); // Kết quả: count = 1
Cuối cùng, có thể get và set các giá trị của Book instance và class filed. Sử dụng một object reference để truy cập vào một trường đối tượng và class name để truy cập vào một class field:
System.out.println(book.title); // Kết quả: Moby Dick
System.out.println(Book.count); // Kết quả: 1
book.pubYear = 2015;
System.out.println(book.pubYear); // Kết quả: 2015
Trước đây mình đã đề cập rằng các phương thức cá thể chỉ ảnh hưởng đến các đối tượng mà chúng được gọi. Chúng không ảnh hưởng đến các đối tượng khác.
Ví dụ sau củng cố sự thật này bằng cách tạo hai đối tượng Book và sau đó truy cập vào tiêu đề của từng đối tượng, sau đó là ouput:
Book book1 = new Book("A Tale of Two Cities", 1859);
Book book2 = new Book("Moby Dick", 1851);
Book book3 = new Book("Unknown");
System.out.println(book1.getTitle()); // Kết quả: A Tale of Two Cities
System.out.println(book2.getTitle()); // Kết quả: Moby Dick
System.out.println(book3.getPubYear()); // Kết quả: -1
Book.showCount(); // Kết quả: count = 3
Infomation hiding và access level
Phần body của một class bao gồm interface và implementation.
Interface là một phần của class có thể truy cập được từ bên ngoài class.
Implementation là một phần của class tồn tại để hỗ trợ giao diện. Implementation nên được ẩn đi để có thể thay đổi để đáp ứng các yêu cầu lập trình.
Hãy xem xét class Book
. Constructor và các method header tạo thành interface của class này.
Code trong các hàm constructor và method, và các trường khác nhau là một phần của implementation.
Không cần phải truy cập vào các trường này vì chúng có thể được đọc hoặc ghi thông qua các phương thức getter và setter.
Tuy nhiên, vì không có biện pháp phòng ngừa nào được thực hiện, nên có thể truy cập trực tiếp vào các trường này.
Bằng cách sử dụng biến tham chiếu book
trước đó, bạn có thể chỉ định book.title
và book.pubYear
và điều đó là ok với trình biên dịch Java.
Để ngăn truy cập vào các trường này (hoặc ít nhất là xác định ai có thể truy cập chúng), bạn cần tận dụng các cấp truy cập (access level).
-
private: Chỉ mã trong cùng một class với thành viên có thể truy cập thành viên.
-
public: Bất kỳ mã nào trong bất kỳ class nào trong bất kỳ package nào cũng có thể truy cập thành viên.
-
protected: Bất kỳ mã nào trong cùng một class hoặc các class con của nó đều có thể truy cập vào thành viên.
Nếu không có từ khóa thì sẽ được ngầm hiểu là Package access. Package access tương tự như public access.
Tuy nhiên, không giống như public access, mã phải được đặt trong một class thuộc cùng một package.
Bạn có thể ngăn mã bên ngoài truy cập vào các trường title
và pubYear
của Book
để mọi nỗ lực truy cập các trường này từ ngoài Book
sẽ dẫn đến thông báo lỗi trình biên dịch.
Làm việc này bằng cách đặt private
cho các khai báo của chúng, như được làm giống bên dưới đây:
class Book
{
// fields
private String title;
private int pubYear; // Năm xuất bản
// ...
}
Để cho bạn biết lý do tại sao nên ẩn quyền truy cập vào các trường, mình đã tạo một ví dụ khác mở rộng lớp Book để thêm trường author, phương thức getter / setter để truy cập trường này và một hàm constructor khác cho phép bạn cũng chỉ định tên tác giả.
Ví dụ sau đây cho thấy các phần đã sửa đổi của class Book đã cập nhật:
class Book
{
// ...
private String author;
// ...
Book(String title, int pubYear)
{
this(title, pubYear, "");
}
Book(String title, int pubYear, String author)
{
setTitle(title);
setPubYear(pubYear);
setAuthor(author);
++count;
}
// ...
String getAuthor()
{
return author;
}
// ...
void setAuthor(String author)
{
this.author = author;
}
// ...
}
Nếu bạn nhớ lại, hàm constructor Book(String title)
thực thi this(title, -1);
để tránh trùng lặp code.
Tương tự, mình đã thiết kế Book(String title, int pubYear)
để thực thi this(title, pubYear, '');
, gọi hàm constructor mới nhất, để tránh duplicate.
Bạn có thể dễ dàng tạo một đối tượng Book mới bao gồm tên tác giả và lấy tên này thông qua phương thức getAuthor()
:
Book book = new Book("A Tale of Two Cities", 1859, "Charles Dickens");
System.out.println(book.getAuthor()); // Kết quả: Charles Dickens
Hãy xem xét rằng nâng cấp class Book không cho phép nhiều tác giả.
Bạn không thể thay đổi các header của hàm constructor hoặc getAuthor()
/ setAuthor()
để thực hiện điều chỉnh này vì làm như vậy sẽ thay đổi interface và khiến code liên quan đến interface ở bên ngoài ngừng hoạt động.
Thay vào đó, bạn sẽ cần phải làm lại như sau:
class Book
{
// ...
private String[] authors;
// ...
Book(String title, int pubYear, String author)
{
this(title, pubYear, new String[] { author });
}
Book(String title, int pubYear, String[] authors)
{
setTitle(title);
setPubYear(pubYear);
setAuthors(authors);
++count;
}
// ...
String getAuthor()
{
return authors[0];
}
String[] getAuthors()
{
return authors;
}
// ...
void setAuthor(String author)
{
setAuthors(new String[] { author });
}
void setAuthors(String[] authors)
{
this.authors = authors;
}
// ...
}
Mình đã thay đổi trường author
trước đó thành một trường authors
. Ngoài ra, mình đã thay đổi kiểu của nó từ kiểu String
sang kiểu mảng String[]
.
Mình đã sửa đổi hàm constructor của Book(String title, int pubYear, String Author)
để gọi hàm constructor Book(String title, int pubYear, String[] Author)
.
Vì tham số thứ ba của hàm constructor đó là kiểu String[]
, nên lệnh gọi this()
chuyển đổi đối số String author
của nó thành một mảng single - element bao gồm chính nó thông qua new String[] { author }
.
Toán tử new được sử dụng để tạo mảng single - element String và gán tham chiếu chuỗi author cho phần tử này.
Phương thức getAuthor()
ban đầu truy cập phần tử đầu tiên trong mảng authors.
Mình đã sửa đổi phương thức setAuthor()
ban đầu để chuyển đổi đối số đơn String
thành mảng single - element String, sau đó gọi setAuthors()
với mảng này làm đối số.
Cuối cùng, mình đã thêm các phương thức getAuthors()
và setAuthors()
mới để trả về tham chiếu mảng authors
hoặc gán tham chiếu mới cho authors
Bạn có thể tạo một đối tượng Book
mới bao gồm nhiều tên tác giả bằng cách gọi hàm constructor mới.
Sau đó, bạn chỉ có thể nhận được tên đầu tiên thông qua getAuthor()
hoặc có được tất cả các tên tác giả qua getAuthors()
:
Book book = new Book("Grimms' Fairy Tales", 1812,
new String[] { "Jacob Grimm", "Wilhelm Grimm" });
System.out.println(book.getTitle()); // Kết quả: Grimms' Fairy Tales
System.out.println(book.getPubYear()); // Kết quả: 1812
System.out.println(book.getAuthor()); // Kết quả: Jacob Grimm
String[] authors = book.getAuthors();
for (int i = 0; i < authors.length; i++)
System.out.println(authors[i]); // Kết quả: Jacob Grimm Wilhelm Grimm (on successive lines)
Nếu trường author
ban đầu không được đánh dấu là private
, bạn sẽ không thể mở rộng (extends
) Book
theo cách này.
Hơn nữa, các mã bên ngoài sẽ có thể truy cập trực tiếp vào trường này (ví dụ: book.author
) và sẽ bị lỗi khi bạn thay đổi tên trường.
Cách ẩn các hàm Constructor và Method
Cũng giống như việc ẩn các trường, bạn có thể cần ẩn các hàm constructor hoặc các method. Bạn thường ẩn một hàm constructor để ngăn lớp utility class được khởi tạo.
Và bạn cũng thường ẩn một method khi nó chỉ tồn tại để phục vụ một medhod khác (hoặc một hàm constructor). Một phương pháp như vậy được gọi là một helper method. Dưới đây là một ví dụ:
// Tính giai thừa của n như n! / (n - r)!.
public long perm(long n)
{
return fact(n) / fact(n - r);
}
// fact() bị ẩn
private long fact(long n)
{
// Code tính toán và trả về n!
}
TỔNG KẾT
Như bạn đã được hướng dẫn trong bài viết này, một ứng dụng object-based được đóng gói trạng thái và hành vi bên trong các class và object.
Ngược lại, một ứng dụng Java hướng đối tượng cũng hỗ trợ kế thừa (inheritance).
Bài viết tiếp theo của mình sẽ tập trung vào lập trình với sự kế thừa. Trong thời gian chờ đợi, bạn có thể xem qua Series Học Java trong 7 ngày nếu bạn muốn.
Hi vọng sẽ giúp bạn học lập trình Java căn bản vững chắc hơn.
---
HỌC VIỆN ĐÀO TẠO CNTT NIIT - ICT HÀ NỘI
Học Lập trình chất lượng cao (Since 2002). Học thực tế + Tuyển dụng ngay!
Đc: Tầng 3, 25T2, N05, Nguyễn Thị Thập, Cầu Giấy, Hà Nội
SĐT: 02435574074 - 0383.180086
Email: hello@niithanoi.edu.vn
Fanpage: https://facebook.com/NIIT.ICT/
#niit #niithanoi #niiticthanoi #hoclaptrinh #khoahoclaptrinh #hoclaptrinhjava #hoclaptrinhphp #python #java #php