Tại sao lại ném NullPulumException một cách rõ ràng thay vì để nó diễn ra tự nhiên?


182

Khi đọc mã nguồn JDK, tôi thấy thông thường rằng tác giả sẽ kiểm tra các tham số nếu chúng là null và sau đó ném NullPulumException () mới theo cách thủ công. Tại sao họ làm điều đó? Tôi nghĩ rằng không cần phải làm như vậy vì nó sẽ ném NullPulumException () mới khi nó gọi bất kỳ phương thức nào. (Đây là một số mã nguồn của HashMap, ví dụ :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

32
một điểm quan trọng của mã hóa là ý định
Scary Wombat

19
Đây là một câu hỏi rất hay cho câu hỏi đầu tiên của bạn! Tôi đã thực hiện một số chỉnh sửa nhỏ; Tôi hy vọng bạn không phiền. Tôi cũng đã xóa lời cảm ơn và lưu ý về nó là câu hỏi đầu tiên của bạn vì thông thường đó không phải là một phần của câu hỏi SO.
David Conrad

11
Tôi là C # quy ước là nêu ra ArgumentNullExceptiontrong những trường hợp như vậy (chứ không phải NullReferenceException) - đó thực sự là một câu hỏi hay về lý do tại sao bạn nêu ra NullPointerExceptionmột cách rõ ràng ở đây (chứ không phải là một câu hỏi khác).
EJoshuaS - Phục hồi Monica

21
@EJoshuaS Đó là một cuộc tranh luận cũ về việc nên ném IllegalArgumentExceptionhay NullPointerExceptioncho một cuộc tranh luận không có giá trị. Quy ước JDK là sau.
shmosel

33
Vấn đề THỰC SỰ là họ ném lỗi và vứt bỏ tất cả thông tin dẫn đến lỗi này . Có vẻ đây mã nguồn thực tế . Thậm chí không phải là một tin nhắn chuỗi máu đơn giản. Buồn.
Martin Ba

Câu trả lời:


254

Có một số lý do xuất hiện trong đầu, một số lý do liên quan chặt chẽ:

Thất bại nhanh: Nếu nó sẽ thất bại, tốt nhất là thất bại sớm hơn là muộn hơn. Điều này cho phép các vấn đề được nắm bắt gần hơn với nguồn của chúng, giúp chúng dễ dàng xác định và phục hồi hơn. Nó cũng tránh lãng phí chu kỳ CPU vào mã bị ràng buộc thất bại.

Ý định: Ném ngoại lệ một cách rõ ràng cho người bảo trì thấy rằng lỗi là có chủ ý và tác giả đã nhận thức được hậu quả.

Tính nhất quán: Nếu lỗi được phép xảy ra một cách tự nhiên, nó có thể không xảy ra trong mọi kịch bản. Nếu không tìm thấy ánh xạ, ví dụ, remappingFunctionsẽ không bao giờ được sử dụng và ngoại lệ sẽ không được ném. Xác nhận đầu vào trước cho phép hành vi xác định hơn và tài liệu rõ ràng hơn .

Ổn định: Mã phát triển theo thời gian. Mã gặp một ngoại lệ tự nhiên có thể, sau một chút tái cấu trúc, ngừng làm như vậy hoặc làm như vậy trong các trường hợp khác nhau. Ném nó một cách rõ ràng làm cho hành vi ít có khả năng thay đổi vô tình.


14
Ngoài ra: theo cách này, vị trí mà từ đó ném ngoại lệ được gắn với chính xác một biến được kiểm tra. Không có nó, ngoại lệ có thể là do một trong nhiều biến là null.
Jens Schauder

45
Một số khác: nếu bạn chờ NPE diễn ra tự nhiên, một số mã ở giữa có thể đã thay đổi trạng thái chương trình của bạn thông qua các hiệu ứng phụ.
Thomas

6
Trong khi đoạn mã này không làm điều đó, bạn có thể sử dụng hàm new NullPointerException(message)tạo để làm rõ cái gì là null. Tốt cho những người không có quyền truy cập vào mã nguồn của bạn. Họ thậm chí đã biến nó thành một lớp lót trong JDK 8 với Objects.requireNonNull(object, message)phương thức tiện ích.
Robin

3
FAILURE nên ở gần FAULT. "Fail Fast" không chỉ là một quy tắc. Khi nào bạn không muốn hành vi này? Bất kỳ hành vi khác có nghĩa là bạn đang che giấu lỗi. Có "FAULTS" và "FAILOUND". FAILURE là khi chương trình này tiêu hóa một con trỏ NULL và gặp sự cố. Nhưng dòng mã đó không phải là nơi FAULT nằm. NULL đến từ một nơi nào đó - một đối số phương thức. Ai đã thông qua lập luận đó? Từ một số dòng mã tham chiếu một biến cục bộ. Nơi đó đã ... Thấy chưa? Đó là hút. Trách nhiệm của ai là phải thấy giá trị xấu được lưu trữ? Chương trình của bạn đã bị sập rồi.
Noah Spurrier

4
@Thomas Điểm tốt. Shmosel: Quan điểm của Thomas có thể được ngụ ý ở điểm không nhanh, nhưng đó là loại bị chôn vùi. Đó là một khái niệm đủ quan trọng mà nó có tên riêng: nguyên tử thất bại . Xem Bloch, Java hiệu quả , Mục 46. Nó có ngữ nghĩa mạnh hơn là không nhanh. Tôi đề nghị gọi nó ra ở một điểm riêng biệt. Câu trả lời tuyệt vời tổng thể, BTW. +1
Stuart Marks

40

Đó là cho sự rõ ràng, nhất quán và để ngăn chặn các công việc không cần thiết được thực hiện.

Xem xét những gì sẽ xảy ra nếu không có một điều khoản bảo vệ ở đầu phương thức. Nó sẽ luôn gọi hash(key)getNode(hash, key)ngay cả khi nullđã được thông qua choremappingFunction trước khi NPE bị ném.

Thậm chí tệ hơn, nếu ifđiều kiện là falsesau đó chúng ta lấy elsenhánh, hoàn toàn không sử dụng remappingFunction, điều đó có nghĩa là phương thức không luôn luôn ném NPE khi a nullđược thông qua; cho dù nó phụ thuộc vào trạng thái của bản đồ.

Cả hai kịch bản đều xấu. Nếu nullkhông phải là một giá trị hợp lệ cho remappingFunctionphương thức thì nên liên tục đưa ra một ngoại lệ bất kể trạng thái bên trong của đối tượng tại thời điểm cuộc gọi và nó sẽ làm như vậy mà không làm những việc không cần thiết mà vô nghĩa là nó sẽ bị ném. Cuối cùng, đó là một nguyên tắc tốt về mã sạch, rõ ràng để có người bảo vệ ngay trước mặt để bất kỳ ai xem lại mã nguồn đều có thể dễ dàng thấy rằng nó sẽ làm như vậy.

Ngay cả khi ngoại lệ hiện đang bị ném bởi mọi nhánh mã, có thể bản sửa đổi mã trong tương lai sẽ thay đổi điều đó. Thực hiện kiểm tra ngay từ đầu đảm bảo chắc chắn sẽ được thực hiện.


25

Ngoài những lý do được liệt kê bởi câu trả lời tuyệt vời của @ shmosel ...

Hiệu năng: Có thể có / đã có lợi ích về hiệu năng (trên một số JVM) để ném NPE một cách rõ ràng thay vì để JVM làm điều đó.

Nó phụ thuộc vào chiến lược mà trình thông dịch Java và trình biên dịch JIT thực hiện để phát hiện sự hủy bỏ hội nghị của các con trỏ null. Một chiến lược là không kiểm tra null, nhưng thay vào đó bẫy SIGSEGV xảy ra khi một lệnh cố truy cập địa chỉ 0. Đây là cách tiếp cận nhanh nhất trong trường hợp tham chiếu luôn hợp lệ, nhưng đó là tốn kém trong trường hợp NPE.

Một bài kiểm tra rõ ràng cho null mã sẽ tránh được hiệu năng SIGSEGV đạt được trong một kịch bản thường xuyên xảy ra NPE.

(Tôi nghi ngờ rằng đây sẽ là một tối ưu hóa vi mô đáng giá trong một JVM hiện đại, nhưng nó có thể là trong quá khứ.)


Khả năng tương thích: Lý do có khả năng không có thông báo trong ngoại lệ là vì khả năng tương thích với các NPE do chính JVM ném ra. Trong một triển khai Java tuân thủ, một NPE được ném bởi JVM có một nullthông báo. (Java Java thì khác.)


20

Ngoài những gì người khác đã chỉ ra, đáng chú ý vai trò của hội nghị ở đây. Ví dụ, trong C #, bạn cũng có cùng một quy ước về việc nêu rõ một ngoại lệ trong các trường hợp như thế này, nhưng cụ thể là ArgumentNullException, một phần cụ thể hơn. (Quy ước C # là NullReferenceException luôn đại diện cho một loại lỗi nào đó - khá đơn giản, nó không bao giờ xảy ra trong mã sản xuất;ArgumentNullException thường là như vậy, nhưng nó có thể là một lỗi nhiều hơn trong dòng "bạn không hiểu cách sử dụng thư viện một cách chính xác "loại lỗi).

Vì vậy, về cơ bản, trong C # NullReferenceExceptioncó nghĩa là chương trình của bạn thực sự đã cố gắng sử dụng nó, trong khi ArgumentNullExceptionđiều đó có nghĩa là nó nhận ra rằng giá trị đó là sai và thậm chí không thèm thử sử dụng nó. Các hàm ý thực sự có thể khác nhau (tùy thuộc vào hoàn cảnh) bởi vì ArgumentNullExceptioncó nghĩa là phương pháp được đề cập chưa có tác dụng phụ (vì nó không thành công các điều kiện tiên quyết của phương pháp).

Ngẫu nhiên, nếu bạn đang nêu ra một cái gì đó như ArgumentNullExceptionhoặc IllegalArgumentException, đó là một phần của việc kiểm tra: bạn muốn có một ngoại lệ khác với "bình thường" bạn nhận được.

Dù bằng cách nào, việc nêu rõ ngoại lệ củng cố thực tiễn tốt về việc rõ ràng về các điều kiện trước và các đối số dự kiến ​​của phương thức của bạn, giúp mã dễ đọc, sử dụng và duy trì hơn. Nếu bạn không kiểm tra rõ ràng null, tôi không biết có phải vì bạn nghĩ rằng sẽ không có ai vượt qua được một nullcuộc tranh cãi hay không, dù sao bạn cũng đang đếm nó để ném ngoại lệ, hoặc bạn chỉ quên kiểm tra điều đó.


4
+1 cho đoạn giữa. Tôi sẽ lập luận rằng mã trong câu hỏi nên 'ném IllegalArgumentException mới ("ánh xạ lại không thể là null");' Theo cách đó, nó ngay lập tức rõ ràng những gì đã sai. NPE hiển thị là một chút mơ hồ.
Chris Parker

1
@ChrisParker Tôi đã từng có cùng quan điểm, nhưng hóa ra NullPulumException có ý định biểu thị một đối số null được truyền cho một phương thức mong đợi một đối số không null, ngoài ra còn là một phản hồi thời gian chạy để cố gắng hủy bỏ tính hợp lệ. Từ javadoc: Các ứng dụng có thể ném các thể hiện của lớp này để chỉ ra các mục đích sử dụng bất hợp pháp khác của nullđối tượng. Tôi không điên về nó, nhưng dường như đó là thiết kế dự định.
VGR

1
Tôi đồng ý, @ChrisParker - Tôi nghĩ rằng ngoại lệ đó cụ thể hơn (vì mã thậm chí không bao giờ cố gắng làm bất cứ điều gì với giá trị, nó ngay lập tức nhận ra rằng nó không nên sử dụng nó). Tôi thích quy ước C # trong trường hợp này. Quy ước C # là NullReferenceException(tương đương NullPointerException) có nghĩa là mã của bạn thực sự đã cố sử dụng nó (luôn luôn là một lỗi - nó không bao giờ xảy ra trong mã sản xuất), so với "Tôi biết đối số là sai, vì vậy tôi thậm chí không cố gắng sử dụng nó. " Cũng có ArgumentException(có nghĩa là tranh luận đã sai vì một số lý do khác).
EJoshuaS - Phục hồi Monica

2
Tôi sẽ nói nhiều như vậy, TÔI LUÔN ném IllegalArgumentException như mô tả. Tôi luôn cảm thấy thoải mái khi bỏ qua hội nghị khi tôi cảm thấy như hội nghị bị câm.
Chris Parker

1
@PieterGeerkens - vâng, bởi vì dòng NullPulumException 35 tốt hơn nhiều so với IllegalArgumentException ("Chức năng không thể là null") dòng 35. Nghiêm túc chứ?
Chris Parker

12

Đó là vì vậy bạn sẽ nhận được ngoại lệ ngay khi bạn khắc phục lỗi, thay vì sau này khi bạn đang sử dụng bản đồ và sẽ không hiểu tại sao nó lại xảy ra.


9

Nó biến một điều kiện lỗi dường như thất thường thành vi phạm hợp đồng rõ ràng: Hàm này có một số điều kiện tiên quyết để hoạt động chính xác, do đó, nó sẽ kiểm tra chúng trước, buộc chúng phải được đáp ứng.

Hiệu quả là, bạn sẽ không phải gỡ lỗi computeIfPresent()khi bạn lấy ngoại lệ ra khỏi nó. Khi bạn thấy rằng ngoại lệ xuất phát từ kiểm tra điều kiện tiên quyết, bạn biết rằng bạn đã gọi hàm với một đối số bất hợp pháp. Nếu kiểm tra không có ở đó, bạn sẽ cần loại trừ khả năng có một số lỗi trong computeIfPresent()chính nó dẫn đến ngoại lệ bị ném.

Rõ ràng, ném chung NullPointerExceptionlà một lựa chọn thực sự tồi tệ, vì nó không báo hiệu sự vi phạm hợp đồng trong chính nó. IllegalArgumentExceptionsẽ là một lựa chọn tốt hơn


Sidenote:
Tôi không biết liệu Java có cho phép điều này không (tôi nghi ngờ về điều đó), nhưng các lập trình viên C / C ++ sử dụng một assert()trường hợp này, điều này tốt hơn đáng kể cho việc gỡ lỗi: Nó bảo chương trình bị sập ngay lập tức và càng khó càng tốt đánh giá điều kiện thành sai. Vì vậy, nếu bạn chạy

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

trong một trình gỡ lỗi và một cái gì đó được chuyển NULLvào một trong hai đối số, chương trình sẽ dừng ngay tại dòng cho biết đối số đó là gì NULLvà bạn có thể kiểm tra tất cả các biến cục bộ của toàn bộ ngăn xếp cuộc gọi.


1
assert something != null;nhưng điều đó đòi hỏi -assertionscờ khi chạy ứng dụng. Nếu -assertionscờ không có ở đó, từ khóa khẳng định sẽ không ném Ass AssException
Zoe

Tôi đồng ý, đó là lý do tại sao tôi thích quy ước C # ở đây - tham chiếu null, đối số không hợp lệ và đối số null thường ngụ ý một loại lỗi nào đó, nhưng chúng bao hàm các loại lỗi khác nhau . "Bạn đang cố gắng sử dụng tài liệu tham khảo null" thường rất khác so với "bạn đang sử dụng sai thư viện."
EJoshuaS - Phục hồi Monica

7

Đó là vì nó không thể xảy ra một cách tự nhiên. Chúng ta hãy xem đoạn mã như thế này:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(tất nhiên có rất nhiều vấn đề trong mẫu này về chất lượng mã. Xin lỗi vì lười biếng để tưởng tượng mẫu hoàn hảo)

Tình huống khi ckhông thực sự được sử dụng và NullPointerExceptionsẽ không được ném ngay cả khi c == nullcó thể.

Trong những tình huống phức tạp hơn, việc săn lùng những trường hợp như vậy trở nên không dễ dàng. Đây là lý do tại sao kiểm tra chung như thế if (c == null) throw new NullPointerException()là tốt hơn.


Có thể cho rằng, một đoạn mã hoạt động mà không có kết nối cơ sở dữ liệu khi không thực sự cần thiết là một điều tốt và mã kết nối với cơ sở dữ liệu chỉ để xem liệu nó có thể không làm như vậy thường khá khó chịu.
Dmitry Grigoryev

5

Đó là cố ý để bảo vệ thiệt hại hơn nữa, hoặc vào trạng thái không nhất quán.


1

Ngoài tất cả các câu trả lời xuất sắc khác ở đây, tôi cũng muốn thêm một vài trường hợp.

Bạn có thể thêm một tin nhắn nếu bạn tạo ngoại lệ của riêng bạn

Nếu bạn tự ném, NullPointerExceptionbạn có thể thêm một tin nhắn (mà bạn chắc chắn nên!)

Thông điệp mặc định là nulltừ new NullPointerException()và tất cả các phương thức sử dụng nó, ví dụ Objects.requireNonNull. Nếu bạn in null đó, nó thậm chí có thể dịch thành một chuỗi trống ...

Một chút ngắn và không thông tin ...

Theo dõi ngăn xếp sẽ cung cấp rất nhiều thông tin, nhưng để người dùng biết null là gì, họ phải đào mã và xem hàng chính xác.

Bây giờ hãy tưởng tượng rằng NPE được gói và gửi qua mạng, ví dụ như một thông báo trong lỗi dịch vụ web, có lẽ giữa các bộ phận hoặc thậm chí các tổ chức khác nhau. Trường hợp xấu nhất, không ai có thể hiểu được điều gìnull đại diện cho ...

Các cuộc gọi phương thức xích sẽ giúp bạn đoán

Một ngoại lệ sẽ chỉ cho bạn biết hàng nào xảy ra ngoại lệ. Hãy xem xét hàng sau:

repository.getService(someObject.someMethod());

Nếu bạn nhận được một NPE và nó chỉ vào hàng này, cái nào trong số đó repositorysomeObjectlà null?

Thay vào đó, kiểm tra các biến này khi bạn nhận được chúng ít nhất sẽ trỏ đến một hàng nơi chúng hy vọng là biến duy nhất được xử lý. Và, như đã đề cập trước đó, thậm chí tốt hơn nếu thông báo lỗi của bạn chứa tên của biến hoặc tương tự.

Lỗi khi xử lý nhiều đầu vào sẽ cung cấp thông tin nhận dạng

Hãy tưởng tượng rằng chương trình của bạn đang xử lý một tệp đầu vào với hàng ngàn hàng và đột nhiên có một NullPulumException. Bạn nhìn vào địa điểm và nhận ra một số đầu vào không chính xác ... đầu vào nào? Bạn sẽ cần thêm thông tin về số hàng, có thể là cột hoặc thậm chí toàn bộ văn bản hàng để hiểu hàng nào trong tệp đó cần sửa.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.