Ngôn ngữ Java™ cung cấp hầu hết những gì mà các lập trình viên chuyên nghiệp mong đợi đối với một ngôn ngữ lập trình và thậm chí là đối với một ngôn ngữ hướng-đối tượng. Thế nhưng, ngoài những điều căn bản, ngôn ngữ Java còn cung cấp một số công cụ có ích để tạo ra các chương trình tinh vi hơn. Hướng dẫn này sẽ giới thiệu cho bạn một số trong các đặc tính nâng cao hơn này của ngôn ngữ Java thường được thấy trong các dự án phát triển công nghệ Java điển hình.

Roy W. MillerIBM

Roy Miller là một huấn luyện viên phát triển phần mềm, nhà lập trình và tác giả, hoạt động độc lập. Ông bắt đầu sự nghiệp của mình tại Andersen Consulting (bây giờ là Accenture ) và gần đây nhất, dành ba năm để sử dụng Java chuyên nghiệp tại công ty RoleModel Software, Inc ở Holly Springs, NC. Ông đã phát triển phần mềm, quản lý các nhóm và huấn luyện các lập trình viên khác tại các công ty khách hàng, bắt đầu từ các công ty khởi nghiệp chỉ có hai người cho đến các công ty trong danh sách Fortune 500



06 06 2009

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 để 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), 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. 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 AdultBaby 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()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ượngcá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 Person làm cho các phương thức thành trừu tượng, bắt buộc các lớp con triển khai thực hiện chúng.
  • Chúng ta làm cho Adult thành lớp con của Person và triển khai thực hiện các phương thức.
  • Chúng ta làm cho Baby thành lớp con của Person và triển khai thực hiện các phương thức.

Khi bạn khai báo một phương thức là trừu tượng, bạn cần các lớp con để thực hiện phương thức, hoặc chính lớp con lại tiếp tục là trừu tượng và chuyển giao trách nhiệm triển khai thực hiện tới các lớp con bậc dưới nữa. Bạn có thể thực hiện một số phương thức trên lớp trừu tượng và buộc các lớp con thực hiện những phương thức khác. Điều này tùy thuộc vào bạn. Đơn giản chỉ cần khai báo những cái bạn không muốn triển khai thực hiện là trừu tượng và không cung cấp phần thân của phương thức. Nếu một lớp con không thực hiện một phương thức trừu tượng từ một lớp bậc trên, thì trình biên dịch sẽ báo lỗi.

Vì cả hai AdultBaby đề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, AdultBaby 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).

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 constantTypeconstantName = constantValue;
	...
	returnValueTypemethodName( arguments );
	...
}

Một khai báo giao diện trông rất giống với một khai báo lớp, trừ việc bạn sử dụng từ khóa interface. Bạn có thể đặt tên giao diện là bất cứ thứ gì bạn muốn, miễn là tên hợp lệ, nhưng theo quy ước, các tên của giao diện nhìn giống như các tên lớp. Bạn có thể bao gồm các hằng số, các khai báo phương thức, hoặc cả hai vào trong một giao diện.

Các hằng số được định nghĩa trong một giao diện giống như các hằng số được định nghĩa trong các lớp. Các từ khóa publicstatic đượ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 publicabstract đượ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 interfaceName extends superinterfaceName, ... {
	interface body...
}

Một lớp có thể là một lớp con của chỉ một lớp bậc trên, nhưng một giao diện có thể mở rộng nhiều giao diện khác tùy bạn muốn. Chỉ cần liệt kê chúng sau từ khóa extends, phân cách bằng dấu phẩy.

Dưới đây là ví dụ về một giao diện:

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 className extends superclassName implements
   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. AdultBaby 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 MotionListener mà ứng dụng của chúng ta có thể thực hiện và sau đó xử lý các sự kiện trong trình lắng nghe (listener).
  • Thêm một List của các trình lắng nghe vào Adult.
  • Thêm một phương thức addMotionListener() vào Adult để đăng ký trình lắng nghe.
  • Thêm một phương thức fireMoveEvent() vào Adult để nó có thể báo cho trình lắng nghe khi nào thì xử lý sự kiện.
  • Thêm mã vào ứng dụng của chúng ta để tạo ra một Adult và tự đăng ký như là một trình xử lý

Tất cả điều này dễ hiểu. Đây là lớp Adult của chúng ta với các thứ mới thêm:

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ì đó>").

\d

Bất kỳ số nào (tùy chọn, [0-9]).

\D

Bất kỳ cái gì không là số (tùy chọn, [^0-9]).

\s

Bất kỳ khoảng trống nào (tùy chọn, [ \n\t\f\r]).

\S

Không có bất kỳ khoảng trống nào (tùy chọn, [^ \n\t\f\r]).

\w

Bất kỳ từ nào (tùy chọn, [a-zA-Z_0-9]).

\W

Không có bất kỳ từ nào (tùy chọn, [^\w]).

Một số cấu kiện đầu tiên ở đây được gọi là các lượng tử ((quantifiers), bởi vì chúng xác định số lượng cái đứng trước chúng. Các cấu kiện như là \dcá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()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ử Object có thể dẫn hướng.
  • Giao diện Set định nghĩa một sưu tập không có các phần tử trùng lặp.
  • Giao diện Map định nghĩa một sưu tập các cặp khóa - giá trị.

Chúng ta sẽ nói về một vài triển khai thực hiện cụ thể trong hướng dẫn này. Đây không phải là một danh sách đầy đủ, nhưng nhiều khả năng bạn thường xuyên thấy những thứ sau đây trong các dự án phát triển bằng ngôn ngữ Java:

Giao diện

(Các) triển khai thực hiện

List

ArrayList, Vector

Set

HashSet, TreeSet

Map

HashMap

Tất cả các giao diện trong khung công tác, trừ Map là các giao diện con của giao diện Collection, trong đó định nghĩa cấu trúc chung nhất của một sưu tập. Mỗi sưu tập gồm nhiều phần tử. Với vai trò là trình thực hiện các giao diện con của Collection, tất cả kiểu sưu tập chia sẻ chung (theo trực giác) một số hành vi:

  • Các phương thức để mô tả kích thước của sưu tập (như size()isEmpty()).
  • Các phương thức để mô tả nội dung của sưu tập (như contains()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()clear()).
  • Các phương thức để cho phép bạn chuyển đổi một sưu tập thành một mảng (như toArray()).
  • Một phương thức để cho phép bạn nhận được một trình vòng lặp (iterator) trên mảng các phần tử (iterator()).

Chúng ta sẽ nói về một số phương thức trên trong phần này. Đồng thời chúng ta sẽ thảo luận trình vòng lặp (iterator) là gì và cách sử dụng nó như thế nào.

Lưu ý rằng các Map là đặc biệt. Thật sự chúng hoàn toàn không là một sưu tập. Tuy nhiên, chúng có hành vi rất giống các sưu tập, vì vậy chúng ta cũng nói về chúng trong phần này.

Các triển khai thực hiện Danh sách (List)

Các phiên bản cũ hơn của JDK chứa một lớp được gọi là Vector. Nó vẫn còn có trong các phiên bản mới hơn, nhưng bạn chỉ nên sử dụng nó khi bạn cần có một sưu tập đồng bộ hoá -- đó là, một trong những yếu tố là an toàn phân luồng. (Nói về phân luồng đã vượt ra ngoài phạm vi của bài viết này, chúng ta sẽ thảo luận ngắn gọn về khái niệm ấy trong phần Tóm tắt). Trong các trường hợp khác, bạn nên sử dụng lớp ArrayList. Bạn vẫn có thể sử dụng Vector, nhưng nó áp đặt một số chi phí thêm mà bạn thường không cần.

Một ArrayList là cái như tên của nó gợi ý: danh sách các phần tử theo thứ tự. Chúng ta đã thấy làm thế nào để tạo ra một danh sách và làm thế nào để thêm các phần tử vào nó, trong bài hướng dẫn giới thiệu trước. Khi chúng ta tạo ra một lớp Wallet lồng trong trong hướng dẫn này, chúng ta đã tích hợp vào đó một ArrayList để giữ các hoá đơn thanh toán của Adult:

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ánArrayList, 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

containsKey()

Trả lời Map có chứa khóa đã cho hay không.

containsValue()

Trả lời Map có chứa giá trị đã cho hay không.

keySet()

Trả về một Set tập hợp các khóa.

values()

Trả về một Set tập hợp các giá trị.

entrySet()

Trả về một Set tập hợp các cặp khóa - giá trị, được định nghĩa như là các cá thể của các Map.Entry.

remove()

Cho phép bạn loại bỏ giá trị cho một khóa đã cho.

isEmpty()

Trả lời Map có rỗng không (rỗng có nghĩa là, không chứa khóa nào).

Một số trong các phương thức này, chẳng hạn như isEmpty() chỉ là để cho tiện thôi, nhưng một số là rất quan trọng. Ví dụ, cách duy nhất để thực hiện vòng lặp qua các phần tử trong một Map là thông qua một trong các tập hợp có liên quan (tập hợp các khóa, các giá trị, hoặc các cặp khóa-giá trị).

Lớp Các sưu tập (Collections)

Khi bạn đang sử dụng khung công tác các sưu tập Java, bạn cần phải nắm được những gì có sẵn trong lớp Collections. Lớp này gồm có một kho lưu trữ các phương thức tĩnh để hỗ trợ các thao tác trên sưu tập. Chúng tôi sẽ không trình bày tất cả chúng ở đây, bởi vì bạn có thể tự mình đọc API, nhưng chúng tôi sẽ trình bày hai phương thức thường xuyên xuất hiện trong mã Java:

  • copy()
  • sort()

Phương thức đầu tiên cho phép bạn sao chép các nội dung của một sưu tập này tới một sưu tập khác, như sau:

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 DateCalendar để 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 Date mà chúng ta muốn thiết lập.
  • Các giá trị cho trường đó.

Chúng ta có thể tham chiếu các trường bằng các hằng số có tên trong chính lớp Calendar Trong một số trường hợp, có nhiều hơn một tên cho cùng một trường, như với Calendar.DAY_OF_MONTH, mà nó cũng có thể dùng Calendar.DATE. Các giá trị là dễ hiểu, có lẽ chỉ trừ các giá trị của Calendar.MONTH và một giá trị của Calendar.HOUR. Trong ngôn ngữ Java, các tháng được đếm bắt đầu từ số không (tức là, tháng Giêng là 0), vì thế sẽ là khôn ngoan nếu sử dụng các hằng số có tên để đặt thay cho các số, và đồng thời cũng làm cho tháng hiển thị các ngày tháng một cách chính xác hơn. Các giờ chạy từ 0 đến 24.

Sau khi chúng ta đã thiết lập Date, chúng ta có thể trích ra các phần của nó:

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

DEFAULT

Nov 11, 2005

7:44:56 PM

Nov 11, 2005 7:44:56 PM

SHORT

11/11/05

7:44 PM

11/11/05 7:44 PM

MEDIUM

Nov 11, 2005

7:44:56 PM

Nov 11, 2005 7:44:56 PM

LONG

November 11, 2005

7:44:56 PM EST

November 11, 2005 7:44:56 PM EST

FULL

Thursday, November 11, 2005

7:44:56 PM EST

Thursday, November 11, 2005 7:44:56 PM EST

Định dạng có tùy chỉnh

Các khuôn dạng đã định nghĩa trước là tốt đẹp trong hầu hết các trường hợp, nhưng bạn cũng có thể sử dụng SimpleDateFormat để định nghĩa các định dạng của riêng bạn. Việc sử dụng SimpleDateFormat rất dễ hiểu:

  • Khởi tạo một cá thể SimpleDateFormat với một chuỗi kýtự định dạng mẫu (và một tên địa phương, nếu bạn muốn).
  • Gọi format() trên cá thể này với một Date cụ thể.

Kết quả là một chuỗi ngày tháng có định dạng. Dưới đây là một ví dụ:

Date aDate = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
String formattedDate = formatter.format(today);
System.out.println(formattedDate);

Khi bạn chạy mã này, bạn sẽ nhận được các kết quả giống như sau (tất nhiên nó sẽ phản ánh ngày tháng hiện tại khi bạn chạy các mã):

11/05/2005

Chuỗi có ngoặc kép trong ví dụ ở trên tuân theo đúng các quy tắc cú pháp mẫu cho các mẫu định dạng ngày tháng. Java.sun.com có một số tóm tắt tuyệt vời về các quy tắc đó (xem Tài nguyên). Dưới đây là một số quy tắc nhỏ có ích:

  • Bạn có thể chỉ rõ các mẫu cho các ngày tháng và thời gian.
  • Một số cú pháp mẫu không phải là trực giác (ví dụ, mm định nghĩa mẫu phút có hai số; còn để có được viết tắt tên tháng , bạn sử dụng MM).
  • Bạn có thể chèn thêm các chữ vào trong các mẫu của bạn bằng cách đặt chúng trong một dấu nháy đơn (ví dụ, khi sử dụng "'on' MM/dd/yyyy" ở trên tạo ra on 11/05/2005).
  • Số các ký tự trong một thành phần văn bản của một mẫu sẽ áp đặt việc dùng dạng viết tắt hay dạng viết dài ("MM" tạo ra 11, nhưng "MMM" tạo ra Nov"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 SimpleDateFormatDateFormatSymbols 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ẫntemp.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 ReaderWriter). Các luồng khác xử lý chỉ các byte 8-bit (đó là các luồng kiểu InputStreamOutputStream). 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ự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) 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ồngCách sử dụng
FileInputStream
FileOutputStream

Đọc các byte từ một tệp tin và ghi các byte vào một tệp tin.

ByteArrayInputStream
ByteArrayOutputStream
Đọ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ồngCách sử dụng
StringReader
StringWriter
Các luồng này đọc và viết các ký tự từ/đến các String trong bộ nhớ.

InputStreamReader
InputStreamWriter (and subclasses FileReader
FileWriter)
Cầu nối giữa các luồng byte và các luồng ký tự. Reader flavors read bytes from a byte stream and convert them to characters. The Writer chuyển đổi các ký tự thành các byte để đặt chúng vào các luồng byte.

BufferedReader and BufferedWriterLà bộ đệm dữ liệu trong khi đọc hoặc viết một luồng khác, cho phép các hoạt động đọc và ghi có hiệu quả hơn. Bạn gói một luồng khác trong một luồng có bộ đệm.

Các luồng là một chủ đề lớn và chúng ta không thể trình bày toàn bộ chúng ở đây. Thay vào đó, chúng ta sẽ tập trung vào các luồng được khuyến cáo dùng để đọc và viết các tệp tin. Trong hầu hết trường hợp, đây sẽ là các luồng ký tự, nhưng chúng tôi sẽ sử dụng cả các luồng ký tự và các luồng byte để minh họa các khả năng này.

Đọc và viết các tệp tin

Có một số cách để đọc ra từ và viết vào một File. Người ta có thể cho rằng cách tiếp cận đơn giản nhất diễn ra như sau:

  • Tạo một FileOutputStream trên File để viết vào nó.
  • Tạo một FileInputStream trên File để đọc từ nó.
  • Gọi read() để đọc từ Filewrite() để 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). Chúng ta sau đó đọc vào từng byte của dữ liệu đầu vào và ghi nó vào kết quả đầu ra. Sau khi đã xong, chúng ta đóng các luồng. Có lẽ là khôn ngoan khi đặt một lời gọi close() vào trong khối cuỗi cùng (finally). Tuy nhiên, trình biên dịch của ngôn ngữ Java sẽ còn yêu cầu bạn phải nắm bắt nhiều trường hợp ngoại lệ khác có thể xảy ra, điều này có nghĩa là cần một mệnh đề catch khác nữa nằm trong khối finally của bạn. Có chắc chắn đáng làm không? Có thể.

Vậy là bây giờ chúng ta có một cách tiếp cận cơ bản để đọc và viết. Nhưng sự lựa chọn khôn ngoan hơn, và về một vài khía cạnh, sự lựa chọn dễ dàng hơn, là sử dụng một số luồng khác mà chúng ta sẽ thảo luận trong phần tiếp theo.

Các luồng có bộ đệm

Có một số cách để đọc ra và ghi vào một File, nhưng cách tiếp cận điển hình và thuận tiện nhất diễn ra như sau:

  • Tạo một FileWriter trên File.
  • Gói FileWriter trong một BufferedWriter.
  • Gọi write() trên BufferedWriter mỗi khi cần thiết để viết các nội dung của File, thường ở cuối mỗi dòng cần viết ký tự kết thúc dòng (đó là, \n).
  • Gọi flush() trên BufferedWriter để làm rỗng nó.
  • Đóng BufferedWriter, dọn dẹp sạch nếu cần thiết.

Các mã có thể trông giống như sau:

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

ThreadsCó 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 Thread và các lớp có liên quan trong java.lang có thể cung cấp cho bạn khả năng phân luồng mạnh mẽ và linh hoạt trong chương trình của bạn. developerWorks có các trang Web với nhiều tài nguyên tốt về phân luồng trong mã Java của bạn, nhưng một điểm bắt đầu thích hợp là các hướng dẫn "Giới thiệu về các luồng Java" và "Tương tranh trong JDK 5.0" và loạt các bài viết này của Brian Goetz.

ReflectionMộ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 java.lang.reflect bao gồm các lớp như ClassMethod cho phép tra xét cấu trúc mã của bạn. Ví dụ, bạn có thể tìm thấy một phương thức bắt đầu với get, sau đó gọi nó bằng cách gọi invoke() trên đối tượng Method -- rất mạnh. Phần 2 của loạt bài "Các động lực trong lập trình Java" của Dennis M. Sosnoski nói về việc sử dụng phản chiếu.

NIOKể 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ênhcá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ó. "Khởi đầu với I/O mới (NIO)" của Greg Travis là một hướng dẫn tuyệt vời về chủ đề này.

SocketsCá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 "101 các ổ cắm của Java" của Roy Miller và Adam Williams để được giới thiệu về API các ổ cắm. Hướng dẫn "Sử dụng JSSE cho giao tiếp ổ cắm an toàn" của Greg Travis cho bạn thấy làm thế nào để thực hiện bảo mật giao tiếp ổ cắm của bạn.

SwingNgô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 "Giao diện người dùng của Java 2" của Matt Chapman. Chuyên mục Magic với Merlin của John Zukowski tập trung vào những thay đổi và cập nhật Swing gần đây. John cũng chủ trì trang web Diễn đàn thảo luận lập trình Java phía khách, vì vậy bạn có thể nhận được sự hỗ trợ cho việc lập trình Swing ở đó. Hướng dẫn "Di chuyển ứng dụng Swing của bạn đến SWT" thảo luận cách làm thế nào để di chuyển từ Swing đến SWT của IBM, một giải pháp thay thế gọn nhẹ nhưng vẫn rất mạnh mẽ.

JNIKhi chương trình Java của bạn cần phải giao tiếp với một chương trình khác, ví dụ viết bằng ngôn ngữ C, Java cho bạn một cách để làm điều đó, đó là: 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" của developerWorks thảo luận về các cơ cấu của JNI với mã Java, C và C++.

RMIAPI về viện dẫn phương thức từ xa của ngôn ngữ Java (RMI - Remote Method Invocation) cho phép một chương trình bằng ngôn ngữ Java, hoặc trong một tiến trình hoặc trên một máy tính, truy cập vào chương trình ngôn ngữ Java khác đang chạy trong một tiến trình khác và/hoặc trên máy tinh khác. Nói cách khác, RMI hỗ trợ 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" 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 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.

SecurityNgôn ngữ Java bao gồm các API bảo mật tinh vi để hỗ trợ xác thực và cấp phép. Làm thế nào bạn có thể chắc chắn rằng những người nào đó sử dụng chương trình của bạn được phép thực hiện ? Làm thế nào bạn có thể bảo vệ các thông tin khỏi những con mắt tò mò? Các API bảo mật có thể trợ giúp. Trên developerWorks cung cấp nhiều nội dung có liên quan đến bảo mật Java. Dưới đây chỉ là một vài ví dụ: Trang web Diễn đàn thảo luận bảo mật Java đượ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" 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" và "Bảo mật Java , Phần 2: Xác thực và cấp phép" cả hai đều của Brad Rubin.

Tài nguyên

Học tập

  • Đọc "Introduction to Java programming" 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 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, để 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. Đây chỉ là một bài viết trong chuyên mục Magic with Merlin của ông.
  • Sun Java tutorial 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 và tìm hiểu thêm về ví dụ CopyBytes.javaở đây.
  • Trang developerWorks New to Java technology page 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 của developerWorks.
  • Cũng có thể xem trang Java technology zone tutorials để 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.

Lấy sản phẩm và công nghệ

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=395026
ArticleTitle=Lập trình Java trung cấp
publish-date=06062009