markdown:
Hỏi:
Thường huyết áp bao nhiêu gọi là cao, hay gọi là cần lưu ý? Tương tự, huyết áp thế nào gọi là thấp, và thấp bao nhiêu thì cần lưu ý?
Nên đo huyết áp kiểm tra vào thời điểm nào trong ngày thì chuẩn nhất và sau bao lâu thì nên đo lại?
Cần lưu ý những gì khi kiểm tra huyết áp? Xin cảm ơn bác sĩ. (nguyentam@...)
Trả lời:
Theo phân loại của Hiệp hội Tim mạch Mỹ và sự đồng thuận của Hội Tim mạch học quốc gia Việt Nam:
- Huyết áp được gọi là cao khi huyết áp tối đa (người dân hay gọi là huyết áp trên) hơn 140 mmHg, và huyết áp tối thiểu (huyết áp dưới) trên 90 mmHg.
- Còn huyết áp thấp khi huyết áp tối đa dưới 100 mmHg và huyết áp tối thiểu dưới 50 mmHg.
Lưu ý khi đo huyết áp
- Để kiểm tra thì nên đo huyết áp ít nhất hai lần trong ngày, vào buổi sáng sớm và buổi chiều.
Bình thường sẽ có sự khác biệt chút ít giữa huyết áp đo buổi sáng và buổi chiều - thông thường huyết áp buổi chiều thường cao hơn.
- Trước khi đo huyết áp cần phải nằm nghỉ ngơi hoàn toàn trong khoảng 15 phút và đo huyết áp ở tư thế nằm là chuẩn nhất.
Trong điều kiện bất khả kháng có thể đo ở tư thế ngồi và cánh tay đo huyết áp phải đặt ngang mức với vị trí của tim.
- Nếu có nghi ngờ, chưa vừa ý có thể đo lại huyết áp sau 30 phút. Không nên đo huyết áp sát nhau quá vì sẽ gây sai lệch do yếu tố tâm lý.
PGS-TS Nguyễn Hoài Nam
ĐC : 20B7 Đầm Trấu - Bạch Đằng - Hai Bà Trưng - Hà Nội
ĐT : 04. 39328166 - 3984.6134 Fax : 04. 39328167
Email : sales@haviet.com.vn
Website : www.haviet.com.vn
Dien dan thong tin bong da | Dien dan SEO | Dien dan thong tin du lich | mua máy in ở đâu tốt nhất hà nội | mực in hp | mực in chính hãng | mực in hp chính hãng | mua máy in ở đâu tốt nh


markdown:
## Trước khi bạn bắt đầu
### Về hướng dẫn này
Hướng dẫn này giới thiệu cho bạn về các khả năng của ngôn ngữ Java tinh
tế hơn những khả năng đã trình bày trong hướng dẫn "Giới thiệu về lập
trình Java" (xem
[Tài nguyên](#resources) để tìm liên
kết đến hướng dẫn này và các tài liệu khác được tham chiếu trong đó).
Để học được nhiều nhất từ hướng dẫn này, bạn nên tìm hiểu xong hướng
dẫn nhập môn đó hoặc làm quen với các khái niệm được trình bày trong
nó.
Ngôn ngữ Java cung cấp một bộ công cụ khổng lồ có thể giúp cho một lập trình viên hoàn thành hầu hết mọi nhiệm vụ. Trong hướng dẫn này, chúng tôi sẽ trình bày một số trong các công cụ cao cấp hơn thường được dùng trong các dự án phát triển Java, bao gồm như sau:
- Thừa kế và trừu tượng hóa.
- Các giao diện.
- Các lớp lồng trong.
- Các biểu thức chính quy.
- Các sưu tập.
- Ngày tháng.
- Vào/Ra (I/O).
### Các điều kiện cần trước
Nội dung của hướng dẫn này hướng tới các lập trình viên Java mới có ít kinh nghiệm, những người có thể chưa quen với một số trong nhiều đặc tính ngôn ngữ có liên quan. Nó giả định rằng bạn đã có kiến thức thông thường về việc tải về và cài đặt phần mềm và một kiến thức chung về lập trình hướng đối tượng (OOP) với ngôn ngữ Java. Bạn có thể không sử dụng các đặc tính cao cấp hơn của ngôn ngữ Java mà chúng tôi sẽ nói về chúng tại đây trong mọi ứng dụng -- và thực sự, có lẽ bạn không nên làm thế -- nhưng sẽ là sáng suốt khi một lập trình viên chuyên nghiệp biết về chúng và có thể sử dụng chúng khi phù hợp.
Ngoài việc làm quen với các khái niệm được trình bày trong hướng dẫn
"Giới thiệu về lập trình Java", xem
[Tài
nguyên](#resources)), bạn sẽ cần phải cài đặt các phần sau đây để chạy các
ví dụ hay mã mẫu trong hướng dẫn này:
- JDK 1.4.2 hoặc cao hơn (khuyển cáo sử dụng phiên bản 5.0).
- Môi trường phát triển tích hợp (IDE) Eclipse.
Tất cả các mã ví dụ trong hướng dẫn này đã được kiểm tra với JDK 5.0
trên nền tảng Windows XP, nhưng nó cũng sẽ hoạt động được mà không cần
sửa đổi khi sử dụng JDK 1.4.x. Bạn có thể tải về mã nguồn cho hướng
dẫn từ phần
[Tài nguyên](#resources). Nó có chứa trong
tệp tin intermediate.jar mà bạn có thể nhập khẩu vào trong vùng làm
việc trong Eclipse của bạn.
Các tệp tin JAR mẫu không nhất thiết phải chứa mã của mọi ví dụ trong hướng dẫn này ở dạng hoàn tất cuối cùng. Thay vào đó, nó chứa các cốt lõi của những gì mà chúng tôi sẽ trình bày, trừ một số các sửa đổi dần từng bước mà chúng tôi sẽ áp dụng cho các mã theo diễn tiến của bài viết này. Việc sửa đổi phần mã cốt lõi để khám phá các đặc tính ngôn ngữ mà chúng tôi sẽ trình bày trong hướng dẫn này được dành lại như là một bài tập cho bạn.
## Thừa kế và trừu tượng hóa
### Thừ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:
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:
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:
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:
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:
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:
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:
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ụ:
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:
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 đó.
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."); } }
Chúng ta đã thực hiện những gì trong bản Listing này?
- Chúng ta đã thay đổi
Personlà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
Adultthành lớp con của
Personvà triển khai thực hiện các phương thức.
- Chúng ta làm cho
Babythành lớp con của
Personvà 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:
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:
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
move().
### 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 (xem [Tài nguyên](#resources)).
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
### Mộ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:
public interface
interfaceName{ final constantType constantName= constantValue; ... returnValueType methodName( 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:
public interface
interfaceNameextends 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:
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:
public class
classNameextends superclassNameimplements interfaceName, ... { 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:
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:
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:
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:
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:
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
### Mộ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:
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:
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; } }
Chúng ta sẽ định nghĩa lớp này bên trong
Adult, giống như sau:
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:
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
MotionListenermà ứ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
Listcủ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
Adultvà 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:
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:
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:
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:
anAdult.addMotionListener(new MotionListener() { public void handleMove(Adult eventSource) { System.out.println("This Adult moved: \n" + eventSource.toString()); } });
Thay vì có
CommunityApplication triển khai
thực hiện
MotionListener, chúng ta khai báo
một lớp bên trong không đặt tên (và như vậy là vô danh) có kiểu
MotionListener và đã cung cấp cho nó một
triển khai thực hiện
handleMove(). Sự việc
MotionListener là một giao diện, không
phải là một lớp, là không quan trọng. Cả hai đều có thể chấp nhận
được.
Mã này sinh ra chính xác cùng một kết quả giống như các phiên bản trước đó, nhưng nó sử dụng một cách tiếp cận phổ biến và đáng mong muốn hơn. Bạn sẽ hầu như luôn luôn thấy các trình xử lý sự kiện được triển khai thực hiện với các lớp bên trong vô danh.
### Sử dụng các lớp lồng trong
Các lớp có lồng trong có thể rất có ích cho bạn. Chúng cũng có thể gây ra phiền toái.
Sử dụng một lớp lồng trong sẽ không ý nghĩa lắm khi có thể định nghĩa
lớp ở bên ngoài của một lớp bao ngoài. Trong ví dụ của chúng ta, chúng
ta đã có thể định nghĩa
Wallet ở bên ngoài
Adult mà không cảm thấy quá tệ. Nhưng
hãy tưởng tượng một thứ gì đó kiểu như một lớp
Personality. Bạn có bao giờ có một Personality bên ngoài
một cá thể
Person không?. Không, do đó hoàn
toàn cần thiết phải định nghĩa Personality như là một lớp lồng trong.
Một quy tắc ngón tay cái đúng đắn là bạn nên định nghĩa một lớp dưới
dạng lớp không lồng trong cho đến khi rõ ràng là nó phải được lồng
trong, sau đó cấu trúc lại để lồng nó.
Các lớp bên trong vô danh là cách tiếp cận tiêu chuẩn cho các trình xử lý sự kiện, vì vậy, hãy sử dụng chúng cho mục đích đó. Trong các trường hợp khác, cần thận trọng với chúng. Trừ khi các lớp bên trong vô danh là nhỏ, xoay quanh một việc, và quen thuộc, chúng làm cho mã khó hiểu. Chúng cũng có thể làm cho việc gỡ lỗi khó khăn hơn, mặc dù IDE của Eclipse giúp giảm thiểu sự phiền toái đó. Nói chung, hãy thử không sử dụng các lớp bên trong vô danh cho bất cứ thứ gì trừ các trình xử lý sự kiện.
## Các biểu thức chính quy
### Một biểu thức chính quy là gì?
Một
*biểu thức chính quy* (regular expression) về bản chất là một
mẫu để mô tả một tập hợp các chuỗi ký tự chia sẻ chung mẫu này. Ví dụ,
đây là một tập hợp các chuỗi ký tự có một số điều chung:
- một chuỗi (a string).
- một chuỗi dài hơn (a longer string).
- một chuỗi rất dài (a much longer string).
Mỗi chuỗi ký tự này đều bắt đầu bằng "a" và kết thúc bằng "string." API của các biểu thức chính quy của Java (Java Regular Expression) giúp bạn thể hiện điều đó và làm nhiều việc lý thú với các kết quả.
API của biểu thức chính quy (Regular Expression - hoặc viết tắt là
*regex*) của Java là khá giống với các công cụ biểu thức
chính quy có sẵn trong ngôn ngữ Perl. Nếu bạn là một lập trình viên
của Perl, bạn sẽ cảm thấy đúng như đang ở nhà, ít nhất là với cú pháp
mẫu biểu thức chính quy của ngôn ngữ Java. Tuy nhiên, nếu bạn không
thường sử dụng biểu thức chính quy , chắc là nó có vẻ trông hơi lạ một
chút. Đừng lo lắng: không phải phức tạp như nó có vẻ thế đâu.
### API của biểu thức chính quy
Năng lực về biểu thức chính quy của ngôn ngữ Java gồm có ba lớp cốt lõi mà bạn sẽ sử dụng hầu như mọi lúc:
Pattern, trong đó mô tả một mẫu chuỗi ký tự.
Matcher, để kiểm tra một chuỗi ký tự xem nó có khớp với mẫu không.
PatternSyntaxException, để báo cho bạn rằng một số thứ không thể chấp nhận được với mẫu mà bạn đã thử định nghĩa.
Cách tốt nhất để tìm hiểu về biểu thức chính quy là qua các ví dụ, do
đó trong phần này chúng ta sẽ tạo ra một ví dụ đơn giản trong
CommunityApplication.main(). Tuy nhiên, trước
khi chúng ta tiến hành, điều quan trọng là hiểu được một số cú pháp
mẫu biểu thức chính quy . Chúng ta sẽ thảo luận điều đó chi tiết hơn
trong phần kế tiếp.
### Cú pháp mẫu
Một
*mẫu* (pattern) biểu thức chính quy mô tả cấu trúc của chuỗi
ký tự mà một biểu thức sẽ cố gắng tìm kiếm trong một chuỗi ký tự đầu
vào. Đây chính là tại sao biểu thức chính quy nhìn có vẻ hơi lạ
thường. Tuy nhiên, một khi bạn hiểu được cú pháp, để giải mã sẽ ít khó
khăn hơn.
Dưới đây là một số trong các cấu kiện mẫu phổ biến nhất mà bạn có thể sử dụng trong các chuỗi ký tự mẫu:
|Cấu kiện ||Cái được coi là ăn khớp |
|Bất kỳ ký tự nào. |
|Không (0) hoặc một (1) của ký tự đứng trước. |
|Không (0) hoặc lớn hơn của ký tự đứng trước. |
|Một (1) hoặc lớn hơn của ký tự đứng trước. |
|Một dải các ký tự hay chữ số. |
|Không phải cái tiếp sau (tức là, "không phải <cái gì
đó>").|
|Bất kỳ số nào (tùy chọn, [0-9]).|
|Bất kỳ cái gì không là số (tùy chọn, [^0-9]).|
|Bất kỳ khoảng trống nào (tùy chọn, [
\n\t\f\r]).|
|Không có bất kỳ khoảng trống nào (tùy chọn, [^
\n\t\f\r]).|
|Bất kỳ từ nào (tùy chọn, [a-zA-Z_0-9]).|
|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:
Pattern pattern = Pattern.compile("a.*string"); Matcher matcher = pattern.matcher("a string"); boolean didMatch = matcher.matches(); System.out.println(didMatch); int patternStartIndex = matcher.start(); System.out.println(patternStartIndex); int patternEndIndex = matcher.end(); System.out.println(patternEndIndex);
Trước tiên, chúng ta tạo ra một
Pattern.
Chúng ta làm điều đó bằng cách gọi
compile(), một phương thức tĩnh trên
Pattern, với một chữ chuỗi ký tự biểu diễn mẫu mà chúng ta
muốn so khớp. Chữ đó sử dụng cú pháp mẫu biểu thức chính quy mà bây
giờ chúng ta đã có thể hiểu được. Trong ví dụ này, khi dịch thành ngôn
ngữ thông thường, mẫu biểu thức chính quy có nghĩa là: "Tìm một chuỗi
ký tự có dạng bắt đầu là 'a', theo sau là không hay nhiều ký khác, kết
thúc bằng 'string'".
Tiếp theo, chúng ta gọi
matcher() trên
Pattern của chúng ta. Lời gọi này tạo ra một
cá thể
Matcher. Khi điều đó xảy ra,
Matcher tìm kiếm chuỗi ký tự mà chúng ta đã
chuyển cho nó để so khớp với chuỗi mẫu mà chúng ta đã dùng để tạo ra
Pattern Như bạn đã biết, mọi chuỗi ký
tự trong ngôn ngữ Java là một sưu tập các ký tự có đánh chỉ số, bắt
đầu từ 0 và kết thúc bằng độ dài chuỗi trừ một.
Matcher phân tích cú pháp của chuỗi ký tự, bắt đầu từ 0 và
tìm các kết quả khớp với mẫu.
Sau khi hoàn tất quá trình đó,
Matcher chứa
rất nhiều thông tin về các kết quả khớp đã được tìm thấy (hoặc không
tìm thấy) trong chuỗi đầu vào của chúng ta. Chúng ta có thể truy cập
thông tin đó bằng cách gọi các phương thức khác nhau trên
Matcher của chúng ta::
matches()đơn giản cho chúng ta biết rằng toàn bộ chuỗi đầu vào có khớp đúng với mẫu hay không.
start()cho chúng ta biết giá trị chỉ số trong chuỗi ở đó bắt đầu khớp đúng với mẫu.
end()cho chúng ta biết giá trị chỉ số ở đó kết thúc khớp đúng với mẫu, cộng với một.
Trong ví dụ đơn giản của chúng ta, có một kết quả khớp bắt đầu từ 0 và
kết thúc tại 7. Vì vậy, lời gọi
matches()
trả về kết quả
đúng (true), lời gọi
start() trả về 0 và lời gọi
end() trả về 8. Nếu trong chuỗi ký tự của
chúng ta có nhiều ký tự hơn trong mẫu mà chúng ta tìm kiếm, chúng ta
có thể sử dụng
lookingAt() thay cho
matches().
lookingAt() tìm kiếm chuỗi con khớp với mẫu của chúng ta.
Ví dụ, hãy xem xét chuỗi ký tự sau đây:
Here is a string with more than just the pattern.
Chúng ta có thể tìm kiếm mẫu
a.*string và có
được kết quả khớp nếu chúng ta sử dụng
lookingAt(). Nếu chúng ta sử dụng
matches() để thay thế, nó sẽ trả về kết quả là
sai (false), bởi vì có nhiều thứ trong chuỗi
đầu vào hơn là đúng những gì có trong mẫu.
### Các mẫu phức tạp
Những việc tìm kiếm đơn giản là dễ dàng với các lớp biểu thức chính quy, nhưng cũng có thể tìm kiếm tinh vi hơn nhiều.
Bạn có thể đã quen thuộc với
*wiki,* một hệ thống dựa trên web cho
phép người dùng chỉnh sửa các trang web để làm nó "lớn lên". Các Wiki,
cho dù được viết bằng ngôn ngữ Java hay không, hầu như hoàn toàn được
dựa trên các biểu thức chính quy. Nội dung của chúng được dựa trên
chuỗi ký tự mà người sử dụng nhập vào, được các biểu thức chính quy
phân tích cú pháp và định dạng. Một trong những đặc tính nổi bật nhất
của các wiki là ở chỗ bất kỳ người dùng nào cũng có thể tạo ra một
liên kết đến một chủ đề khác trong wiki bằng cách nhập vào một *từ
wiki *, mà thường là một loạt các từ được móc nối với nhau, mỗi
một từ trong đó bắt đầu bằng một chữ cái viết hoa, như sau:
MyWikiWord
Giả sử có chuỗi ký tự sau:
Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.
Bạn có thể tìm kiếm các từ wiki trong chuỗi này với mẫu biểu thức chính quy như sau:
[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); 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:
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ừ:
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("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:
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():
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():
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) + "!"); else System.out.println("I didn't understand."); }
Phương thức này cho phép chúng ta tiến hành một số
cuộc đối thoại với một
Adult.
Nếu chuỗi ký tự đó có dạng đặc biệt,
Adult
của chúng ta có thể trả lời bằng một lời chào tốt lành. Nếu không, nó
có thể nói rằng nó không hiểu được.
Phương thức
listen() kiểm tra chuỗi ký tự
đầu vào để xem nó có khớp với một mẫu nhất định không: một hay nhiều
ký tự, theo sau là "tên tôi là" (my name is), tiếp theo là một hoặc
nhiều ký tự, tiếp theo là một dấu chấm câu. Chúng ta sử dụng
lookingAt() để tìm kiếm một chuỗi con của đầu
vào khớp với mẫu. Nếu chúng ta tìm thấy một kết quả khớp, chúng ta xây
dựng một chuỗi ký tự làm lời chào bằng cách nắm bắt lấy những gì đi
sau " my name is ", mà chúng ta cho rằng đó sẽ là tên (đó là những gì
nhóm 1 sẽ chứa). Nếu chúng ta không tìm thấy một kết quả khớp nào,
chúng ta trả lời rằng chúng ta không hiểu. Dĩ nhiên là
Adult của chúng ta không có nhiều khả năng
đối thoại lắm vào lúc này.
Đây là một ví dụ tầm thường về các khả năng xử lý biểu thức chính quy của ngôn ngữ Java, nhưng nó minh họa cách làm thế nào để sử dụng chúng.
### Làm rõ các biểu thức
Các biểu thức chính quy có vẻ bí hiểm. Rất dễ bị thất bại với mã trông rất giống như tiếng Phạn ấy. Đặt tên các thứ cho đúng và xây dựng các biểu thức cho tốt cũng có thể giúp đỡ nhiều.
Ví dụ, đây là mẫu của chúng ta cho một từ wiki:
[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:
public void listen(String conversation) { Pattern pattern = Pattern.compile(WIKI_WORD); Matcher matcher = pattern.matcher(conversation); ... }
Một thủ thuật khác có thể trợ giúp là định nghĩa các hằng số cho mỗi phần của các mẫu, sau đó xây dựng các mẫu phức tạp hơn như việc lắp ráp các phần có tên. Nói chung, mẫu càng phức tạp thì càng khó khăn để giải mã nó và càng dễ xảy ra lỗi hơn. Bạn sẽ thấy rằng không có cách thực sự nào để gỡ lỗi các biểu thức chính quy khác hơn cách thử nghiệm và sửa lỗi. Hãy làm cho cuộc sống đơn giản hơn bằng cách đặt tên các mẫu và các thành phần mẫu.
## Các sưu tập
### Giớ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ử
Objectcó 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|
|Set|
|Map|
Tất cả các giao diện trong khung công tác, trừ
Map là các giao diện con của giao diện
Collection, trong đó định nghĩa cấu trúc chung nhất của
một sưu tập. Mỗi sưu tập gồm
*nhiều phần tử*. Với vai trò là
trình thực hiện các giao diện con của
Collection, tất cả kiểu sưu tập chia sẻ chung (theo trực
giác) một số hành vi:
- Các phương thức để mô tả kích thước của sưu tập (như
size()và
isEmpty()).
- Các phương thức để mô tả nội dung của sưu tập (như
contains()và
containsAll()).
- Các phương thức để hỗ trợ thao tác về nội dung của sưu tập (như
add(),
remove()và
clear()).
- Các phương thức để cho phép bạn chuyển đổi một sưu tập thành một
mảng (như
toArray()).
- Một phương thức để cho phép bạn nhận được một trình vòng lặp
(iterator) trên mảng các phần tử (
iterator()).
Chúng ta sẽ nói về một số phương thức trên trong phần này. Đồng thời
chúng ta sẽ thảo luận
*trình vòng lặp* (iterator) là gì và cách
sử dụng nó như thế nào.
Lưu ý rằng các
Map là đặc biệt. Thật sự
chúng hoàn toàn không là một sưu tập. Tuy nhiên, chúng có hành vi rất
giống các sưu tập, vì vậy chúng ta cũng nói về chúng trong phần
này.
### Các triển khai thực hiện Danh sách (List)
Các phiên bản cũ hơn của JDK chứa một lớp được gọi là
Vector. Nó vẫn còn có trong các phiên bản mới
hơn, nhưng bạn chỉ nên sử dụng nó khi bạn cần có một sưu tập
*đồng
bộ hoá* -- đó là, một trong những yếu tố là an toàn phân luồng.
(Nói về phân luồng đã vượt ra ngoài phạm vi của bài viết này, chúng ta
sẽ thảo luận ngắn gọn về khái niệm ấy trong phần [Tóm tắt](#summary)). 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:
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:
public void spendMoney(int aBill) { this.wallet.removeBill(aBill); }
Phương thức này gọi
removeBill() trên
Wallet:
protected void removeBill(int aBill) { Iterator iterator = bills.iterator(); 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:
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:
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:
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:
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); 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:
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ể:
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:
public void addNickname(String aNickname) { nicknames.add(aNickname); }
Bây giờ hãy thử chạy mã này:
Adult anAdult = new Adult(); 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ể:
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:
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ẻ:
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:
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.
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 |
|Trả lời |
|Trả lời |
|Trả về một |
|Trả về một |
|Trả về một |
|Cho phép bạn loại bỏ giá trị cho một khóa đã cho.
|
|Trả lời |
Một số trong các phương thức này, chẳng hạn như
isEmpty() chỉ là để cho tiện thôi, nhưng một số là rất
quan trọng. Ví dụ, cách duy nhất để thực hiện vòng lặp qua các phần tử
trong một
Map là thông qua một trong các
tập hợp có liên quan (tập hợp các khóa, các giá trị, hoặc các cặp
khóa-giá trị).
### Lớp Các sưu tập (Collections)
Khi bạn đang sử dụng khung công tác các sưu tập Java, bạn cần phải nắm
được những gì có sẵn trong lớp
Collections.
Lớp này gồm có một kho lưu trữ các phương thức tĩnh để hỗ trợ các thao
tác trên sưu tập. Chúng tôi sẽ không trình bày tất cả chúng ở đây, bởi
vì bạn có thể tự mình đọc API, nhưng chúng tôi sẽ trình bày hai phương
thức thường xuyên xuất hiện trong mã Java:
copy()
sort()
Phương thức đầu tiên cho phép bạn sao chép các nội dung của một sưu tập này tới một sưu tập khác, như sau:
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:
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:
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:
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; 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:
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:
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; comparison = adult1.lastname.compareTo(adult2.lastname); if (comparison != EQUAL) return comparison; return EQUAL; } } public class CommunityApplication { public static void main(String[] args) { Adult adult1 = new Adult(); adult1.setFirstname("Bob"); adult1.setLastname("Smith"); Adult adult2 = new Adult(); adult2.setFirstname("Al"); adult2.setLastname("Jones"); List adults = new ArrayList(); adults.add(adult1); adults.add(adult2); Collections.sort(adults, new AdultComparator()); System.out.println(adults); } }
Bạn sẽ thấy "Al Jones" và "Bob Smith", theo thứ tự đó, trong cửa sổ màn hình của bạn.
Có một số lý do thích đáng để sử dụng cách tiếp cận thứ hai. Các lý do
kỹ thuật vượt ra ngoài phạm vi của hướng dẫn này. Tuy nhiên, từ viễn
cảnh của phát triển hướng đối tượng, đây có thể là một ý tưởng tốt khi
tách biệt phần mã so sánh vào trong đối tượng khác, hơn là cung cấp
cho mỗi
Adult khả năng tự so sánh với nhau.
Tuy nhiên, vì đây thực sự là những gì mà
equals() thực hiện, mặc dù kết quả là toán tử boolean, có
các lập luận thích hợp ủng hộ cho cả hai cách tiếp cận.
### Sử dụng các sưu tập
Khi nào bạn nên sử dụng một kiểu sưu tập cụ thể ? Đó là một phán xét cần đến năng lực của bạn, và chính vì thế mà bạn hy vọng sẽ được trả lương hậu hĩ khi là một lập trình viên.
Bất chấp những gì mà nhiều chuyên gia tin tưởng, có rất ít các quy tắc
chắc chắn và nhanh chóng để xác định cần sử dụng những lớp nào trong
một tình huống đã cho nào đó. Theo kinh nghiệm cá nhân của tôi, trong
phần lớn các lần khi sử dụng các sưu tập, một
ArrayList hoặc một
HashMap (hãy
nhớ, một
Map không thật sự là một sưu tập)
đều bị chơi khăm. Rất có khả năng, bạn cũng từng có trải nghiệm như
vậy. Dưới đây là một số quy tắc ngón tay cái, một số là hiển nhiên hơn
những cái còn lại:
- Khi bạn nghĩ rằng mình cần có một sưu tập, hãy bắt đầu với một
List, sau đó cứ để cho các mã sẽ báo cho bạn biết có cần một kiểu khác không.
- Nếu bạn chỉ cần nhóm các thứ gì đó, hãy sử dụng một
Set.
- Nếu thứ tự trong vòng lặp là rất quan trọng khi duyệt qua một sưu
tập, hãy sử dụng
Tree...một hương vị khác của sưu tập, khi ở đó có sẵn.
- Tránh sử dụng
Vector, trừ khi bạn cần khả năng đồng bộ hóa của nó.
- Không nên lo lắng về việc tối ưu hóa cho đến khi (và trừ khi) hiệu năng trở thành một vấn đề.
Các bộ sưu tập là một trong những khía cạnh mạnh mẽ nhất của ngôn ngữ
Java. Đừng ngại khi sử dụng chúng, nhưng cần cảnh giác về các vụ "Tìm
ra rồi" (gotchas). Ví dụ, có một cách thuận tiện để chuyển đổi từ một
Array thành một
ArrayList:
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
### Giớ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:
Date aDate = new Date(System.currentTimeMillis());
Hoặc chúng ta có thể sử dụng mã này:
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ụ:
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:
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:
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
Datemà 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ó:
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:
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|
|Nov 11, 2005||7:44:56 PM||Nov 11, 2005 7:44:56 PM|
|11/11/05||7:44 PM||11/11/05 7:44 PM|
|Nov 11, 2005||7:44:56 PM||Nov 11, 2005 7:44:56 PM|
|November 11, 2005||7:44:56 PM EST||November 11, 2005 7:44:56 PM EST|
|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ể
SimpleDateFormatvớ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
Datecụ 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ụ:
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](#resources)). 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
Novvà
"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):
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:
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:
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():
roll(int field, boolean up)
roll(int field, int amount)
Chúng ta đã sử dụng dạng thứ nhất. Để giảm một trường bằng cách sử dụng
dạng này, bạn gán giá trị
false cho đối số
thứ hai. Dạng thứ hai của phương thức cho phép bạn định rõ số lượng
tăng lên hay giảm đi. Nếu một hành động cuộn tạo ra một giá trị ngày
tháng không hợp lệ (ví dụ, 09/31/2005), các phương thức này điều chỉnh
các trường khác cho phù hợp, dựa trên các giá trị hợp lệ lớn nhất và
nhỏ nhất cho các ngày tháng, giờ, vv. Bạn có thể cuộn về phía trước
dùng các giá trị dương và cuộn lùi lại khi dùng một giá trị âm.
Thử dự đoán hành động cuộn của bạn sẽ làm gì cũng tốt, và bạn chắc chắn có thể đã làm như thế, nhưng thường thì cách tốt nhất vẫn là thử và sửa lỗi. Đôi khi bạn sẽ đoán đúng, nhưng đôi khi bạn sẽ phải thử nghiệm để xem cái gì sẽ tạo ra kết quả đúng.
### Sử dụng các ngày tháng
Mọi người đều có một ngày sinh. Hãy thêm một ngày sinh vào lớp
Person của chúng ta. Trước tiên, chúng ta
thêm một biến cá thể vào
Person:
protected Date birthdate = new Date();
Tiếp theo, chúng ta thêm các phụ kiện cho biến:
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:
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:
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);
Chúng ta đặt một
ngày sinh cho một
Adult vào ngày 23 tháng Ba năm 1971. Nếu
chúng ta chạy mã này trong tháng Giêng năm 2005, chúng ta sẽ nhận được
kết quả này:
An Adult with: Age: 33 Name: firstname lastname Gender: MALE Progress: 0
Có một vài chi tiết vụn vặt khác tôi dành lại cho các bạn như là bài tập:
- Cập nhật
compareTo()trên
Adultđể phản ánh sự hiện diện của một biến cá thể mới.
- Nếu ta đã triển khai thực hiện nó, chúng ta sẽ phải cập nhật
equals()cho
Adultđể phản ánh sự hiện diện của một biến cá thể mới.
- Nếu chúng ta đã triển khai thực hiện
equals(), chúng ta cũng đã phải triển khai thực hiện
hashCode()và chúng ta sẽ phải cập nhật
hashCode()để phản ánh sự hiện diện của một biến cá thể mới.
## VÀO/RA (I/O)
### Giớ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:
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:
aFile.exists();
Nếu nó không tồn tại, chúng ta có thể tạo nó:
aFile.createNewFile();
Khi sử dụng các phương thức khác trên
File,
chúng ta cũng có thể xóa các tệp tin, tạo các thư mục, xác định xem
một tài nguyên trong hệ thống tệp tin là một thư mục hay là một tệp
tin, v.v.. Tuy nhiên, hoạt động sẽ thực sự xảy ra, khi chúng ta ghi
vào và đọc ra từ tệp tin. Để làm điều đó, chúng ta cần phải hiểu thêm
một chút về các luồng.
### Các luồng
Chúng ta có thể truy cập các tệp tin trên hệ thống tệp tin bằng cách sử
dụng
*các luồng* (streams). Ở mức độ thấp nhất, các luồng cho
phép một chương trình nhận các byte từ một nguồn và/hoặc để gửi kết
quả đến một đích đến. Một số luồng xử lý tất cả các loại ký tự 16-bit,
các ký tự (đó là các luồng kiểu
Reader và
Writer). Các luồng khác xử lý chỉ các
byte 8-bit (đó là các luồng kiểu
InputStream và
OutputStream).
Trong các hệ thống thứ bậc này có một số luồng với hương vị khác (tất
cả nằm trong gói
java.io). Ở mức độ trừu
tượng cao nhất, có
*các luồng ký tự* và *các luồng byte*.
Các luồng byte đọc (
InputStream và các lớp
con của nó) và viết (
OutputStream và các
lớp con của nó) các byte 8-bit. Nói cách khác, các luồng byte có thể
được coi là một kiểu luồng thô hơn. Kết quả là, rất dễ dàng để hiểu lý
do tại sao hướng dẫn Java.sun.com về các lớp ngôn ngữ Java chủ yếu
(xem
[Tài nguyên](#resources)) 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ụng|
|Đọc các byte từ một tệp tin và ghi các byte vào một tệp tin.
|
|Đọ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ụng|
|Các luồng này đọc và viết các ký tự từ/đến các |
|Cầu nối giữa các luồng byte và các luồng ký tự. |
|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
FileOutputStreamtrên
Fileđể viết vào nó.
- Tạo một
FileInputStreamtrên
Fileđể đọc từ nó.
- Gọi
read()để đọc từ
Filevà
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:
try { File source = new File("input.txt"); 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](#resources)). 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
FileWritertrên
File.
- Gói
FileWritertrong một
BufferedWriter.
- Gọi
write()trên
BufferedWritermỗ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:
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(); }
Ở đâ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:
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.
## Phần tóm tắt
### Tóm tắt
Chúng tôi đã trình bày một phần quan trọng của ngôn ngữ Java trong
hướng dẫn "Giới thiệu về lập trình Java" (xem
[Tài nguyên](#resources)) và hướng dẫn này, nhưng ngôn ngữ Java là rất lớn
nên chỉ một hướng dẫn nguyên khối (hoặc thậm chí một số hướng dẫn nhỏ
hơn) không thể bao gồm tất cả mọi vấn đề. Dưới đây nhặt ra một số lĩnh
vực mà chúng tôi còn chưa nêu ra:
|Chủ đề ||Mô tả ngắn gọn|
Threads |Có một chương trình chỉ thực hiện một việc tại một thời điểm
có thể là có ích, nhưng hầu hết các chương trình ngôn ngữ Java
tinh vi có |nhiều luồng (threads) thực thi chạy cùng một
lúc. Ví dụ, các tác vụ in ấn hoặc tìm kiếm có thể chạy ở mặt
sau. Lớp
Reflection |Một trong những khía cạnh mạnh của ngôn ngữ Java (và thường là
một trong những điều ám ảnh nhất) là |sự phản chiếu hay
khả năng để xem thông tin về chính mã của bạn. Gói
NIO |Kể từ JDK 1.4, ngôn ngữ Java đã kết hợp một số khả năng I/O
tinh vi hơn, dựa trên một API hoàn toàn mới gọi là |I/O
mới hoặc viết tắt là NIO. Sự khác biệt chính là
ở chỗ I/O của ngôn ngữ Java truyền thống là dựa trên các luồng
(như chúng ta đã thảo luận ở trên), trong khi NIO dựa trên một
khái niệm gọi là khối I/O, các kênh và các bộ
đệm. Khối I/O này có xu hướng có hiệu quả hơn
việc gửi từng byte đơn lẻ thông qua một luồng. Mặt hạn chế là
NIO là khái niệm khó. "
Sockets |Các chương trình ngôn ngữ Java của bạn có thể giao tiếp với
hầu như bất kỳ chương trình nào trên một thiết bị có khả năng
IP. Tất cả những gì bạn cần làm là mở một kết nối |ổ cắm
(socket) với một địa chỉ IP và một cổng trên thiết bị đó.
API các ổ cắm (Sockets API) của ngôn ngữ Java hỗ trợ việc này.
Xem hướng dẫn "
Swing |Ngôn ngữ Java bao gồm các hỗ trợ rộng lớn cho việc phát triển
GUI dưới dạng Swing. Bộ Swing của các API bao gồm các lớp cho
các vật dụng và các phần tử khác để tạo ra các giao diện người
dùng có đầy đủ đặc tính. Có một số tài nguyên quan trọng trên
developerWorks liên quan đến Swing, nhưng một điểm thích hợp
để bắt đầu là bài viết giới thiệu "|**JNI** *Giao diện bản địa Java*(JNI). API này cho phép bạn chuyển đổi các lời gọi phương thức của Java thành các lời gọi đến các hàm C (để tương tác với các hệ điều hành và tương tự). Hướng dẫn " [Lập trình Java với JNI](http://www.ibm.com/developerworks/edu/j-dw-javajni-i.html?S_TACT=105AGY75)" của developerWorks thảo luận về các cơ cấu của JNI với mã Java, C và C++. **RMI** *các cuộc gọi phương thức phân tán*giữa các chương trình đang chạy trong các máy ảo Java khác nhau. Hướng dẫn " [Các đối tượng phân tán của Java: Sử dụng RMI và CORBA](http://www.ibm.com/developerworks/edu/j-dw-javarmi-i.html?S_TACT=105AGY75)" của Brad Rubin cung cấp một sự mở đầu vững chắc về RMI và thảo luận về RMI và CORBA cùng với nhau. Bạn cũng nên xem [bài viết này](http://www.ibm.com/developerworks/java/library/j-rmiframe/?S_TACT=105AGY75)của Edward Harned để tìm hiểu lý do tại sao RMI *không phải*là một máy chủ ứng dụng làm sẵn. **Security** [Diễn đàn thảo luận bảo mật Java](http://www.ibm.com/developerworks/forums/dw_forum.jsp?forum=178&cat=10&S_TACT=105AGY75)được John Peck, một lập trinh viên và chuyên gia bảo mật Java kỳ cựu, chủ trì; " [Bên trong cơ chế cấp phép Java](http://www.ibm.com/developerworks/java/library/j-javaauth/?S_TACT=105AGY75)" của Abhijit Belapurkar; và " [Bảo mật Java, Phần 1: Những điều căn bản về mật mã hóa](http://www.ibm.com/developerworks/edu/j-dw-javasec1-i.html?S_TACT=105AGY75)" và " [Bảo mật Java , Phần 2: Xác thực và cấp phép](http://www.ibm.com/developerworks/edu/j-dw-javasec2-i.html?S_TACT=105AGY75)" cả hai đều của Brad Rubin.
## Tài nguyên
### Học tập
- Đọc "
[Introduction to Java programming](http://www.ibm.com/developerworks/edu/j-dw-java-intjava-i.html?S_TACT=105AGY75)" của Roy Miller về những điều căn bản của lập trình Java. (developerworks, 11.2004)
- Trang web
[java.sun.com](http://java.sun.com)có liên kết đến tất cả mọi thứ về lập trình Java. Bạn có thể tìm thấy ở đó tất cả các tài nguyên "chính thức" về ngôn ngữ Java mà bạn cần có, bao gồm các đặc tả ngôn ngữ và tài liệu hướng dẫn API. Bạn cũng có thể có thể tìm thấy các liên kết đến các bài hướng dẫn tuyệt vời trên nhiều khía cạnh khác nhau của ngôn ngữ Java, ngoài các hướng dẫn cơ bản.
- Truy cập vào trang
[Sun's Java documentation page](http://java.sun.com/docs/index.html), để có một liên kết đến tài liệu hướng dẫn API cho mỗi phiên bản SDK.
- Xem bài báo xuất sắc của John Zukowski về
biểu thức chính quy với ngôn ngữ Java
[tại đây](http://www.ibm.com/developerworks/java/library/j-mer0827.html?S_TACT=105AGY75). Đây chỉ là một bài viết trong chuyên mục [Magic with Merlin](http://www.ibm.com/developerworks/views/java/libraryview.jsp?search_by=merlin:&S_TACT=105AGY75)của ông. [Sun Java tutorial](http://java.sun.com/docs/books/tutorial/index.html)là một nguồn tài nguyên tuyệt vời. Đó là là một hướng dẫn nhẹ nhàng về ngôn ngữ, nhưng cũng trình bày về nhiều chủ đề được nói đến trong hướng dẫn này. Nếu không có gì khác, đó là một nguồn tài nguyên tốt cho các ví dụ và cho các đường liên kết tới các hướng dẫn khác có mô tả chi tiết hơn về các khía cạnh khác nhau của ngôn ngữ này.
- Trang web Java.sun.com có một số tóm tắt
xuất sắc về các quy tắc mẫu ngày tháng
[ở đây](http://java.sun.com/docs/books/tutorial/i18n/format/datepattern.html)và tìm hiểu thêm về ví dụ
CopyBytes.java
[ở đây](http://java.sun.com/docs/books/tutorial/essential/io/example/CopyBytes.java).
- Trang
[developerWorks New to Java technology page](http://www.ibm.com/developerworks/java/newto/?S_TACT=105AGY75)là một trang ngân hàng trao đổi thông tin dành cho các nguồn tài nguyên của developerWorks cho các nhà phát triển mới bắt đầu với Java, bao gồm cả các đường liên kết tới hướng dẫn và các nguồn tài nguyên có chứng nhận.
- Bạn sẽ tìm thấy các bài viết về mọi khía
cạnh của lập trình Java trong
[Java technology zone](http://www.ibm.com/developerworks/java/)của developerWorks.
- Cũng có thể xem trang
[Java technology zone tutorials](http://www.ibm.com/developerworks/views/java/libraryview.jsp?type_by=Tutorials&S_TACT=105AGY75)để có được một liệt kê đầy đủ về các hướng dẫn xoay quanh Java, truy cập miễn phí trên [developerWorks](http://www.ibm.com/developerworks/?S_TACT=105AGY75).
### Lấy sản phẩm và công nghệ
- Tải về tệp tin
[intermediate.jar](intermediate.jar)đi kèm theo hướng dẫn này.
- Bạn có thể tải về
Eclipse từ các
[trang web Eclipse](http://www.eclipse.org). Hoặc, dễ dàng với bạn hơn, hãy tải về gói [Eclipse kèm với môi trường thực thi Java mới nhất của IBM](http://www.ibm.com/developerworks/java/jdk/eclipse/?S_TACT=105AGY75)(có sẵn cho Linux và Windows).
### Thảo luận
- Tham gia vào các
[developerWorks blogs](http://www.ibm.com/developerworks/blogs/?S_TACT=105AGY75)và tích cực theo sát cộng đồng developerWorks.
## Bình luận
[Vùng dữ liệu trên developerWorks](http://www.ibm.com/developerworks/vn/views/data/libraryview.jsp)
Khám phá các bài viết xoay quanh lĩnh vực dữ liệu, Quản lý hệ thống thông tin
[Vùng mã nguồn mở trên developerWorks](http://www.ibm.com/developerworks/vn/views/opensource/libraryview.jsp)
Xem các bài viết về HTML, CSS, JavaScript,...
[Điện toán đám mây](http://www.ibm.com/developerworks/vn/views/cloud/libraryview.jsp)
Tìm hiểu về điện toán đám mây

WARC-Type: response

Content-Type: application/http; msgtype=response

Content-Encoding: gzip

Content-Type: text/html; charset=UTF-8

Content-Language: en-US

Connection: close

Vary: Accept-Encoding