Mặc dù Java 8 đã được phát hành từ lâu (tháng 3 năm 2014), rất nhiều dự án vẫn đang chạy nó vì đây là một bản phát hành lớn với nhiều tính năng mới.
Chúng ta hãy cùng điểm qua các tính năng của Java 8 với một vài ví dụ cụ thể để giúp bạn hiểu rõ chúng hơn.
Các tính năng nổi bật trong Java 8
1. Phương thức forEach() trong interface (giao diện) Iterable
Mỗi khi cần duyệt qua một Collection, chúng ta thường phải tạo một Iterator với mục đích chính là để duyệt qua các phần tử. Sau đó, ta triển khai logic nghiệp vụ (business logic) trong một vòng lặp cho từng phần tử của Collection. Nếu iterator không được sử dụng đúng cách, chúng ta có thể gặp phải lỗi ConcurrentModificationException.
Java 8 đã thêm phương thức forEach
vào interface java.lang.Iterable
để khi viết code chúng ta có thể tập trung vào business logic. Nó nhận một đối tượng java.util.function.Consumer
làm đối số. Điều này giúp tách riêng business logic ra một nơi khác để có thể tái sử dụng.
Chúng ta hãy xem cách sử dụng forEach
qua một ví dụ đơn giản.
package com.journaldev.java8.foreach;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;
public class Java8ForEachExample {
public static void main(String[] args) {
//tạo Collection mẫu
List<Integer> myList = new ArrayList<Integer>();
for(int i=0; i<10; i++) myList.add(i);
//duyệt với Iterator
Iterator<Integer> it = myList.iterator();
while(it.hasNext()){
Integer i = it.next();
System.out.println("Iterator Value::"+i);
}
//duyệt phương thức forEach của Iterable vơi lớp anonymous
myList.forEach(new Consumer<Integer>() {
public void accept(Integer t) {
System.out.println("forEach anonymous class Value::"+t);
}
});
//duyệt triển khai Consumer interface
MyConsumer action = new MyConsumer();
myList.forEach(action);
}
}
//triển khai Consumer có thể được dùng lại
class MyConsumer implements Consumer<Integer>{
public void accept(Integer t) {
System.out.println("Consumer impl Value::"+t);
}
}
Số dòng mã có thể tăng lên, nhưng phương thức forEach
giúp tách biệt logic duyệt (iteration logic) và business logic. Nhờ đó nó nâng cao tính phân tách trách nhiệm (separation of concern) và làm cho code trở nên sạch hơn.
2. Phương thức default và static trong Interface
Nếu đọc kỹ chi tiết về phương thức forEach
, bạn sẽ nhận thấy nó được định nghĩa trong interface Iterable
. Tuy nhiên, chúng ta đều biết rằng các interface truyền thống không thể có phần thân phương thức (method body).
Kể từ Java 8, các interface đã được cải tiến để có thể chứa các phương thức có code triển khai. Ta có thể sử dụng từ khóa default
và static
để tạo ra các interface với các phương thức này.
Ví dụ phần triển khai của phương thức forEach
trong interface Iterable
:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Chúng ta biết rằng Java không hỗ trợ đa kế thừa (multiple inheritance) đối với class, bởi vì điều này sẽ dẫn đến Diamond Problem (vấn đề kim cương**)**. Vậy thì vấn đề này sẽ được xử lý như thế nào với các interface, khi mà giờ đây chúng đã trở nên tương tự như các abstract class?
Giải pháp được đưa ra là trình biên dịch (compiler) sẽ báo lỗi trong trường hợp này và chúng ta sẽ phải tự cung cấp phần code logic cụ thể trong class triển khai các interface đó.
package com.journaldev.java8.defaultmethod;
@FunctionalInterface
public interface Interface1 {
void method1(String str);
default void log(String str){
System.out.println("I1 logging::"+str);
}
static void print(String str){
System.out.println("Printing "+str);
}
//sẽ có lỗi biên dịch khi ta cố ghi đè phương thức Object
//"A default method cannot override a method from java.lang.Object"
// default String toString(){
// return "i1";
// }
}
package com.journaldev.java8.defaultmethod;
@FunctionalInterface
public interface Interface2 {
void method2();
default void log(String str){
System.out.println("I2 logging::"+str);
}
}
Lưu ý rằng cả hai interface này đều có một phương thức chung là log()
với logic triển khai riêng.
package com.journaldev.java8.defaultmethod;
public class MyClass implements Interface1, Interface2 {
@Override
public void method2() {
}
@Override
public void method1(String str) {
}
//MyClass won't compile without having it's own log() implementation
@Override
public void log(String str){
System.out.println("MyClass logging::"+str);
Interface1.print("abc");
}
}
Như bạn có thể thấy, Interface1 có phần triển khai phương thức static
được sử dụng trong phần triển khai của phương thức MyClass.log()
. Java 8 sử dụng rất nhiều phương thức default
và static
trong Collection API. Các phương thức default
được thêm vào để đảm bảo code của chúng ta vẫn duy trì tính tương thích ngược.
Nếu một class nào đó trong hệ thống phân cấp có một phương thức với cùng signature (chữ ký phương thức), thì các phương thức default
trong interface sẽ bị bỏ qua (trở nên không liên quan).
Class Object
là class cha cơ sở của tất cả các class. Do đó nếu chúng ta định nghĩa các phương thức default
như equals()
, hashCode()
trong interface, chúng cũng sẽ bị bỏ qua. Vì lý do này, để đảm bảo tính rõ ràng, các interface không được phép định nghĩa các phương thức default
trùng với các phương thức của class Object
.
3. Giao diện hàm (functional interface) và biểu thức Lambda
Nếu bạn để ý đoạn mã interface ở trên, bạn sẽ thấy annotation @FunctionalInterface
. Functional interface là một khái niệm mới được giới thiệu trong Java 8. Một interface chỉ có duy nhất một phương thức trừu tượng (abstract method) được gọi là functional interface. Chúng ta không bắt buộc phải sử dụng annotation @FunctionalInterface
để đánh dấu một interface là functional interface.
Annotation @FunctionalInterface
là một công cụ giúp tránh việc vô tình thêm các phương thức trừu tượng khác vào functional interface. Bạn có thể hình dung nó giống như annotation @Override
, và nên được sử dụng. java.lang.Runnable
với chỉ một phương thức trừu tượng duy nhất là run()
, là một ví dụ điển hình của functional interface.
Một trong những lợi ích chính của functional interface là khả năng sử dụng các biểu thức lambda (lambda expression) để khởi tạo chúng.
Mặc dù chúng ta có thể khởi tạo một interface bằng anonymous class (lớp ẩn danh), cách này thường làm cho code trở nên khá cồng kềnh.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
Do functional interface chỉ có một phương thức duy nhất, các biểu thức lambda có thể dễ dàng cung cấp phần triển khai cho phương thức đó. Chúng ta chỉ cần cung cấp các tham số (argument) của phương thức và business logic.
Ví dụ, ta có thể viết lại phần triển khai ở trên bằng biểu thức lambda như sau:
Runnable r1 = () -> {
System.out.println("My Runnable");
};
Nếu phần triển khai phương thức chỉ có một câu lệnh (statement), chúng ta thậm chí không cần dùng dấu ngoặc nhọn {}
. Ví dụ, anonymous class Interface1
ở trên có thể được khởi tạo bằng lambda như sau:
Interface1 i1 = (s) -> System.out.println(s);
i1.method1("abc");
Như vậy, biểu thức lambda là một cách tiện lợi để tạo ra các instance của functional interface (thông qua anonymous class). Việc sử dụng biểu thức lambda không mang lại lợi ích nào lúc chạy (runtime). Bạn có thể sử dụng chúng một cách thận trọng nếu không ngại việc viết thêm một vài dòng code.
Một package mới là java.util.function
đã được thêm vào với nhiều functional interface được thiết kế để làm type mục tiêu (target type) cho các biểu thức lambda và tham chiếu phương thức.
4. Java Stream API cho các thao tác dữ liệu hàng loạt trên Collection
Một API mới là java.util.stream
(Stream API) đã được thêm vào Java 8 để thực hiện các thao tác kiểu filter/map/reduce trên collection. Nó cho phép chả hai kiểu chạy tuần tự (sequential) và song song (parallel).
Đây là một trong những tính năng hay nhất cho những ai làm việc rất nhiều với Collection và thường xuyên phải xử lý Big Data, nơi chúng ta cần lọc dữ liệu dựa trên các điều kiện nhất định.
Interface Collection
đã được mở rộng với các phương thức default là stream()
và parallelStream()
để lấy về đối tượng Stream phục vụ cho việc thực thi tuần tự hoặc song song.
Chúng ta hãy xem cách sử dụng chúng qua một ví dụ đơn giản:
package com.journaldev.java8.stream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamExample {
public static void main(String[] args) {
List<Integer> myList = new ArrayList<>();
for(int i=0; i<100; i++) myList.add(i);
//stream tuần tự
Stream<Integer> sequentialStream = myList.stream();
//stream song song
Stream<Integer> parallelStream = myList.parallelStream();
//dùng lambda với Stream API cho ví dụ filter
Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
//dùng lambda trong forEach
highNums.forEach(p -> System.out.println("High Nums parallel="+p));
Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));
}
}
Nếu bạn chạy đoạn code ví dụ ở trên, bạn sẽ nhận được output như sau:
High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99
Lưu ý rằng kết quả của việc xử lý song song không được sắp xếp theo thứ tự. Do đó, tính năng này sẽ rất hữu ích khi bạn làm việc với các collection có kích thước lớn.
5. Java Time API
Việc xử lý Date (Ngày), Time (Giờ), và Time Zone (Múi giờ) trong Java trước đây luôn gặp nhiều trở ngại. Java thiếu API và cách tiếp cận chuẩn cho các vấn đề này. Một trong những bổ sung đáng giá của Java 8 là package java.time
để giúp chuẩn hóa và đơn giản hóa quy trình làm việc với thời gian trong Java.
Chỉ cần nhìn qua cấu trúc các package của Java Time API, bạn sẽ thấy rằng nó sẽ rất dễ sử dụng. API này bao gồm các package con như java.time.format
(cung cấp các class để hiển thị và chuyển đổi ngày giờ) và java.time.zone
(hỗ trợ cho các múi giờ và quy tắc của chúng).
Time API mới ưu tiên sử dụng enum
thay vì các hằng số kiểu integer để biểu diễn tháng và các ngày trong tuần. Một trong những class hữu ích là DateTimeFormatter
. Nó có thể chuyển đổi các đối tượng DateTime sang dạng String.
6. Các cải tiến trong Collection API
Chúng ta đã tìm hiểu về phương thức forEach()
và Stream API dành cho collection. Một số phương thức mới khác được thêm vào Collection API bao gồm:
- Phương thức default
Iterator.forEachRemaining(Consumer action)
: thực hiện một action (hành động) đã cho đối với các phần tử còn lại cho đến khi tất cả chúng đã được xử lý hết hoặc action đó gây ra lỗi. - Phương thức default
Collection.removeIf(Predicate filter)
: xóa tất cả các phần tử của collection hiện tại thỏa mãn một điều kiện cho trước. - Phương thức
Collection.spliterator()
: trả về một instanceSpliterator
có thể được dùng để duyệt qua các phần tử một cách tuần tự hoặc song song. - Các phương thức mới trong
Map
nhưcompute()
,putIfAbsent()
,forEach()
vàreplace()
. - Cải thiện hiệu năng của class
HashMap
trong trường hợp xảy ra xung đột khóa.
7. Các cải tiến trong Concurrency API
Một số cải tiến quan trọng cho Concurrency API bao gồm:
- Các phương thức mới trong
ConcurrentHashMap
nhưcompute()
,forEach()
,forEachEntry()
,forEachKey()
,forEachValue()
,merge()
,reduce()
vàsearch()
. CompletableFuture
: có thể được hoàn thành một cách tường minh (thiết lập giá trị và trạng thái của nó).- Phương thức
Executors.newWorkStealingPool()
: tạo ra một nhóm luồng (thread pool) kiểu work-stealing (cơ chế đánh cắp công việc), sử dụng tất cả các processor hiện có làm mức độ song song (parallelism level) mục tiêu.
8. Các cải tiến trong Java IO
Một số cải tiến đáng chú ý về Java IO (Input/Output) bao gồm:
Files.list(Path dir)
: trả về mộtStream
được khởi tạo lười với các phần tử là các phần tử trong thư mụcdir
.Files.lines(Path path)
: đọc tất cả các dòng từ một file và trả về dưới dạng mộtStream<String>
.Files.find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
: trả về mộtStream<Path>
được khởi tạo lười bằng cách tìm kiếm file trong một cây thư mục bắt đầu từstart
.BufferedReader.lines()
: trả về mộtStream<String>
, với các phần tử là các dòng được đọc từBufferedReader
đó.
Các cải tiến API Core khác trong Java 8
Một số cải tiến API khác trong Java 8 Core bao gồm:
- Phương thức static
ThreadLocal.withInitial(Supplier<S> supplier)
giúp tạo instanceThreadLocal
một cách dễ dàng. - Interface
Comparator
được mở rộng với nhiều phương thức default và static mới, hỗ trợ việc sắp xếp theo thứ tự tự nhiên (natural ordering), thứ tự đảo ngược (reverse order), v.v. - Các phương thức
min()
,max()
vàsum()
được thêm vào các lớp wrapperInteger
,Long
, vàDouble
. - Các phương thức
logicalAnd()
,logicalOr()
, vàlogicalXor()
được thêm vào classBoolean
. - Phương thức
ZipFile.stream()
: trả về mộtStream
có thứ tự duyệt qua các entry trong file ZIP. Các entry này xuất hiện trongStream
theo đúng thứ tự chúng có trong thư mục trung tâm của file ZIP. - Một vài phương thức tiện ích mới trong class
Math
. - Lệnh
jjs
được thêm vào để gọi Nashorn Engine. - Lệnh
jdeps
được thêm vào để phân tích các file class. - JDBC-ODBC Bridge đã bị loại bỏ.
- Vùng nhớ PermGen đã bị loại bỏ.
Tổng kết
Trên đây là cái nhìn tổng quan về các tính năng quan trọng của Java 8 kèm theo các ví dụ minh họa. Nếu bạn nhận thấy vẫn còn những khía cạnh quan trọng chưa được đề cập, đừng ngần ngại chia sẻ trong phần bình luận bên dưới. Chúng tôi rất mong nhận được góp ý từ bạn!