Các tính năng nâng cao mới trong Java
- 02-03-2024
- Toanngo92
- 0 Comments
Mục lục
Cải tiến trong Switch
Cho đến nay, một kiểu câu lệnh switch truyền thống đã được sử dụng phổ biến trong Java. Switch này tuân theo thiết kế của C và C++. Điểm quan trọng nhất cần lưu ý trong switch cũ này là hành vi rơi vào (fall-through behavior), hoạt động tốt miễn là có các câu lệnh break được bao gồm. Ở đây, khi một case được kích hoạt và thực thi, switch tự động tiếp tục đến case tiếp theo, trừ khi có một câu lệnh break. Tuy nhiên, hãy xem xét tình huống nếu câu lệnh break bị bỏ sót. Quá trình thực thi sẽ tiếp tục đến các case khác, làm cho mã lệnh dễ phát sinh lỗi. Do đó, điều này nên được sử dụng nhiều hơn trong mã nguồn cấp thấp hơn thay vì mã nguồn cấp cao.
Từ Java 14 trở đi, có sự cải tiến trong câu lệnh switch.
Các tính năng của switch được cải tiến như sau:
Hỗ trợ nhiều giá trị cho mỗi case
Switch truyền thống không hỗ trợ việc chỉ định nhiều giá trị cho mỗi case. Ngược lại, switch được cải tiến cho phép bạn chỉ định nhiều giá trị cho mỗi case, mỗi giá trị được phân tách bằng dấu phẩy.
import java.util.Scanner;
public class EnhancedSwitchDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter Product ID to check the Product label: ");
int prodID = sc.nextInt();
switch (prodID) {
case 101, 102, 103:
System.out.println("You have selected a smartwatch!");
break;
case 104, 105:
System.out.println("You have selected a smartphone!");
break;
default:
System.out.println("Product not recognized.");
break;
}
}
}
Trong Đoạn mã trên, mã kiểm tra ID sản phẩm mà người dùng đã nhập. Nếu ID khớp với bất kỳ giá trị case nào được cung cấp, nó sẽ thực thi khối mã/lệnh tương ứng. Vì 105 được chỉ định ở đây làm đầu vào, case tương ứng với giá trị này sẽ khớp thành công và lệnh thích hợp sẽ được in ra.
Sử dụng “yield” để trả về một giá trị
‘Một từ khóa mới được giới thiệu là “yield”, giống với câu lệnh return, nhưng chỉ được sử dụng với switch. Nó được sử dụng để chỉ định giá trị cần được trả về từ một nhánh của switch. Một yield kết thúc biểu thức switch, vì không cần phải có một câu lệnh break sau đó.
Ví dụ:
import java.util.Scanner;
public class EnhancedSwitchDemo2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter the Product Name to check the Product availability: ");
String prodName = sc.next();
int res = getResultViaSwitch(prodName);
String status = (res == 1) ? "Available." : "Not Available.";
System.out.println("The Product is " + status);
}
private static int getResultViaSwitch(String name) {
int result = switch (name) {
case "Bolt", "Nut" -> 1;
case "Rivet", "Screw" -> 2;
case "Nail" -> 3;
default -> -1;
};
return result;
}
}
Trong Đoạn mã trên, switch và yield được sử dụng để kiểm tra tên sản phẩm mà người dùng đã nhập. Trong đoạn mã này, không có các câu lệnh return hoặc break trong các khối case của switch. Khối switch đơn giản kiểm tra tên sản phẩm khớp và sử dụng yield hoặc return để trả về kết quả tương ứng. Chương trình sau đó sử dụng kết quả trả về trong một câu lệnh với toán tử ba ngôi để gán trạng thái “Có sẵn” hoặc “Không có sẵn”. Trạng thái sau đó được thêm vào một chuỗi “Sản phẩm là” trong một câu lệnh in. Do đó, kết quả cuối cùng sẽ thông báo cho người dùng liệu sản phẩm mà tên đã được nhập có sẵn hiện tại hay không có sẵn.
Hoạt động như một biểu thức
Switch truyền thống chỉ được sử dụng như một câu lệnh, trong khi switch được cải tiến cũng có thể hoạt động như một biểu thức. Sự khác biệt giữa chúng là bạn có thể sử dụng switch như một biểu thức dựa trên đầu vào và nó cũng có thể trực tiếp trả về một giá trị. Điều này tương tự như toán tử ba ngôi, thực hiện cùng một logic bằng cách sử dụng khối if-else.
Đoạn mã dưới mô tả một ví dụ để hiểu rõ hơn điều này:
import java.util.Scanner;
public class EnhancedSwitchDemo3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter a Code to check State stats:");
int ivar = sc.nextInt();
// Retrieve the result of a switch expression
// and assign it to a variable.
String numberYieldColon = switch (ivar) {
case 0 -> "Texas";
case 1 -> "California";
case 2 -> {
String colResult = "Exclusively";
colResult = colResult + " Seattle";
yield colResult;
}
case 3 -> "Finally, Chicago";
default -> "NA";
}; // switch ends with semicolon
System.out.println("Leading State is " + numberYieldColon);
}
}
Trong đoạn mã này, switch đang hoạt động như một biểu thức thay vì một câu lệnh, vì kết quả của switch đang được gán cho một biến. Dựa trên mã nhập của người dùng, khối switch so sánh từng case một. Từ khóa yield đã được sử dụng để trả về một chuỗi cho mỗi case. Cũng có một case trong đó một chuỗi được nối thêm và sau đó được trả về thông qua yield. Lưu ý rằng khối switch kết thúc bằng một dấu chấm phẩy vì nó hiện đang là một biểu thức. Toàn bộ khối bên phải của toán tử gán gán giá trị cho numberYieldColon đều tạo thành một biểu thức.
Sự Cần Thiết của Việc Trả về Giá Trị/Exception
Khi sử dụng biểu thức switch, nhà phát triển phải xử lý tất cả các giá trị có thể có. Điều này có thể được thực hiện bằng cách cung cấp case cho tất cả các giá trị có thể có hoặc bằng cách cung cấp trường hợp mặc định.
Việc sử dụng biểu thức switch là bắt buộc phải trả về một giá trị hoặc ném một exception một cách rõ ràng, bất kể giá trị đầu vào là gì. Ngoài ra, người lập trình cần đảm bảo rằng không có NullPointerException nào được phát sinh do biểu thức switch.
Ví dụ, nếu bạn tắt bỏ khối mặc định trong Đoạn mã trên, một lỗi biên dịch sẽ xuất hiện, cho biết rằng switch đã không bao hết tất cả các giá trị có thể có. Do đó, bạn có thể sử dụng một khối mặc định với một số hành động hoặc ném một exception trong khối mặc định.
Đoạn mã dưới thể hiện một phiên bản được sửa đổi của Đoạn mã trên trong đó một exception rõ ràng được ném khi tên sản phẩm nhập vào không được tìm thấy.
import java.util.Scanner;
public class EnhancedSwitchDemo4 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter the Product Name to check the Product availability:");
String prodName = sc.next();
int res = getResultViaYield(prodName);
String status = (res == 1) ? "Available" : "Not Available";
System.out.println("The Product is " + status);
}
private static int getResultViaYield(String name) {
int result = switch (name) {
case "Bolt", "Nut" -> 1;
case "Rivet", "Screw" -> 2;
case "Nail" -> 3;
default -> throw new IllegalArgumentException(name + " is an unknown product and not found in the catalog.");
};
return result;
}
}
Sử dụng cú pháp mũi tên
Một bổ sung mới khác cho câu lệnh hoặc biểu thức switch là cú pháp mũi tên. Trong cú pháp này, một ký hiệu mũi tên được sử dụng trong mỗi trường hợp và trong khối mặc định.
switch với cú pháp mũi tên -> có thể được sử dụng cả như một biểu thức và một câu lệnh. Nếu cả hai trường hợp câu lệnh ở bên trái và bên phải khớp, thì các lệnh ở phía bên phải của mũi tên -> được thực thi.
Các yếu tố có thể xuất hiện ở phía bên phải của mũi tên -> là một câu lệnh ném (throw statement), một câu lệnh hoặc một biểu thức và một khối {}.
Trong cú pháp mũi tên mới, không cần phải bao gồm break và nó không có hành vi rơi vào/chạy qua (fall-through) (đây là tình huống đặt case nhưng không có break). Nhà phát triển vẫn có thể sử dụng nhiều giá trị cho mỗi case. Lợi ích chính là nó không yêu cầu một câu lệnh break để bỏ qua hành vi rơi vào mặc định.
case: nên được sử dụng nếu cần hành vi rơi vào/chạy qua, nếu không thì nên sử dụng case ->
Ví dụ:
import java.util.Scanner;
public class EnhancedSwitchDemoArrow {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter Product ID to check the Product label: ");
int prodId = sc.nextInt();
System.out.println(getResultViaYield(prodId));
}
private static String getResultViaYield(int id) {
String res = switch (id) {
case 1 -> "This id represents a smart television";
case 2 -> "This id represents a smartphone";
case 3, 4 -> "This id represents a smart microwave";
default -> "Sorry, No match found";
};
return res;
}
}
Thay đổi phạm vi
Đã thay đổi Phạm vi
Trong một switch truyền thống, các biến được khai báo sẽ tồn tại cho đến khi câu lệnh switch được thực thi. Giả sử nếu bạn khai báo một biến trong một trong những nhánh case, nó sẽ tồn tại trong tất cả các nhánh tiếp theo cho đến khi kết thúc switch. Do đó, nếu bạn muốn giữ các nhánh case cá nhân như là các phạm vi riêng biệt, thì việc cung cấp một khối {} là cần thiết. Với điều này, bạn có thể tránh được xung đột biến.
Ví dụ:
import java.util.Scanner;
public class ProductLabelDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter Product ID to check the Product label: ");
int prodID = sc.nextInt();
switch (prodID) {
case 101: {
// The num variable exists just in this {} block
int num = 20;
System.out.println("The value of num is " + num);
break;
}
case 102: {
// This is okay, {} block has a separate scope
int num = 300;
System.out.println("The value of num is " + num);
break;
}
default:
System.out.println("Oops! No matches");
break;
}
}
}
Các Phương thức Mới của FileSystems
Có ba phương thức mới đã được thêm vào lớp FileSystems. Điều này giúp dễ dàng xử lý các nhà cung cấp hệ thống tệp, mà xem xét nội dung của một tệp như một hệ thống tệp. Dưới đây là các phương thức/cú pháp mới được thêm vào:
- newFileSystem(path)
- newFileSystem(Path, Map < String, ?>)
- newFileSystem(Path, Map<String, ?>, ClassLoader)
Mục đích của mỗi phương thức này là xây dựng một FileSystem mới dựa trên các tham số đã cho.
Ví dụ:
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class NewFilesystemDemo {
public static void main(String[] args) {
try {
Map<String, String> env = new HashMap<>();
// In the following line, we are trying to get the path of the zip file
Path zipPath = Paths.get("ASample.zip");
// Creating URI from the zip path received
URI uri = new URI("jar:file", zipPath.toUri().getPath(), null);
// Create a new file system from URI
FileSystem fileSystem = FileSystems.newFileSystem(uri, env);
// Display a message to inform the user
System.out.println("Hurray, you have created File System successfully.");
// Here, we check if the file system is open or not using isOpen() method
if (fileSystem.isOpen()) {
System.out.println("It seems File system is open");
} else {
System.out.println("It seems File system is closed");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Lớp java.nio.file.FileSystems
trong Java được thiết kế như một nhà máy (factory) để tạo ra các hệ thống tệp mới. Đối tượng file system đã được tạo ra trong Đoạn mã 7 giúp truy cập các tệp và đối tượng khác trong hệ thống tệp. Trong đoạn mã này, nhà phát triển cung cấp đường dẫn của một tệp Zip và sử dụng các phương thức của FileSystems
để kiểm tra xem nó có mở hay đã đóng. Hệ thống tệp này hoạt động như một nhà máy để tạo ra các đối tượng khác như Path
, PathMatcher
, UserPrincipalLookupService
, và WatchService
.
Ví dụ FileSystem sử dụng HashMap:
import java.util.*;
import java.net.URI;
import java.nio.file.*;
public class NewFileSystemDemo2 {
public static void main(String[] args) throws Throwable {
Map<String, String> env = new HashMap<>();
env.put("create", "true");
// Locate the file system by using the syntax defined in java.net.JarURLConnection
URI uri = URI.create("jar:file:/c:/first/aSample.zip");
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) {
Path externalTxtFile = Paths.get("/first/Java1.txt");
Path pathInZipFile = zipfs.getPath("/Java1.txt");
// Here we copy a file into the zip file
Files.copy(externalTxtFile, pathInZipFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copy Successful");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Trong Đoạn mã trên, một thể hiện của lớp Path
được tạo ra. Sau đó, một hệ thống tệp zip được tạo ra bằng cách chỉ định đường dẫn của tệp zip. Các tùy chọn cấu hình cho hệ thống tệp zip được chỉ định trong đối tượng java.util.Map
được truyền vào phương thức FileSystems.newFileSystem()
. Tiếp theo, một hoạt động sao chép được thực hiện. Đường dẫn của tệp nguồn trong hệ thống tệp mặc định được chỉ định. Hệ thống tệp đích cũng đã được chỉ định là thư mục zip. Tệp văn bản cụ thể sẽ được sao chép vào hệ thống tệp mới, nghĩa là tệp zip. Khi bạn mở tệp zip, bạn sẽ thấy rằng tệp văn bản đã được thêm vào đó.
Phương thức Predicate.not()
Phương thức tĩnh Predicate.not()
được sử dụng để phủ định một predicate hiện tại. Giao diện Predicate
có sẵn trong gói java.util.function
.
Cú pháp
negate = Predicate.not(positivePredicate);
Trong đó,
negate
: một predicate phủ định kết quả của predicate được cung cấp.positivePredicate
: predicate để phủ định.
Quy trình bao gồm việc tạo ra một predicate và sau đó, khởi tạo các điều kiện cho nó. Hoặc bạn cũng có thể tạo một predicate khác để tạo một phủ định và sau đó, gán nó với phương thức not()
.
Ví dụ:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateNotDemo {
public static void main(String[] args) {
List<Integer> sampleList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
/* Let's create a predicate for negation */
Predicate<Integer> findEven = i -> i % 2 == 0;
/* Now, let's create a predicate object which is the negation of the supplied predicate */
Predicate<Integer> findOdd = Predicate.not(findEven);
/* Start filtering the even numbers using the even predicate */
List<Integer> evenNumbers = sampleList.stream().filter(findEven).collect(Collectors.toList());
/* Start filtering the odd numbers using the odd predicate */
List<Integer> oddNumbers = sampleList.stream().filter(findOdd).collect(Collectors.toList());
/* Try to print the Lists for odd or even numbers */
System.out.println("Here is the list of even numbers:\n" + evenNumbers);
System.out.println("Here is the list of odd numbers:\n" + oddNumbers);
}
}
Phương thức Predicate.negate()
Phương thức Predicate.negate()
trước tiên tạo ra phủ định logic của predicate hiện tại và sau đó, trả về nó. Đoạn mã 10 thể hiện một ví dụ về phương thức Predicate.negate()
.
Ví dụ:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateNegateDemo {
public static void main(String[] args) {
List<Integer> sampleList = Arrays.asList(2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029);
// This is to check whether the year is a leap year or not
Predicate<Integer> isLeap = i -> i % 4 == 0;
Predicate<Integer> isNotLeap = isLeap.negate();
List<Integer> leapList = sampleList.stream()
.filter(isLeap)
.collect(Collectors.toList());
List<Integer> notLeapList = sampleList.stream()
.filter(isNotLeap)
.collect(Collectors.toList());
// Print both the lists
System.out.println("Leap Years are " + leapList);
System.out.println("Not Leap Years are " + notLeapList);
}
}
Records
Trong Java, records là một dạng hạn chế mới của lớp để khai báo các loại như enum.
Được giới thiệu lần đầu tiên trong Java 14, records đã được cải tiến trong Java 16. Các thay đổi bổ sung đã được mang lại trong phiên bản này. Việc định nghĩa một record là một cách dễ dàng để định nghĩa một đối tượng giữ dữ liệu không thay đổi. Nó cam kết với một API phù hợp với biểu diễn sau khi khai báo nó.
Records không thể tách API khỏi biểu diễn. Do lý do này, records trở nên ngắn gọn hơn.
Từ khóa “record” được sử dụng để tạo một lớp record. Một khai báo lớp record bao gồm tên, các tham số kiểu tùy chọn, một tiêu đề và một thân. Tiêu đề là một danh sách các thành phần của lớp record, đó là các biến tạo nên trạng thái của nó (còn được gọi là mô tả trạng thái).
Vì records được xem xét là đơn giản và là người giữ trong suốt cho dữ liệu, chúng tự động có được nhiều thành viên tiêu chuẩn. Dưới đây là một số thành viên tiêu chuẩn của một record:
- Mỗi thành phần của mô tả trạng thái chứa một trường private final.
- Mỗi thành phần của mô tả trạng thái chứa một phương thức truy cập đọc công khai. Kiểu và tên giống như thành phần.
- Sử dụng một constructor công khai để khởi tạo mỗi trường bằng các đối số tương ứng. Chữ ký của constructor này khớp với mô tả trạng thái.
- Các phương thức hashCode() và equals() được triển khai. Xem xét hai records chứa trạng thái và kiểu giống nhau. Hai records này sẽ được xem xét là bằng nhau do triển khai của các phương thức này.
- Phương thức toString() sẽ được triển khai. Điều này bao gồm tất cả các chuỗi của tất cả các thành phần của record, cùng với tên của chúng.
Lưu ý những điều sau khi tạo lớp record:
- Một lớp record không thể được mở rộng và là final. Nó không thể là abstract.
- Lớp record mở rộng lớp java.lang.Record một cách ngầm định.
- Các trường được định nghĩa trong khai báo record là final.
Các phương thức Constructor, equals(), hashCode(), và toString() được tạo tự động cho một lớp record. Các phương thức truy cập cũng được tạo tự động từ lớp record. Tên phương thức và tên trường giống nhau, khác với phương thức truy cập thông thường và phổ quát. Constructor được tạo mặc định có thể khá phiền toái khi sử dụng trong thực tế, vì vậy như một lựa chọn thay thế, bạn có thể khai báo một constructor compact với chữ ký ngầm định. Điều này làm cho mã nguồn dễ dàng hơn và cũng giúp đọc mã nguồn hiệu quả hơn.
- Lập trình viên có thể chỉnh sửa các trường record tùy thuộc vào kiểu dữ liệu.
- Sử dụng tất cả các trường được đưa ra trong định nghĩa record, có thể tạo ra một constructor duy nhất.
Ví dụ:
package examrecord;
public class Main {
public record ExamRecord(String studentName, int score) {
}
public static void main(String[] args) {
ExamRecord record = new ExamRecord("John Doe", 85);
System.out.println("Student: " + record.studentName());
System.out.println("Score: " + record.score());
}
}
Text Blocks
Mặc dù là tính năng xem trước trong các phiên bản trước đó, text blocks là một tính năng chuẩn trong Java 16. Text blocks cung cấp một cách khác để viết chuỗi literal trong mã nguồn. Chúng cho phép bạn bao gồm các đoạn văn bản literal của HTML, JSON, SQL, hoặc bất cứ thứ gì bạn muốn, một cách chính xác và dễ hiểu hơn. Lợi ích chính của việc sử dụng text blocks là có thể sử dụng dòng mới cùng với dấu ngoặc kép một cách tự do, mà không cần phải thoát dòng.
Ví dụ, một text block đơn giản như sau:
String example = """
Sample text""";
Tính năng Text Block đã được giới thiệu trong Java với mục đích:
- Dễ dàng diễn đạt các chuỗi dài, tránh sử dụng chuỗi ký tự thoát. Điều này giúp làm cho việc viết chương trình Java trở nên dễ dàng hơn.
- Hỗ trợ hiểu các chuỗi mô tả mã không phải là Java trong các chương trình Java, từ đó cải thiện tính đọc của mã.
- Cho phép thao tác trên một cấu trúc tương tự như một chuỗi ký tự thoát. Bạn có thể chuyển đổi từ chuỗi ký tự thoát, vì bất kỳ cấu trúc mới nào cũng có thể diễn đạt các chuỗi tương tự như một chuỗi ký tự thoát và giải thích các chuỗi thoát giống nhau.
Trước đây, khi nhúng một đoạn mã HTML, XML, SQL hoặc JSON vào một chuỗi ký tự trong Java, cần phải sửa đổi một cách đáng kể, chẳng hạn như thêm delimiters và chuỗi thoát, kết hợp hai chuỗi (gọi là nối chuỗi), và vân vân. Đối với nhà phát triển, điều này làm cho các đoạn mã nguồn trở nên phức tạp và khó bảo trì. Tính năng khối văn bản giờ đây giúp giải quyết vấn đề này bằng cách loại bỏ nhu cầu cho tất cả công việc bổ sung này. Như vậy, nhà phát triển có thêm quyền kiểm soát đối với định dạng.
Ví dụ sử dụng chuỗi json theo cách cũ:
public class JsonDemo {
public static void main(String[] args) {
String json = "{\"name\": \"Dune\", \"year\": 2021, " +
"\"details\": {\"actors\": 25, \"budget\": 35, \"units\": 6}," +
"\"tags\": [\"films\", \"epic\"], \"rating\": 9}";
System.out.println(json);
}
}
Ví dụ sử dụng sử dụng textBlock:
public class JsonDemo {
public static void main(String[] args) {
String json = """
{
"name": "Dune",
"year": 2021,
"details": {
"actors": 25,
"budget (millions)": 35,
"units": 6
},
"tags": ["films", "epic"],
"rating": 9
}
""";
System.out.println(json);
}
}
Flow API
Flow API là sự hỗ trợ chính thức cho Reactive Streams Specification. Đó là sự kết hợp giữa các mô hình Iterator và Observer, tức là các mô hình Pull và Push. Khác với RxJava là một API dành cho người dùng cuối, Flow API là một đặc tả tương tác.
Flow API bao gồm bốn giao diện cơ bản:
- subscriber: Subscriber đăng ký theo dõi Publisher để có thể theo dõi các gọi lại được tạo ra.
- Publisher: Publisher công bố luồng các mục dữ liệu cho các subscriber đã đăng ký.
- subscription: Là giao diện kết nối giữa publisher và subscriber.
- Processor: Processor nằm giữa Publisher và Subscriber và thực hiện nhiệm vụ chuyển đổi một luồng thành một luồng khác.
Đoạn mã dưới cho thấy một subscriber chính chấp nhận một đối tượng dữ liệu, in nó ra và mong đợi thêm một đối tượng nữa. Một triển khai của publisher được cung cấp bởi Java (SubmissionPublisher) có thể được sử dụng để hoàn thành phiên truyền dữ liệu.
Ví dụ: tạo file SubscriberDemo.java
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
public class SubscriberDemo<T> implements Flow.Subscriber<T> {
private Flow.Subscription subs;
@Override
public void onSubscribe(Flow.Subscription subs) {
this.subs = subs;
this.subs.request(1);
}
@Override
public void onNext(T item) {
System.out.println(item);
subs.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Control has reached onComplete method");
}
}
Đoạn mã trên là một ví dụ về lớp Subscription. Khi triển khai lớp Flow.Subscriber, các phương thức onNext() và onComplete() được ghi đè.
Ví dụ dưới triển khai FlowAPI khởi tạo đối tượng subcriberDemo
import java.util.concurrent.Flow;
import java.util.List;
import java.util.concurrent.SubmissionPublisher;
public class FlowAPIDemo {
public static void main(String[] args) {
List<String> items = List.of("1", "2", "3", "4", "5", "6", "7", "8");
SubmissionPublisher<String> samplePublisher = new SubmissionPublisher<>();
samplePublisher.subscribe(new SubscriberDemo<>());
items.forEach(s -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
samplePublisher.submit(s);
});
samplePublisher.close();
}
}
Output:
1
2
3
4
5
6
7
8
9
10
Control has reached onComplete method
Ở đây, một publisher mẫu đã được tạo ra. Sau đó, một danh sách được tạo. Phương thức submit() của lớp SubmissionPublisher được sử dụng để xuất bản mỗi mục trong danh sách cho mỗi subscriber.
Giao diện API HTTP Client
Các phiên bản trước đây của Java chỉ cung cấp một API HttpURLConnection cấp thấp. API này không có các tính năng hiệu suất cao và không thân thiện với người dùng. Để khắc phục nhược điểm, các thư viện bên thứ ba như Apache HttpClient, Jetty và Spring RestTemplate thường được sử dụng.
Sau đó, Java giới thiệu một API HTTP Client mới, dựng sẵn từ phiên bản 11. API này được sử dụng để gửi yêu cầu và nhận lại phản hồi. Mô-đun HTTP Client không thay đổi đã được tạo ra lần đầu tiên như một mô-đun thử nghiệm trong JDK 9 và trở nên ổn định từ Java 11. Nó hỗ trợ HTTP/2 với sự tương thích ngược. Để sử dụng nó, nhà phát triển nên xác định mô-đun bằng cách sử dụng tệp module-info.java chỉ định các mô-đun cần thiết để chạy ứng dụng. Khác với HttpURLConnection, HTTP Client cung cấp cả cơ chế yêu cầu đồng bộ và không đồng bộ.
Các loại API HTTP mới có thể được tìm thấy trong gói java.net.http. Chúng bao gồm các lớp, giao diện, enums và nhiều hơn nữa.
Ba lớp chính trong API là như sau:
- HttpRequest — Đây là lớp đại diện cho yêu cầu được gửi thông qua HttpClient.
- HttpClient — Lớp này được sử dụng như một bảng chứa để cấu hình thông tin chung cho nhiều yêu cầu.
- HttpResponse — Lớp này đại diện cho kết quả của một cuộc gọi HttpRequest.
Để xử lý một yêu cầu, bạn nên đầu tiên tạo một HttpClient thông qua một builder (HttpClient.Builder).
Ví dụ, người ta có thể tạo một client, yêu cầu và phản hồi như sau:
HttpClient testClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
HttpResponse<String> response = testClient.send(request, BodyHandlers.ofString());
Một HttpClient
cung cấp thông tin chia sẻ tài nguyên và cấu hình cho các yêu cầu được gửi thông qua nó. Sau đó, nhà phát triển có thể sử dụng builder để cấu hình theo trạng thái của client, chẳng hạn như phiên bản giao thức ưa thích (HTTP/1.1 hoặc HTTP/2), liệu có theo dõi chuyển hướng, proxy hay authenticator không.
Mỗi HttpRequest
được gửi đi đều được cung cấp với một BodyHandler
. Phần thân phản hồi, nếu có, được xử lý bởi BodyHandler
. Khi bạn nhận được một HttpResponse
, bạn có thể truy cập vào các tiêu đề, mã phản hồi và thân phản hồi (thường là). Dựa trên kiểu T
của thân phản hồi, các byte thân phản hồi có thể được đọc hoặc không. Phản hồi HTTP đến với một số tham số, liệu phần thân có phải là HTML chỉ đọc hay có thể sửa đổi vì bất kỳ lý do nào như điền biểu mẫu hoặc JavaScript sẽ được biết thông qua tham số T
và BodyHandler
sẽ xử lý HTML Script.
Bạn có thể gửi các yêu cầu theo cách đồng bộ hoặc không đồng bộ.
send(HttpRequest, BodyHandler)
sẽ chặn cho đến khi toàn bộ quá trình gửi yêu cầu và nhận phản hồi được hoàn thành.
Khi yêu cầu được gửi bằng cách sử dụng sendAsync(HttpRequest, BodyHandler)
, phản hồi được nhận không đồng bộ. Phương thức sendAsync()
trả về ngay lập tức một CompletableFuture<HttpResponse>
. CompletableFuture
này kết thúc khi phản hồi sẵn có để khai báo các phụ thuộc giữa nhiều công việc không đồng bộ. CompletableFuture
được trả về có thể được kết hợp theo nhiều cách khác nhau.
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
import java.time.Duration;
public class HttpClientExample {
public static void main(String[] args) throws Exception {
// Create an HttpClient with custom configurations
HttpClient myClient = HttpClient.newBuilder()
.version(Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(10))
.proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
.authenticator(java.net.Authenticator.getDefault())
.build();
// Create an HttpRequest
HttpRequest request = HttpRequest.newBuilder()
.uri(new java.net.URI("http://example.com"))
.build();
// Send the request and retrieve the response
HttpResponse<String> response = myClient.send(request, HttpResponse.BodyHandlers.ofString());
// Print the status code and body of the response
System.out.println("Status Code: " + response.statusCode());
System.out.println("Response Body: " + response.body());
}
}
Trong trường hợp này, các yêu cầu là đồng bộ và chặn, có nghĩa là quá trình xử lý bị chặn cho đến khi phản hồi đến. Điều này có thể dẫn đến vấn đề khi xử lý lượng dữ liệu lớn.
Đoạn mã dưới thể hiện một ví dụ về bất đồng bộ. Trong trường hợp này, quá trình xử lý sẽ là không chặn, nghĩa là nó không đợi phản hồi. Các nhiệm vụ sẽ được thực hiện bất đồng bộ:
HttpRequest myRequest = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/"))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofFile(Paths.get("file.json")))
.build();
// Finally, the client request is processed
client.sendAsync(myRequest, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
Kiểm tra bảo mật:
Khi sử dụng các phương thức send
của HttpClient
, các kiểm tra bảo mật được thực hiện miễn là có một quản lý bảo mật có sẵn. Bạn sẽ cần một quyền URLPermission
hợp lệ để truy cập máy chủ đích và máy chủ proxy nếu có.
Hỗ trợ Định dạng Tiền tệ theo Phong cách Kế toán
Hầu hết các ứng dụng ngày nay được tạo ra để phục vụ đối tượng lớn, ví dụ như người dùng Internet, thường xử lý tiền. Do đó, trong các ứng dụng như vậy, có yêu cầu hiển thị tiền hoặc đơn vị tiền tệ yêu cầu theo định dạng cụ thể của địa điểm hoặc quốc gia đó.
Ở một số quốc gia và địa điểm, có một kiểu đặc biệt cho tiền tệ trong đó số tiền được biểu thị trong dấu ngoặc. Điều này được gọi là kiểu kế toán. Trong kiểu này, trong địa điểm như en_US
, giá trị -$9.44 sẽ xuất hiện dưới dạng ($9.44). Từ Java 14 trở đi, các trường hợp định dạng tiền tệ như kiểu kế toán có thể được có được bằng cách gọi phương thức NumberFormat.getCurrencyInstance(Locale)
với phần mở rộng Unicode u-cf-account
.
Ví dụ:
import java.text.*;
import java.util.*;
public class CurrencyDemo {
public static void main(String[] args) {
DecimalFormat df1 = (DecimalFormat) DecimalFormat.getCurrencyInstance(Locale.US);
System.out.println(df1.format(-9.44)); // -$9.44
Locale myLocale = new Locale.Builder().setLocale(Locale.US)
.setExtension(Locale.UNICODE_LOCALE_EXTENSION, "nu-account")
.build();
DecimalFormat df2 = (DecimalFormat) NumberFormat.getCurrencyInstance(myLocale);
System.out.println(df2.format(-9.44)); // ($9.44)
}
}
Output:
-$9.44
($9.44)
Lớp CompactNumberFormat
CompactNumberFormat
là một lớp con cụ thể của NumberFormat
được sử dụng để định dạng số thập phân dưới dạng rút gọn dựa trên các mẫu.
Bạn có thể tạo một instance mới của CompactNumberFormat
cho một khu vực sử dụng một trong những phương thức tạo mẫu được cung cấp bởi NumberFormat
. Một ví dụ về CompactNumberFormat
được hiển thị trong Đoạn mã dưới:
import java.text.NumberFormat;
import java.util.Locale;
public class CurrencyDemo {
public static void main(String[] args) {
long nfl = 15000000;
NumberFormat numFormatObj2 = NumberFormat.getNumberInstance(Locale.GERMANY);
NumberFormat numFormatObj3 = NumberFormat.getNumberInstance(Locale.ITALY);
System.out.println("Currencies without compact numbering:");
System.out.printf("%s %s %s %s%n", numFormatObj2.format(nfl), "Germany",
numFormatObj3.format(nfl), "Italy");
final NumberFormat numberFormatGrShort = NumberFormat.getCompactNumberInstance(Locale.GERMANY, NumberFormat.Style.SHORT);
final NumberFormat numberFormatGrLong = NumberFormat.getCompactNumberInstance(Locale.GERMANY, NumberFormat.Style.LONG);
final NumberFormat numberFormatItShort = NumberFormat.getCompactNumberInstance(Locale.ITALY, NumberFormat.Style.SHORT);
final NumberFormat numberFormatItLong = NumberFormat.getCompactNumberInstance(Locale.ITALY, NumberFormat.Style.LONG);
System.out.println("\nDemonstrating Compact Number Formatting on " + nfl);
System.out.println("\tDE/Short: " + numberFormatGrShort.format(nfl));
System.out.println("\tDE/Long: " + numberFormatGrLong.format(nfl));
System.out.println("\tIT/Short: " + numberFormatItShort.format(nfl));
System.out.println("\tIT/Long: " + numberFormatItLong.format(nfl));
}
}
Output:
Currencies without compact numbering:
15.000.000 Germany 15.000.000 Italy
Demonstrating Compact Number Formatting on 15000000
DE/Short: 15M
DE/Long: 15 million
IT/Short: 15M
IT/Long: 15 milioni
Đoạn mã dưới hiển thị một ví dụ khác về định dạng số rút gọn đơn giản.
import java.text.NumberFormat;
import java.util.Locale;
public class CompactNumberFormatDemo {
public static void main(String[] args) {
NumberFormat sampleNoFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println(sampleNoFormat.format(200));
System.out.println(sampleNoFormat.format(2000));
System.out.println(sampleNoFormat.format(20000));
System.out.println(sampleNoFormat.format(200000));
NumberFormat sampleShortFormat = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println(sampleShortFormat.format(200));
System.out.println(sampleShortFormat.format(2000));
System.out.println(sampleShortFormat.format(20000));
System.out.println(sampleShortFormat.format(200000));
}
}
Output:
200
2,000
20,000
200,000
200
2K
20K
200K
Một phiên bản tùy chỉnh của CompactNumberFormat
có thể được sử dụng để biểu diễn số dưới dạng ngắn gọn bằng cách sử dụng constructor CompactNumberFormat(String, DecimalFormatSymbols, String[])
.
Đoạn mã dưới hiển thị một phiên bản tùy chỉnh của CompactNumberFormat
.
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Currency;
public class CustomCompactDemo {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
final String[] cmpctPttrns = {"", "", "", "Ok", "00k", "000k",
"0M", "00M", "0B", "0b", "00b", "0t", "00t"};
final DecimalFormat decimalFormat = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
final CompactNumberFormat customCompactNoFormat = new CompactNumberFormat(
decimalFormat.toPattern(),
decimalFormat.getDecimalFormatSymbols(),
cmpctPttrns);
System.out.println(decimalFormat.toPattern());
}
}
Output:
#,##0.###
Các mẫu rút gọn được sử dụng để biểu diễn một chuỗi các mẫu. Ở đây, mỗi mẫu được sử dụng trong quá trình định dạng một dãy số. Ví dụ, một đến mười lăm mẫu có thể được cung cấp trong một mảng, nhưng mẫu đầu tiên cung cấp, luôn giống như 100. Dựa trên số lượng phần tử mảng này, các giá trị cho khoảng từ 100 đến 1014 được xác định.
Set fractional number
Khái niệm này giải thích về số chữ số phần phân số tối thiểu mà có thể chấp nhận trong một số. Mặc định, phần phân số được đặt là ‘0’ chữ số. Đoạn mã dưới cho thấy một ví dụ về định dạng với số chữ số phân số.
import java.text.NumberFormat;
import java.util.Locale;
public class FractionFormatDeno {
public static void main(String[] args) {
NumberFormat format = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
format.setMinimumFractionDigits(3);
System.out.println(format.format(20000));
System.out.println(format.format(20012));
System.out.println(format.format(200201));
System.out.println(format.format(2222222));
}
}
Output:
20.000K
20.012K
200.201K
2.222M
Trong đoạn mã này, một instance của CompactNumberFormat
dựa trên khu vực hiện tại được tạo bằng cách sử dụng NumberFormat
. Sau đó, bằng cách sử dụng phương thức setMinimumFractionDigits()
, số chữ số phần phân số được chỉ định. Các giá trị số đã cho được định dạng và kết quả được hiển thị.
Phân tích số ngắn gọn (compact number parsing) là một quy trình được sử dụng để phân tích số ngắn gọn thành một mẫu dài. Đoạn mã dưới hiển thị quá trình phân tích của một số ngắn gọn:
import java.text.NumberFormat;
import java.util.Locale;
public class ParsingFormatDemo {
public static void main(String[] args) throws Exception {
NumberFormat format = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println(format.parse("200"));
System.out.println(format.parse("2 thousand"));
System.out.println(format.parse("20 thousand"));
System.out.println(format.parse("200 thousand"));
}
}
Output:
200
2000
20000
200000
Đoạn mã này phân tích các giá trị đã cho bằng cách sử dụng phương thức parse()
của CompactNumberFormat
dựa trên Locale
đã chỉ định.