Các trường hợp chống lại ngoại lệ được kiểm tra


456

Trong một số năm nay tôi đã không thể có được một câu trả lời xác đáng cho câu hỏi sau: tại sao một số nhà phát triển chống lại các ngoại lệ được kiểm tra? Tôi đã có rất nhiều cuộc trò chuyện, đọc những điều trên blog, đọc những gì Bruce Eckel đã nói (người đầu tiên tôi thấy lên tiếng chống lại họ).

Tôi hiện đang viết một số mã mới và chú ý rất cẩn thận đến cách tôi xử lý các trường hợp ngoại lệ. Tôi đang cố gắng để xem quan điểm của đám đông "chúng tôi không thích ngoại lệ được kiểm tra" và tôi vẫn không thể nhìn thấy nó.

Mọi cuộc trò chuyện tôi có đều kết thúc với cùng một câu hỏi chưa được trả lời ... hãy để tôi thiết lập nó:

Nói chung (từ cách Java được thiết kế),

  • Error dành cho những thứ không bao giờ bị bắt (VM bị dị ứng đậu phộng và ai đó đã làm rơi một lọ đậu phộng lên nó)
  • RuntimeException là cho những thứ mà lập trình viên đã làm sai (lập trình viên đi ra khỏi phần cuối của một mảng)
  • Exception(ngoại trừ RuntimeException) dành cho những thứ nằm ngoài tầm kiểm soát của lập trình viên (đĩa đầy trong khi ghi vào hệ thống tệp, đã đạt đến giới hạn xử lý tệp cho quy trình và bạn không thể mở thêm bất kỳ tệp nào)
  • Throwable chỉ đơn giản là cha mẹ của tất cả các loại ngoại lệ.

Một lập luận phổ biến tôi nghe được là nếu một ngoại lệ xảy ra thì tất cả các nhà phát triển sẽ làm là thoát khỏi chương trình.

Một đối số phổ biến khác mà tôi nghe thấy là các ngoại lệ được kiểm tra làm cho việc cấu trúc lại mã khó hơn.

Đối với đối số "tất cả những gì tôi sẽ làm là thoát" Tôi nói rằng ngay cả khi bạn đang thoát, bạn cần hiển thị một thông báo lỗi hợp lý. Nếu bạn chỉ loay hoay trong việc xử lý lỗi thì người dùng của bạn sẽ không quá vui khi chương trình thoát mà không có dấu hiệu rõ ràng về lý do.

Đối với đám đông "làm cho nó khó tái cấu trúc", điều đó cho thấy mức độ trừu tượng thích hợp đã không được chọn. Thay vì khai báo một phương thức ném một IOException, IOExceptionnên chuyển thành một ngoại lệ phù hợp hơn cho những gì đang diễn ra.

Tôi không gặp vấn đề gì với việc gói Main với catch(Exception)(hoặc trong một số trường hợp catch(Throwable)để đảm bảo rằng chương trình có thể thoát một cách duyên dáng - nhưng tôi luôn nắm bắt được các ngoại lệ cụ thể mà tôi cần. Ít nhất, cho phép tôi hiển thị một cách phù hợp thông báo lỗi.

Câu hỏi mà mọi người không bao giờ trả lời là đây:

Nếu bạn ném các RuntimeException lớp con thay vì các Exception lớp con thì làm sao bạn biết bạn phải bắt cái gì?

Nếu câu trả lời là bắt Exceptionthì bạn cũng đang xử lý các lỗi lập trình viên giống như ngoại lệ của hệ thống. Điều đó có vẻ sai với tôi.

Nếu bạn bắt Throwablethì bạn đang xử lý ngoại lệ hệ thống và lỗi VM (và tương tự) theo cùng một cách. Điều đó có vẻ sai với tôi.

Nếu câu trả lời là bạn chỉ bắt được những ngoại lệ mà bạn biết là bị ném thì làm sao bạn biết cái nào được ném? Điều gì xảy ra khi lập trình viên X ném một ngoại lệ mới và quên bắt nó? Điều đó có vẻ rất nguy hiểm với tôi.

Tôi muốn nói rằng một chương trình hiển thị dấu vết ngăn xếp là sai. Có phải những người không thích ngoại lệ được kiểm tra không cảm thấy như vậy?

Vì vậy, nếu bạn không thích các ngoại lệ được kiểm tra, bạn có thể giải thích tại sao không VÀ trả lời câu hỏi không được trả lời không?

Chỉnh sửa: Tôi không tìm kiếm lời khuyên khi nào nên sử dụng một trong hai mô hình, điều tôi đang tìm kiếm là tại sao mọi người mở rộng RuntimeExceptionvì họ không thích kéo dài từ Exceptionvà / hoặc tại sao họ bắt một ngoại lệ và sau đó nghĩ lại RuntimeExceptionthay vì ném vào phương pháp của họ. Tôi muốn hiểu động cơ cho việc không thích các ngoại lệ được kiểm tra.


46
Tôi không nghĩ nó hoàn toàn chủ quan - đó là một tính năng ngôn ngữ được thiết kế để có một mục đích sử dụng cụ thể, thay vì cho mọi người quyết định những gì nó dành cho chính họ. Và nó không đặc biệt tranh luận, nó giải quyết trước những phản bác cụ thể mà mọi người có thể dễ dàng nghĩ ra.
Gareth

6
Nào. Được xem như một tính năng ngôn ngữ, chủ đề này đã và có thể được tiếp cận một cách khách quan.
Kurt Schelfthout

6
@cletus "trả lời câu hỏi của riêng bạn" nếu tôi có câu trả lời tôi sẽ không hỏi câu hỏi đó!
TofuBeer

8
Câu hỏi tuyệt vời. Trong C ++ không có ngoại lệ nào được kiểm tra cả, và theo tôi, nó làm cho tính năng ngoại lệ không thể sử dụng được. Bạn kết thúc trong một tình huống mà bạn phải nắm bắt mọi chức năng mà bạn thực hiện, bởi vì bạn không biết liệu nó có thể ném thứ gì đó không.
Dimitri C.

4
Đối số mạnh nhất mà tôi biết đối với các ngoại lệ được kiểm tra là chúng không có trong Java và khi chúng được giới thiệu, chúng đã phát hiện ra hàng trăm lỗi trong JDK. Điều này có phần trước Java 1.0. Cá nhân tôi sẽ không thể không có họ, và không đồng ý dữ dội với Bruce Eckel và những người khác về điều này.
Hầu tước Lorne

Câu trả lời:


277

Tôi nghĩ rằng tôi đã đọc cùng một cuộc phỏng vấn Bruce Eckel mà bạn đã làm - và nó luôn làm tôi khó chịu. Trên thực tế, cuộc tranh luận được đưa ra bởi người được phỏng vấn (nếu đây thực sự là bài đăng mà bạn đang nói đến) Anders Hejlsberg, thiên tài MS đứng sau .NET và C #.

http://www.artima.com/intv/handcuffs.html

Fan mặc dù tôi là Hejlsberg và công việc của anh ấy, cuộc tranh luận này luôn khiến tôi không có thật. Về cơ bản, nó sôi sùng sục xuống:

"Các trường hợp ngoại lệ được kiểm tra là xấu vì các lập trình viên chỉ lạm dụng chúng bằng cách luôn bắt chúng và loại bỏ chúng dẫn đến các vấn đề bị ẩn và bỏ qua sẽ được trình bày cho người dùng".

Bằng cách "trình bày cho người dùng" Ý tôi là nếu bạn sử dụng ngoại lệ thời gian chạy, lập trình viên lười biếng sẽ bỏ qua nó (so với việc bắt nó với một khối bắt trống) và người dùng sẽ thấy nó.

Tóm tắt tóm tắt của đối số là "Các lập trình viên sẽ không sử dụng chúng đúng cách và không sử dụng chúng đúng cách còn tệ hơn là không có chúng" .

Có một số sự thật đối với lập luận này và trên thực tế, tôi nghi ngờ động lực của Goslings về việc không đặt các toán tử ghi đè lên Java xuất phát từ một đối số tương tự - chúng gây nhầm lẫn cho lập trình viên vì chúng thường bị lạm dụng.

Nhưng cuối cùng, tôi thấy đó là một lập luận không có thật của Hejlsberg và có thể là một bài hậu-hoc được tạo ra để giải thích sự thiếu thốn hơn là một quyết định được cân nhắc kỹ lưỡng.

Tôi sẽ lập luận rằng mặc dù việc sử dụng quá mức các ngoại lệ được kiểm tra là một điều xấu và có xu hướng dẫn đến việc xử lý cẩu thả của người dùng, nhưng việc sử dụng chúng đúng cách cho phép lập trình viên API mang lại lợi ích lớn cho lập trình viên API.

Bây giờ, lập trình viên API phải cẩn thận để không ném các ngoại lệ được kiểm tra khắp nơi, hoặc đơn giản là chúng sẽ gây khó chịu cho lập trình viên máy khách. Lập trình viên khách hàng rất lười biếng sẽ dùng đến để bắt (Exception) {}như Hejlsberg cảnh báo và tất cả lợi ích sẽ bị mất và địa ngục sẽ xảy ra. Nhưng trong một số trường hợp, không có sự thay thế nào cho một ngoại lệ được kiểm tra tốt.

Đối với tôi, ví dụ kinh điển là API mở tệp. Mọi ngôn ngữ lập trình trong lịch sử ngôn ngữ (ít nhất là trên các hệ thống tệp) đều có API ở đâu đó cho phép bạn mở tệp. Và mọi lập trình viên khách hàng sử dụng API này đều biết rằng họ phải giải quyết trường hợp tệp mà họ đang cố mở không tồn tại. Hãy để tôi nói lại rằng: Mọi lập trình viên khách hàng sử dụng API này nên biết rằng họ phải giải quyết trường hợp này. Và có một điều khó khăn: lập trình viên API có thể giúp họ biết họ nên giải quyết vấn đề đó thông qua nhận xét một mình hay họ thực sự có thể yêu cầu khách hàng giải quyết nó.

Trong C thành ngữ đi một cái gì đó như

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

trong đó fopenchỉ ra thất bại bằng cách trả về 0 và C (một cách dại dột) cho phép bạn coi 0 là một boolean và ... Về cơ bản, bạn học thành ngữ này và bạn ổn. Nhưng điều gì sẽ xảy ra nếu bạn là một người mới và bạn không học thành ngữ này. Sau đó, tất nhiên, bạn bắt đầu với

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

và học một cách khó khăn.

Lưu ý rằng chúng ta chỉ nói về các ngôn ngữ được gõ mạnh ở đây: Có một ý tưởng rõ ràng về API là gì trong ngôn ngữ được gõ mạnh: Đó là một chức năng (phương thức) để bạn sử dụng với giao thức được xác định rõ ràng cho từng ngôn ngữ.

Giao thức được xác định rõ ràng thường được xác định bởi một chữ ký phương thức. Ở đây fopen yêu cầu bạn truyền cho nó một chuỗi (hoặc char * trong trường hợp C). Nếu bạn cung cấp cho nó một cái gì đó khác, bạn nhận được một lỗi thời gian biên dịch. Bạn đã không tuân theo giao thức - bạn không sử dụng API đúng cách.

Trong một số ngôn ngữ (tối nghĩa), kiểu trả về cũng là một phần của giao thức. Nếu bạn cố gắng gọi tương đương với fopen()một số ngôn ngữ mà không gán nó cho một biến, bạn cũng sẽ gặp lỗi thời gian biên dịch (bạn chỉ có thể làm điều đó với các hàm void).

Điểm tôi đang cố gắng đưa ra là: Trong một ngôn ngữ được gõ tĩnh, lập trình viên API khuyến khích khách hàng sử dụng API đúng cách bằng cách ngăn không cho mã máy khách của họ biên dịch nếu nó có bất kỳ lỗi rõ ràng nào.

(Trong một ngôn ngữ được gõ động, như Ruby, bạn có thể chuyển bất kỳ thứ gì, nói nổi, như tên tệp - và nó sẽ biên dịch. Tại sao lại gây rắc rối cho người dùng với các ngoại lệ được kiểm tra nếu bạn thậm chí không kiểm soát các đối số phương thức. các đối số được thực hiện ở đây chỉ áp dụng cho các ngôn ngữ được nhập tĩnh.)

Vì vậy, những gì về ngoại lệ được kiểm tra?

Đây là một trong những API Java mà bạn có thể sử dụng để mở tệp.

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

Thấy mà bắt không? Đây là chữ ký cho phương thức API đó:

public FileInputStream(String name)
                throws FileNotFoundException

Lưu ý rằng đó FileNotFoundExceptionlà một ngoại lệ được kiểm tra .

Lập trình viên API đang nói điều này với bạn: "Bạn có thể sử dụng hàm tạo này để tạo FileInputStream mới nhưng bạn

a) phải chuyển vào tên tệp dưới dạng Chuỗi
b) phải chấp nhận khả năng tệp có thể không được tìm thấy trong thời gian chạy "

Và đó là toàn bộ vấn đề mà tôi quan tâm.

Chìa khóa về cơ bản là những gì câu hỏi nêu là "Những thứ nằm ngoài tầm kiểm soát của lập trình viên". Suy nghĩ đầu tiên của tôi là anh ấy / cô ấy có nghĩa là những thứ nằm ngoài sự kiểm soát của các lập trình viên API . Nhưng trên thực tế, các trường hợp ngoại lệ được kiểm tra khi được sử dụng đúng cách thực sự phải dành cho những thứ nằm ngoài sự kiểm soát của cả lập trình viên và lập trình viên API. Tôi nghĩ rằng đây là chìa khóa để không lạm dụng các ngoại lệ được kiểm tra.

Tôi nghĩ rằng việc mở tệp minh họa điểm độc đáo. Lập trình viên API biết rằng bạn có thể cung cấp cho họ một tên tệp hóa ra không tồn tại vào thời điểm API được gọi và họ sẽ không thể trả lại cho bạn những gì bạn muốn, nhưng sẽ phải đưa ra một ngoại lệ. Họ cũng biết rằng điều này sẽ xảy ra khá thường xuyên và lập trình viên máy khách có thể mong đợi tên tệp là chính xác tại thời điểm họ viết cuộc gọi, nhưng nó có thể sai trong thời gian chạy vì những lý do ngoài tầm kiểm soát của họ.

Vì vậy, API làm cho nó rõ ràng: Sẽ có trường hợp tệp này không tồn tại tại thời điểm bạn gọi cho tôi và bạn đã giải quyết tốt hơn với nó.

Điều này sẽ rõ ràng hơn với một trường hợp phản tác dụng. Hãy tưởng tượng tôi đang viết API bảng. Tôi có mô hình bảng ở đâu đó với API bao gồm phương thức này:

public RowData getRowData(int row) 

Bây giờ là một lập trình viên API, tôi biết sẽ có trường hợp một số khách hàng chuyển vào một giá trị âm cho hàng hoặc một giá trị hàng bên ngoài bảng. Vì vậy, tôi có thể bị cám dỗ ném một ngoại lệ được kiểm tra và buộc khách hàng phải đối phó với nó:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(Tất nhiên tôi sẽ không gọi nó là "Đã kiểm tra".)

Đây là sử dụng xấu của ngoại lệ được kiểm tra. Mã máy khách sẽ có đầy đủ các lệnh gọi để tìm nạp dữ liệu hàng, mỗi một trong số chúng sẽ phải sử dụng thử / bắt và để làm gì? Họ sẽ báo cáo cho người dùng rằng hàng sai đã được tìm kiếm? Có lẽ là không - bởi vì bất kể giao diện người dùng nào xung quanh chế độ xem bảng của tôi là gì, nó không nên cho phép người dùng vào trạng thái yêu cầu một hàng bất hợp pháp. Vì vậy, đó là một lỗi về phía lập trình viên máy khách.

Lập trình viên API vẫn có thể dự đoán rằng máy khách sẽ mã hóa các lỗi như vậy và sẽ xử lý nó với ngoại lệ thời gian chạy như IllegalArgumentException.

Với một ngoại lệ được kiểm tra getRowData, đây rõ ràng là một trường hợp sẽ dẫn đến lập trình viên lười biếng của Hejlsberg chỉ đơn giản là thêm các sản phẩm khai thác trống. Khi điều đó xảy ra, các giá trị hàng bất hợp pháp sẽ không rõ ràng ngay cả với người kiểm tra hoặc nhà phát triển máy khách gỡ lỗi, thay vào đó chúng sẽ dẫn đến các lỗi gõ cửa khó xác định chính xác nguồn. Tên lửa Arianne sẽ nổ tung sau khi phóng.

Được rồi, đây là vấn đề: Tôi đang nói rằng ngoại lệ được kiểm tra FileNotFoundExceptionkhông chỉ là một điều tốt mà còn là một công cụ thiết yếu trong hộp công cụ lập trình API để xác định API theo cách hữu ích nhất cho lập trình viên máy khách. Nhưng đó CheckedInvalidRowNumberExceptionlà một bất tiện lớn, dẫn đến lập trình xấu và nên tránh. Nhưng làm thế nào để nói sự khác biệt.

Tôi đoán đó không phải là một khoa học chính xác và tôi đoán rằng đó là nền tảng và có lẽ biện minh cho một mức độ nhất định của Hejlsberg. Nhưng tôi không vui khi ném em bé ra ngoài bằng nước tắm ở đây, vì vậy hãy cho phép tôi trích ra một số quy tắc ở đây để phân biệt các trường hợp ngoại lệ được kiểm tra tốt và xấu:

  1. Mất kiểm soát của khách hàng hoặc Đóng so với Mở:

    Các ngoại lệ được kiểm tra chỉ nên được sử dụng khi trường hợp lỗi nằm ngoài tầm kiểm soát của cả API lập trình viên máy khách. Điều này phải làm với cách mở hoặc đóng hệ thống. Trong một giao diện người dùng bị ràng buộc nơi lập trình viên máy khách có quyền kiểm soát, giả sử, trên tất cả các nút, lệnh bàn phím, v.v. có thêm và xóa các hàng khỏi chế độ xem bảng (một hệ thống đóng), đó là lỗi lập trình máy khách nếu nó cố gắng tìm nạp dữ liệu từ một hàng không tồn tại. Trong một hệ điều hành dựa trên tệp mà bất kỳ số lượng người dùng / ứng dụng nào cũng có thể thêm và xóa các tệp (một hệ thống mở), có thể hiểu rằng tệp mà khách hàng đang yêu cầu đã bị xóa mà họ không nên biết để xử lý .

  2. Tính phổ biến:

    Các trường hợp ngoại lệ được kiểm tra không nên được sử dụng trong lệnh gọi API được thực hiện thường xuyên bởi máy khách. Bởi thường thì tôi có nghĩa là từ rất nhiều nơi trong mã máy khách - không thường xuyên về thời gian. Vì vậy, một mã máy khách không có xu hướng cố gắng mở cùng một tệp, nhưng chế độ xem bảng của tôi xuất hiện ở RowDatamọi nơi từ các phương thức khác nhau. Đặc biệt, tôi sẽ viết rất nhiều mã như

    if (model.getRowData().getCell(0).isEmpty())

    và sẽ rất đau đớn khi phải thử / bắt mỗi lần.

  3. Thông báo cho người dùng:

    Các ngoại lệ được kiểm tra nên được sử dụng trong trường hợp bạn có thể tưởng tượng một thông báo lỗi hữu ích được trình bày cho người dùng cuối. Đây là "và bạn sẽ làm gì khi nó xảy ra?" câu hỏi tôi nêu ở trên. Nó cũng liên quan đến mục 1. Vì bạn có thể dự đoán rằng một cái gì đó bên ngoài hệ thống API khách của bạn có thể khiến tệp không ở đó, nên bạn có thể nói với người dùng về nó một cách hợp lý:

    "Error: could not find the file 'goodluckfindingthisfile'"

    Vì số hàng bất hợp pháp của bạn là do lỗi nội bộ và không phải do lỗi của người dùng, nên thực sự không có thông tin hữu ích nào bạn có thể cung cấp cho họ. Nếu ứng dụng của bạn không để ngoại lệ thời gian chạy rơi vào bảng điều khiển, có thể cuối cùng nó sẽ cung cấp cho họ một số thông báo xấu như:

    "Internal error occured: IllegalArgumentException in ...."

    Nói tóm lại, nếu bạn không nghĩ rằng lập trình viên khách hàng của bạn có thể giải thích ngoại lệ của bạn theo cách giúp người dùng, thì có lẽ bạn không nên sử dụng một ngoại lệ được kiểm tra.

Vì vậy, đó là những quy tắc của tôi. Một chút gì đó bị chiếm đoạt, và chắc chắn sẽ có ngoại lệ (xin vui lòng giúp tôi tinh chỉnh chúng nếu bạn muốn). Nhưng đối số chính của tôi là có những trường hợp như FileNotFoundExceptionngoại lệ được kiểm tra là một phần quan trọng và hữu ích của hợp đồng API như các loại tham số. Vì vậy, chúng ta không nên phân phối với nó chỉ vì nó được sử dụng sai.

Xin lỗi, không có nghĩa là làm cho điều này quá dài và gượng gạo. Hãy để tôi kết thúc với hai gợi ý:

Trả lời: Lập trình viên API: sử dụng ngoại lệ được kiểm tra một cách tiết kiệm để duy trì tính hữu dụng của chúng. Khi nghi ngờ sử dụng một ngoại lệ không được kiểm tra.

B: Lập trình viên khách hàng: có thói quen tạo một ngoại lệ được bao bọc (google nó) ngay từ đầu trong quá trình phát triển của bạn. JDK 1.4 và sau đó cung cấp một hàm tạo RuntimeExceptioncho việc này, nhưng bạn cũng có thể dễ dàng tạo riêng cho mình. Đây là hàm tạo:

public RuntimeException(Throwable cause)

Sau đó, hãy tập thói quen bất cứ khi nào bạn phải xử lý một ngoại lệ được kiểm tra và bạn cảm thấy lười biếng (hoặc bạn nghĩ rằng lập trình viên API đã quá nhiệt tình khi sử dụng ngoại lệ được kiểm tra ở nơi đầu tiên), đừng nuốt ngoại lệ, hãy bọc nó và suy nghĩ lại nó.

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

Đặt cái này vào một trong các mẫu mã nhỏ của IDE của bạn và sử dụng nó khi bạn cảm thấy lười biếng. Bằng cách này nếu bạn thực sự cần xử lý ngoại lệ đã kiểm tra, bạn sẽ buộc phải quay lại và xử lý sau khi gặp sự cố khi chạy. Bởi vì, hãy tin tôi (và Anders Hejlsberg), bạn sẽ không bao giờ quay lại TODO đó trong bạn

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

110
Mở tệp thực sự là một ví dụ tốt cho các trường hợp ngoại lệ được kiểm tra hoàn toàn phản tác dụng. Bởi vì hầu hết thời gian mã mở tệp KHÔNG thể làm bất cứ điều gì hữu ích về ngoại lệ - điều đó tốt hơn nên thực hiện một số lớp trong ngăn xếp cuộc gọi. Ngoại lệ được kiểm tra buộc bạn phải làm lộn xộn chữ ký phương thức của mình chỉ để bạn có thể làm những gì bạn đã làm bằng mọi cách - xử lý ngoại lệ ở nơi có ý nghĩa nhất để làm như vậy.
Michael Borgwardt

116
@Michael: Đồng ý rằng bạn thường có thể xử lý các ngoại lệ IO này ở một số cấp độ cao hơn - nhưng tôi không nghe thấy bạn phủ nhận rằng với tư cách là khách hàng API, bạn phải lường trước những điều này. Vì lý do này, tôi nghĩ rằng ngoại lệ được kiểm tra là phù hợp. Vâng, bạn sẽ phải khai báo "ném" trên mỗi phương thức lên ngăn xếp cuộc gọi, nhưng tôi không đồng ý rằng đây là sự lộn xộn. Đó là thông tin khai báo hữu ích trong chữ ký phương thức của bạn. Bạn đang nói: các phương thức cấp thấp của tôi có thể chạy vào một tệp bị thiếu, nhưng chúng sẽ để lại việc xử lý nó cho bạn. Tôi thấy không có gì để mất và chỉ có mã tốt, sạch để đạt được
hoàng

23
@Rhubarb: +1, câu trả lời rất thú vị, hiển thị các đối số cho cả hai bên. Tuyệt quá. Về nhận xét cuối cùng của bạn: lưu ý rằng việc khai báo ném trên mỗi phương thức lên ngăn xếp cuộc gọi có thể không phải lúc nào cũng có thể, đặc biệt nếu bạn đang triển khai giao diện trên đường đi.
R. Martinho Fernandes

8
Đây là một lập luận rất mạnh mẽ (và tôi đồng ý với nó nói chung), nhưng có một trường hợp nó không hoạt động: gọi lại chung. Nếu tôi có một thư viện gọi một số mã do người dùng định nghĩa, thì không có cách nào (mà tôi biết, trong java) để thư viện đó truyền bá các ngoại lệ được kiểm tra từ mã người dùng mà nó gọi tới mã người dùng gọi nó. Vấn đề này cũng mở rộng đến các phần khác của hệ thống loại của nhiều ngôn ngữ được nhập tĩnh không hỗ trợ các loại chung chung thích hợp. Câu trả lời này sẽ được cải thiện bằng cách đề cập đến điều này (và có lẽ là một phản hồi).
Mankude

7
Sai @Rhubarb. Các trường hợp ngoại lệ được kiểm tra được dành cho 'các tình huống bất ngờ' có thể & nên được xử lý, nhưng luôn không tương thích với thực tiễn tốt nhất "ném sớm, bắt muộn". Mở một tập tin là một ví dụ anh đào, có thể được xử lý. Hầu hết IOException (ví dụ viết một byte), SQLException, RemoteException đều không thể xử lý theo cách có ý nghĩa. Thất bại hoặc thử lại phải ở mức "kinh doanh" hoặc "yêu cầu" và các trường hợp ngoại lệ được kiểm tra - như được sử dụng trong các thư viện Java - là một lỗi gây khó khăn. lítatejava.com/exceptions/ từ
Thomas W

184

Điều về các ngoại lệ được kiểm tra là chúng không thực sự là ngoại lệ bởi cách hiểu thông thường về khái niệm này. Thay vào đó, chúng là các giá trị trả về thay thế API.

Toàn bộ ý tưởng của các trường hợp ngoại lệ là một lỗi được ném ở đâu đó xuống chuỗi cuộc gọi có thể nổi lên và được xử lý bằng mã ở nơi nào đó xa hơn, mà không cần mã can thiệp phải lo lắng về nó. Mặt khác, các trường hợp ngoại lệ được kiểm tra yêu cầu mọi cấp mã giữa người ném và người bắt phải khai báo họ biết về tất cả các dạng ngoại lệ có thể đi qua chúng. Điều này thực sự rất khác biệt trong thực tế nếu các ngoại lệ được kiểm tra chỉ đơn giản là các giá trị trả về đặc biệt mà người gọi phải kiểm tra. ví dụ: [mã giả]:

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

Do Java không thể thực hiện các giá trị trả về thay thế hoặc các bộ dữ liệu nội tuyến đơn giản làm giá trị trả về, các ngoại lệ được kiểm tra là một phản hồi hợp lý.

Vấn đề là rất nhiều mã, bao gồm cả các thư viện lớn, sử dụng sai các ngoại lệ được kiểm tra cho các điều kiện đặc biệt thực sự mà bạn có thể rất muốn bắt kịp nhiều cấp độ. Tại sao IOException không phải là RuntimeException? Trong mọi ngôn ngữ khác, tôi có thể để ngoại lệ IO xảy ra và nếu tôi không làm gì để xử lý nó, ứng dụng của tôi sẽ dừng lại và tôi sẽ nhận được một dấu vết ngăn xếp tiện dụng để xem xét. Đây là điều tốt nhất có thể xảy ra.

Có thể có hai phương pháp từ ví dụ bạn muốn bắt tất cả IOExceptions từ toàn bộ quy trình ghi vào luồng, hủy bỏ quy trình và nhảy vào mã báo cáo lỗi; trong Java, bạn không thể làm điều đó mà không cần thêm 'ném IOException' ở mọi cấp độ cuộc gọi, ngay cả các cấp độ mà bản thân chúng không có IO. Các phương pháp như vậy không cần biết về xử lý ngoại lệ; phải thêm ngoại lệ cho chữ ký của họ:

  1. tăng không cần thiết khớp nối;
  2. làm cho chữ ký giao diện rất dễ vỡ để thay đổi;
  3. làm cho mã ít đọc hơn;
  4. thật khó chịu khi phản ứng của lập trình viên thông thường là đánh bại hệ thống bằng cách làm một việc khủng khiếp như 'ném Exception', 'Catch (Exception e) {}' hoặc gói mọi thứ vào RuntimeException (khiến việc gỡ lỗi khó hơn).

Và sau đó, có rất nhiều trường hợp ngoại lệ thư viện vô lý như:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

Khi bạn phải làm lộn xộn mã của mình với mớ hỗn độn lố bịch như thế này, không có gì lạ khi các ngoại lệ được kiểm tra nhận được một loạt sự ghét bỏ, mặc dù thực sự đây chỉ là thiết kế API đơn giản.

Một tác động xấu đặc biệt khác là đối với Inversion of Control, trong đó thành phần A cung cấp một cuộc gọi lại cho thành phần chung B. Thành phần A muốn có thể để một ngoại lệ ném từ cuộc gọi lại của nó trở lại nơi mà nó được gọi là thành phần B, nhưng nó không thể bởi vì điều đó sẽ thay đổi giao diện gọi lại được cố định bởi B. A chỉ có thể làm điều đó bằng cách gói ngoại lệ thực sự trong RuntimeException, đây là bản tóm tắt xử lý ngoại lệ để viết.

Đã kiểm tra các trường hợp ngoại lệ như được triển khai trong Java và thư viện chuẩn của nó có nghĩa là bản tóm tắt, bản tóm tắt, bản tóm tắt. Trong một ngôn ngữ đã dài dòng, đây không phải là một chiến thắng.


14
Trong ví dụ mã của bạn, tốt nhất là xâu chuỗi các ngoại lệ để có thể tìm thấy nguyên nhân ban đầu khi đọc nhật ký: throw CanNeverHappenException (e);
Esko Luontola

5
Tôi không đồng ý. Ngoại lệ, kiểm tra hay không, là điều kiện đặc biệt. Ví dụ: một phương thức truy xuất một đối tượng thông qua HTTP. Giá trị trả về là đối tượng khác hoặc không có gì, tất cả những thứ có thể trở nên xấu là đặc biệt. Việc coi chúng là giá trị trả về vì nó được thực hiện trong C chỉ dẫn đến sự nhầm lẫn và thiết kế kém.
Smith

15
@Mister: Điều tôi đang nói là các trường hợp ngoại lệ được kiểm tra như được thực hiện trong Java, trong thực tế, giống như các giá trị trả về như trong C hơn là các 'ngoại lệ' truyền thống mà chúng ta có thể nhận ra từ C ++ và các ngôn ngữ tiền Java khác. Và IMO này thực sự dẫn đến sự nhầm lẫn và thiết kế kém.
bobince

9
Đồng ý rằng các thư viện tiêu chuẩn lạm dụng các ngoại lệ được kiểm tra chắc chắn đã thêm vào sự nhầm lẫn và hành vi bắt xấu. Và, thường thì đó chỉ là từ tài liệu kém, ví dụ như một phương pháp phá bỏ như ngắt kết nối () sẽ ném IOException khi "một số lỗi I / O khác xảy ra". Vâng, tôi đã ngắt kết nối! Tôi đang rò rỉ một tay cầm hoặc tài nguyên khác? Tôi có cần thử lại không? Không biết tại sao nó lại xảy ra, tôi không thể rút ra hành động tôi nên làm và vì vậy tôi phải đoán xem liệu tôi có nên nuốt nó, thử lại hay bảo lãnh không.
charstar

14
+1 cho "Giá trị trả về thay thế API". Cách thú vị để xem xét các ngoại lệ được kiểm tra.
Zsolt Török

75

Thay vì thử lại tất cả (nhiều) lý do chống lại các ngoại lệ được kiểm tra, tôi sẽ chỉ chọn một lý do. Tôi đã mất số lần tôi đã viết khối mã này:

try {
  // do stuff
} catch (AnnoyingcheckedException e) {
  throw new RuntimeException(e);
}

99% thời gian tôi không thể làm bất cứ điều gì về nó. Cuối cùng, các khối thực hiện bất kỳ việc dọn dẹp cần thiết nào (hoặc ít nhất là chúng nên).

Tôi cũng đã mất số lần tôi đã thấy điều này:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

Tại sao? Bởi vì ai đó đã phải đối phó với nó và lười biếng. Có sai không? Chắc chắn rồi. Nó có xảy ra không? Chắc chắn rồi. Nếu đây là một ngoại lệ không được kiểm soát thì sao? Ứng dụng sẽ chết (tốt nhất là nuốt một ngoại lệ).

Và sau đó chúng ta có mã gây khó chịu sử dụng các ngoại lệ như một hình thức kiểm soát luồng, giống như java.text.Format . Bzzzt. Sai lầm. Người dùng đặt "abc" vào trường số trên biểu mẫu không phải là ngoại lệ.

Ok, tôi đoán đó là ba lý do.


4
Nhưng nếu một ngoại lệ được nắm bắt chính xác, bạn có thể thông báo cho người dùng, thực hiện các tác vụ khác (đăng nhập?) Và thoát khỏi ứng dụng theo cách được kiểm soát. Tôi đồng ý rằng một số phần API có thể đã được thiết kế tốt hơn. Và đối với lý do lập trình viên lười biếng, tốt, tôi nghĩ là một lập trình viên, bạn chịu trách nhiệm 100% về mã của mình.
Smith

3
lưu ý rằng try-Catch-rethrow cho phép bạn chỉ định một thông báo - Tôi thường sử dụng nó để thêm thông tin về nội dung của các biến trạng thái. Một ví dụ thường gặp là IOExceptions để thêm perfectPathName () của tệp đang đề cập.
Thorbjørn Ravn Andersen

15
Tôi nghĩ rằng các IDE như Eclipse có rất nhiều điều đáng trách về số lần bạn đã thấy khối bắt trống. Thực sự, họ nên suy nghĩ lại theo mặc định.
artbristol

12
"99% thời gian tôi không thể làm bất cứ điều gì về nó" - sai, bạn có thể hiển thị cho người dùng một thông báo "Không thể kết nối với máy chủ" hoặc "Thiết bị IO bị lỗi", thay vì chỉ để ứng dụng bị sập do một chút trục trặc mạng. Cả hai ví dụ của bạn là nghệ thuật làm việc từ các lập trình viên xấu. Bạn nên tấn công các lập trình viên xấu và không được kiểm tra ngoại lệ. Nó giống như tôi tấn công insulin vì không giúp đỡ với bệnh tiểu đường của tôi khi tôi sử dụng nó như là salad.
AxiomaticNexus

2
@YasmaniLlanes Bạn không thể luôn luôn làm những điều này. Đôi khi bạn có một giao diện để tuân thủ. Và điều này đặc biệt đúng khi bạn thiết kế các API có thể bảo trì tốt vì bạn không thể bắt đầu ném các tác dụng phụ khắp mọi nơi. Cả điều đó, và sự phức tạp mà nó sẽ giới thiệu, sẽ cắn bạn nghiêm trọng trên diện rộng. Vì vậy, có, 99% thời gian, không có gì để làm về nó.
MasterMastic

50

Tôi biết đây là một câu hỏi cũ nhưng tôi đã dành một thời gian vật lộn với các ngoại lệ được kiểm tra và tôi có một cái gì đó để thêm. Xin hãy tha thứ cho tôi về chiều dài của nó!

Thịt bò chính của tôi với các ngoại lệ được kiểm tra là chúng phá hỏng tính đa hình. Không thể làm cho chúng chơi độc đáo với giao diện đa hình.

Sử dụng Listgiao diện Java tốt . Chúng tôi có các triển khai trong bộ nhớ chung như ArrayListLinkedList. Chúng tôi cũng có lớp khung xương AbstractListgiúp dễ dàng thiết kế các loại danh sách mới. Đối với danh sách chỉ đọc, chúng ta chỉ cần thực hiện hai phương thức: size()get(int index).

Lớp ví dụ này WidgetListđọc một số đối tượng có kích thước cố định Widget(không hiển thị) từ một tệp:

class WidgetList extends AbstractList<Widget> {
    private static final int SIZE_OF_WIDGET = 100;
    private final RandomAccessFile file;

    public WidgetList(RandomAccessFile file) {
        this.file = file;
    }

    @Override
    public int size() {
        return (int)(file.length() / SIZE_OF_WIDGET);
    }

    @Override
    public Widget get(int index) {
        file.seek((long)index * SIZE_OF_WIDGET);
        byte[] data = new byte[SIZE_OF_WIDGET];
        file.read(data);
        return new Widget(data);
    }
}

Bằng cách hiển thị các Widget bằng Listgiao diện quen thuộc , bạn có thể truy xuất các mục ( list.get(123)) hoặc lặp lại một danh sách ( for (Widget w : list) ...) mà không cần phải biết về WidgetListchính nó. Người ta có thể chuyển danh sách này cho bất kỳ phương thức tiêu chuẩn nào sử dụng danh sách chung hoặc bọc nó trong một Collections.synchronizedList. Mã sử ​​dụng nó không cần biết cũng không quan tâm liệu "Widgets" được tạo ra tại chỗ, đến từ một mảng hoặc được đọc từ tệp, cơ sở dữ liệu hoặc từ mạng hoặc từ chuyển tiếp không gian con trong tương lai. Nó vẫn sẽ hoạt động chính xác vì Listgiao diện được thực hiện chính xác.

Ngoại trừ nó không phải là. Lớp trên không biên dịch được vì các phương thức truy cập tệp có thể đưa ra IOExceptionmột ngoại lệ được kiểm tra mà bạn phải "bắt hoặc chỉ định". Bạn không thể chỉ định nó là bị ném - trình biên dịch sẽ không cho phép bạn vì điều đó sẽ vi phạm hợp đồng của Listgiao diện. Và không có cách nào hữu ích mà WidgetListchính nó có thể xử lý ngoại lệ (như tôi sẽ giải thích sau).

Rõ ràng điều duy nhất cần làm là bắt và rút lại các ngoại lệ được kiểm tra vì một số ngoại lệ không được kiểm tra:

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw new WidgetListException(e);
    }
}

public static class WidgetListException extends RuntimeException {
    public WidgetListException(Throwable cause) {
        super(cause);
    }
}

((Chỉnh sửa: Java 8 đã thêm một UncheckedIOExceptionlớp cho chính xác trường hợp này: để bắt và nghĩ lại IOExceptions qua các ranh giới phương thức đa hình. Loại chứng minh quan điểm của tôi!))

Vì vậy, các trường hợp ngoại lệ được kiểm tra chỉ đơn giản là không hoạt động trong các trường hợp như thế này. Bạn không thể ném chúng. Ditto cho một thông minh được Maphỗ trợ bởi cơ sở dữ liệu hoặc triển khai java.util.Randomkết nối với nguồn entropy lượng tử thông qua cổng COM. Ngay khi bạn cố gắng làm bất cứ điều gì mới lạ với việc thực hiện giao diện đa hình, khái niệm ngoại lệ được kiểm tra thất bại. Nhưng các trường hợp ngoại lệ được kiểm tra rất xảo quyệt đến mức chúng sẽ không khiến bạn yên tâm, bởi vì bạn vẫn phải nắm bắt và truy xuất lại bất kỳ phương thức cấp thấp nào, làm lộn xộn mã và làm lộn xộn dấu vết ngăn xếp.

Tôi thấy rằng Runnablegiao diện phổ biến thường được sao lưu vào góc này, nếu nó gọi một cái gì đó ném ngoại lệ được kiểm tra. Nó không thể ném ngoại lệ như vậy, vì vậy tất cả những gì có thể làm là làm lộn xộn mã bằng cách bắt và suy nghĩ lại như một RuntimeException.

Trên thực tế, bạn có thể đưa ra các ngoại lệ được kiểm tra không được khai báo nếu bạn dùng đến hack. JVM, trong thời gian chạy, không quan tâm đến các quy tắc ngoại lệ được kiểm tra, vì vậy chúng ta chỉ cần đánh lừa trình biên dịch. Cách dễ nhất để làm điều này là lạm dụng thuốc generic. Đây là phương thức của tôi cho nó (tên lớp được hiển thị bởi vì (trước Java 8) nó được yêu cầu trong cú pháp gọi cho phương thức chung):

class Util {
    /**
     * Throws any {@link Throwable} without needing to declare it in the
     * method's {@code throws} clause.
     * 
     * <p>When calling, it is suggested to prepend this method by the
     * {@code throw} keyword. This tells the compiler about the control flow,
     * about reachable and unreachable code. (For example, you don't need to
     * specify a method return value when throwing an exception.) To support
     * this, this method has a return type of {@link RuntimeException},
     * although it never returns anything.
     * 
     * @param t the {@code Throwable} to throw
     * @return nothing; this method never returns normally
     * @throws Throwable that was provided to the method
     * @throws NullPointerException if {@code t} is {@code null}
     */
    public static RuntimeException sneakyThrow(Throwable t) {
        return Util.<RuntimeException>sneakyThrow1(t);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> RuntimeException sneakyThrow1(
            Throwable t) throws T {
        throw (T)t;
    }
}

Tiếng hoan hô! Sử dụng điều này, chúng ta có thể ném một ngoại lệ được kiểm tra bất kỳ chiều sâu nào lên ngăn xếp mà không cần khai báo nó, mà không gói nó trong một RuntimeException, và không làm lộn xộn dấu vết ngăn xếp! Sử dụng lại ví dụ "WidgetList":

@Override
public int size() {
    try {
        return (int)(file.length() / SIZE_OF_WIDGET);
    } catch (IOException e) {
        throw sneakyThrow(e);
    }
}

Thật không may, sự xúc phạm cuối cùng của các ngoại lệ được kiểm tra là trình biên dịch từ chối cho phép bạn bắt một ngoại lệ được kiểm tra nếu, theo ý kiến ​​thiếu sót của nó, nó không thể bị ném. (Các trường hợp ngoại lệ không được kiểm tra không có quy tắc này.) Để bắt được ngoại lệ ném lén lút, chúng ta phải làm điều này:

try {
    ...
} catch (Throwable t) { // catch everything
    if (t instanceof IOException) {
        // handle it
        ...
    } else {
        // didn't want to catch this one; let it go
        throw t;
    }
}

Đó là một chút khó xử, nhưng về mặt tích cực, nó vẫn đơn giản hơn một chút so với mã để trích xuất một ngoại lệ được kiểm tra được gói trong a RuntimeException.

Hạnh phúc, throw t;tuyên bố này là hợp pháp ở đây, mặc dù loại đã tđược kiểm tra, nhờ vào một quy tắc được thêm vào trong Java 7 về việc lấy lại các ngoại lệ bị bắt.


Khi các ngoại lệ được kiểm tra đáp ứng đa hình, trường hợp ngược lại cũng là một vấn đề: khi một phương thức được xác định là có khả năng ném một ngoại lệ được kiểm tra, nhưng việc thực hiện bị ghi đè thì không. Ví dụ, tất cả các phương thức OutputStreamcủa lớp trừu tượng writeđều chỉ định throws IOException. ByteArrayOutputStreamlà một lớp con ghi vào một mảng trong bộ nhớ thay vì nguồn I / O thực. Các writephương thức được ghi đè của nó không thể gây ra IOExceptions, vì vậy chúng không có throwsmệnh đề và bạn có thể gọi chúng mà không phải lo lắng về yêu cầu bắt hoặc chỉ định.

Ngoại trừ không phải lúc nào. Giả sử Widgetcó một phương thức để lưu nó vào luồng:

public void writeTo(OutputStream out) throws IOException;

Tuyên bố phương pháp này để chấp nhận một đồng bằng OutputStreamlà điều nên làm, vì vậy nó có thể được sử dụng đa hình với tất cả các loại đầu ra: tệp, cơ sở dữ liệu, mạng, v.v. Và mảng trong bộ nhớ. Tuy nhiên, với một mảng trong bộ nhớ, có một yêu cầu giả để xử lý một ngoại lệ không thể thực sự xảy ra:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
    someWidget.writeTo(out);
} catch (IOException e) {
    // can't happen (although we shouldn't ignore it if it does)
    throw new RuntimeException(e);
}

Như thường lệ, các trường hợp ngoại lệ được kiểm tra cản trở. Nếu các biến của bạn được khai báo là một loại cơ sở có nhiều yêu cầu ngoại lệ kết thúc mở hơn, bạn phải thêm các trình xử lý cho các ngoại lệ đó ngay cả khi bạn biết chúng sẽ không xảy ra trong ứng dụng của bạn.

Nhưng chờ đã, ngoại lệ được kiểm tra thực sự rất khó chịu, thậm chí chúng sẽ không cho phép bạn làm điều ngược lại!Hãy tưởng tượng bạn đang bắt bất cứ IOExceptionném bởi writecác cuộc gọi trên OutputStream, nhưng bạn muốn thay đổi kiểu được khai báo của biến để một ByteArrayOutputStream, trình biên dịch sẽ trách móc bạn đã cố gắng bắt một ngoại lệ kiểm tra rằng nó nói không thể được ném.

Quy tắc đó gây ra một số vấn đề vô lý. Ví dụ, một trong ba writephương pháp OutputStreamkhông ghi đè bởi ByteArrayOutputStream. Cụ thể, write(byte[] data)là một phương thức tiện lợi ghi toàn bộ mảng bằng cách gọiwrite(byte[] data, int offset, int length) với độ lệch bằng 0 và độ dài của mảng. ByteArrayOutputStreamghi đè phương thức ba đối số nhưng kế thừa phương thức tiện lợi một đối số. Phương thức kế thừa thực hiện chính xác điều đúng, nhưng nó bao gồm một throwsmệnh đề không mong muốn . Đó có lẽ là một sự giám sát trong thiết kế ByteArrayOutputStream, nhưng họ không bao giờ có thể sửa nó vì nó sẽ phá vỡ tính tương thích nguồn với bất kỳ mã nào bắt ngoại lệ - ngoại lệ không bao giờ, không bao giờ và sẽ không bao giờ bị ném!

Quy tắc đó gây phiền nhiễu trong quá trình chỉnh sửa và gỡ lỗi. Ví dụ, đôi khi tôi sẽ nhận xét tạm thời một cuộc gọi phương thức và nếu nó có thể ném ngoại lệ được kiểm tra, trình biên dịch sẽ phàn nàn về sự tồn tại của các khối tryvà cục bộ catch. Vì vậy, tôi cũng phải bình luận những điều đó và bây giờ khi chỉnh sửa mã bên trong, IDE sẽ thụt lề sai vì{}được nhận xét. Trời ạ! Đó là một khiếu nại nhỏ nhưng có vẻ như điều duy nhất được kiểm tra ngoại lệ từng gây ra là gây rắc rối.


Tôi sắp xong rồi. Sự thất vọng cuối cùng của tôi với các ngoại lệ được kiểm tra là tại hầu hết các trang web cuộc gọi , bạn không thể làm gì hữu ích với chúng. Lý tưởng nhất là khi có sự cố xảy ra, chúng tôi sẽ có một trình xử lý dành riêng cho ứng dụng có thẩm quyền có thể thông báo cho người dùng về sự cố và / hoặc kết thúc hoặc thử lại thao tác khi thích hợp. Chỉ một người xử lý cao lên ngăn xếp có thể làm điều này bởi vì đó là người duy nhất biết mục tiêu tổng thể.

Thay vào đó, chúng tôi nhận được thành ngữ sau, tràn lan như là một cách để đóng trình biên dịch:

try {
    ...
} catch (SomeStupidExceptionOmgWhoCares e) {
    e.printStackTrace();
}

Trong GUI hoặc chương trình tự động, tin nhắn được in sẽ không được nhìn thấy. Tệ hơn, nó tiếp tục với phần còn lại của mã sau ngoại lệ. Là ngoại lệ không thực sự là một lỗi? Sau đó không in nó. Nếu không, một cái gì đó khác sẽ nổ tung trong giây lát, đến lúc đó đối tượng ngoại lệ ban đầu sẽ biến mất. Thành ngữ này không tốt hơn BASICOn Error Resume Next hay PHP error_reporting(0);.

Gọi một số loại lớp logger không tốt hơn nhiều:

try {
    ...
} catch (SomethingWeird e) {
    logger.log(e);
}

Đó chỉ là lười biếng như e.printStackTrace(); và vẫn tiếp tục với mã trong trạng thái không xác định. Thêm vào đó, sự lựa chọn của một hệ thống ghi nhật ký cụ thể hoặc trình xử lý khác là dành riêng cho ứng dụng, vì vậy điều này làm tổn thương việc sử dụng lại mã.

Nhưng chờ đã! Có một cách dễ dàng và phổ quát để tìm trình xử lý dành riêng cho ứng dụng. Nó cao hơn ngăn xếp cuộc gọi (hoặc nó được đặt làm trình xử lý ngoại lệ chưa được xử lý của Thread ). Vì vậy, ở hầu hết các nơi, tất cả những gì bạn cần làm là ném ngoại lệ lên cao hơn . Ví dụ,throw e; . Kiểm tra ngoại lệ chỉ nhận được trong cách.

Tôi chắc chắn rằng các trường hợp ngoại lệ được kiểm tra nghe có vẻ là một ý tưởng hay khi ngôn ngữ được thiết kế, nhưng trong thực tế, tôi thấy chúng đều gây phiền toái và không có lợi ích gì.


Đối với phương thức kích thước của bạn với WidgetList, tôi sẽ lưu trữ kích thước trong một biến và đặt nó trong hàm tạo. Các nhà xây dựng là miễn phí để ném một ngoại lệ. Điều này sẽ không hoạt động nếu tập tin thay đổi trong khi sử dụng WidgetList, điều này có thể sẽ tệ nếu nó xảy ra.
TofuBeer

2
Một sốStStExExOOmgWhoCares ai đó quan tâm đủ để ném nó. Vì vậy, hoặc nó không bao giờ nên được ném (thiết kế xấu) hoặc bạn thực sự nên xử lý nó. Điều tương tự cũng đúng với việc triển khai kém của lớp trước 1.0 (luồng đầu ra mảng byte) trong đó thiết kế không may là xấu.
TofuBeer

2
Thành ngữ thích hợp sẽ là một lệnh sẽ bắt bất kỳ trường hợp ngoại lệ được chỉ định nào được ném bởi các lệnh gọi chương trình con lồng nhau và chia lại chúng được gói trong a RuntimeException. Lưu ý rằng một thói quen có thể đồng thời được khai báo là throws IOExceptionvà cũng chỉ định rằng bất kỳ lệnh IOExceptionném nào từ một cuộc gọi lồng nhau sẽ được coi là bất ngờ và được bao bọc.
supercat

15
Tôi là một nhà phát triển C # chuyên nghiệp với một số kinh nghiệm Java đã tình cờ tìm thấy bài đăng này. Tôi bối rối về lý do tại sao bất cứ ai sẽ ủng hộ hành vi kỳ lạ này. Trong .NET nếu tôi muốn bắt một loại ngoại lệ cụ thể, tôi có thể bắt được. Nếu tôi muốn để nó bị ném lên ngăn xếp, không có gì để làm. Tôi ước Java không quá kỳ quặc. :)
aikeru

Liên quan đến "đôi khi tôi sẽ bình luận một cuộc gọi phương thức tạm thời" - Tôi đã học cách sử dụng if (false)cho việc này. Nó tránh được vấn đề mệnh đề ném và cảnh báo giúp tôi điều hướng trở lại nhanh hơn. +++ Điều đó nói rằng, tôi đồng ý với tất cả mọi thứ bạn đã viết. Các ngoại lệ được kiểm tra có một số giá trị, nhưng giá trị này không đáng kể khi so sánh với chi phí của chúng. Gần như luôn luôn họ cản đường.
maaartinus

45

Chà, không phải là về việc hiển thị một stacktrace hay âm thầm bị rơi. Đó là về khả năng giao tiếp lỗi giữa các lớp.

Vấn đề với các ngoại lệ được kiểm tra là họ khuyến khích mọi người nuốt các chi tiết quan trọng (cụ thể là lớp ngoại lệ). Nếu bạn chọn không nuốt chi tiết đó, thì bạn phải tiếp tục thêm các tuyên bố ném trên toàn bộ ứng dụng của mình. Điều này có nghĩa là 1) loại ngoại lệ mới sẽ ảnh hưởng đến nhiều chữ ký hàm và 2) bạn có thể bỏ lỡ một trường hợp cụ thể của ngoại lệ mà bạn thực sự muốn nắm bắt (giả sử bạn mở tệp phụ cho chức năng ghi dữ liệu vào tệp. Tệp phụ là tùy chọn, vì vậy bạn có thể bỏ qua lỗi của nó, nhưng vì chữ kýthrows IOException , nên dễ dàng bỏ qua điều này).

Tôi thực sự đang đối phó với tình huống này trong một ứng dụng. Chúng tôi đóng gói lại gần như các trường hợp ngoại lệ là AppSpecificException. Điều này làm cho chữ ký thực sự sạch sẽ và chúng tôi không phải lo lắng về việc nổ tungthrows chữ ký.

Tất nhiên, bây giờ chúng ta cần chuyên môn hóa việc xử lý lỗi ở các cấp cao hơn, thực hiện logic thử lại và như vậy. Tuy nhiên, mọi thứ đều là AppSpecificException, vì vậy chúng tôi không thể nói "Nếu ném IOException, hãy thử lại" hoặc "Nếu ClassNotFound bị ném, hãy hủy bỏ hoàn toàn". Chúng tôi không có cách đáng tin cậy để đến ngoại lệ thực sự bởi vì mọi thứ được đóng gói lại nhiều lần khi chúng chuyển giữa mã của chúng tôi và mã của bên thứ ba.

Đây là lý do tại sao tôi là một fan hâm mộ lớn của việc xử lý ngoại lệ trong python. Bạn chỉ có thể nắm bắt những điều bạn muốn và / hoặc có thể xử lý. Mọi thứ khác nổi lên như thể bạn tự mình lấy lại (mà bạn đã thực hiện bằng mọi cách).

Tôi đã tìm thấy, hết lần này đến lần khác và trong suốt dự án tôi đã đề cập, việc xử lý ngoại lệ đó rơi vào 3 loại:

  1. Bắt và xử lý một ngoại lệ cụ thể . Điều này là để thực hiện thử lại logic, ví dụ.
  2. Bắt và nghĩ lại các ngoại lệ khác . Tất cả những gì xảy ra ở đây thường là ghi nhật ký và thông thường là một thông điệp khá hay như "Không thể mở tên tệp $". Đây là những lỗi bạn không thể làm gì được; chỉ một cấp cao hơn biết đủ để xử lý nó.
  3. Bắt tất cả mọi thứ và hiển thị một thông báo lỗi. Đây thường là gốc của một bộ điều phối và tất cả những gì nó đảm bảo nó có thể truyền lỗi đến người gọi thông qua cơ chế không ngoại lệ (đối thoại bật lên, xử lý lỗi đối tượng Lỗi RPC, v.v.).

5
Bạn có thể đã tạo các lớp con cụ thể của AppSpecificException để cho phép tách trong khi vẫn giữ chữ ký phương thức đơn giản.
Thorbjørn Ravn Andersen

1
Ngoài ra, một bổ sung rất quan trọng cho mục 2, là nó cho phép bạn THÊM THÔNG TIN vào ngoại lệ bị bắt (ví dụ: bằng cách lồng trong RuntimeException). Sẽ tốt hơn nhiều nếu tên của tệp không được tìm thấy trong theo dõi ngăn xếp, hơn là ẩn sâu trong tệp nhật ký.
Thorbjørn Ravn Andersen

1
Về cơ bản lập luận của bạn là "Quản lý ngoại lệ là mệt mỏi vì vậy tôi không muốn giải quyết nó". Khi ngoại lệ nổi lên, nó mất đi ý nghĩa và thực tế bối cảnh là vô dụng. Là người thiết kế API, bạn nên làm rõ hợp đồng về những gì có thể xảy ra khi có sự cố xảy ra, nếu chương trình của tôi gặp sự cố vì tôi không được thông báo rằng ngoại lệ này có thể "nổi bong bóng" thì bạn, với tư cách là nhà thiết kế, đã thất bại và như kết quả của sự thất bại của bạn hệ thống của tôi không ổn định như nó có thể được.
Newtopian

4
Đó không phải là những gì tôi đang nói. Câu cuối cùng của bạn thực sự đồng ý với tôi. Nếu mọi thứ được gói trong AppSpecificException, thì nó sẽ không xuất hiện (và ý nghĩa / bối cảnh bị mất) và, vâng, ứng dụng API không được thông báo - đây chính xác là những gì xảy ra với các ngoại lệ được kiểm tra (vì chúng ở trong java) , bởi vì mọi người không muốn xử lý các hàm với nhiều throwskhai báo.
Richard Levasseur

6
@Newtopian - trường hợp ngoại lệ phần lớn chỉ có thể được xử lý ở cấp độ "doanh nghiệp" hoặc "yêu cầu". Nó có ý nghĩa để thất bại hoặc thử lại ở độ chi tiết lớn, không phải cho mọi thất bại tiềm năng nhỏ. Vì lý do này, thực tiễn tốt nhất xử lý ngoại lệ được tóm tắt là "ném sớm, bắt muộn". Các trường hợp ngoại lệ được kiểm tra làm cho việc quản lý độ tin cậy ở mức chính xác trở nên khó khăn hơn và khuyến khích số lượng lớn các khối bắt được mã hóa sai. lítatejava.com/exceptions/ từ
Thomas W

24

SNR

Đầu tiên, các ngoại lệ được kiểm tra làm giảm "tỷ lệ tín hiệu trên tạp âm" cho mã. Anders Hejlsberg cũng nói về lập trình mệnh lệnh và khai báo vốn là một khái niệm tương tự. Dù sao, hãy xem xét các đoạn mã sau:

Cập nhật giao diện người dùng từ luồng không phải UI trong Java:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

Cập nhật giao diện người dùng từ chủ đề không phải UI trong C #:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

Mà có vẻ rõ ràng hơn nhiều đối với tôi. Khi bạn bắt đầu làm nhiều hơn và nhiều công việc UI hơn trong các trường hợp ngoại lệ được kiểm tra, bắt đầu trở nên thực sự khó chịu và vô dụng.

Vượt ngục

Để triển khai ngay cả những triển khai cơ bản nhất, như giao diện Danh sách của Java, đã kiểm tra các ngoại lệ như một công cụ để thiết kế theo hợp đồng rơi xuống. Xem xét một danh sách được hỗ trợ bởi cơ sở dữ liệu hoặc hệ thống tập tin hoặc bất kỳ triển khai nào khác đưa ra một ngoại lệ được kiểm tra. Việc thực hiện duy nhất có thể là bắt ngoại lệ được kiểm tra và suy nghĩ lại như một ngoại lệ không được kiểm tra:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

Và bây giờ bạn phải hỏi điểm của tất cả các mã đó là gì? Các ngoại lệ được kiểm tra chỉ thêm tiếng ồn, ngoại lệ đã bị bắt nhưng không được xử lý và thiết kế theo hợp đồng (về các ngoại lệ được kiểm tra) đã bị hỏng.

Phần kết luận

  • Bắt ngoại lệ là khác nhau để xử lý chúng.
  • Đã kiểm tra ngoại lệ thêm tiếng ồn vào mã.
  • Xử lý ngoại lệ hoạt động tốt trong C # mà không có chúng.

Tôi viết blog về điều này trước đây .


3
Trong ví dụ, cả Java và C # chỉ để các ngoại lệ lan truyền mà không xử lý chúng (Java thông qua IllegalStateException). Sự khác biệt là bạn có thể muốn xử lý FileNotFoundException nhưng việc xử lý InvocationTargetException hoặc InterruptedException sẽ không hữu ích.
Luke Quinane

3
Và theo cách C #, làm thế nào để tôi biết rằng ngoại lệ I / O có thể xảy ra? Ngoài ra tôi sẽ không bao giờ ném ngoại lệ khỏi chạy ... Tôi xem xét việc lạm dụng xử lý ngoại lệ. Xin lỗi nhưng về phần mã của bạn, tôi chỉ có thể thấy mặt của bạn về nó.
TofuBeer

7
Chúng tôi đang đến đó :-) Vì vậy, với mỗi lần phát hành API mới, bạn phải thực hiện tất cả các cuộc gọi của mình và tìm kiếm bất kỳ ngoại lệ mới nào có thể xảy ra? Điều này có thể dễ dàng xảy ra với API nội bộ của công ty vì họ không phải lo lắng về khả năng tương thích ngược.
TofuBeer

3
Ý của bạn là giảm tỷ lệ tín hiệu trên tạp âm?
neo2862

3
@TofuBeer Không bị buộc phải cập nhật mã của bạn sau khi giao diện của API lót thay đổi có tốt không? Nếu bạn chỉ có các trường hợp ngoại lệ không được kiểm tra ở đó, bạn đã kết thúc với một chương trình bị hỏng / không đầy đủ mà không biết.
Francois Bourgeois

23

Artima đã xuất bản một cuộc phỏng vấn với một trong những kiến ​​trúc sư của .NET, Anders Hejlsberg, trong đó bao quát sâu sắc các lập luận chống lại các ngoại lệ được kiểm tra. Một người khai thác ngắn:

Mệnh đề ném, ít nhất là cách nó được triển khai trong Java, không nhất thiết buộc bạn phải xử lý các ngoại lệ, nhưng nếu bạn không xử lý chúng, nó buộc bạn phải thừa nhận chính xác những ngoại lệ nào có thể đi qua. Nó yêu cầu bạn phải bắt các ngoại lệ được khai báo hoặc đặt chúng trong mệnh đề ném của riêng bạn. Để giải quyết yêu cầu này, mọi người làm những điều lố bịch. Ví dụ, họ trang trí mọi phương thức bằng "ném Ngoại lệ". Điều đó chỉ hoàn toàn đánh bại tính năng, và bạn chỉ cần làm cho lập trình viên viết nhiều gunk gobbledy. Điều đó không giúp được ai.


2
Trong thực tế, kiến ​​trúc sư trưởng.
Bẫy

18
Tôi đã đọc được điều đó, với tôi lập luận của anh ấy sôi nổi lên rằng "có những lập trình viên tồi ở ngoài kia".
TofuBeer

6
TofuBeer, hoàn toàn không. Toàn bộ vấn đề là nhiều lần bạn không biết phải làm gì với Ngoại lệ mà phương thức được gọi đưa ra và trường hợp bạn thực sự quan tâm thậm chí không được đề cập. Bạn mở một tệp, bạn nhận được một Ngoại lệ IO, chẳng hạn ... đó không phải là vấn đề của tôi, vì vậy tôi ném nó lên. Nhưng phương thức gọi cấp cao nhất sẽ chỉ muốn dừng xử lý và thông báo cho người dùng rằng có một vấn đề không xác định. Ngoại lệ được kiểm tra không giúp được gì cả. Đó là một trong một triệu điều kỳ lạ có thể xảy ra.
Dan Rosenstark

4
@yar, nếu bạn không thích ngoại lệ được kiểm tra, thì hãy thực hiện "ném RuntimeException mới (" chúng tôi không mong đợi điều này khi thực hiện Foo.bar () ", e)" và được thực hiện với nó.
Thorbjørn Ravn Andersen

4
@ ThorbjørnRavnAndersen: Một điểm yếu về thiết kế cơ bản trong Java, mà .net không may sao chép, là nó sử dụng loại ngoại lệ vừa là phương tiện chính để quyết định xem có nên hành động hay không và là phương tiện chính để chỉ ra loại chung của sự vật Điều đó đã sai, trong thực tế, hai vấn đề chủ yếu là trực giao. Vấn đề không phải là những gì đã sai, mà là những đối tượng trạng thái nào. Ngoài ra, cả .net và Java đều mặc định cho rằng hành động và giải quyết một ngoại lệ nói chung là giống nhau, trong khi thực tế chúng thường khác nhau.
supercat

20

Ban đầu tôi đồng ý với bạn, vì tôi luôn ủng hộ các ngoại lệ được kiểm tra và bắt đầu suy nghĩ về lý do tại sao tôi không thích kiểm tra ngoại lệ trong .Net. Nhưng sau đó tôi nhận ra rằng tôi không còn nguyên vẹn như ngoại lệ được kiểm tra.

Để trả lời câu hỏi của bạn, vâng, tôi thích các chương trình của tôi hiển thị dấu vết ngăn xếp, tốt nhất là những cái thực sự xấu xí. Tôi muốn ứng dụng bùng nổ thành một đống khủng khiếp các thông báo lỗi xấu nhất mà bạn có thể muốn xem.

Và lý do là bởi vì, nếu nó làm điều đó, tôi phải sửa nó, và tôi phải sửa nó ngay. Tôi muốn biết ngay rằng có một vấn đề.

Bao nhiêu lần bạn thực sự xử lý các trường hợp ngoại lệ? Tôi không nói về việc bắt ngoại lệ - Tôi đang nói về việc xử lý chúng? Thật quá dễ dàng để viết như sau:

try {
  thirdPartyMethod();
} catch(TPException e) {
  // this should never happen
}

Và tôi biết bạn có thể nói rằng đó là một thực tiễn tồi, và 'câu trả lời' là làm điều gì đó ngoại lệ (hãy để tôi đoán, đăng nhập nó?), Nhưng trong Thế giới thực (tm), hầu hết các lập trình viên chỉ không làm nó

Vì vậy, vâng, tôi không muốn bắt ngoại lệ nếu tôi không phải làm như vậy và tôi muốn chương trình của mình bùng nổ một cách ngoạn mục khi tôi làm hỏng việc. Âm thầm thất bại là kết quả tồi tệ nhất có thể.


Java khuyến khích bạn làm điều này, để bạn không phải thêm mọi loại ngoại lệ vào mỗi chữ ký phương thức.
yfeldblum

17
Buồn cười .. kể từ khi tôi chấp nhận kiểm tra ngoại lệ một cách chính xác và sử dụng chúng một cách thích hợp, các chương trình của tôi đã ngừng nổ tung trong đống hơi nước lớn trong sự không hài lòng của khách hàng. Nếu trong khi phát triển bạn có dấu vết ngăn xếp xấu xấu lớn thì khách hàng cũng bị ràng buộc để có được chúng. Bạn muốn nhìn thấy khuôn mặt của mình khi nhìn thấy ArrayIndexOutOfBoundException với dấu vết ngăn xếp cao trên hệ thống bị hỏng thay vì một thông báo khay nhỏ nói rằng cấu hình màu cho nút XYZ không thể được phân tích cú pháp để thay vào đó phần mềm được sử dụng một cách vui vẻ cùng
Newtopian

1
Có lẽ những gì Java cần là một tuyên bố "cantHandle" sẽ xác định rằng một phương thức hoặc khối mã thử / bắt không được chuẩn bị để xử lý một ngoại lệ cụ thể xảy ra trong nó và bất kỳ ngoại lệ nào xảy ra thông qua các phương tiện khác ngoài một cách rõ ràng ném trong phương thức đó (trái ngược với phương thức được gọi) sẽ được tự động gói và truy xuất lại trong RuntimeException. IMHO, ngoại lệ được kiểm tra sẽ hiếm khi truyền lên ngăn xếp cuộc gọi mà không được gói.
supercat

3
@Newtopian - Tôi viết máy chủ và phần mềm có độ tin cậy cao và đã làm như vậy trong 25 năm. Các chương trình của tôi chưa bao giờ nổ ra, và tôi làm việc với các hệ thống tài chính & quân sự có tính sẵn sàng cao, thử lại và kết nối lại. Tôi có một cơ sở khách quan tuyệt đối để thích ngoại lệ thời gian chạy. Các trường hợp ngoại lệ được kiểm tra làm cho việc thực hiện đúng "ném sớm, bắt muộn" trở nên khó khăn hơn. Độ tin cậy và xử lý lỗi chính xác ở cấp độ "doanh nghiệp", "kết nối" hoặc "yêu cầu". (Hoặc đôi khi khi phân tích dữ liệu). Kiểm tra ngoại lệ có được trong cách làm đúng.
Thomas W

1
Các trường hợp ngoại lệ mà bạn đang nói ở đây RuntimeExceptionsthực sự là bạn không cần phải nắm bắt và tôi đồng ý rằng bạn nên để chương trình nổ tung. Các ngoại lệ bạn nên luôn luôn nắm bắt và xử lý là các ngoại lệ được kiểm tra như thế nào IOException. Nếu bạn nhận được một IOException, không có gì để sửa trong mã của bạn; chương trình của bạn không nên nổ tung chỉ vì có một trục trặc mạng.
AxiomaticNexus

20

Bài viết Các ngoại lệ Java hiệu quả giải thích độc đáo khi nào nên sử dụng không được kiểm tra và khi nào nên sử dụng các ngoại lệ được kiểm tra. Dưới đây là một số trích dẫn từ bài viết đó để làm nổi bật những điểm chính:

Tình huống bất ngờ : Một điều kiện dự kiến ​​đòi hỏi một phản ứng thay thế từ một phương pháp có thể được thể hiện theo mục đích dự định của phương pháp. Người gọi phương thức mong đợi các loại điều kiện này và có một chiến lược để đối phó với chúng.

Lỗi: Một điều kiện không có kế hoạch ngăn cản một phương thức đạt được mục đích dự định của nó mà không thể được mô tả mà không cần tham khảo đến việc thực hiện bên trong của phương thức.

(SO không cho phép các bảng, vì vậy bạn có thể muốn đọc phần sau từ trang gốc ...)

Dự phòng

  • Được coi là: Một phần của thiết kế
  • Dự kiến ​​sẽ xảy ra: Thường xuyên nhưng hiếm khi
  • Ai quan tâm đến nó: Mã ngược dòng gọi phương thức
  • Ví dụ: Chế độ hoàn trả thay thế
  • Ánh xạ tốt nhất: Một ngoại lệ được kiểm tra

Lỗi

  • Được coi là: Một bất ngờ khó chịu
  • Dự kiến ​​sẽ xảy ra: Không bao giờ
  • Ai quan tâm đến nó: Những người cần khắc phục vấn đề
  • Ví dụ: Lỗi lập trình, trục trặc phần cứng, lỗi cấu hình, tệp bị thiếu, máy chủ không khả dụng
  • Ánh xạ tốt nhất: Một ngoại lệ không được kiểm tra

Tôi biết khi nào nên sử dụng chúng, tôi muốn biết lý do tại sao những người không làm theo lời khuyên đó ... đừng làm theo lời khuyên đó :-)
TofuBeer

Lỗi lập trình là gì và làm thế nào để phân biệt với lỗi sử dụng ? Đây có phải là lỗi lập trình nếu người dùng chuyển các đối số sai cho chương trình không? Theo quan điểm Java, nó có thể không có lỗi lập trình nhưng theo quan điểm của shell script thì đó là lỗi lập trình. Vì vậy, các đối số không hợp lệ là args[]gì? Họ là một tình huống ngẫu nhiên hay một lỗi?
thúc

3
@TofuBeer - Bởi vì các nhà thiết kế thư viện Java, đã chọn đặt tất cả các loại lỗi cấp thấp không thể phục hồi làm các ngoại lệ được kiểm tra khi chúng rõ ràng không được kiểm tra . Ví dụ, FileNotFound là IOException cần được kiểm tra. Đối với JDBC với - chỉ kết nối với cơ sở dữ liệu, hợp lý có thể được coi là một bất ngờ . Tất cả các tham số SQLEx khác phải là thất bại và không được kiểm tra. Việc xử lý lỗi phải chính xác ở cấp độ "kinh doanh" hoặc "yêu cầu" - xem phần thực hành tốt nhất "ném sớm, bắt muộn". Các ngoại lệ được kiểm tra là một rào cản cho điều đó.
Thomas W

1
Có một lỗ hổng lớn trong cuộc tranh luận của bạn. "Dự phòng" KHÔNG được xử lý thông qua các trường hợp ngoại lệ, nhưng thông qua các giá trị trả về mã doanh nghiệp và phương thức. Các trường hợp ngoại lệ là, như từ đã nói, các tình huống NGOẠI TRỪ, do đó là Lỗi.
Matteo Mosca

@MatteoMosca Mã trả về lỗi có xu hướng bị bỏ qua và điều đó đủ để loại bỏ chúng. Trên thực tế, bất cứ điều gì bất thường đôi khi chỉ có thể được xử lý ở đâu đó trong ngăn xếp và đó là trường hợp sử dụng cho các trường hợp ngoại lệ. Tôi có thể tưởng tượng một cái gì đó như File#openInputStreamtrở về Either<InputStream, Problem>- nếu đó là ý bạn, thì chúng tôi có thể đồng ý.
maaartinus

19

Nói ngắn gọn:

Ngoại lệ là một câu hỏi thiết kế API. -- Không nhiều không ít.

Đối số cho các ngoại lệ được kiểm tra:

Để hiểu tại sao các ngoại lệ được kiểm tra có thể không phải là điều tốt, chúng ta hãy chuyển câu hỏi và hỏi: Khi nào hoặc tại sao các ngoại lệ được kiểm tra lại hấp dẫn, tức là tại sao bạn muốn trình biên dịch thực thi khai báo ngoại lệ?

Câu trả lời rất rõ ràng: Đôi khi bạn cần phải bắt một ngoại lệ và điều đó chỉ có thể nếu mã được gọi cung cấp một lớp ngoại lệ cụ thể cho lỗi mà bạn quan tâm.

Do đó, đối số cho các ngoại lệ được kiểm tra là trình biên dịch buộc các lập trình viên phải khai báo các ngoại lệ nào được đưa ra và hy vọng lập trình viên cũng sẽ ghi lại các lớp ngoại lệ cụ thể và các lỗi gây ra chúng.

Trong thực tế, bao giờ quá thường xuyên một gói com.acmechỉ ném một AcmeExceptionlớp thay vì các lớp con cụ thể. Người gọi sau đó cần xử lý, khai báo hoặc báo hiệu lại AcmeExceptions, nhưng vẫn không thể chắc chắn liệu có AcmeFileNotFoundErrorxảy ra hay không AcmePermissionDeniedError.

Vì vậy, nếu bạn chỉ quan tâm đến một AcmeFileNotFoundError, giải pháp là gửi yêu cầu tính năng với các lập trình viên ACME và bảo họ thực hiện, khai báo và ghi lại lớp con đó AcmeException.

Vậy tại sao phải bận tâm?

Do đó, ngay cả với các ngoại lệ được kiểm tra, trình biên dịch không thể buộc các lập trình viên ném các ngoại lệ hữu ích . Đây vẫn chỉ là một câu hỏi về chất lượng của API.

Do đó, các ngôn ngữ không có ngoại lệ được kiểm tra thường không tệ hơn nhiều. Các lập trình viên có thể bị cám dỗ ném các trường hợp không đặc biệt của một Errorlớp chung chứ không phải là một AcmeException, nhưng nếu họ quan tâm đến tất cả về chất lượng API của họ, họ sẽ học cách giới thiệu AcmeFileNotFoundErrorsau cùng.

Nhìn chung, đặc điểm kỹ thuật và tài liệu về các trường hợp ngoại lệ không khác nhiều so với đặc điểm kỹ thuật và tài liệu về các phương pháp thông thường. Những câu hỏi đó cũng là một câu hỏi thiết kế API và nếu lập trình viên quên thực hiện hoặc xuất một tính năng hữu ích, API cần được cải thiện để bạn có thể làm việc với nó một cách hữu ích.

Nếu bạn theo dòng lý luận này, rõ ràng là "rắc rối" của việc khai báo, bắt và ném lại các ngoại lệ rất phổ biến trong các ngôn ngữ như Java thường thêm ít giá trị.

Cũng cần lưu ý rằng máy ảo Java không có ngoại lệ được kiểm tra - chỉ có trình biên dịch Java kiểm tra chúng và các tệp lớp có khai báo ngoại lệ thay đổi là tương thích trong thời gian chạy. Bảo mật Java VM không được cải thiện bởi các ngoại lệ được kiểm tra, chỉ có kiểu mã hóa.


4
Lập luận của bạn lập luận chống lại chính nó. Nếu "đôi khi bạn cần bắt ngoại lệ" và chất lượng API thường kém, không có ngoại lệ được kiểm tra, bạn sẽ không biết liệu nhà thiết kế có bỏ qua tài liệu rằng một phương thức nhất định sẽ ném ngoại lệ cần phải bắt hay không. Kết hợp với ném AcmeExceptionchứ không phải AcmeFileNotFoundErrorvà may mắn tìm ra những gì bạn đã làm sai và nơi bạn cần để bắt nó. Các ngoại lệ được kiểm tra cung cấp cho các lập trình viên một mô-đun bảo vệ chống lại thiết kế API xấu.
Eva

1
Thiết kế thư viện Java đã phạm sai lầm nghiêm trọng. 'Các trường hợp ngoại lệ được kiểm tra' là cho các trường hợp dự đoán và có thể phục hồi - chẳng hạn như không tìm thấy tệp, không kết nối được. Chúng không bao giờ có nghĩa hoặc phù hợp cho thất bại hệ thống cấp thấp. Sẽ rất ổn nếu buộc mở một tệp để kiểm tra, nhưng không có thử lại hoặc khôi phục hợp lý cho việc không viết một byte / thực hiện truy vấn SQL, v.v. Thử lại hoặc khôi phục được xử lý chính xác theo yêu cầu "doanh nghiệp" hoặc " "Cấp độ, mà kiểm tra ngoại lệ làm cho vô cùng khó khăn. lítatejava.com/exceptions/ từ
Thomas W

17

Tôi đã làm việc với một số nhà phát triển trong ba năm qua trong các ứng dụng tương đối phức tạp. Chúng tôi có một cơ sở mã sử dụng Ngoại lệ được kiểm tra khá thường xuyên với việc xử lý lỗi thích hợp và một số khác thì không.

Cho đến nay, tôi thấy nó dễ dàng hơn để làm việc với cơ sở mã với Ngoại lệ được kiểm tra. Khi tôi đang sử dụng API của người khác, thật tuyệt khi tôi có thể thấy chính xác loại điều kiện lỗi nào tôi có thể gặp khi tôi gọi mã và xử lý chúng đúng cách, bằng cách đăng nhập, hiển thị hoặc bỏ qua (Có, có trường hợp hợp lệ để bỏ qua các trường hợp ngoại lệ, chẳng hạn như triển khai ClassLoader). Điều đó mang lại cho mã tôi đang viết một cơ hội để phục hồi. Tất cả các ngoại lệ thời gian chạy tôi truyền lên cho đến khi chúng được lưu trữ và xử lý với một số mã xử lý lỗi chung. Khi tôi tìm thấy một ngoại lệ được kiểm tra mà tôi không thực sự muốn xử lý ở mức cụ thể hoặc tôi xem xét lỗi logic lập trình, sau đó tôi bọc nó thành RuntimeException và để nó nổi lên. Không bao giờ, không bao giờ nuốt một ngoại lệ mà không có lý do chính đáng (và lý do chính đáng để làm điều này là khá khan hiếm)

Khi tôi làm việc với cơ sở mã không có ngoại lệ được kiểm tra, điều đó khiến tôi khó hiểu hơn một chút về việc tôi có thể mong đợi gì khi gọi hàm, điều này có thể phá vỡ một số thứ khủng khiếp.

Tất nhiên đây là vấn đề ưu tiên và kỹ năng của nhà phát triển. Cả hai cách lập trình và xử lý lỗi đều có thể hiệu quả như nhau (hoặc không hiệu quả), vì vậy tôi sẽ không nói rằng có Cách duy nhất.

Nói chung, tôi thấy làm việc với Kiểm tra ngoại lệ dễ dàng hơn, đặc biệt trong các dự án lớn có nhiều nhà phát triển.


6
I làm gì để. Đối với tôi họ là một phần thiết yếu của hợp đồng. Không cần phải đi vào chi tiết trong tài liệu API, tôi có thể nhanh chóng biết được các tình huống lỗi dễ xảy ra nhất.
Wayne Hartman

2
Đồng ý. Tôi đã trải nghiệm sự cần thiết của các ngoại lệ được kiểm tra trong .Net một lần khi tôi cố gắng thực hiện các cuộc gọi mạng. Biết rằng một trục trặc mạng có thể xảy ra bất cứ lúc nào, tôi đã phải đọc qua toàn bộ tài liệu của API để tìm ra ngoại lệ nào mà tôi cần phải nắm bắt cụ thể cho kịch bản đó. Nếu C # đã kiểm tra ngoại lệ, tôi sẽ biết ngay lập tức. Các nhà phát triển C # khác có thể sẽ chỉ để ứng dụng gặp sự cố do lỗi mạng đơn giản.
AxiomaticNexus

13

Danh mục ngoại lệ

Khi nói về các trường hợp ngoại lệ, tôi luôn đề cập đến bài viết blog ngoại lệ Vexing của Eric Lippert . Ông đặt ngoại lệ vào các loại này:

  • Gây tử vong - Những ngoại lệ này không phảilỗi của bạn : sau đó bạn không thể ngăn chặn và bạn không thể xử lý chúng một cách hợp lý. Ví dụ, OutOfMemoryErrorhoặc ThreadAbortException.
  • Boneheaded - Những ngoại lệ này là lỗi của bạn : bạn nên ngăn chặn chúng và chúng đại diện cho các lỗi trong mã của bạn. Ví dụ ArrayIndexOutOfBoundsException, NullPointerExceptionhoặc bất kỳ IllegalArgumentException.
  • Bực mình - Những ngoại lệ này không phảingoại lệ , không phải lỗi của bạn, bạn không thể ngăn chặn chúng, nhưng bạn sẽ phải đối phó với chúng. Chúng thường là kết quả của một quyết định thiết kế không may, chẳng hạn như ném NumberFormatExceptiontừ Integer.parseIntthay vì cung cấp một Integer.tryParseIntphương thức trả về sai boolean khi thất bại phân tích cú pháp.
  • Ngoại sinh - Những ngoại lệ này thường là ngoại lệ , không phải lỗi của bạn, bạn không thể (một cách hợp lý) ngăn chặn chúng, nhưng bạn phải xử lý chúng . Ví dụ , FileNotFoundException.

Một người dùng API:

  • không được xử lý các trường hợp ngoại lệ gây tử vong hoặc đau đầu .
  • nên xử lý vexing trường hợp ngoại lệ, nhưng họ không nên xảy ra trong một API lý tưởng.
  • phải xử lý các ngoại lệ ngoại sinh .

Đã kiểm tra ngoại lệ

Việc người dùng API phải xử lý một ngoại lệ cụ thể là một phần trong hợp đồng của phương thức giữa người gọi và callee. Hợp đồng chỉ định, trong số những điều khác: số lượng và loại đối số mà callee mong đợi, loại giá trị trả về mà người gọi có thể mong đợi và các trường hợp ngoại lệ mà người gọi dự kiến ​​sẽ xử lý .

Kể từ vexing trường hợp ngoại lệ không nên tồn tại trong một API, chỉ có những ngoại sinh ngoại lệ phải được kiểm tra trường hợp ngoại lệ là một phần của hợp đồng của phương pháp. Tương đối ít ngoại lệ là ngoại sinh , do đó, bất kỳ API nào cũng nên có tương đối ít ngoại lệ được kiểm tra.

Một ngoại lệ được kiểm tra là một ngoại lệ phải được xử lý . Xử lý một ngoại lệ có thể đơn giản như nuốt nó. Đó! Ngoại lệ được xử lý. Giai đoạn = Stage. Nếu nhà phát triển muốn xử lý theo cách đó, tốt thôi. Nhưng anh ta không thể bỏ qua ngoại lệ, và đã được cảnh báo.

Sự cố API

Nhưng bất kỳ API đã kiểm tra gây nhiều tranh cãigây tử vong trường hợp ngoại lệ (ví dụ như JCL) sẽ đặt căng thẳng không cần thiết trên người sử dụng API. Các trường hợp ngoại lệ như vậy phải được xử lý, nhưng ngoại lệ là phổ biến đến mức nó không phải là ngoại lệ ở nơi đầu tiên, hoặc không thể làm gì khi xử lý nó. Và điều này khiến các nhà phát triển Java ghét các ngoại lệ được kiểm tra.

Ngoài ra, nhiều API không có hệ thống phân cấp lớp ngoại lệ phù hợp, khiến tất cả các loại nguyên nhân ngoại lệ không ngoại sinh được biểu thị bằng một lớp ngoại lệ được kiểm tra duy nhất (ví dụ IOException). Và điều này cũng khiến các nhà phát triển Java ghét các ngoại lệ được kiểm tra.

Phần kết luận

Các ngoại lệ ngoại sinh là những trường hợp không phải là lỗi của bạn, không thể ngăn chặn được và cần được xử lý. Chúng tạo thành một tập hợp nhỏ của tất cả các ngoại lệ có thể bị ném. API chỉ nên kiểm tra ngoại lệ ngoại sinh và tất cả các ngoại lệ khác không được chọn. Điều này sẽ tạo ra các API tốt hơn, giảm bớt sự căng thẳng cho người dùng API và do đó giảm nhu cầu bắt tất cả, nuốt hoặc nghĩ lại các ngoại lệ không được kiểm tra.

Vì vậy, đừng ghét Java và các ngoại lệ được kiểm tra của nó. Thay vào đó, ghét các API lạm dụng các ngoại lệ được kiểm tra.


Và lạm dụng chúng bằng cách không có một hệ thống phân cấp.
TofuBeer

1
FileNotFound và thiết lập kết nối JDBC / mạng là các trường hợp dự phòng và chính xác để được kiểm tra ngoại lệ, vì đây là những dự đoán có thể dự đoán được và (có thể) có thể phục hồi. Hầu hết các IOExceptions, SQLExceptions, RemoteException, v.v. đều là những thất bại không thể đoán trước và không thể phục hồi , và đáng lẽ phải là ngoại lệ thời gian chạy. Do thiết kế thư viện Java nhầm, tất cả chúng ta đã bị mắc phải lỗi này và hiện chủ yếu sử dụng Spring & Hibernate (người đã thiết kế đúng).
Thomas W

Bạn thường nên xử lý các trường hợp ngoại lệ, mặc dù bạn có thể không muốn gọi nó là "xử lý". Ví dụ, trong một máy chủ web, tôi đăng nhập chúng và hiển thị 500 cho người dùng. Vì ngoại lệ là bất ngờ, có tất cả những gì tôi có thể làm trước khi sửa lỗi.
maaartinus

6

Ok ... Các trường hợp ngoại lệ được kiểm tra không lý tưởng và có một số cảnh báo nhưng chúng phục vụ một mục đích. Khi tạo API, có những trường hợp lỗi cụ thể là hợp đồng của API này. Khi trong bối cảnh của một ngôn ngữ được gõ tĩnh mạnh như Java nếu người ta không sử dụng các ngoại lệ được kiểm tra thì người ta phải dựa vào tài liệu và quy ước đặc biệt để truyền đạt khả năng lỗi. Làm như vậy sẽ loại bỏ tất cả lợi ích mà trình biên dịch có thể mang lại khi xử lý lỗi và bạn hoàn toàn để lại thiện chí cho các lập trình viên.

Vì vậy, người ta loại bỏ ngoại lệ Đã kiểm tra, chẳng hạn như được thực hiện trong C #, làm thế nào để một người có thể truyền đạt về mặt lập trình và cấu trúc khả năng xảy ra lỗi? Làm thế nào để thông báo mã khách hàng rằng những lỗi như vậy và có thể xảy ra và phải được xử lý?

Tôi nghe thấy tất cả các loại kinh hoàng khi xử lý các ngoại lệ được kiểm tra, chúng bị lạm dụng điều này là chắc chắn nhưng vì vậy là các ngoại lệ không được kiểm soát. Tôi nói hãy đợi vài năm khi các API được xếp chồng lên nhau nhiều lớp và bạn sẽ cầu xin sự trở lại của một loại ý nghĩa có cấu trúc nào đó để truyền đạt những thất bại.

Hãy xem trường hợp khi ngoại lệ được ném ở đâu đó ở dưới cùng của các lớp API và chỉ nổi lên vì không ai biết có thể xảy ra lỗi này, mặc dù đây là một loại lỗi rất hợp lý khi mã cuộc gọi đã ném nó (ví dụ FileNotFoundException trái ngược với VogonsTrashingEarthExcept ... trong trường hợp đó sẽ không có vấn đề gì nếu chúng tôi xử lý hay không vì không còn gì để xử lý).

Nhiều người lập luận rằng việc không thể tải tập tin gần như luôn luôn là kết thúc của quá trình và nó phải chết một cái chết kinh hoàng và đau đớn. Vì vậy, vâng .. chắc chắn ... ok .. bạn xây dựng API cho một cái gì đó và nó tải tệp tại một số điểm ... Tôi là người dùng API đã nói chỉ có thể trả lời ... "Bạn là ai mà quyết định khi nào chương trình sẽ sụp đổ! " Chắc chắn Đưa ra lựa chọn trong đó các trường hợp ngoại lệ bị ngấu nghiến và không để lại dấu vết hoặc EletroFlabbingChunkFluxManifoldChuggingException với một dấu vết ngăn xếp sâu hơn rãnh Marianna, tôi sẽ lấy cái sau mà không cần phải đắn đo, ? Chúng ta không thể ở một nơi nào đó ở giữa, nơi mà ngoại lệ sẽ được thu lại và bao bọc mỗi khi nó đi qua một mức độ trừu tượng mới để nó thực sự có ý nghĩa gì đó?

Cuối cùng, hầu hết các lập luận tôi thấy là "Tôi không muốn đối phó với các ngoại lệ, nhiều người không muốn xử lý các ngoại lệ. Các ngoại lệ được kiểm tra buộc tôi phải xử lý chúng vì vậy tôi ghét ngoại lệ được kiểm tra" đưa nó xuống vực sâu của địa ngục goto chỉ là ngớ ngẩn và thiếu sự bình tĩnh và tầm nhìn.

Nếu chúng ta loại bỏ ngoại lệ được kiểm tra, chúng ta cũng có thể loại bỏ kiểu trả về cho các hàm và luôn trả về biến "anytype" ... Điều đó sẽ làm cho cuộc sống đơn giản hơn rất nhiều bây giờ phải không?


2
Các ngoại lệ được kiểm tra sẽ hữu ích nếu có một phương tiện khai báo cho biết không có cuộc gọi phương thức nào trong một khối được dự kiến ​​sẽ ném một số (hoặc bất kỳ) ngoại lệ nào được kiểm tra và bất kỳ ngoại lệ nào như vậy sẽ tự động được gói và trả lại. Chúng thậm chí còn hữu ích hơn nếu các lệnh gọi đến các phương thức được tuyên bố là ném các ngoại lệ được kiểm tra đổi lấy tốc độ cuộc gọi / trả lại cho tốc độ xử lý ngoại lệ (để các ngoại lệ dự kiến ​​có thể được xử lý gần như nhanh như luồng chương trình bình thường). Không có tình huống hiện nay áp dụng, mặc dù.
supercat

6

Thật vậy, các trường hợp ngoại lệ được kiểm tra một mặt làm tăng tính mạnh mẽ và chính xác của chương trình của bạn (bạn buộc phải đưa ra tuyên bố chính xác về giao diện của mình - các ngoại lệ mà một phương thức ném về cơ bản là một kiểu trả về đặc biệt). Mặt khác, bạn phải đối mặt với vấn đề, vì ngoại lệ "bong bóng", rất thường xuyên bạn cần thay đổi rất nhiều phương thức (tất cả người gọi và người gọi của người gọi, v.v.) khi bạn thay đổi ngoại lệ phương pháp ném.

Các ngoại lệ được kiểm tra trong Java không giải quyết được vấn đề sau; C # và VB.NET vứt em bé bằng nước tắm.

Một cách tiếp cận tốt đẹp đi đường giữa được mô tả trong bài báo OOPSLA 2005 này (hoặc báo cáo kỹ thuật liên quan .)

Nói tóm lại, nó cho phép bạn nói : method g(x) throws like f(x), có nghĩa là g ném tất cả các ngoại lệ f ném. Voila, kiểm tra ngoại lệ mà không có vấn đề thay đổi tầng.

Mặc dù đây là một bài viết học thuật, tôi khuyến khích bạn đọc (một phần) nó, vì nó là một công việc tốt để giải thích những lợi ích và nhược điểm của các ngoại lệ được kiểm tra là gì.


5

Đây không phải là một đối số chống lại khái niệm thuần túy của các ngoại lệ được kiểm tra, nhưng hệ thống phân cấp lớp mà Java sử dụng cho chúng là một chương trình kỳ dị. Chúng tôi luôn gọi những thứ đơn giản là "ngoại lệ" - điều này đúng, bởi vì đặc tả ngôn ngữ cũng gọi chúng như vậy - nhưng làm thế nào một ngoại lệ được đặt tên và thể hiện trong hệ thống loại?

Do lớp Exceptionai tưởng tượng? À không, bởi vì Exceptions là ngoại lệ, và ngoại lệ tương tự là Exceptions, ngoại trừ những ngoại lệ đó không phải Exception là s, bởi vì những ngoại lệ khác thực sự Errorlà s, là loại ngoại lệ khác, một loại ngoại lệ đặc biệt không bao giờ xảy ra ngoại trừ khi có, và điều mà bạn không bao giờ nên bắt ngoại trừ đôi khi bạn phải làm. Ngoại trừ đó không phải là tất cả bởi vì bạn cũng có thể xác định các ngoại lệ khác không phải là Exceptions hay Errors mà chỉ là Throwablengoại lệ.

Cái nào trong số này là ngoại lệ "được kiểm tra"? Throwables được kiểm tra ngoại lệ, ngoại trừ nếu chúng cũng Errorlà ngoại lệ không được kiểm tra, và sau đó có Exceptions, cũng Throwablelà loại ngoại lệ được kiểm tra chính, ngoại trừ có một ngoại lệ đó, đó là nếu chúng cũng có ngoại lệ cũng RuntimeExceptionlà s, bởi vì đó là loại ngoại lệ không được kiểm soát khác.

Để làm gì RuntimeException? Cũng giống như tên của nó, chúng là ngoại lệ, giống như tất cả mọi thứ Exception, và chúng xảy ra vào thời gian chạy, giống như tất cả các ngoại lệ thực sự, ngoại trừ đó RuntimeExceptionlà ngoại lệ so với các thời gian chạy khác Exceptionvì chúng không được phép xảy ra ngoại trừ khi bạn mắc một số lỗi ngớ ngẩn, mặc dù RuntimeExceptions không bao giờ Errorlà s, vì vậy chúng là những thứ đặc biệt sai nhưng thực tế không phải là Errors. Ngoại trừ RuntimeErrorException, đó thực sự là một RuntimeExceptioncho Errors. Nhưng không phải tất cả các ngoại lệ được cho là đại diện cho hoàn cảnh sai lầm? Vâng, tất cả bọn họ. Ngoại trừ ThreadDeath, một ngoại lệ đặc biệt ngoại lệ, vì tài liệu giải thích rằng đó là "sự xuất hiện bình thường" và đó là 'Error

Dù sao, vì chúng tôi chia tất cả các ngoại lệ ở giữa thành Errors (dành cho các ngoại lệ thực thi đặc biệt, nên không được kiểm tra) và Exceptions (dành cho các lỗi thực thi ít đặc biệt hơn, vì vậy, đã kiểm tra trừ khi chúng không phải), bây giờ chúng tôi cần hai các loại khác nhau của một số ngoại lệ. Vì vậy, chúng ta cần IllegalAccessErrorIllegalAccessExceptionInstantiationErrorInstantiationExceptionNoSuchFieldErrorNoSuchFieldExceptionNoSuchMethodErrorNoSuchMethodExceptionZipErrorZipException.

Ngoại trừ việc ngay cả khi một ngoại lệ được kiểm tra, luôn có những cách (khá dễ dàng) để gian lận trình biên dịch và ném nó mà không bị kiểm tra. Nếu bạn làm cho bạn rằng bạn có thể nhận được UndeclaredThrowableException, ngoại trừ trong các trường hợp khác, trong đó nó có thể ném lên như một UnexpectedExceptionhoặc UnknownException(không liên quan đến UnknownError, chỉ dành cho "trường hợp ngoại lệ nghiêm trọng"), hoặc ExecutionException, hoặc InvocationTargetException, hoặc ExceptionInInitializerError.

Ồ, và chúng ta không được quên cái mới lạ lùng của Java 8 UncheckedIOException, đây là một RuntimeExceptionngoại lệ được thiết kế để cho phép bạn ném khái niệm kiểm tra ngoại lệ ra khỏi cửa sổ bằng cách gói các IOExceptionngoại lệ được kiểm tra do lỗi I / O ( IOErrormặc dù không gây ra ngoại lệ, mặc dù điều đó tồn tại quá) rất khó xử lý và vì vậy bạn cần kiểm tra chúng.

Cảm ơn Java!


2
Theo như tôi có thể nói, câu trả lời này chỉ nói rằng "các ngoại lệ của Java là một mớ hỗn độn" theo một cách đầy trớ trêu, gây tranh cãi. Những gì nó dường như không làm là giải thích tại sao các lập trình viên có xu hướng tránh cố gắng hiểu làm thế nào những thứ này được cho là hoạt động. Ngoài ra, trong các trường hợp thực tế (ít nhất là những trường hợp tôi có cơ hội giải quyết), nếu các lập trình viên không cố tình làm cho cuộc sống của họ khó khăn hơn, các ngoại lệ không phức tạp như bạn mô tả.
CptBartender

5

Vấn đề

Vấn đề tồi tệ nhất tôi thấy với cơ chế xử lý ngoại lệ là nó giới thiệu sao chép mã ở quy mô lớn ! Hãy trung thực: Trong hầu hết các dự án trong 95% thời gian tất cả các nhà phát triển thực sự cần làm ngoại lệ là truyền đạt nó bằng cách nào đó tới người dùng (và, trong một số trường hợp, cho nhóm phát triển, ví dụ như bằng cách gửi e -mail với dấu vết ngăn xếp). Vì vậy, thường thì cùng một dòng / khối mã được sử dụng ở mọi nơi ngoại lệ được xử lý.

Giả sử rằng chúng tôi đăng nhập đơn giản vào từng khối bắt đối với một số loại ngoại lệ được kiểm tra:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

Nếu đó là một ngoại lệ phổ biến, thậm chí có thể có hàng trăm khối bắt thử như vậy trong một cơ sở mã lớn hơn. Bây giờ, giả sử rằng chúng tôi cần giới thiệu xử lý ngoại lệ dựa trên hộp thoại bật lên thay vì ghi nhật ký bảng điều khiển hoặc bắt đầu gửi thêm e-mail cho nhóm phát triển.

Đợi một lát ... chúng ta thực sự sẽ chỉnh sửa tất cả hàng trăm vị trí đó trong mã?! Bạn nhận được quan điểm của tôi :-).

Giải pháp

Những gì chúng tôi đã làm để giải quyết vấn đề đó là giới thiệu khái niệm về các trình xử lý ngoại lệ (mà tôi sẽ gọi thêm là EH) để tập trung xử lý ngoại lệ. Đối với mỗi lớp cần xử lý ngoại lệ, một thể hiện của trình xử lý ngoại lệ được đưa vào bởi khung công tác Dependency Injection của chúng tôi . Vì vậy, mô hình điển hình của xử lý ngoại lệ bây giờ trông như thế này:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

Bây giờ để tùy chỉnh xử lý ngoại lệ của chúng tôi, chúng tôi chỉ cần thay đổi mã ở một nơi duy nhất (mã EH).

Tất nhiên đối với các trường hợp phức tạp hơn, chúng ta có thể triển khai một số lớp con của EH và các tính năng đòn bẩy mà khung DI của chúng ta cung cấp cho chúng ta. Bằng cách thay đổi cấu hình khung DI của chúng tôi, chúng tôi có thể dễ dàng chuyển đổi triển khai EH trên toàn cầu hoặc cung cấp các triển khai cụ thể của EH cho các lớp có nhu cầu xử lý ngoại lệ đặc biệt (ví dụ: sử dụng chú thích Guice @ Named).

Bằng cách đó, chúng tôi có thể phân biệt hành vi xử lý ngoại lệ trong phiên bản phát triển và phát hành ứng dụng (ví dụ: phát triển - ghi nhật ký lỗi và tạm dừng ứng dụng, prod - ghi nhật ký lỗi với nhiều chi tiết hơn và để ứng dụng tiếp tục thực thi) mà không cần nỗ lực.

Điều cuối cùng

Cuối cùng nhưng không kém phần quan trọng, có vẻ như có thể thu được cùng một loại tập trung bằng cách chỉ cần vượt qua các ngoại lệ của chúng tôi "lên" cho đến khi chúng đến một lớp xử lý ngoại lệ cấp cao nhất. Nhưng điều đó dẫn đến sự lộn xộn của mã và chữ ký của các phương thức của chúng tôi và đưa ra các vấn đề bảo trì được đề cập bởi những người khác trong chủ đề này.


6
Các ngoại lệ được phát minh để làm một cái gì đó hữu ích với chúng. Viết chúng vào một tệp nhật ký hoặc hiển thị một cửa sổ đẹp không hữu ích, vì vấn đề ban đầu không được giải quyết bằng cách này. Làm một cái gì đó hữu ích đòi hỏi phải thử một chiến lược giải pháp khác. Ví dụ: Nếu tôi không thể lấy dữ liệu của mình từ máy chủ AI hãy thử dữ liệu trên máy chủ B. Hoặc nếu thuật toán A tạo ra một lỗi tràn, tôi thử thuật toán B chậm hơn nhiều nhưng có thể thành công.
ceving

2
@ceving Vâng, tất cả đều tốt và đúng trong lý thuyết. Nhưng bây giờ hãy trở lại để thực hành từ. Hãy trả lời thực sự trung thực tần suất bạn làm điều đó trong dự án từ thật của bạn? Phần nào của catchcác khối trong dự án thực tế này làm một cái gì đó thực sự "hữu ích" với ngoại lệ? 10% sẽ tốt. Các vấn đề thông thường tạo ra ngoại lệ giống như cố gắng đọc cấu hình từ tệp không tồn tại, OutOfMemoryErrors, NullPulumExceptions, lỗi toàn vẹn ràng buộc cơ sở dữ liệu, v.v. Bạn có thực sự cố gắng phục hồi một cách duyên dáng từ tất cả chúng không? Tôi không tin bạn :). Thường không có cách nào để phục hồi.
Piotr Sobchot

3
@PiotrSobchot: Nếu một chương trình thực hiện một số hành động do yêu cầu của người khởi kiện và hoạt động không thành công trong một số trường hợp không làm hỏng bất cứ điều gì trong trạng thái hệ thống, thông báo cho người dùng rằng hoạt động không thể hoàn thành là hoàn toàn hữu ích cách xử lý tình huống. Thất bại lớn nhất của các trường hợp ngoại lệ trong C # và .net là không có cách nào nhất quán để xác định liệu có bất cứ điều gì trong trạng thái hệ thống có thể bị hỏng hay không.
supercat

4
Đúng, @PiotrSobchot. Thông thường, hành động đúng duy nhất để thực hiện phản hồi với một ngoại lệ là khôi phục giao dịch và trả lại phản hồi lỗi. Ý tưởng "giải quyết ngoại lệ" ngụ ý kiến ​​thức & thẩm quyền mà chúng tôi không (và không nên) có, và vi phạm đóng gói. Nếu ứng dụng của chúng tôi không phải là DB, chúng tôi không nên cố gắng sửa DB. Không sạch sẽ & tránh ghi dữ liệu sai, ceving, là đủ hữu ích .
Thomas W

@PiotrSobchot Hôm qua, tôi đã xử lý một ngoại lệ "không thể đọc đối tượng" (sẽ chỉ xuất hiện do cơ sở dữ liệu cơ bản đã được cập nhật trước phần mềm - điều không bao giờ xảy ra nhưng có thể là do lỗi của con người) phiên bản lịch sử của cơ sở dữ liệu được đảm bảo để trỏ đến một phiên bản cũ của đối tượng.
Eponymous

4

Anders nói về những cạm bẫy của các trường hợp ngoại lệ được kiểm tra và lý do tại sao anh ta bỏ chúng ra khỏi C # trong tập 97 của đài phát thanh Kỹ thuật phần mềm.


4

Bài viết của tôi trên c2.com vẫn không thay đổi so với dạng ban đầu của nó: CheckedExceptionsAreIncompiverseWithVisitorPotype

Tóm tắt:

Kiểu khách truy cập và họ hàng của nó là một lớp giao diện trong đó người gọi gián tiếp và thực hiện giao diện đều biết về một ngoại lệ nhưng giao diện và người gọi trực tiếp tạo thành một thư viện không thể biết.

Giả định cơ bản của CheckedExceptions là tất cả các ngoại lệ được khai báo có thể được ném từ bất kỳ điểm nào gọi một phương thức với khai báo đó. Số lượt truy cập cho thấy giả định này bị lỗi.

Kết quả cuối cùng của các trường hợp ngoại lệ được kiểm tra trong các trường hợp như thế này là rất nhiều mã vô dụng khác về cơ bản loại bỏ ràng buộc ngoại lệ được kiểm tra của trình biên dịch khi chạy.

Đối với vấn đề cơ bản:

Ý tưởng chung của tôi là trình xử lý cấp cao nhất cần diễn giải ngoại lệ và hiển thị thông báo lỗi thích hợp. Tôi hầu như luôn thấy các ngoại lệ IO, ngoại lệ giao tiếp (vì một số lý do API phân biệt) hoặc lỗi nghiêm trọng về tác vụ (lỗi chương trình hoặc sự cố nghiêm trọng trên máy chủ sao lưu), vì vậy điều này không quá khó nếu chúng tôi cho phép theo dõi ngăn xếp nghiêm trọng vấn đề máy chủ.


1
Bạn nên có một cái gì đó giống như DAGNodeException trong giao diện, sau đó bắt IOException và chuyển đổi nó thành DAGNodeException: cuộc gọi void công khai (DAGNode arg) ném DAGNodeException;
TofuBeer

2
@TofuBeer, đó chính xác là quan điểm của tôi. Tôi thấy rằng các ngoại lệ liên tục gói và hủy ghép nối còn tệ hơn loại bỏ các ngoại lệ được kiểm tra.
Joshua

2
Chà, chúng tôi hoàn toàn không đồng ý ... nhưng bài viết của bạn vẫn không trả lời được câu hỏi thực sự tiềm ẩn về cách bạn ngăn ứng dụng của mình hiển thị dấu vết ngăn xếp cho người dùng khi ném ngoại lệ thời gian chạy.
TofuBeer

1
@TofuBeer - Khi thất bại, nói với người dùng rằng nó thất bại là chính xác! Lựa chọn thay thế của bạn là gì, ngoài việc "giải quyết" sự thất bại với dữ liệu 'null' hoặc dữ liệu không đầy đủ / không chính xác? Giả vờ nó thành công là một lời nói dối, điều đó chỉ làm cho mọi thứ tồi tệ hơn. Với 25 năm kinh nghiệm trong các hệ thống có độ tin cậy cao, logic thử lại chỉ nên được sử dụng cẩn thận & khi thích hợp. Tôi cũng mong đợi Khách truy cập có thể thất bại lần nữa, bất kể bạn thử lại bao nhiêu lần. Trừ khi bạn đang lái máy bay, việc đổi sang phiên bản thứ hai của cùng một thuật toán là không thực tế & không thể thực hiện được (và dù sao cũng có thể thất bại).
Thomas W

4

Để cố gắng giải quyết câu hỏi chưa được trả lời:

Nếu bạn ném các lớp con RuntimeException thay vì các lớp con Exception thì làm sao bạn biết bạn phải bắt cái gì?

Câu hỏi có chứa lý do đặc biệt IMHO. Chỉ vì API cho bạn biết những gì nó ném không có nghĩa là bạn xử lý nó theo cùng một cách trong mọi trường hợp. Nói cách khác, các ngoại lệ bạn cần nắm bắt khác nhau tùy thuộc vào bối cảnh bạn sử dụng thành phần ném ngoại lệ.

Ví dụ:

Nếu tôi đang viết một trình kiểm tra kết nối cho cơ sở dữ liệu hoặc một cái gì đó để kiểm tra tính hợp lệ của người dùng đã nhập XPath, thì có lẽ tôi muốn nắm bắt và báo cáo về tất cả các ngoại lệ được kiểm tra và không được kiểm tra bị ném bởi thao tác.

Tuy nhiên, nếu tôi đang viết một công cụ xử lý, tôi có thể sẽ xử lý XPathException (đã kiểm tra) theo cách tương tự như NPE: Tôi sẽ để nó chạy lên đầu chuỗi công nhân, bỏ qua phần còn lại của lô đó, đăng nhập vấn đề (hoặc gửi nó đến bộ phận hỗ trợ để chẩn đoán) và để lại phản hồi cho người dùng liên hệ với bộ phận hỗ trợ.


2
Chính xác. Dễ dàng và đơn giản, cách xử lý ngoại lệ. Như Dave nói, xử lý ngoại lệ chính xác thường được thực hiện ở mức cao . "Ném sớm, bắt muộn" là nguyên tắc. Kiểm tra ngoại lệ làm cho khó khăn.
Thomas W

4

Bài viết này là đoạn văn bản hay nhất về xử lý ngoại lệ trong Java mà tôi từng đọc.

Nó ủng hộ không được kiểm tra đối với các ngoại lệ được kiểm tra nhưng sự lựa chọn này được giải thích rất tiết kiệm và dựa trên các lập luận mạnh mẽ.

Tôi không muốn trích dẫn quá nhiều nội dung bài viết ở đây (tốt nhất nên đọc toàn bộ) nhưng nó bao gồm hầu hết các lập luận của những trường hợp ngoại lệ không được kiểm soát ủng hộ từ chủ đề này. Đặc biệt là đối số này (có vẻ khá phổ biến) được đề cập:

Hãy xem trường hợp khi ngoại lệ được ném ở đâu đó ở dưới cùng của các lớp API và chỉ nổi lên vì không ai biết có thể xảy ra lỗi này, mặc dù đây là một loại lỗi rất hợp lý khi mã cuộc gọi đã ném nó (ví dụ FileNotFoundException trái ngược với VogonsTrashingEarthExcept ... trong trường hợp đó sẽ không có vấn đề gì nếu chúng tôi xử lý hay không vì không còn gì để xử lý).

Tác giả "trả lời":

Hoàn toàn không đúng khi cho rằng tất cả các ngoại lệ trong thời gian chạy không nên bị bắt và được phép truyền đến "đỉnh" của ứng dụng. (...) Đối với mọi điều kiện đặc biệt bắt buộc phải được xử lý riêng biệt - bởi các yêu cầu hệ thống / kinh doanh - lập trình viên phải quyết định nơi bắt nó và phải làm gì khi điều kiện được bắt. Điều này phải được thực hiện đúng theo nhu cầu thực tế của ứng dụng, không dựa trên cảnh báo của trình biên dịch. Tất cả các lỗi khác phải được phép tự do tuyên truyền đến trình xử lý cao nhất, nơi chúng sẽ được ghi lại và một hành động duyên dáng (có lẽ, chấm dứt) sẽ được thực hiện.

Và ý nghĩ hay bài viết chính là:

Khi nói đến việc xử lý lỗi trong phần mềm, giả định duy nhất an toàn và chính xác có thể được đưa ra là lỗi có thể xảy ra trong tất cả các chương trình con hoặc mô-đun tồn tại!

Vì vậy, nếu " không ai biết rằng lỗi này thậm chí có thể xảy ra " thì có gì đó không ổn với dự án đó. Ngoại lệ như vậy phải được xử lý bởi ít nhất là trình xử lý ngoại lệ chung nhất (ví dụ: trình xử lý ngoại lệ không được xử lý bởi các trình xử lý cụ thể hơn) như tác giả đề xuất.

Thật đáng buồn khi dường như không có nhiều người phát hiện ra bài viết tuyệt vời này :-(. Tôi khuyên mọi người nên ngần ngại cách tiếp cận nào tốt hơn để dành thời gian và đọc nó.


4

Các trường hợp ngoại lệ được kiểm tra là, ở dạng ban đầu của chúng, là một nỗ lực để xử lý các tình huống dự phòng thay vì thất bại. Mục tiêu đáng khen ngợi là làm nổi bật các điểm dự đoán cụ thể (không thể kết nối, không tìm thấy tệp, v.v.) và đảm bảo các nhà phát triển xử lý các điểm này.

Điều không bao giờ được bao gồm trong khái niệm ban đầu, là buộc một loạt các thất bại mang tính hệ thống và không thể phục hồi được tuyên bố. Những thất bại này không bao giờ chính xác để được tuyên bố là ngoại lệ được kiểm tra.

Thất bại thường có thể xảy ra trong mã và các bộ chứa EJB, web & Swing / AWT đã phục vụ cho việc này bằng cách cung cấp một trình xử lý ngoại lệ yêu cầu ngoại lệ. Chiến lược chính xác cơ bản nhất là phục hồi giao dịch và trả lại lỗi.

Một điểm rất quan trọng, đó là thời gian chạy & ngoại lệ được kiểm tra là tương đương về chức năng.Không có xử lý hoặc phục hồi mà ngoại lệ được kiểm tra có thể làm, ngoại lệ thời gian chạy không thể.

Lập luận lớn nhất chống lại các trường hợp ngoại lệ đã được kiểm tra của Wap là hầu hết các ngoại lệ không thể được sửa. Thực tế đơn giản là, chúng tôi không sở hữu mã / hệ thống con bị hỏng. Chúng tôi không thể thấy việc thực hiện, chúng tôi không chịu trách nhiệm về việc đó và không thể khắc phục.

Nếu ứng dụng của chúng tôi không phải là DB .. chúng tôi không nên thử và sửa DB. Điều đó sẽ vi phạm nguyên tắc đóng gói .

Đặc biệt có vấn đề là các lĩnh vực của JDBC (SQLException) và RMI cho EJB (RemoteException). Thay vì xác định các trường hợp có thể sửa chữa theo khái niệm ban đầu ngoại lệ đã kiểm tra, các vấn đề về độ tin cậy hệ thống bắt buộc này, không thực sự có thể sửa chữa được tuyên bố rộng rãi.

Một lỗ hổng nghiêm trọng khác trong thiết kế Java, đó là việc xử lý ngoại lệ phải được đặt chính xác ở mức "doanh nghiệp" hoặc "yêu cầu" cao nhất có thể. Nguyên tắc ở đây là "ném sớm, bắt muộn". Kiểm tra ngoại lệ làm ít nhưng có được trong cách này.

Chúng tôi có một vấn đề rõ ràng trong Java là yêu cầu hàng ngàn khối bắt không làm gì, với tỷ lệ đáng kể (40% +) bị sảy thai. Hầu như không ai trong số này thực hiện bất kỳ xử lý chính xác hoặc độ tin cậy, nhưng áp đặt chi phí mã hóa lớn.

Cuối cùng, "ngoại lệ được kiểm tra" không tương thích nhiều với lập trình chức năng FP.

Sự khăng khăng của họ về "xử lý ngay lập tức" là mâu thuẫn với cả thực tiễn tốt nhất về xử lý ngoại lệ "bắt muộn" và bất kỳ cấu trúc FP nào trừu tượng hóa các vòng lặp / hoặc luồng kiểm soát.

Nhiều người nói về "xử lý" các trường hợp ngoại lệ được kiểm tra, nhưng đang nói chuyện qua mũ của họ. Tiếp tục sau một thất bại với dữ liệu null, không đầy đủ hoặc không chính xác để giả vờ thành công là không xử lý bất cứ điều gì. Đó là sơ suất kỹ thuật / độ tin cậy của dạng thấp nhất.

Thất bại sạch sẽ, là chiến lược chính xác cơ bản nhất để xử lý một ngoại lệ. Quay trở lại giao dịch, ghi lại lỗi và báo cáo phản hồi "thất bại" cho người dùng là thực tế đúng đắn - và quan trọng nhất là ngăn chặn dữ liệu kinh doanh không chính xác được cam kết với cơ sở dữ liệu.

Các chiến lược khác để xử lý ngoại lệ là "thử lại", "kết nối lại" hoặc "bỏ qua", ở cấp độ kinh doanh, hệ thống con hoặc yêu cầu. Tất cả đều là các chiến lược độ tin cậy chung và hoạt động tốt / tốt hơn với các ngoại lệ thời gian chạy.

Cuối cùng, tốt hơn hết là thất bại, hơn là chạy với dữ liệu không chính xác. Tiếp tục sẽ gây ra lỗi thứ cấp, cách xa nguyên nhân ban đầu & khó gỡ lỗi hơn; hoặc cuối cùng sẽ dẫn đến dữ liệu sai lầm được cam kết. Mọi người bị sa thải vì điều đó.

Xem:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/


1
Quan điểm của tôi là thất bại đúng như chiến lược chung. Các trường hợp ngoại lệ không được kiểm tra giúp rằng vì chúng không buộc các khối bắt phải được can thiệp. Việc bắt và ghi nhật ký lỗi sau đó có thể được để lại cho một vài trình xử lý ngoài cùng, thay vì bị mã hóa hàng ngàn lần trong toàn bộ cơ sở mã (đây thực sự là những gì che giấu lỗi) . Đối với các thất bại tùy ý, các trường hợp ngoại lệ không được kiểm soát là hoàn toàn chính xác nhất. Các trường hợp dự phòng - kết quả có thể dự đoán được như không đủ tiền - là những ngoại lệ duy nhất hợp pháp là đáng để kiểm tra.
Thomas W

1
Câu trả lời của tôi đã ở trên giải quyết điều này. Đầu tiên và quan trọng nhất, 1) Người xử lý thất bại ngoài cùng nên nắm bắt mọi thứ. Ngoài ra, đối với các trang web được xác định cụ thể, 2) Các tình huống dự kiến ​​cụ thể có thể bị bắt và xử lý - tại trang web ngay lập tức chúng bị ném. Điều đó có nghĩa là không tìm thấy tệp, không đủ tiền, v.v. tại thời điểm này có thể được phục hồi từ - không cao hơn. Nguyên tắc đóng gói có nghĩa là các lớp bên ngoài không thể / không nên chịu trách nhiệm hiểu / phục hồi từ những thất bại sâu bên trong. Thứ ba, 3) Mọi thứ khác nên được ném ra ngoài - không được kiểm tra nếu có thể.
Thomas W

1
Trình xử lý ngoài cùng bắt Ngoại lệ, ghi nhật ký và trả về phản hồi "không thành công" hoặc hiển thị hộp thoại báo lỗi. Rất đơn giản, không khó để định nghĩa chút nào. Vấn đề là mọi ngoại lệ không thể phục hồi ngay lập tức & cục bộ, là một thất bại không thể phục hồi do nguyên tắc đóng gói. Nếu mã có nghĩa là không thể khôi phục mã, thì tổng thể yêu cầu sẽ thất bại một cách sạch sẽ & đúng cách. Đây là cách đúng đắn để làm điều đó đúng.
Thomas W

1
Sai. Công việc của người xử lý ngoài cùng là không hoàn toàn sạch sẽ & ghi lại các lỗi trên ranh giới 'yêu cầu'. Yêu cầu bị hỏng không đúng, ngoại lệ được báo cáo, luồng có thể tiếp tục phục vụ yêu cầu tiếp theo. Trình xử lý ngoài cùng như vậy là một tính năng tiêu chuẩn trong các thùng chứa Tomcat, AWT, Spring, EJB và luồng 'chính' của Java.
Thomas W

1
Tại sao việc báo cáo "lỗi chính hãng" ở ranh giới yêu cầu hoặc trình xử lý ngoài cùng lại nguy hiểm ??? Tôi thường xuyên làm việc trong tích hợp và độ tin cậy của hệ thống, trong đó kỹ thuật độ tin cậy chính xác thực sự quan trọng và sử dụng các phương pháp "ngoại lệ không được kiểm soát" để làm như vậy. Tôi không thực sự chắc chắn những gì bạn đang thực sự tranh luận - có vẻ như bạn có thể thực sự muốn dành 3 tháng theo cách ngoại lệ không được kiểm soát, cảm nhận về nó và sau đó có lẽ chúng ta có thể thảo luận thêm. Cảm ơn.
Thomas W

4

Như mọi người đã nêu, các trường hợp ngoại lệ được kiểm tra không tồn tại trong mã byte Java. Chúng chỉ đơn giản là một cơ chế biên dịch, không giống như các kiểm tra cú pháp khác. Tôi thấy các ngoại lệ được kiểm tra rất giống như tôi thấy trình biên dịch phàn nàn về một điều kiện dư thừa : if(true) { a; } b;. Điều đó hữu ích nhưng tôi có thể đã cố tình làm điều này, vì vậy hãy để tôi bỏ qua những cảnh báo của bạn.

Thực tế của vấn đề là, bạn sẽ không thể buộc mọi lập trình viên "làm điều đúng đắn" nếu bạn thi hành các ngoại lệ được kiểm tra và mọi người khác hiện đang thiệt hại tài sản, những người chỉ ghét bạn vì quy tắc bạn đưa ra.

Sửa các chương trình xấu ngoài kia! Đừng cố sửa ngôn ngữ để không cho phép chúng! Đối với hầu hết mọi người, "làm điều gì đó về một ngoại lệ" thực sự chỉ là nói với người dùng về nó. Tôi cũng có thể nói với người dùng về một ngoại lệ chưa được kiểm tra, vì vậy hãy tránh các lớp ngoại lệ được kiểm tra của bạn ra khỏi API của tôi.


Đúng vậy, tôi chỉ muốn nhấn mạnh sự khác biệt giữa mã không thể truy cập (tạo ra lỗi) và các điều kiện có kết quả có thể dự đoán được. Tôi sẽ xóa bình luận này sau.
Holger

3

Một vấn đề với các ngoại lệ được kiểm tra là các ngoại lệ thường được gắn vào các phương thức của giao diện nếu ngay cả một triển khai giao diện đó sử dụng nó.

Một vấn đề khác với các ngoại lệ được kiểm tra là chúng có xu hướng bị sử dụng sai. Các ví dụ hoàn hảo của việc này là ở java.sql.Connection's close()phương pháp. Nó có thể ném SQLException, mặc dù bạn đã tuyên bố rõ ràng rằng bạn đã hoàn thành với Kết nối. Thông tin nào có thể đóng () có thể truyền tải mà bạn quan tâm?

Thông thường, khi tôi đóng () một kết nối *, nó trông giống như thế này:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

Ngoài ra, đừng để tôi bắt đầu với các phương thức phân tích cú pháp khác nhau và NumberFormatException ... TryPude của .NET, không đưa ra ngoại lệ, việc sử dụng Java trở nên dễ dàng hơn nhiều (chúng tôi sử dụng cả Java và Java C # nơi tôi làm việc).

*Là một nhận xét bổ sung, Connection.close () của PooledConnection thậm chí không đóng kết nối, nhưng bạn vẫn phải nắm bắt được nhận thức SQLEx do nó là một ngoại lệ được kiểm tra.


2
Phải, bất kỳ trình điều khiển nào cũng có thể ... câu hỏi là "tại sao lập trình viên phải quan tâm?" như anh ấy đã hoàn thành việc truy cập cơ sở dữ liệu Các tài liệu thậm chí còn cảnh báo bạn rằng bạn phải luôn cam kết () hoặc rollback () giao dịch hiện tại trước khi gọi close ().
Powerlord

Nhiều người nghĩ rằng đóng trên một tập tin không thể tạo ra một ngoại lệ ... stackoverflow.com/questions/588546/ Bạn có chắc chắn 100% rằng không có trường hợp nào có vấn đề không?
TofuBeer

Tôi chắc chắn 100% rằng không có trường hợp nào quan trọng và người gọi sẽ không thử / bắt.
Martin

1
Ví dụ tuyệt vời với các kết nối đóng, Martin! Tôi chỉ có thể viết lại cho bạn: Nếu chúng tôi chỉ tuyên bố rõ ràng rằng chúng tôi đã hoàn thành với một kết nối thì tại sao phải bận tâm những gì đang xảy ra khi chúng tôi đóng nó. Có nhiều trường hợp như thế này mà lập trình viên không thực sự quan tâm nếu ngoại lệ xảy ra và anh ta hoàn toàn đúng về nó.
Piotr Sobchot

1
@PiotrSobchot: Một số trình điều khiển SQL sẽ squawk nếu một người đóng kết nối sau khi bắt đầu giao dịch nhưng không xác nhận hay không khôi phục lại. IMHO, squawking tốt hơn là âm thầm bỏ qua vấn đề, ít nhất là trong trường hợp squawking sẽ không khiến các ngoại lệ khác bị mất.
supercat

3

Lập trình viên cần biết tất cả các ngoại lệ mà một phương thức có thể đưa ra, để sử dụng nó một cách chính xác. Vì vậy, đánh vào đầu anh ta chỉ bằng một số ngoại lệ không nhất thiết giúp một lập trình viên bất cẩn tránh được lỗi.

Lợi ích mỏng hơn nhiều so với chi phí nặng nề (đặc biệt là trong các cơ sở mã lớn hơn, kém linh hoạt hơn khi liên tục sửa đổi chữ ký giao diện là không thực tế).

Phân tích tĩnh có thể tốt, nhưng phân tích tĩnh thực sự đáng tin cậy thường đòi hỏi công việc nghiêm ngặt từ lập trình viên. Có một tính toán lợi ích chi phí và thanh cần được đặt ở mức cao để kiểm tra dẫn đến lỗi thời gian biên dịch. Sẽ hữu ích hơn nếu IDE đảm nhận vai trò truyền đạt ngoại lệ mà một phương thức có thể đưa ra (bao gồm cả những điều không thể tránh khỏi). Mặc dù có lẽ nó sẽ không đáng tin cậy nếu không có tuyên bố ngoại lệ bắt buộc, hầu hết các trường hợp ngoại lệ vẫn sẽ được khai báo trong tài liệu và độ tin cậy của cảnh báo IDE không quá quan trọng.


2

Tôi nghĩ rằng đây là một câu hỏi xuất sắc và hoàn toàn không tranh luận. Tôi nghĩ rằng các thư viện bên thứ 3 (nói chung) nên đưa ra các ngoại lệ không được kiểm soát . Điều này có nghĩa là bạn có thể cô lập sự phụ thuộc của mình vào thư viện (nghĩa là bạn không phải ném lại ngoại lệ của họ hoặc ném Exception- thường là thực tiễn xấu). Lớp DAO của Spring là một ví dụ tuyệt vời về điều này.

Mặt khác, trường hợp ngoại lệ từ lõi Java API nên trong là Tổng kiểm tra nếu họ có thể đã từng được xử lý. Lấy FileNotFoundExceptionhoặc (yêu thích của tôi) InterruptedException. Những điều kiện này hầu như luôn luôn phải được xử lý một cách cụ thể (nghĩa là phản ứng của bạn đối với InterruptedExceptionkhông giống với phản ứng của bạn đối với một IllegalArgumentException). Thực tế là các trường hợp ngoại lệ của bạn được kiểm tra buộc các nhà phát triển phải suy nghĩ về việc liệu một điều kiện có thể xử lý được hay không. (Điều đó nói rằng, tôi hiếm khi thấy InterruptedExceptionxử lý đúng cách!)

Một điều nữa - RuntimeExceptionkhông phải lúc nào cũng là "nơi mà một nhà phát triển có gì đó không đúng". Một ngoại lệ đối số bất hợp pháp được đưa ra khi bạn thử và tạo một enumcách sử dụng valueOfvà không có enumtên đó. Đây không hẳn là một sai lầm của nhà phát triển!


1
Vâng, đó là một sai lầm của nhà phát triển. Họ rõ ràng đã không sử dụng đúng tên, vì vậy họ phải quay lại và sửa mã của họ.
AxiomaticNexus

@AxiomaticNexus Không có nhà phát triển lành mạnh nào sử dụng enumtên thành viên, đơn giản vì họ sử dụng enumcác đối tượng thay thế. Vì vậy, một tên sai chỉ có thể đến từ bên ngoài, có thể là một tệp nhập hoặc bất cứ điều gì. Một cách khả thi để đối phó với những cái tên như vậy là gọi MyEnum#valueOfvà bắt IAE. Một cách khác là sử dụng một bản điền sẵn Map<String, MyEnum>, nhưng đây là những chi tiết triển khai.
maaartinus

@maaartinus Có những trường hợp tên thành viên enum được sử dụng mà không có chuỗi đến từ bên ngoài. Ví dụ, khi bạn muốn lặp lại tất cả các thành viên một cách linh hoạt để làm điều gì đó với từng thành viên. Hơn nữa, việc chuỗi đến từ bên ngoài hay không là không liên quan. Nhà phát triển có tất cả thông tin họ cần, để biết liệu việc chuyển x chuỗi sang "MyEnum # valueOf" có dẫn đến lỗi hay không trước khi truyền. Việc chuyển chuỗi x sang "MyEnum # valueOf" dù sao đi nữa khi nó gây ra lỗi, rõ ràng sẽ là một lỗi về phía nhà phát triển.
AxiomaticNexus

2

Đây là một đối số chống lại các ngoại lệ được kiểm tra (từ joelonsoftware.com):

Lý do là tôi coi các trường hợp ngoại lệ không tốt hơn "goto", được coi là có hại kể từ những năm 1960, trong đó chúng tạo ra một bước nhảy đột ngột từ điểm này sang điểm khác. Trên thực tế, chúng còn tệ hơn đáng kể so với goto:

  • Chúng vô hình trong mã nguồn. Nhìn vào một khối mã, bao gồm các hàm có thể hoặc không thể ném ngoại lệ, không có cách nào để xem ngoại lệ nào có thể được ném và từ đâu. Điều này có nghĩa là ngay cả việc kiểm tra mã cẩn thận cũng không tiết lộ các lỗi tiềm ẩn.
  • Chúng tạo quá nhiều điểm thoát có thể cho một hàm. Để viết mã chính xác, bạn thực sự phải suy nghĩ về mọi đường dẫn mã có thể thông qua chức năng của bạn. Mỗi khi bạn gọi một hàm có thể đưa ra một ngoại lệ và không bắt gặp tại chỗ, bạn sẽ tạo cơ hội cho các lỗi bất ngờ gây ra bởi các hàm bị chấm dứt đột ngột, khiến dữ liệu ở trạng thái không nhất quán hoặc các đường dẫn mã khác mà bạn không nghĩ về.

3
+1 Bạn có thể muốn tóm tắt đối số trong câu trả lời của bạn không? Chúng giống như những hình ảnh vô hình và lối thoát sớm cho thói quen của bạn, nằm rải rác trong chương trình.
MarkJ

13
Đó là một cuộc tranh luận chống lại Ngoại lệ nói chung.
Ionuț G. Stan

10
bạn đã thực sự đọc bài viết chưa !! Đầu tiên, ông nói về các trường hợp ngoại lệ nói chung, thứ hai là phần "Chúng vô hình trong mã nguồn" áp dụng cụ thể cho ngoại lệ UNCHECKED. Đây là toàn bộ điểm của ngoại lệ được kiểm tra ... để bạn BIẾT mã nào ném vào đâu
Newtopian

2
@Eva Họ không giống nhau. Với một tuyên bố goto bạn có thể thấy gototừ khóa. Với một vòng lặp, bạn có thể thấy dấu ngoặc nhọn breakhoặc continuetừ khóa. Tất cả đều nhảy đến một điểm trong phương thức hiện tại. Nhưng bạn không thể luôn luôn nhìn thấy throw, bởi vì thường thì nó không phải trong phương thức hiện tại mà là một phương thức khác mà nó gọi (có thể là gián tiếp.)
finnw

5
@finnw Hàm là một dạng của goto. Bạn thường không biết chức năng nào mà chức năng bạn đang gọi đang gọi. Nếu bạn lập trình mà không có chức năng, bạn sẽ không gặp vấn đề với các ngoại lệ vô hình. Điều đó có nghĩa là vấn đề không liên quan cụ thể đến các ngoại lệ và không phải là một đối số hợp lệ chống lại các ngoại lệ nói chung. Bạn có thể nói mã lỗi nhanh hơn, bạn có thể nói các đơn vị sạch hơn, nhưng đối số goto là ngớ ngẩn.
Eva

2

Điều tốt chứng minh rằng Ngoại lệ được kiểm tra là không cần thiết là:

  1. Rất nhiều khung làm việc một số cho Java. Giống như Spring bao bọc ngoại lệ JDBC cho các ngoại lệ không được kiểm tra, ném các thông điệp vào nhật ký
  2. Rất nhiều ngôn ngữ xuất hiện sau java, thậm chí trên đỉnh trên nền tảng java - họ không sử dụng chúng
  3. Đã kiểm tra các ngoại lệ, đó là dự đoán tử tế về cách khách hàng sẽ sử dụng mã ném ngoại lệ. Nhưng một nhà phát triển viết mã này sẽ không bao giờ biết về hệ thống và doanh nghiệp mà khách hàng của mã đang làm việc. Ví dụ như các phương thức Interfcace buộc phải ném ngoại lệ được kiểm tra. Có 100 triển khai trên hệ thống, 50 hoặc thậm chí 90 triển khai không ném ngoại lệ này, nhưng khách hàng vẫn phải bắt ngoại lệ này nếu người dùng tham chiếu đến giao diện đó. 50 hoặc 90 triển khai đó có xu hướng xử lý các ngoại lệ đó trong chính chúng, đặt ngoại lệ cho nhật ký (và đây là hành vi tốt cho chúng). Chúng ta nên làm gì với điều đó? Tôi tốt hơn nên có một số logic nền sẽ làm tất cả công việc đó - gửi tin nhắn đến nhật ký. Và nếu tôi, với tư cách là một khách hàng của mã, sẽ cảm thấy tôi cần xử lý ngoại lệ - tôi sẽ làm điều đó.
  4. Một ví dụ khác khi tôi làm việc với I / O trong java, nó buộc tôi phải kiểm tra tất cả ngoại lệ, nếu tập tin không tồn tại? Tôi nên làm gì với điều đó? Nếu nó không tồn tại, hệ thống sẽ không chuyển sang bước tiếp theo. Ứng dụng khách của phương thức này, sẽ không nhận được nội dung mong đợi từ tệp đó - anh ta có thể xử lý Ngoại lệ Thời gian chạy, nếu không trước tiên tôi nên kiểm tra Ngoại lệ đã kiểm tra, đặt một thông báo để đăng nhập, sau đó ném ngoại lệ ra khỏi phương thức. Không ... không - Tôi tốt hơn nên tự động làm điều đó với RuntimeEception, điều đó sẽ tự động thực hiện. Không có bất kỳ ý nghĩa nào để xử lý nó một cách thủ công - tôi sẽ rất vui khi thấy một thông báo lỗi trong nhật ký (AOP có thể giúp với điều đó .. một cái gì đó sửa lỗi java). Cuối cùng, nếu tôi cho rằng hệ thống đó sẽ hiển thị thông báo bật lên cho người dùng cuối - tôi sẽ hiển thị nó, không phải là vấn đề.

Tôi rất vui nếu java sẽ cung cấp cho tôi lựa chọn sử dụng cái gì, khi làm việc với các lib cốt lõi, như I / O. Like cung cấp hai bản sao của cùng một lớp - một bản được gói bằng RuntimeEception. Sau đó chúng ta có thể so sánh những gì mọi người sẽ sử dụng . Tuy nhiên, hiện tại, nhiều người nên sử dụng một số khung công tác trên java hoặc ngôn ngữ khác. Giống như Scala, JRuby bất cứ điều gì. Nhiều người chỉ tin rằng SUN đã đúng.


1
Thay vì có hai phiên bản của các lớp, nên có một cách ngắn gọn để chỉ định không có cuộc gọi phương thức nào được thực hiện bởi một khối mã không được dự kiến ​​sẽ ném ngoại lệ của một số loại nhất định và rằng bất kỳ ngoại lệ nào như vậy phải được gói qua một số phương tiện được chỉ định và rethrown (theo mặc định, tạo một cái mới RuntimeExceptionvới một ngoại lệ bên trong thích hợp). Thật không may là nó ngắn gọn hơn khi có phương thức bên ngoài throwsngoại lệ so với phương thức bên trong, hơn là nó bao bọc các ngoại lệ từ phương thức bên trong, khi hành động sau thường sẽ đúng hơn.
supercat

2

Một điều quan trọng không ai đề cập đến là làm thế nào nó can thiệp vào các giao diện và biểu thức lambda.

Giả sử bạn xác định a MyAppException extends Exception. Đây là ngoại lệ cấp cao nhất được kế thừa bởi tất cả các ngoại lệ được ném bởi ứng dụng của bạn. Mọi phương pháp đều tuyên bố throws MyAppExceptionđó là một chút phiền toái, nhưng có thể quản lý được. Một trình xử lý ngoại lệ ghi lại ngoại lệ và thông báo cho người dùng bằng cách nào đó.

Tất cả đều ổn cho đến khi bạn muốn thực hiện một số giao diện không phải của bạn. Rõ ràng là nó không tuyên bố ý định ném MyApException, vì vậy trình biên dịch không cho phép bạn ném ngoại lệ từ đó.

Tuy nhiên, nếu ngoại lệ của bạn mở rộng RuntimeException, sẽ không có vấn đề gì với giao diện. Bạn có thể tự nguyện đề cập đến ngoại lệ trong JavaDoc nếu bạn muốn. Nhưng khác hơn là nó chỉ âm thầm sủi bọt qua bất cứ thứ gì, để bị cuốn vào lớp xử lý ngoại lệ của bạn.


1

Chúng tôi đã thấy một số tài liệu tham khảo cho kiến ​​trúc sư trưởng của C #.

Đây là một quan điểm thay thế từ một anh chàng Java về thời điểm sử dụng các ngoại lệ được kiểm tra. Ông thừa nhận nhiều tiêu cực mà người khác đã đề cập: Ngoại lệ hiệu quả


2
Vấn đề với các ngoại lệ được kiểm tra trong Java bắt nguồn từ một vấn đề sâu sắc hơn, đó là cách mà quá nhiều thông tin được gói gọn trong TYPE của ngoại lệ, thay vì thuộc tính của một cá thể. Sẽ rất hữu ích nếu đã kiểm tra các ngoại lệ, nếu "được kiểm tra" là một thuộc tính của các trang web ném / bắt và nếu người ta có thể chỉ định một cách rõ ràng liệu một ngoại lệ được kiểm tra thoát khỏi một khối mã có nên được xem là ngoại lệ được kiểm tra hay không bởi bất kỳ khối kèm theo nào là một ngoại lệ không được kiểm tra; các khối bắt tương tự cũng có thể chỉ định rằng chúng chỉ muốn các ngoại lệ được kiểm tra.
supercat

1
Giả sử một thói quen tra cứu từ điển được chỉ định để ném một số loại ngoại lệ cụ thể nếu một nỗ lực được thực hiện để truy cập vào một khóa không tồn tại. Có thể hợp lý khi mã máy khách bắt được một ngoại lệ như vậy. Tuy nhiên, nếu một số phương thức được sử dụng bởi thói quen tra cứu xảy ra để ném loại ngoại lệ tương tự theo cách mà thói quen tra cứu không mong đợi, thì mã máy khách có lẽ không nên bắt nó. Sau khi kiểm tra-Ness là một tài sản của ngoại lệ trường hợp , các trang web ném, và các trang web bắt, sẽ tránh các vấn đề như vậy. Khách hàng sẽ nắm bắt các trường hợp ngoại lệ 'đã kiểm tra' của loại đó, tránh những trường hợp không mong muốn.
supercat

0

Tôi đã đọc rất nhiều về việc xử lý ngoại lệ, ngay cả khi (hầu hết thời gian) tôi thực sự không thể nói rằng tôi vui hay buồn về sự tồn tại của các ngoại lệ được kiểm tra, đây là trường hợp của tôi: kiểm tra ngoại lệ trong mã cấp thấp (IO, kết nối mạng , HĐH, v.v.) và các trường hợp ngoại lệ không được kiểm tra ở cấp độ ứng dụng / API cấp cao.

Ngay cả khi không dễ dàng để vẽ một đường thẳng giữa chúng, tôi thấy rằng thật sự khó chịu / khó khăn khi tích hợp một số API / thư viện dưới cùng một mái nhà mà không bao gồm tất cả các trường hợp ngoại lệ được kiểm tra, nhưng mặt khác, đôi khi nó là hữu ích / tốt hơn để bị buộc phải bắt một số ngoại lệ và cung cấp một ngoại lệ khác có ý nghĩa hơn trong bối cảnh hiện tại.

Dự án tôi đang thực hiện cần rất nhiều thư viện và tích hợp chúng trong cùng một API, API hoàn toàn dựa trên các ngoại lệ không được kiểm tra. Các khung này cung cấp API cấp cao, lúc đầu có đầy đủ các ngoại lệ được kiểm tra và chỉ có một số trường hợp không được kiểm tra ngoại lệ (Ngoại lệ khởi tạo, Cấu hình ngoại lệ, v.v.) và tôi phải nói là không thân thiện lắm . Hầu hết thời gian bạn phải nắm bắt hoặc ném lại các ngoại lệ mà bạn không biết cách xử lý hoặc thậm chí bạn không quan tâm (đừng nhầm lẫn với bạn nên bỏ qua các ngoại lệ), đặc biệt là về phía khách hàng nhấp có thể ném 10 ngoại lệ có thể (đã kiểm tra).

Phiên bản hiện tại (phiên bản thứ 3) chỉ sử dụng các ngoại lệ không được kiểm tra và nó có trình xử lý ngoại lệ toàn cầu chịu trách nhiệm xử lý mọi nội dung chưa được xử lý. API cung cấp một cách để đăng ký các trình xử lý ngoại lệ, sẽ quyết định xem một ngoại lệ có bị coi là lỗi hay không (phần lớn là trường hợp này) có nghĩa là đăng nhập và thông báo cho ai đó, hoặc nó có thể có ý nghĩa khác - như ngoại lệ này, AbortException, có nghĩa là phá vỡ chuỗi thực thi hiện tại và không ghi lại bất kỳ lỗi nào 'vì nó không muốn. Tất nhiên, để xử lý tất cả các luồng tùy chỉnh phải xử lý phương thức run () với một lệnh bắt {...} (tất cả).

công khai void run () {

try {
     ... do something ...
} catch (Throwable throwable) {
     ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

}

Điều này là không cần thiết nếu bạn sử dụng WorkerService để lên lịch công việc (Runnable, Callable, Worker), xử lý mọi thứ cho bạn.

Tất nhiên đây chỉ là ý kiến ​​của tôi, và nó có thể không phải là ý kiến ​​đúng, nhưng có vẻ như đó là một cách tiếp cận tốt với tôi. Tôi sẽ thấy sau khi tôi sẽ phát hành dự án nếu những gì tôi nghĩ nó tốt cho tôi, nó cũng tốt cho những người khác ... :)

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.