lập trình java cơ bản đến nâng cao
TRANSCRIPT
Lập trình Java cơ bản đến nâng cao (phần 2)
Bài 4 – Chia hết, chia lấy dư
*Lí thuyết: một số kiểu biến trong Java
Bạn đã biết 2 kiểu String (chuỗi) và int (nguyên) bây giờ bạn biết thêm kiểu float (thực)
Số nguyên và số thực bạn biết sự khác nhau rồi chứ. Bây giờ ta bắt đầu bài toán ví dụ
?1234567891011121314
</div><div>HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");float a = Float.parseFloat(in.readLine());System.out.print("Nhap b: ");float b = Float.parseFloat(in.readLine());float ketqua = a/b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}
Bạn thử bài toán xem, nhớ đừng nhập số b=0 nhé, chuyện ấy sẽ xử lí sau.
Ví dụ nhập a=5, b=2, kết quả in ra sẽ là 2.5, thú vị phải không ?
Bây giờ cũng bài toán ấy, bạn thay đổi như sau
?123456789101112
</div><div>HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");int a = Integer.parseInt(in.readLine());System.out.print("Nhap b: ");int b = Integer.parseInt(in.readLine());float ketqua = a/b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}</div>
131415
<div>
Cũng nhập a=5, b=2, lần này kết quả in ra là … 2
Phép chia sẽ là phép chia hết nếu cả 2 toán hạng đều kiểu nguyên, gọi là chia lấy nguyên (/) hay div
Bây giờ cũng chương trình ấy mà ta thay đổi lại chút xíu xem sao
?12345678910111213
HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");int a = Integer.parseInt(in.readLine());System.out.print("Nhap b: ");int b = Integer.parseInt(in.readLine());float ketqua = a%b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}
Cũng nhập a=5, b=2, lần này kết quả in ra là … 1
Đây là kết quả phép chia lấy dư 5 chia cho 2, gọi là chia lấy dư (%) hay mod
*Thế nếu tôi muốn 2 số nguyên chia nhau mà ra kiểu thực chứ không phải phép chia lấy nguyên thì sao ?
Trong trường hợp đó, bạn dùng “ép kiểu”
?1234
</div><div>int a=5,b=2;float ket qua;ketqua=(float)a/b;</div><div>
Cấu trúc một chương trình JavaSau đây chúng tôi xin chia sẻ với bạn đọc cấu trúc một chương trình Java
Ví dụ như một bài văn, ta sẽ có cấu trúc bao gồm:
- Mở bài
- Thân bài
- Kết luận
Các ngôn ngữ lập trình nói chung cũng sẽ có cấu trúc tương tự:
- Khai báo: chứa các khai báo về tài nguyên sẽ sử dụng
- Thân chương trình: là phần chính, bao gồm các hàm, các lệnh mô tả công việc sẽđược thực hiện.
- Giải phóng bộ nhớ
Đó là nói chung, tùy vào từng ngôn ngữ cụ thể sẽ có các cấu trúc khác nhau.
Đây là một ví dụ của chương trình C++
?1234567
int main () { cout << " Hello World ";
return 0; }
Còn đây là chương trình tương tự viết bằng Java
?123456
public class HelloWorld { public static void main(String[] args) {System.out.println("Hello World");}}
Phần xanh có thể coi là phần khai báo, phần đỏ có thể coi như là phần thân chương trình.
Tạm thời chưa bàn tới việc giải phóng bộ nhớ.
Nói chung, một chương trình Java được chia thành các lớp (class) riêng biệt.
Thật ra ở đây luôn có sự lẫn lộn và mơ hồ cho các bạn mới làm quen với Java.
Nhiều người trong chúng ta đã quen thuộc với hệ thống Thư mục và Tập tin của Windows.
Một chương trình Java có thể chỉ gồm có một File cũng có thể gồm nhiều File khác nhau.
Các file này có thể nằm cùng Folder, cũng có thể nằm trong các Folder khác nhau.
Class như một chương trong File truyện của bạn.
Bạn có thể viết nhiều truyện và lưu trong vài Folder khác nhau.
Mỗi Folder như vậy gọi là một gói hay Package.
Tóm lại:
-Một chương trình Java có thể bao gồm một hay nhiều Package.
-Mỗi package sẽ có một hay nhiều File.
-Trong mỗi File sẽ có một hay nhiều Class.
Nếu muốn sử dụng “bất cứ cái gì” ở Package khác, chúng ta phải dùng phát biểu “import”
Dạng cơ bản của một lớp được xác định như sau :
?1234567
class classname{. . .memberVariableDeclarations. . .methodDeclarations. . .}
Ví dụ:
?12345678910
Class classname{
int num1,num2; // Khai báo biến với các dấu phảy giữa các biến Show() { // Method trong Class
statement (s); // Kết thúc bởi dấu chấm phảy
}}
Lớp (Class) trong Java:
Trong Java, một lớp (class) là một tập hợp các attribute và các phương thức (method-còn gọi là các hàm
thành viên). Lớp là cơ sở để tạo để tạo ra các đối tượng (Object). Ví dụ về cái xe chẳng hạn: Lớp (class) xe sẽ
có những attribute như số chỗ ngồi, màu sắc…và các method như chạy, phanh, dừng…
Hay như trong dạng cơ bản phía trên num1, num2 là attributes còn Show() là một Method.
Thừa kế và trừu tượng hóa trong JavaThừa kế là gì?
Các lớp trong mã Java tồn tại trong một hệ thống thứ bậc. Các lớp ở bậc trên một lớp đã cho trong một hệ
thống thứ bậc là các lớp bậc trên (superclasses) của lớp đó. Lớp cụ thể đó là một lớp con (subclass) của tất cả
các lớp bậc cao hơn. Một lớp con thừa kế từ các lớp bậc trên của nó. Lớp Object ở trên đỉnh của mọi hệ thống
thứ bậc các lớp. Nói cách khác, mọi lớp là một lớp con của (và thừa kế từ) Object.
Ví dụ, giả sử chúng ta có một lớp Adult trông như sau:
?123456789101112131415
public class Adult {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Adult() { }public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Lớp Adult của chúng ta kế thừa ngầm từ lớp Object. Điều này được thừa nhận cho mọi lớp, vì vậy bạn không
phải gõ extends Object trong định nghĩa lớp. Nhưng nói rằng lớp của chúng ta thừa kế từ (các) lớp bậc trên
của nó có nghĩa là gì? Nó đơn giản có nghĩa là lớp Adult có quyền truy cập vào các biến và các phương thức
đã được trưng ra (exposed) trong các lớp bậc trên của nó. Trong trường hợp này, nó muốn nói rằng lớp Adult
có thể thấy và sử dụng những phần sau đây từ bất kỳ các lớp bậc trên nào của nó (chúng ta chỉ có một vào lúc
này):
* Các biến và phương thức công khai (public).
* Các biến và phương thức công khai có bảo vệ (protected).
* Các biến và phương thức có bảo vệ theo gói (Package protected) (có nghĩa là, chúng không có từ đặc tả
(specifier) quyền truy cập), nếu lớp bậc trên ở trong cùng một gói như lớp Adult
Các hàm tạo là đặc biệt. Chúng không phải là các thành viên hướng đối tượng (OO) đã đủ lông cánh, do đó
chúng không được thừa kế.
Nếu một lớp con ghi đè một phương thức hay một biến từ lớp bậc trên — nói cách khác là khi lớp con triển
khai thực hiện một thành viên có cùng tên — thì nó che dấu thành viên của lớp bậc trên. Chính xác hơn, việc
ghi đè một biến sẽ che giấu nó và việc ghi đè một phương thức chỉ đơn giản ghi đè nó, nhưng có hiệu quả
tương tự nhau: thành viên bị ghi đè về cơ bản được ẩn đi. Bạn vẫn có thể truy cập đến được các thành viên
của lớp bậc trên bằng cách sử dụng từ khóa super:
?
1 super.hiddenMemberName
Trong trường hợp lớp Adult, tất cả những gì mà nó thừa kế vào lúc này là các phương thức về Object
(toString(), chẳng hạn). Do đó, các đoạn mã sau đây là hoàn toàn có thể chấp nhận được:
?12
Adult anAdult = new Adult();anAdult.toString();
Phương thức toString() không tồn tại rõ ràng trên lớp Adult, nhưng lớp Adult thừa kế nó.
Bạn hãy nên ghi nhớ rằng ở đây có các vụ “Tìm ra rồi” (gotchas- nói về việc phát hiện ra nguyên nhân của một
hành vi bất ngờ, không như mong đợi trong chương trình, tuy nhiên không phải là lỗi, vì tất cả đều đúng như
tài liệu hướng dẫn). Trước hết, rất dễ dàng đặt tên các biến và các phương thức trong một lớp con với tên
giống như các biến và các phương thức trong các lớp bậc trên của lớp đó, rồi sau đó bị lẫn lộn khi bạn không
thể gọi ra một phương thức được thừa kế. Hãy nhớ rằng, khi bạn đưa ra một phương thức có cùng tên trong
một lớp con giống như một lớp đã tồn tại trong một lớp bậc trên, bạn đã che giấu nó. Thứ hai, các hàm tạo
không được thừa kế, nhưng chúng được gọi ra. Có một lời gọi ngầm đến hàm tạo của lớp bậc trên trong bất
kỳ hàm tạo nào của lớp con được bạn viết và đó là điều đầu tiên mà hàm tạo của lớp con thực hiện. Bạn phải
sống chung với điều này; bạn không thể làm gì để thay đổi nó. Ví dụ, hàm tạo Adult của chúng ta thực sự trông
như dưới đây khi thực chạy, mặc dù chúng ta không gõ bất cứ cái gì trong phần thân:
?123
public Adult() {super();}
Dòng đó trong phần thân của hàm tạo sẽ gọi hàm tạo không có đối số của lớp bậc trên. Trong trường hợp này,
đó là hàm tạo của Object.
Định nghĩa một hệ thống thứ bậc các lớp
Giả chúng ta có một lớp có tên là Baby. Nó trong giống như thế này:
?12345678910111213141516
public class Baby {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Baby() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Các lớp Adult and Baby của chúng ta trông rất giống nhau. Trong thực tế, chúng hầu như đồng nhất. Việc sao
đúp mã như thế làm cho việc bảo trì mã thêm khó khăn hơn mức cần thiết. Chúng ta có thể tạo ra một lớp bậc
trên, di chuyển tất cả các phần tử chung lên lớp đó và loại bỏ phần mã sao đúp. Lớp bậc trên của chúng ta có
thể được đặt tên là Person và nó có thể trông giống như sau:
?123456789101112131415
public class Person {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0;public Person() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Bây giờ chúng ta có thể để cho Adult và Baby làm lớp con của Person, và làm cho hai lớp ấy thành khá đơn
giản vào lúc này:
?12345678
public class Adult {public Adult() {}}public class Baby {public Baby() {}}
Một khi chúng ta có hệ thống thứ bậc, chúng ta có thể tham chiếu một cá thể của mỗi lớp con như là một cá
thể của bất kỳ các lớp bậc trên nào của nó trong hệ thống thứ bậc. Ví dụ:
?12345
Adult anAdult = new Adult();System.out.println("anAdult is an Object: " + (Adult instanceof Object));System.out.println("anAdult is a Person: " + (Adult instanceof Person));System.out.println("anAdult is anAdult: " + (Adult instanceof Adult));System.out.println("anAdult is a Baby: " + (Adult instanceof Baby));
Mã này sẽ cho chúng ta ba kết quả đúng và một kết quả sai. Bạn cũng có thể ép kiểu (cast) một đối tượng
thành bất kỳ kiểu bậc cao hơn nào trong hệ thống thứ bậc của nó, như dưới đây:
?12
Adult anAdult = new Adult();Person aPerson = (Person) anAdult;aPerson.move();
3Mã này sẽ biên dịch mà không có vấn đề gì. Chúng ta có thể ép kiểu một Adult thành kiểu Person, sau đó gọi
một phương thức Person trên đó.
Do chúng ta có hệ thống thứ bậc này, nên mã trên các lớp con của chúng ta đơn giản hơn. Nhưng bạn có thấy
một vấn đề ở đây không? Bây giờ tất cả các Adult và và tất cả các Baby (thứ lỗi vì việc dùng từ số nhiều không
đúng) sẽ nói năng và đi lại theo cùng một cách. Chỉ có duy nhất một triển khai thực hiện cho mỗi hành vi. Đó
không phải là những gì mà chúng ta muốn, bởi vì những người trưởng thành không nói năng hoặc đi lại giống
như trẻ con. Chúng ta có thể ghi đè move() và talk() trong các lớp con, nhưng sau đó về cơ bản chúng ta
không sử dụng được hành vi “tiêu chuẩn” đã định nghĩa trong lớp bậc trên của chúng ta. Cái mà chúng ta thực
sự muốn là một cách để bắt buộc các lớp con của chúng ta đi lại và nói năng theo cách cụ thể riêng của
chúng. Đó là những gì mà các lớp trừu tượng sẽ làm.Định nghĩa một hệ thống thứ bậc các lớp
Giả chúng ta có một lớp có tên là Baby. Nó trong giống như thế này:
?12345678910111213141516
public class Baby {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Baby() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Các lớp Adult and Baby của chúng ta trông rất giống nhau. Trong thực tế, chúng hầu như đồng nhất. Việc sao
đúp mã như thế làm cho việc bảo trì mã thêm khó khăn hơn mức cần thiết. Chúng ta có thể tạo ra một lớp bậc
trên, di chuyển tất cả các phần tử chung lên lớp đó và loại bỏ phần mã sao đúp. Lớp bậc trên của chúng ta có
thể được đặt tên là Person và nó có thể trông giống như sau:
?12345678910
public class Person {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0;public Person() {}public void move() {System.out.println("Moved.");}public void talk() {
1112131415
System.out.println("Spoke.");}}
Bây giờ chúng ta có thể để cho Adult và Baby làm lớp con của Person, và làm cho hai lớp ấy thành khá đơn
giản vào lúc này:
?12345678
public class Adult {public Adult() {}}public class Baby {public Baby() {}}
Một khi chúng ta có hệ thống thứ bậc, chúng ta có thể tham chiếu một cá thể của mỗi lớp con như là một cá
thể của bất kỳ các lớp bậc trên nào của nó trong hệ thống thứ bậc. Ví dụ:
?12345
Adult anAdult = new Adult();System.out.println("anAdult is an Object: " + (Adult instanceof Object));System.out.println("anAdult is a Person: " + (Adult instanceof Person));System.out.println("anAdult is anAdult: " + (Adult instanceof Adult));System.out.println("anAdult is a Baby: " + (Adult instanceof Baby));
Mã này sẽ cho chúng ta ba kết quả đúng và một kết quả sai. Bạn cũng có thể ép kiểu (cast) một đối tượng
thành bất kỳ kiểu bậc cao hơn nào trong hệ thống thứ bậc của nó, như dưới đây:
?123
Adult anAdult = new Adult();Person aPerson = (Person) anAdult;aPerson.move();
Mã này sẽ biên dịch mà không có vấn đề gì. Chúng ta có thể ép kiểu một Adult thành kiểu Person, sau đó gọi
một phương thức Person trên đó.
Do chúng ta có hệ thống thứ bậc này, nên mã trên các lớp con của chúng ta đơn giản hơn. Nhưng bạn có thấy
một vấn đề ở đây không? Bây giờ tất cả các Adult và và tất cả các Baby (thứ lỗi vì việc dùng từ số nhiều không
đúng) sẽ nói năng và đi lại theo cùng một cách. Chỉ có duy nhất một triển khai thực hiện cho mỗi hành vi. Đó
không phải là những gì mà chúng ta muốn, bởi vì những người trưởng thành không nói năng hoặc đi lại giống
như trẻ con. Chúng ta có thể ghi đè move() và talk() trong các lớp con, nhưng sau đó về cơ bản chúng ta
không sử dụng được hành vi “tiêu chuẩn” đã định nghĩa trong lớp bậc trên của chúng ta. Cái mà chúng ta thực
sự muốn là một cách để bắt buộc các lớp con của chúng ta đi lại và nói năng theo cách cụ thể riêng của
chúng. Đó là những gì mà các lớp trừu tượng sẽ làm.
Trừu tượng hóa
Trong bối cảnh hướng đối tượng, trừu tượng hóa đề cập đến hoạt động tổng quát hóa dữ liệu và hành vi thành
một kiểu bậc cao hơn so với lớp hiện tại trong hệ thống thứ bậc. Khi bạn di chuyển các biến hoặc các phương
thức từ một lớp con vào một lớp bậc trên, bạn đang trừu tượng hóa các thành viên này.
Đó là các thuật ngữ chung và chúng cũng được áp dụng trong ngôn ngữ Java. Nhưng ngôn ngữ Java cũng bổ
sung thêm các khái niệm về các lớp trừu tượng và các phương thức trừu tượng. Lớp trừu tượng là một lớp
không cá thể hóa được. Ví dụ, bạn có thể tạo ra một lớp có tên là Animal. Việc tạo ra một cá thể từ lớp như
vậy sẽ không có ý nghĩa: Trong thực tế, bạn chỉ muốn tạo ra các cá thể của một lớp cụ thể như Dog. Nhưng
tất cả các Animal có một số điểm chung, chẳng hạn như khả năng kêu. Việc nói rằng một Animal kêu không
cho bạn biết gì nhiều. Tiếng kêu thế nào phụ thuộc vào loại động vật. Làm thế nào để mô hình hóa điều đó?
Bạn định nghĩa các điểm chung trong các lớp trừu tượng và bạn bắt buộc các lớp con triển khai thực hiện
hành vi cụ thể đặc thù cho loài của chúng.
Bạn có thể có cả lớp trừu tượng và lớp cụ thể trong hệ thống thứ bậc của bạn.
Sử dụng lớp trừu tượng
Lớp Person của chúng ta chứa một số phương thức hành vi mà chúng ta còn chưa biết là chúng ta cần hay
không. Hãy gỡ bỏ nó và bắt buộc các lớp con triển khai thực hiện hành vi đó một cách đa dạng. Chúng ta có
thể làm điều đó bằng cách định nghĩa các phương thức của Person là trừu tượng. Sau đó, các lớp con của
chúng ta sẽ phải triển khai thực hiện các phương thức đó.
?12345678910111213141516171819202122232425
public abstract class Person {...abstract void move();abstract void talk();}
public class Adult extends Person {public Adult() {}public void move() {System.out.println("Walked.");}public void talk() {System.out.println("Spoke.");}}
public class Baby extends Person {public Baby() {}public void move() {System.out.println("Crawled.");}public void talk() {System.out.println("Gurgled.");}}
2627
Chúng ta đã thực hiện những gì trong bản Listing này?
Chúng ta đã thay đổi Person làm cho các phương thức thành trừu tượng, bắt buộc các lớp con triển khai
thực hiện chúng.
Chúng ta làm cho Adult thành lớp con của Person và triển khai thực hiện các phương thức.
Chúng ta làm cho Baby thành lớp con của Person và triển khai thực hiện các phương thức.
Khi bạn khai báo một phương thức là trừu tượng, bạn cần các lớp con để thực hiện phương thức, hoặc
chính lớp con lại tiếp tục là trừu tượng và chuyển giao trách nhiệm triển khai thực hiện tới các lớp con bậc
dưới nữa. Bạn có thể thực hiện một số phương thức trên lớp trừu tượng và buộc các lớp con thực hiện những
phương thức khác. Điều này tùy thuộc vào bạn. Đơn giản chỉ cần khai báo những cái bạn không muốn triển
khai thực hiện là trừu tượng và không cung cấp phần thân của phương thức. Nếu một lớp con không thực
hiện một phương thức trừu tượng từ một lớp bậc trên, thì trình biên dịch sẽ báo lỗi.
Vì cả hai Adult và Baby đều là lớp con của Person, chúng ta có thể quy một cá thể của mỗi lớp là thuộc
kiểu Person.
Cấu trúc lại thành hành vi trừu tượng
Bây giờ chúng ta đã có lớp Person, Adult và Baby trong hệ thống thứ bậc của chúng ta. Giả chúng ta muốn
làm cho hai lớp con thực tế hơn bằng cách thay đổi các phương thức move() của chúng như sau:
?123456789101112131415
public class Adult extends Person {...public void move() {this.progress++;}...}
public class Baby extends Person {...public void move() {this.progress++;}...}
Bây giờ mỗi lớp cập nhật biến cá thể của nó để phản ánh một tiến triển nào đó đang được thực hiện mỗi khi
chúng ta gọi move(). Tuy nhiên, cần lưu ý rằng hành vi lại vẫn giống nhau. Cấu trúc lại mã để loại bỏ sao đúp
mã là có ý nghĩa. Việc cấu trúc lại thích hợp nhất là di chuyển move() tới Person.
Đúng, chúng ta đang thêm việc triển khai thực hiện phương thức quay trở lại lớp bậc trên mà chúng ta vừa lấy
nó ra. Đây là một ví dụ rất đơn giản, do đó việc chuyển qua chuyển lại này có vẻ quá lãng phí. Nhưng những
gì mà chúng ta vừa trải qua là phổ biến khi bạn viết mã Java. Bạn thường xuyên thấy các lớp và các phương
thức thay đổi khi hệ thống lớn lên và đôi khi bạn đi đến chỗ có sao đúp mã mà bạn có thể cấu trúc lại, đưa vào
các lớp bậc trên. Thậm chí bạn có thể làm điều đó, sau đó quyết định rằng đây là một sai lầm và đưa hành vi
quay trở lại xuống các lớp con. Đơn giản là có thể bạn không biết vị trí thích đáng của tất cả các hành vi tại
thời điểm bắt đầu quá trình phát triển. Bạn chỉ biết được vị trí thích đáng dành cho hành vi khi bạn tiến lên.
Hãy cấu trúc lại các lớp của chúng ta để đặt move() quay lại lớp bậc trên:
?1234567891011121314151617181920212223
public abstract class Person {...public void move() {this.progress++;}public abstract void talk();}
public class Adult extends Person {public Adult() {}public void talk() {System.out.println("Spoke.");}}
public class Baby extends Person {public Baby() {}public void talk() {System.out.println("Gurgled.");}}
Bây giờ các lớp con của chúng ta triển khai thực hiện các phiên bản khác nhau của talk(), nhưng chia sẻ cùng
một hành vi
Khi nào trừu tượng hóa… và khi nào thì không
Việc quyết định khi nào trừu tượng hóa (hay tạo một hệ thống thứ bậc) là một chủ đề tranh luận nóng trong
giới hướng đối tượng, đặc biệt là giữa các lập trình viên ngôn ngữ Java. Chắc chắn có rất ít các câu trả lời
đúng hoặc sai về cách làm thế nào để cấu trúc hệ thống thứ bậc các lớp. Đây là một lĩnh vực ở đó các nhà
thực hành có tay nghề và tận tâm có thể (và thường xuyên là) không đồng ý với nhau. Dù sao đi nữa, có một
số quy tắc ngón tay cái thích hợp phải theo đối với các hệ thống thứ bậc.
Thứ nhất, không trừu tượng hóa ngay từ đầu. Hãy đợi cho đến khi mã thông báo cho bạn rằng bạn nên trừu
tượng hóa. Cấu trúc lại trên đường bạn đi đến trừu tượng hóa luôn luôn là cách tốt hơn là giả định bạn cần nó
ngay ở lúc bắt đầu. Đừng giả định rằng bạn cần có một hệ thống thứ bậc. Nhiều lập trình viên Java lạm dụng
các hệ thống thứ bậc.
Thứ hai, chống lại việc sử dụng các lớp trừu tượng khi bạn có thể. Chúng không phải là xấu, chúng chỉ có các
hạn chế. Chúng ta thường sử dụng một lớp trừu tượng để bắt buộc các lớp con của chúng ta triển khai hành vi
nhất định nào đó. Một giao diện (mà chúng ta sẽ thảo luận trong phần Các giao diện ) có phải là một ý tưởng
tốt hơn không? Hoàn toàn có thể. Mã của bạn sẽ là dễ hiểu hơn nếu nó không tạo nên một hệ thống thứ bậc
phức tạp với một hỗn hợp các phương thức triển khai thực hiện và ghi đè. Bạn có thể có một phương thức
được định nghĩa xuyên qua ba hoặc bốn lớp, thành một dãy. Thời điểm mà bạn sử dụng nó trong một lớp con
của lớp con của lớp con của lớp con (sub-sub-sub-subclass), bạn có thể phải tìm kiếm lâu để phát hiện
phương thức này sẽ làm gì. Điều đó có thể làm nản lòng việc gỡ rối.
Thứ ba, hãy sử dụng một hệ thống thứ bậc và/hoặc các lớp trừu tượng khi làm như thế là đẹp. Có rất nhiều
mẫu mã lệnh sử dụng các khái niệm phương thức trừu tượng và lớp trừu tượng của ngôn ngữ Java, ví dụ như
mẫu phương thức khuôn mẫu Gang of Four
Thứ tư, hiểu được giá mà bạn phải trả khi bạn sử dụng một hệ thống thứ bậc một cánh vội vã. Nó thực sự có
thể dẫn bạn nhanh chóng rơi vào con đường sai lầm, bởi vì có các lớp ở đó rồi, được đặt tên như vậy, với các
phương thức mà chúng đã có, làm cho rất dễ dàng để cho rằng tất cả những thứ đó nên là như thế. Có lẽ rằng
hệ thống thứ bậc có ý nghĩa khi bạn tạo ra nó, nhưng nó có thể không có ý nghĩa gì hơn nữa. Sức ỳ có thể
chống lại các đổi thay.
Nói tóm lại, hãy khôn khéo khi sử dụng các hệ thống thứ bậc. Kinh nghiệm sẽ giúp bạn khôn ngoan hơn,
nhưng nó sẽ không làm cho bạn lúc nào cũng sáng suốt. Hãy nhớ cấu trúc lại.
Các giao diện trong lập trình JavaMột giao diện là gì?
Ngôn ngữ Java bao gồm khái niệm về một giao diện (interface), nó chỉ đơn giản là một tập hợp có tên của các
hành vi có sẵn công khai và/hoặc các phần tử dữ liệu không thay đổi mà trình triển khai thực hiện giao diện đó
phải cung cấp mã lệnh. Nó không chỉ rõ các chi tiết hành vi. Về bản chất (và với trình biên dịch Java), một giao
diện định nghĩa một kiểu dữ liệu mới và nó là một trong những đặc tính mạnh của ngôn ngữ này.
Các lớp khác triển khai thực hiện giao diện, có nghĩa là chúng có thể sử dụng bất kỳ các hằng số trong giao
diện đó bằng tên và chúng phải chỉ rõ hành vi cho các định nghĩa phương thức trong giao diện.
Bất kỳ lớp nào trong hệ thống thứ bậc cũng có thể thực hiện một giao diện cụ thể nào đó. Điều đó có nghĩa là
các lớp không liên quan nhau có thể thực hiện cùng một giao diện.
Định nghĩa giao diện
Định nghĩa một giao diện là đơn giản:
?12345678
public interface interfaceName {final constantTypeconstantName = constantValue;...returnValueTypemethodName( arguments );...}
Một khai báo giao diện trông rất giống với một khai báo lớp, trừ việc bạn sử dụng từ khóa interface. Bạn có thể
đặt tên giao diện là bất cứ thứ gì bạn muốn, miễn là tên hợp lệ, nhưng theo quy ước, các tên của giao diện
nhìn giống như các tên lớp. Bạn có thể bao gồm các hằng số, các khai báo phương thức, hoặc cả hai vào
trong một giao diện.
Các hằng số được định nghĩa trong một giao diện giống như các hằng số được định nghĩa trong các lớp. Các
từ khóa public và static được giả định sẵn cho các hằng số được định nghĩa trong một giao diện, vì vậy bạn
không cần phải gõ thêm chúng. (Từ khóa final cũng được giả định sẵn, nhưng hầu hết các lập trình viên đều
gõ vào từ khóa này).
Các phương thức được định nghĩa trong một giao diện (nói chung) trông khác với các phương thức được định
nghĩa trong các lớp, bởi vì các phương thức trong một giao diện không có phần triển khai thực hiện. Chúng kết
thúc bằng dấu chấm phẩy sau khi khai báo phương thức và chúng không có phần thân. Bất kỳ trình thực hiện
nào của giao diện có trách nhiệm cung cấp phần thân của các phương thức. Các từ khóa public và abstract
được giả định sẵn cho các phương thức, vì vậy bạn không cần phải gõ thêm chúng.
Bạn có thể định nghĩa các hệ thống thứ bậc của các giao diện giống như bạn định nghĩa các hệ thống thứ bậc
các lớp. Bạn làm điều này với từ khóa extends như sau:
?123
public interface interfaceName extends superinterfaceName, ... {interface body...}
Một lớp có thể là một lớp con của chỉ một lớp bậc trên, nhưng một giao diện có thể mở rộng nhiều giao diện
khác tùy bạn muốn. Chỉ cần liệt kê chúng sau từ khóa extends, phân cách bằng dấu phẩy.
Dưới đây là ví dụ về một giao diện:
?123456
public interface Human {final String GENDER_MALE = "MALE";final String GENDER_FEMALE = "FEMALE";void move();void talk();}
Triển khai thực hiện các giao diện
Để sử dụng một giao diện, bạn chỉ cần triển khai thực hiện (implement) nó, điều này có nghĩa là cung cấp hành
vi cho các phương thức được định nghĩa trong giao diện. Bạn làm điều đó với từ khóa implements:
?1234
public class className extends superclassName implementsinterfaceName, ... {class body}
Theo quy ước, mệnh đề extends (nếu có) đứng trước, tiếp theo sau là mệnh đề implements. Bạn có thể triển
khai thực hiện nhiều hơn một giao diện bằng cách liệt kê các tên giao diện, phân cách bằng dấu phẩy.
Ví dụ, chúng ta có thể yêu cầu lớp Person của chúng ta thực hiện giao diện Human (nói tắt “thực hiện Human”
cũng có nghĩa tương tự) như sau:
?12345678910
public abstract class Person implements Human {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = Human.GENDER_MALE;protected int progress = 0;public void move() {this.progress++;}}
Khi chúng ta thực hiện giao diện, chúng ta cung cấp hành vi cho các phương thức. Chúng ta phải thực hiện
các phương thức này với các chữ ký (signatures) khớp với các chữ ký trong giao diện, có thêm từ khóa bổ
nghĩa quyền truy cập public. Nhưng chúng ta đã chỉ triển khai thực hiện phương thức move() trên Person.
Chúng ta có cần phải thực hiện phương thức talk() không? Không, bởi vì Person là một lớp trừu tượng và từ
khóa abstract được giả định sẵn cho các phương thức trong một giao diện. Điều đó có nghĩa là bất kỳ lớp trừu
tượng nào triển khai thực hiện các giao diện có thể thực hiện những gì nó muốn và bỏ qua phần còn lại. Nếu
nó không thực hiện một hoặc nhiều phương thức, nó chuyển giao trách nhiệm đó đến các lớp con của nó.
Trong lớp Person của chúng ta, chúng ta đã chọn thực hiện move() và không thực hiện talk(), nhưng chúng ta
có thể chọn không triển khai thực hiện phương thức nào cả.
Các biến cá thể trong lớp của chúng ta không được định nghĩa trong giao diện. Nhưng trong giao diện có định
nghĩa một số hằng số có ích và chúng ta có thể tham khảo chúng bằng tên, trong bất kỳ lớp nào thực hiện giao
diện, giống như chúng ta đã làm khi chúng ta khởi tạo biến giới tính (gender). Cũng rất thường thấy các giao
diện chỉ chứa các hằng số. Nếu như vậy, bạn không cần phải thực hiện giao diện để sử dụng các hằng số đó.
Đơn giản chỉ cần nhập khẩu giao diện (nếu giao diện và các lớp triển khai thực hiện ở trong cùng một gói, bạn
thậm chí không cần phải làm điều đó) và tham khảo các hằng số như sau:
?1 interfaceName.constantName
Sử dụng các giao diện
Một giao diện định nghĩa một kiểu dữ liệu tham chiếu mới. Điều đó có nghĩa là bạn có thể tham chiếu đến một
giao diện bất cứ nơi nào bạn có thể tham chiếu một lớp, chẳng hạn như khi bạn ép kiểu, như được minh họa
bằng các đoạn mã sau đây của phương thức main() mà bạn có thể thêm vào lớp Adult:
?1234567
public static void main(String[] args) {...Adult anAdult = new Adult();anAdult.talk();Human aHuman = (Human) anAdult;aHuman.talk();}
Cả hai cuộc gọi tới talk() sẽ hiển thị Spoke. trên màn hình. Tại sao vậy? Bởi vì một Adult là một Human một khi
nó thực hiện giao diện đó. Bạn có thể ép kiểu một Adult như là một Human, sau đó gọi ra phương thức được
định nghĩa bởi giao diện, cũng giống như bạn có thể ép kiểu anAdult thành Person và gọi các phương thức
Person trên anAdult.
Lớp Baby cũng thực hiện Human. Một Adult không phải là một Baby và một Baby không phải là một Adult,
nhưng cả hai có thể được mô tả như có kiểu Human (hoặc là kiểu Person trong hệ thống thứ bậc của chúng
ta). Hãy xem xét mã này ở một nơi nào đó trong hệ thống của chúng ta:
?12345
public static void main(String[] args) {...Human aHuman = getHuman();aHuman.move();}
Human có là một Adult hoặc một Baby không?. Chúng ta không cần phải quan tâm. Cho đến khi mà mọi thứ ta
nhận được từ lời gọi getPerson() là có kiểu Human, thì trên đó chúng ta có thể gọi move() và chờ đợi nó đáp
ứng thích hợp. Chúng ta thậm chí không cần phải quan tâm các lớp đang triển khai thực hiện giao diện có ở
trong cùng hệ thống thứ bậc hay không.
Tại sao cần dùng các giao diện?
Có ba lý do chính để sử dụng các giao diện:
* Để tạo các vùng tên gợi tả và thuận tiện.
* Để liên kết các lớp trong các hệ thống thứ bậc khác nhau.
* Để che giấu các chi tiết kiểu bên dưới khỏi mã của bạn.
Khi bạn tạo một giao diện để thu thập các hằng số liên quan, giao diện này cho bạn một tên gợi tả để sử dụng
khi tham chiếu các hằng số này. Ví dụ, bạn có thể có một giao diện có tên là Language để lưu trữ các tên của
các ngôn ngữ, là một chuỗi ký tự không đổi. Sau đó, bạn có thể tham chiếu các tên ngôn ngữ đó như là
Language.ENGLISH và tương tự. Điều này có thể làm cho mã của bạn dễ đọc hơn.
Ngôn ngữ Java chỉ hỗ trợ thừa kế đơn (single inheritance). Nói cách khác, một lớp chỉ có thể là lớp con trực
tiếp của một lớp bậc trên. Đôi khi điều này trở thành khá hạn chế. Với các giao diện, bạn có thể liên kết các
lớp trong các hệ thống thứ bậc khác nhau. Đó là một đặc tính mạnh của ngôn ngữ này. Về bản chất, một giao
diện đơn giản chỉ định nghĩa rõ một tập hợp các hành vi mà tất cả các trình triển khai thực hiện giao diện phải
hỗ trợ. Có thể rằng mối quan hệ duy nhất sẽ tồn tại giữa các lớp đang triển khai thực hiện giao diện là chúng
cùng chia sẻ các hành vi mà giao diện định nghĩa. Ví dụ, giả sử chúng ta đã có một giao diện được gọi là
Mover:
?123
public interface Mover {void move();}
Bây giờ giả sử rằng Person đã mở rộng giao diện đó. Điều đó có nghĩa là bất kỳ lớp nào đang triển khai thực
hiện Person cũng là một Mover. Adult và Baby sẽ đủ điều kiện. Nhưng Cat hoặc Vehicle cũng có thể sẽ như
thế. Và sẽ là hợp lý khi cho rằng Mountain sẽ không như vậy. Bất kỳ lớp nào đã triển khai thực hiện Mover sẽ
có hành vi move(). Một cá thể Mountain sẽ không thể có hành vi đó.
Cuối cùng, nhưng không kém quan trọng, việc sử dụng giao diện cho phép bạn bỏ qua những chi tiết của kiểu
cụ thể khi bạn muốn. Nhớ lại ví dụ của chúng ta khi gọi getPerson(). Chúng ta đã không quan tâm đến cái mà
chúng ta đã nhận được có kiểu là gì; chúng ta chỉ muốn nó là một thứ gì đó mà chúng ta có thể gọi move() từ
đó.
Tất cả nhưng điều này là các lý do thích đáng để sử dụng các giao diện. Sử dụng một giao diện chỉ đơn giản là
vì bạn có thể sử dụng chúng không phải là lý do thích đáng.
Các lớp lồng trong – Lập trình JavaMột lớp lồng trong là gì?
Như tên của nó đã gợi ý, trong ngôn ngữ Java một lớp lồng trong là một lớp được khai báo trong một lớp
khác. Đây là một ví dụ đơn giản:
?123456
public class EnclosingClass {...public class NestedClass {...}}
Thông thường, các lập trình viên giỏi định nghĩa các lớp lồng trong khi lớp lồng trong chỉ có ý nghĩa bên trong
bối cảnh của lớp bao bọc bên ngoài. Một số ví dụ phổ biến như sau:
* Các trình xử lý sự kiện trong một lớp UI.
* Các lớp Helper cho các thành phần UI trong một thành phần UI khác.
* Các lớp Adapter để biến đổi bộ phận bên trong của một lớp thành một số dạng khác cho người dùng lớp này.
Bạn có thể định nghĩa một lớp lồng trong là lớp công khai (public), riêng tư (private) hay có bảo vệ (protected).
Bạn cũng có thể định nghĩa một lớp lồng trong là lớp final (để ngăn cho nó không bị thay đổi), lớp trừu tượng
(abstract) (có nghĩa là nó không thể khởi tạo thành cá thể cụ thể) hoặc lớp tĩnh (static).
Khi bạn tạo ra một lớp static bên trong một lớp khác, bạn đang tạo ra cái được gọi một cách phù hợp nhất là
lớp lồng trong. Một lớp lồng trong được định nghĩa bên trong một lớp khác, nhưng có thể tồn tại bên ngoài một
cá thể của lớp bao ngoài. Nếu lớp lồng trong của bạn không phải là lớp static, nó chỉ có thể tồn tại bên trong
một cá thể của lớp bao ngoài và được gọi một cách phù hợp hơn là lớp bên trong (inner class). Nói khác đi,
mọi lớp bên trong là lớp lồng trong nhưng không phải mọi lớp lồng trong là lớp bên trong. Phần lớn các lớp
lồng trong mà bạn sẽ gặp phải trong sự nghiệp của bạn sẽ là lớp bên trong hơn là các lớp chỉ đơn giản lồng
trong.
Bất kỳ lớp lồng trong nào đều có quyền truy cập vào tất cả các thành viên của lớp bao ngoài, ngay cả khi
chúng được khai báo là private.
Định nghĩa các lớp lồng trong
Bạn định nghĩa một lớp lồng trong đúng như bạn định nghĩa một lớp thông thường khác, nhưng bạn thực hiện
nó trong một lớp bao ngoài. Một ví dụ như đã bày sẵn là hãy định nghĩa một lớp Wallet bên trong lớp Adult.
Cho dù trong thực tế bạn có thể có một Cái ví (Wallet) tách khỏi một Adult, nhưng điều này sẽ không có ích
lắm và điều có ý nghĩa hơn là mọi Adult đều có một Wallet (hoặc ít nhất là một thứ gì đó để giữ tiền, nhưng
nếu dùng MoneyContainer nghe hơi lạ). Cũng là có nghĩa khi cho rằng Wallet sẽ không tồn tại trong Person,
bởi vì một Baby không có ví và tất cả các lớp con của Person sẽ thừa kế nó nếu nó tồn tại trong Person.
Lớp Wallet của chúng ta sẽ khá đơn giản, vì nó chỉ phục vụ để minh họa định nghĩa về một lớp lồng trong:
?123
protected class Wallet {protected ArrayList bills = new ArrayList();
4567891011121314151617
protected void addBill(int aBill) {bills.add(new Integer(aBill));}
protected int getMoneyTotal() {int total = 0;for (Iterator i = bills.iterator(); i.hasNext(); ) {Integer wrappedBill = (Integer) i.next();int bill = wrappedBill.intValue();total += bill;}return total;}}
Chúng ta sẽ định nghĩa lớp này bên trong Adult, giống như sau:
?1234567891011121314151617
public class Adult extends Person {protected Wallet wallet = new Wallet();public Adult() {}public void talk() {System.out.println("Spoke.");}public void acceptMoney(int aBill) {this.wallet.addBill(aBill);}public int moneyTotal() {return this.wallet.getMoneyTotal();}protected class Wallet {...}}
Lưu ý rằng chúng ta đã thêm acceptMoney() để cho phép một Adult nhận thêm tiền. (Xin cứ tự nhiên mở rộng
ví dụ để bắt buộc Adult của bạn phải chi tiêu một vài thứ, đó là việc phổ biến trong cuộc sống thực).
Sau khi chúng ta có lớp lồng trong và phương thức acceptMoney() mới, chúng ta có thể sử dụng chúng như
sau:
?123
Adult anAdult = new Adult();anAdult.acceptMoney(5);System.out.println("I have this much money: " + anAdult.moneyTotal());
Thực hiện mã này sẽ cho kết quả rằng anAdult có một tổng số tiền là 5.
Xử lý sự kiện rất đơn giản
Ngôn ngữ Java định nghĩa một cách tiếp cận xử lý sự kiện với các lớp kết hợp để cho phép bạn tạo và xử lý
các sự kiện của riêng bạn. Nhưng việc xử lý sự kiện có thể đơn giản hơn nhiều. Tất cả những gì mà bạn thực
sự cần là một lô gic nào đó để sinh ra một “sự kiện” (mà thực sự không cần phải hoàn toàn là một lớp sự kiện)
và một lô gic nào đó để lắng nghe sự kiện và sau đó trả lời một cách thích hợp. Ví dụ, giả sử rằng bất cứ khi
nào một Person di chuyển, hệ thống của chúng ta tạo ra (hoặc kích hoạt) một MoveEvent, mà chúng ta có thể
chọn xử lý hay không xử lý. Điều này sẽ yêu cầu một số thay đổi cho hệ thống của chúng ta. Chúng ta phải:
* Tạo ra một lớp “ứng dụng” (application) để khởi chạy hệ thống của chúng ta và minh họa việc sử dụng lớp
bên trong vô danh.
* Tạo một MotionListener mà ứng dụng của chúng ta có thể thực hiện và sau đó xử lý các sự kiện trong trình
lắng nghe (listener).
* Thêm một List của các trình lắng nghe vào Adult.
* Thêm một phương thức addMotionListener() vào Adult để đăng ký trình lắng nghe.
* Thêm một phương thức fireMoveEvent() vào Adult để nó có thể báo cho trình lắng nghe khi nào thì xử lý sự
kiện.
* Thêm mã vào ứng dụng của chúng ta để tạo ra một Adult và tự đăng ký như là một trình xử lý
Tất cả điều này dễ hiểu. Đây là lớp Adult của chúng ta với các thứ mới thêm:
?1234567891011121314151617181920212223
public class Adult extends Person {protected Wallet wallet = new Wallet();protected ArrayList listeners = new ArrayList();public Adult() {}public void move() {super.move(); fireMoveEvent();}...public void addMotionListener(MotionListener aListener) {listeners.add(aListener);}protected void fireMoveEvent() {Iterator iterator = listeners.iterator();while(iterator.hasNext()) {MotionListener listener = (MotionListener) iterator.next();listener.handleMove(this);}}protected class Wallet {...}}
Lưu ý rằng bây giờ chúng ta ghi đè move(), đầu tiên gọi move() trên Person, sau đó gọi fireMoveEvent() để
báo cho trình lắng nghe trả lời. Chúng ta cũng đã thêm phương thức addMotionListener() để thêm một
MotionListener vào một danh sách trình lắng nghe đang hoạt động. Đây là những gì giống với một
MotionListener:
?123
public interface MotionListener {public void handleMove(Adult eventSource);}
Tất cả những gì còn lại là tạo ra lớp ứng dụng của chúng ta:
?1234567891011
public class CommunityApplication implements MotionListener {public void handleMove(Adult eventSource) {System.out.println("This Adult moved: n" + eventSource.toString());}public static void main(String[] args) {CommunityApplication application = new CommunityApplication();Adult anAdult = new Adult();anAdult.addMotionListener(application);anAdult.move();}}
Lớp này thực hiện giao diện MotionListener có nghĩa là nó triển khai thực hiện phương thức handleMove(). Tất
cả những điều mà chúng ta làm ở đây là in một thông báo để minh họa những gì xảy ra khi một sự kiện được
kích hoạt.
Các lớp bên trong vô danh
Các lớp bên trong vô danh cho phép bạn định nghĩa một lớp ngay tại chỗ, mà không đặt tên nó, để cung cấp
một số hành vi trong bối cảnh cụ thể. Đó là một cách tiếp cận phổ biến cho các trình xử lý sự kiện trong các
giao diện người dùng, bàn về chúng là một chủ đề vượt ra ngoài phạm vi của hướng dẫn này. Nhưng chúng ta
có thể sử dụng một lớp bên trong vô danh ngay cả trong ví dụ xử lý sự kiện rất đơn giản của chúng ta.
Bạn có thể chuyển đổi ví dụ từ các trang trước để sử dụng một lớp bên trong vô danh bằng cách thay đổi lời
gọi đến addMotionListener() trong CommunityApplication.main() như sau:
?12345
anAdult.addMotionListener(new MotionListener() {public void handleMove(Adult eventSource) {System.out.println("This Adult moved: n" + eventSource.toString());}});
Thay vì có CommunityApplication triển khai thực hiện MotionListener, chúng ta khai báo một lớp bên trong
không đặt tên (và như vậy là vô danh) có kiểu MotionListener và đã cung cấp cho nó một triển khai thực hiện
handleMove(). Sự việc MotionListener là một giao diện, không phải là một lớp, là không quan trọng. Cả hai đều
có thể chấp nhận được.
Mã này sinh ra chính xác cùng một kết quả giống như các phiên bản trước đó, nhưng nó sử dụng một cách
tiếp cận phổ biến và đáng mong muốn hơn. Bạn sẽ hầu như luôn luôn thấy các trình xử lý sự kiện được triển
khai thực hiện với các lớp bên trong vô danh.
Sử dụng các lớp lồng trong
Các lớp có lồng trong có thể rất có ích cho bạn. Chúng cũng có thể gây ra phiền toái.
Sử dụng một lớp lồng trong sẽ không ý nghĩa lắm khi có thể định nghĩa lớp ở bên ngoài của một lớp bao
ngoài. Trong ví dụ của chúng ta, chúng ta đã có thể định nghĩa Wallet ở bên ngoài Adult mà không cảm thấy
quá tệ. Nhưng hãy tưởng tượng một thứ gì đó kiểu như một lớp Personality. Bạn có bao giờ có một
Personality bên ngoài một cá thể Person không?. Không, do đó hoàn toàn cần thiết phải định nghĩa Personality
như là một lớp lồng trong. Một quy tắc ngón tay cái đúng đắn là bạn nên định nghĩa một lớp dưới dạng lớp
không lồng trong cho đến khi rõ ràng là nó phải được lồng trong, sau đó cấu trúc lại để lồng nó.
Các lớp bên trong vô danh là cách tiếp cận tiêu chuẩn cho các trình xử lý sự kiện, vì vậy, hãy sử dụng chúng
cho mục đích đó. Trong các trường hợp khác, cần thận trọng với chúng. Trừ khi các lớp bên trong vô danh là
nhỏ, xoay quanh một việc, và quen thuộc, chúng làm cho mã khó hiểu. Chúng cũng có thể làm cho việc gỡ lỗi
khó khăn hơn, mặc dù IDE của Eclipse giúp giảm thiểu sự phiền toái đó. Nói chung, hãy thử không sử dụng
các lớp bên trong vô danh cho bất cứ thứ gì trừ các trình xử lý sự kiện.
Các biểu thức chính quy – Học lập trình JavaMột biểu thức chính quy là gì?
Một biểu thức chính quy (regular expression) về bản chất là một mẫu để mô tả một tập hợp các chuỗi ký tự
chia sẻ chung mẫu này. Ví dụ, đây là một tập hợp các chuỗi ký tự có một số điều chung:
* một chuỗi (a string).
* một chuỗi dài hơn (a longer string).
* một chuỗi rất dài (a much longer string).
Mỗi chuỗi ký tự này đều bắt đầu bằng “a” và kết thúc bằng “string.” API của các biểu thức chính quy của Java
(Java Regular Expression) giúp bạn thể hiện điều đó và làm nhiều việc lý thú với các kết quả.
API của biểu thức chính quy (Regular Expression – hoặc viết tắt là regex) của Java là khá giống với các công
cụ biểu thức chính quy có sẵn trong ngôn ngữ Perl. Nếu bạn là một lập trình viên của Perl, bạn sẽ cảm thấy
đúng như đang ở nhà, ít nhất là với cú pháp mẫu biểu thức chính quy của ngôn ngữ Java. Tuy nhiên, nếu bạn
không thường sử dụng biểu thức chính quy , chắc là nó có vẻ trông hơi lạ một chút. Đừng lo lắng: không phải
phức tạp như nó có vẻ thế đâu.
API của biểu thức chính quy
Năng lực về biểu thức chính quy của ngôn ngữ Java gồm có ba lớp cốt lõi mà bạn sẽ sử dụng hầu như mọi
lúc:
* Pattern, trong đó mô tả một mẫu chuỗi ký tự.
* Matcher, để kiểm tra một chuỗi ký tự xem nó có khớp với mẫu không.
* PatternSyntaxException, để báo cho bạn rằng một số thứ không thể chấp nhận được với mẫu mà bạn đã thử
định nghĩa.
Cách tốt nhất để tìm hiểu về biểu thức chính quy là qua các ví dụ, do đó trong phần này chúng ta sẽ tạo ra một
ví dụ đơn giản trong CommunityApplication.main(). Tuy nhiên, trước khi chúng ta tiến hành, điều quan trọng là
hiểu được một số cú pháp mẫu biểu thức chính quy . Chúng ta sẽ thảo luận điều đó chi tiết hơn trong phần kế
tiếp.
Cú pháp mẫu
Một mẫu (pattern) biểu thức chính quy mô tả cấu trúc của chuỗi ký tự mà một biểu thức sẽ cố gắng tìm kiếm
trong một chuỗi ký tự đầu vào. Đây chính là tại sao biểu thức chính quy nhìn có vẻ hơi lạ thường. Tuy nhiên,
một khi bạn hiểu được cú pháp, để giải mã sẽ ít khó khăn hơn.
Dưới đây là một số trong các cấu kiện mẫu phổ biến nhất mà bạn có thể sử dụng trong các chuỗi ký tự mẫu:
Cấu kiện
Cái được coi là ăn khớp
.
Bất kỳ ký tự nào.
?
Không (0) hoặc một (1) của ký tự đứng trước.
*
Không (0) hoặc lớn hơn của ký tự đứng trước.
+
Một (1) hoặc lớn hơn của ký tự đứng trước.
[]
Một dải các ký tự hay chữ số.
^
Không phải cái tiếp sau (tức là, “không phải <cái gì đó>”).
d
Bất kỳ số nào (tùy chọn, [0-9]).
D
Bất kỳ cái gì không là số (tùy chọn, [^0-9]).
s
Bất kỳ khoảng trống nào (tùy chọn, [ ntfr]).
S
Không có bất kỳ khoảng trống nào (tùy chọn, [^ ntfr]).
w
Bất kỳ từ nào (tùy chọn, [a-zA-Z_0-9]).
W
Không có bất kỳ từ nào (tùy chọn, [^w]).
Một số cấu kiện đầu tiên ở đây được gọi là các lượng tử ((quantifiers), bởi vì chúng xác định số lượng cái
đứng trước chúng. Các cấu kiện như là d là các lớp ký tự được định nghĩa trước. Bất kỳ ký tự nào mà không
có ý nghĩa đặc biệt trong một mẫu sẽ là một trực kiện và chỉ khớp với chính nó.
So khớp
Sau khi đã trang bị những hiểu biết mới của chúng ta về các mẫu, đây là một ví dụ đơn giản về mã sử dụng
các lớp trong API của biểu thức chính quy Java:
?1234567891011
Pattern pattern = Pattern.compile("a.*string");Matcher matcher = pattern.matcher("a string");
boolean didMatch = matcher.matches();System.out.println(didMatch);
int patternStartIndex = matcher.start();System.out.println(patternStartIndex);
int patternEndIndex = matcher.end();System.out.println(patternEndIndex);
Trước tiên, chúng ta tạo ra một Pattern. Chúng ta làm điều đó bằng cách gọi compile(), một phương thức tĩnh
trên Pattern, với một chữ chuỗi ký tự biểu diễn mẫu mà chúng ta muốn so khớp. Chữ đó sử dụng cú pháp mẫu
biểu thức chính quy mà bây giờ chúng ta đã có thể hiểu được. Trong ví dụ này, khi dịch thành ngôn ngữ thông
thường, mẫu biểu thức chính quy có nghĩa là: “Tìm một chuỗi ký tự có dạng bắt đầu là ‘a’, theo sau là không
hay nhiều ký khác, kết thúc bằng ‘string’”.
Tiếp theo, chúng ta gọi matcher() trên Pattern của chúng ta. Lời gọi này tạo ra một cá thể Matcher. Khi điều đó
xảy ra, Matcher tìm kiếm chuỗi ký tự mà chúng ta đã chuyển cho nó để so khớp với chuỗi mẫu mà chúng ta đã
dùng để tạo ra Pattern Như bạn đã biết, mọi chuỗi ký tự trong ngôn ngữ Java là một sưu tập các ký tự có đánh
chỉ số, bắt đầu từ 0 và kết thúc bằng độ dài chuỗi trừ một. Matcher phân tích cú pháp của chuỗi ký tự, bắt đầu
từ 0 và tìm các kết quả khớp với mẫu.
Sau khi hoàn tất quá trình đó, Matcher chứa rất nhiều thông tin về các kết quả khớp đã được tìm thấy (hoặc
không tìm thấy) trong chuỗi đầu vào của chúng ta. Chúng ta có thể truy cập thông tin đó bằng cách gọi các
phương thức khác nhau trên Matcher của chúng ta::
* matches() đơn giản cho chúng ta biết rằng toàn bộ chuỗi đầu vào có khớp đúng với mẫu hay không.
* start() cho chúng ta biết giá trị chỉ số trong chuỗi ở đó bắt đầu khớp đúng với mẫu.
* end() cho chúng ta biết giá trị chỉ số ở đó kết thúc khớp đúng với mẫu, cộng với một.
Trong ví dụ đơn giản của chúng ta, có một kết quả khớp bắt đầu từ 0 và kết thúc tại 7. Vì vậy, lời gọi matches()
trả về kết quả đúng (true), lời gọi start() trả về 0 và lời gọi end() trả về 8. Nếu trong chuỗi ký tự của chúng ta có
nhiều ký tự hơn trong mẫu mà chúng ta tìm kiếm, chúng ta có thể sử dụng lookingAt() thay cho matches().
lookingAt() tìm kiếm chuỗi con khớp với mẫu của chúng ta. Ví dụ, hãy xem xét chuỗi ký tự sau đây:
Here is a string with more than just the pattern.
Chúng ta có thể tìm kiếm mẫu a.*string và có được kết quả khớp nếu chúng ta sử dụng lookingAt(). Nếu chúng
ta sử dụng matches() để thay thế, nó sẽ trả về kết quả là sai (false), bởi vì có nhiều thứ trong chuỗi đầu vào
hơn là đúng những gì có trong mẫu.
Các mẫu phức tạp
Những việc tìm kiếm đơn giản là dễ dàng với các lớp biểu thức chính quy, nhưng cũng có thể tìm kiếm tinh vi
hơn nhiều.
Bạn có thể đã quen thuộc với wiki, một hệ thống dựa trên web cho phép người dùng chỉnh sửa các trang web
để làm nó “lớn lên”. Các Wiki, cho dù được viết bằng ngôn ngữ Java hay không, hầu như hoàn toàn được dựa
trên các biểu thức chính quy. Nội dung của chúng được dựa trên chuỗi ký tự mà người sử dụng nhập vào,
được các biểu thức chính quy phân tích cú pháp và định dạng. Một trong những đặc tính nổi bật nhất của các
wiki là ở chỗ bất kỳ người dùng nào cũng có thể tạo ra một liên kết đến một chủ đề khác trong wiki bằng cách
nhập vào một từ wiki , mà thường là một loạt các từ được móc nối với nhau, mỗi một từ trong đó bắt đầu bằng
một chữ cái viết hoa, như sau:
MyWikiWord
Giả sử có chuỗi ký tự sau:
Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.
Bạn có thể tìm kiếm các từ wiki trong chuỗi này với mẫu biểu thức chính quy như sau:
?1 [A-Z][a-z]*([A-Z][a-z]*)+
Dưới đây là một số mã để tìm kiếm các từ wiki:
String input = “Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.”;
Pattern pattern = Pattern.compile(“[A-Z][a-z]*([A-Z][a-z]*)+”);
Matcher matcher = pattern.matcher(input);
?
123
while (matcher.find()) {System.out.println("Found this wiki word: " + matcher.group());}
Bạn sẽ thấy ba từ wiki trong màn hình của bạn.
Việc thay thế
Tìm kiếm các kết quả khớp đúng là rất có ích, nhưng chúng ta cũng có thể thao tác chuỗi ký tự sau khi tìm
thấy một kết quả khớp. Chúng ta có thể thực hiện điều đó bằng cách thay thế các kết quả khớp bằng một thứ
gì khác, cũng giống như bạn có thể tìm kiếm một đoạn văn bản trong một chương trình xử lý van bản và thay
thế nó bằng một cái gì khác. Có một số phương thức trên Matcher để giúp cho chúng ta:
* replaceAll(), để thay thế tất cả các kết quả khớp bằng một chuỗi ký tự mà chúng ta chỉ định.
* replaceFirst(), để chỉ thay thế kết quả khớp đầu tiên bằng một chuỗi ký tự mà chúng ta chỉ định.
Sử dụng các phương thức này rất dễ hiểu:
?1234567
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");Matcher matcher = pattern.matcher(input);System.out.println("Before: " + input); String result = matcher.replaceAll("replacement");System.out.println("After: " + result);
Mã này tìm các từ wiki, như trước đây. Khi Matcher tìm thấy một kết quả khớp, nó thay mỗi từ wiki bằng chuỗi
replacement. Khi bạn chạy mã này, bạn sẽ thấy phần sau đây trên màn hình:
Trước: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is replacement followed by replacement, then replacement.
Nếu chúng ta đã sử dụng replaceFirst(), chúng ta sẽ thấy như sau:
Trước: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.
Các nhóm
Chúng ta cũng có thể tưởng tượng hơn một chút. Khi bạn tìm kiếm các kết quả khớp với một mẫu biểu thức
chính quy, bạn có thể nhận được thông tin về những gì bạn đã tìm thấy. Chúng ta đã thấy điều này với các
phương thức start() và end() trên Matcher. Nhưng chúng ta cũng có thể tham khảo các kết quả khớp thông
qua các nhóm bắt giữ (capturing groups). Trong mỗi mẫu, bạn thường tạo ra các nhóm bằng cách bao quanh
một phần mẫu bằng cặp dấu ngoặc đơn. Các nhóm được đánh số từ trái sang phải, bắt đầu từ 1 (nhóm 0 đại
diện cho kết quả khớp toàn bộ). Sau đây là một số mã để thay thế mỗi từ wiki bằng một chuỗi ký tự “bọc
quanh” từ:
?12345
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");Matcher matcher = pattern.matcher(input);System.out.println("Before: " + input);
67
String result = matcher.replaceAll("blah$0blah");System.out.println("After: " + result);
Việc chạy mã này sẽ tạo ra kết quả sau:
Trước: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.
Trong mã này, chúng ta đã tham chiếu kết quả khớp toàn bộ bằng cách đưa thêm $0 vào trong chuỗi thay thế.
Bất kỳ phần nào của chuỗi ký tự thay thế có dạng $<một số nguyên> sẽ tham chiếu đến nhóm được xác định
bởi các số nguyên (do đó $1 trỏ đến nhóm 1 và tiếp tục). Nói cách khác, $0 tương đương với điều sau đây:
?1 matcher.group(0);
Chúng ta có thể hoàn thành mục tiêu thay thế tương tự bằng cách sử dụng một số các phương thức khác, hơn
là gọi replaceAll():
?123456
StringBuffer buffer = new StringBuffer();while (matcher.find()) {matcher.appendReplacement(buffer, "blah$0blah");}matcher.appendTail(buffer);System.out.println("After: " + buffer.toString());
Chúng ta lại nhận được các kết quả này một lần nữa:
Trước: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.
Một ví dụ đơn giản
Hệ thống thứ bậc Person của chúng ta không cung cấp cho chúng ta nhiều cơ hội để xử lý các chuỗi ký tự,
nhưng chúng ta có thể tạo ra một ví dụ đơn giản để cho phép chúng ta sử dụng một số kỹ năng về biểu thức
chính quy mà chúng ta đã học được.
Hãy thêm một phương thức listen():
?123456789
public void listen(String conversation) {Pattern pattern = Pattern.compile(".*my name is (.*).");Matcher matcher = pattern.matcher(conversation);
if (matcher.lookingAt())System.out.println("Hello, " + matcher.group(1) + "!");elseSystem.out.println("I didn't understand.");}
Phương thức này cho phép chúng ta tiến hành một số cuộc đối thoại với một Adult. Nếu chuỗi ký tự đó có
dạng đặc biệt, Adult của chúng ta có thể trả lời bằng một lời chào tốt lành. Nếu không, nó có thể nói rằng nó
không hiểu được.
Phương thức listen() kiểm tra chuỗi ký tự đầu vào để xem nó có khớp với một mẫu nhất định không: một hay
nhiều ký tự, theo sau là “tên tôi là” (my name is), tiếp theo là một hoặc nhiều ký tự, tiếp theo là một dấu chấm
câu. Chúng ta sử dụng lookingAt() để tìm kiếm một chuỗi con của đầu vào khớp với mẫu. Nếu chúng ta tìm
thấy một kết quả khớp, chúng ta xây dựng một chuỗi ký tự làm lời chào bằng cách nắm bắt lấy những gì đi sau
” my name is “, mà chúng ta cho rằng đó sẽ là tên (đó là những gì nhóm 1 sẽ chứa). Nếu chúng ta không tìm
thấy một kết quả khớp nào, chúng ta trả lời rằng chúng ta không hiểu. Dĩ nhiên là Adult của chúng ta không có
nhiều khả năng đối thoại lắm vào lúc này.
Đây là một ví dụ tầm thường về các khả năng xử lý biểu thức chính quy của ngôn ngữ Java, nhưng nó minh
họa cách làm thế nào để sử dụng chúng.
Làm rõ các biểu thức
Các biểu thức chính quy có vẻ bí hiểm. Rất dễ bị thất bại với mã trông rất giống như tiếng Phạn ấy. Đặt tên
các thứ cho đúng và xây dựng các biểu thức cho tốt cũng có thể giúp đỡ nhiều.
Ví dụ, đây là mẫu của chúng ta cho một từ wiki:
?1 [A-Z][a-z]*([A-Z][a-z]*)+
Bây giờ bạn hiểu cú pháp của biểu thức chính quy, bạn sẽ có thể đọc mà không phải tốn công quá nhiều,
nhưng mã của chúng ta sẽ dễ hiểu hơn nhiều nếu chúng ta khai báo một hằng số để lưu giữ chuỗi mẫu.
Chúng ta có thể đặt tên nó kiểu như WIKI_WORD. Phương thức listen() của chúng ta sẽ bắt đầu như thế này:
?12345
public void listen(String conversation) {Pattern pattern = Pattern.compile(WIKI_WORD);Matcher matcher = pattern.matcher(conversation);...}
Một thủ thuật khác có thể trợ giúp là định nghĩa các hằng số cho mỗi phần của các mẫu, sau đó xây dựng các
mẫu phức tạp hơn như việc lắp ráp các phần có tên. Nói chung, mẫu càng phức tạp thì càng khó khăn để giải
mã nó và càng dễ xảy ra lỗi hơn. Bạn sẽ thấy rằng không có cách thực sự nào để gỡ lỗi các biểu thức chính
quy khác hơn cách thử nghiệm và sửa lỗi. Hãy làm cho cuộc sống đơn giản hơn bằng cách đặt tên các mẫu và
các thành phần mẫu.
Các sưu tập – Học lập trình JavaGiới thiệu
Khung công tác của các sưu tập Java (Java Collections Framework) là rộng lớn. Trong hướng dẫn Giới thiệu
về lập trình Java, tôi đã nói về lớp ArrayList, nhưng đó là chỉ bàn sơ qua trên bề mặt. Có rất nhiều lớp và giao
diện trong khung công tác. Tại đây, chúng ta sẽ trình bày nhiều hơn, dù không phải là tất cả về chúng.
Các giao diện và các lớp sưu tập
Khung công tác của các sưu tập Java dựa trên triển khai thực hiện cụ thể một số giao diện định nghĩa các kiểu
sưu tập (collection):
* Giao diện List định nghĩa một sưu tập các phần tử Object có thể dẫn hướng.
* Giao diện Set định nghĩa một sưu tập không có các phần tử trùng lặp.
* Giao diện Map định nghĩa một sưu tập các cặp khóa – giá trị.
Chúng ta sẽ nói về một vài triển khai thực hiện cụ thể trong hướng dẫn này. Đây không phải là một danh sách
đầy đủ, nhưng nhiều khả năng bạn thường xuyên thấy những thứ sau đây trong các dự án phát triển bằng
ngôn ngữ Java:
Giao diện
(Các) triển khai thực hiện
List
ArrayList, Vector
Set
HashSet, TreeSet
Map
HashMap
Tất cả các giao diện trong khung công tác, trừ Map là các giao diện con của giao diện Collection, trong đó định
nghĩa cấu trúc chung nhất của một sưu tập. Mỗi sưu tập gồm nhiều phần tử. Với vai trò là trình thực hiện các
giao diện con của Collection, tất cả kiểu sưu tập chia sẻ chung (theo trực giác) một số hành vi:
* Các phương thức để mô tả kích thước của sưu tập (như size() và isEmpty()).
* Các phương thức để mô tả nội dung của sưu tập (như contains() và containsAll()).
* Các phương thức để hỗ trợ thao tác về nội dung của sưu tập (như add(), remove() và clear()).
* Các phương thức để cho phép bạn chuyển đổi một sưu tập thành một mảng (như toArray()).
* Một phương thức để cho phép bạn nhận được một trình vòng lặp (iterator) trên mảng các phần tử (iterator()).
Chúng ta sẽ nói về một số phương thức trên trong phần này. Đồng thời chúng ta sẽ thảo luận trình vòng lặp
(iterator) là gì và cách sử dụng nó như thế nào.
Lưu ý rằng các Map là đặc biệt. Thật sự chúng hoàn toàn không là một sưu tập. Tuy nhiên, chúng có hành vi
rất giống các sưu tập, vì vậy chúng ta cũng nói về chúng trong phần này.
Các triển khai thực hiện Danh sách (List)
Các phiên bản cũ hơn của JDK chứa một lớp được gọi là Vector. Nó vẫn còn có trong các phiên bản mới hơn,
nhưng bạn chỉ nên sử dụng nó khi bạn cần có một sưu tập đồng bộ hoá — đó là, một trong những yếu tố là an
toàn phân luồng. (Nói về phân luồng đã vượt ra ngoài phạm vi của bài viết này, chúng ta sẽ thảo luận ngắn
gọn về khái niệm ấy trong phần Tóm tắt). Trong các trường hợp khác, bạn nên sử dụng lớp ArrayList. Bạn vẫn
có thể sử dụng Vector, nhưng nó áp đặt một số chi phí thêm mà bạn thường không cần.
Một ArrayList là cái như tên của nó gợi ý: danh sách các phần tử theo thứ tự. Chúng ta đã thấy làm thế nào để
tạo ra một danh sách và làm thế nào để thêm các phần tử vào nó, trong bài hướng dẫn giới thiệu trước. Khi
chúng ta tạo ra một lớp Wallet lồng trong trong hướng dẫn này, chúng ta đã tích hợp vào đó một ArrayList để
giữ các hoá đơn thanh toán của Adult:
?1234567891011121314151617
protected class Wallet {protected ArrayList bills = new ArrayList(); protected void addBill(int aBill) {bills.add(new Integer(aBill));}
protected int getMoneyTotal() {int total = 0;for (Iterator i = bills.iterator(); i.hasNext(); ) {Integer wrappedBill = (Integer) i.next();int bill = wrappedBill.intValue();total += bill;}return total;}}
Phương thức getMoneyTotal() sử dụng một trình vòng lặp (iterator) để duyệt qua danh sách các hoá đơn
thanh toán và tính tổng giá trị của chúng. Một Iterator tương tự như một Enumeration trong các phiên bản cũ
hơn của ngôn ngữ Java. Khi bạn nhận được một trình vòng lặp trên sưu tập (bằng cách gọi iterator()), trình
vòng lặp cho phép bạn duyệt qua (traverse) toàn bộ sưu tập bằng cách sử dụng một số phương thức quan
trọng, được minh họa trong mã lệnh ở trên:
* hasNext() cho bạn biết còn có một phần tử tiếp theo khác trong sưu tập không.
* next() cho bạn phần tử tiếp theo đó.
Như chúng ta đã thảo luận ở trên, bạn phải ép kiểu đúng khi bạn trích ra các phần tử từ sưu tập khi sử dụng
next().
Tuy nhiên, Iterator còn cho chúng ta một số khả năng bổ sung thêm. Chúng ta có thể loại bỏ các phần tử khỏi
lớp ArrayList bằng cách gọi remove() (hay removeAll(), hay clear()), nhưng chúng ta cũng có thể sử dụng
Iterator để làm điều đó. Hãy thêm một phương thức rất đơn giản được gọi là spendMoney() tới Adult:
?123
public void spendMoney(int aBill) {this.wallet.removeBill(aBill);}
Phương thức này gọi removeBill() trên Wallet:
?1 protected void removeBill(int aBill) {
Iterator iterator = bills.iterator();
2345678
while (iterator.hasNext()) {Integer bill = (Integer) iterator.next();if (bill.intValue() == aBill)iterator.remove();}}
Chúng ta nhận được một Iterator trên các hoá đơn thanh toán ArrayList, và duyệt qua danh sách để tìm một
kết quả khớp với giá trị hóa đơn được chuyển qua (aBill). Nếu chúng ta tìm thấy một kết quả khớp, chúng ta
gọi remove() trên trình vòng lặp để loại bỏ hóa đơn đó. Cũng đơn giản, nhưng còn chưa phải là đơn giản hết
mức. Mã dưới đây thực hiện cùng một công việc và dễ đọc hơn nhiều:
?123
protected void removeBill(int aBill) {bills.remove(new Integer(aBill));}
Có thể bạn sẽ không thường xuyên gọi remove() trên một Iterator nhưng sẽ rất tốt nếu có công cụ đó khi bạn
cần nó.
Lúc này, chúng ta có thể loại bỏ chỉ một hóa đơn riêng lẻ mỗi lần khỏi Wallet. Sẽ là tốt hơn nếu sử dụng sức
mạnh của một List để giúp chúng ta loại bỏ nhiều hóa đơn cùng một lúc, như sau:
?123
public void spendMoney(List bills) {this.wallet.removeBills(bills);}
Chúng ta cần phải thêm removeBills() vào wallet của chúng ta để thực hiện việc này. Hãy thử mã dưới đây:
?123
protected void removeBills(List billsToRemove) {this.bills.removeAll(bills);}
Đây là việc triển khai thực hiện dễ dàng nhất mà chúng ta có thể sử dụng. Chúng ta gọi removeAll() trên List
các hoá đơn của chúng ta, chuyển qua một Collection. Sau đó phương thức này loại bỏ tất cả các phần tử khỏi
danh sách có trong Collection. Hãy thử chạy mã dưới đây:
?123456789101112
List someBills = new ArrayList();someBills.add(new Integer(1));someBills.add(new Integer(2)); Adult anAdult = new Adult();anAdult.acceptMoney(1);anAdult.acceptMoney(1);anAdult.acceptMoney(2);
List billsToRemove = new ArrayList();billsToRemove.add(new Integer(1));billsToRemove.add(new Integer(2)); anAdult.spendMoney(someBills);
131415
System.out.println(anAdult.wallet.bills);
Các kết quả không phải là những gì mà chúng ta muốn. Chúng ta đã kết thúc mà không còn hóa đơn nào trong
ví cả. Tại sao? Bởi vì removeAll() loại bỏ tất cả các kết quả khớp. Nói cách khác, bất kỳ và tất cả các kết quả
khớp với một mục trong List mà chúng ta chuyển cho phương thức đều bị loại bỏ. Các hoá đơn thanh toán mà
chúng ta đã chuyển cho phương thức có chứa 1 và 2. Ví của chúng ta có chứa hai số 1 và một số 2. Khi
removeAll() tìm kiếm kết quả khớp với phần tử số 1, nó tìm thấy hai kết quả khớp và loại bỏ chúng cả hai. Đó
không phải là những gì mà chúng ta muốn! Chúng ta cần thay đổi mã của chúng ta trong removeBills() để sửa
lại điều này:
?123456
protected void removeBills(List billsToRemove) {Iterator iterator = billsToRemove.iterator();while (iterator.hasNext()) {this.bills.remove(iterator.next());}}
Mã này chỉ loại bỏ một kết quả khớp riêng rẽ, chứ không phải là tất cả các kết quả khớp. Nhớ cẩn thận với
removeAll().
Triển khai thực hiện tập hợp
Có hai triển khai thực hiện Tập hợp (Set) thường được sử dụng phổ biến:
* HashSet, không đảm bảo thứ tự vòng lặp.
* TreeSet, bảo đảm thứ tự vòng lặp.
Các tài liệu hướng dẫn ngôn ngữ Java gợi ý rằng bạn sẽ đi đến chỗ sử dụng triển khai thực hiện thứ nhất
trong hầu hết các trường hợp. Nói chung, nếu bạn cần phải chắc chắn rằng các phần tử trong Set của bạn xếp
theo một thứ tự nhất định nào đó khi bạn duyệt qua nó bằng một trình vòng lặp, thì hãy sử dụng triển khai thực
hiện thứ hai. Nếu không, sử dụng cách thứ nhất. Thứ tự của các phần tử trong một TreeSet (có thực hiện giao
diện SortedSet) được gọi là thứ tự tự nhiên (natural ordering); điều này có nghĩa là, hầu hết mọi trường hợp,
bạn sẽ có khả năng sắp xếp các phần tử dựa trên phép so sánh equals().
Giả sử rằng mỗi Adult có một tập hợp các biệt hiệu. Chúng ta thực sự không quan tâm đến chúng được sắp
đặt thế nào, nhưng các bản sao sẽ không có ý nghĩa. Chúng ta có thể sử dụng một HashSet để lưu giữ chúng.
Trước tiên, chúng ta thêm một biến cá thể:
?1 protected Set nicknames = new HashSet();
Sau đó chúng ta thêm một phương thức để thêm biệt hiệu vào Set:
?123
public void addNickname(String aNickname) {nicknames.add(aNickname);}
Bây giờ hãy thử chạy mã này:
?1 Adult anAdult = new Adult();
2345
anAdult.addNickname("Bobby");anAdult.addNickname("Bob");anAdult.addNickname("Bobby");System.out.println(anAdult.nicknames);
Bạn sẽ thấy chỉ có một Bobby đơn lẻ xuất hiện trên màn hình.
Các triển khai thực hiện Map
Map (Ánh xạ) là một tập hợp các cặp khóa – giá trị. Nó không thể chứa các khóa giống hệt nhau. Mỗi khóa
phải ánh xạ tới một giá trị đơn lẻ, nhưng giá trị đó có thể là bất kỳ kiểu gì. Bạn có thể nghĩ về một ánh xạ như
là List có đặt tên. Hãy tưởng tượng một List trong đó mỗi phần tử có một tên mà bạn có thể sử dụng để trích ra
phần tử đó trực tiếp. Khóa có thể là bất cứ cái gì kiểu Object, giống như giá trị. Một lần nữa, điều đó có nghĩa
là bạn không thể lưu trữ các giá trị kiểu nguyên thủy (primitive) trực tiếp vào trong một Map (bạn có ghét các
giá trị kiểu nguyên thủy không đấy ?). Thay vào đó, bạn phải sử dụng các lớp bao gói kiểu nguyên thủy để lưu
giữ các giá trị đó.
Mặc dù đây là một chiến lược tài chính mạo hiểm, chúng ta sẽ cung cấp cho mỗi Adult một tập hợp các thẻ tín
dụng đơn giản nhất có thể chấp nhận được. Mỗi thẻ sẽ có một tên và một số dư (ban đầu là 0). Trước tiên,
chúng ta thêm một biến cá thể:
?1 protected Map creditCards = new HashMap();
Sau đó chung ta thêm một phương thức để bổ sung thêm một thẻ tín dụng (CreditCard)tới Map:
?123
public void addCreditCard(String aCardName) {creditCards.put(aCardName, new Double(0));}
Giao diện của Map khác với các giao diện của các sưu tập khác. Bạn gọi put() với một khóa và một giá trị để
thêm một mục vào ánh xạ. Bạn gọi get() với khóa để trích ra một giá trị. Chúng ta sẽ làm việc này trong một
phương thức để hiển thị số dư của một thẻ:
?1234
public double getBalanceFor(String cardName) {Double balance = (Double) creditCards.get(cardName);return balance.doubleValue();}
Tất cả những gì còn lại là thêm phương thức charge() để cho phép cộng thêm vào số dư của chúng ta:
?12345678
public void charge(String cardName, double amount) {Double balance = (Double) creditCards.get(cardName);double primitiveBalance = balance.doubleValue();primitiveBalance += amount;balance = new Double(primitiveBalance); creditCards.put(cardName, balance);}
Bây giờ hãy thử chạy mã dưới đây, nó sẽ hiển thị cho bạn 19.95 trên màn hình.
?123456
Adult anAdult = new Adult();anAdult.addCreditCard("Visa");anAdult.addCreditCard("MasterCard");
anAdult.charge("Visa", 19.95);adAdult.showBalanceFor("Visa");
Một thẻ tín dụng điển hình có một tên, một số tài khoản, một hạn mức tín dụng và một số dư. Mỗi mục trong
một Map chỉ có thể có một khóa và một giá trị. Các thẻ tín dụng rất đơn giản của chúng ta rất phù hợp, bởi vì
chúng chỉ có một tên và một số dư hiện tại. Chúng ta có thể làm cho phức tạp hơn bằng cách tạo ra một lớp
được gọi là CreditCard, với các biến cá thể dành cho tất cả các đặc tính của một thẻ tín dụng, sau đó lưu trữ
các cá thể của lớp này như các giá trị cho các mục trong Map của chúng ta.
Có một số khía cạnh thú vị khác về giao diện Map để trình bày trước khi chúng ta đi tiếp (đây không phải là
một danh sách đầy đủ):
Phương thức
Hành vi
containsKey()
Trả lời Map có chứa khóa đã cho hay không.
containsValue()
Trả lời Map có chứa giá trị đã cho hay không.
keySet()
Trả về một Set tập hợp các khóa.
values()
Trả về một Set tập hợp các giá trị.
entrySet()
Trả về một Set tập hợp các cặp khóa – giá trị, được định nghĩa như là các cá thể của các Map.Entry.
remove()
Cho phép bạn loại bỏ giá trị cho một khóa đã cho.
isEmpty()
Trả lời Map có rỗng không (rỗng có nghĩa là, không chứa khóa nào).
Một số trong các phương thức này, chẳng hạn như isEmpty() chỉ là để cho tiện thôi, nhưng một số là rất quan
trọng. Ví dụ, cách duy nhất để thực hiện vòng lặp qua các phần tử trong một Map là thông qua một trong các
tập hợp có liên quan (tập hợp các khóa, các giá trị, hoặc các cặp khóa-giá trị).
Lớp Các sưu tập (Collections)
Khi bạn đang sử dụng khung công tác các sưu tập Java, bạn cần phải nắm được những gì có sẵn trong lớp
Collections. Lớp này gồm có một kho lưu trữ các phương thức tĩnh để hỗ trợ các thao tác trên sưu tập. Chúng
tôi sẽ không trình bày tất cả chúng ở đây, bởi vì bạn có thể tự mình đọc API, nhưng chúng tôi sẽ trình bày hai
phương thức thường xuyên xuất hiện trong mã Java:
* copy()
* sort()
Phương thức đầu tiên cho phép bạn sao chép các nội dung của một sưu tập này tới một sưu tập khác, như
sau:
?123456789
List source = new ArrayList();source.add("one");source.add("two");List target = new ArrayList();target.add("three");target.add("four");
Collections.copy(target, source);System.out.println(target);
Mã này sao chép từ nguồn (source) vào đích (target). Đích phải có cùng kích thước như nguồn, vì thế bạn
không thể sao chép một List vào một List rỗng.
Phương thức sort() sắp xếp các phần tử theo thứ tự tự nhiên của chúng. Tất cả các phần tử phải triển khai
thực hiện giao diện Comparable sao cho chúng có thể so sánh với nhau. Các lớp có sẵn giống như String đã
thực hiện điều này. Vì vậy, đối với một tập hợp các chuỗi ký tự, chúng ta có thể sắp xếp chúng theo thứ tự
tăng dẫn theo kiểu biên soạn từ điển bằng mã sau đây:
?12345678
List strings = new ArrayList();strings.add("one");strings.add("two");strings.add("three");strings.add("four");
Collections.sort(strings);System.out.println(strings);
Bạn sẽ nhận được [four, one, three, two] trên màn hình. Nhưng bạn có thể sắp xếp các lớp mà bạn tạo ra như
thế nào? Chúng ta có thể làm điều này cho Adult. Trước tiên, chúng ta làm cho lớp Adult có thể so sánh lẫn
nhau:
?123
public class Adult extends Person implements Comparable {...}
Sau đó, chúng ta ghi đè compareTo() để so sánh hai cá thể Adult Chúng ta sẽ duy trì việc so sánh rất đơn giản
để làm ví dụ, do đó nó làm rất ít việc:
?1234567
public int compareTo(Object other) {final int LESS_THAN = -1;final int EQUAL = 0;final int GREATER_THAN = 1; Adult otherAdult = (Adult) other;if ( this == otherAdult ) return EQUAL;
8910111213141516
int comparison = this.firstname.compareTo(otherAdult.firstname);if (comparison != EQUAL) return comparison; comparison = this.lastname.compareTo(otherAdult.lastname);if (comparison != EQUAL) return comparison; return EQUAL;}
Bất kỳ số nào nhỏ hơn 0 có nghĩa là “bé hơn”, và -1 là giá trị thích hợp để sử dụng. Tương tự, 1 là thuận tiện
để dành cho “lớn hơn”. Như bạn có thể thấy, 0 có nghĩa là “bằng nhau”. So sánh hai đối tượng theo cách này
rõ ràng là một quá trình thủ công. Bạn cần phải đi qua các biến cá thể và so sánh từng biến. Trong trường hợp
này, chúng ta so sánh tên và họ và sắp xếp thực tế theo họ. Nhưng bạn nên biết, tại sao ví dụ của chúng ta lại
rất đơn giản. Mỗi Adult có nhiều hơn là chỉ tên và họ. Nếu chúng ta muốn làm một phép so sánh sâu hơn,
chúng ta sẽ phải so sánh các Wallet của mỗi Adult để xem xem chúng có bằng nhau không, nghĩa là chúng ta
sẽ phải triển khai thực hiện compareTo() trên Wallet và phần còn lại. Ngoài ra, để thật chính xác khi so sánh,
bất cứ khi nào bạn ghi đè compareTo(), bạn cần phải chắc chắn là phép so sánh là tương thích với equals().
Chúng ta không triển khai thực hiện equals(), vì thế chúng ta không lo lắng về việc tương thích với nó, nhưng
chúng ta có thể phải làm. Trong thực tế, tôi đã thấy mã có bao gồm một dòng như sau, trước khi trả về
EQUAL:
?1 assert this.equals(otherAdult) : "compareTo inconsistent with equals.";
Cách tiếp cận khác để so sánh các đối tượng là trích thuật toán trong compareTo() vào một đối tượng có kiểu
Trình so sánh (Comparator), sau đó gọi Collections.sort() với sưu tập cần sắp xếp và Comparator, như sau:
?12345678910111213141516171819
public class AdultComparator implements Comparator { public int compare(Object object1, Object object2) {final int LESS_THAN = -1;final int EQUAL = 0;final int GREATER_THAN = 1; if ((object1 == null) ;amp;amp (object2 == null))return EQUAL;if (object1 == null)return LESS_THAN;if (object2 == null)return GREATER_THAN; Adult adult1 = (Adult) object1;Adult adult2 = (Adult) object2;if (adult1 == adult2)return EQUAL; int comparison = adult1.firstname.compareTo(adult2.firstname);if (comparison != EQUAL)return comparison;
20212223242526272829303132333435363738394041424344454647484950
comparison = adult1.lastname.compareTo(adult2.lastname);if (comparison != EQUAL)return comparison; return EQUAL;}}
public class CommunityApplication { public static void main(String[] args) {Adult adult1 = new Adult();adult1.setFirstname("Bob");adult1.setLastname("Smith");
Adult adult2 = new Adult();adult2.setFirstname("Al");adult2.setLastname("Jones");
List adults = new ArrayList();adults.add(adult1);adults.add(adult2);
Collections.sort(adults, new AdultComparator());System.out.println(adults);}}
Bạn sẽ thấy “Al Jones” và “Bob Smith”, theo thứ tự đó, trong cửa sổ màn hình của bạn.
Có một số lý do thích đáng để sử dụng cách tiếp cận thứ hai. Các lý do kỹ thuật vượt ra ngoài phạm vi của
hướng dẫn này. Tuy nhiên, từ viễn cảnh của phát triển hướng đối tượng, đây có thể là một ý tưởng tốt khi tách
biệt phần mã so sánh vào trong đối tượng khác, hơn là cung cấp cho mỗi Adult khả năng tự so sánh với nhau.
Tuy nhiên, vì đây thực sự là những gì mà equals() thực hiện, mặc dù kết quả là toán tử boolean, có các lập
luận thích hợp ủng hộ cho cả hai cách tiếp cận.
Sử dụng các sưu tập
Khi nào bạn nên sử dụng một kiểu sưu tập cụ thể ? Đó là một phán xét cần đến năng lực của bạn, và chính vì
thế mà bạn hy vọng sẽ được trả lương hậu hĩ khi là một lập trình viên.
Bất chấp những gì mà nhiều chuyên gia tin tưởng, có rất ít các quy tắc chắc chắn và nhanh chóng để xác định
cần sử dụng những lớp nào trong một tình huống đã cho nào đó. Theo kinh nghiệm cá nhân của tôi, trong
phần lớn các lần khi sử dụng các sưu tập, một ArrayList hoặc một HashMap (hãy nhớ, một Map không thật sự
là một sưu tập) đều bị chơi khăm. Rất có khả năng, bạn cũng từng có trải nghiệm như vậy. Dưới đây là một số
quy tắc ngón tay cái, một số là hiển nhiên hơn những cái còn lại:
* Khi bạn nghĩ rằng mình cần có một sưu tập, hãy bắt đầu với một List, sau đó cứ để cho các mã sẽ báo cho
bạn biết có cần một kiểu khác không.
* Nếu bạn chỉ cần nhóm các thứ gì đó, hãy sử dụng một Set.
* Nếu thứ tự trong vòng lặp là rất quan trọng khi duyệt qua một sưu tập, hãy sử dụng Tree… một hương vị
khác của sưu tập, khi ở đó có sẵn.
* Tránh sử dụng Vector, trừ khi bạn cần khả năng đồng bộ hóa của nó.
* Không nên lo lắng về việc tối ưu hóa cho đến khi (và trừ khi) hiệu năng trở thành một vấn đề.
Các bộ sưu tập là một trong những khía cạnh mạnh mẽ nhất của ngôn ngữ Java. Đừng ngại khi sử dụng
chúng, nhưng cần cảnh giác về các vụ “Tìm ra rồi” (gotchas). Ví dụ, có một cách thuận tiện để chuyển đổi từ
một Array thành một ArrayList:
?123456
Adult adult1 = new Adult();Adult adult2 = new Adult();Adult adult3 = new Adult(); List immutableList = Arrays.asList(new Object[] { adult1, adult2, adult3 });immutableList.add(new Adult());
Mã này đưa ra một UnsupportedOperationException, vì List được Arrays.asList() trả về là không thay đổi
được. Bạn không thể thêm một phần tử mới vào một List không thay đổi. Hãy để ý.
Các ngày tháng – Học lập trình JavaGiới thiệu
Ngôn ngữ Java sẽ mang lại cho bạn khá nhiều công cụ để xử lý các ngày tháng. Một số trong các công cụ này
gây ra sự bực bội hơn là các công cụ có sẵn trong các ngôn ngữ khác. Dù sao chăng nữa, với các công cụ mà
ngôn ngữ Java cung cấp, hầu như không có điều gì mà bạn không thể làm để tạo ra các ngày tháng và định
dạng chúng chính xác theo cách mà bạn muốn.
Tạo các ngày tháng
Khi ngôn ngữ Java còn trẻ, nó có một lớp gọi là Date khá hữu ích cho việc tạo và thao tác các ngày tháng.
Thật không may, lớp đó đã không hỗ trợ đủ tốt cho yêu cầu quốc tế hóa (internationalization), do đó Sun đã
thêm hai lớp để nhằm cải thiện tình hình:
* Lịch (Calendar).
* Định dạng ngày tháng (Dateformat).
Đầu tiên chúng ta sẽ nói về Calendar và để DateFormat lại về sau.
Việc tạo ra một Date vẫn còn tương đối đơn giản:
?1 Date aDate = new Date(System.currentTimeMillis());
Hoặc chúng ta có thể sử dụng mã này:
?1 Date aDate = new Date();
Điều này sẽ cho chúng ta một cá thể Date biểu diễn chính xác ngày tháng và giờ phút lúc ấy, theo định dạng
thời gian địa phương hiện dùng. Định dạng quốc tế hóa vượt ra ngoài phạm vi của hướng dẫn này, nhưng bây
giờ, chỉ cần biết rằng Date mà bạn nhận được tương thích với vị trí địa lý của máy tính tại chỗ của bạn.
Bây giờ khi chúng ta đã có một cá thể Date, chúng ta có thể làm gì với nó? Rất ít, nếu trực tiếp. Chúng ta có
thể so sánh một Date với Date khác để xem cái đầu là xảy ra trước-before() hay xảy ra sau-after() cái thứ hai.
Chúng ta cũng có thể thiết lập lại nó thành một thời khắc mới bằng cách gọi setTime() với một số nguyên dài
(long) biểu diễn số mili giây tính từ nửa đêm ngày1 tháng Giêng năm 1970 (đó là những gì được
System.currentTimeMillis() trả về). Ngoài ra, chúng ta bó tay.
Các lịch (Calendars)
Lớp Date bây giờ gây lộn xộn hơn là ích lợi, do hầu hết hành vi xử lý ngày tháng của nó đã lỗi thời. Bạn đã
quen việc có thể lấy ra và thiết lập (get and set) từng phần của Date (như là năm, tháng, vv). Bây giờ chúng ta
phải sử dụng cả hai Date và Calendar để làm được việc ấy. Khi chúng ta có một cá thể Date, chúng ta có thể
sử dụng Calendar để lấy ra và thiết lập từng phần của nó. Ví dụ:
?123
Date aDate = new Date(System.currentTimeMillis());Calendar calendar = GregorianCalendar.getInstance();calendar.setTime(aDate);
Ở đây chúng ta tạo ra một GregorianCalendar và thiết lập thời gian của nó bằng với Date mà chúng ta đã tạo
ra trước. Chúng ta đã có thể hoàn thành cùng một mục tiêu bằng cách gọi một phương thức khác trên
Calendar của chúng ta:
?12
Calendar calendar = GregorianCalendar.getInstance();calendar.setTimeInMillis(System.currentTimeMillis());
Được trang bị một Calendar, bây giờ chúng ta có thể truy cập và thao tác các thành phần Date. của chúng ta.
Việc lấy ra và thiết lập các phần của Date là một quá trình đơn giản. Chỉ cần gọi các getters và setters thích
hợp trên Calendar, của chúng ta, như sau:
?1234567
calendar.set(Calendar.MONTH, Calendar.JULY);calendar.set(Calendar.DAY_OF_MONTH, 15);calendar.set(Calendar.YEAR, 1978);calendar.set(Calendar.HOUR, 2);calendar.set(Calendar.MINUTE, 15);calendar.set(Calendar.SECOND, 37);System.out.println(calendar.getTime());
Đoạn mã này sẽ in ra chuỗi kết quả đã định dạng cho ngày 15 tháng Bảy năm 1978 lúc 02:15:37 sáng (July 15,
1978 at 02:15:37 a.m) (cũng có các phương thức trình trợ giúp trên Calendar để cho phép chúng ta thiết lập
một số hoặc gần như tất cả các thành phần đó cùng một lúc). Ở đây, chúng ta đã gọi set(), nó nhận hai tham
số:
* Trường (field) (hoặc thành phần) của Date mà chúng ta muốn thiết lập.
* Các giá trị cho trường đó.
Chúng ta có thể tham chiếu các trường bằng các hằng số có tên trong chính lớp Calendar Trong một số
trường hợp, có nhiều hơn một tên cho cùng một trường, như với Calendar.DAY_OF_MONTH, mà nó cũng có
thể dùng Calendar.DATE. Các giá trị là dễ hiểu, có lẽ chỉ trừ các giá trị của Calendar.MONTH và một giá trị của
Calendar.HOUR. Trong ngôn ngữ Java, các tháng được đếm bắt đầu từ số không (tức là, tháng Giêng là 0), vì
thế sẽ là khôn ngoan nếu sử dụng các hằng số có tên để đặt thay cho các số, và đồng thời cũng làm cho tháng
hiển thị các ngày tháng một cách chính xác hơn. Các giờ chạy từ 0 đến 24.
Sau khi chúng ta đã thiết lập Date, chúng ta có thể trích ra các phần của nó:
?1234567
System.out.println("The YEAR is: " + calendar.get(Calendar.YEAR));System.out.println("The MONTH is: " + calendar.get(Calendar.MONTH));System.out.println("The DAY is: " + calendar.get(Calendar.DATE));System.out.println("The HOUR is: " + calendar.get(Calendar.HOUR));System.out.println("The MINUTE is: " + calendar.get(Calendar.MINUTE));System.out.println("The SECOND is: " + calendar.get(Calendar.SECOND));System.out.println("The AM_PM indicator is: " + calendar.get(Calendar.AM_PM));
Định dạng ngày tháng có sẵn
Bạn đã quen có thể định dạng các ngày tháng với Date. Bây giờ bạn phải sử dụng một vài lớp khác:
* DateFormat
* SimpleDateFormat
* DateFormatSymbols
Chúng ta sẽ không trình bày tất những điều phức tạp của việc định dạng ngày tháng ở đây. Bạn có thể tự
khám phá các lớp này. Nhưng chúng ta sẽ nói về những điều căn bản khi sử dụng các công cụ này.
Lớp DateFormat cho phép chúng ta tạo ra một trình định dạng đặc thù theo địa phương, như sau:
?123
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT);Date aDate = new Date();String formattedDate = dateFormatter.format(today);
Mã này tạo ra một chuỗi ngày tháng đã định dạng với khuôn dạng mặc định cho địa phương này. Trong máy
tính của tôi, nó trông giống như sau:
Nov 11, 2005
Đây là kiểu dáng mặc định, nhưng nó không phải là tất cả những gì sẵn có. Chúng ta có thể sử dụng bất kỳ cái
nào trong số các kiểu dáng đã định nghĩa trước. Chúng ta cũng có thể gọi DateFormat.getTimeInstance() tđể
định dạng phần giờ phút hoặc DateFormat.getDateTimeInstance() để định dạng cả phần ngày tháng và phần
giờ phút. Đây là kết quả với các kiểu dáng khác nhau, tất cả đều là các địa phương trong nước Mỹ:
Kiểu dáng
Ngày tháng
Thời gian
Ngày tháng /Thời gian
DEFAULT
Nov 11, 2005
7:44:56 PM
Nov 11, 2005 7:44:56 PM
SHORT
11/11/05
7:44 PM
11/11/05 7:44 PM
MEDIUM
Nov 11, 2005
7:44:56 PM
Nov 11, 2005 7:44:56 PM
LONG
November 11, 2005
7:44:56 PM EST
November 11, 2005 7:44:56 PM EST
FULL
Thursday, November 11, 2005
7:44:56 PM EST
Thursday, November 11, 2005 7:44:56 PM EST
Định dạng có tùy chỉnh
Các khuôn dạng đã định nghĩa trước là tốt đẹp trong hầu hết các trường hợp, nhưng bạn cũng có thể sử dụng
SimpleDateFormat để định nghĩa các định dạng của riêng bạn. Việc sử dụng SimpleDateFormat rất dễ hiểu:
* Khởi tạo một cá thể SimpleDateFormat với một chuỗi kýtự định dạng mẫu (và một tên địa phương, nếu bạn
muốn).
* Gọi format() trên cá thể này với một Date cụ thể.
Kết quả là một chuỗi ngày tháng có định dạng. Dưới đây là một ví dụ:
?1234
Date aDate = new Date();SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");String formattedDate = formatter.format(today);System.out.println(formattedDate);
Khi bạn chạy mã này, bạn sẽ nhận được các kết quả giống như sau (tất nhiên nó sẽ phản ánh ngày tháng hiện
tại khi bạn chạy các mã):
11/05/2005
Chuỗi có ngoặc kép trong ví dụ ở trên tuân theo đúng các quy tắc cú pháp mẫu cho các mẫu định dạng ngày
tháng. Java.sun.com có một số tóm tắt tuyệt vời về các quy tắc đó (xem Tài nguyên). Dưới đây là một số quy
tắc nhỏ có ích:
* Bạn có thể chỉ rõ các mẫu cho các ngày tháng và thời gian.
* Một số cú pháp mẫu không phải là trực giác (ví dụ, mm định nghĩa mẫu phút có hai số; còn để có được viết
tắt tên tháng , bạn sử dụng MM).
* Bạn có thể chèn thêm các chữ vào trong các mẫu của bạn bằng cách đặt chúng trong một dấu nháy đơn (ví
dụ, khi sử dụng “‘on’ MM/dd/yyyy” ở trên tạo ra on 11/05/2005).
* Số các ký tự trong một thành phần văn bản của một mẫu sẽ áp đặt việc dùng dạng viết tắt hay dạng viết dài
(“MM” tạo ra 11, nhưng “MMM” tạo ra Nov và “MMMM” tạo ra November).
* Số các ký tự trong một thành phần số của một mẫu sẽ áp đặt số tối thiểu các chữ số.
Nếu các ký hiệu tiêu chuẩn của SimpleDateFormat vẫn không đáp ứng được nhu cầu định dạng tuỳ chỉnh của
bạn, bạn có thể sử dụng DateFormatSymbols để tùy chỉnh các ký hiệu cho bất cứ thành phần nào của Date
hoặc thời gian. Ví dụ, chúng ta có thể thực hiện một tập hợp duy nhất các tên viết tắt của các tháng trong năm,
như sau (sử dụng chính SimpleDateFormat như trước):
?12345678
DateFormatSymbols symbols = new DateFormatSymbols();String[] oddMonthAbbreviations = new String[] {"Ja","Fe","Mh","Ap","My","Jn","Jy","Au","Se","Oc","No","De" };symbols.setShortMonths(oddMonthAbbreviations);
formatter = new SimpleDateFormat("MMM dd, yyyy", symbols);formattedDate = formatter.format(now);System.out.println(formattedDate);
Mã này gọi một hàm tạo khác trên SimpleDateFormat, nó nhận một chuỗi ký tự mẫu và một
DateFormatSymbols định nghĩa các dạng viết tắt được sử dụng khi một tên tháng viết tắt xuất hiện trong một
mẫu. Khi chúng ta định dạng ngày tháng với các biểu tượng này, kết quả sẽ trông giống như kết quả của Date
mà chúng ta đã thấy ở trên:
No 15, 2005
Các khả năng tuỳ chỉnh của SimpleDateFormat và DateFormatSymbols sẽ là đủ để tạo ra bất kỳ định dạng nào
mà bạn cần.
Thao tác các ngày tháng
Bạn có thể tiến lên và lùi lại theo trục thời gian bằng cách tăng và giảm các ngày tháng hoặc từng phần của
chúng. Hai phương thức cho phép bạn làm điều này:
* add()
* roll()
Phương thức thứ nhất cho phép bạn cộng một số lượng (hoặc trừ đi, nếu là cộng thêm một số lượng âm) thời
gian vào một trường Date cụ thể. Việc thực hiện điều đó sẽ điều chỉnh tất cả các trường khác của Date một
cách tương ứng, dựa trên việc cộng vào một trường cụ thể. Ví dụ, giả sử chúng ta bắt đầu với ngày 15 tháng
Mười Một năm 2005 và tăng trường ngày lên thêm 20. Chúng ta có thể sử dụng mã như sau:
?123456789
Calendar calendar = GregorianCalendar.getInstance();calendar.set(Calendar.MONTH, 10);calendar.set(Calendar.DAY_OF_MONTH, 15);calendar.set(Calendar.YEAR, 2005);formatter = new SimpleDateFormat("MMM dd, yyyy");System.out.println("Before: " + formatter.format(calendar.getTime())); calendar.add(Calendar.DAY_OF_MONTH, 20);System.out.println("After: " + formatter.format(calendar.getTime()));
Kết quả sẽ giống như sau:
Before: Nov 15, 2005
After: Dec 05, 2005
Khá đơn giản. Nhưng việc cuộn (roll) một Date muốn nói lên điều gì?. Nó có nghĩa là bạn đang tăng hoặc giảm
một trường cụ thể của ngày tháng/thời giờ một lượng đã cho, mà không ảnh hưởng đến các trường khác. Ví
dụ, chúng ta có thể cuộn ngày tháng của chúng ta từ tháng Mười Một đến tháng Mười Hai như sau:
?12345678
Calendar calendar = GregorianCalendar.getInstance();calendar.set(Calendar.MONTH, 10);calendar.set(Calendar.DAY_OF_MONTH, 15);calendar.set(Calendar.YEAR, 2005);formatter = new SimpleDateFormat("MMM dd, yyyy");System.out.println("Before: " + formatter.format(calendar.getTime()));calendar.roll(Calendar.MONTH, true);System.out.println("After: " + formatter.format(calendar.getTime()));
Lưu ý rằng tháng được cuộn lên (hoặc tăng lên) thêm 1. Có hai dạng roll():
?12
* roll(int field, boolean up)* roll(int field, int amount)
Chúng ta đã sử dụng dạng thứ nhất. Để giảm một trường bằng cách sử dụng dạng này, bạn gán giá trị false
cho đối số thứ hai. Dạng thứ hai của phương thức cho phép bạn định rõ số lượng tăng lên hay giảm đi. Nếu
một hành động cuộn tạo ra một giá trị ngày tháng không hợp lệ (ví dụ, 09/31/2005), các phương thức này điều
chỉnh các trường khác cho phù hợp, dựa trên các giá trị hợp lệ lớn nhất và nhỏ nhất cho các ngày tháng, giờ,
vv. Bạn có thể cuộn về phía trước dùng các giá trị dương và cuộn lùi lại khi dùng một giá trị âm.
Thử dự đoán hành động cuộn của bạn sẽ làm gì cũng tốt, và bạn chắc chắn có thể đã làm như thế, nhưng
thường thì cách tốt nhất vẫn là thử và sửa lỗi. Đôi khi bạn sẽ đoán đúng, nhưng đôi khi bạn sẽ phải thử
nghiệm để xem cái gì sẽ tạo ra kết quả đúng.
Sử dụng các ngày tháng
Mọi người đều có một ngày sinh. Hãy thêm một ngày sinh vào lớp Person của chúng ta. Trước tiên, chúng ta
thêm một biến cá thể vào Person:
?1 protected Date birthdate = new Date();
Tiếp theo, chúng ta thêm các phụ kiện cho biến:
?123456
public Date getBirthdate() {return birthdate;}public void setBirthdate(Date birthday) {this.birthdate = birthday;}
Tiếp theo, chúng ta sẽ loại bỏ biến cá thể age bởi vì bây giờ chúng ta sẽ tính toán nó. Chúng ta cũng loại bỏ
phương thức truy cập setAge() vì bây giờ age sẽ là một giá trị tính ra được. Chúng ta thay thế phần thân của
getAge() bằng các mã sau đây:
?12345678910
public int getAge() {Calendar calendar = GregorianCalendar.getInstance();calendar.setTime(new Date());int currentYear = calendar.get(Calendar.YEAR); calendar.setTime(birthdate);int birthYear = calendar.get(Calendar.YEAR); return currentYear - birthYear;}
Trong phương thức này, chúng ta tính toán giá trị của age dựa vào năm sinh của Person và năm hiện tại.
Bây giờ chúng ta có thể kiểm tra nó bằng các mã sau:
?12345678
Calendar calendar = GregorianCalendar.getInstance();calendar.setTime(new Date());calendar.set(Calendar.YEAR, 1971);calendar.set(Calendar.MONTH, 2);calendar.set(Calendar.DAY_OF_MONTH, 23);
Adult anAdult = new Adult();anAdult.setBirthdate(calendar.getTime());System.out.println(anAdult);
9Chúng ta đặt một ngày sinh cho một Adult vào ngày 23 tháng Ba năm 1971. Nếu chúng ta chạy mã này trong
tháng Giêng năm 2005, chúng ta sẽ nhận được kết quả này:
An Adult with:
Age: 33
Name: firstname lastname
Gender: MALE
Progress: 0
Có một vài chi tiết vụn vặt khác tôi dành lại cho các bạn như là bài tập:
* Cập nhật compareTo() trên Adult để phản ánh sự hiện diện của một biến cá thể mới.
* Nếu ta đã triển khai thực hiện nó, chúng ta sẽ phải cập nhật equals() cho Adult để phản ánh sự hiện diện của
một biến cá thể mới.
* Nếu chúng ta đã triển khai thực hiện equals(), chúng ta cũng đã phải triển khai thực hiện hashCode() và
chúng ta sẽ phải cập nhật hashCode() để phản ánh sự hiện diện của một biến cá thể mới.
VÀO/RA (I/O) – Học lập trình JavaGiới thiệu
Các dữ liệu mà một chương trình ngôn ngữ Java sử dụng phải đến từ một nơi nào đó. Nó thường hay đến từ
một số nguồn dữ liệu bên ngoài. Có rất nhiều loại nguồn dữ liệu khác nhau, bao gồm cả cơ sở dữ liệu, chuyển
giao byte trực tiếp qua một ổ cắm (socket) và các tệp tin. Ngôn ngữ Java sẽ mang lại cho bạn rất nhiều công
cụ để bạn có thể nhận được các thông tin từ các nguồn bên ngoài. Các công cụ này phần lớn nằm trong gói
java.io.
Trong tất cả các nguồn dữ liệu sẵn có, các tệp tin là phổ biến nhất và thường thuận tiện nhất. Việc hiểu biết
cách sử dụng các API có sẵn của ngôn ngữ Java để tương tác với các tệp tin là một kỹ năng cơ bản của lập
trình viên.
Nhìn chung, ngôn ngữ Java cung cấp cho bạn một lớp bao gói (File) cho kiểu tệp tin trong hệ điều hành của
bạn. Để đọc tệp tin đó, bạn phải sử dụng các luồng (streams) phân tích cú pháp các byte đầu vào thành các
kiểu của ngôn ngữ Java. Trong phần này, chúng tôi sẽ nói về tất cả các đối tượng mà bạn sẽ thường sử dụng
để đọc các tệp tin.
Các tệp tin (File)
Lớp File định nghĩa một nguồn tài nguyên trên hệ thống tệp tin của bạn. Nó gây phiền toái, đặc biệt là cho việc
kiểm thử, nhưng đó là thực tế mà các lập trình viên Java phải đối phó với nó.
Đây là cách bạn khởi tạo một cá thể File:
?1 File aFile = new File("temp.txt");
Đoạn mã này tạo ra một cá thể File với đường dẫn temp.txt trong thư mục hiện tại. Chúng ta có thể tạo một
File với chuỗi ký tự đường dẫn bất kỳ như mong muốn, miễn là nó hợp lệ. Lưu ý rằng khi có đối tượng File này
không có nghĩa là tệp tin mà nó đại diện thực sự tồn tại trên hệ thống tệp tin ở vị trí dự kiến. Đối tượng của
chúng ta chỉ đại diện cho một tệp tin thực tế có thể có hoặc có thể không có ở đó. Nếu tệp tin liên quan không
tồn tại, chúng ta sẽ không phát hiện ra là có một vấn đề cho đến khi chúng ta cố đọc hay viết vào nó. Đó là một
chút khó chịu, nhưng nó có ý nghĩa. Ví dụ, chúng ta có thể hỏi xem File của ta có tồn tại hay không:
?1 aFile.exists();
Nếu nó không tồn tại, chúng ta có thể tạo nó:
?1 aFile.createNewFile();
Khi sử dụng các phương thức khác trên File, chúng ta cũng có thể xóa các tệp tin, tạo các thư mục, xác định
xem một tài nguyên trong hệ thống tệp tin là một thư mục hay là một tệp tin, v.v.. Tuy nhiên, hoạt động sẽ thực
sự xảy ra, khi chúng ta ghi vào và đọc ra từ tệp tin. Để làm điều đó, chúng ta cần phải hiểu thêm một chút về
các luồng.
Các luồng
Chúng ta có thể truy cập các tệp tin trên hệ thống tệp tin bằng cách sử dụng các luồng (streams). Ở mức độ
thấp nhất, các luồng cho phép một chương trình nhận các byte từ một nguồn và/hoặc để gửi kết quả đến một
đích đến. Một số luồng xử lý tất cả các loại ký tự 16-bit, các ký tự (đó là các luồng kiểu Reader và Writer). Các
luồng khác xử lý chỉ các byte 8-bit (đó là các luồng kiểu InputStream và OutputStream). Trong các hệ thống
thứ bậc này có một số luồng với hương vị khác (tất cả nằm trong gói java.io). Ở mức độ trừu tượng cao nhất,
có các luồng ký tự và các luồng byte.
Các luồng byte đọc (InputStream và các lớp con của nó) và viết (OutputStream và các lớp con của nó) các
byte 8-bit. Nói cách khác, các luồng byte có thể được coi là một kiểu luồng thô hơn. Kết quả là, rất dễ dàng để
hiểu lý do tại sao hướng dẫn Java.sun.com về các lớp ngôn ngữ Java chủ yếu nói rằng các luồng byte thường
được sử dụng cho các dữ liệu nhị phân, chẳng hạn như các hình ảnh. Đây là một danh sách lựa chọn các
luồng byte:Các luồng Cách sử dụngFileInputStreamFileOutputStream Đọc các byte từ một tệp tin và ghi các byte vào một tệp tin.
ByteArrayInputStreamByteArrayOutputStream
Đọc các byte từ một mảng trong bộ nhớ và ghi các byte vào một mảng trong bộ nhớ.
Các luồng ký tự đọc (Reader và các lớp con của nó) và viết (Writer và các lớp con của nó) các ký tự 16-bit.
Các lớp con hoặc đọc hoặc viết từ/đến các bộ nhớ (sinks) dữ liệu hoặc xử lý các byte được chuyển qua nó.
Đây là một danh sách chọn lọc của các luồng ký tự:Các luồng Cách sử dụngStringReaderStringWriter
Các luồng này đọc và viết các ký tự từ/đến các String trong bộ nhớ.
InputStreamReader
InputStreamWriter(and subclassesFileReaderFileWriter)
Cầu nối giữa các luồng byte và các luồng ký tự. Reader flavors read bytes from a byte stream and convert them to characters. The Writer chuyển đổi các ký tự thành các byte để đặt chúng vào các luồng byte.
BufferedReader andBufferedWriter
Là bộ đệm dữ liệu trong khi đọc hoặc viết một luồng khác, cho phép các hoạt động đọc và ghi có hiệu quả hơn. Bạn gói một luồng khác trong một luồng có bộ đệm.
Các luồng là một chủ đề lớn và chúng ta không thể trình bày toàn bộ chúng ở đây. Thay vào đó, chúng ta sẽ
tập trung vào các luồng được khuyến cáo dùng để đọc và viết các tệp tin. Trong hầu hết trường hợp, đây sẽ là
các luồng ký tự, nhưng chúng tôi sẽ sử dụng cả các luồng ký tự và các luồng byte để minh họa các khả năng
này.
Đọc và viết các tệp tin
Có một số cách để đọc ra từ và viết vào một File. Người ta có thể cho rằng cách tiếp cận đơn giản nhất diễn ra
như sau:
* Tạo một FileOutputStream trên File để viết vào nó.
* Tạo một FileInputStream trên File để đọc từ nó.
* Gọi read() để đọc từ File và write() để viết vào File.
* Đóng các luồng, dọn dẹp sạch sẽ nếu cần thiết.
Các mã có thể trông giống như sau:
?1 try {
File source = new File("input.txt");
2345678910111213141516
File sink = new File("output.txt"); FileInputStream in = new FileInputStream(source);FileOutputStream out = new FileOutputStream(sink);int c; while ((c = in.read()) != -1)out.write(c);
in.close();out.close();} catch (Exception e) {e.printStackTrace();}
Ở đây chúng ta tạo ra hai đối tượng File: một FileInputStream để đọc từ tệp tin nguồn và một
FileOutputStream để viết tới File kết quả. (Lưu ý: Ví dụ này đã được điều chỉnh từ ví dụ CopyBytes.java trong
Java.sun.com; xem Tài nguyên). Chúng ta sau đó đọc vào từng byte của dữ liệu đầu vào và ghi nó vào kết quả
đầu ra. Sau khi đã xong, chúng ta đóng các luồng. Có lẽ là khôn ngoan khi đặt một lời gọi close() vào trong
khối cuỗi cùng (finally). Tuy nhiên, trình biên dịch của ngôn ngữ Java sẽ còn yêu cầu bạn phải nắm bắt nhiều
trường hợp ngoại lệ khác có thể xảy ra, điều này có nghĩa là cần một mệnh đề catch khác nữa nằm trong khối
finally của bạn. Có chắc chắn đáng làm không? Có thể.
Vậy là bây giờ chúng ta có một cách tiếp cận cơ bản để đọc và viết. Nhưng sự lựa chọn khôn ngoan hơn, và
về một vài khía cạnh, sự lựa chọn dễ dàng hơn, là sử dụng một số luồng khác mà chúng ta sẽ thảo luận trong
phần tiếp theo.
Các luồng có bộ đệm
Có một số cách để đọc ra và ghi vào một File, nhưng cách tiếp cận điển hình và thuận tiện nhất diễn ra như
sau:
* Tạo một FileWriter trên File.
* Gói FileWriter trong một BufferedWriter.
* Gọi write() trên BufferedWriter mỗi khi cần thiết để viết các nội dung của File, thường ở cuối mỗi dòng cần
viết ký tự kết thúc dòng (đó là, n).
* Gọi flush() trên BufferedWriter để làm rỗng nó.
* Đóng BufferedWriter, dọn dẹp sạch nếu cần thiết.
Các mã có thể trông giống như sau:
?1234567
try {FileWriter writer = new FileWriter(aFile);BufferedWriter buffered = new BufferedWriter(writer);buffered.write("A line of text.n");buffered.flush();} catch (IOException e1) {e1.printStackTrace();}
8Ở đây, chúng ta tạo ra một FileWriter trên aFile, sau đó chúng ta gói nó trong một BufferedWriter. Thao tác viết
có bộ đệm hiệu quả hơn là đơn giản viết mỗi lần một byte. Khi chúng ta đã thực hiện viết từng dòng (mà chúng
ta tự kết thúc bằng n), chúng ta gọi flush() trên BufferedWriter. Nếu chúng ta không làm như vậy, chúng ta sẽ
không nhìn thấy bất kỳ dữ liệu nào trong tệp tin đích, bất chấp mọi công sức cố gắng viết tệp tin này.
Khi chúng ta có dữ liệu trong tệp tin, chúng ta có thể đọc nó bằng các mã tương tự đơn giản:
?12345678910111213
String line = null;StringBuffer lines = new StringBuffer();try {FileReader reader = new FileReader(aFile);BufferedReader bufferedReader = new BufferedReader(reader);while ( (line = bufferedReader.readLine()) != null) {lines.append(line);lines.append("n");}} catch (IOException e1) {e1.printStackTrace();}System.out.println(lines.toString());
Chúng ta tạo ra một FileReader, sau đó gói nó trong một BufferedReader. Điều đó cho phép chúng ta sử dụng
phương thức thuận tiện readLine(). Chúng ta đọc từng dòng cho đến khi không có gì còn lại, viết thêm mỗi
dòng vào cuối StringBuffer. của chúng ta. Khi đọc từ một tệp tin, một IOException có thể xảy ra, do đó chúng ta
bao quanh tất cả logic đọc tệp tin của chúng ta bằng một khối try/catch.