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 {
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Lập trình Java cơ bản đến nâng cao (phần 2)
Bài 4 – Chia hết, chia lấy dư
*Lí thuyết: một số kiểu biến trong Java
Bạn đã biết 2 kiểu String (chuỗi) và int (nguyên) bây giờ bạn biết thêm kiểu float (thực)
Số nguyên và số thực bạn biết sự khác nhau rồi chứ. Bây giờ ta bắt đầu bài toán ví dụ
?1234567891011121314
</div><div>HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");float a = Float.parseFloat(in.readLine());System.out.print("Nhap b: ");float b = Float.parseFloat(in.readLine());float ketqua = a/b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}
Bạn thử bài toán xem, nhớ đừng nhập số b=0 nhé, chuyện ấy sẽ xử lí sau.
Ví dụ nhập a=5, b=2, kết quả in ra sẽ là 2.5, thú vị phải không ?
Bây giờ cũng bài toán ấy, bạn thay đổi như sau
?123456789101112
</div><div>HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");int a = Integer.parseInt(in.readLine());System.out.print("Nhap b: ");int b = Integer.parseInt(in.readLine());float ketqua = a/b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}</div>
131415
<div>
Cũng nhập a=5, b=2, lần này kết quả in ra là … 2
Phép chia sẽ là phép chia hết nếu cả 2 toán hạng đều kiểu nguyên, gọi là chia lấy nguyên (/) hay div
Bây giờ cũng chương trình ấy mà ta thay đổi lại chút xíu xem sao
?12345678910111213
HTML Code:import java.io.*;public class Hello {public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));System.out.print("Nhap a: ");int a = Integer.parseInt(in.readLine());System.out.print("Nhap b: ");int b = Integer.parseInt(in.readLine());float ketqua = a%b;System.out.println("Ket qua bai toan a+b la: " + ketqua);}}
Cũng nhập a=5, b=2, lần này kết quả in ra là … 1
Đây là kết quả phép chia lấy dư 5 chia cho 2, gọi là chia lấy dư (%) hay mod
*Thế nếu tôi muốn 2 số nguyên chia nhau mà ra kiểu thực chứ không phải phép chia lấy nguyên thì sao ?
Trong trường hợp đó, bạn dùng “ép kiểu”
?1234
</div><div>int a=5,b=2;float ket qua;ketqua=(float)a/b;</div><div>
Cấu trúc một chương trình JavaSau đây chúng tôi xin chia sẻ với bạn đọc cấu trúc một chương trình Java
Ví dụ như một bài văn, ta sẽ có cấu trúc bao gồm:
- Mở bài
- Thân bài
- Kết luận
Các ngôn ngữ lập trình nói chung cũng sẽ có cấu trúc tương tự:
- Khai báo: chứa các khai báo về tài nguyên sẽ sử dụng
- Thân chương trình: là phần chính, bao gồm các hàm, các lệnh mô tả công việc sẽđược thực hiện.
- Giải phóng bộ nhớ
Đó là nói chung, tùy vào từng ngôn ngữ cụ thể sẽ có các cấu trúc khác nhau.
Đây là một ví dụ của chương trình C++
?1234567
int main () { cout << " Hello World ";
return 0; }
Còn đây là chương trình tương tự viết bằng Java
?123456
public class HelloWorld { public static void main(String[] args) {System.out.println("Hello World");}}
Phần xanh có thể coi là phần khai báo, phần đỏ có thể coi như là phần thân chương trình.
Tạm thời chưa bàn tới việc giải phóng bộ nhớ.
Nói chung, một chương trình Java được chia thành các lớp (class) riêng biệt.
Thật ra ở đây luôn có sự lẫn lộn và mơ hồ cho các bạn mới làm quen với Java.
Nhiều người trong chúng ta đã quen thuộc với hệ thống Thư mục và Tập tin của Windows.
Một chương trình Java có thể chỉ gồm có một File cũng có thể gồm nhiều File khác nhau.
Các file này có thể nằm cùng Folder, cũng có thể nằm trong các Folder khác nhau.
Class như một chương trong File truyện của bạn.
Bạn có thể viết nhiều truyện và lưu trong vài Folder khác nhau.
Mỗi Folder như vậy gọi là một gói hay Package.
Tóm lại:
-Một chương trình Java có thể bao gồm một hay nhiều Package.
-Mỗi package sẽ có một hay nhiều File.
-Trong mỗi File sẽ có một hay nhiều Class.
Nếu muốn sử dụng “bất cứ cái gì” ở Package khác, chúng ta phải dùng phát biểu “import”
Dạng cơ bản của một lớp được xác định như sau :
?1234567
class classname{. . .memberVariableDeclarations. . .methodDeclarations. . .}
Ví dụ:
?12345678910
Class classname{
int num1,num2; // Khai báo biến với các dấu phảy giữa các biến Show() { // Method trong Class
statement (s); // Kết thúc bởi dấu chấm phảy
}}
Lớp (Class) trong Java:
Trong Java, một lớp (class) là một tập hợp các attribute và các phương thức (method-còn gọi là các hàm
thành viên). Lớp là cơ sở để tạo để tạo ra các đối tượng (Object). Ví dụ về cái xe chẳng hạn: Lớp (class) xe sẽ
có những attribute như số chỗ ngồi, màu sắc…và các method như chạy, phanh, dừng…
Hay như trong dạng cơ bản phía trên num1, num2 là attributes còn Show() là một Method.
Thừa kế và trừu tượng hóa trong JavaThừa kế là gì?
Các lớp trong mã Java tồn tại trong một hệ thống thứ bậc. Các lớp ở bậc trên một lớp đã cho trong một hệ
thống thứ bậc là các lớp bậc trên (superclasses) của lớp đó. Lớp cụ thể đó là một lớp con (subclass) của tất cả
các lớp bậc cao hơn. Một lớp con thừa kế từ các lớp bậc trên của nó. Lớp Object ở trên đỉnh của mọi hệ thống
thứ bậc các lớp. Nói cách khác, mọi lớp là một lớp con của (và thừa kế từ) Object.
Ví dụ, giả sử chúng ta có một lớp Adult trông như sau:
?123456789101112131415
public class Adult {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Adult() { }public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Lớp Adult của chúng ta kế thừa ngầm từ lớp Object. Điều này được thừa nhận cho mọi lớp, vì vậy bạn không
phải gõ extends Object trong định nghĩa lớp. Nhưng nói rằng lớp của chúng ta thừa kế từ (các) lớp bậc trên
của nó có nghĩa là gì? Nó đơn giản có nghĩa là lớp Adult có quyền truy cập vào các biến và các phương thức
đã được trưng ra (exposed) trong các lớp bậc trên của nó. Trong trường hợp này, nó muốn nói rằng lớp Adult
có thể thấy và sử dụng những phần sau đây từ bất kỳ các lớp bậc trên nào của nó (chúng ta chỉ có một vào lúc
này):
* Các biến và phương thức công khai (public).
* Các biến và phương thức công khai có bảo vệ (protected).
* Các biến và phương thức có bảo vệ theo gói (Package protected) (có nghĩa là, chúng không có từ đặc tả
(specifier) quyền truy cập), nếu lớp bậc trên ở trong cùng một gói như lớp Adult
Các hàm tạo là đặc biệt. Chúng không phải là các thành viên hướng đối tượng (OO) đã đủ lông cánh, do đó
chúng không được thừa kế.
Nếu một lớp con ghi đè một phương thức hay một biến từ lớp bậc trên — nói cách khác là khi lớp con triển
khai thực hiện một thành viên có cùng tên — thì nó che dấu thành viên của lớp bậc trên. Chính xác hơn, việc
ghi đè một biến sẽ che giấu nó và việc ghi đè một phương thức chỉ đơn giản ghi đè nó, nhưng có hiệu quả
tương tự nhau: thành viên bị ghi đè về cơ bản được ẩn đi. Bạn vẫn có thể truy cập đến được các thành viên
của lớp bậc trên bằng cách sử dụng từ khóa super:
?
1 super.hiddenMemberName
Trong trường hợp lớp Adult, tất cả những gì mà nó thừa kế vào lúc này là các phương thức về Object
(toString(), chẳng hạn). Do đó, các đoạn mã sau đây là hoàn toàn có thể chấp nhận được:
?12
Adult anAdult = new Adult();anAdult.toString();
Phương thức toString() không tồn tại rõ ràng trên lớp Adult, nhưng lớp Adult thừa kế nó.
Bạn hãy nên ghi nhớ rằng ở đây có các vụ “Tìm ra rồi” (gotchas- nói về việc phát hiện ra nguyên nhân của một
hành vi bất ngờ, không như mong đợi trong chương trình, tuy nhiên không phải là lỗi, vì tất cả đều đúng như
tài liệu hướng dẫn). Trước hết, rất dễ dàng đặt tên các biến và các phương thức trong một lớp con với tên
giống như các biến và các phương thức trong các lớp bậc trên của lớp đó, rồi sau đó bị lẫn lộn khi bạn không
thể gọi ra một phương thức được thừa kế. Hãy nhớ rằng, khi bạn đưa ra một phương thức có cùng tên trong
một lớp con giống như một lớp đã tồn tại trong một lớp bậc trên, bạn đã che giấu nó. Thứ hai, các hàm tạo
không được thừa kế, nhưng chúng được gọi ra. Có một lời gọi ngầm đến hàm tạo của lớp bậc trên trong bất
kỳ hàm tạo nào của lớp con được bạn viết và đó là điều đầu tiên mà hàm tạo của lớp con thực hiện. Bạn phải
sống chung với điều này; bạn không thể làm gì để thay đổi nó. Ví dụ, hàm tạo Adult của chúng ta thực sự trông
như dưới đây khi thực chạy, mặc dù chúng ta không gõ bất cứ cái gì trong phần thân:
?123
public Adult() {super();}
Dòng đó trong phần thân của hàm tạo sẽ gọi hàm tạo không có đối số của lớp bậc trên. Trong trường hợp này,
đó là hàm tạo của Object.
Định nghĩa một hệ thống thứ bậc các lớp
Giả chúng ta có một lớp có tên là Baby. Nó trong giống như thế này:
?12345678910111213141516
public class Baby {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Baby() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Các lớp Adult and Baby của chúng ta trông rất giống nhau. Trong thực tế, chúng hầu như đồng nhất. Việc sao
đúp mã như thế làm cho việc bảo trì mã thêm khó khăn hơn mức cần thiết. Chúng ta có thể tạo ra một lớp bậc
trên, di chuyển tất cả các phần tử chung lên lớp đó và loại bỏ phần mã sao đúp. Lớp bậc trên của chúng ta có
thể được đặt tên là Person và nó có thể trông giống như sau:
?123456789101112131415
public class Person {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0;public Person() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Bây giờ chúng ta có thể để cho Adult và Baby làm lớp con của Person, và làm cho hai lớp ấy thành khá đơn
giản vào lúc này:
?12345678
public class Adult {public Adult() {}}public class Baby {public Baby() {}}
Một khi chúng ta có hệ thống thứ bậc, chúng ta có thể tham chiếu một cá thể của mỗi lớp con như là một cá
thể của bất kỳ các lớp bậc trên nào của nó trong hệ thống thứ bậc. Ví dụ:
?12345
Adult anAdult = new Adult();System.out.println("anAdult is an Object: " + (Adult instanceof Object));System.out.println("anAdult is a Person: " + (Adult instanceof Person));System.out.println("anAdult is anAdult: " + (Adult instanceof Adult));System.out.println("anAdult is a Baby: " + (Adult instanceof Baby));
Mã này sẽ cho chúng ta ba kết quả đúng và một kết quả sai. Bạn cũng có thể ép kiểu (cast) một đối tượng
thành bất kỳ kiểu bậc cao hơn nào trong hệ thống thứ bậc của nó, như dưới đây:
?12
Adult anAdult = new Adult();Person aPerson = (Person) anAdult;aPerson.move();
3Mã này sẽ biên dịch mà không có vấn đề gì. Chúng ta có thể ép kiểu một Adult thành kiểu Person, sau đó gọi
một phương thức Person trên đó.
Do chúng ta có hệ thống thứ bậc này, nên mã trên các lớp con của chúng ta đơn giản hơn. Nhưng bạn có thấy
một vấn đề ở đây không? Bây giờ tất cả các Adult và và tất cả các Baby (thứ lỗi vì việc dùng từ số nhiều không
đúng) sẽ nói năng và đi lại theo cùng một cách. Chỉ có duy nhất một triển khai thực hiện cho mỗi hành vi. Đó
không phải là những gì mà chúng ta muốn, bởi vì những người trưởng thành không nói năng hoặc đi lại giống
như trẻ con. Chúng ta có thể ghi đè move() và talk() trong các lớp con, nhưng sau đó về cơ bản chúng ta
không sử dụng được hành vi “tiêu chuẩn” đã định nghĩa trong lớp bậc trên của chúng ta. Cái mà chúng ta thực
sự muốn là một cách để bắt buộc các lớp con của chúng ta đi lại và nói năng theo cách cụ thể riêng của
chúng. Đó là những gì mà các lớp trừu tượng sẽ làm.Định nghĩa một hệ thống thứ bậc các lớp
Giả chúng ta có một lớp có tên là Baby. Nó trong giống như thế này:
?12345678910111213141516
public class Baby {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0; public Baby() {}public void move() {System.out.println("Moved.");}public void talk() {System.out.println("Spoke.");}}
Các lớp Adult and Baby của chúng ta trông rất giống nhau. Trong thực tế, chúng hầu như đồng nhất. Việc sao
đúp mã như thế làm cho việc bảo trì mã thêm khó khăn hơn mức cần thiết. Chúng ta có thể tạo ra một lớp bậc
trên, di chuyển tất cả các phần tử chung lên lớp đó và loại bỏ phần mã sao đúp. Lớp bậc trên của chúng ta có
thể được đặt tên là Person và nó có thể trông giống như sau:
?12345678910
public class Person {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = "MALE";protected int progress = 0;public Person() {}public void move() {System.out.println("Moved.");}public void talk() {
1112131415
System.out.println("Spoke.");}}
Bây giờ chúng ta có thể để cho Adult và Baby làm lớp con của Person, và làm cho hai lớp ấy thành khá đơn
giản vào lúc này:
?12345678
public class Adult {public Adult() {}}public class Baby {public Baby() {}}
Một khi chúng ta có hệ thống thứ bậc, chúng ta có thể tham chiếu một cá thể của mỗi lớp con như là một cá
thể của bất kỳ các lớp bậc trên nào của nó trong hệ thống thứ bậc. Ví dụ:
?12345
Adult anAdult = new Adult();System.out.println("anAdult is an Object: " + (Adult instanceof Object));System.out.println("anAdult is a Person: " + (Adult instanceof Person));System.out.println("anAdult is anAdult: " + (Adult instanceof Adult));System.out.println("anAdult is a Baby: " + (Adult instanceof Baby));
Mã này sẽ cho chúng ta ba kết quả đúng và một kết quả sai. Bạn cũng có thể ép kiểu (cast) một đối tượng
thành bất kỳ kiểu bậc cao hơn nào trong hệ thống thứ bậc của nó, như dưới đây:
?123
Adult anAdult = new Adult();Person aPerson = (Person) anAdult;aPerson.move();
Mã này sẽ biên dịch mà không có vấn đề gì. Chúng ta có thể ép kiểu một Adult thành kiểu Person, sau đó gọi
một phương thức Person trên đó.
Do chúng ta có hệ thống thứ bậc này, nên mã trên các lớp con của chúng ta đơn giản hơn. Nhưng bạn có thấy
một vấn đề ở đây không? Bây giờ tất cả các Adult và và tất cả các Baby (thứ lỗi vì việc dùng từ số nhiều không
đúng) sẽ nói năng và đi lại theo cùng một cách. Chỉ có duy nhất một triển khai thực hiện cho mỗi hành vi. Đó
không phải là những gì mà chúng ta muốn, bởi vì những người trưởng thành không nói năng hoặc đi lại giống
như trẻ con. Chúng ta có thể ghi đè move() và talk() trong các lớp con, nhưng sau đó về cơ bản chúng ta
không sử dụng được hành vi “tiêu chuẩn” đã định nghĩa trong lớp bậc trên của chúng ta. Cái mà chúng ta thực
sự muốn là một cách để bắt buộc các lớp con của chúng ta đi lại và nói năng theo cách cụ thể riêng của
chúng. Đó là những gì mà các lớp trừu tượng sẽ làm.
Trừu tượng hóa
Trong bối cảnh hướng đối tượng, trừu tượng hóa đề cập đến hoạt động tổng quát hóa dữ liệu và hành vi thành
một kiểu bậc cao hơn so với lớp hiện tại trong hệ thống thứ bậc. Khi bạn di chuyển các biến hoặc các phương
thức từ một lớp con vào một lớp bậc trên, bạn đang trừu tượng hóa các thành viên này.
Đó là các thuật ngữ chung và chúng cũng được áp dụng trong ngôn ngữ Java. Nhưng ngôn ngữ Java cũng bổ
sung thêm các khái niệm về các lớp trừu tượng và các phương thức trừu tượng. Lớp trừu tượng là một lớp
không cá thể hóa được. Ví dụ, bạn có thể tạo ra một lớp có tên là Animal. Việc tạo ra một cá thể từ lớp như
vậy sẽ không có ý nghĩa: Trong thực tế, bạn chỉ muốn tạo ra các cá thể của một lớp cụ thể như Dog. Nhưng
tất cả các Animal có một số điểm chung, chẳng hạn như khả năng kêu. Việc nói rằng một Animal kêu không
cho bạn biết gì nhiều. Tiếng kêu thế nào phụ thuộc vào loại động vật. Làm thế nào để mô hình hóa điều đó?
Bạn định nghĩa các điểm chung trong các lớp trừu tượng và bạn bắt buộc các lớp con triển khai thực hiện
hành vi cụ thể đặc thù cho loài của chúng.
Bạn có thể có cả lớp trừu tượng và lớp cụ thể trong hệ thống thứ bậc của bạn.
Sử dụng lớp trừu tượng
Lớp Person của chúng ta chứa một số phương thức hành vi mà chúng ta còn chưa biết là chúng ta cần hay
không. Hãy gỡ bỏ nó và bắt buộc các lớp con triển khai thực hiện hành vi đó một cách đa dạng. Chúng ta có
thể làm điều đó bằng cách định nghĩa các phương thức của Person là trừu tượng. Sau đó, các lớp con của
chúng ta sẽ phải triển khai thực hiện các phương thức đó.
?12345678910111213141516171819202122232425
public abstract class Person {...abstract void move();abstract void talk();}
public class Adult extends Person {public Adult() {}public void move() {System.out.println("Walked.");}public void talk() {System.out.println("Spoke.");}}
public class Baby extends Person {public Baby() {}public void move() {System.out.println("Crawled.");}public void talk() {System.out.println("Gurgled.");}}
2627
Chúng ta đã thực hiện những gì trong bản Listing này?
Chúng ta đã thay đổi Person làm cho các phương thức thành trừu tượng, bắt buộc các lớp con triển khai
thực hiện chúng.
Chúng ta làm cho Adult thành lớp con của Person và triển khai thực hiện các phương thức.
Chúng ta làm cho Baby thành lớp con của Person và triển khai thực hiện các phương thức.
Khi bạn khai báo một phương thức là trừu tượng, bạn cần các lớp con để thực hiện phương thức, hoặc
chính lớp con lại tiếp tục là trừu tượng và chuyển giao trách nhiệm triển khai thực hiện tới các lớp con bậc
dưới nữa. Bạn có thể thực hiện một số phương thức trên lớp trừu tượng và buộc các lớp con thực hiện những
phương thức khác. Điều này tùy thuộc vào bạn. Đơn giản chỉ cần khai báo những cái bạn không muốn triển
khai thực hiện là trừu tượng và không cung cấp phần thân của phương thức. Nếu một lớp con không thực
hiện một phương thức trừu tượng từ một lớp bậc trên, thì trình biên dịch sẽ báo lỗi.
Vì cả hai Adult và Baby đều là lớp con của Person, chúng ta có thể quy một cá thể của mỗi lớp là thuộc
kiểu Person.
Cấu trúc lại thành hành vi trừu tượng
Bây giờ chúng ta đã có lớp Person, Adult và Baby trong hệ thống thứ bậc của chúng ta. Giả chúng ta muốn
làm cho hai lớp con thực tế hơn bằng cách thay đổi các phương thức move() của chúng như sau:
?123456789101112131415
public class Adult extends Person {...public void move() {this.progress++;}...}
public class Baby extends Person {...public void move() {this.progress++;}...}
Bây giờ mỗi lớp cập nhật biến cá thể của nó để phản ánh một tiến triển nào đó đang được thực hiện mỗi khi
chúng ta gọi move(). Tuy nhiên, cần lưu ý rằng hành vi lại vẫn giống nhau. Cấu trúc lại mã để loại bỏ sao đúp
mã là có ý nghĩa. Việc cấu trúc lại thích hợp nhất là di chuyển move() tới Person.
Đúng, chúng ta đang thêm việc triển khai thực hiện phương thức quay trở lại lớp bậc trên mà chúng ta vừa lấy
nó ra. Đây là một ví dụ rất đơn giản, do đó việc chuyển qua chuyển lại này có vẻ quá lãng phí. Nhưng những
gì mà chúng ta vừa trải qua là phổ biến khi bạn viết mã Java. Bạn thường xuyên thấy các lớp và các phương
thức thay đổi khi hệ thống lớn lên và đôi khi bạn đi đến chỗ có sao đúp mã mà bạn có thể cấu trúc lại, đưa vào
các lớp bậc trên. Thậm chí bạn có thể làm điều đó, sau đó quyết định rằng đây là một sai lầm và đưa hành vi
quay trở lại xuống các lớp con. Đơn giản là có thể bạn không biết vị trí thích đáng của tất cả các hành vi tại
thời điểm bắt đầu quá trình phát triển. Bạn chỉ biết được vị trí thích đáng dành cho hành vi khi bạn tiến lên.
Hãy cấu trúc lại các lớp của chúng ta để đặt move() quay lại lớp bậc trên:
?1234567891011121314151617181920212223
public abstract class Person {...public void move() {this.progress++;}public abstract void talk();}
public class Adult extends Person {public Adult() {}public void talk() {System.out.println("Spoke.");}}
public class Baby extends Person {public Baby() {}public void talk() {System.out.println("Gurgled.");}}
Bây giờ các lớp con của chúng ta triển khai thực hiện các phiên bản khác nhau của talk(), nhưng chia sẻ cùng
một hành vi
Khi nào trừu tượng hóa… và khi nào thì không
Việc quyết định khi nào trừu tượng hóa (hay tạo một hệ thống thứ bậc) là một chủ đề tranh luận nóng trong
giới hướng đối tượng, đặc biệt là giữa các lập trình viên ngôn ngữ Java. Chắc chắn có rất ít các câu trả lời
đúng hoặc sai về cách làm thế nào để cấu trúc hệ thống thứ bậc các lớp. Đây là một lĩnh vực ở đó các nhà
thực hành có tay nghề và tận tâm có thể (và thường xuyên là) không đồng ý với nhau. Dù sao đi nữa, có một
số quy tắc ngón tay cái thích hợp phải theo đối với các hệ thống thứ bậc.
Thứ nhất, không trừu tượng hóa ngay từ đầu. Hãy đợi cho đến khi mã thông báo cho bạn rằng bạn nên trừu
tượng hóa. Cấu trúc lại trên đường bạn đi đến trừu tượng hóa luôn luôn là cách tốt hơn là giả định bạn cần nó
ngay ở lúc bắt đầu. Đừng giả định rằng bạn cần có một hệ thống thứ bậc. Nhiều lập trình viên Java lạm dụng
các hệ thống thứ bậc.
Thứ hai, chống lại việc sử dụng các lớp trừu tượng khi bạn có thể. Chúng không phải là xấu, chúng chỉ có các
hạn chế. Chúng ta thường sử dụng một lớp trừu tượng để bắt buộc các lớp con của chúng ta triển khai hành vi
nhất định nào đó. Một giao diện (mà chúng ta sẽ thảo luận trong phần Các giao diện ) có phải là một ý tưởng
tốt hơn không? Hoàn toàn có thể. Mã của bạn sẽ là dễ hiểu hơn nếu nó không tạo nên một hệ thống thứ bậc
phức tạp với một hỗn hợp các phương thức triển khai thực hiện và ghi đè. Bạn có thể có một phương thức
được định nghĩa xuyên qua ba hoặc bốn lớp, thành một dãy. Thời điểm mà bạn sử dụng nó trong một lớp con
của lớp con của lớp con của lớp con (sub-sub-sub-subclass), bạn có thể phải tìm kiếm lâu để phát hiện
phương thức này sẽ làm gì. Điều đó có thể làm nản lòng việc gỡ rối.
Thứ ba, hãy sử dụng một hệ thống thứ bậc và/hoặc các lớp trừu tượng khi làm như thế là đẹp. Có rất nhiều
mẫu mã lệnh sử dụng các khái niệm phương thức trừu tượng và lớp trừu tượng của ngôn ngữ Java, ví dụ như
mẫu phương thức khuôn mẫu Gang of Four
Thứ tư, hiểu được giá mà bạn phải trả khi bạn sử dụng một hệ thống thứ bậc một cánh vội vã. Nó thực sự có
thể dẫn bạn nhanh chóng rơi vào con đường sai lầm, bởi vì có các lớp ở đó rồi, được đặt tên như vậy, với các
phương thức mà chúng đã có, làm cho rất dễ dàng để cho rằng tất cả những thứ đó nên là như thế. Có lẽ rằng
hệ thống thứ bậc có ý nghĩa khi bạn tạo ra nó, nhưng nó có thể không có ý nghĩa gì hơn nữa. Sức ỳ có thể
chống lại các đổi thay.
Nói tóm lại, hãy khôn khéo khi sử dụng các hệ thống thứ bậc. Kinh nghiệm sẽ giúp bạn khôn ngoan hơn,
nhưng nó sẽ không làm cho bạn lúc nào cũng sáng suốt. Hãy nhớ cấu trúc lại.
Các giao diện trong lập trình JavaMột giao diện là gì?
Ngôn ngữ Java bao gồm khái niệm về một giao diện (interface), nó chỉ đơn giản là một tập hợp có tên của các
hành vi có sẵn công khai và/hoặc các phần tử dữ liệu không thay đổi mà trình triển khai thực hiện giao diện đó
phải cung cấp mã lệnh. Nó không chỉ rõ các chi tiết hành vi. Về bản chất (và với trình biên dịch Java), một giao
diện định nghĩa một kiểu dữ liệu mới và nó là một trong những đặc tính mạnh của ngôn ngữ này.
Các lớp khác triển khai thực hiện giao diện, có nghĩa là chúng có thể sử dụng bất kỳ các hằng số trong giao
diện đó bằng tên và chúng phải chỉ rõ hành vi cho các định nghĩa phương thức trong giao diện.
Bất kỳ lớp nào trong hệ thống thứ bậc cũng có thể thực hiện một giao diện cụ thể nào đó. Điều đó có nghĩa là
các lớp không liên quan nhau có thể thực hiện cùng một giao diện.
Định nghĩa giao diện
Định nghĩa một giao diện là đơn giản:
?12345678
public interface interfaceName {final constantTypeconstantName = constantValue;...returnValueTypemethodName( arguments );...}
Một khai báo giao diện trông rất giống với một khai báo lớp, trừ việc bạn sử dụng từ khóa interface. Bạn có thể
đặt tên giao diện là bất cứ thứ gì bạn muốn, miễn là tên hợp lệ, nhưng theo quy ước, các tên của giao diện
nhìn giống như các tên lớp. Bạn có thể bao gồm các hằng số, các khai báo phương thức, hoặc cả hai vào
trong một giao diện.
Các hằng số được định nghĩa trong một giao diện giống như các hằng số được định nghĩa trong các lớp. Các
từ khóa public và static được giả định sẵn cho các hằng số được định nghĩa trong một giao diện, vì vậy bạn
không cần phải gõ thêm chúng. (Từ khóa final cũng được giả định sẵn, nhưng hầu hết các lập trình viên đều
gõ vào từ khóa này).
Các phương thức được định nghĩa trong một giao diện (nói chung) trông khác với các phương thức được định
nghĩa trong các lớp, bởi vì các phương thức trong một giao diện không có phần triển khai thực hiện. Chúng kết
thúc bằng dấu chấm phẩy sau khi khai báo phương thức và chúng không có phần thân. Bất kỳ trình thực hiện
nào của giao diện có trách nhiệm cung cấp phần thân của các phương thức. Các từ khóa public và abstract
được giả định sẵn cho các phương thức, vì vậy bạn không cần phải gõ thêm chúng.
Bạn có thể định nghĩa các hệ thống thứ bậc của các giao diện giống như bạn định nghĩa các hệ thống thứ bậc
các lớp. Bạn làm điều này với từ khóa extends như sau:
?123
public interface interfaceName extends superinterfaceName, ... {interface body...}
Một lớp có thể là một lớp con của chỉ một lớp bậc trên, nhưng một giao diện có thể mở rộng nhiều giao diện
khác tùy bạn muốn. Chỉ cần liệt kê chúng sau từ khóa extends, phân cách bằng dấu phẩy.
Dưới đây là ví dụ về một giao diện:
?123456
public interface Human {final String GENDER_MALE = "MALE";final String GENDER_FEMALE = "FEMALE";void move();void talk();}
Triển khai thực hiện các giao diện
Để sử dụng một giao diện, bạn chỉ cần triển khai thực hiện (implement) nó, điều này có nghĩa là cung cấp hành
vi cho các phương thức được định nghĩa trong giao diện. Bạn làm điều đó với từ khóa implements:
?1234
public class className extends superclassName implementsinterfaceName, ... {class body}
Theo quy ước, mệnh đề extends (nếu có) đứng trước, tiếp theo sau là mệnh đề implements. Bạn có thể triển
khai thực hiện nhiều hơn một giao diện bằng cách liệt kê các tên giao diện, phân cách bằng dấu phẩy.
Ví dụ, chúng ta có thể yêu cầu lớp Person của chúng ta thực hiện giao diện Human (nói tắt “thực hiện Human”
cũng có nghĩa tương tự) như sau:
?12345678910
public abstract class Person implements Human {protected int age = 0;protected String firstname = "firstname";protected String lastname = "lastname";protected String gender = Human.GENDER_MALE;protected int progress = 0;public void move() {this.progress++;}}
Khi chúng ta thực hiện giao diện, chúng ta cung cấp hành vi cho các phương thức. Chúng ta phải thực hiện
các phương thức này với các chữ ký (signatures) khớp với các chữ ký trong giao diện, có thêm từ khóa bổ
nghĩa quyền truy cập public. Nhưng chúng ta đã chỉ triển khai thực hiện phương thức move() trên Person.
Chúng ta có cần phải thực hiện phương thức talk() không? Không, bởi vì Person là một lớp trừu tượng và từ
khóa abstract được giả định sẵn cho các phương thức trong một giao diện. Điều đó có nghĩa là bất kỳ lớp trừu
tượng nào triển khai thực hiện các giao diện có thể thực hiện những gì nó muốn và bỏ qua phần còn lại. Nếu
nó không thực hiện một hoặc nhiều phương thức, nó chuyển giao trách nhiệm đó đến các lớp con của nó.
Trong lớp Person của chúng ta, chúng ta đã chọn thực hiện move() và không thực hiện talk(), nhưng chúng ta
có thể chọn không triển khai thực hiện phương thức nào cả.
Các biến cá thể trong lớp của chúng ta không được định nghĩa trong giao diện. Nhưng trong giao diện có định
nghĩa một số hằng số có ích và chúng ta có thể tham khảo chúng bằng tên, trong bất kỳ lớp nào thực hiện giao
diện, giống như chúng ta đã làm khi chúng ta khởi tạo biến giới tính (gender). Cũng rất thường thấy các giao
diện chỉ chứa các hằng số. Nếu như vậy, bạn không cần phải thực hiện giao diện để sử dụng các hằng số đó.
Đơn giản chỉ cần nhập khẩu giao diện (nếu giao diện và các lớp triển khai thực hiện ở trong cùng một gói, bạn
thậm chí không cần phải làm điều đó) và tham khảo các hằng số như sau:
?1 interfaceName.constantName
Sử dụng các giao diện
Một giao diện định nghĩa một kiểu dữ liệu tham chiếu mới. Điều đó có nghĩa là bạn có thể tham chiếu đến một
giao diện bất cứ nơi nào bạn có thể tham chiếu một lớp, chẳng hạn như khi bạn ép kiểu, như được minh họa
bằng các đoạn mã sau đây của phương thức main() mà bạn có thể thêm vào lớp Adult:
?1234567
public static void main(String[] args) {...Adult anAdult = new Adult();anAdult.talk();Human aHuman = (Human) anAdult;aHuman.talk();}
Cả hai cuộc gọi tới talk() sẽ hiển thị Spoke. trên màn hình. Tại sao vậy? Bởi vì một Adult là một Human một khi
nó thực hiện giao diện đó. Bạn có thể ép kiểu một Adult như là một Human, sau đó gọi ra phương thức được
định nghĩa bởi giao diện, cũng giống như bạn có thể ép kiểu anAdult thành Person và gọi các phương thức
Person trên anAdult.
Lớp Baby cũng thực hiện Human. Một Adult không phải là một Baby và một Baby không phải là một Adult,
nhưng cả hai có thể được mô tả như có kiểu Human (hoặc là kiểu Person trong hệ thống thứ bậc của chúng
ta). Hãy xem xét mã này ở một nơi nào đó trong hệ thống của chúng ta:
?12345
public static void main(String[] args) {...Human aHuman = getHuman();aHuman.move();}
Human có là một Adult hoặc một Baby không?. Chúng ta không cần phải quan tâm. Cho đến khi mà mọi thứ ta
nhận được từ lời gọi getPerson() là có kiểu Human, thì trên đó chúng ta có thể gọi move() và chờ đợi nó đáp
ứng thích hợp. Chúng ta thậm chí không cần phải quan tâm các lớp đang triển khai thực hiện giao diện có ở
trong cùng hệ thống thứ bậc hay không.
Tại sao cần dùng các giao diện?
Có ba lý do chính để sử dụng các giao diện:
* Để tạo các vùng tên gợi tả và thuận tiện.
* Để liên kết các lớp trong các hệ thống thứ bậc khác nhau.
* Để che giấu các chi tiết kiểu bên dưới khỏi mã của bạn.
Khi bạn tạo một giao diện để thu thập các hằng số liên quan, giao diện này cho bạn một tên gợi tả để sử dụng
khi tham chiếu các hằng số này. Ví dụ, bạn có thể có một giao diện có tên là Language để lưu trữ các tên của
các ngôn ngữ, là một chuỗi ký tự không đổi. Sau đó, bạn có thể tham chiếu các tên ngôn ngữ đó như là
Language.ENGLISH và tương tự. Điều này có thể làm cho mã của bạn dễ đọc hơn.
Ngôn ngữ Java chỉ hỗ trợ thừa kế đơn (single inheritance). Nói cách khác, một lớp chỉ có thể là lớp con trực
tiếp của một lớp bậc trên. Đôi khi điều này trở thành khá hạn chế. Với các giao diện, bạn có thể liên kết các
lớp trong các hệ thống thứ bậc khác nhau. Đó là một đặc tính mạnh của ngôn ngữ này. Về bản chất, một giao
diện đơn giản chỉ định nghĩa rõ một tập hợp các hành vi mà tất cả các trình triển khai thực hiện giao diện phải
hỗ trợ. Có thể rằng mối quan hệ duy nhất sẽ tồn tại giữa các lớp đang triển khai thực hiện giao diện là chúng
cùng chia sẻ các hành vi mà giao diện định nghĩa. Ví dụ, giả sử chúng ta đã có một giao diện được gọi là
Mover:
?123
public interface Mover {void move();}
Bây giờ giả sử rằng Person đã mở rộng giao diện đó. Điều đó có nghĩa là bất kỳ lớp nào đang triển khai thực
hiện Person cũng là một Mover. Adult và Baby sẽ đủ điều kiện. Nhưng Cat hoặc Vehicle cũng có thể sẽ như
thế. Và sẽ là hợp lý khi cho rằng Mountain sẽ không như vậy. Bất kỳ lớp nào đã triển khai thực hiện Mover sẽ
có hành vi move(). Một cá thể Mountain sẽ không thể có hành vi đó.
Cuối cùng, nhưng không kém quan trọng, việc sử dụng giao diện cho phép bạn bỏ qua những chi tiết của kiểu
cụ thể khi bạn muốn. Nhớ lại ví dụ của chúng ta khi gọi getPerson(). Chúng ta đã không quan tâm đến cái mà
chúng ta đã nhận được có kiểu là gì; chúng ta chỉ muốn nó là một thứ gì đó mà chúng ta có thể gọi move() từ
đó.
Tất cả nhưng điều này là các lý do thích đáng để sử dụng các giao diện. Sử dụng một giao diện chỉ đơn giản là
vì bạn có thể sử dụng chúng không phải là lý do thích đáng.
Các lớp lồng trong – Lập trình JavaMột lớp lồng trong là gì?
Như tên của nó đã gợi ý, trong ngôn ngữ Java một lớp lồng trong là một lớp được khai báo trong một lớp
khác. Đây là một ví dụ đơn giản:
?123456
public class EnclosingClass {...public class NestedClass {...}}
Thông thường, các lập trình viên giỏi định nghĩa các lớp lồng trong khi lớp lồng trong chỉ có ý nghĩa bên trong
bối cảnh của lớp bao bọc bên ngoài. Một số ví dụ phổ biến như sau:
* Các trình xử lý sự kiện trong một lớp UI.
* Các lớp Helper cho các thành phần UI trong một thành phần UI khác.
* Các lớp Adapter để biến đổi bộ phận bên trong của một lớp thành một số dạng khác cho người dùng lớp này.
Bạn có thể định nghĩa một lớp lồng trong là lớp công khai (public), riêng tư (private) hay có bảo vệ (protected).
Bạn cũng có thể định nghĩa một lớp lồng trong là lớp final (để ngăn cho nó không bị thay đổi), lớp trừu tượng
(abstract) (có nghĩa là nó không thể khởi tạo thành cá thể cụ thể) hoặc lớp tĩnh (static).
Khi bạn tạo ra một lớp static bên trong một lớp khác, bạn đang tạo ra cái được gọi một cách phù hợp nhất là
lớp lồng trong. Một lớp lồng trong được định nghĩa bên trong một lớp khác, nhưng có thể tồn tại bên ngoài một
cá thể của lớp bao ngoài. Nếu lớp lồng trong của bạn không phải là lớp static, nó chỉ có thể tồn tại bên trong
một cá thể của lớp bao ngoài và được gọi một cách phù hợp hơn là lớp bên trong (inner class). Nói khác đi,
mọi lớp bên trong là lớp lồng trong nhưng không phải mọi lớp lồng trong là lớp bên trong. Phần lớn các lớp
lồng trong mà bạn sẽ gặp phải trong sự nghiệp của bạn sẽ là lớp bên trong hơn là các lớp chỉ đơn giản lồng
trong.
Bất kỳ lớp lồng trong nào đều có quyền truy cập vào tất cả các thành viên của lớp bao ngoài, ngay cả khi
chúng được khai báo là private.
Định nghĩa các lớp lồng trong
Bạn định nghĩa một lớp lồng trong đúng như bạn định nghĩa một lớp thông thường khác, nhưng bạn thực hiện
nó trong một lớp bao ngoài. Một ví dụ như đã bày sẵn là hãy định nghĩa một lớp Wallet bên trong lớp Adult.
Cho dù trong thực tế bạn có thể có một Cái ví (Wallet) tách khỏi một Adult, nhưng điều này sẽ không có ích
lắm và điều có ý nghĩa hơn là mọi Adult đều có một Wallet (hoặc ít nhất là một thứ gì đó để giữ tiền, nhưng
nếu dùng MoneyContainer nghe hơi lạ). Cũng là có nghĩa khi cho rằng Wallet sẽ không tồn tại trong Person,
bởi vì một Baby không có ví và tất cả các lớp con của Person sẽ thừa kế nó nếu nó tồn tại trong Person.
Lớp Wallet của chúng ta sẽ khá đơn giản, vì nó chỉ phục vụ để minh họa định nghĩa về một lớp lồng trong:
?123
protected class Wallet {protected ArrayList bills = new ArrayList();
protected int getMoneyTotal() {int total = 0;for (Iterator i = bills.iterator(); i.hasNext(); ) {Integer wrappedBill = (Integer) i.next();int bill = wrappedBill.intValue();total += bill;}return total;}}
Chúng ta sẽ định nghĩa lớp này bên trong Adult, giống như sau:
?1234567891011121314151617
public class Adult extends Person {protected Wallet wallet = new Wallet();public Adult() {}public void talk() {System.out.println("Spoke.");}public void acceptMoney(int aBill) {this.wallet.addBill(aBill);}public int moneyTotal() {return this.wallet.getMoneyTotal();}protected class Wallet {...}}
Lưu ý rằng chúng ta đã thêm acceptMoney() để cho phép một Adult nhận thêm tiền. (Xin cứ tự nhiên mở rộng
ví dụ để bắt buộc Adult của bạn phải chi tiêu một vài thứ, đó là việc phổ biến trong cuộc sống thực).
Sau khi chúng ta có lớp lồng trong và phương thức acceptMoney() mới, chúng ta có thể sử dụng chúng như
sau:
?123
Adult anAdult = new Adult();anAdult.acceptMoney(5);System.out.println("I have this much money: " + anAdult.moneyTotal());
Thực hiện mã này sẽ cho kết quả rằng anAdult có một tổng số tiền là 5.
Xử lý sự kiện rất đơn giản
Ngôn ngữ Java định nghĩa một cách tiếp cận xử lý sự kiện với các lớp kết hợp để cho phép bạn tạo và xử lý
các sự kiện của riêng bạn. Nhưng việc xử lý sự kiện có thể đơn giản hơn nhiều. Tất cả những gì mà bạn thực
sự cần là một lô gic nào đó để sinh ra một “sự kiện” (mà thực sự không cần phải hoàn toàn là một lớp sự kiện)
và một lô gic nào đó để lắng nghe sự kiện và sau đó trả lời một cách thích hợp. Ví dụ, giả sử rằng bất cứ khi
nào một Person di chuyển, hệ thống của chúng ta tạo ra (hoặc kích hoạt) một MoveEvent, mà chúng ta có thể
chọn xử lý hay không xử lý. Điều này sẽ yêu cầu một số thay đổi cho hệ thống của chúng ta. Chúng ta phải:
* Tạo ra một lớp “ứng dụng” (application) để khởi chạy hệ thống của chúng ta và minh họa việc sử dụng lớp
bên trong vô danh.
* Tạo một MotionListener mà ứng dụng của chúng ta có thể thực hiện và sau đó xử lý các sự kiện trong trình
lắng nghe (listener).
* Thêm một List của các trình lắng nghe vào Adult.
* Thêm một phương thức addMotionListener() vào Adult để đăng ký trình lắng nghe.
* Thêm một phương thức fireMoveEvent() vào Adult để nó có thể báo cho trình lắng nghe khi nào thì xử lý sự
kiện.
* Thêm mã vào ứng dụng của chúng ta để tạo ra một Adult và tự đăng ký như là một trình xử lý
Tất cả điều này dễ hiểu. Đây là lớp Adult của chúng ta với các thứ mới thêm:
?1234567891011121314151617181920212223
public class Adult extends Person {protected Wallet wallet = new Wallet();protected ArrayList listeners = new ArrayList();public Adult() {}public void move() {super.move(); fireMoveEvent();}...public void addMotionListener(MotionListener aListener) {listeners.add(aListener);}protected void fireMoveEvent() {Iterator iterator = listeners.iterator();while(iterator.hasNext()) {MotionListener listener = (MotionListener) iterator.next();listener.handleMove(this);}}protected class Wallet {...}}
Lưu ý rằng bây giờ chúng ta ghi đè move(), đầu tiên gọi move() trên Person, sau đó gọi fireMoveEvent() để
báo cho trình lắng nghe trả lời. Chúng ta cũng đã thêm phương thức addMotionListener() để thêm một
MotionListener vào một danh sách trình lắng nghe đang hoạt động. Đây là những gì giống với một
MotionListener:
?123
public interface MotionListener {public void handleMove(Adult eventSource);}
Tất cả những gì còn lại là tạo ra lớp ứng dụng của chúng ta:
?1234567891011
public class CommunityApplication implements MotionListener {public void handleMove(Adult eventSource) {System.out.println("This Adult moved: n" + eventSource.toString());}public static void main(String[] args) {CommunityApplication application = new CommunityApplication();Adult anAdult = new Adult();anAdult.addMotionListener(application);anAdult.move();}}
Lớp này thực hiện giao diện MotionListener có nghĩa là nó triển khai thực hiện phương thức handleMove(). Tất
cả những điều mà chúng ta làm ở đây là in một thông báo để minh họa những gì xảy ra khi một sự kiện được
kích hoạt.
Các lớp bên trong vô danh
Các lớp bên trong vô danh cho phép bạn định nghĩa một lớp ngay tại chỗ, mà không đặt tên nó, để cung cấp
một số hành vi trong bối cảnh cụ thể. Đó là một cách tiếp cận phổ biến cho các trình xử lý sự kiện trong các
giao diện người dùng, bàn về chúng là một chủ đề vượt ra ngoài phạm vi của hướng dẫn này. Nhưng chúng ta
có thể sử dụng một lớp bên trong vô danh ngay cả trong ví dụ xử lý sự kiện rất đơn giản của chúng ta.
Bạn có thể chuyển đổi ví dụ từ các trang trước để sử dụng một lớp bên trong vô danh bằng cách thay đổi lời
gọi đến addMotionListener() trong CommunityApplication.main() như sau:
while (matcher.find()) {System.out.println("Found this wiki word: " + matcher.group());}
Bạn sẽ thấy ba từ wiki trong màn hình của bạn.
Việc thay thế
Tìm kiếm các kết quả khớp đúng là rất có ích, nhưng chúng ta cũng có thể thao tác chuỗi ký tự sau khi tìm
thấy một kết quả khớp. Chúng ta có thể thực hiện điều đó bằng cách thay thế các kết quả khớp bằng một thứ
gì khác, cũng giống như bạn có thể tìm kiếm một đoạn văn bản trong một chương trình xử lý van bản và thay
thế nó bằng một cái gì khác. Có một số phương thức trên Matcher để giúp cho chúng ta:
* replaceAll(), để thay thế tất cả các kết quả khớp bằng một chuỗi ký tự mà chúng ta chỉ định.
* replaceFirst(), để chỉ thay thế kết quả khớp đầu tiên bằng một chuỗi ký tự mà chúng ta chỉ định.
Sử dụng các phương thức này rất dễ hiểu:
?1234567
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");Matcher matcher = pattern.matcher(input);System.out.println("Before: " + input); String result = matcher.replaceAll("replacement");System.out.println("After: " + result);
Mã này tìm các từ wiki, như trước đây. Khi Matcher tìm thấy một kết quả khớp, nó thay mỗi từ wiki bằng chuỗi
replacement. Khi bạn chạy mã này, bạn sẽ thấy phần sau đây trên màn hình:
Trước: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is replacement followed by replacement, then replacement.
Nếu chúng ta đã sử dụng replaceFirst(), chúng ta sẽ thấy như sau:
Trước: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.
Các nhóm
Chúng ta cũng có thể tưởng tượng hơn một chút. Khi bạn tìm kiếm các kết quả khớp với một mẫu biểu thức
chính quy, bạn có thể nhận được thông tin về những gì bạn đã tìm thấy. Chúng ta đã thấy điều này với các
phương thức start() và end() trên Matcher. Nhưng chúng ta cũng có thể tham khảo các kết quả khớp thông
qua các nhóm bắt giữ (capturing groups). Trong mỗi mẫu, bạn thường tạo ra các nhóm bằng cách bao quanh
một phần mẫu bằng cặp dấu ngoặc đơn. Các nhóm được đánh số từ trái sang phải, bắt đầu từ 1 (nhóm 0 đại
diện cho kết quả khớp toàn bộ). Sau đây là một số mã để thay thế mỗi từ wiki bằng một chuỗi ký tự “bọc
quanh” từ:
?12345
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");Matcher matcher = pattern.matcher(input);System.out.println("Before: " + input);
67
String result = matcher.replaceAll("blah$0blah");System.out.println("After: " + result);
Việc chạy mã này sẽ tạo ra kết quả sau:
Trước: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
Sau: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
then blahSomeWikiWordblah.
Trong mã này, chúng ta đã tham chiếu kết quả khớp toàn bộ bằng cách đưa thêm $0 vào trong chuỗi thay thế.
Bất kỳ phần nào của chuỗi ký tự thay thế có dạng $<một số nguyên> sẽ tham chiếu đến nhóm được xác định
bởi các số nguyên (do đó $1 trỏ đến nhóm 1 và tiếp tục). Nói cách khác, $0 tương đương với điều sau đây:
?1 matcher.group(0);
Chúng ta có thể hoàn thành mục tiêu thay thế tương tự bằng cách sử dụng một số các phương thức khác, hơn
Đây là việc triển khai thực hiện dễ dàng nhất mà chúng ta có thể sử dụng. Chúng ta gọi removeAll() trên List
các hoá đơn của chúng ta, chuyển qua một Collection. Sau đó phương thức này loại bỏ tất cả các phần tử khỏi
danh sách có trong Collection. Hãy thử chạy mã dưới đây:
?123456789101112
List someBills = new ArrayList();someBills.add(new Integer(1));someBills.add(new Integer(2)); Adult anAdult = new Adult();anAdult.acceptMoney(1);anAdult.acceptMoney(1);anAdult.acceptMoney(2);
List billsToRemove = new ArrayList();billsToRemove.add(new Integer(1));billsToRemove.add(new Integer(2)); anAdult.spendMoney(someBills);
131415
System.out.println(anAdult.wallet.bills);
Các kết quả không phải là những gì mà chúng ta muốn. Chúng ta đã kết thúc mà không còn hóa đơn nào trong
ví cả. Tại sao? Bởi vì removeAll() loại bỏ tất cả các kết quả khớp. Nói cách khác, bất kỳ và tất cả các kết quả
khớp với một mục trong List mà chúng ta chuyển cho phương thức đều bị loại bỏ. Các hoá đơn thanh toán mà
chúng ta đã chuyển cho phương thức có chứa 1 và 2. Ví của chúng ta có chứa hai số 1 và một số 2. Khi
removeAll() tìm kiếm kết quả khớp với phần tử số 1, nó tìm thấy hai kết quả khớp và loại bỏ chúng cả hai. Đó
không phải là những gì mà chúng ta muốn! Chúng ta cần thay đổi mã của chúng ta trong removeBills() để sửa
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;
public class CommunityApplication { public static void main(String[] args) {Adult adult1 = new Adult();adult1.setFirstname("Bob");adult1.setLastname("Smith");
Adult adult2 = new Adult();adult2.setFirstname("Al");adult2.setLastname("Jones");
List adults = new ArrayList();adults.add(adult1);adults.add(adult2);
Collections.sort(adults, new AdultComparator());System.out.println(adults);}}
Bạn sẽ thấy “Al Jones” và “Bob Smith”, theo thứ tự đó, trong cửa sổ màn hình của bạn.
Có một số lý do thích đáng để sử dụng cách tiếp cận thứ hai. Các lý do kỹ thuật vượt ra ngoài phạm vi của
hướng dẫn này. Tuy nhiên, từ viễn cảnh của phát triển hướng đối tượng, đây có thể là một ý tưởng tốt khi tách
biệt phần mã so sánh vào trong đối tượng khác, hơn là cung cấp cho mỗi Adult khả năng tự so sánh với nhau.
Tuy nhiên, vì đây thực sự là những gì mà equals() thực hiện, mặc dù kết quả là toán tử boolean, có các lập
luận thích hợp ủng hộ cho cả hai cách tiếp cận.
Sử dụng các sưu tập
Khi nào bạn nên sử dụng một kiểu sưu tập cụ thể ? Đó là một phán xét cần đến năng lực của bạn, và chính vì
thế mà bạn hy vọng sẽ được trả lương hậu hĩ khi là một lập trình viên.
Bất chấp những gì mà nhiều chuyên gia tin tưởng, có rất ít các quy tắc chắc chắn và nhanh chóng để xác định
cần sử dụng những lớp nào trong một tình huống đã cho nào đó. Theo kinh nghiệm cá nhân của tôi, trong
phần lớn các lần khi sử dụng các sưu tập, một ArrayList hoặc một HashMap (hãy nhớ, một Map không thật sự
là một sưu tập) đều bị chơi khăm. Rất có khả năng, bạn cũng từng có trải nghiệm như vậy. Dưới đây là một số
quy tắc ngón tay cái, một số là hiển nhiên hơn những cái còn lại:
* Khi bạn nghĩ rằng mình cần có một sưu tập, hãy bắt đầu với một List, sau đó cứ để cho các mã sẽ báo cho
bạn biết có cần một kiểu khác không.
* Nếu bạn chỉ cần nhóm các thứ gì đó, hãy sử dụng một Set.
* Nếu thứ tự trong vòng lặp là rất quan trọng khi duyệt qua một sưu tập, hãy sử dụng Tree… một hương vị
khác của sưu tập, khi ở đó có sẵn.
* Tránh sử dụng Vector, trừ khi bạn cần khả năng đồng bộ hóa của nó.
* Không nên lo lắng về việc tối ưu hóa cho đến khi (và trừ khi) hiệu năng trở thành một vấn đề.
Các bộ sưu tập là một trong những khía cạnh mạnh mẽ nhất của ngôn ngữ Java. Đừng ngại khi sử dụng
chúng, nhưng cần cảnh giác về các vụ “Tìm ra rồi” (gotchas). Ví dụ, có một cách thuận tiện để chuyển đổi từ
một Array thành một ArrayList:
?123456
Adult adult1 = new Adult();Adult adult2 = new Adult();Adult adult3 = new Adult(); List immutableList = Arrays.asList(new Object[] { adult1, adult2, adult3 });immutableList.add(new Adult());
Mã này đưa ra một UnsupportedOperationException, vì List được Arrays.asList() trả về là không thay đổi
được. Bạn không thể thêm một phần tử mới vào một List không thay đổi. Hãy để ý.
Các ngày tháng – Học lập trình JavaGiới thiệu
Ngôn ngữ Java sẽ mang lại cho bạn khá nhiều công cụ để xử lý các ngày tháng. Một số trong các công cụ này
gây ra sự bực bội hơn là các công cụ có sẵn trong các ngôn ngữ khác. Dù sao chăng nữa, với các công cụ mà
ngôn ngữ Java cung cấp, hầu như không có điều gì mà bạn không thể làm để tạo ra các ngày tháng và định
dạng chúng chính xác theo cách mà bạn muốn.
Tạo các ngày tháng
Khi ngôn ngữ Java còn trẻ, nó có một lớp gọi là Date khá hữu ích cho việc tạo và thao tác các ngày tháng.
Thật không may, lớp đó đã không hỗ trợ đủ tốt cho yêu cầu quốc tế hóa (internationalization), do đó Sun đã
thêm hai lớp để nhằm cải thiện tình hình:
* Lịch (Calendar).
* Định dạng ngày tháng (Dateformat).
Đầu tiên chúng ta sẽ nói về Calendar và để DateFormat lại về sau.
Việc tạo ra một Date vẫn còn tương đối đơn giản:
?1 Date aDate = new Date(System.currentTimeMillis());
Hoặc chúng ta có thể sử dụng mã này:
?1 Date aDate = new Date();
Điều này sẽ cho chúng ta một cá thể Date biểu diễn chính xác ngày tháng và giờ phút lúc ấy, theo định dạng
thời gian địa phương hiện dùng. Định dạng quốc tế hóa vượt ra ngoài phạm vi của hướng dẫn này, nhưng bây
giờ, chỉ cần biết rằng Date mà bạn nhận được tương thích với vị trí địa lý của máy tính tại chỗ của bạn.
Bây giờ khi chúng ta đã có một cá thể Date, chúng ta có thể làm gì với nó? Rất ít, nếu trực tiếp. Chúng ta có
thể so sánh một Date với Date khác để xem cái đầu là xảy ra trước-before() hay xảy ra sau-after() cái thứ hai.
Chúng ta cũng có thể thiết lập lại nó thành một thời khắc mới bằng cách gọi setTime() với một số nguyên dài
(long) biểu diễn số mili giây tính từ nửa đêm ngày1 tháng Giêng năm 1970 (đó là những gì được
System.currentTimeMillis() trả về). Ngoài ra, chúng ta bó tay.
Các lịch (Calendars)
Lớp Date bây giờ gây lộn xộn hơn là ích lợi, do hầu hết hành vi xử lý ngày tháng của nó đã lỗi thời. Bạn đã
quen việc có thể lấy ra và thiết lập (get and set) từng phần của Date (như là năm, tháng, vv). Bây giờ chúng ta
phải sử dụng cả hai Date và Calendar để làm được việc ấy. Khi chúng ta có một cá thể Date, chúng ta có thể
sử dụng Calendar để lấy ra và thiết lập từng phần của nó. Ví dụ:
?123
Date aDate = new Date(System.currentTimeMillis());Calendar calendar = GregorianCalendar.getInstance();calendar.setTime(aDate);
Ở đây chúng ta tạo ra một GregorianCalendar và thiết lập thời gian của nó bằng với Date mà chúng ta đã tạo
ra trước. Chúng ta đã có thể hoàn thành cùng một mục tiêu bằng cách gọi một phương thức khác trên
Đ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:
Mã này tạo ra một chuỗi ngày tháng đã định dạng với khuôn dạng mặc định cho địa phương này. Trong máy
tính của tôi, nó trông giống như sau:
Nov 11, 2005
Đây là kiểu dáng mặc định, nhưng nó không phải là tất cả những gì sẵn có. Chúng ta có thể sử dụng bất kỳ cái
nào trong số các kiểu dáng đã định nghĩa trước. Chúng ta cũng có thể gọi DateFormat.getTimeInstance() tđể
định dạng phần giờ phút hoặc DateFormat.getDateTimeInstance() để định dạng cả phần ngày tháng và phần
giờ phút. Đây là kết quả với các kiểu dáng khác nhau, tất cả đều là các địa phương trong nước Mỹ:
Kiểu dáng
Ngày tháng
Thời gian
Ngày tháng /Thời gian
DEFAULT
Nov 11, 2005
7:44:56 PM
Nov 11, 2005 7:44:56 PM
SHORT
11/11/05
7:44 PM
11/11/05 7:44 PM
MEDIUM
Nov 11, 2005
7:44:56 PM
Nov 11, 2005 7:44:56 PM
LONG
November 11, 2005
7:44:56 PM EST
November 11, 2005 7:44:56 PM EST
FULL
Thursday, November 11, 2005
7:44:56 PM EST
Thursday, November 11, 2005 7:44:56 PM EST
Định dạng có tùy chỉnh
Các khuôn dạng đã định nghĩa trước là tốt đẹp trong hầu hết các trường hợp, nhưng bạn cũng có thể sử dụng
SimpleDateFormat để định nghĩa các định dạng của riêng bạn. Việc sử dụng SimpleDateFormat rất dễ hiểu:
* Khởi tạo một cá thể SimpleDateFormat với một chuỗi kýtự định dạng mẫu (và một tên địa phương, nếu bạn
muốn).
* Gọi format() trên cá thể này với một Date cụ thể.
Kết quả là một chuỗi ngày tháng có định dạng. Dưới đây là một ví dụ:
?1234
Date aDate = new Date();SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");String formattedDate = formatter.format(today);System.out.println(formattedDate);
Khi bạn chạy mã này, bạn sẽ nhận được các kết quả giống như sau (tất nhiên nó sẽ phản ánh ngày tháng hiện
tại khi bạn chạy các mã):
11/05/2005
Chuỗi có ngoặc kép trong ví dụ ở trên tuân theo đúng các quy tắc cú pháp mẫu cho các mẫu định dạng ngày
tháng. Java.sun.com có một số tóm tắt tuyệt vời về các quy tắc đó (xem Tài nguyên). Dưới đây là một số quy
tắc nhỏ có ích:
* Bạn có thể chỉ rõ các mẫu cho các ngày tháng và thời gian.
* Một số cú pháp mẫu không phải là trực giác (ví dụ, mm định nghĩa mẫu phút có hai số; còn để có được viết
tắt tên tháng , bạn sử dụng MM).
* Bạn có thể chèn thêm các chữ vào trong các mẫu của bạn bằng cách đặt chúng trong một dấu nháy đơn (ví
dụ, khi sử dụng “‘on’ MM/dd/yyyy” ở trên tạo ra on 11/05/2005).
* Số các ký tự trong một thành phần văn bản của một mẫu sẽ áp đặt việc dùng dạng viết tắt hay dạng viết dài
(“MM” tạo ra 11, nhưng “MMM” tạo ra Nov và “MMMM” tạo ra November).
* Số các ký tự trong một thành phần số của một mẫu sẽ áp đặt số tối thiểu các chữ số.
Nếu các ký hiệu tiêu chuẩn của SimpleDateFormat vẫn không đáp ứng được nhu cầu định dạng tuỳ chỉnh của
bạn, bạn có thể sử dụng DateFormatSymbols để tùy chỉnh các ký hiệu cho bất cứ thành phần nào của Date
hoặc thời gian. Ví dụ, chúng ta có thể thực hiện một tập hợp duy nhất các tên viết tắt của các tháng trong năm,
như sau (sử dụng chính SimpleDateFormat như trước):
?12345678
DateFormatSymbols symbols = new DateFormatSymbols();String[] oddMonthAbbreviations = new String[] {"Ja","Fe","Mh","Ap","My","Jn","Jy","Au","Se","Oc","No","De" };symbols.setShortMonths(oddMonthAbbreviations);
formatter = new SimpleDateFormat("MMM dd, yyyy", symbols);formattedDate = formatter.format(now);System.out.println(formattedDate);
Mã này gọi một hàm tạo khác trên SimpleDateFormat, nó nhận một chuỗi ký tự mẫu và một
DateFormatSymbols định nghĩa các dạng viết tắt được sử dụng khi một tên tháng viết tắt xuất hiện trong một
mẫu. Khi chúng ta định dạng ngày tháng với các biểu tượng này, kết quả sẽ trông giống như kết quả của Date
mà chúng ta đã thấy ở trên:
No 15, 2005
Các khả năng tuỳ chỉnh của SimpleDateFormat và DateFormatSymbols sẽ là đủ để tạo ra bất kỳ định dạng nào
mà bạn cần.
Thao tác các ngày tháng
Bạn có thể tiến lên và lùi lại theo trục thời gian bằng cách tăng và giảm các ngày tháng hoặc từng phần của
chúng. Hai phương thức cho phép bạn làm điều này:
* add()
* roll()
Phương thức thứ nhất cho phép bạn cộng một số lượng (hoặc trừ đi, nếu là cộng thêm một số lượng âm) thời
gian vào một trường Date cụ thể. Việc thực hiện điều đó sẽ điều chỉnh tất cả các trường khác của Date một
cách tương ứng, dựa trên việc cộng vào một trường cụ thể. Ví dụ, giả sử chúng ta bắt đầu với ngày 15 tháng
Mười Một năm 2005 và tăng trường ngày lên thêm 20. Chúng ta có thể sử dụng mã như sau:
Cầu nối giữa các luồng byte và các luồng ký tự. Reader flavors read bytes from a byte stream and convert them to characters. The Writer chuyển đổi các ký tự thành các byte để đặt chúng vào các luồng byte.
BufferedReader andBufferedWriter
Là bộ đệm dữ liệu trong khi đọc hoặc viết một luồng khác, cho phép các hoạt động đọc và ghi có hiệu quả hơn. Bạn gói một luồng khác trong một luồng có bộ đệm.
Các luồng là một chủ đề lớn và chúng ta không thể trình bày toàn bộ chúng ở đây. Thay vào đó, chúng ta sẽ
tập trung vào các luồng được khuyến cáo dùng để đọc và viết các tệp tin. Trong hầu hết trường hợp, đây sẽ là
các luồng ký tự, nhưng chúng tôi sẽ sử dụng cả các luồng ký tự và các luồng byte để minh họa các khả năng
này.
Đọc và viết các tệp tin
Có một số cách để đọc ra từ và viết vào một File. Người ta có thể cho rằng cách tiếp cận đơn giản nhất diễn ra
như sau:
* Tạo một FileOutputStream trên File để viết vào nó.
* Tạo một FileInputStream trên File để đọc từ nó.
* Gọi read() để đọc từ File và write() để viết vào File.
* Đóng các luồng, dọn dẹp sạch sẽ nếu cần thiết.
Các mã có thể trông giống như sau:
?1 try {
File source = new File("input.txt");
2345678910111213141516
File sink = new File("output.txt"); FileInputStream in = new FileInputStream(source);FileOutputStream out = new FileOutputStream(sink);int c; while ((c = in.read()) != -1)out.write(c);
try {FileWriter writer = new FileWriter(aFile);BufferedWriter buffered = new BufferedWriter(writer);buffered.write("A line of text.n");buffered.flush();} catch (IOException e1) {e1.printStackTrace();}
8Ở đây, chúng ta tạo ra một FileWriter trên aFile, sau đó chúng ta gói nó trong một BufferedWriter. Thao tác viết
có bộ đệm hiệu quả hơn là đơn giản viết mỗi lần một byte. Khi chúng ta đã thực hiện viết từng dòng (mà chúng
ta tự kết thúc bằng n), chúng ta gọi flush() trên BufferedWriter. Nếu chúng ta không làm như vậy, chúng ta sẽ
không nhìn thấy bất kỳ dữ liệu nào trong tệp tin đích, bất chấp mọi công sức cố gắng viết tệp tin này.
Khi chúng ta có dữ liệu trong tệp tin, chúng ta có thể đọc nó bằng các mã tương tự đơn giản:
?12345678910111213
String line = null;StringBuffer lines = new StringBuffer();try {FileReader reader = new FileReader(aFile);BufferedReader bufferedReader = new BufferedReader(reader);while ( (line = bufferedReader.readLine()) != null) {lines.append(line);lines.append("n");}} catch (IOException e1) {e1.printStackTrace();}System.out.println(lines.toString());
Chúng ta tạo ra một FileReader, sau đó gói nó trong một BufferedReader. Điều đó cho phép chúng ta sử dụng
phương thức thuận tiện readLine(). Chúng ta đọc từng dòng cho đến khi không có gì còn lại, viết thêm mỗi
dòng vào cuối StringBuffer. của chúng ta. Khi đọc từ một tệp tin, một IOException có thể xảy ra, do đó chúng ta
bao quanh tất cả logic đọc tệp tin của chúng ta bằng một khối try/catch.