Những mã xấu mà Java 8 có thể khử

Bài viết được sự cho phép của Nguyễn Bình Sơn

Tới hiện tại, Java 8 đã được sử dụng trên hầu hết các ứng dụng chạy trên JVM, nhưng điều đó không có nghĩa là những gì tối tân của phiên bản này đã được khai thác triệt để. Dưới đây là một số cách viết mã già cỗi nên được cập nhật.

  10 câu hỏi JavaScript để tăng cường kỹ năng của bạn
  Giới thiệu về Reactive Programing trong javascript

1. Inner class vô danh

Bất cứ khi nào bạn gặp một inner class vô danh, bạn nên cân nhắc sử dụng biểu thức lambda. Lấy ví dụ:

list.sort(new Comparator()
  public int compare (String o1, String o2) {
    return o1.length() - o2.length();
  }
});

…chuyển thành như sau ngắn gọn hơn nhiều:

[java]list.sort((o1,o2) -> o1.length() - o2.length());

Dù vậy, mã ở đây vẫn không quá dễ đọc, chúng ta sẽ tiếp tục xem xét về Comparator.

2. Comparators

Comparator trong Java 8 không chỉ tối tân ở sự tương thích với các biểu thức lambda. Nó có những phương thức kết hợp với các tham chiếu phương thức, giúp cho mã trong sáng hơn rất nhiều:

[java]list.sort((o1,o2) -> o1.length() - o2.length());

Một ví dụ khác, bạn thậm chí có thể lấy các kết quả theo thứ tự ngược lại, tất cả chỉ bằng cách sử dụng thêm một phương thức hỗ trợ khác:

[java]list.sort((o1,o2) -> o1.length() - o2.length());

3. Các class không trạng thái

Thường thì bạn sẽ hay gặp các lớp không có gì khác ngoài các phương thức tĩnh (chúng thường được đặt tên được tận cùng là “Util” hay “Helper”), chúng có nhiệm vụ gom nhóm các phương thức lại trong một không gian tên duy nhất. Không ai cần phải tạo đối tượng của các class này, và kể cả khi có làm việc đó thì các đối tượng này cũng không lưu giữ bất cứ dữ liệu riêng nào, do đó các class này được gọi là class không trạng thái (stateless).

Với Java 8, các interface có khả năng chứa các phương thức tĩnh, và trở thành lựa chọn tốt hơn so với class, bởi chúng ta không phải lo có ai đó tạo đối tượng của interface. Ví dụ kinh điển lần nữa lại là Comparator – với các phương thức tĩnh hữu dụng và mạnh mẽ của nó.

Tương tự như thế, nếu bạn gặp một class trừu tượng không trạng thái, chỉ có duy nhất những phương thức trừu tượng được thiết kế để ghi đè, lớp đó có thể được chuyển đổi thành những FunctionalInterface và sau đó được implement bằng biểu thức lambda, như Runnable chẳng hạn:

Trước Java 8:

new Thread(new Runnable() {
    @Override
public void run() { 
      System.out.println("New thread created"); 
    } 
  }).start();

Java 8:

new Thread(() -> {
    System.out.println("New thread created");
}).start();

Một ví dụ khác để thử xem xét cách triển khai, sử dụng annotation @FunctionalInterface cho interface:

@FunctionalInterface
interface Square {
  int calculate(int x);
}

… và implement bằng biểu thức lambda:

int a = 5;
Square s = (x) -> x * x;
int ans = s.calculate(a);
System.out.println(ans);

4. Các chỉ dẫn lặp và rẽ nhánh lồng nhau

Chúng ta có bộ API Streams được thiết kế để truy cập các collection theo cách vô cùng uyển chuyển. Khi gặp mã nguồn như dưới đây:

List<Field> validFields = new ArrayList<Field>();
for (Field field : fields) {
  if (meetsCriteria(field)) {
    validFields.add(field);
  }
}
return validFields;

…bạn phải nghĩ tới việc thay thế bằng mã sử dụng Streams. Ở đây ta dùng filter và collect:

return fields.stream()
  .filter(this::meetsCriteria)
  .collect(Collectors.toList());

Đôi khi, các vòng lặp với một lệnh rẽ nhánh ở trong có thể được tái cấu trúc thành anyMatch hay findFirst:

for (String current : strings) {
  if (current.equals(wanted)) {
    return true;
  }
}
return false;

…có thể được thay thế bởi:

return strings.stream()
  .anyMatch(current -> current.equals(wanted));


Và:

for (String current : strings) {
  if (current.equals(wanted)) {
    return current;
  }
}
return null;

…có thể sửa thành:

return strings.stream()
    .filter(current -> current.equals(wanted))
    .findFirst()
    .orElse(null);

Lưu ý rằng orElse(null) thực tế là một mã xấu, chúng ta sẽ nói đến sau.

5. Đa thao tác trên collection

Mặc dù đã cố gắng tối ưu hóa, nhưng chúng ta vẫn thường xuyên gặp trường hợp một chuỗi các thao tác được thực hiện trên một hay nhiều collection để có được kết quả mong muốn. Xem xét ví dụ dưới đây:

// collect messages for logging
List<LogLine> lines = new ArrayList<>();
for (Message message : messages) {
  lines.add(new LogLine(message));
}
// sort
Collections.sort(lines);
// log them
for (LogLine line : lines) {
  line.log(LOG);
}

Chia để trị đã giúp cho mã được rõ ràng, nhưng chưa nói tới việc phải sử dụng tới comment để làm rõ ý, thì lời gọi Collections.sort vẫn gợi ý rằng chúng ta có thể sử dụng Streams tại đây. Trong thực tế, toàn bộ đoạn mã trên có thể được gộp vào một stream duy nhất:

messages.stream()
    .map(LogLine::new)
    .sorted()
    .forEach(logLine -> logLine.log(LOG));

Tái cấu trúc này không chỉ giúp mã dễ đọc hơn hay ít lại một biến trung gian, mà thực tế còn có hiệu năng cao hơn.

6. Duyệt để xóa bỏ các phần tử

Mã trước-Java-8 có thể có những đoạn mã như sau:

Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
  String current = iterator.next();
  if (current.endsWith("foo bar")) {
    iterator.remove();
  }
}

Giờ đây đoạn mã trên có thể được rút ngắn còn một dòng:

[java]list.sort((o1,o2) -> o1.length() - o2.length());[java]list.sort((o1,o2) -> o1.length() - o2.length());

Ngắn hơn, dễ đọc hơn, và cũng nhanh hơn!

7. Kiểm Null

NullPointerExceptions là điểm chí tử của các nhà phát triển Java, và bạn sẽ không lấy làm lạ khi các phép kiểm null nằm rải rác trong mã. Java 8 cho chúng ta API Optional giúp chúng ta mô tả kết quả trả về tốt hơn nhiều cũng như loại bỏ các kiểm null không cần thiết. Hãy quay lại với orElse mà chúng ta có ở tái cấu trúc số 4:

public static String findString (String wanted){
  List<String> strings = new ArrayList<>();
  return strings.stream()
      .filter(current -> current.equals(wanted))
      .findFirst()
      .orElse(null);
}

Bất kỳ lời gọi findString nào cũng sẽ phải kiểm null cho giá trị nhận được, và nếu an toàn thì làm một thao tác nào đó:

String foundString = findString(wantedString);
if (foundString == null) {
  return "Did not find value" + wantedString;
} else {
  return foundString;
}

Mã này xấu, dễ lặp và tẻ nhạt. Nếu cập nhật phương thức findString thành sử dụng Optional:

public static Optional<String> findString(String wanted) {
  List<String> strings = new ArrayList<>();
  return strings.stream()
      .filter(current -> current.equals(wanted))
      .findFirst();
}

…thì chúng ta có thể thoát khỏi trường hợp không có giá trị một cách thanh nhã hơn nhiều:

[java]list.sort((o1,o2) -> o1.length() - o2.length());

Bài viết gốc được đăng tải tại Tạp Chí Lập Trình

Có thể bạn quan tâm:

Xem thêm các việc làm Java hấp dẫn tại TopDev