59
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ụ ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 </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 ? 1 2 3 4 </div> <div>HTML Code: import java.io.*; public class Hello { public static void main(String[] args) throws Exception {

Lập trình Java cơ bản đến nâng cao

Embed Size (px)

Citation preview

Page 1: Lập trình Java cơ bản đến nâng cao

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>

Page 2: Lập trình Java cơ bản đến nâng cao

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>

Page 3: Lập trình Java cơ bản đến nâng cao

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.

Page 4: Lập trình Java cơ bản đến nâng cao

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

Page 5: Lập trình Java cơ bản đến nâng cao

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:

?

Page 6: Lập trình Java cơ bản đến nâng cao

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.");}}

Page 7: Lập trình Java cơ bản đến nâng cao

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();

Page 8: Lập trình Java cơ bản đến nâng cao

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() {

Page 9: Lập trình Java cơ bản đến nâng cao

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

Page 10: Lập trình Java cơ bản đến nâng cao

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.");}}

Page 11: Lập trình Java cơ bản đến nâng cao

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

Page 12: Lập trình Java cơ bản đến nâng cao

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

Page 13: Lập trình Java cơ bản đến nâng cao

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.

Page 14: Lập trình Java cơ bản đến nâng cao

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

Page 15: Lập trình Java cơ bản đến nâng cao

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ả.

Page 16: Lập trình Java cơ bản đến nâng cao

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:

Page 17: Lập trình Java cơ bản đến nâng cao

* Để 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.

Page 18: Lập trình Java cơ bản đến nâng cao

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(); 

Page 19: Lập trình Java cơ bản đến nâng cao

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

Page 20: Lập trình Java cơ bản đến nâng cao

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

Page 21: Lập trình Java cơ bản đến nâng cao

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.

Page 22: Lập trình Java cơ bản đến nâng cao

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.

Page 23: Lập trình Java cơ bản đến nâng cao

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.

Page 24: Lập trình Java cơ bản đến nâng cao

+

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’”.

Page 25: Lập trình Java cơ bản đến nâng cao

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

?

Page 26: Lập trình Java cơ bản đến nâng cao

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

Page 27: Lập trình Java cơ bản đến nâng cao

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.

Page 28: Lập trình Java cơ bản đến nâng cao

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.

Page 29: Lập trình Java cơ bản đến nâng cao

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

Page 30: Lập trình Java cơ bản đến nâng cao

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();

Page 31: Lập trình Java cơ bản đến nâng cao

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

Page 32: Lập trình Java cơ bản đến nâng cao

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();

Page 33: Lập trình Java cơ bản đến nâng cao

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.

Page 34: Lập trình Java cơ bản đến nâng cao

?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()

Page 35: Lập trình Java cơ bản đến nâng cao

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; 

Page 36: Lập trình Java cơ bản đến nâng cao

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;

Page 37: Lập trình Java cơ bản đến nâng cao

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ự

Page 38: Lập trình Java cơ bản đến nâng cao

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 để ý.

Page 39: Lập trình Java cơ bản đến nâng cao

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:

Page 40: Lập trình Java cơ bản đến nâng cao

?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:

Page 41: Lập trình Java cơ bản đến nâng cao

?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:

Page 42: Lập trình Java cơ bản đến nâng cao

* 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

Page 43: Lập trình Java cơ bản đến nâng cao

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

Page 44: Lập trình Java cơ bản đến nâng cao

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

Page 45: Lập trình Java cơ bản đến nâng cao

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.

Page 46: Lập trình Java cơ bản đến nâng cao

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

Page 47: Lập trình Java cơ bản đến nâng cao

đí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");

Page 48: Lập trình Java cơ bản đến nâng cao

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

Page 49: Lập trình Java cơ bản đến nâng cao

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.