0

Bài 6: Tránh tạo object không cần thiết

Bad practice thường gặp (tạo object dư thừa)

boolean isRomanNumeral(String s) {
	return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

Vì sao cách này không tốt

  • Mỗi lần gọi matches là mỗi lần compile regex mới.
  • Ở hot path, chi phí tạo object lặp lại gây tốn CPU và tăng GC pressure.

Cách tác giả giải quyết vấn đề

Nguyên tắc cốt lõi: ưu tiên tái sử dụng object hiện có thay vì tạo object mới khi không cần. Việc tạo quá nhiều object ngắn hạn có thể làm tăng GC pressure, giảm hiệu năng, và làm code rối hơn.

Diễn giải tương tự theo tinh thần tác giả

Thông điệp cốt lõi ở đây không phải là "đừng bao giờ tạo object", mà là đừng tạo object mới khi bản chất bạn có thể tái sử dụng object cũ hoặc tránh allocation đó hoàn toàn. Vấn đề nằm ở allocation vô nghĩa, không nằm ở bản thân object.

Trong Java hiện đại, việc cấp phát object thường khá rẻ, nhưng nếu bạn lặp lại thao tác đó ở những đoạn code chạy thường xuyên thì tổng chi phí sẽ cộng dồn thành vấn đề thật: nhiều rác hơn, nhiều lần GC hơn, và throughput kém hơn.

Tác giả muốn người đọc hình thành phản xạ nhận diện ba nhóm lãng phí phổ biến:

  • tạo lại immutable object mà có thể dùng chung.
  • tạo wrapper do autoboxing khi primitive là đủ.
  • tạo object đắt đỏ nhiều lần trong hot path, như regex pattern hoặc formatter.

Hướng xử lý đúng là:

  • tái sử dụng object đã có nếu nó immutable hoặc an toàn để dùng chung.
  • chuyển object dùng lặp lại thành static final khi phù hợp.
  • ưu tiên primitive thay cho boxed type ở các đoạn tính toán lớn.
  • chỉ tối ưu sâu sau khi đã đo bằng profiler, tránh tối ưu mù quáng.

Các trường hợp nên tránh tạo object dư thừa

1. Tái sử dụng immutable object

Với immutable object, nếu giá trị không đổi, bạn có thể dùng lại an toàn.

Ví dụ không tốt:

String s = new String("hello");

Ví dụ tốt:

String s = "hello";

Literal string được intern và tái sử dụng tốt hơn trong đa số trường hợp.

2. Dùng static factory thay vì constructor khi phù hợp

Boolean.valueOf("true") thường tốt hơn new Boolean("true") (constructor đã lỗi thời) vì có thể trả về object dùng chung.

3. Tránh tạo object lặp lại trong vòng lặp

Đặc biệt với object đắt đỏ (regex Pattern, formatter, parser...).

Ví dụ tối ưu regex:

import java.util.regex.Pattern;

public class RomanNumerals {
	private static final Pattern ROMAN =
			Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"
					+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

	public static boolean isRomanNumeral(String s) {
		return ROMAN.matcher(s).matches();
	}
}

Nếu compile regex mỗi lần gọi method, bạn sẽ trả chi phí lặp đi lặp lại không cần thiết.

4. Ưu tiên primitive khi phù hợp, tránh autoboxing không cần thiết

Autoboxing có thể tạo nhiều object wrapper (Long, Integer, ...) ngoài ý muốn.

Ví dụ kém hiệu năng:

Long sum = 0L;
for (long i = 0; i <= 1_000_000L; i++) {
	sum += i;
}

Ví dụ tốt hơn:

long sum = 0L;
for (long i = 0; i <= 1_000_000L; i++) {
	sum += i;
}

Lưu ý quan trọng

  • Không phải mọi object allocation đều xấu. JVM hiện đại tối ưu allocation rất tốt.
  • Mục tiêu là tránh allocation vô nghĩa ở điểm nóng (hot path), không phải tối ưu cực đoan mọi nơi.
  • Đừng giữ cache quá tay cho object nhẹ vì có thể tăng memory footprint và độ phức tạp.

Cân bằng giữa hiệu năng và độ rõ ràng

  • Viết code rõ ràng trước.
  • Dùng profiler để xác định bottleneck thật.
  • Chỉ tối ưu nơi có dữ liệu đo đạc chứng minh cần thiết.

Checklist áp dụng nhanh

  • Có object nào đang được tạo lặp lại nhưng giá trị không đổi không?
  • Có thể chuyển sang static final để tái sử dụng không?
  • Có autoboxing trong loop lớn không?
  • Có constructor nào nên thay bằng static factory không?
  • Đã benchmark/profiler trước khi tối ưu chưa?

Tóm lại: tránh tạo object không cần thiết giúp code hiệu quả hơn, nhưng tối ưu cần dựa trên đo đạc và ngữ cảnh thực tế.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí