Best Practices từ “Effective Java (3rd Edition)”
Trích từ “Effective Java (3rd Edition) ” của Joshua Bloch
1. Trả về collection hoặc mảng rỗng, không trả về null
Nếu phương thức trả về một Set, đừng return null; khi kết quả rỗng – hãy dùng return Set.of(); thay thế.
2. Cân nhắc dùng static factory method thay cho constructor
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Không phải lúc nào cũng tốt hơn, nhưng trong một số trường hợp chúng mang lại nhiều ưu điểm:
- Một ưu điểm của static factory method là, không giống constructor, chúng có tên.
- Chúng không bắt buộc phải tạo một đối tượng mới mỗi lần được gọi.
- Chúng có thể trả về đối tượng thuộc bất kỳ kiểu con nào của kiểu trả về.
- Class của đối tượng trả về có thể thay đổi theo từng lần gọi, tùy vào tham số đầu vào.
- Class của đối tượng trả về không cần phải tồn tại vào lúc viết class chứa phương thức đó.
Nhược điểm:
- Hạn chế chính khi chỉ cung cấp static factory method là các class không có constructor public hoặc protected thì không thể tạo class con.
- Nhược điểm thứ hai của static factory method là lập trình viên khó tìm ra chúng.
3. Cân nhắc dùng builder khi gặp nhiều tham số constructor
Static factory và constructor có chung một hạn chế: chúng không mở rộng tốt khi có nhiều tham số tùy chọn. Vấn đề này từng giáng đòn rất nặng vào một số dự án tôi tham gia.
4. Đảm bảo tính singleton bằng constructor private hoặc kiểu enum
Bất cứ thứ gì không cần nhiều instance thì chỉ nên được khởi tạo một lần – dù là bằng @ApplicationScoped hay một singleton pattern khác.
5. Ngăn việc khởi tạo bằng constructor private
Đôi khi bạn sẽ muốn viết một class chỉ để gom các phương thức static và field static. Những class như vậy mang tiếng xấu vì một số người lạm dụng chúng để né việc tư duy theo hướng đối tượng, nhưng chúng thực sự có những công dụng chính đáng.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Tránh tạo các đối tượng không cần thiết
String s = new String("bikini"); // DON'T DO THIS!
Hoặc:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
Lời giải: Chương trình này cho ra kết quả đúng, nhưng chậm hơn rất nhiều so với mức cần thiết, chỉ vì một lỗi đánh máy một ký tự. Biến sum được khai báo là Long thay vì long, nghĩa là chương trình tạo ra khoảng 2^31 instance Long không cần thiết (đại khái mỗi lần long i được cộng vào Long sum là một instance).
7. Tránh finalizer và cleaner
Finalizer khó đoán, thường nguy hiểm và nhìn chung là không cần thiết. Việc dùng chúng có thể gây ra hành vi thất thường, hiệu năng kém và các vấn đề về tính di động.
8. Ưu tiên try-with-resources hơn try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Cân nhắc implement Comparable
Khi implement Comparable, bạn cho phép class của mình phối hợp với vô số thuật toán generic và các bản triển khai collection dựa vào interface này.
10. Giảm thiểu khả năng truy cập của class và thành viên
Yếu tố quan trọng bậc nhất phân biệt một thành phần được thiết kế tốt với một thành phần thiết kế kém là mức độ mà thành phần đó che giấu dữ liệu nội bộ và các chi tiết triển khai khác khỏi các thành phần khác. Một thành phần được thiết kế tốt che giấu mọi chi tiết triển khai, tách bạch API khỏi phần triển khai. Khi đó các thành phần chỉ giao tiếp với nhau qua API và không hề biết đến cách vận hành bên trong của nhau. Khái niệm này, được gọi là che giấu thông tin (information hiding) hay đóng gói (encapsulation), là một nguyên lý nền tảng của thiết kế phần mềm.
11. Trong class public, hãy dùng phương thức truy cập, không dùng field public
Đôi khi bạn có thể bị cám dỗ viết những class suy biến, không có mục đích gì khác ngoài việc gom các field của instance lại:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Vì các field dữ liệu của những class như vậy được truy cập trực tiếp, chúng không mang lại lợi ích của tính đóng gói. Bạn không thể thay đổi cách biểu diễn mà không thay đổi API, không thể đảm bảo các bất biến (invariant), và không thể thực hiện hành động phụ khi một field được truy cập. Những lập trình viên hướng đối tượng theo đường lối cứng rắn cho rằng các class như vậy là điều cấm kỵ và luôn phải được thay bằng class có field private và phương thức truy cập public (getter), và với class mutable thì có thêm mutator (setter):
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Chắc chắn những người cứng rắn đó nói đúng khi nói về class public: nếu một class có thể được truy cập từ bên ngoài package của nó, hãy cung cấp phương thức truy cập để giữ sự linh hoạt trong việc thay đổi cách biểu diễn nội bộ của class. Nếu một class public để lộ các field dữ liệu, mọi hy vọng thay đổi cách biểu diễn đều tan biến, vì code phía client có thể đã lan khắp nơi.
Tuy nhiên, nếu một class là package-private hoặc là nested class private, thì việc để lộ các field dữ liệu không có gì sai về bản chất—miễn là chúng mô tả thỏa đáng sự trừu tượng mà class cung cấp.
12. Giảm thiểu tính khả biến
Class immutable đơn giản là class mà các instance của nó không thể bị sửa đổi. Mọi thông tin chứa trong mỗi instance là cố định trong suốt vòng đời của đối tượng, nên không bao giờ có thay đổi nào được ghi nhận. Các thư viện của nền tảng Java chứa nhiều class immutable, bao gồm String, các class primitive dạng boxed, cùng BigInteger và BigDecimal. Có nhiều lý do chính đáng cho điều này: Class immutable dễ thiết kế, triển khai và sử dụng hơn class mutable. Chúng ít bị lỗi hơn và an toàn hơn. Để một class trở thành immutable, hãy tuân theo năm quy tắc sau:
- Không cung cấp phương thức sửa đổi trạng thái của đối tượng (gọi là mutator).
-
Đảm bảo class không thể được kế thừa. Điều này ngăn các class con cẩu thả hoặc có ác ý phá vỡ hành vi immutable của class bằng cách hành xử như thể trạng thái của đối tượng
đã thay đổi. Việc ngăn tạo class con thường được thực hiện, chẳng hạn, bằng cách khai báo class là
final. - Khai báo mọi field là final. Điều này thể hiện rõ ý định của bạn theo cách được hệ thống đảm bảo thực thi. Ngoài ra, nó còn cần thiết để đảm bảo hành vi đúng nếu một tham chiếu đến instance vừa được tạo được truyền từ thread này sang thread khác mà không có đồng bộ hóa, như đã nêu rõ trong memory model.
- Khai báo mọi field là private. Điều này ngăn client truy cập vào các đối tượng mutable mà field tham chiếu đến và sửa đổi trực tiếp các đối tượng đó. Mặc dù về mặt kỹ thuật class immutable được phép có field public final chứa giá trị primitive hoặc tham chiếu đến đối tượng immutable, điều này không được khuyến khích vì nó ngăn việc thay đổi cách biểu diễn nội bộ trong một bản phát hành sau.
-
Đảm bảo quyền truy cập độc quyền vào mọi thành phần mutable. Nếu class của bạn có field nào tham chiếu đến đối tượng mutable, hãy đảm bảo client của class không thể lấy được tham chiếu đến
các đối tượng đó. Đừng bao giờ khởi tạo một field như vậy bằng tham chiếu đối tượng do client cung cấp, hoặc trả về field đó từ một phương thức truy cập. Hãy tạo bản sao phòng vệ (defensive copy) trong constructor, phương thức truy cập và
phương thức
readObject.
13. Ưu tiên composition hơn kế thừa
Kế thừa là một cách mạnh mẽ để tái sử dụng code, nhưng không phải lúc nào cũng là công cụ tốt nhất cho công việc. Nếu dùng không phù hợp, nó dẫn đến phần mềm dễ vỡ. Dùng kế thừa trong cùng một package là an toàn, khi phần triển khai của class con và class cha nằm dưới sự kiểm soát của cùng những lập trình viên. Dùng kế thừa cũng an toàn khi mở rộng các class được thiết kế và tài liệu hóa riêng cho việc mở rộng. Tuy nhiên, kế thừa từ các class cụ thể thông thường vượt qua ranh giới package là nguy hiểm.
Khác với việc gọi phương thức, kế thừa vi phạm tính đóng gói. Bạn có thể nghĩ rằng việc mở rộng một class là an toàn nếu bạn chỉ thêm phương thức mới và không override các phương thức hiện có. Dù kiểu mở rộng này an toàn hơn nhiều, nó không phải không có rủi ro. Nếu class cha có thêm một phương thức mới trong bản phát hành sau, và bạn không may đã cho class con một phương thức có cùng signature nhưng kiểu trả về khác, class con của bạn sẽ không còn biên dịch được nữa. Nếu bạn đã cho class con một phương thức có cùng signature và kiểu trả về với phương thức mới của class cha, thì giờ bạn đang override nó, nên bạn sẽ gặp phải những vấn đề đã mô tả ở trên. Hơn nữa, khó mà chắc phương thức của bạn sẽ đáp ứng hợp đồng của phương thức mới trong class cha, vì hợp đồng đó còn chưa được viết ra khi bạn viết phương thức của class con.
May mắn là có một cách để tránh mọi vấn đề nêu trên. Thay vì mở rộng một class có sẵn, hãy cho class mới của bạn một field private tham chiếu đến một instance của class có sẵn đó. Thiết kế này được gọi là composition vì class có sẵn trở thành một thành phần của class mới. Mỗi phương thức instance trong class mới gọi phương thức tương ứng trên instance được chứa của class có sẵn và trả về kết quả. Cách này được gọi là forwarding, và các phương thức trong class mới được gọi là forwarding method. Class thu được sẽ vững như bàn thạch, không hề phụ thuộc vào chi tiết triển khai của class có sẵn. Ngay cả việc thêm phương thức mới vào class có sẵn cũng không ảnh hưởng gì đến class mới.
14. Ưu tiên interface hơn abstract class
Java có hai cơ chế để định nghĩa một kiểu cho phép nhiều cách triển khai: interface và abstract class. Từ khi default method cho interface được giới thiệu trong Java 8, cả hai cơ chế đều cho phép bạn cung cấp phần triển khai cho một số phương thức instance.
Một khác biệt lớn là để triển khai kiểu được định nghĩa bởi một abstract class, một class phải là class con của abstract class đó. Vì Java chỉ cho phép đơn kế thừa, hạn chế này của abstract class giới hạn nghiêm trọng việc dùng chúng làm định nghĩa kiểu. Bất kỳ class nào định nghĩa đủ các phương thức cần thiết và tuân thủ hợp đồng chung đều được phép implement một interface, bất kể class đó nằm ở đâu trong cây phân cấp class. Các class có sẵn có thể dễ dàng được điều chỉnh để implement một interface mới.
- Interface lý tưởng để định nghĩa mixin.
- Interface cho phép xây dựng các khung kiểu không phân cấp.
- Interface cho phép mở rộng chức năng một cách an toàn và mạnh mẽ thông qua idiom wrapper class. Nếu bạn dùng abstract class để định nghĩa kiểu, lập trình viên muốn thêm chức năng sẽ không còn lựa chọn nào khác ngoài kế thừa.
15. Generics: Đừng dùng raw type
Bạn sẽ không phát hiện ra lỗi cho đến lúc runtime, rất lâu sau khi nó xảy ra, và ở chỗ code có thể cách xa đoạn code chứa lỗi. Khi thấy ClassCastException, bạn phải lục tìm khắp codebase để tìm lời gọi phương thức đã bỏ đồng xu vào bộ sưu tập tem. Trình biên dịch không thể giúp bạn.
16. Generics: Loại bỏ các cảnh báo unchecked
Khi lập trình với generics, bạn sẽ thấy rất nhiều cảnh báo của trình biên dịch:
- cảnh báo unchecked cast
- cảnh báo unchecked method invocation
- cảnh báo unchecked parameterized vararg type
- cảnh báo unchecked conversion.
Bạn càng có nhiều kinh nghiệm với generics thì càng gặp ít cảnh báo, nhưng đừng mong code mới viết sẽ biên dịch sạch sẽ. Nhiều cảnh báo unchecked rất dễ loại bỏ. Ví dụ, giả sử bạn vô tình viết khai báo này:
Set<Lark> exaltation = new HashSet();
Hãy sửa thành:
Set<Lark> exaltation = new HashSet<>();
Một số cảnh báo sẽ khó loại bỏ hơn rất nhiều. Khi gặp những cảnh báo cần suy nghĩ, hãy kiên trì! Hãy loại bỏ mọi cảnh báo unchecked mà bạn có thể.
Nếu loại bỏ được tất cả cảnh báo, bạn có thể yên tâm rằng code của mình typesafe, và đó là điều rất tốt. Nó có nghĩa là bạn sẽ không gặp ClassCastException lúc runtime, và nó tăng thêm sự tự tin
rằng chương trình sẽ chạy đúng như bạn mong muốn. Nếu bạn không thể loại bỏ một cảnh báo, nhưng có thể chứng minh rằng đoạn code gây ra cảnh báo là typesafe, thì khi đó (và chỉ khi đó) hãy chặn
cảnh báo bằng annotation @SuppressWarnings("unchecked"). Nếu bạn chặn cảnh báo mà không chứng minh trước rằng code typesafe, bạn đang tự tạo cho mình một cảm giác
an toàn giả tạo.
Code có thể biên dịch mà không phát ra cảnh báo nào, nhưng vẫn có thể ném ClassCastException lúc runtime.
Mỗi lần dùng annotation @SuppressWarnings("unchecked"), hãy thêm một comment giải thích vì sao làm vậy là an toàn.
17. Generics: Khi nào thì hợp lý
Ối – một class đang rất cần generics:
Những thứ như thế này phải bị phát hiện muộn nhất là ở bước merge request!
18. Generics: Đừng làm thế này
Xin lỗi, không cần nói nhiều. Làm ơn ĐỪNG BAO GIỜ merge thứ này:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
Chỉ một khai báo class thôi thì không bao giờ nên mất tới 5 phút mới hiểu được.
19. Ưu tiên list hơn mảng
Mảng khác kiểu generic ở hai điểm quan trọng. Thứ nhất, mảng là covariant. Từ nghe đáng sợ này đơn giản có nghĩa là nếu Sub là kiểu con của Super, thì
kiểu mảng Sub[] là kiểu con của kiểu mảng Super[]. Ngược lại, generics là invariant: với hai kiểu khác nhau bất kỳ Type1 và Type2,
List<Type1> không phải là kiểu con cũng không phải là kiểu cha của List<Type2>.
20. Ưu tiên lambda hơn anonymous class
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
Thay vào đó, hãy làm thế này:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Thu hẹp phạm vi của biến cục bộ
Kỹ thuật hiệu quả nhất để thu hẹp phạm vi của một biến cục bộ là khai báo nó ngay tại nơi nó được dùng lần đầu...
22. Ưu tiên vòng lặp for-each hơn vòng lặp for truyền thống
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Hiểu và sử dụng thư viện
Giả sử bạn muốn sinh các số nguyên ngẫu nhiên từ không đến một giới hạn trên nào đó. Trước nhiệm vụ phổ biến này, nhiều lập trình viên sẽ viết một phương thức nhỏ trông đại loại như thế này:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Phương thức này trông có vẻ ổn, nhưng nó có ba lỗi. Thứ nhất, nếu n là một lũy thừa nhỏ của hai, dãy số ngẫu nhiên sẽ lặp lại sau một chu kỳ khá ngắn. Lỗi thứ hai là nếu n không phải lũy thừa của hai, một số con số sẽ được trả về thường xuyên hơn các số khác xét trung bình. Nếu n lớn, hiệu ứng này có thể khá rõ rệt.
Lỗi thứ ba của phương thức random là trong một số ít trường hợp, nó có thể thất bại thảm hại, trả về một số nằm ngoài khoảng đã chỉ định. Lý do là
phương thức cố ánh xạ giá trị do rnd.nextInt() trả về thành một int không âm bằng cách gọi Math.abs. Nếu nextInt() trả về
Integer.MIN_VALUE, thì Math.abs cũng sẽ trả về Integer.MIN_VALUE, và toán tử chia lấy dư (%) sẽ trả về một số âm, giả sử
n không phải lũy thừa của hai. Điều này gần như chắc chắn sẽ khiến chương trình của bạn lỗi, và lỗi đó có thể khó tái hiện.
24. Tránh float và double nếu cần kết quả chính xác
Hãy dùng BigDecimal cho các khoản tiền. float và double gây ra sai số làm tròn.
Ví dụ, giả sử bạn có $1.03 trong túi và tiêu hết 42¢. Bạn còn lại bao nhiêu tiền? Đây là một đoạn chương trình ngây thơ cố gắng trả lời câu hỏi này:
System.out.println(1.03 - 0.42);
Đáng tiếc là nó in ra 0.6100000000000001. Đây không phải trường hợp cá biệt. Giả sử bạn có một đô la trong túi và mua chín chiếc vòng đệm, mỗi chiếc giá mười xu. Bạn nhận lại
bao nhiêu tiền thừa?
System.out.println(1.00 - 9 * 0.10);
Theo đoạn chương trình này, bạn nhận được $0.09999999999999998.
Bạn có thể nghĩ rằng vấn đề có thể giải quyết chỉ bằng cách làm tròn kết quả trước khi in, nhưng tiếc là cách này không phải lúc nào cũng hiệu quả. Ví dụ, giả sử bạn có một đô la trong túi, và bạn thấy một kệ bày hàng kẹo ngon lành có giá 10¢, 20¢, 30¢, v.v., cho đến một đô la. Bạn mua mỗi loại một viên, bắt đầu từ viên giá 10¢, cho đến khi không còn đủ tiền mua viên tiếp theo trên kệ.
25. Ưu tiên kiểu primitive hơn boxed primitive
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Tham chiếu đến đối tượng qua interface của chúng
Ví dụ tốt:
Set<Son> sonSet = new LinkedHashSet<>();
Ví dụ tệ:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Ưu tiên interface hơn reflection
Nếu không:
- Bạn mất toàn bộ lợi ích của việc kiểm tra kiểu lúc biên dịch, bao gồm cả kiểm tra exception. Nếu một chương trình cố gọi qua reflection một phương thức không tồn tại hoặc không truy cập được, nó sẽ thất bại lúc runtime trừ khi bạn đã có biện pháp phòng ngừa đặc biệt.
- Code cần thiết để truy cập qua reflection rườm rà và dài dòng. Viết thì tẻ nhạt, đọc thì khó.
- Hiệu năng giảm sút. Gọi phương thức qua reflection chậm hơn nhiều so với gọi phương thức thông thường.
28. Tuân thủ các quy ước đặt tên được chấp nhận rộng rãi
Nền tảng Java có một bộ quy ước đặt tên đã được thiết lập vững chắc, nhiều quy ước trong số đó nằm trong The Java Language Specification [JLS, 6.1].
29. Chỉ dùng exception cho các tình huống ngoại lệ
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Đoạn code này làm gì? Nhìn vào thì hoàn toàn không rõ, và chỉ riêng điều đó đã đủ lý do để không dùng nó. Hóa ra đây là một idiom được nghĩ ra cực kỳ tồi để duyệt qua các phần tử của một
mảng. Vòng lặp vô hạn kết thúc bằng cách ném, bắt và phớt lờ một ArrayIndexOutOfBoundsException khi nó
cố truy cập phần tử đầu tiên nằm ngoài giới hạn của mảng. Nó được cho là tương đương với idiom chuẩn để duyệt mảng, thứ mà bất kỳ lập trình viên Java nào
cũng nhận ra ngay:
for (Mountain m : range) {
m.climb();
}
Exception, đúng như tên gọi, chỉ nên được dùng cho các tình huống ngoại lệ; không bao giờ nên dùng chúng cho luồng điều khiển thông thường.
30. Dùng checked exception cho tình huống có thể khắc phục và runtime exception cho lỗi lập trình
Hãy dùng checked exception cho những tình huống mà có thể kỳ vọng hợp lý rằng bên gọi sẽ khắc phục được. Khi buộc người dùng phải đối mặt với một checked exception, người thiết kế API đưa ra yêu cầu bắt buộc phải khắc phục tình huống đó. Người dùng có thể phớt lờ yêu cầu này bằng cách bắt exception rồi bỏ qua, nhưng thường đó là một ý tưởng tồi.
Hãy dùng runtime exception để báo hiệu lỗi lập trình. Đại đa số runtime exception báo hiệu vi phạm điều kiện tiên quyết (precondition). Vi phạm điều kiện tiên quyết đơn giản là việc
client của một API không tuân thủ hợp đồng mà đặc tả API đã thiết lập. Ví dụ, hợp đồng truy cập mảng quy định rằng chỉ số mảng phải nằm trong khoảng từ không đến độ dài
mảng trừ một, tính cả hai đầu. ArrayIndexOutOfBoundsException cho biết điều kiện tiên quyết này đã bị vi phạm.
31. Tránh dùng checked exception không cần thiết
Tóm lại, khi được dùng có chừng mực, checked exception có thể tăng độ tin cậy của chương trình; khi bị lạm dụng, chúng khiến API trở nên khổ sở khi sử dụng.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. Ưu tiên dùng exception tiêu chuẩn
Một đặc điểm phân biệt lập trình viên chuyên gia với người ít kinh nghiệm hơn là chuyên gia luôn hướng tới và thường đạt được mức độ tái sử dụng code cao.
Exception cũng không phải ngoại lệ đối với quy tắc rằng tái sử dụng code là điều tốt. Các thư viện Java cung cấp một bộ exception đáp ứng hầu hết nhu cầu ném exception của phần lớn API. Tái sử dụng exception tiêu chuẩn có nhiều lợi ích. Quan trọng nhất là nó giúp API của bạn dễ học và dễ dùng hơn, vì nó khớp với các quy ước đã được thiết lập mà lập trình viên vốn đã quen thuộc. Đứng thứ hai không kém cạnh là các chương trình dùng API của bạn dễ đọc hơn, vì chúng không bị rối bởi những exception lạ lẫm. Cuối cùng (và ít quan trọng nhất), ít class exception hơn nghĩa là tốn ít bộ nhớ hơn và mất ít thời gian nạp class hơn.
Kiểu exception được tái sử dụng phổ biến nhất là IllegalArgumentException. Đây thường là exception nên ném khi bên gọi truyền vào một đối số có giá trị không phù hợp. Ví
dụ, đây sẽ là exception nên ném nếu bên gọi truyền một số âm vào tham số biểu thị số lần một hành động cần được lặp lại. Một exception khác thường được tái sử dụng
là IllegalStateException. Đây thường là exception nên ném nếu lời gọi không hợp lệ do trạng thái của đối tượng nhận lời gọi. Ví dụ, đây sẽ là
exception nên ném nếu bên gọi cố dùng một đối tượng nào đó trước khi nó được khởi tạo đúng cách.
Có thể nói rằng mọi lời gọi phương thức sai đều quy về một đối số hoặc trạng thái không hợp lệ, nhưng có những exception khác được dùng theo chuẩn cho một số loại đối số và trạng thái không hợp lệ nhất định. Nếu bên gọi
truyền null vào một tham số không cho phép giá trị null, theo quy ước nên ném NullPointerException thay vì IllegalArgumentException.
Tương tự, nếu bên gọi truyền một giá trị nằm ngoài khoảng vào tham số biểu thị chỉ số trong một dãy, nên ném IndexOutOfBoundsException thay vì IllegalArgumentException.
Đừng tái sử dụng trực tiếp Exception, RuntimeException, Throwable hay Error. Hãy coi các class này như thể chúng là abstract. Dù vậy, có thể dùng RuntimeException nếu bạn muốn
chuyển một checked exception thành runtime exception, và bạn thực sự biết mình đang làm gì.
33. Đừng phớt lờ exception
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Một khối catch rỗng làm mất đi mục đích của exception, vốn là buộc bạn phải xử lý các tình huống ngoại lệ. Phớt lờ một exception cũng giống như phớt lờ chuông báo cháy – rồi tắt luôn nó đi để không ai khác có cơ hội xem liệu có cháy thật hay không.