Добри или лоши са проверените изключения?

Java поддържа проверени изключения. Тази противоречива езикова функция е обичана от някои, а мразена от други, до степен, в която повечето езици за програмиране избягват проверени изключения и поддържат само своите непроверени аналози.

В тази публикация разглеждам противоречието около проверените изключения. Първо представям концепцията за изключения и описвам накратко езиковата поддръжка на Java за изключения, за да помогна на начинаещите да разберат по-добре противоречията.

Какви са изключенията?

В идеалния свят компютърните програми никога не биха се сблъскали с проблеми: файловете биха съществували, когато се предполага, че мрежовите връзки никога няма да се затворят неочаквано, никога няма да има опит за извикване на метод чрез нулевата препратка, integer-division-by -нулеви опити няма да се случват и т.н. Нашият свят обаче далеч не е идеален; тези и други изключения от идеалното изпълнение на програмата са широко разпространени.

Ранните опити за разпознаване на изключения включват връщане на специални стойности, които показват неуспех. Например функцията на езика C се fopen()връща, NULLкогато не може да отвори файл. Също така, mysql_query()функцията на PHP се връща, FALSEкогато възникне SQL грешка. Трябва да потърсите другаде за действителния код за повреда. Макар и лесен за изпълнение, има два проблема с този подход за връщане на специална стойност за разпознаване на изключения:

  • Специалните стойности не описват изключението. Какво означава NULLили FALSEвсъщност означава? Всичко зависи от автора на функционалността, която връща специалната стойност. Освен това, как свързвате специална стойност с контекста на програмата, когато се е случило изключението, за да можете да представите смислено съобщение на потребителя?
  • Твърде лесно е да се игнорира специална стойност. Например, int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);е проблематично, тъй като този фрагмент от C код се изпълнява, за fgetc()да прочете символ от файла, дори когато се fopen()връща NULL. В този случай fgetc()няма да успее: имаме грешка, която може да е трудно да се намери.

Първият проблем се решава чрез използване на класове за описване на изключения. Името на клас идентифицира вида изключение и неговите полета обобщават подходящ контекст на програмата за определяне (чрез извиквания на методи) какво се е объркало. Вторият проблем се решава, като компилаторът принуди програмиста или да отговори директно на изключение, или да посочи, че изключението трябва да бъде обработено другаде.

Някои изключения са много сериозни. Например програма може да се опита да разпредели малко памет, когато няма свободна памет. Друг пример е безграничната рекурсия, която изчерпва стека. Такива изключения са известни като грешки .

Изключения и Java

Java използва класове, за да опише изключения и грешки. Тези класове са организирани в йерархия, която се корени в java.lang.Throwableкласа. (Причината, поради която Throwableбеше избрана да назовем този специален клас ще стане ясно след малко.) Непосредствено под Throwableса java.lang.Exceptionи java.lang.Errorкласове, които описват изключения и грешки, съответно.

Например библиотеката на Java включва java.net.URISyntaxException, което се разширява Exceptionи показва, че низ не може да бъде анализиран като препратка към унифициран идентификатор на ресурс. Имайте предвид, че URISyntaxExceptionследва конвенция за именуване, в която името на клас на изключение завършва с думата Exception. Подобна конвенция се прилага за имена на класове грешки, като например java.lang.OutOfMemoryError.

Exceptionе подклас от java.lang.RuntimeException, което е суперкласът на онези изключения, които могат да бъдат изхвърлени по време на нормалната работа на Java Virtual Machine (JVM). Например, java.lang.ArithmeticExceptionописва аритметични неуспехи, като опити за разделяне на цели числа на цяло число 0. Също така, java.lang.NullPointerExceptionописва опити за достъп до членовете на обекта чрез нулевата препратка.

Друг начин за гледане RuntimeException

Раздел 11.1.1 от Спецификацията на езика Java 8 гласи: RuntimeExceptionе суперкласът на всички изключения, които могат да бъдат хвърлени по много причини по време на оценяването на израза, но от които все още може да е възможно възстановяване.

Когато възникне изключение или грешка, обект от съответния Exceptionили Errorподклас се създава и предава на JVM. Актът на предаване на обекта е известен като хвърляне на изключението . Java предоставя throwизявлението за тази цел. Например throw new IOException("unable to read file");създава нов java.io.IOExceptionобект, който е инициализиран в посочения текст. Впоследствие този обект се хвърля на JVM.

Java предоставя tryизявлението за разграничаване на код, от който може да се изведе изключение. Това изявление се състои от ключова дума, tryпоследвана от блок, разделен със скоби. Следният кодов фрагмент демонстрира tryи throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

В този фрагмент на код изпълнението влиза в tryблока и извиква method(), което хвърля екземпляр на NullPointerException.

JVM получава хвърляемото и търси стека за извикване на метод за манипулатор, който да обработва изключението. RuntimeExceptionЧесто се обработват изключения, които не са получени от ; изключения и грешки по време на работа рядко се обработват.

Защо грешките се обработват рядко

Рядко се обработват грешки, защото често няма нищо, което програма Java може да направи, за да се възстанови от грешката. Например, когато свободната памет е изчерпана, програма не може да разпредели допълнителна памет. Ако обаче неуспешното разпределение се дължи на задържане на много памет, която трябва да бъде освободена, хандър може да се опита да освободи паметта с помощта на JVM. Въпреки че манипулаторът може да изглежда полезен в този контекст на грешка, опитът може да не успее.

Манипулаторът е описан от catchблок, който следва tryблока. В catchблок осигурява заглавен ред, който изброява видовете изключения, че е готов да дръжка. Ако типът на хвърлимия е включен в списъка, хвърляният се предава на catchблока, чийто код се изпълнява. Кодът реагира на причината за отказ по такъв начин, че да накара програмата да продължи или евентуално да прекрати:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

В този фрагмент на код съм добавил catchблок към tryблока. Когато NullPointerExceptionобектът е хвърлен от method(), JVM локализира и предава изпълнението на catchблока, който извежда съобщение.

Накрая блокове

А tryблок или окончателното му catchблок може да бъде последвана от finallyблок, който се използва за извършване на почистване задачи, като например освобождаване на придобитите ресурси. Няма какво повече да кажа, finallyзащото това не е от значение за дискусията.

Изключенията, описани от Exceptionи неговите подкласове, с изключение на RuntimeExceptionи неговите подкласове, са известни като проверени изключения . За всеки throwизраз компилаторът проверява типа на обекта на изключение. Ако типът показва отметка, компилаторът проверява изходния код, за да гарантира, че изключението се обработва в метода, където е хвърлено или е декларирано, че се обработва по-нататък в стека на извикване на метод. Всички други изключения са известни като непроверени изключения .

Java ви позволява да декларирате, че провереното изключение се обработва по-нататък в стека на извикване на метод, като добавя throwsклауза (ключова дума, throwsпоследвана от списък с отметнати със запетаи имена на проверени класове на изключения) към заглавката на метода:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Тъй като IOExceptionе проверен тип изключение, хвърлените екземпляри на това изключение трябва да се обработват в метода, където са хвърлени, или да се декларират, че се обработват по-нататък в стека на извикване на метод, като се добавя throwsклауза към заглавката на всеки засегнат метод. В този случай throws IOExceptionкъм method()заглавката на ' е добавена клауза . Хвърленият IOExceptionобект се предава на JVM, който локализира и прехвърля изпълнението на catchманипулатора.

Аргументи за и против проверени изключения

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

Освен това срещнах аргумента, че приложенията трябва да обработват голям брой проверени изключения, генерирани от множеството библиотеки, до които имат достъп. Този проблем обаче може да бъде преодолян чрез хитро проектирана фасада, която използва съоръжението за верижно изключение на Java и пренасочването на изключения, за да намали значително броя на изключенията, които трябва да бъдат обработени, като същевременно се запази първоначалното изключение, което беше хвърлено.

Заключение

Проверените изключения добри ли са или са лоши? С други думи, трябва ли програмистите да бъдат принудени да се справят с проверени изключения или да им се даде възможност да ги игнорират? Харесва ми идеята за налагане на по-надежден софтуер. Въпреки това също мисля, че механизмът за обработка на изключения на Java трябва да се развива, за да стане по-удобен за програмисти. Ето няколко начина за подобряване на този механизъм: