JavaRush /Blog Java /Random-VI /Hướng dẫn Java 8. 1 phần.
ramhead
Mức độ

Hướng dẫn Java 8. 1 phần.

Xuất bản trong nhóm

"Java vẫn còn tồn tại - và mọi người đang bắt đầu hiểu nó."

Chào mừng bạn đến với phần giới thiệu của tôi về Java 8. Hướng dẫn này sẽ hướng dẫn bạn từng bước qua tất cả các tính năng mới của ngôn ngữ. Thông qua các ví dụ mã ngắn, đơn giản, bạn sẽ học cách sử dụng các phương thức mặc định của giao diện , biểu thức lambda , phương thức tham chiếucác chú thích có thể lặp lại . Đến cuối bài viết, bạn sẽ làm quen với những thay đổi mới nhất đối với API như luồng, giao diện chức năng, tiện ích mở rộng liên kết và API ngày mới. Không có những bức tường văn bản nhàm chán - chỉ là một loạt các đoạn mã nhận xét. Thưởng thức!

Các phương thức mặc định cho giao diện

Java 8 cho phép chúng ta thêm các phương thức không trừu tượng được triển khai trong giao diện thông qua việc sử dụng từ khóa mặc định . Tính năng này còn được gọi là phương pháp mở rộng . Đây là ví dụ đầu tiên của chúng tôi: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } Ngoài phương thức trừu tượng tính toán , giao diện Công thức còn xác định một phương thức mặc định sqrt . Các lớp triển khai giao diện Công thức chỉ triển khai phương thức tính toán trừu tượng . Phương thức sqrt mặc định có thể được sử dụng ngay lập tức. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 Đối tượng công thức được triển khai dưới dạng đối tượng ẩn danh. Mã này khá ấn tượng: 6 dòng mã để tính toán đơn giản sqrt(a * 100) . Như chúng ta sẽ thấy sâu hơn trong phần tiếp theo, có một cách hấp dẫn hơn để triển khai các đối tượng phương thức đơn lẻ trong Java 8.

Biểu thức Lambda

Hãy bắt đầu với một ví dụ đơn giản về cách sắp xếp một mảng chuỗi trong các phiên bản Java đầu tiên: Phương thức trợ giúp thống kê Collections.sort lấy một danh sách và một Bộ so sánh để sắp xếp các phần tử của danh sách đã cho. Điều thường xảy ra là bạn tạo các bộ so sánh ẩn danh và chuyển chúng sang các phương thức sắp xếp. Thay vì luôn tạo các đối tượng ẩn danh, Java 8 cung cấp cho bạn khả năng sử dụng ít cú pháp, biểu thức lambda hơn nhiều : Như bạn có thể thấy, mã ngắn hơn và dễ đọc hơn nhiều. Nhưng ở đây nó thậm chí còn ngắn hơn: Đối với phương thức một dòng, bạn có thể loại bỏ dấu ngoặc nhọn {} và từ khóa return . Nhưng đây là nơi mã thậm chí còn ngắn hơn: trình biên dịch Java nhận biết được các loại tham số, vì vậy bạn cũng có thể loại bỏ chúng. Bây giờ chúng ta hãy tìm hiểu sâu hơn về cách sử dụng biểu thức lambda trong cuộc sống thực. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

Giao diện chức năng

Làm thế nào để các biểu thức lambda phù hợp với hệ thống kiểu Java? Mỗi lambda tương ứng với một loại nhất định được xác định bởi một giao diện. Và cái gọi là giao diện chức năng phải chứa chính xác một phương thức trừu tượng được khai báo. Mọi biểu thức lambda của một loại nhất định sẽ tương ứng với phương thức trừu tượng này. Vì các phương thức mặc định không phải là phương thức trừu tượng nên bạn có thể tự do thêm các phương thức mặc định vào giao diện chức năng của mình. Chúng ta có thể sử dụng một giao diện tùy ý làm biểu thức lambda, miễn là giao diện đó chỉ chứa một phương thức trừu tượng. Để đảm bảo giao diện của bạn đáp ứng các điều kiện này, bạn phải thêm chú thích @FunctionalInterface . Trình biên dịch sẽ được thông báo bằng chú thích này rằng giao diện chỉ được chứa một phương thức và nếu nó gặp phương thức trừu tượng thứ hai trong giao diện này, nó sẽ báo lỗi. Ví dụ: Hãy nhớ rằng mã này cũng hợp lệ ngay cả khi chú thích @FunctionalInterface chưa được khai báo. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Tham chiếu đến các phương thức và hàm tạo

Ví dụ trên có thể được đơn giản hóa hơn nữa bằng cách sử dụng tham chiếu phương thức thống kê: Java 8 cho phép bạn chuyển tham chiếu đến các phương thức và hàm tạo bằng cách sử dụng ký hiệu từ khóa :: . Ví dụ trên cho thấy cách sử dụng các phương pháp thống kê. Nhưng chúng ta cũng có thể tham chiếu các phương thức trên các đối tượng: Hãy xem cách sử dụng :: hoạt động như thế nào đối với các hàm tạo. Trước tiên, hãy xác định một ví dụ với các hàm tạo khác nhau: Tiếp theo, chúng ta xác định giao diện nhà máy PersonFactory để tạo các đối tượng người mới : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } Thay vì triển khai nhà máy theo cách thủ công, chúng tôi liên kết mọi thứ lại với nhau bằng cách sử dụng tham chiếu hàm tạo: Chúng tôi tạo một tham chiếu đến hàm tạo của lớp Person thông qua Person::new . Trình biên dịch Java sẽ tự động gọi hàm tạo thích hợp bằng cách so sánh chữ ký của hàm tạo với chữ ký của phương thức PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Vùng Lambda

Việc tổ chức quyền truy cập vào các biến phạm vi bên ngoài từ biểu thức lambda tương tự như việc truy cập từ một đối tượng ẩn danh. Bạn có thể truy cập các biến cuối cùng từ phạm vi cục bộ, cũng như các trường mẫu và biến tổng hợp.
Truy cập các biến cục bộ
Chúng ta có thể đọc một biến cục bộ bằng công cụ sửa đổi cuối cùng trong phạm vi của biểu thức lambda: Nhưng không giống như các đối tượng ẩn danh, các biến không cần phải được khai báo cuối cùng để có thể truy cập được từ biểu thức lambda . Mã này cũng đúng: Tuy nhiên, biến num phải không thay đổi, tức là. được ngầm định cuối cùng để biên dịch mã. Đoạn mã sau sẽ không được biên dịch: Không được phép thay đổi số trong biểu thức lambda. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Truy cập các trường mẫu và biến thống kê
Không giống như các biến cục bộ, chúng ta có thể đọc và sửa đổi các trường phiên bản và biến thống kê bên trong biểu thức lambda. Chúng tôi biết hành vi này từ các đối tượng ẩn danh. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Truy cập vào các phương thức giao diện mặc định
Bạn có nhớ ví dụ về công thức ở phần đầu tiên không? Giao diện Công thức xác định một phương thức sqrt mặc định có thể được truy cập từ mọi phiên bản của công thức , bao gồm cả các đối tượng ẩn danh. Điều này không hoạt động với biểu thức lambda. Không thể truy cập các phương thức mặc định bên trong biểu thức lambda. Đoạn mã sau không biên dịch: Formula formula = (a) -> sqrt( a * 100);

Giao diện chức năng tích hợp

API JDK 1.8 chứa nhiều giao diện chức năng tích hợp. Một số trong số chúng đã được biết đến từ các phiên bản Java trước đó. Ví dụ: Comparator hoặc Runnable . Các giao diện này được mở rộng để bao gồm hỗ trợ lambda bằng cách sử dụng chú thích @FunctionalInterface . Nhưng API Java 8 cũng có đầy đủ các giao diện chức năng mới giúp cuộc sống của bạn dễ dàng hơn. Một số giao diện này được biết đến nhiều từ thư viện Guava của Google . Ngay cả khi bạn đã quen thuộc với thư viện này, bạn vẫn nên xem xét kỹ hơn cách các giao diện này được mở rộng bằng một số phương pháp mở rộng hữu ích.
Vị ngữ
Vị từ là các hàm Boolean có một đối số. Giao diện chứa các phương thức mặc định khác nhau để tạo các biểu thức logic phức tạp (và, hoặc, phủ định) bằng cách sử dụng các vị từ Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Chức năng
Các hàm lấy một đối số và tạo ra một kết quả. Các phương thức mặc định có thể được sử dụng để kết hợp nhiều hàm lại với nhau thành một chuỗi (compose vàThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Các nhà cung cấp
Các nhà cung cấp trả về một kết quả (ví dụ) thuộc loại này hay loại khác. Không giống như hàm, trình cung cấp không nhận đối số. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Người tiêu dùng
Người tiêu dùng đại diện cho các phương thức giao diện bằng một đối số duy nhất. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Bộ so sánh
Bộ so sánh được chúng ta biết đến từ các phiên bản trước của Java. Java 8 cho phép bạn thêm nhiều phương thức mặc định khác nhau vào giao diện. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Tùy chọn
Giao diện Tùy chọn không hoạt động nhưng nó là một tiện ích tuyệt vời để ngăn chặn NullPointerException . Đây là điểm quan trọng cho phần tiếp theo, vì vậy chúng ta hãy xem nhanh cách hoạt động của giao diện này. Giao diện Tùy chọn là một vùng chứa đơn giản cho các giá trị có thể rỗng hoặc không rỗng. Hãy tưởng tượng rằng một phương thức có thể trả về một giá trị hoặc không trả về gì. Trong Java 8, thay vì trả về null , bạn trả về một phiên bản Tùy chọn . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Suối

java.util.Stream là một chuỗi các phần tử trên đó một hoặc nhiều thao tác được thực hiện. Mỗi hoạt động của Luồng đều là trung gian hoặc là thiết bị đầu cuối. Các thao tác đầu cuối trả về kết quả của một loại cụ thể, trong khi các thao tác trung gian trả về chính đối tượng luồng, cho phép tạo một chuỗi lệnh gọi phương thức. Luồng là một giao diện, giống như java.util.Collection dành cho danh sách và bộ (bản đồ không được hỗ trợ). Mỗi thao tác Luồng có thể được thực hiện tuần tự hoặc song song. Chúng ta hãy xem luồng hoạt động như thế nào. Đầu tiên, chúng ta sẽ tạo mã mẫu dưới dạng danh sách các chuỗi: Bộ sưu tập trong Java 8 được cải tiến để bạn có thể tạo luồng khá đơn giản bằng cách gọi Collection.stream() hoặc Collection.parallelStream() . Phần tiếp theo sẽ giải thích các thao tác truyền phát đơn giản, quan trọng nhất. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Lọc
Bộ lọc chấp nhận các vị từ để lọc tất cả các phần tử của luồng. Hoạt động này mang tính chất trung gian, cho phép chúng ta gọi các hoạt động luồng khác (ví dụ: forEach) trên kết quả thu được (đã được lọc). ForEach chấp nhận một thao tác sẽ được thực hiện trên từng thành phần của luồng đã được lọc. ForEach là một hoạt động đầu cuối. Hơn nữa, việc gọi các hoạt động khác là không thể. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Đã sắp xếp
Đã sắp xếp là một thao tác trung gian trả về một biểu diễn luồng đã được sắp xếp. Các phần tử được sắp xếp theo đúng thứ tự trừ khi bạn chỉ định Comparator của mình . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Hãy nhớ rằng được sắp xếp sẽ tạo ra một biểu diễn luồng được sắp xếp mà không ảnh hưởng đến chính bộ sưu tập. Thứ tự của các phần tử stringCollection vẫn được giữ nguyên: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Bản đồ
Thao tác ánh xạ trung gian chuyển đổi từng phần tử thành một đối tượng khác bằng cách sử dụng hàm kết quả. Ví dụ sau chuyển đổi từng chuỗi thành chuỗi chữ hoa. Nhưng bạn cũng có thể sử dụng bản đồ để chuyển đổi từng đối tượng sang một loại khác. Loại đối tượng luồng kết quả phụ thuộc vào loại hàm bạn chuyển tới bản đồ. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Cuộc thi đấu
Có thể sử dụng nhiều thao tác so khớp khác nhau để kiểm tra tính đúng đắn của một vị từ cụ thể trong quan hệ luồng. Tất cả các hoạt động khớp đều là thiết bị đầu cuối và trả về kết quả Boolean. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Đếm
Count là một thao tác cuối cùng trả về số phần tử của luồng dưới dạng long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Giảm bớt
Đây là thao tác đầu cuối giúp rút ngắn các phần tử của luồng bằng cách sử dụng hàm được truyền. Kết quả sẽ là Tùy chọn chứa giá trị rút gọn. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Luồng song song

Như đã đề cập ở trên, các luồng có thể là tuần tự hoặc song song. Các hoạt động luồng tuần tự được thực hiện trên một luồng nối tiếp, trong khi các hoạt động luồng song song được thực hiện trên nhiều luồng song song. Ví dụ sau đây minh họa cách dễ dàng tăng hiệu suất bằng cách sử dụng luồng song song. Trước tiên, hãy tạo một danh sách lớn các phần tử độc đáo: Bây giờ chúng ta sẽ xác định thời gian dành cho việc sắp xếp luồng của bộ sưu tập này. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Luồng nối tiếp
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Luồng song song
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Như bạn có thể thấy, cả hai đoạn gần như giống hệt nhau, nhưng việc sắp xếp song song nhanh hơn 50%. Tất cả những gì bạn cần là thay đổi stream() thành ParallelStream() .

Bản đồ

Như đã đề cập, bản đồ không hỗ trợ luồng. Thay vào đó, bản đồ bắt đầu hỗ trợ các phương pháp mới và hữu ích để giải quyết các vấn đề phổ biến. Đoạn mã trên phải trực quan: putIfAbsent cảnh báo chúng ta không nên viết thêm các bước kiểm tra rỗng. forEach chấp nhận một hàm để thực thi cho từng giá trị bản đồ. Ví dụ này cho thấy cách các thao tác được thực hiện trên các giá trị bản đồ bằng cách sử dụng các hàm: Tiếp theo, chúng ta sẽ tìm hiểu cách xóa một mục nhập cho một khóa nhất định chỉ khi nó ánh xạ tới một giá trị nhất định: Một phương pháp hay khác: Hợp nhất các mục bản đồ khá dễ dàng: Hợp nhất sẽ chèn khóa/giá trị vào bản đồ, nếu không có mục nhập nào cho khóa đã cho hoặc hàm hợp nhất sẽ được gọi, hàm này sẽ thay đổi giá trị của mục nhập hiện có. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION