Design Pattern (Mẫu thiết kế) và các tính năng nâng cao khác
- 20-12-2023
- Toanngo92
- 0 Comments
Trong thế giới sinh học, khi một số hữu cơ biểu hiện nhiều hình thái khác nhau, đó được gọi là đa hình. Một khái niệm tương tự có thể được áp dụng vào Java, nơi một lớp con có thể có hành vi duy nhất của riêng mình ngay cả khi chia sẻ một số chức năng chung với lớp cha.
Mục lục
Mẫu Thiết Kế
“Một mẫu thiết kế là một giải pháp được xác định rõ cho các vấn đề xảy ra thường xuyên. Nó có thể được coi là một mẫu hoặc một thực hành tốt được đề xuất bởi những lập trình viên chuyên nghiệp. Vì vậy, nếu một nhà phát triển có kinh nghiệm giáo dục một nhà phát triển khác về một mẫu factory cụ thể được sử dụng để giải quyết một vấn đề, nhà phát triển khác có thể hiểu chính xác cách xử lý với một vấn đề tương tự. Một mẫu thiết kế là một sự giúp đỡ tuyệt vời cho những nhà phát triển không có kinh nghiệm. Họ có thể nghiên cứu các mẫu và vấn đề liên quan và học chi tiết tốt về thiết kế phần mềm. Điều này giảm thiểu độ cong học. Việc sử dụng đúng các mẫu thiết kế dẫn đến việc tăng khả năng bảo trì mã nguồn.”
Mẫu thiết kế dựa trên các nguyên tắc cơ bản của thiết kế hướng đối tượng. Một mẫu thiết kế không phải là một cài đặt, cũng không phải là một framework. Nó không thể được cài đặt bằng mã nguồn. Hiện nay, có một số mẫu thiết kế chuẩn và phổ biến đã được phát triển sau một khoảng thời gian dài nghiên cứu và thử nghiệm bởi nhiều nhà phát triển phần mềm khác nhau.
Loại Mẫu Thiết Kế
Bảng 10.1 liệt kê các loại mẫu thiết kế khác nhau.
Mô tả Mẫu | Loại Mẫu |
---|---|
Mẫu Singleton | Tạo đối tượng mà ẩn logic khởi tạo |
Mẫu Factory | Tạo đối tượng mà ẩn logic khởi tạo bằng cách sử dụng một phương thức tạo đối tượng chung. |
Mẫu Builder | Xây dựng đối tượng theo cách linh hoạt và có thể bị thay đổi |
Mẫu Prototype | Tạo đối tượng mới bằng cách sao chép một đối tượng đã tồn tại |
Mẫu Adapter | Điều chỉnh giao diện của một lớp thành một giao diện khác mà client mong muốn sử dụng |
Mẫu Composite | Liên quan đến sự kết hợp của lớp và đối tượng. Giao diện được đề xuất bằng cách kết hợp sự kế thừa và sự sáng tạo. |
Mẫu Proxy | Đại diện cho một đối tượng khác |
Mẫu Bridge | Tách biệt giữa sự kế thừa và cách triển khai, để cả hai có thể thay đổi mà không ảnh hưởng đến nhau. |
Mẫu Decorator | Mở rộng chức năng của một đối tượng mà không thay đổi cấu trúc của nó. |
Mẫu Template Method | Định nghĩa các bước cơ bản của một thuật toán và để các bước cụ thể được triển khai bởi các lớp con. |
Mẫu Mediator | Giữ các đối tượng tương tác thông qua một đối tượng trung gian |
Mẫu Chain of Responsibility | Chuyển yêu cầu từ một đối tượng xử lý đến đối tượng xử lý khác theo một chuỗi đã cho. |
Mẫu Observer | Xác định một phụ thuộc “một nhiều” giữa các đối tượng để khi một đối tượng thay đổi trạng thái, tất cả các đối tượng phụ thuộc sẽ được thông báo và cập nhật tự động. |
Mẫu Thiết Kế (Design Pattern) Singleton
Một số triển khai của lớp chỉ có thể được khởi tạo một lần. Mẫu thiết kế singleton cung cấp thông tin đầy đủ về các triển khai của lớp như vậy. Để thực hiện điều này, thường thì một trường tĩnh được tạo để đại diện cho lớp. Đối tượng mà trường tĩnh này tham chiếu có thể được tạo ra vào thời điểm khi lớp được khởi tạo hoặc lần đầu tiên phương thức getInstance()
được gọi. Constructor của một lớp sử dụng mẫu singleton được khai báo là private để ngăn lớp được khởi tạo.
Nên sử dụng các lớp singleton để tập trung quyền truy cập vào các tài nguyên cụ thể vào một trường instance duy nhất.
Để thực hiện một mẫu thiết kế singleton, thực hiện các bước sau:
- Sử dụng một tham chiếu tĩnh để trỏ đến một thể hiện duy nhất.
- Sau đó, thêm một constructor private duy nhất vào lớp singleton.
- Tiếp theo, một phương thức factory công cộng được khai báo là static để truy cập trường tĩnh.
Lưu ý: Một phương thức factory công cộng trả về một bản sao của tham chiếu singleton.
- Sử dụng phương thức static
getInstance()
để nhận một thể hiện của singleton.
Xem xét các điểm sau khi thực hiện mẫu thiết kế singleton:
- Tham chiếu được làm cố định để nó không trỏ đến một thể hiện khác.
- Modifiers private cho phép chỉ có sự truy cập của cùng một lớp và hạn chế việc cố gắng khởi tạo lớp singleton.
- Phương thức factory cung cấp độ linh hoạt cao hơn và thường được sử dụng trong các triển khai singleton.
- Lớp singleton thường bao gồm một constructor private để ngăn constructor khác khởi tạo lớp singleton.
- Để tránh sử dụng phương thức factory, một biến công cộng có thể được sử dụng vào thời điểm sử dụng một tham chiếu tĩnh.
Ví dụ:
public class Singleton {
// Private static instance variable
private static Singleton instance;
// Private constructor to prevent instantiation outside of the class
private Singleton() {
// Initialization code, if needed
}
// Public method to get the singleton instance
public static Singleton getInstance() {
// Lazy initialization: create the instance only if it doesn't exist
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// Other methods, if any
public void showMessage() {
System.out.println("Hello, I am a Singleton!");
}
public static void main(String[] args) {
// Get the singleton instance
Singleton singleton = Singleton.getInstance();
// Call a method on the singleton instance
singleton.showMessage();
}
}
Ví dụ:
public enum EnumSingleton {
INSTANCE;
// Other methods, if any
public void showMessage() {
System.out.println("Hello, I am an Enum Singleton!");
}
public static void main(String[] args) {
// Access the singleton instance
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
// Call a method on the singleton instance
enumSingleton.showMessage();
}
}
Giao Diện (Interface) trong Mẫu Thiết Kế
Hãy xem xét một tình huống nơi các chương trình sau được tạo ra để tự động hóa một số công việc của một phương tiện giao thông:
- Một chương trình dừng phương tiện khi đèn đỏ
- Một chương trình tăng tốc cho phương tiện
- Một chương trình xoay phương tiện theo các hướng khác nhau
- Có thể có nhiều chương trình khác nhau như vậy cho tự động hóa các chức năng của phương tiện giao thông.
Bây giờ, tất cả những chương trình này không cần phải được tạo bởi một cá nhân duy nhất. Mỗi chương trình có thể thuộc sở hữu của các lập trình viên hoặc tổ chức khác nhau.
Để tích hợp tất cả các chương trình này từ các nguồn khác nhau trên một phương tiện giao thông, cần có một phương tiện mô tả cách một phần mềm tương tác. Phương tiện này chính là giao diện. Vì vậy, khi các lập trình viên quyết định viết mã cho một mục tiêu tương tự (như tự động hóa phương tiện giao thông), họ tuân theo giao diện này.
Trong Java, giao diện bao gồm các trường hằng số. Chúng có thể được sử dụng như các kiểu tham chiếu. Trong thực tế, chúng là các thành phần quan trọng của nhiều mẫu thiết kế.
Java sử dụng giao diện để định nghĩa trừu tượng kiểu. Định rõ các kiểu trừu tượng là một tính năng mạnh mẽ của Java. Dưới đây là các lợi ích của trừu tượng:
- Thực Hiện Cụ Thể Theo Nhà Sản Xuất: Nhà phát triển định nghĩa các phương thức cho gói java.sql.
Giao tiếp giữa cơ sở dữ liệu và gói java.sql xảy ra bằng cách sử dụng các phương thức này.
Tuy nhiên, cài đặt là cụ thể cho từng nhà sản xuất. - Phát triển Song Song: Dựa trên API doanh nghiệp, giao diện người dùng của ứng dụng và logic doanh nghiệp có thể được phát triển đồng thời.
- Dễ Bảo Trì: Các lớp được cải thiện có thể thay thế các lớp có lỗi logic bất cứ lúc nào.
Lưu ý: Java cũng sử dụng các lớp trừu tượng để định nghĩa trừu tượng kiểu.
Dưới đây là thông tin bổ sung về giao diện:
- Giao diện không thể được khởi tạo.
- Chỉ có các lớp mới có thể triển khai giao diện.
- Một giao diện có thể được định nghĩa giống như khi tạo ra một lớp mới.
- Giao diện cũng có thể được mở rộng bởi các giao diện khác. Một giao diện có thể mở rộng bất kỳ số lượng giao diện nào.
- Chúng có thể bao gồm các trường hằng số.
- Hầu hết các mẫu thiết kế sử dụng giao diện.
Một khai báo giao diện bao gồm các thành phần sau:
- Các bộ bổ từ (modifiers)
- Từ khóa interface
- Tên giao diện
- Một danh sách các giao diện cha có thể mở rộng (extends)
Nội dung (body) giao diện
Ví dụ:
public interface Aircraft {
public int passengerCapacity = 400;
// các chữ ký phương thức
void fly();
// thêm các chữ ký phương thức khác
}
Chỉ các trường hằng số được phép trong một giao diện. Một trường được tự động hiểu là public, static và final khi một giao diện được khai báo. Là một thiết kế chuẩn hóa nên khuyến khích phân phối giá trị hằng số của một ứng dụng qua nhiều lớp và giao diện.
Định nghĩa một giao diện mới đồng nghĩa với việc định nghĩa một kiểu dữ liệu tham chiếu mới. Hãy xem xét các điểm sau liên quan đến các loại tham chiếu:
- Tên của giao diện có thể được sử dụng bất kỳ nơi nào. Ngoài ra, bất kỳ tên kiểu dữ liệu nào khác cũng có thể được sử dụng.
- Toán tử
instanceof
có thể được sử dụng với các giao diện. - Nếu một biến tham chiếu được định nghĩa với kiểu là một giao diện, thì bất kỳ đối tượng được gán cho nó phải là một thể hiện của một lớp triển khai giao diện đó.
- Giao diện ngầm định bao gồm tất cả các phương thức từ
java.lang.Object
. - Nếu một đối tượng bao gồm tất cả các phương thức được mô tả trong giao diện nhưng không triển khai giao diện, thì giao diện đó không thể được sử dụng như một kiểu tham chiếu cho đối tượng đó.
Sự Khác Biệt Giữa Kế Thừa Lớp và Kế Thừa Giao Diện
Một lớp có thể mở rộng một lớp cha duy nhất trong khi nó có thể triển khai nhiều giao diện. Khi một lớp mở rộng một lớp cha, chỉ có một số chức năng cụ thể có thể được ghi đè trong lớp kế thừa. Ngược lại, khi một lớp kế thừa một giao diện, nó triển khai tất cả các chức năng của các giao diện đó.
Một lớp được mở rộng vì một số lớp cần triển khai chi tiết dựa trên lớp cha. Tuy nhiên, tất cả các lớp đều cần một số phương thức và thuộc tính của lớp cha. Ngược lại, một giao diện được sử dụng khi có nhiều triển khai khác nhau của cùng một chức năng.
Bảng dưới cung cấp so sánh giữa một giao diện và một lớp trừu tượng.
Giao Diện | Lớp Trừu Tượng |
---|---|
Hỗ trợ kế thừa của nhiều giao diện bởi một lớp. | Hỗ trợ kế thừa của chỉ một lớp trừu tượng bởi một lớp. |
Yêu cầu thời gian nhiều hơn để tìm phương thức thực tế trong các lớp con tương ứng. | Yêu cầu ít thời gian để tìm phương thức thực tế trong các lớp con tương ứng. |
Tốt nhất khi các triển khai khác nhau chỉ chia sẻ chữ ký phương thức. | Tốt nhất khi các triển khai khác nhau sử dụng hành vi hoặc trạng thái chung. |
Mở Rộng Giao Diện
Được khuyến khích đặc tả tất cả các sử dụng của giao diện ngay từ đầu. Tuy nhiên, điều này không phải lúc nào cũng có thể. Trong trường hợp này, có thể tạo thêm các giao diện sau này. Như vậy, giao diện có thể được sử dụng để mở rộng giao diện.
Đoạn mã dưới tạo một giao diện có tên là IVehicle
.
public interface IVehicle {
int getMileage(String s);
}
Đoạn mã dưới thể hiện cách một giao diện mới có thể được tạo ra mà mở rộng từ IVehicle
.
public interface IAutomobile extends IVehicle {
boolean accelerate(int i, double x, String s);
}
Trong đoạn mã này, IAutomobile
mở rộng từ IVehicle
, tức là nó thừa hưởng tất cả các phương thức được định nghĩa trong IVehicle
. Đồng thời, nó cũng khai báo một phương thức mới là accelerate
.
Thực Hiện Mối Quan Hệ IS-A và HAS-A
Đây là một khái niệm dựa trên kế thừa lớp hoặc triển khai giao diện. Mối quan hệ IS-A hiển thị
thứ bậc lớp trong trường hợp của kế thừa lớp. Ví dụ, nếu lớp Ferrari mở rộng từ lớp Car, câu lệnh Ferrari IS-A Car là đúng.
Hình dưới minh họa mối quan hệ IS-A.
Mối quan hệ IS-A cũng được sử dụng cho triển khai giao diện. Điều này được thực hiện bằng cách sử dụng từ khóa implements
hoặc extends
.
Mối quan hệ HAS-A hoặc mối quan hệ sở hữu sử dụng biến thể thể hiện là tham chiếu đến các đối tượng khác, như một chiếc Ferrari bao gồm một Engine. Hình dưới minh họa ví dụ này.
Mẫu Thiết Kế Đối Tượng Truy Cập Dữ Liệu (Data Object Access) (DAO)
Mẫu thiết kế DAO được sử dụng khi một ứng dụng được tạo ra cần lưu trữ dữ liệu của nó. Mẫu thiết kế DAO liên quan đến một kỹ thuật tách biệt logic kinh doanh và logic lưu trữ, làm cho việc triển khai và bảo trì ứng dụng trở nên dễ dàng hơn.
Ưu điểm của phương pháp DAO là nó làm cho việc thay đổi cơ chế lưu trữ của ứng dụng mà không cần thay đổi toàn bộ ứng dụng, vì lớp truy cập dữ liệu sẽ được tách biệt và không bị ảnh hưởng.
Mẫu thiết kế DAO sử dụng các thành phần sau:
- DAO Interface: Định nghĩa các hoạt động tiêu chuẩn cho một đối tượng mô hình. Nói cách khác, nó định nghĩa các phương thức được sử dụng để lưu trữ.
- DAO Concrete Class: Thực hiện DAO Interface và lấy dữ liệu từ nguồn dữ liệu như cơ sở dữ liệu.
- Đối tượng Mô hình hoặc Đối tượng Giá trị: Bao gồm các phương thức get/set để lưu trữ dữ liệu được lấy bởi lớp DAO.
Như tên gọi, DAO có thể được sử dụng với bất kỳ đối tượng dữ liệu nào, không nhất thiết phải là cơ sở dữ liệu. Do đó, nếu tầng truy cập dữ liệu của bạn bao gồm các cửa hàng dữ liệu của tệp XML, DAO vẫn có thể hữu ích ở đó.
Một số loại đối tượng dữ liệu mà DAO có thể hỗ trợ bao gồm:
- DAO dựa trên bộ nhớ: Đại diện cho các giải pháp tạm thời.
- DAO dựa trên tệp: Có thể yêu cầu cho một phiên bản ban đầu.
- DAO dựa trên JDBC: Hỗ trợ lưu trữ cơ sở dữ liệu.
- DAO dựa trên Java Persistence API: Cũng hỗ trợ lưu trữ cơ sở dữ liệu.
Hình trên thể hiện cấu trúc của mẫu thiết kế DAO sẽ được tạo ra. Các điểm sau đây cần lưu ý trong Hình:
- Đối tượng Book sẽ hoạt động như Mô hình hoặc Đối tượng Giá trị.
- BookDao là DAO Interface
- BookDaoImpl là lớp cụ thể thực hiện DAO interface.
- DAOPatternApplication là lớp chính. Nó sẽ sử dụng BookDao để hiển thị việc sử dụng mẫu thiết kế DAO.
Ví dụ mô phỏng:
Tạo lớp Book:
public class Book {
private String name;
private int bookID;
public Book(String name, int bookID) {
this.name = name;
this.bookID = bookID;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBookID() {
return bookID;
}
public void setBookID(int bookID) {
this.bookID = bookID;
}
}
Tạo interface BookCase:
import java.util.List;
public interface BookBase {
List<Book> getAllBooks();
Book getBook(int bookID);
void updateBook(Book book);
void deleteBook(Book book);
}
Tạo lớp BookDaoImpl:
import java.util.List;
import java.util.ArrayList;
public class BookDaoImpl implements BookDao {
// list is working as a database
private List<Book> booksList;
public BookDaoImpl() {
booksList = new ArrayList<>();
Book bookObj1 = new Book("Anna", 1);
Book bookObj2 = new Book("John", 2);
booksList.add(bookObj1);
booksList.add(bookObj2);
}
@Override
public void deleteBook(Book book) {
booksList.removeIf(b -> b.getBookID() == book.getBookID());
System.out.println("Book: Book ID " + book.getBookID() + ", deleted from the database");
}
@Override
public List<Book> getAllBooks() {
return booksList;
}
@Override
public Book getBook(int bookID) {
for (Book book : booksList) {
if (book.getBookID() == bookID) {
return book;
}
}
return null;
}
@Override
public void updateBook(Book book) {
for (Book b : booksList) {
if (b.getBookID() == book.getBookID()) {
b.setName(book.getName());
System.out.println("Book: Book ID " + book.getBookID() + ", updated in the database");
return;
}
}
}
}
Tạo lớp DAGPatternApplication:
public class DAGPatternApplication {
public static void main(String[] args) {
BookDao bookDao = new BookDaoImpl();
System.out.println("Book List:");
// print all books in the list
for (Book book : bookDao.getAllBooks()) {
System.out.println("\nBookID: " + book.getBookID() + ", Name: " + book.getName());
}
System.out.println("Book: [BookID: " + book.getBookID() + ", Name: " + book.getName() + " ]");
}
}
Mẫu Thiết Kế Factory
Mẫu thiết kế Factory là một trong những mẫu thiết kế phổ biến trong Java. Nó thuộc vào loại mẫu thiết kế sáng tạo và cung cấp nhiều cách để tạo một đối tượng. Mẫu này không thực hiện các cuộc gọi trực tiếp tới constructor khi gọi một phương thức. Nó ngăn chặn ứng dụng từ việc liên kết chặt chẽ với một cài đặt DAO cụ thể.
Trong mẫu thiết kế Factory, hãy lưu ý các điểm sau:
Client không biết về logic để tạo đối tượng.
Nó sử dụng một giao diện chung để tham chiếu đến đối tượng mới được tạo.
Giả sử có một kịch bản nơi cần thiết phải thiết kế một số lớp ô tô. Một giao diện chung Vehicle
với một phương thức chung move()
sẽ được tạo ra. Các lớp có tên Car
và Truck
sẽ triển khai giao diện này tương ứng. Một lớp VehicleFactory
được tạo ra để nhận một loại Vehicle
và dựa vào đó, các đối tượng tương ứng sẽ được trả về cho chương trình gọi.
Đoạn Mã 1 tạo một giao diện chung để triển khai mẫu Factory.
Đoạn Mã 1:
interface Vehicle {
void move();
}
Đoạn Mã 2 tạo một lớp triển khai giao diện.
Đoạn Mã 2:
class Car implements Vehicle {
@Override
public void move() {
System.out.println("Inside Car::move() method.");
}
}
Đoạn Mã 3 tạo một lớp khác triển khai giao diện.
Đoạn Mã 3:
class Truck implements Vehicle {
@Override
public void move() {
System.out.println("Inside Truck::move() method.");
}
}
Đoạn Mã 4 tạo một factory để tạo đối tượng của lớp cụ thể dựa trên thông tin được cung cấp.
Đoạn Mã 4:
class VehicleFactory {
// Sử dụng phương thức getVehicle để nhận đối tượng của loại Vehicle
public Vehicle getVehicle(String vehicleType) {
if (vehicleType == null) {
return null;
}
if (vehicleType.equalsIgnoreCase("Car")) {
return new Car();
} else if (vehicleType.equalsIgnoreCase("Truck")) {
return new Truck();
}
return null;
}
}
Đoạn Mã Snippet 4 sử dụng mẫu Factory để nhận các đối tượng của các lớp cụ thể bằng cách truyền thông tin loại Vehicle vào.
Đoạn Mã Snippet 5:
public class FactoryPatternExample {
public static void main(String[] args) {
VehicleFactory vehicleFactory = new VehicleFactory();
// Nhận một đối tượng Car và gọi phương thức move của nó.
Vehicle carObj = vehicleFactory.getVehicle("Car");
// Gọi phương thức move của Car
carObj.move();
// Nhận một đối tượng Truck và gọi phương thức move của nó.
Vehicle truckObj = vehicleFactory.getVehicle("Truck");
// Gọi phương thức move của Truck
truckObj.move();
}
}
Trong đoạn mã này, đối tượng chính xác được tạo ra và dựa vào loại đối tượng, phương thức move()
thích hợp được gọi.
Hình dưới mô tả Factory Design Pattern
Delegation (Uỷ quyền)
Ngoài các mẫu thiết kế tiêu chuẩn, người ta cũng có thể sử dụng mẫu thiết kế uỷ quyền (delegation). Trong Java, uỷ quyền có nghĩa là sử dụng một đối tượng của một lớp khác như một biến thể hiện và chuyển tiếp các thông điệp đến thể hiện đó. Do đó, uỷ quyền là một mối quan hệ giữa các đối tượng. Ở đây, một đối tượng chuyển tiếp cuộc gọi phương thức cho một đối tượng khác, được gọi là đối tượng đại diện của nó.
Uỷ quyền khác biệt so với kế thừa. Khác với kế thừa, uỷ quyền không tạo ra một lớp cha. Trong trường hợp này, thể hiện là của một lớp đã biết. Ngoài ra, uỷ quyền không buộc phải chấp nhận tất cả các phương thức của lớp cha. Uỷ quyền hỗ trợ tái sử dụng mã và cung cấp tính linh hoạt tại thời điểm chạy.
Chú ý: Tính linh hoạt tại thời điểm chạy có nghĩa là đại diện có thể dễ dàng thay đổi tại thời điểm chạy.
Đoạn Mã 1 hiển thị việc sử dụng uỷ quyền bằng một kịch bản thực tế.
Đoạn Mã 1:
interface Employee {
Result sendMail();
}
class Secretary implements Employee {
public Result sendMail() {
Result myResult = new Result();
return myResult;
}
}
class Manager implements Employee {
private Secretary secretary;
public Result sendMail() {
return secretary.sendMail();
}
}
Trong đoạn mã trên, thể hiện của lớp Manager
chuyển tiếp công việc gửi thư đến thể hiện của lớp Secretary
, người sau đó chuyển tiếp yêu cầu đến Employee
. Khi nhìn chung, có vẻ như quản lý đang gửi thư, nhưng thực tế, đó là nhân viên thực hiện công việc. Do đó, yêu cầu công việc đã được uỷ quyền.
Composition and Aggregation (Tổ Hợp và Tổng Hợp)
Tổ hợp đề cập đến quá trình tạo ra một lớp từ các tham chiếu đến các đối tượng khác. Như vậy, các tham chiếu đến các đối tượng chính là các trường của đối tượng chứa. Tổ hợp tạo thành các khối xây dựng cho cấu trúc dữ liệu. Người lập trình có thể sử dụng tổ hợp đối tượng để tạo ra các đối tượng phức tạp hơn. Tổ hợp làm nền tảng cho việc tạo ra các cấu trúc dữ liệu phức tạp. Người lập trình có thể sử dụng tổ hợp đối tượng để tạo ra các đối tượng phức tạp hơn.
Tổng hợp là một khái niệm tương tự với một số sự khác biệt. Trong tổng hợp, một lớp sở hữu một lớp khác. Trong tổ hợp, khi đối tượng sở hữu bị hủy bỏ, các đối tượng bên trong cũng sẽ bị hủy bỏ, nhưng trong tổng hợp, điều này không đúng.
Ví dụ, một tổ chức bao gồm các bộ phận khác nhau và mỗi bộ phận có một số quản lý. Nếu tổ chức đóng cửa, các bộ phận này sẽ không còn tồn tại nữa, nhưng các quản lý vẫn tiếp tục tồn tại. Do đó, một tổ chức có thể được xem xét như là tổ hợp của các bộ phận, trong khi các bộ phận có tổng hợp của quản lý. Ngoài ra, một quản lý có thể làm việc trong nhiều bộ phận (nếu anh/chị đang xử lý nhiều trách nhiệm), nhưng một bộ phận không thể tồn tại trong nhiều tổ chức.
Tổ hợp và tổng hợp là các khái niệm thiết kế và không phải là các mẫu thiết kế thực sự.
Đoạn Mã Snippet dưới thể hiện một ví dụ cơ bản về tổ hợp.
// Tổ hợp
class House {
// House có cửa.
// Cửa được xây dựng khi House được xây dựng,
// nó bị phá hủy khi House bị phá hủy.
private Door door;
}
Để thực hiện tổ hợp đối tượng, thực hiện các bước sau:
- Tạo một lớp với tham chiếu đến các lớp khác.
- Thêm các phương thức cùng chữ ký chuyển tiếp đến đối tượng tham chiếu.
Xem xét một ví dụ về một sinh viên tham gia một khóa học. Sinh viên “có” một khóa học. Tổ hợp cho các lớp Student
và Course
được mô tả trong các Đoạn Mã dưới.
Đoạn mã 1:
package composition;
public class Course {
private String title;
private long score;
private int id;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public long getScore() {
return score;
}
public void setScore(long score) {
this.score = score;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
Đoạn Mã 2:
package compositions;
public class Student {
// Tổ hợp có mối quan hệ has-a
private Course course;
public Student() {
this.course = new Course();
course.setScore(1000);
}
public long getScore() {
return course.getScore();
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getScore());
}
}
Trong Đoạn Mã 2, lớp Course
sau đó được sử dụng trong lớp Student
. Các sinh viên “có” một khóa học, thể hiện mối quan hệ “has-a”. Các thuộc tính và phương thức của lớp Course
được sử dụng bên trong lớp Student
để tạo ra một đối tượng phức tạp hơn.
Quốc Tế Hóa và Địa Phương Hóa
Trong vài thập kỷ gần đây, với sự phổ biến của Internet, quá trình toàn cầu hóa các sản phẩm phần mềm đã trở thành một yêu cầu cấp bách.
Những vấn đề chính mà phải đối mặt khi toàn cầu hóa các sản phẩm phần mềm bao gồm:
- Không phải tất cả các quốc gia trên thế giới nói hoặc hiểu tiếng Anh.
- Ký hiệu tiền tệ thay đổi theo quốc gia.
- Ngày và giờ được biểu diễn khác nhau ở một số quốc gia.
- Cách viết chính tả cũng thay đổi giữa một số quốc gia.
Hai phương án có thể được sử dụng để giải quyết các vấn đề này:
- Phát triển toàn bộ sản phẩm bằng ngôn ngữ mong muốn: Phương án này sẽ dẫn đến việc lặp lại công việc lập trình. Điều này mất thời gian và không phải là một giải pháp chấp nhận được.
- Dịch toàn bộ sản phẩm sang ngôn ngữ mong muốn: Việc dịch tất cả các tệp nguồn thành công việc khó khăn. Các menu, nhãn và thông báo của hầu hết các thành phần GUI thường được mã hóa cứng trong mã nguồn. Việc này không có khả năng có một người phát triển hoặc người dịch có cả kỹ năng về ngôn ngữ và lập trình.
Khi các hoạt động nhập và xuất của một ứng dụng được thực hiện đặc biệt cho các địa điểm và sở thích người dùng khác nhau, người dùng trên khắp thế giới có thể sử dụng nó một cách dễ dàng. Điều này có thể được đạt được bằng quá trình gọi là quốc tế hóa và địa phương hóa. Sự thích ứng được thực hiện một cách dễ dàng vì không cần thay đổi mã nguồn.
Quốc Tế Hóa
Để làm cho một ứng dụng có thể tiếp cận thị trường quốc tế, cần đảm bảo rằng các hoạt động nhập và xuất là cụ thể cho các địa điểm khác nhau và sở thích người dùng. Quá trình thiết kế một ứng dụng như vậy được gọi là quốc tế hóa. Lưu ý rằng quá trình này diễn ra mà không cần thay đổi kỹ thuật.
Java bao gồm sự hỗ trợ tích hợp để quốc tế hóa ứng dụng, được gọi là Quốc tế hóa Java.
Địa Phương Hóa
Trong khi quốc tế hóa xử lý với các địa điểm và sở thích người dùng khác nhau, hóa địa phương xử lý với một khu vực hoặc ngôn ngữ cụ thể. Trong quá trình hóa địa phương, một ứng dụng được điều chỉnh để phù hợp với một khu vực hoặc ngôn ngữ cụ thể. Một khu vực cụ thể được đại diện bằng cách sử dụng các thành phố ngôn ngữ và quốc gia.
Chủ yếu trong quá trình hóa địa phương, các yếu tố giao diện người dùng và tài liệu được dịch. Các thay đổi liên quan đến ngày tháng, tiền tệ và các yếu tố khác cũng được xem xét. Ngoài ra, dữ liệu nhạy cảm với văn hóa như hình ảnh cũng được hóa địa phương. Nếu một ứng dụng đã được quốc tế hóa một cách hiệu quả, việc hóa địa phương cho một ngôn ngữ và bảng mã ký tự cụ thể sẽ trở nên dễ dàng hơn.
Lợi ích của Quốc Tế Hóa và Địa Phương Hóa
Một ứng dụng được quốc tế hóa mang lại những lợi ích sau:
- Không cần biên dịch lại cho các ngôn ngữ mới: Các ngôn ngữ mới được hỗ trợ mà không cần biên dịch lại.
- Cùng một tệp thực thi: Dữ liệu được hóa địa phương phải được tích hợp vào ứng dụng và tệp thực thi chạy trên toàn cầu.
- Truy xuất động các phần tử văn bản: Các phần tử văn bản như nhãn thành phần GUI được lưu trữ bên ngoài mã nguồn. Chúng không được mã hóa cứng trong chương trình. Do đó, những phần tử này có thể được truy xuất động.
Mã ISO và Unicode
Trong quá trình quốc tế hóa và hóa địa phương, một ngôn ngữ được biểu diễn bằng mã ISO
639 alpha-2 hoặc alpha-3, chẳng hạn như “es” đại diện cho tiếng Tây Ban Nha. Mã này luôn được biểu diễn bằng chữ thường.
Một quốc gia được biểu diễn bằng mã ISO 3166 alpha-2 hoặc mã số khu vực số học UN M.49. Nó luôn được biểu diễn bằng chữ in hoa. Ví dụ, “ES” đại diện cho Tây Ban Nha. Nếu một ứng dụng đã được quốc tế hóa một cách hiệu quả, việc hóa địa phương cho một bảng mã ký tự cụ thể sẽ trở nên dễ dàng hơn.
Ví dụ:
import java.util.Locale;
import java.util.ResourceBundle;
public class InternationalApplication {
/**
* Main method for the International Application.
*
* @param args the command line arguments
*/
public static void main(String[] args) {
// TODO: Add application logic here
String language;
String country;
if (args.length != 2) {
language = "en";
country = "US";
} else {
language = args[0];
country = args[1];
}
Locale currentLocale;
ResourceBundle messages;
currentLocale = new Locale(language, country);
messages = ResourceBundle.getBundle("MessagesBundle", currentLocale);
System.out.println(messages.getString("greetings"));
System.out.println(messages.getString("inquiry"));
System.out.println(messages.getString("farewell"));
}
}
Trong mã, chấp nhận hai đối số để đại diện cho quốc gia và ngôn ngữ. Tùy thuộc vào các đối số được chuyển vào trong quá trình thực thi chương trình, tin nhắn tương ứng với quốc gia và ngôn ngữ đó sẽ được hiển thị. Để thực hiện điều này, đã được tạo năm tập tin properties. Bạn có thể tạo một tập tin .properties trong NetBeans bằng cách sử dụng lựa chọn File > New > Other. Nếu tạo một dự án Java dựa trên Maven, các tập tin .properties nên được đặt trong đường dẫn src\main\resources.
Nội dung của năm tập tin properties là như sau:
// — MessagesBundle.properties
greetings = Xin chào.
farewell = Tạm biệt.
inquiry = Bạn có khỏe không?
// — MessagesBundle_de_DE.properties
greetings = Hallo.
farewell = Tschüss.
inquiry = Wie geht’s?
// — MessagesBundle_en_US.properties
greetings = Hello.
farewell = Goodbye.
inquiry = How are you?
// — MessagesBundle_fr_FR.properties
greetings = Bonjour.
farewell = Au revoir.
inquiry = Comment allez-vous?
// — MessagesBundle_ja_JP.properties
greetings = おはようございます (Ohayōgozaimasu).
farewell = さよなら/さようなら (Sayonara/Sayōnara).
inquiry = 元気ですか? (Genkidesuka)?
Unicode là một tiêu chuẩn ngành công nghiệp máy tính. Nó được sử dụng để mã hóa duy nhất các ký tự cho các ngôn ngữ khác nhau trên thế giới bằng các giá trị hexa. Nói một cách khác, Unicode cung cấp một số duy nhất cho mỗi ký tự không phụ thuộc vào nền tảng, chương trình, hoặc ngôn ngữ.
Lưu ý: Java sử dụng Unicode làm bảng mã ký tự nội địa của mình.
Các chương trình Java vẫn cần xử lý ký tự trong các hệ thống mã hóa khác. Lớp String có thể được sử dụng để chuyển đổi các hệ thống mã hóa tiêu chuẩn vào và ra khỏi hệ thống Unicode. Để chỉ định các ký tự Unicode không thể được biểu diễn trong ASCII, chẳng hạn như 6, bạn sử dụng dãy thoát \u220cx. Mỗi x trong dãy thoát là một chữ số hexa.
Dưới đây là danh sách định nghĩa các thuật ngữ được sử dụng trong mã hóa ký tự Unicode:
- Character: Đại diện cho đơn vị tối thiểu của văn bản có giá trị ngữ nghĩa.
- Character Set: Đại diện cho tập hợp các ký tự có thể được sử dụng bởi nhiều ngôn ngữ. Ví dụ, bảng ký tự Latin được sử dụng bởi tiếng Anh và một số ngôn ngữ châu Âu.
- Ký Tự Mã Hóa: Đây là tập hợp các ký tự trong bảng ký tự. Mỗi ký tự trong tập hợp được gán một số duy nhất.
- Mã Điểm: Đây là giá trị được sử dụng trong bảng ký tự mã hóa. Mã điểm là kiểu dữ liệu int 32-bit. Ở đây, 11 bit cao là 0 và 21 bit thấp đại diện cho một giá trị mã điểm hợp lệ.
- Đơn Vị Mã: Đây là giá trị char 16-bit.
- Ký Tự Bổ Sung: Đây là các ký tự có phạm vi từ U+10000 đến U+10FFFF. Các ký tự bổ sung được biểu diễn bằng một cặp giá trị mã điểm gọi là surrogate để hỗ trợ các ký tự mà không làm thay đổi kiểu dữ liệu nguyên thủy char. Surrogate cũng cung cấp tính tương thích với các chương trình Java trước đây.
Mặt Đất Ngôn Ngữ Đa Ngôn Ngữ (BMP): Đây là tập hợp các ký tự từ U+0000 đến U+FFFF.
Xem xét các điểm sau đối với mã hóa ký tự Unicode:
Giá trị hexa được tiền tố bằng chuỗi “U+”
Phạm vi mã điểm hợp lệ cho tiêu chuẩn Unicode là U+0000 đến U+10FFFF.
Bảng 10.3 hiển thị giá trị mã điểm cho một số ký tự cụ thể:
Ký Tự | Unicode Code Point | Glyph |
---|---|---|
Latin A | U+0041 | |
Latin sharp § | U+00DF | 8 |
Quá Trình Quốc Tế Hóa (đa ngôn ngữ)
Nếu mã nguồn được quốc tế hóa, hãy chú ý rằng các thông báo tiếng Anh cứng cáp đã bị loại bỏ. Các thông báo không còn được cứng cáp và mã ngôn ngữ được chỉ định tại thời điểm chạy, để có thể phân phối cùng một tệp thực thi trên toàn thế giới. Không cần biên dịch lại cho việc đa ngôn ngữ hóa. Đối với quá trình quốc tế hóa, các bước cần tuân theo như sau:
- Tạo tệp Properties
- Xác định Locale
- Tạo ResourceBundle
- Lấy văn bản từ lớp ResourceBundle
Tạo Tệp Properties
Một tệp properties lưu trữ thông tin về đặc điểm của một chương trình hoặc môi trường. Một tệp properties có định dạng văn bản thuần. Nó có thể được tạo bằng bất kỳ trình soạn thảo văn bản nào.
Trong ví dụ sau, các tệp properties lưu trữ văn bản cần được dịch. Lưu ý rằng trước khi quốc tế hóa, phiên bản gốc của văn bản đã được cứng cáp trong các câu lệnh System.out.print.
Tệp properties mặc định, MessagesBundle.properties, bao gồm các dòng sau:
greetings=Hello
farewell=Goodbye
inquiry=How are you?
Vì các thông báo được lưu trong tệp properties, chúng có thể được dịch sang nhiều ngôn ngữ khác nhau mà không cần thay đổi mã nguồn. Để dịch thông báo sang tiếng Pháp, người dịch tiếng Pháp tạo một tệp properties có tên là MessagesBundle_fr_FR.properties, chứa các dòng sau:
greetings=Bonjour
farewell=Aurevoir
inquiry=Comment allez-vous?
Chú ý rằng các giá trị bên phải của dấu bằng được dịch. Các khóa ở bên trái không thay đổi. Những khóa này không được phép thay đổi vì chúng được tham chiếu khi chương trình truy xuất văn bản đã được dịch.
Tên của tệp properties là quan trọng. Ví dụ, tên tệp MessagesBundle_fr_FR.properties chứa mã ngôn ngữ fr và mã quốc gia FR. Những mã này cũng được sử dụng khi tạo một đối tượng Locale.
Định nghĩa Locale
Đối tượng Locale xác định một kết hợp cụ thể giữa ngôn ngữ và quốc gia. Một Locale
đơn giản chỉ là một định danh cho một khu vực địa lý, chính trị hoặc văn hóa trong lớp java.util.Locale
. Bất kỳ hoạt động nào yêu cầu một locale để thực hiện nhiệm vụ của nó đều được coi là có liên quan đến locale. Những hoạt động này sử dụng đối tượng Locale
để điều chỉnh thông tin cho người dùng.
Ví dụ, hiển thị một số là một hoạt động phụ thuộc vào locale. Số đó nên được định dạng theo phong tục và quy ước của ngôn ngữ, khu vực hoặc văn hóa bản địa của người dùng.
Một đối tượng Locale
được tạo bằng cách sử dụng các hàm tạo sau:
public Locale(String language, String country)
: Điều này tạo một đối tượngLocale
với ngôn ngữ và quốc gia được chỉ định. Cú pháp như sau:
public Locale(String language, String country)
trong đó:
language
là mã ngôn ngữ gồm hai chữ cái viết thường.country
là mã quốc gia gồm hai chữ cái viết hoa.
public Locale(String language)
: Điều này tạo một đối tượngLocale
với ngôn ngữ được chỉ định. Cú pháp như sau:
public Locale(String language)
trong đó:
language
là mã ngôn ngữ gồm hai chữ cái viết thường.
Câu lệnh sau đây xác định một Locale
với ngôn ngữ là Tiếng Anh và quốc gia là Hoa Kỳ:
Locale aLocale = new Locale("en", "US");
Đoạn mã dưới tạo đối tượng Locale cho ngôn ngữ Pháp cho các quốc gia Canada và Pháp:
Locale caLocale = new Locale("fr", "CA");
frLocale = new Locale("fr", "FR");
Trong đó:
caLocale
là mộtLocale
cho ngôn ngữ Pháp và quốc gia Canada.frLocale
là mộtLocale
cho ngôn ngữ Pháp mà không chỉ định quốc gia.
Chương trình sẽ linh hoạt khi nó nhận thông tin locale từ dòng lệnh khi chạy, thay vì sử dụng mã ngôn ngữ và quốc gia được cứng cáp. Đoạn mã 21 mô tả cách chấp nhận mã ngôn ngữ và quốc gia từ dòng lệnh:
Đoạn mã 21:
String language = new String(args[0]);
String country = new String(args[1]);
Locale currentLocale = new Locale(language, country);
Ở đây:
language
là một chuỗi chứa mã ngôn ngữ từ dòng lệnh.country
là một chuỗi chứa mã quốc gia từ dòng lệnh.currentLocale
là một đối tượngLocale
được tạo với mã ngôn ngữ và quốc gia từ dòng lệnh.
Các đối tượng Locale chỉ là các định danh. Sau khi xác định một Locale, bước tiếp theo là chuyển nó cho các đối tượng khác thực hiện các nhiệm vụ hữu ích, như định dạng ngày tháng và số. Những đối tượng này phụ thuộc vào locale vì hành vi của chúng thay đổi tùy thuộc vào Locale. Một ResourceBundle là một ví dụ về một đối tượng phụ thuộc vào locale.
Phần tiếp theo mô tả một số phương thức quan trọng của lớp Locale:
public static Locale getDefault()
: Phương thức này lấy Locale mặc định cho phiên bản của JVM hiện tại. Ở đây, Locale là kiểu trả về. Nói cách khác, phương thức trả về một đối tượng của lớp Locale.public final String getDisplayCountry()
: Phương thức này trả về tên của quốc gia cho Locale hiện tại, thích hợp để hiển thị cho người dùng. Ở đây, String là kiểu trả về. Nói cách khác, phương thức trả về một chuỗi đại diện cho tên của quốc gia.public final String getDisplayLanguage()
: Phương thức này trả về tên của ngôn ngữ cho Locale hiện tại, thích hợp để hiển thị cho người dùng. Ví dụ, nếu locale mặc định là fr_FR, phương thức trả về “French”. Ở đây, String là kiểu trả về. Nói cách khác, phương thức trả về một chuỗi đại diện cho tên của ngôn ngữ.
Tạo ResourceBundle
Các đối tượng ResourceBundle chứa các đối tượng cụ thể của locale. Những đối tượng này được sử dụng để cô lập dữ liệu phụ thuộc vào locale, như văn bản có thể dịch được.
Lớp ResourceBundle được sử dụng để truy xuất thông tin cụ thể của locale từ tệp properties.
Thông tin này cho phép người dùng viết ứng dụng có thể:
- Đa ngôn ngữ hoặc dịch sang các ngôn ngữ khác nhau.
- Xử lý cho nhiều locale cùng một lúc.
- Hỗ trợ cho nhiều locale hơn sau này.
Lớp ResourceBundle có một phương thức tĩnh và cuối cùng gọi là getBundle()
giúp truy xuất một đối tượng ResourceBundle.
Phương thức getBundle(String, Locale)
của ResourceBundle giúp truy xuất thông tin cụ thể của locale từ một tệp properties cụ thể và nhận hai đối số, một chuỗi và một đối tượng của lớp Locale. Đối tượng của lớp ResourceBundle được khởi tạo với một ngôn ngữ và quốc gia hợp lệ phù hợp với tệp properties có sẵn.
Để tạo ResourceBundle, xem xét câu lệnh sau đây. Câu lệnh minh họa cách truy xuất giá trị từ cặp khóa-giá bằng cách sử dụng phương thức getString()
:
String msg1 = messages.getString("greetings");
Câu lệnh này sử dụng khóa “greetings” vì nó phản ánh nội dung của thông báo. Khóa thường được cứng cáp trong chương trình và phải xuất hiện trong các tệp properties. Nếu các nhà dịch vô tình sửa đổi các khóa trong các tệp properties, phương thức getString()
sẽ không thể định vị được các thông báo.
Các Thành phần Quốc Tế Hóa
Có nhiều yếu tố thay đổi theo văn hóa, khu vực và ngôn ngữ. Do đó, cần đảm bảo rằng tất cả các yếu tố như vậy được quốc tế hóa.
Tiêu Đề Các Phần Tử
Đây là những phần tiêu đề của các thành phần GUI như văn bản, ngày tháng và số. Các phần tiêu đề GUI này nên được địa phương hóa vì cách sử dụng của chúng thay đổi theo ngôn ngữ, văn hóa và khu vực.
Định dạng các phần tiêu đề của các thành phần GUI đảm bảo rằng giao diện của ứng dụng phản ánh theo cách nhạy cảm với ngôn ngữ địa phương. Mã hiển thị GUI là độc lập với locale. Không cần thiết phải viết các rutin định dạng cho các locale cụ thể.
Số, Tiền Tệ và Phần Trăm
Định dạng của số, tiền tệ và phần trăm thay đổi theo văn hóa, khu vực và ngôn ngữ. Do đó, cần định dạng chúng trước khi hiển thị. Ví dụ, số 12345678 nên được định dạng và hiển thị là 12.345.678 ở Mỹ và 12.345.678 ở Đức.
Tương tự, các biểu tượng tiền tệ và cách hiển thị yếu tố phần trăm cũng thay đổi theo khu vực và ngôn ngữ. Định dạng là cần thiết để tạo ra một ứng dụng quốc tế hóa, độc lập với các quy ước địa phương về dấu thập phân, phân cách hàng nghìn và biểu diễn phần trăm.
Lớp NumberFormat
được sử dụng để tạo các định dạng cụ thể cho số, tiền tệ và phần trăm.
NumberFormat
Lớp NumberFormat
có một phương thức tĩnh là getNumberInstance()
. Phương thức getNumberInstance()
trả về một thể hiện của lớp NumberFormat
được khởi tạo với locale mặc định hoặc được chỉ định. Sau đó, phương thức format()
của lớp NumberFormat
nên được gọi, nơi số cần được định dạng được truyền làm đối số. Đối số có thể là một nguyên thủy hoặc một đối tượng lớp wrapper.
Cú pháp cho một số phương thức được sử dụng để định dạng số như sau:
public static final NumberFormat getNumberInstance();
Ở đây, NumberFormat
là kiểu trả về. Phương thức này trả về một đối tượng của lớp NumberFormat
.
public final String format(double number)
Trong trường hợp này, number là số cần được định dạng. String là kiểu trả về và đại diện cho văn bản đã được định dạng.
public static NumberFormat getNumberInstance(Locale inLocale)
Ở đây, NumberFormat là kiểu trả về. Phương thức này trả về một đối tượng của lớp NumberFormat. Đối số inLocale là một đối tượng của lớp Locale.
Ví dụ:
import java.text.NumberFormat;
import java.util.Locale;
import java.util.ResourceBundle;
public class InternationalApplication {
public static void printValue(Locale currentLocale) {
Integer value = new Integer(123456);
Double amount = new Double(345987.246);
NumberFormat numFormatObj;
String valueDisplay;
String amtDisplay;
numFormatObj = NumberFormat.getNumberInstance(currentLocale);
valueDisplay = numFormatObj.format(value);
amtDisplay = numFormatObj.format(amount);
System.out.println(valueDisplay + " " + currentLocale.toString());
System.out.println(amtDisplay + " " + currentLocale.toString());
}
/**
* @param args the command.
*/
public static void main(String[] args) {
String language;
String country;
if (args.length != 2) {
language = new String("en");
country = new String("US");
} else {
language = new String(args[0]);
country = new String(args[1]);
}
Locale currentLocale;
ResourceBundle messages;
currentLocale = new Locale(language, country);
messages = ResourceBundle.getBundle("MessagesBundle", currentLocale);
System.out.println(messages.getString("greetings"));
System.out.println(messages.getString("inquiry"));
System.out.println(messages.getString("farewell"));
printValue(currentLocale);
}
}
Tiền tệ
Lớp NumberFormat có phương thức tĩnh getCurrencyInstance(), nhận một đối tượng của lớp Locale làm đối số. Phương thức getCurrencyInstance() trả về một đối tượng của lớp NumberFormat được khởi tạo cho locale cụ thể.
Cú pháp cho một số phương thức định dạng tiền tệ như sau:
public final String format(double currency)
: Ở đây, currency là số tiền cần định dạng. String là kiểu trả về và đại diện cho văn bản đã được định dạng.public static final NumberFormat getCurrencyInstance()
: Ở đây, NumberFormat là kiểu trả về, nghĩa là phương thức này trả về một đối tượng của lớp NumberFormat.public static NumberFormat getCurrencyInstance(Locale inLocale)
: Ở đây, NumberFormat là kiểu trả về, nghĩa là phương thức này trả về một đối tượng của lớp NumberFormat. Thêm vào đó, inLocale là Locale được chỉ định.
Đoạn mã dưới mô tả cách tạo định dạng tiền tệ dựa trên locale cho quốc gia Pháp.
NumberFormat currencyFormatter;
String strCurrency;
// Tạo một đối tượng Locale với ngôn ngữ là tiếng Pháp và quốc gia là Pháp
Locale locale = new Locale("fr", "FR");
// Tạo một đối tượng của lớp wrapper Double
Double currency = new Double(123456.78);
// Lấy đối tượng CurrencyFormatter
currencyFormatter = NumberFormat.getCurrencyInstance(locale);
// Định dạng tiền tệ
strCurrency = currencyFormatter.format(currency);
Tỷ lệ phần trăm
Lớp này có một phương thức tĩnh là getPercentInstance(), nhận một đối tượng của lớp Locale làm đối số. Phương thức getPercentInstance() trả về một đối tượng của lớp NumberFormat được khởi tạo cho locale cụ thể.
Cú pháp cho một số phương thức định dạng phần trăm như sau:
public final String format(double percent)
: Ở đây, percent là tỷ lệ phần trăm cần định dạng. String là kiểu trả về và đại diện cho văn bản đã được định dạng.public static final NumberFormat getPercentInstance()
: Ở đây, NumberFormat là kiểu trả về, nghĩa là phương thức này trả về một đối tượng của lớp NumberFormat.public static NumberFormat getPercentInstance(Locale inLocale)
: Ở đây, NumberFormat là kiểu trả về, nghĩa là phương thức này trả về một đối tượng của lớp NumberFormat. Thêm vào đó, inLocale là Locale được chỉ định.
Đoạn mã dưới cho thấy cách tạo định dạng phần trăm dựa trên locale cho quốc gia Pháp.
NumberFormat percentFormatter;
String strPercent;
// Tạo một đối tượng Locale với ngôn ngữ là tiếng Pháp và quốc gia là Pháp
Locale locale = new Locale("fr", "FR");
// Tạo một đối tượng của lớp wrapper Double
double percent = 0.05;
// Lấy đối tượng percentFormatter
percentFormatter = NumberFormat.getPercentInstance(locale);
// Định dạng số phần trăm
strPercent = percentFormatter.format(percent);
System.out.println(strPercent);
Ngày và Giờ (Date & Time)
“Định dạng ngày và giờ nên tuân theo quy ước của người dùng cuối cùng. Định dạng ngày và giờ thay đổi tùy thuộc vào văn hóa, khu vực và ngôn ngữ. Do đó, việc định dạng chúng trước khi hiển thị là cần thiết. Ví dụ, ở Đức, ngày có thể được biểu diễn dưới dạng 20.04.07, trong khi ở Hoa Kỳ, nó được biểu diễn dưới dạng 04/20/07. Java cung cấp các lớp java.text.DateFormat và java.text.SimpleDateFormat để định dạng ngày và giờ.
Lớp DateFormat được sử dụng để tạo định dạng cụ thể cho ngày dựa trên locale. Tiếp theo, phương thức format() của lớp NumberFormat cũng được gọi. Ngày cần định dạng được chuyển làm đối số.
Phương thức DateFormat.getDateInstance(style, locale) trả về một đối tượng của lớp DateFormat cho style và locale đã chỉ định. Cân nhắc đến cú pháp sau:
Cú pháp:
public static final DateFormat getDateInstance(int style, Locale locale)
Trong đó,
- style là một số nguyên và chỉ định kiểu của ngày.
- Các giá trị hợp lệ là DateFormat.LONG, DateFormat.SHORT và DateFormat.MEDIUM.
- Locale là một đối tượng của lớp Locale và chỉ định định dạng của locale.
Đối tượng DateFormat bao gồm một số hằng số như:
- SHORT: Hoàn toàn số như 12.13.45 hoặc 4:30 PM
- MEDIUM: Dài hơn, như Dec 25, 1945
- LONG: Dài hơn, như December 25, 1945
- FULL: Đại diện cho một thông số hoàn chỉnh như Tuesday, April 12, 1945 AD
Đoạn mã dưới thể hiện cách lấy một đối tượng DateFormat và hiển thị ngày theo định dạng tiếng Nhật.
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class DateInternationalApplication {
public static void main(String[] args) {
Date today;
String strDate;
DateFormat dateFormatter;
Locale locale = new Locale("ja", "JP");
dateFormatter = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
today = new Date();
strDate = dateFormatter.format(today);
System.out.println(strDate);
}
}
10.11.4 Tin nhắn
Hiển thị các tin nhắn như trạng thái và thông báo lỗi là một phần không thể thiếu của bất kỳ phần mềm nào.
Nếu các tin nhắn đã được xác định trước, như “Bản quyền của bạn đã hết hạn”, chúng có thể dễ dàng được dịch sang nhiều ngôn ngữ. Tuy nhiên, nếu các tin nhắn chứa dữ liệu biến, việc tạo các bản dịch ngôn ngữ một cách ngữ pháp đúng là khó khăn.
Ví dụ, xem xét tin nhắn sau đây trong tiếng Anh:
“Vào ngày 06/03/2007, chúng tôi phát hiện 10 virus”.
Trong tiếng Pháp, nó được dịch là:
“Le 06/03/2007, nous avons détecté 10 virus”.
Trong tiếng Đức, nó được dịch là:
“Am 06/03/2007 haben wir Virus 10 erkannt”.
Vị trí của động từ và dữ liệu biến thay đổi trong các ngôn ngữ khác nhau.
Không phải lúc nào cũng có thể tạo ra một câu có ngữ pháp đúng với việc nối các cụm từ và biến. Phương pháp nối hoạt động tốt trong tiếng Anh, nhưng nó không hoạt động cho các ngôn ngữ mà trong đó động từ xuất hiện ở cuối câu. Nếu thứ tự từ trong một tin nhắn được cố định trước, thì không thể tạo ra các bản dịch ngôn ngữ một cách ngữ pháp đúng cho tất cả các ngôn ngữ. Giải pháp là sử dụng lớp MessageFormat để tạo ra một tin nhắn phức hợp.
Để sử dụng lớp MessageFormat, thực hiện các bước sau:
Xác định các biến trong tin nhắn. Để làm điều này, viết xuống tin nhắn và xác định tất cả các phần biến của tin nhắn.
Ví dụ, xem xét tin nhắn sau:
“Vào lúc 6:41 PM ngày 25 tháng 4 năm 2007, chúng tôi phát hiện 7 virus trên ổ đĩa D:”
Trong tin nhắn này, có bốn phần biến được gạch chân.
Tạo một mẫu. Một mẫu là một chuỗi, chứa phần cố định của tin nhắn và các phần biến. Các phần biến được mã hóa trong {} với số đối số, ví dụ {0}, {1}, và cứ thế. Mỗi số đối số phải phù hợp với một chỉ số của một phần tử trong một đối tượng mảng chứa các giá trị đối số.
Tạo một mảng đối tượng cho các đối số biến. Đối với mỗi phần biến trong mẫu, cần có một giá trị để thay thế. Những giá trị này được khởi tạo trong một mảng Object. Các phần tử trong mảng Object có thể được xây dựng bằng cách sử dụng các constructor. Nếu một phần tử trong mảng yêu cầu dịch, nó nên được lấy từ Bộ tài nguyên với phương thức getString().
Tạo một đối tượng MessageFormat và đặt locale mong muốn. Locale là quan trọng vì tin nhắn có thể chứa các giá trị ngày và số, cần được dịch.
Áp dụng và định dạng mẫu. Để làm điều này, lấy chuỗi mẫu từ Bộ tài nguyên với phương thức getString(). Lớp MessageFormat có một phương thức applyPattern() để áp dụng mẫu cho đối tượng MessageFormat. Một khi mẫu đã được áp dụng cho đối tượng MessageFormat, gọi phương thức format().
Ví dụ:
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;
public class MessageFormatterInternationalApplication {
public static void main(String[] args) {
String template = "At {2, time, short} on {2,date, long}, we detected {1, number, integer} virus on the disk {0}";
MessageFormat formatter = new MessageFormat("");
String language, country;
if (args.length != 2) {
language = "en";
country = "US";
} else {
language = args[0];
country = args[1];
}
Locale currentLocale = new Locale(language, country);
formatter.setLocale(currentLocale);
ResourceBundle messages = ResourceBundle.getBundle("MessageFormatBundle", currentLocale);
Object[] messageArguments = { messages.getString("disk"), new Integer(7), new Date() };
formatter.applyPattern(messages.getString("template"));
String output = formatter.format(messageArguments);
System.out.println(output);
}
}
Dưới đây là ba tệp thuộc tính được yêu cầu bởi mã nguồn.
MessageFormatBundle.properties:
template=At {2,time,short} on {2,date,long}, we detected {1,number,integer} virus on the disk {0}
MessageFormatBundle_fr_FR.properties:
disk=D:
template=À {2,time,short} {2,date,long}, nous avons détecté le virus {1,number,integer} sur le disque {0}
MessageFormatBundle_de.properties:
disk=D:
template=Um {2,time,short} am {2,date,long}, ermittelten wir Virus {1,number,integer} auf der Scheibe {0}
Trong các tệp thuộc tính này:
{0}
,{1}
,{2}
là các vị trí của các đối số trong mẫu tin nhắn.{time,short}
,{date,long}
là định dạng thời gian và ngày tương ứng.{number,integer}
là định dạng số nguyên cho các số nguyên.
Những chuỗi như {0}
, {1}
, … sẽ được thay thế bằng giá trị tương ứng khi định dạng tin nhắn sử dụng lớp MessageFormat.
Parallelization (song song)
Java hỗ trợ việc song song hóa. Bằng cách song song hóa công việc, các lập trình viên có thể làm cho ứng dụng chạy nhanh hơn bằng cách sử dụng hiệu quả bộ xử lý đa lõi. Tuy nhiên, việc song song hóa bất kỳ loại công việc nào đều đến với những thách thức. Thách thức chính xuất hiện trong bước phân vùng. Lý tưởng nhất, một lập trình viên muốn phân vùng công việc sao cho mỗi phần công việc hoàn thành thực thi chính xác trong cùng một khoảng thời gian. Tuy nhiên, khi viết mã, lập trình viên phải đoán nơi phân vùng nên được thực hiện. Thường, điều này dẫn đến việc một số phần của chương trình mất thời gian xử lý hơn so với một số phần khác. Kết quả là phần đã hoàn thành phải đợi phần chưa hoàn thành, làm mất đi ý nghĩa của việc song song hóa. Những thách thức như vậy được giải quyết bằng cách sử dụng cách tiếp cận work stealing, là một chiến lược lập lịch cho các chương trình đa luồng.
Song song hóa đảm bảo rằng nếu các luồng của một số phần của chương trình hoàn thành công việc của họ sớm hơn so với các phần khác, họ sẽ tiếp tục công việc của những phần đang chậm để hoàn thành công việc tổng thể nhanh hơn. Tuy nhiên, work stealing cũng đặt ra một số thách thức. Đặc biệt, tính toàn vẹn dữ liệu phải được đảm bảo để đảm tránh các luồng khác nhau trong quá trình work stealing không thực hiện đọc và ghi dữ liệu không an toàn. Mặc dù thách thức này có thể được giải quyết bằng cách sử dụng các kỹ thuật đồng bộ hóa, việc làm như vậy có thể làm chậm quá trình xử lý hơn do đồng bộ hóa mang theo chi phí hiệu suất. Do đó, việc thực hiện work stealing nên được thực hiện với đồng bộ hóa tối thiểu.
Khung công cụ Fork-Join giới thiệu trong Java 7 đáp ứng yêu cầu của work stealing thông qua phân vùng công việc đệ quy cùng với một cấu trúc hàng đợi kép (deque) để giữ các nhiệm vụ. Ở cấp độ API, phương thức ForkJoinTask.join() cho phép một luồng tránh việc chặn chính nó và thay vào đó, yêu cầu công việc mà nó nên thực hiện từ pool.
Ngoài ra, từ Java 8 trở đi, cung cấp nhiều cải tiến trong tính năng đồng thời và song song hóa.
Java đã thực hiện nhiều cải tiến trong gói java.util.concurrent
từ phiên bản 8 trở đi.
CompletableFuture.AsynchronousCompletionTask
là một giao diện trong gói Java.util.concurrent
. Nó hoạt động như một giao diện đánh dấu để xác định các nhiệm vụ bất đồng bộ mà các phương thức async tạo ra. Giao diện đánh dấu này hữu ích để theo dõi, gỡ lỗi và theo dõi các hoạt động bất đồng bộ. Một giao diện mới khác được thêm vào gói java.util.concurrent
là CompletionStage<T>
. Giao diện này đại diện cho một giai đoạn trong quá trình tính toán bất đồng bộ. Khi tính toán kết thúc, giai đoạn hoàn thành. Tuy nhiên, đôi khi giai đoạn có thể khởi động một hoặc nhiều giai đoạn phụ thuộc được đại diện bởi CompletionStage<T>
.
Java cũng giới thiệu một exception mới, CompletionException
, trong gói java.util.concurrent
từ phiên bản 8. Một CompletionException
được ném khi gặp lỗi hoặc exception khác trong quá trình hoàn thành một kết quả hoặc nhiệm vụ.
Ngoài ra, Java đi kèm với các lớp sau trong gói java.util.concurrent
:
CompletableFuture
CountedCompleter
ConcurrentMap.KeySetView
Lớp CompletableFuture
Lớp CompletableFuture
triển khai cả hai giao diện CompletionStage
và Future
để đơn giản hóa việc đồng bộ hóa các hoạt động bất đồng bộ. Một triển khai của giao diện đã tồn tại đại diện cho kết quả của một tính toán bất đồng bộ. Phương thức get()
của Future
trả về kết quả của một tính toán sau khi tính toán hoàn thành, bị hủy bỏ một cách rõ ràng hoặc ném một exception. Điều này là một hạn chế trong lập trình bất đồng bộ mà lớp CompletableFuture
được thiết kế để giải quyết. Các phương thức của lớp CompletableFuture
có thể chạy bất đồng bộ và không làm dừng thực thi chương trình.
Bảng dưới giải thích các phương thức quan trọng có sẵn trong lớp CompletableFuture
.
Mô tả | Phương thức |
---|---|
supplyAsync() | Chấp nhận một đối tượng Supplier chứa mã được thực hiện bất đồng bộ. Phương thức này sau khi thực hiện mã bất đồng bộ trả về một đối tượng CompletableFuture mới mà có thể áp dụng các phương thức khác. |
thenApply() | Trả về một đối tượng CompletableFuture mới được thực thi với kết quả của giai đoạn đã hoàn thành, miễn là giai đoạn hiện tại hoàn thành một cách bình thường. |
join() | Trả về kết quả khi tính toán bất đồng bộ hiện tại hoàn thành hoặc ném một exception thuộc loại CompletionException . |
thenAccept() | Chấp nhận một đối tượng Consumer . Khi giai đoạn hiện tại hoàn thành, phương thức này bọc kết quả bằng đối tượng Consumer và trả về một đối tượng CompletableFuture mới. |
whenComplete() | Sử dụng một đối tượng BiConsumer làm đối số. Khi giai đoạn hoàn thành của cuộc gọi kết thúc, phương thức whenComplete() áp dụng kết quả giai đoạn hoàn thành cho BiConsumer . BiConsumer chấp nhận kết quả làm đối số đầu tiên và lỗi nếu có làm đối số thứ hai. |
getNow() | Đặt giá trị được chuyển vào nó làm kết quả nếu giai đoạn hoàn thành cuộc gọi không hoàn thành. |
Lớp CountedCompleter
Lớp CountedCompleter
mở rộng từ ForkJoinTask
để đại diện cho một hành động hoàn thành được thực hiện khi kích hoạt, miễn là không có hành động đang chờ. Phương thức compute()
của lớp CountedCompleter
thực hiện tính toán chính và thường gọi phương thức tryComplete()
một lần trước khi trả về. Phương thức tryComplete()
kiểm tra xem số lượng đang chờ có khác không và nếu có, giảm giá trị số lượng. Ngược lại, phương thức tryComplete()
gọi phương thức onCompletion(CountedCompleter)
và cố gắng hoàn thành người thực hiện của công việc này, và nếu thành công, đánh dấu công việc này là đã hoàn thành.
Tùy chọn, lớp CountedCompleter
có thể ghi đè các phương thức sau:
onCompletion(CountedCompleter)
: Để thực hiện một số hành động khi hoàn thành bình thường.onExceptionalCompletion(Throwable, CountedCompleter)
: Để thực hiện một số hành động khi có một exception được ném.
Một lớp CountedCompleter
được khai báo là CountedCompleter<Void>
khi lớp không tạo ra kết quả. Một lớp như vậy trả về null
như giá trị kết quả. Đối với một lớp như vậy, phải ghi đè phương thức getRawResult()
để cung cấp một kết quả từ join()
, invoke()
, và các phương thức liên quan.
Đoạn mã Snippet 27 thể hiện triển khai của lớp CountedCompleter
.
import java.util.ArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountedCompleter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
public class CountedCompleterDemo {
static class NumberComputator extends CountedCompleter<Void> {
final ConcurrentLinkedQueue<String> concurrentLinkedQueue;
final int start;
final int end;
NumberComputator(ConcurrentLinkedQueue<String> concurrentLinkedQueue, int start, int end) {
this(concurrentLinkedQueue, start, end, null);
}
NumberComputator(ConcurrentLinkedQueue<String> concurrentLinkedQueue, int start, int end, NumberComputator parent) {
super(parent);
this.concurrentLinkedQueue = concurrentLinkedQueue;
this.start = start;
this.end = end;
}
@Override
public void compute() {
if (end - start < 5) {
String s = Thread.currentThread().getName();
for (int i = start; i < end; i++) {
concurrentLinkedQueue.add(String.format("Iteration number: %d performed by thread %s", i, s));
}
propagateCompletion();
} else {
int mid = (end + start) / 2;
NumberComputator subTaskA = new NumberComputator(concurrentLinkedQueue, start, mid, this);
NumberComputator subTaskB = new NumberComputator(concurrentLinkedQueue, mid, end, this);
setPendingCount(1);
subTaskA.fork();
subTaskB.compute();
}
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ConcurrentLinkedQueue<String> linkedQueue = new ConcurrentLinkedQueue<>();
NumberComputator numberComputator = new NumberComputator(linkedQueue, 10, 100);
ForkJoinPool.commonPool().invoke(numberComputator);
ArrayList<String> list = new ArrayList<>(linkedQueue);
for (String listItem : list) {
System.out.println("" + listItem);
}
}
}
Đoạn mã tạo một lớp CountedCompleterDemo
với một lớp NumberComputator
tĩnh nằm bên trong. Lớp NumberComputator
có hai hàm tạo quá tải. Hàm tạo đầu tiên chấp nhận các tham số sau: một ConcurrentLinkedQueue
và các chỉ số bắt đầu và kết thúc để thực hiện các lặp. Hàm tạo quá tải đầu tiên, lẻn này, lại gọi hàm tạo thứ hai có một tham số bổ sung, một tham chiếu đến chính nó. Khối if
trong phương thức compute()
được ghi đè, cùng với một vòng lặp for
, lặp qua chỉ số bắt đầu và kết thúc nếu sự chênh lệch của chúng nhỏ hơn năm và cuối cùng gọi propagateCompletion()
để đánh dấu công việc hiện tại là đã hoàn thành. Ngược lại, hai công việc con của NumberComputator
được tạo ra. Phương thức setPendingCount(1)
với đối số 1 cho biết chỉ có công việc con được fork. Phương thức compute()
được gọi trên công việc con khiến công việc con thực thi đồng bộ. Sau đó, phương thức main()
gọi một NumberComputator
đã được khởi tạo bằng một ForkJoinPool
và xuất kết quả ra màn hình console.
Lớp ConcurrentHashMap.KeySetView
Lớp ConcurrentHashMap.KeySetView
cung cấp một cái nhìn thuận tiện về các khóa chứa trong ConcurrentHashMap
. ConcurrentHashMap.KeySetView
triển khai giao diện Set
, do đó, bạn có thể truy cập các khóa của ConcurrentHashMap
như một đối tượng Set
. Đối tượng Set
và đối tượng ConcurrentHashMap
duy trì một mối quan hệ hai chiều. Do đó, cập nhật đối tượng Set
cập nhật ConcurrentHashMap
và ngược lại.
Lưu ý: Bạn không thể trực tiếp tạo một trường hợp của ConcurrentHashMap.KeySetView
. Nó được sử dụng khi bạn gọi các phương thức như keySet()
, keySet(V)
, newKeySet()
, và newKeySet(int)
trên một triển khai của Map, chẳng hạn như ConcurrentHashMap
.
Đoạn mã Snippet 28 thể hiện việc tạo một KeySetView
mới để truy cập các khóa của ConcurrentHashMap
như một tập hợp.
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class KeySetViewDemo {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
map.put("Java", "Java");
map.put("Java EE", "Java EE");
map.put("Spring", "Spring");
Set<String> keySet = map.keySet();
System.out.println(keySet);
}
}
Mã tạo một Map và khởi tạo nó với các cặp giá trị khóa. Cuộc gọi đến keySet()
sử dụng ConcurrentHashMap.KeySetView
để trả về một Set các khóa, mà mã in ra đầu ra.
Những Cải Tiến Mới Gần Đây
Java 15 bao gồm nhiều cải tiến trong gói java.util.concurrent
. Các lớp tiện ích thường hữu ích trong lập trình đồng thời. Có các frameworks cụ thể, được chuẩn hóa và mở rộng, có sẵn trong gói này. Cũng bao gồm một số lớp chứa chức năng hữu ích, nhưng khó triển khai. Dưới đây là các module chính với mô tả ngắn cho mỗi module:
Giao Diện Executor
Giao diện Executor
có thể được sử dụng để định nghĩa các hệ thống giống như luồng tùy chỉnh, bao gồm cả các nhóm luồng, Input/Output (1/0) không đồng bộ và các frameworks nhẹ của một nhiệm vụ. Lớp triển khai giao diện quyết định cách và nơi nhiệm vụ được thực hiện, tức là, trong một luồng mới được tạo, một luồng thực thi nhiệm vụ đã tồn tại, hoặc luồng gọi execute()
. Những nhiệm vụ này có thể được thực hiện theo chuỗi hoặc đồng thời.
Phương thức execute()
có cú pháp như sau:
void execute(Runnable command)
Giao diện quan trọng khác là ExecutorService
. Là một framework tuyệt đối cho việc thực thi nhiệm vụ không đồng bộ, ExecutorService
quản lý cách tốt hơn việc xếp hàng và lập lịch nhiệm vụ và cho phép một cách kiểm soát hơn về việc tắt máy. Thực hiện định kỳ và trễ thực thi các nhiệm vụ được cung cấp bởi ScheduledExecutorService
và các giao diện liên quan của nó. Callable
là phiên bản mang kết quả của Runnable
. Tất cả các chức năng được biểu diễn dưới dạng Callable
được thực hiện không đồng bộ bởi các phương thức của ExecutorService
. Kết quả của mỗi chức năng này được trả về bởi các trường hợp Future
. Executors
xác định sự hoàn thành của một thực thi và cung cấp cách hủy một thực thi. RunnableFuture
chứa một phương thức run
sẽ đặt kết quả khi nó được thực thi.
Các Module Triển Khai
Các lớp ThreadPoolExecutor
và ScheduledThreadPoolExecutor
cung cấp các nhóm luồng linh hoạt và có thể điều chỉnh. ThreadPoolExecutor
thực hiện nhiệm vụ đã cho (Callable hoặc Runnable) thông qua một trong những luồng được lưu trữ nội bộ của nó.
Ngoài ra, lớp Executors
chứa nhiều phương thức nhà máy cho cấu hình các bộ thực thi. Các phương thức tiện ích để sử dụng chúng cũng được cung cấp. Các lớp cụ thể FutureTask
và ExecutorCompletionService
là một số tiện ích khác dựa trên Executors
.
Lớp ForkJoinPool
chứa một Executor
được thiết kế chủ yếu để xử lý các thể hiện của ForkJoinTask
và các lớp con của nó. Những lớp này sử dụng chiến lược lập lịch work-stealing để có được hiệu suất cao cho các nhiệm vụ yêu cầu tính toán mở rộng. Work stealing trong Java là một chiến lược để giảm xung đột và cải thiện thời gian xử lý và sử dụng tài nguyên trong ứng dụng đa luồng
Ví dụ:
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
public class ExecutorExample {
public static void main(String[] args) {
ImplementExecutor obj = new ImplementExecutor();
try {
obj.execute(new NewThrd());
} catch (RejectedExecutionException | NullPointerException exception) {
System.out.println(exception);
}
}
static class ImplementExecutor implements Executor {
@Override
public void execute(Runnable command) {
new Thread(command).start();
}
}
static class NewThrd implements Runnable {
@Override
public void run() {
System.out.println("This thread executed under executor");
}
}
}
Đoạn code trên thể hiện cách triển khai một hàm chạy lệnh vào một thời điểm nào đó. Tùy thuộc vào cách triển khai của Executor
, lệnh có thể chạy trong một luồng được gom nhóm, trong một luồng mới, hoặc trong luồng gọi.
Kết quả: This thread executed under executor.
Queues
Giao diện BlockingQueue
mở rộng có trách nhiệm định nghĩa các phiên bản chặn của put
và take
.
Có năm triển khai trong java.util.concurrent
cung cấp hỗ trợ cho giao diện này, bao gồm:
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue
PriorityBlockingQueue
DelayQueue
Giao diện mở rộng TransferQueue
và triển khai LinkedTransferQueue
giới thiệu một phương thức transfer
đồng bộ. Giao diện BlockingDeque
cũng mở rộng từ BlockingQueue
để hỗ trợ cả thao tác theo FIFO và LIFO (Last In First Out) dựa trên ngăn xếp. Triển khai được cung cấp bởi lớp LinkedBlockingDeque
.
Ví dụ thể hiện hàng đợi trễ và triển khai của DelayedTask
.
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DelayedTaskExample {
public static void main(String[] args) {
// Create a DelayQueue to store DelayedTask objects
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();
// Create a ScheduledExecutorService to schedule tasks
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
// Create and schedule DelayedTask instances
DelayedTask task1 = new DelayedTask("Task 1", 2000); // 2 seconds delay
DelayedTask task2 = new DelayedTask("Task 2", 4000); // 4 seconds delay
DelayedTask task3 = new DelayedTask("Task 3", 6000); // 6 seconds delay
// Schedule the tasks to be added to the DelayQueue
executorService.schedule(() -> delayQueue.put(task1), 0, TimeUnit.MILLISECONDS);
executorService.schedule(() -> delayQueue.put(task2), 0, TimeUnit.MILLISECONDS);
executorService.schedule(() -> delayQueue.put(task3), 0, TimeUnit.MILLISECONDS);
// Start a separate thread to process tasks from the DelayQueue
new Thread(() -> {
while (true) {
try {
// Take and process the next delayed task from the DelayQueue
DelayedTask nextTask = delayQueue.take();
System.out.println("Executing task: " + nextTask.getName() +
" at " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// Shutdown the executor service after a delay (just for demonstration)
executorService.schedule(() -> executorService.shutdown(), 10, TimeUnit.SECONDS);
}
}
Synchronizers
Bảng dưới liệt kê năm lớp hỗ trợ các biểu thức đồng bộ hóa cho một mục đích cụ thể.
Lớp | Mô tả |
---|---|
Semaphore | Đây là công cụ thông thường được sử dụng cho đồng thời. |
CountDownLatch | Đây là một tiện ích tiêu chuẩn và đơn giản để chặn, cho đến khi một số cụ thể điều kiện, sự kiện, hoặc tín hiệu được đáp ứng. |
CyclicBarrier | Đây là một điểm đồng bộ nhiều hướng có thể đặt lại. Nó được sử dụng trong một số kiểu lập trình song song cụ thể. |
Phaser | Cung cấp một rào cản linh hoạt được sử dụng để xử lý nhiều luồng với tính toán phân đợt. |
Exchanger | Lớp này thường được sử dụng trong các thiết kế đường ống, vì nó cho phép trao đổi đối tượng giữa hai luồng tại một điểm cụ thể. |
Ví dụ:
import java.util.concurrent.Semaphore;
class SharedData {
static int count = 0;
}
class AppThread extends Thread {
Semaphore sema;
String thrdName;
public AppThread(Semaphore sema, String thrdName) {
this.sema = sema;
this.thrdName = thrdName;
}
@Override
public void run() {
if (thrdName.equals("X")) {
System.out.println("Starting " + thrdName);
try {
sema.acquire();
System.out.println(thrdName + " gets a permit.");
// Accessing the SharedData resource
for (int i = 0; i < 5; i++) {
SharedData.count++;
System.out.println(thrdName + ": " + SharedData.count);
Thread.sleep(10);
}
} catch (InterruptedException exc) {
System.out.println(exc);
} finally {
// Release the permit
System.out.println(thrdName + " releases the permit.");
sema.release();
}
} else if (thrdName.equals("Y")) {
System.out.println("Starting " + thrdName);
try {
sema.acquire();
System.out.println(thrdName + " gets a permit.");
// Accessing the SharedData resource
for (int i = 0; i < 5; i++) {
SharedData.count--;
System.out.println(thrdName + ": " + SharedData.count);
Thread.sleep(10);
}
} catch (InterruptedException exc) {
System.out.println(exc);
} finally {
// Release the permit
System.out.println(thrdName + " releases the permit.");
sema.release();
}
}
}
}
public class SemaphoreExample {
public static void main(String[] args) throws InterruptedException {
// Creating a Semaphore object with the number of permits as 1
Semaphore sema = new Semaphore(1);
// Creating two threads with name X and Y
AppThread mt1 = new AppThread(sema, "X");
AppThread mt2 = new AppThread(sema, "Y");
// Starting threads X and Y
mt1.start();
mt2.start();
// Waiting for threads X and Y
mt1.join();
mt2.join();
// Displaying the final count value
System.out.println("Final count: " + SharedData.count);
}
}
Output:
Starting X
X gets a permit.
X: 1
X: 2
X: 3
X: 4
X: 5
X releases the permit.
Starting Y
Y gets a permit.
Y: 4
Y: 3
Y: 2
Y: 1
Y: 0
Y releases the permit.
Final count: 0
Thời gian (Timing)
Bằng cách sử dụng các độ mịn khác nhau, lớp TimeUnit giúp xác định và kiểm soát các hoạt động dựa trên thời gian chờ đợi. Một thời gian chờ đợi được mô tả là thời gian tối thiểu mà một phương thức phải chờ đợi trước khi nó cho biết đã hết thời gian. Những thời gian chờ đợi như vậy được phát hiện bởi các triển khai ngay khi chúng xảy ra. Tuy nhiên, có thể có khoảng thời gian giữa việc xác định một thời gian chờ đợi và luồng được thực hiện lại sau thời gian chờ đợi. Giá trị bằng hoặc nhỏ hơn không được xác định là không thời gian chờ đợi bởi tất cả các phương thức có tham số thời gian chờ đợi. Nếu bạn muốn một phương thức chờ đợi mãi mãi, hãy sử dụng giá trị Long.MAX_VALUE.
Bộ sưu tập đồng thời (Concurrent Collections)
Ngoài các hàng đợi, nhiều triển khai Collection khác cũng được cung cấp trong Concurrent Collections. Những triển khai như ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet, CopyOnWriteArrayList và CopyOnWriteArraySet có thể được sử dụng trong ngữ cảnh đa luồng. Khi cần nhiều luồng để truy cập một bộ sưu tập cụ thể, hãy sử dụng ConcurrentHashMap thay vì HashMap được đồng bộ hóa và ConcurrentSkipListMap thay vì TreeMap được đồng bộ hóa. Ngoài ra, khi số lần đọc và duyệt nhiều hơn số lần cập nhật được thực hiện trên một danh sách, bạn nên sử dụng CopyOnWriteArrayList thay vì ArrayList được đồng bộ hóa.
Một số lớp chứa trong gói này sử dụng tiền tố Concurrent. Tiền tố này là một cách rút gọn chỉ ra sự khác biệt giữa chúng và các lớp đồng bộ tương tự. Mặc dù một bộ sưu tập đồng thời là an toàn đối với luồng, nhưng nó không bị ràng buộc bởi một khóa loại trừ duy nhất. Việc truy cập một bộ sưu tập thông qua một khóa duy nhất dẫn đến tính mở rộng kém. Để tránh vấn đề này, hãy sử dụng các lớp được đồng bộ hóa. Bộ sưu tập đồng thời thường chỉ được sử dụng khi nhiều luồng cần truy cập một bộ sưu tập chung. Các bộ sưu tập không đồng bộ hữu ích chỉ khi các bộ sưu tập không được chia sẻ hoặc khi chúng có thể truy cập trong khi giữ các khóa khác.
Thuộc tính Tính nhất quán Bộ nhớ (Memory Consistency Properties)
Mối quan hệ “xảy ra trước” trên các đọc và ghi của biến được chia sẻ có thể được xác định. Ví dụ, một đọc của một luồng có thể xem xét kết quả của một hoạt động ghi bởi luồng khác chỉ khi các hoạt đ
ộng ghi xảy ra trước hoạt động đọc. Mối quan hệ “xảy ra trước” có thể được hình thành bởi các phương thức Thread.start() và Thread.join() và các cấu trúc volatile và synchronized. Một số mối quan hệ “xảy ra trước” bao gồm:
- Các hành động trong một luồng xảy ra trước các hành động sau trong luồng đó.
- Mở khóa một monitor xảy ra trước mỗi lần khóa liên tiếp của monitor cụ thể đó.
- Ghi xảy ra trước đọc cho một trường cụ thể.
- Một cuộc gọi để bắt đầu luồng xảy ra trước một cuộc gọi đến bất kỳ hành động nào khác trên luồng cụ thể đó.
- Tất cả các hành động cần thiết xảy ra trước khi các luồng khác trả về từ một cuộc gọi join trên luồng cụ thể đó.
Các phương thức có trong tất cả các lớp trong java.util.concurrent và các gói con của nó đảm bảo mức độ đồng bộ hóa cao.
Hoạt động nguyên thủy và khóa (Atomic Operations and Locks)
Trong ứng dụng Java, một thách thức liên quan đến khả năng mở rộng là duy trì một số lượng hoặc tổng duy nhất được cập nhật đồng thời bởi nhiều luồng. Đã từ Java 8, thách thức này đã được giải quyết thông qua một tập hợp nhỏ các lớp mới trong gói atomic của java.util.concurrent. Những lớp này được thiết kế để sử dụng các kỹ thuật giảm xung đột để cung cấp cải tiến về hiệu suất so với các biến atomic.
Các lớp mới được giới thiệu trong Java 8 như sau:
- LongAccumulator: Duy trì một giá trị dài chạy được cập nhật bằng cách sử dụng một hàm được cung cấp.
- LongAdder: Duy trì một tổng dài ban đầu là không.
- DoubleAccumulator: Duy trì một giá trị chạy dạng double được cập nhật bằng cách sử dụng một hàm được cung cấp.
- DoubleAdder: Duy trì một tổng double ban đầu là không.
Ví dụ:
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
public class AtomicOperationClassDemo {
private final LongAdder longAdder;
private final DoubleAdder doubleAdder;
public AtomicOperationClassDemo(LongAdder longAdder, DoubleAdder doubleAdder) {
this.longAdder = longAdder;
this.doubleAdder = doubleAdder;
}
public void incrementLong() {
longAdder.increment();
}
public long getLongCounter() {
return longAdder.longValue();
}
public void addDouble(int doubleValue) {
doubleAdder.add(doubleValue);
}
public double getSumAsDouble() {
return doubleAdder.doubleValue();
}
public static void main(String[] args) {
AtomicOperationClassDemo atomicOperationClassDemo = new AtomicOperationClassDemo(new LongAdder(), new DoubleAdder());
System.out.println("Long Counter:");
for (int i = 0; i < 10; i++) {
atomicOperationClassDemo.incrementLong();
System.out.println("Long Counter: " + atomicOperationClassDemo.getLongCounter());
}
System.out.println("\nDouble Sum:");
for (int j = 0; j < 10; j++) {
atomicOperationClassDemo.addDouble(j);
System.out.println("Double Sum: " + atomicOperationClassDemo.getSumAsDouble());
}
}
}
Mã nguồn khởi tạo một đối tượng LongAdder và một DoubleAdder trong phương thức khởi tạo. Phương thức incrementLong()
tăng giá trị LongAdder hiện tại lên 1 thông qua cuộc gọi LongAdder.increment()
.
Phương thức getLongCounter()
trả về giá trị LongAdder hiện tại. Phương thức addDouble()
thêm giá trị double được truyền vào vào giá trị hiện tại của DoubleAdder thông qua cuộc gọi DoubleAdder.add()
. Phương thức getSumAsDouble()
trả về giá trị DoubleAdder hiện tại.
Sau đó, phương thức main()
tạo một đối tượng AtomicOperationClassDemo
với các đối tượng LongAdder và DoubleAdder. Vòng lặp đầu tiên gọi phương thức incrementLong()
sau đó là getLongCounter()
. Vòng lặp thứ hai gọi phương thức addDouble()
sau đó là getSumAsDouble()
. Mã cũng in kết quả ra màn hình console từ cả hai vòng lặp for
.
Output:
Long Counter:
Long Counter: 1
Long Counter: 2
Long Counter: 3
Long Counter: 4
Long Counter: 5
Long Counter: 6
Long Counter: 7
Long Counter: 8
Long Counter: 9
Double Sum:
Double Sum: 0.0
Double Sum: 1.0
Double Sum: 3.0
Double Sum: 6.0
Double Sum: 10.0
Double Sum: 15.0
Double Sum: 21.0
Double Sum: 28.0
Double Sum: 36.0
Double Sum: 45.0
Một tính năng quan trọng khác mà Java bao gồm trong lập trình đồng thời là lớp StampedLock thuộc gói java.util.concurrent.Locks.
Trước Java 8, ReadWriteLock là cách phổ biến nhất để triển khai khóa. Tuy nhiên, ReadWriteLock có một số hạn chế. Hạn chế chính là nó gặp vấn đề đói (ví dụ, đói có thể xảy ra khi nhiều luồng có khóa đọc và chỉ một số luồng có khóa ghi). Ngoài ra, thông qua ReadWriteLock, không thể nâng cấp một khóa đọc thành khóa ghi. Một hạn chế khác là thiếu hỗ trợ cho đọc lạc quan. Lớp StampedLock giải quyết tất cả những hạn chế này. Thay vì cơ chế khóa thông thường, lớp StampedLock trả về một số long, còn được gọi là stamp, mỗi khi một khóa được cấp. Stamp này có thể được sử dụng để giải phóng một khóa hoặc kiểm tra xem khóa có hiệu quả.
Lớp StampedLock thực hiện khóa với ba chế độ để kiểm soát quyền truy cập đọc/gọi. Các chế độ này bao gồm:
- Ghi: Được thực hiện thông qua phương thức writeLock() để độc quyền cấp phát khóa, chặn nếu cần thiết cho đến khi có sẵn. Phương thức writeLock() trả về một stamp có thể được sử dụng trong phương thức unlockWrite(long) để giải phóng khóa. Chế độ ghi cũng được hỗ trợ bởi các phiên bản có thời gian và không có thời gian của tryWriteLock(). Khi một luồng giữ khóa ở chế độ ghi, không có khóa đọc nào có thể được cấp và tất cả các xác nhận đọc lạc quan đều thất bại.
- Đọc: Được thực hiện thông qua phương thức readLock() để không độc quyền cấp phát khóa, chặn nếu cần thiết cho đến khi có sẵn. Phương thức readLock() trả về một stamp có thể được sử dụng trong phương thức unlockRead(long) để giải phóng khóa. Chế độ đọc cũng được hỗ trợ bởi các phiên bản có thời gian và không có thời gian của tryReadLock().
- Đọc lạc quan: Được thực hiện thông qua phương thức tryOptimisticRead() và validate(long). Phương thức tryOptimisticRead() trả về một stamp khác không chỉ khi khóa hiện không được giữ ở chế độ ghi. Phương thức validate(long) trả về true nếu khóa chưa được cấp bởi một luồng khác ở chế độ ghi kể từ khi có được stamp đã cho. Vì chế độ đọc lạc quan có thể dễ dàng bị một luồng ghi chiếm đóng bất cứ lúc nào, chế độ này nên được sử dụng cẩn thận. Đề xuất sử dụng chế độ đọc lạc quan để cải thiện hiệu suất của các đoạn mã chỉ đọc ngắn mà không có tranh chấp.
Ví dụ:
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private double balance;
public StampedLockDemo(double balance) {
this.balance = balance;
System.out.println("available balance: " + balance);
}
public void deposit(double amount) {
System.out.println("\nAbout to deposit $" + amount);
long stamp = stampedLock.writeLock();
System.out.println("Applied write lock");
try {
balance += amount;
System.out.println("Available balance: " + balance);
} finally {
stampedLock.unlockWrite(stamp);
System.out.println("Unlocked write lock");
}
}
public void withdraw(double amount) {
System.out.println("\nAbout to withdraw $" + amount);
long stamp = stampedLock.writeLock();
System.out.println("Applied write lock");
try {
balance -= amount;
System.out.println("Available balance: " + balance);
} finally {
stampedLock.unlockWrite(stamp);
System.out.println("Unlocked write lock");
}
}
public double checkBalance() {
System.out.println("\nAbout to check balance");
long stamp = stampedLock.readLock();
System.out.println("Applied read lock");
try {
System.out.println("Available balance: " + balance);
return balance;
} finally {
stampedLock.unlockRead(stamp);
System.out.println("Unlocked read lock");
}
}
public double checkBalanceOptimisticRead() {
System.out.println("\nAbout to check balance with optimistic read lock");
long stamp = stampedLock.tryOptimisticRead();
System.out.println("Applied non-blocking optimistic read lock");
double balance = this.balance;
if (!stampedLock.validate(stamp)) {
System.out.println("Stamp has changed. Applying full-blown read lock.");
stamp = stampedLock.readLock();
try {
balance = this.balance;
} finally {
stampedLock.unlockRead(stamp);
System.out.println("Unlocked read lock");
}
}
System.out.println("Available balance: " + balance);
return balance;
}
public static void main(String[] args) {
StampedLockDemo stampedLockDemo = new StampedLockDemo(4000.00);
stampedLockDemo.withdraw(1000.00);
stampedLockDemo.deposit(5000.00);
stampedLockDemo.checkBalance();
stampedLockDemo.checkBalanceOptimisticRead();
}
}
Phương thức deposit()
trong mã nguồn có sử dụng khóa ghi thông qua cuộc gọi đến stampedLock.writeLock()
trước khi cập nhật trường số dư. Khối finally
bên trong phương thức deposit()
giải phóng khóa.
Phương thức withdraw()
cũng sử dụng khóa ghi thông qua cuộc gọi đến stampedLock.writeLock()
trước khi cập nhật trường số dư và giải phóng khóa trong khối finally
. Sau đó, phương thức checkBalance()
cấp phát khóa đọc thông qua cuộc gọi đến stampedLock.readLock()
để đọc số dư và giải phóng khóa trong khối finally
. Tiếp theo, phương thức checkBalanceOptimisticRead()
cấp phát một phương thức đọc lạc quan thông qua cuộc gọi StampedLock.tryOptimisticRead()
.
Sau khi có số dư hiện tại, mã kiểm tra xem con tem (stamp) có hợp lệ hay không thông qua cuộc gọi StampedLock.validate()
. Nếu con tem hợp lệ, số dư hiện tại được trả về. Ngược lại, một khóa đọc đầy đủ được cấp phát trước khi đọc số dư lại. Khối finally
giải phóng khóa với cuộc gọi stampedLock.unlockRead()
. Phương thức main()
tạo một đối tượng StampedLockDemo
được khởi tạo với một số dư kiểu long
. Cuối cùng, các phương thức withdraw()
, deposit()
, checkBalance()
, và checkBalanceOptimisticRead()
được gọi theo chuỗi.
Output:
available balance: 4000.0
About to withdraw $1000.0
Applied write lock
Available balance: 3000.0
Unlocked write lock
About to deposit $5000.0
Applied write lock
Available balance: 8000.0
Unlocked write lock
About to check balance
Applied read lock
Available balance: 8000.0
Unlocked read lock
About to check balance with optimistic read lock
Applied non-blocking optimistic read lock
Available balance: 8000.0
Các tính năng bổ sung của the Fork-Join Framework
Một số tính năng bổ sung của Framework Fork-Join bao gồm các tính năng được thêm vào lớp ForkJoinPool trong Java 8, song song hóa luồng và song song hóa sắp xếp mảng.
Các Tính Năng Mới của ForkJoinPool
Java 8 giới thiệu tính năng gọi là common thread pool, cho phép bất kỳ ForkJoinTask nào không được gửi một cách rõ ràng đến một thread pool cụ thể có thể sử dụng một common thread. Việc sử dụng common thread pool giúp ứng dụng giảm sử dụng tài nguyên.
Các phương thức mới của lớp ForkJoinPool như sau:
commonPool()
: Một phương thức tĩnh trả về một common thread pool.
getCommonPoolParallelism()
: Một phương thức mới trả về số lượng song song mục tiêu của common thread pool.
Song Song Hóa luồng (Stream Parallelization)
API Stream trong Java hoạt động trên bộ sưu tập và mảng để tạo ra dữ liệu theo dạng luồng có thể được sử dụng để thực hiện các hoạt động như lọc, sắp xếp và duyệt qua dữ liệu. Để hỗ trợ xử lý nhanh chóng của các luồng lớn, có thể áp dụng các phương pháp lập trình đa luồng truyền thống. Tuy nhiên, mặc định, các luồng không an toàn đối với luồng và do đó, việc sử dụng nhiều luồng để làm việc trên các luồng cần được xem xét cẩn thận để tránh mọi vấn đề về luồng. Để đối phó với các hạn chế của các phương pháp lập trình đa luồng để làm việc với các luồng , Java cung cấp hỗ trợ cho tính toán song song của các luồng . Với tính toán song song, các luồng có thể được truy cập nhanh chóng hơn mà không có rủi ro về vấn đề luồng.
Đoạn mã dưới mô tả việc sử dụng luồng song song để duyệt qua các phần tử của một ArrayList.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class ParallelStreamDemo {
public static void main(String[] args) {
List<String> items = new ArrayList<>();
items.add("one");
items.add("two");
items.add("three");
items.add("four");
Stream<String> parallelStream = items.parallelStream();
parallelStream.forEach(System.out::println);
}
}
Mã tạo một ArrayList và khởi tạo nó với các phần tử chuỗi. Phương thức parallelStream()
trả về một đối tượng luồng song song kiểu Stream. Vòng lặp forEach()
lặp qua luồng song song và in ra các phần tử.
Output:
three
one
two
four
Song Song Hóa Sắp Xếp Mảng
Java 8 giới thiệu một phương thức mới là parallelSort()
trong lớp Arrays
cho phép sắp xếp các phần tử của mảng một cách song song.
Đoạn mã dưới mô tả cách sắp xếp mảng một cách song song. Giả sử các câu lệnh import
liên quan đã được thêm vào.
import java.util.Arrays;
public class ParallelArraySortDemo {
public static void main(String[] args) {
int[] intArray = new int[100];
for (int i = 0; i < intArray.length; i++) {
intArray[i] = (int) (Math.random() * 100);
}
Arrays.parallelSort(intArray);
System.out.println(Arrays.toString(intArray));
}
}
Mã tạo một mảng int có kích thước 100 và điền giá trị là số nguyên ngẫu nhiên từ 0 đến 100. Phương thức parallelSort()
sắp xếp mảng một cách song song trước khi in các phần tử của mảng ra màn hình console.
Output:
[1, 2, 3, 5, 7, 9, 11, 14, 15, 16, 17, 18, 20, 21, 23, 25, 26, 28, 29, 32, 33, 34, 37, 38, 40, 42, 43, 44, 47, 49, 50, 51, 54, 56, 57, 58, 59, 60, 62, 63, 66, 68, 70, 72, 73, 75, 76, 78, 79, 80, 81, 82, 83, 85, 87, 88, 89, 91, 93, 95, 96, 97, 98, 99]
Hành Động Đệ Quy (Recursive Action)
Lớp ForkJoinPool
, một phần của Framework Fork-Join, mở rộng lớp AbstractExecutorService
để thực hiện thuật toán chính đánh đổi công việc để thực hiện các tiến trình ForkJoinTask
. RecursiveTask
và RecursiveAction
, là các triển khai của ForkJoinTask
, tương tự về cách chúng hoạt động. Cả RecursiveTask
và RecursiveAction
đều mở rộng từ ForkJoinTask
để đại diện cho các công việc chạy trong một ForkJoinPool
. Sự khác biệt là trong khi RecursiveTask
trả về một kết quả, RecursiveAction
thì không.
Đoạn mã dưới mô tả việc sử dụng chức năng Fork-Join với RecursiveAction
.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class RecursiveActionDemo extends RecursiveAction {
private long assignedWork;
public RecursiveActionDemo(long assignedWork) {
this.assignedWork = assignedWork;
}
private List<RecursiveActionDemo> createSubtasks() {
List<RecursiveActionDemo> subtaskList = new ArrayList<>();
RecursiveActionDemo subtask1 = new RecursiveActionDemo(this.assignedWork / 2);
RecursiveActionDemo subtask2 = new RecursiveActionDemo(this.assignedWork / 2);
subtaskList.add(subtask1);
subtaskList.add(subtask2);
return subtaskList;
}
@Override
protected void compute() {
if (this.assignedWork > 50) {
System.out.println("Splitting assignedWork: " + Thread.currentThread() + " computing: " + this.assignedWork);
List<RecursiveActionDemo> subtaskList = new ArrayList<>();
subtaskList.addAll(createSubtasks());
for (RecursiveAction subtask : subtaskList) {
subtask.fork();
}
} else {
System.out.println("Main thread " + Thread.currentThread() + " computing: " + this.assignedWork);
}
}
public static void main(String[] args) {
RecursiveActionDemo recursiveActionDemo = new RecursiveActionDemo(500);
final ForkJoinPool forkJoinPool = new ForkJoinPool(4);
forkJoinPool.invoke(recursiveActionDemo);
}
}
Constructor chứa mã để tạo một RecursiveActionDemo
được khởi tạo với giá trị assignedWork
được chuyển vào khi constructor được gọi. Phương thức createSubtasks()
tạo ra hai công việc con và trả về chúng dưới dạng một đối tượng List
cho người gọi.
Phương thức compute()
được ghi đè và gọi phương thức createSubtasks()
nếu assignedWork
lớn hơn 50 và sau đó, gọi phương thức fork()
của các công việc con để phân phối công việc. Nếu workLoad
nhỏ hơn 50, công việc sẽ được thực hiện bởi chính RecursiveActionDemo
. Phương thức main()
tạo một RecursiveActionDemo
được khởi tạo với giá trị 500 và gọi phương thức invoke()
trên một đối tượng ForkJoinPool
để bắt đầu hành động đệ quy.
Output:
Splitting assignedWork: Thread[ForkJoinPool.commonPool-worker-1,5,main] computing: 500
Splitting assignedWork: Thread[ForkJoinPool.commonPool-worker-1,5,main] computing: 250
Main thread Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 125
Main thread Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 125
Main thread Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 250
Splitting assignedWork: Thread[ForkJoinPool.commonPool-worker-1,5,main] computing: 62
Main thread Thread[ForkJoinPool.commonPool-worker-4,5,main] computing: 31
Main thread Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 62
Lưu ý: Đầu ra có thể thay đổi do thứ tự thực thi các luồng không được đảm bảo. Ngoài ra, tên luồng chính xác có thể khác nhau trong mỗi lần chạy.