Время на прочтение
8 мин
Количество просмотров 20K
Независимо от того, новичок вы или профессионал, всегда полезно освежить в памяти методы обработки исключений, чтобы убедиться, что вы и ваша команда можете справиться с проблемами.
Обработка исключений в Java — непростая тема. Новичкам сложно понять, и даже опытные разработчики могут часами обсуждать, как и какие исключения следует создавать или обрабатывать.
Вот почему у большинства команд разработчиков есть собственный набор правил их использования. И если вы новичок в команде, вас может удивить, насколько эти правила могут отличаться от тех, которые вы использовали раньше.
Тем не менее, есть несколько передовых практик, которые используются большинством команд. Вот 9 самых важных из них, которые помогут вам начать работу или улучшить обработку исключений.
1. Освободите ресурсы в блоке finally или используйте инструкцию «Try-With-Resource»
Довольно часто вы используете ресурс в своем блоке try, например InputStream, который вам нужно закрыть позже. Распространенной ошибкой в таких ситуациях является закрытие ресурса в конце блока try.
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// используем inputStream для чтения файла
// не делайте этого
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
Проблема в том, что этот подход работает отлично до тех пор, пока не генерируется исключение. Все операторы в блоке try будут выполнены, и ресурс будет закрыт.
Но вы не зря добавили блок try. Вы вызываете один или несколько методов, которые могут вызвать исключение, или, может быть, вы сами вызываете исключение. Это означает, что вы можете не дойти до конца блока try. И как следствие, вы не закроете ресурсы.
Поэтому вам следует поместить весь код очистки в блок finally или использовать оператор try-with-resource.
Используйте блок Finally
В отличие от последних нескольких строк вашего блока try, блок finally всегда выполняется. Это происходит либо после успешного выполнения блока try, либо после обработки исключения в блоке catch. Благодаря этому вы можете быть уверены, что освободите все захваченные ресурсы.
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// используем inputStream для чтения файла
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
Оператор Java 7 «Try-With-Resource»
Другой вариант — это оператор try-with-resource, который я объяснил более подробно во введении в обработку исключений Java.
Вы можете использовать его, если ваш ресурс реализует интерфейс AutoCloseable. Это то, что делает большинство стандартных ресурсов Java. Когда вы открываете ресурс в предложении try, он автоматически закрывается после выполнения блока try или обработки исключения.
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// используем inputStream для чтения файла
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
2. Конкретные исключения предпочтительнее
Чем конкретнее исключение, которое вы генерируете, тем лучше. Всегда помните, что коллеге, который не знает вашего кода, а может быть, и вам через несколько месяцев, необходимо вызвать ваш метод и обработать исключение.
Поэтому постарайтесь предоставить им как можно больше информации. Это упрощает понимание вашего API. В результате вызывающий ваш метод сможет лучше обработать исключение или избежать его с помощью дополнительной проверки.
Поэтому всегда старайтесь найти класс, который лучше всего подходит для вашего исключительного события, например, генерируйте NumberFormatException вместо IllegalArgumentException. И избегайте создания неспецифического исключения.
public void doNotDoThis() throws Exception {
...
}
public void doThis() throws NumberFormatException {
...
}
3. Документируйте определенные вами исключения
Каждый раз, когда вы определяете исключение в сигнатуре вашего метода, вы также должны задокументировать его в своем Javadoc. Это преследует ту же цель, что и предыдущая передовая практика: предоставить вызывающему как можно больше информации, чтобы он мог избежать или обработать исключение.
Итак, не забудьте добавить объявление @throws в свой Javadoc и описать ситуации, которые могут вызвать исключение.
/**
* Этот метод делает что-то чрезвычайно полезное ...
*
* @param input
* @throws MyBusinessException, если ... происходит
*/
public void doSomething(String input) throws MyBusinessException {
...
}
4. Генерирование исключений с описательными сообщениями
Идея, лежащая в основе этой передовой практики, аналогична двум предыдущим. Но на этот раз вы не предоставляете информацию вызывающей стороне вашего метода. Сообщение об исключении читают все, кто должен понимать, что произошло, когда исключение было зарегистрировано в файле журнала или в вашем инструменте мониторинга.
Следовательно, он должен как можно точнее описать проблему и предоставить наиболее актуальную информацию для понимания исключительного события.
Не поймите меня неправильно; вы не должны писать абзац текста. Но вам следует объяснить причину исключения в 1-2 коротких предложениях. Это помогает вашей группе эксплуатации понять серьезность проблемы, а также упрощает анализ любых инцидентов, связанных с обслуживанием.
Если вы выберете конкретное исключение, его имя класса, скорее всего, уже будет описывать тип ошибки. Таким образом, вам не нужно предоставлять много дополнительной информации. Хорошим примером этого является NumberFormatException. Оно вызывается конструктором класса java.lang.Long, когда вы предоставляете String в неправильном формате.
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
Название класса NumberFormatException уже говорит вам о типе проблемы. Его сообщение должно содержать только строку ввода, которая вызвала проблему. Если имя класса исключения не так выразительно, вам необходимо предоставить необходимую информацию в сообщении.
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
5. Сначала перехватите наиболее конкретное исключение
Большинство IDE помогут вам в этой лучшей практике. Они сообщают о недостижимом блоке кода, когда вы сначала пытаетесь перехватить менее конкретное исключение.
Проблема в том, что выполняется только первый блок catch, соответствующий исключению. Итак, если вы сначала поймаете IllegalArgumentException, вы никогда не достигнете блока catch, который должен обрабатывать более конкретное NumberFormatException, потому что это подкласс IllegalArgumentException.
Всегда сначала перехватывайте наиболее конкретный класс исключения и добавляйте менее конкретные блоки перехвата в конец вашего списка.
Пример такого оператора try-catch представлен в следующем фрагменте кода. Первый блок catch обрабатывает все NumberFormatException, а второй — все IllegalArgumentException, которые не являются NumberFormatException.
public void catchMostSpecificExceptionFirst() {
try {
doSomething("Сообщение");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
6. Не перехватывайте Throwable
Throwable — это суперкласс всех исключений и ошибок. Вы можете использовать его в предложении catch, но никогда не должны этого делать!
Если вы используете Throwable в предложении catch, он не только перехватит все исключения; он также перехватит все ошибки. JVM выдает ошибки, чтобы указать на серьезные проблемы, которые не предназначены для обработки приложением. Типичными примерами этого являются OutOfMemoryError или StackOverflowError. И то, и другое вызвано ситуациями, которые находятся вне контроля приложения и не могут быть обработаны.
Итак, лучше не перехватывайте Throwable, если вы не абсолютно уверены, что находитесь в исключительной ситуации, в которой вы можете или обязаны обрабатывать ошибку.
public void doNotCatchThrowable() {
try {
// делает что-нибудь
} catch (Throwable t) {
// не делает этого!
}
}
7. Не игнорируйте исключения
Вы когда-нибудь анализировали отчет об ошибке, в котором выполнялась только первая часть вашего сценария использования?
Часто это вызвано игнорируемым исключением. Разработчик, вероятно, был уверен, что оно никогда не будет вызвано, и добавил блок catch, который не обрабатывает и не регистрирует его. И когда вы найдете этот блок, вы, скорее всего, даже найдете один из известных комментариев «Этого никогда не будет».
public void doNotIgnoreExceptions() {
try {
// делает что-нибудь
} catch (NumberFormatException e) {
// это никогда не выполнится
}
}
Что ж, возможно, вы анализируете проблему, в которой произошло невозможное.
Поэтому, пожалуйста, никогда не игнорируйте исключения. Вы не знаете, как код изменится в будущем. Кто-то может удалить проверку, которая предотвратила исключительное событие, не осознавая, что это создает проблему. Или код, который генерирует исключение, изменяется и теперь генерирует несколько исключений одного и того же класса, а вызывающий код не предотвращает их все.
Вы должны хотя бы написать сообщение в журнале, сообщающее всем, что произошло немыслимое и что кто-то должен это проверить.
public void logAnException() {
try {
// делает что-нибудь
} catch (NumberFormatException e) {
log.error("Это никогда не должно происходить: " + e);
}
}
8. Не пишите в лог сгенерированные исключения
Это, вероятно, наиболее часто игнорируемая передовая практика в списке. Вы можете найти множество фрагментов кода и даже библиотек, в которых исключение перехватывается, регистрируется и повторно генерируется.
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
Может показаться интуитивно понятным регистрировать исключение, когда оно произошло, а затем повторно генерировать его, чтобы вызывающий мог обработать его соответствующим образом. Но, в таком случае, приложение будет писать в лог несколько сообщений об ошибках для одного и того же исключения.
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
Повторные сообщения также не добавляют никакой информации. Как объясняется в лучшей практике №4, сообщение об исключении должно описывать исключительное событие. А трассировка стека сообщает вам, в каком классе, методе и строке было сгенерировано исключение.
Если вам нужно добавить дополнительную информацию, вы должны перехватить исключение и обернуть его в пользовательское. Но обязательно следуйте передовой практике номер 9.
public void wrapException(String input) throws MyBusinessException {
try {
// делает что-нибудь
} catch (NumberFormatException e) {
throw new MyBusinessException("Сообщение с описанием ошибки.", e);
}
}
Итак, перехватывайте исключение, только если вы хотите его обработать. В противном случае укажите это в сигнатуре метода и позвольте вызывающей стороне позаботиться об этом.
9. Оберните исключение, не обрабатывая его
Иногда лучше поймать стандартное исключение и превратить его в настраиваемое. Типичным примером такого исключения является бизнес-исключение для конкретного приложения или платформы. Это позволяет вам добавлять дополнительную информацию, а также вы можете реализовать специальную обработку для вашего класса исключения.
Когда вы это сделаете, обязательно установите исходное исключение в качестве причины. Класс Exception предоставляет определенные методы конструктора, которые принимают Throwable в качестве параметра. В противном случае вы потеряете трассировку стека и сообщение об исходном исключении, что затруднит анализ исключительного события, вызвавшего ваше исключение.
public void wrapException(String input) throws MyBusinessException {
try {
// делает что-нибудь
} catch (NumberFormatException e) {
throw new MyBusinessException("Сообщение с описанием ошибки.", e);
}
}
Резюме
Как вы видели, есть много разных вещей, которые вы должны учитывать, когда генерируете или перехватываете исключение. Большинство из них имеют цель улучшить читаемость вашего кода или удобство использования вашего API.
Чаще всего исключения являются одновременно механизмом обработки ошибок и средством связи. Поэтому вам следует обязательно обсудить передовые практики и правила, которые вы хотите применять, со своими коллегами, чтобы все понимали общие концепции и использовали их одинаково.
➜
Catching and Handling Exceptions
➜
Catching and Handling Exceptions
This section describes how to use the three exception handler components — the try
, catch
, and finally
blocks — to write an exception handler. Then, the try-with-resources statement, introduced in Java SE 7, is explained. The try-with-resources statement is particularly suited to situations that use Closeable
resources, such as streams.
The last part of this section walks through an example and analyzes what occurs during various scenarios.
The following example defines and implements a class named ListOfNumbers
. When constructed, ListOfNumbers
creates an ArrayList
that contains 10 Integer
elements with sequential values 0 through 9. The ListOfNumbers
class also defines a method named writeList()
, which writes the list of numbers into a text file called OutFile.txt
. This example uses output classes defined in java.io
, which are covered in the Basic I/O section.
// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;
public class ListOfNumbers {
private List<Integer> list;
private static final int SIZE = 10;
public ListOfNumbers () {
list = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(i);
}
}
public void writeList() {
// The FileWriter constructor throws IOException, which must be caught.
PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
// The get(int) method throws IndexOutOfBoundsException, which must be caught.
out.println("Value at: " + i + " = " + list.get(i));
}
out.close();
}
}
The first line in boldface is a call to a constructor. The constructor initializes an output stream on a file. If the file cannot be opened, the constructor throws an IOException
. The second boldface line is a call to the ArrayList
class’s get method, which throws an IndexOutOfBoundsException
if the value of its argument is too small (less than 0) or too large (more than the number of elements currently contained by the ArrayList
.
If you try to compile the ListOfNumbers
class, the compiler prints an error message about the exception thrown by the FileWriter
constructor. However, it does not display an error message about the exception thrown by get()
. The reason is that the exception thrown by the constructor, IOException
, is a checked exception, and the one thrown by the get()
method, IndexOutOfBoundsException
, is an unchecked exception.
Now that you’re familiar with the ListOfNumbers
class and where the exceptions can be thrown within it, you’re ready to write exception handlers to catch and handle those exceptions.
The Try Block
The first step in constructing an exception handler is to enclose the code that might throw an exception within a try
block. In general, a try
block looks like the following:
try {
code
}
catch and finally blocks . . .
The segment in the example labeled code contains one or more legal lines of code that could throw an exception. (The catch
and finally
blocks are explained in the next two subsections.)
To construct an exception handler for the writeList()
method from the ListOfNumbers
class, enclose the exception-throwing statements of the writeList()
method within a try
block. There is more than one way to do this. You can put each line of code that might throw an exception within its own try block and provide separate exception handlers for each. Or, you can put all the writeList()
code within a single try
block and associate multiple handlers with it. The following listing uses one try
block for the entire method because the code in question is very short.
private List<Integer> list;
private static final int SIZE = 10;
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entered try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
catch and finally blocks . . .
}
If an exception occurs within the try
block, that exception is handled by an exception handler associated with it. To associate an exception handler with a try
block, you must put a catch
block after it; the next section, The catch Blocks, shows you how.
The Catch Blocks
You associate exception handlers with a try
block by providing one or more catch blocks directly after the try
block. No code can be between the end of the try
block and the beginning of the first catch
block.
try {
} catch (ExceptionType name) {
} catch (ExceptionType name) {
}
Each catch
block is an exception handler that handles the type of exception indicated by its argument. The argument type, ExceptionType
, declares the type of exception that the handler can handle and must be the name of a class that inherits from the Throwable
class. The handler can refer to the exception with name.
The catch
block contains code that is executed if and when the exception handler is invoked. The runtime system invokes the exception handler when the handler is the first one in the call stack whose ExceptionType
matches the type of the exception thrown. The system considers it a match if the thrown object can legally be assigned to the exception handler’s argument.
The following are two exception handlers for the writeList()
method:
try {
} catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
}
Exception handlers can do more than just print error messages or halt the program. They can do error recovery, prompt the user to make a decision, or propagate the error up to a higher-level handler using chained exceptions, as described in the Chained Exceptions section.
Multi-Catching Exceptions
You can catch more than one type of exception with one exception handler, with the multi-catch pattern.
In Java SE 7 and later, a single catch
block can handle more than one type of exception. This feature can reduce code duplication and lessen the temptation to catch an overly broad exception.
In the catch
clause, specify the types of exceptions that block can handle, and separate each exception type with a vertical bar (|
):
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
Note: If a catch block handles more than one exception type, then the catch
parameter is implicitly final
. In this example, the catch
parameter ex
is final
and therefore you cannot assign any values to it within the catch
block.
The Finally Block
The finally block always executes when the try
block exits. This ensures that the finally
block is executed even if an unexpected exception occurs. But finally
is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return
, continue
, or break
. Putting cleanup code in a finally
block is always a good practice, even when no exceptions are anticipated.
Note: If the JVM exits while the
try
orcatch
code is being executed, then thefinally
block may not execute.
The try
block of the writeList()
method that you’ve been working with here opens a PrintWriter
. The program should close that stream before exiting the writeList()
method. This poses a somewhat complicated problem because writeList()
‘s try
block can exit in one of three ways.
- The new
FileWriter
statement fails and throws anIOException
. - The
list.get(i)
statement fails and throws anIndexOutOfBoundsException
. - Everything succeeds and the
try
block exits normally.
The runtime system always executes the statements within the finally
block regardless of what happens within the try
block. So it’s the perfect place to perform cleanup.
The following finally
block for the writeList()
method cleans up and then closes the PrintWriter
.
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
} else {
System.out.println("PrintWriter not open");
}
}
Important: The
finally
block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in afinally
block to ensure that resource is always recovered.Consider using the try-with-resources statement in these situations, which automatically releases system resources when no longer needed. The try-with-resources Statement section has more information.
The Try-with-resources Statement
The try-with-resources statement is a try
statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable
, which includes all objects which implement java.io.Closeable
, can be used as a resource.
The following example reads the first line from a file. It uses an instance of BufferedReader
to read data from the file. BufferedReader
is a resource that must be closed after the program is finished with it:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
In this example, the resource declared in the try-with-resources statement is a BufferedReader
. The declaration statement appears within parentheses immediately after the try
keyword. The class BufferedReader
, in Java SE 7 and later, implements the interface java.lang.AutoCloseable
. Because the BufferedReader
instance is declared in a try-with-resource statement, it will be closed regardless of whether the try
statement completes normally or abruptly (as a result of the method BufferedReader.readLine()
throwing an IOException
.
Prior to Java SE 7, you can use a finally
block to ensure that a resource is closed regardless of whether the try
statement completes normally or abruptly. The following example uses a finally
block instead of a try-with-resources statement:
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
However, in this example, if the methods readLine()
and close both throw exceptions, then the method readFirstLineFromFileWithFinallyBlock()
throws the exception thrown from the finally
block; the exception thrown from the try
block is suppressed. In contrast, in the example readFirstLineFromFile()
, if exceptions are thrown from both the try
block and the try-with-resources statement, then the method readFirstLineFromFile()
throws the exception thrown from the try
block; the exception thrown from the try-with-resources block is suppressed. In Java SE 7 and later, you can retrieve suppressed exceptions; see the section Suppressed Exceptions for more information.
You may declare one or more resources in a try-with-resources statement. The following example retrieves the names of the files packaged in the zip file zipFileName
and creates a text file that contains the names of these files:
public static void writeToFileZipFileContents(String zipFileName,
String outputFileName)
throws java.io.IOException {
java.nio.charset.Charset charset =
java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath =
java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with
// try-with-resources statement
try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements();) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName =
((java.util.zip.ZipEntry)entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
In this example, the try-with-resources statement contains two declarations that are separated by a semicolon: ZipFile
and BufferedWriter
. When the block of code that directly follows it terminates, either normally or because of an exception, the close()
methods of the BufferedWriter
and ZipFile
objects are automatically called in this order. Note that the close methods of resources are called in the opposite order of their creation.
The following example uses a try-with-resources statement to automatically close a java.sql.Statement
object:
public static void viewTable(Connection con) throws SQLException {
String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " +
price + ", " + sales + ", " + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
The resource java.sql.Statement
used in this example is part of the JDBC 4.1 and later API.
Note: A try-with-resources statement can have catch
and finally
blocks just like an ordinary try
statement. In a try-with-resources statement, any catch
or finally
block is run after the resources declared have been closed.
Suppressed Exceptions
An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents()
, an exception can be thrown from the try
block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile
and BufferedWriter
objects. If an exception is thrown from the try
block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents()
method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed()
method from the exception thrown by the try
block.
Classes That Implement the AutoCloseable or Closeable Interface
See the Javadoc of the AutoCloseable
and Closeable
interfaces for a list of classes that implement either of these interfaces. The Closeable
interface extends the AutoCloseable
interface. The close()
method of the Closeable
interface throws exceptions of type IOException
while the close()
method of the AutoCloseable
interface throws exceptions of type Exception
. Consequently, subclasses of the AutoCloseable
interface can override this behavior of the close()
method to throw specialized exceptions, such as IOException
, or no exception at all.
Putting It All Together
The previous sections described how to construct the try
, catch
, and finally
code blocks for the writeList()
method in the ListOfNumbers
class. Now, let’s walk through the code and investigate what can happen.
When all the components are put together, the writeList()
method looks like the following.
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering" + " try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
As mentioned previously, this method’s try
block has three different exit possibilities; here are two of them.
- Code in the
try
statement fails and throws an exception. This could be anIOException
caused by the newFileWriter
statement or anIndexOutOfBoundsException
caused by a wrong index value in thefor
loop. - Everything succeeds and the
try
statement exits normally.
Let’s look at what happens in the writeList()
method during these two exit possibilities.
Scenario 1: An Exception Occurs
The statement that creates a FileWriter
can fail for a number of reasons. For example, the constructor for the FileWriter
throws an IOException
if the program cannot create or write to the file indicated.
When FileWriter
throws an IOException
, the runtime system immediately stops executing the try
block; method calls being executed are not completed. The runtime system then starts searching at the top of the method call stack for an appropriate exception handler. In this example, when the IOException
occurs, the FileWriter
constructor is at the top of the call stack. However, the FileWriter
constructor doesn’t have an appropriate exception handler, so the runtime system checks the next method — the writeList()
method — in the method call stack. The writeList()
method has two exception handlers: one for IOException
and one for IndexOutOfBoundsException
.
The runtime system checks writeList()
‘s handlers in the order in which they appear after the try
statement. The argument to the first exception handler is IndexOutOfBoundsException
. This does not match the type of exception thrown, so the runtime system checks the next exception handler — IOException
. This matches the type of exception that was thrown, so the runtime system ends its search for an appropriate exception handler. Now that the runtime has found an appropriate handler, the code in that catch
block is executed.
After the exception handler executes, the runtime system passes control to the finally
block. Code in the finally
block executes regardless of the exception caught above it. In this scenario, the FileWriter
was never opened and doesn’t need to be closed. After the finally
block finishes executing, the program continues with the first statement after the finally
block.
Here’s the complete output from the ListOfNumbers
program that appears when an IOException
is thrown.
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open
Scenario 2: The try Block Exits Normally
In this scenario, all the statements within the scope of the try
block execute successfully and throw no exceptions. Execution falls off the end of the try
block, and the runtime system passes control to the finally
block. Because everything was successful, the PrintWriter
is open when control reaches the finally
block, which closes the PrintWriter
. Again, after the finally
block finishes executing, the program continues with the first statement after the finally
block.
Here is the output from the ListOfNumbers
program when no exceptions are thrown.
Entering try statement
Closing PrintWriter
➜
Catching and Handling Exceptions
➜
Исключение (exception) — это ненормальная ситуация (термин «исключение» здесь следует понимать как «исключительная ситуация»), возникающая во время выполнения программного кода. Иными словами, исключение — это ошибка, возникающая во время выполнения программы (в runtime).
Исключение — это способ системы Java (в частности, JVM — виртуальной машины Java) сообщить вашей программе, что в коде произошла ошибка. К примеру, это может быть деление на ноль, попытка обратиться к массиву по несуществующему индексу, очень распространенная ошибка нулевого указателя (NullPointerException
) — когда вы обращаетесь к ссылочной переменной, у которой значение равно null и так далее.
В любом случае, с формальной точки зрения, Java не может продолжать выполнение программы.
Обработка исключений (exception handling) — название объектно-ориентированной техники, которая пытается разрешить эти ошибки.
Программа в Java может сгенерировать различные исключения, например:
-
программа может пытаться прочитать файл из диска, но файл не существует;
-
программа может попытаться записать файл на диск, но диск заполнен или не отформатирован;
-
программа может попросить пользователя ввести данные, но пользователь ввел данные неверного типа;
-
программа может попытаться осуществить деление на ноль;
-
программа может попытаться обратиться к массиву по несуществующему индексу.
Используя подсистему обработки исключений Java, можно управлять реакцией программы на появление ошибок во время выполнения. Средства обработки исключений в том или ином виде имеются практически во всех современных языках программирования. В Java подобные инструменты отличаются большей гибкостью, понятнее и удобнее в применении по сравнению с большинством других языков программирования.
Преимущество обработки исключений заключается в том, что она предусматривает автоматическую реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код.
В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable
. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable
имеются два непосредственных подкласса: Exception
и Error
.
Исключения типа Error
относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в приложении, как правило, не предусматривается. В связи с этим исключения данного типа не будут рассматриваться в книге.
Ошибки, связанные с работой программы, представлены отдельными подклассами, производными от класса Exception
. В частности, к этой категории относятся ошибки деления на нуль, выхода за пределы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception
, является класс RuntimeException
, который служит для представления различных видов ошибок, часто возникающих во время выполнения программ.
Каждой исключительной ситуации поставлен в соответствие некоторый класс. Если подходящего класса не существует, то он может быть создан разработчиком.
Так как в Java ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ, то исключение тоже является объектом некоторого класса, который описывает исключительную ситуацию, возникающую в определенной части программного кода.
«Обработка исключений» работает следующим образом:
-
когда возникает исключительная ситуация, JVM генерирует (говорят, что JVM ВЫБРАСЫВАЕТ исключение, для описания этого процесса используется ключевое слово
throw
) объект исключения и передает его в метод, в котором произошло исключение; -
вы можете перехватить исключение (используется ключевое слово
catch
), чтобы его каким-то образом обработать. Для этого, необходимо определить специальный блок кода, который называется обработчиком исключений, этот блок будет выполнен при возникновении исключения, код должен содержать реакцию на исключительную ситуацию; -
таким образом, если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.
Если вы не предусмотрите обработчик исключений, то исключение будет перехвачено стандартным обработчиком Java. Стандартный обработчик прекратит выполнение программы и выведет сообщение об ошибке.
Рассмотрим пример исключения и реакцию стандартного обработчика Java.
public static void main(String[] args) {
System.out.println(5 / 0);
Мы видим, что стандартный обработчик вывел в консоль сообщение об ошибке. Давайте разберемся с содержимым этого сообщения:
«C:\Program Files\Java\jdk1.8.0_60\bin\java»…
Exception in thread «main» java.lang.ArithmeticException: / by zero
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code 1
Exception in thread «main» java.lang.ArithmeticException: / by zero
сообщает нам тип исключения, а именно класс ArithmeticException
(про классы исключений мы будем говорить позже), после чего сообщает, какая именно ошибка произошла. В нашем случае это деление на ноль.
at ua.opu.Main.main(Main.java:6)
в каком классе, методе и строке произошло исключение. Используя эту информацию, мы можем найти ту строчку кода, которая привела к исключительной ситуации, и предпринять какие-то действия. Строки
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
называются «трассировкой стека» (stack tracing). О каком стеке идет речь? Речь идет о стеке вызовов (call stack). Соответственно, эти строки означают последовательность вызванных методов, начиная от метода, в котором произошло исключение, заканчивая самым первым вызванным методом.
Для вызова методов в программе используется инструкция «call». Когда вы вызываете метод в программе, важно сохранить адрес следующей инструкции, чтобы, когда вызванный метод отработал, программа продолжила работу со следующей инструкции. Этот адрес нужно где-то хранить в памяти. Также перед вызовом необходимо сохранить аргументы функции, которые тоже необходимо где-то хранить.
Вся эта информация хранится в специальной структуре – стеке вызовов. Каждая запись в стеке вызовов называется кадром или фреймом (stack frame).
Таким образом, зная, какая строка привела к возникновению исключения, вы можете изменить код либо предусмотреть обработчик событий.
Как уже было сказано выше, исключение это объект некоторого класса. В Java существует разветвленная иерархия классов исключений.
В Java, класс исключения служит для описания типа исключения. Например, класс NullPointerException
описывает исключение нулевого указателя, а FileNotFoundException
означает исключение, когда файл, с которым пытается работать приложение, не найден. Рассмотрим иерархию классов исключений:
На самом верхнем уровне расположен класс Throwable
, который является базовым для всех исключений (как мы помним, JVM «выбрасывает» исключение», поэтому класс Throwable
означает – то, что может «выбросить» JVM).
От класса Throwable
наследуются классы Error
и Exception
. Среди подклассов Exception
отдельно выделен класс RuntimeException
, который играет важную роль в иерархии исключений.
В Java существует некоторая неопределенность насчет того – существует ли два или три вида исключений.
Если делить исключения на два вида, то это:
-
1.
контролируемые исключения (checked exceptions) – подклассы класса
Exception
, кроме подклассаRuntimeException
и его производных; -
2.
неконтролируемые исключения (unchecked exceptions) – класс
Error
с подклассами, а также классRuntimeException
и его производные;
В некоторых источниках класс Error
и его подклассы выделяют в отдельный вид исключений — ошибки (errors).
Далее мы видим класс Error
. Классы этой ветки составляют вид исключений, который можно обозначить как «ошибки» (errors). Ошибки представляют собой серьезные проблемы, которые не следует пытаться обработать в собственной программе, поскольку они связаны с проблемами уровня JVM.
На самом деле, вы конечно можете предпринять некоторые действия при возникновении ошибок, например, вывести сообщение для пользователя в удобном формате, выслать трассировку стека себе на почту, чтобы понять – что вообще произошло.
Но, по факту, вы ничего не можете предпринять в вашей программе, чтобы эту ошибку исправить, и ваша программа, как правило, при возникновении такой ошибки дальше работать не может.
В качестве примеров «ошибок» можно привести: переполнение стека вызова (класс StackOverflowError
); нехватка памяти в куче (класс OutOfMemoryError
), вследствие чего JVM не может выделить память под новый объект и сборщик мусора не помогает; ошибка виртуальной машины, вследствие которой она не может работать дальше (класс VirtualMachineError
) и так далее.
Несмотря на то, что в нашей программе мы никак не можем помочь этой проблеме, и приложение не может работать дальше (ну как может работать приложение, если стек вызовов переполнен или JVM не может дальше выполнять код?!); знание природы этих ошибок поможет вам предпринять некоторые действия, чтобы избежать этих ошибок в дальнейшем. Например, ошибки типа StackOverflowError
и OutOfMemoryError
могут быть следствием вашего некорректного кода.
Например, попробуем спровоцировать ошибку StackOverflowError
public static void main(String[] args) {
public static void methodA() {
private static void methodB() {
Получим такое сообщение об ошибке
Exception in thread «main» java.lang.StackOverflowError
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
Ошибка OutOfMemoryError
может быть вызвана тем, что ваш код, вследствие ошибки при программировании, создает очень большое количество массивных объектов, которые очень быстро заполняют кучу и свободного места не остается.
Exception in thread «main» java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3511)
at java.base/java.util.Arrays.copyOf(Arrays.java:3480)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:454)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at com.company.Main.main(Main.java:13)
Process finished with exit code 1
Ошибка VirtualMachineError
может означать, что следует переустановить библиотеки Java.
В любом случае, следует относиться к типу Error
не как к неизбежному злу и «воле богов», а просто как к сигналу к тому, что в вашем приложении что-то не так, или что-то не так с программным или аппаратным обеспечением, которое вы используете.
Класс Exception
описывает исключения, связанные непосредственно с работой программы. Такого рода исключения «решаемы» и их грамотная обработка позволит программе работать дальше в нормальном режиме.
В классе Exception описаны исключения двух видов: контролируемые исключения (checked exceptions) и неконтролируемые исключения (unchecked exceptions).
Неконтролируемые исключения содержатся в подклассе RuntimeException
и его наследниках. Контролируемые исключения содержатся в остальных подклассах Exception
.
В чем разница между контролируемыми и неконтролируемыми исключениями, мы узнаем позже, а теперь рассмотрим вопрос – а как же именно нам обрабатывать исключения?
Обработка исключений в методе может выполняться двумя способами:
-
1.
с помощью связки
try-catch
; -
2.
с помощью ключевого слова
throws
в сигнатуре метода.
Рассмотрим оба метода поподробнее:
Способ 1. Связка try-catch
Этот способ кратко можно описать следующим образом.
Код, который теоретически может вызвать исключение, записывается в блоке try{}
. Сразу за блоком try
идет блок код catch{}
, в котором содержится код, который будет выполнен в случае генерации исключения. В блоке finally{}
содержится код, который будет выполнен в любом случае – произошло ли исключение или нет.
Теперь разберемся с этим способом более подробно. Рассмотрим следующий пример – программу, которая складывает два числа, введенные пользователем из консоли
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(«Введите первое число: «);
String firstNumber = scanner.nextLine();
System.out.println(«Введите второе число: «);
String secondNumber = scanner.nextLine();
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
System.out.println(«Результат: « + (a + b));
Первое, что нам нужно определить – и что является главным при работе с исключениями, КАКАЯ ИНСТРУКЦИЯ МОЖЕТ ПРИВЕСТИ К ВОЗНИКНОВЕНИЮ ИСКЛЮЧЕНИЯ?
То есть, мы должны понять – где потенциально у нас может возникнуть исключение? Понятно, что речь идет не об операции сложения и не об операции чтения данных из консоли. Потенциально опасными строчками кода здесь являются строчки
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
в которых происходит преобразование ввода пользователя в целое число (метод parseInt()
преобразует цифры в строке в число).
Почему здесь может возникнуть исключение? Потому что пользователь может ввести не число, а просто какой-то текст и тогда непонятно – что записывать в переменную a
или b
. И да, действительно, если пользователь введет некорректное значение, возникнет исключение в методе Integer.parseInt()
.
Итак, что мы можем сделать. «Опасный код» нужно поместить в блок try{}
Обратите внимание на синтаксис блока try
. В самом простом случае это просто ключевое слово try
, после которого идут парные фигурные скобки. Внутри этих скобок и заключается «опасный» код, который может вызвать исключение. Сразу после блока try должен идти блок catch()
.
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
System.out.println(«Результат: « + (a + b));
Обратите внимание на синтаксис блока catch
. После ключевого слова, в скобках описывается аргумент с именем e
типа NumberFormatException
.
Когда произойдет исключение, то система Java прервет выполнение инструкций в блоке try
и передаст управление блоку catch
и запишет в этот аргумент объект исключения, который сгенерировала Java-машина.
То есть, как только в блоке try
возникнет исключение, то дальше инструкции в блоке try выполняться не будут! А сразу же начнут выполняться действия в блоке catch
.
Обработчик исключения находится в блоке catch
, в котором мы можем отреагировать на возникновение исключения. Также, в этом блоке нам будет доступен объект исключения, от которого мы можем получить дополнительные сведения об исключении.
Блок catch
сработает только в том случае, если указанный в скобках тип объекта исключения будет суперклассом или будет того же типа, что и объект исключения, который сгенерировала Java.
Например, если в нашем примере мы напишем код, который потенциально может выбросить исключение типа IOException
, но не изменим блок catch
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
тогда обработчик не будет вызван и исключение будет обработано стандартным обработчиком Java.
Способ 2. Использование ключевого слова throws
Второй способ позволяет передать обязанность обработки исключения тому методу, который вызывает данный метод (а тот, в свою очередь может передать эту обязанность выше и т.д.).
Изменим наш пример и выделим в отдельный метод код, который будет запрашивать у пользователя число и возвращать его как результат работы метода
public static void main(String[] args) {
int a = getNumberFromConsole(«Введите первое число»);
int b = getNumberFromConsole(«Введите второе число»);
System.out.println(«Результат: « + (a + b));
public static int getNumberFromConsole(String message) {
Scanner scanner = new Scanner(System.in);
System.out.print(message + «: «);
String s = scanner.nextLine();
return Integer.parseInt(s);
Мы понимаем, что в данном методе может произойти исключение, но мы не хотим или не можем его обработать. Причины могут быть разными, например:
-
1.
обработка исключений может происходить централизованно однотипным способом (например, показ окошка с сообщением и с определенным текстом);
-
2.
это не входит в нашу компетенцию как программиста – обработкой исключений занимается другой программист;
-
3.
мы пишем только некоторую часть программы и непонятно – как будет обрабатывать исключение другой программист, который потом будет использовать наш код (например, мы пишем просто какую-то библиотеку, которая производит вычисления, и как будет выглядеть обработка – это не наше дело).
В любом случае, мы знаем, что в этом коде может быть исключение, но мы не хотим его обрабатывать, а хотим просто предупредить другой метод, который будет вызывать наш код, что выполнение кода может привести к исключению. В этом случае, используется ключевое слово throws
, которое указывается в сигнатуре метода
public static int getNumberFromConsole(String message) throws NumberFormatException {
Scanner scanner = new Scanner(System.in);
System.out.print(message + «: «);
String s = scanner.nextLine();
return Integer.parseInt(s);
Обратите внимание на расположение сигнатуру метода. Мы привыкли, что при объявлении метода сразу после скобок входных аргументов мы открываем фигурную скобку и записываем тело метода. Здесь же, после входных аргументов, мы пишем ключевое слово throws
и потом указываем тип исключения, которое может быть сгенерировано в нашем методе. Если метод может выбрасывать несколько типов исключений, они записываются через запятую
public static void foo() throws NumberFormatException, ArithmeticException, IOException {
Тогда, в методе main мы должны написать примерно следующее
public static void main(String[] args) {
a = getNumberFromConsole(«Введите первое число»);
b = getNumberFromConsole(«Введите второе число»);
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
System.out.println(«Результат: « + (a + b));
Основное преимущество этого подхода – мы передаем обязанность по обработке исключений другому, вышестоящему методу.
Отличия между контролируемыми и неконтролируемыми исключениями
Отличия между контролируемыми и неконтролируемыми исключениями
Если вы вызываете метод, который выбрасывает checked исключение, то вы ОБЯЗАНЫ предусмотреть обработку возможного исключения, то есть связку try-catch
.
Яркий пример checked исключения – класс IOException
и его подклассы.
Рассмотрим пример – попробуем прочитать файл и построчно вывести его содержимое на экран консоли:
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Как мы видим, компилятор не хочет компилировать наш код. Чем же он недоволен? У нас в коде происходит вызов двух методов – статического метода Files.newBufferedReader()
и обычного метода BufferedReader.readLine()
.
Если посмотреть на сигнатуры этих методов то можно увидеть, что оба этих метода выбрасывают исключения типа IOException
. Этот тип исключения относится к checked-исключению и поэтому, если вы вызываете эти методы, компилятор ТРЕБУЕТ от вас предусмотреть блок catch
, либо в самом вашем методе указать throws IOException
и, таким образом, передать обязанность обрабатывать исключение другому методу, который будет вызывать ваш.
Таким образом, «оборачиваем» наш код в блок try
и пишем блок catch
.
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
Еще один способ — указать в сигнатуре метода, что он выбрасывает исключение типа IOException
и переложить обязанность обработать ошибку в вызывающем коде
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Eсли метод выбрасывает checked-исключение, то проверка на наличие catch-блока происходит на этапе компиляции. И вы обязаны предусмотреть обработку исключения для checked-исключения.
Что касается unchecked-исключения, то обязательной обработки исключения нет – вы можете оставить подобные ситуации без обработки.
Зачем необходимо наличие двух видов исключений?
Зачем необходимо наличие двух видов исключений?
В большинстве языков существует всего лишь один тип исключений – unchecked. Некоторые языки, например, C#, в свое время отказались от checked-исключений.
Во-первых, мы не можем сделать все исключения checked, т.к. очень многие операции могут генерировать исключения, и если каждый такой участок кода «оборачивать» в блок try-catch
, то код получится слишком громоздким и нечитабельным.
С другой стороны, зачем нужно делать некоторые типы исключений checked? Почему просто не сделать все исключения unchecked и оставить решения об обработке исключений целиком на совести программиста?
В официальной документации написано, что unchecked-исключения – это те исключения, от которых программа «не может восстановиться», тогда как checked-исключения позволяют откатить некоторую операцию и повторить ее снова.
На самом деле, если вы посмотрите на различные типы unchecked-исключений, то вы увидите, что большинство их связаны с ошибками самого программиста. Выход за пределы массива, исключение нулевого указателя, деление на ноль – большинство из подобного рода исключений целиком лежат на совести программистов. Тогда мы можем сказать, что лучше программист пишет более хороший код, чем везде вставляет проверки на исключения.
Контролируемые исключения, как правило, представляют те ошибки, которые возникают не из-за программиста и предусмотреть которые программист не может. Например, это отсутствующие файлы, работа с сокетами, подключение к базе данных, сетевые соединения, некорректный пользовательский ввод.
Вы можете написать идеальный код, но потом вы отдадите приложение пользователю, а он введет название файла, которого нет или напишет неправильный IP для сокет-соединения. Таким образом, мы заранее должны быть готовыми к неверным действиям пользователя или к программным или аппаратным проблемам на его стороне и в обязательном порядке предусмотреть обработку возможных исключений.
Дополнительно об исключениях
Дополнительно об исключениях
Рассмотрим детально различные возможности механизма исключений, которые позволяют программисту максимально эффективно противодействовать исключениям:
Java позволяет вам для одного блока try
предусмотреть несколько блоков catch
, каждый из которых должен обрабатывать свой тип исключения
public static void foo() {
} catch (ArithmeticException e) {
// обработка арифметического исключения
} catch (IndexOutOfBoundsException e) {
// обработка выхода за пределы коллекции
} catch (IllegalArgumentException e) {
// обработка некорректного аргумента
Важно помнить, что Java обрабатывает исключения последовательно. Java просматривает блок catch сверху вниз и выполняет первый подходящий блок, который может обработать данное исключение.
Так как вы можете указать как точный класс, так и суперкласс, то если первым блоком будет блок для суперкласса – выполнится он. Например, исключение FileNotFoundException
является подклассом IOException
. И поэтому если вы первым поставите блок с IOException
– он будет вызываться для всех подтипов исключений, в том числе и для FileNotFoundException
и блок c FileNotFoundException
никогда не выполнится.
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
} catch (FileNotFoundException e) {
// данный блок никогда не будет вызван
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Один блок для обработки нескольких типов исключений
Один блок для обработки нескольких типов исключений
Начиная с версии Java 7, вы можете использовать один блок catch
для обработки исключений нескольких, не связанных друг с другом типов. Приведем пример
public static void foo() {
} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {
// три типа исключений обрабатываются одинаково
Как мы видим, один блок catch используется для обработки и типа IOException
и NullPointerException
и NumberFormaException
.
Вы можете использовать вложенные блоки try
, которые могут помещаться в других блоках try
. После вложенного блока try
обязательно идет блок catch
public static void foo() {
} catch (IllegalArgumentException e) {
// обработка вложенного блока try
} catch (ArithmeticException e) {
Выбрасывание исключения с помощью ключевого слова throw
С помощью ключевого слова throw
вы можете преднамеренно «выбросить» определенный тип исключения.
public static void foo(int a) {
throw new IllegalArgumentException(«Аргумент не может быть отрицательным!»);
Кроме блока try
и catch
существует специальный блок finally
. Его отличительная особенность – он гарантированно отработает, вне зависимости от того, будет выброшено исключение в блоке try
или нет. Как правило, блок finally
используется для того, чтобы выполнить некоторые «завершающие» операции, которые могли быть инициированы в блоке try
.
public static void foo(int a) {
FileOutputStream fout = null;
File file = new File(«file.txt»);
fout = new FileOutputStream(file);
} catch (IOException e) {
// обработка исключения при записи в файл
} catch (IOException e) {
При любом развитии события в блоке try
, код в блоке finally
отработает в любом случае.
Блок finally
отработает, даже если в try-catch
присутствует оператор return
.
Как правило, блок finally
используется, когда мы в блоке try
работаем с ресурсами (файлы, базы данных, сокеты и т.д.), когда по окончании блока try-catch
мы освобождаем ресурсы. Например, допустим, в процессе работы программы возникло исключение, требующее ее преждевременного закрытия. Но в программе открыт файл или установлено сетевое соединение, а, следовательно, файл нужно закрыть, а соединение – разорвать. Для этого удобно использовать блок finally
.
Блок try-with-resources
является модификацией блока try
. Данный блок позволяет автоматически закрывать ресурс после окончания работы блока try
и является удобной альтернативой блоку finally
.
public static void foo() {
Path p = Paths.get(«c:\\temp\\file.txt»);
try (BufferedReader reader = Files.newBufferedReader(p)) {
while ((line = reader.readLine()) != null)
System.out.println(line);
} catch (IOException e) {
Внутри скобок блока try
объявляется один или несколько ресурсов, которые после отработки блока try-catch
будут автоматически освобождены. Для этого объект ресурса должен реализовывать интерфейс java.lang.AutoCloseable
.
Создание собственных подклассов исключений
Создание собственных подклассов исключений
Встроенные в Java исключения позволяют обрабатывать большинство распространенных ошибок. Тем не менее, вы можете создавать и обрабатывать собственные типы исключений. Для того, чтобы создать класс собственного исключения, достаточно определить как его произвольный от Exception
или от RuntimeException
(в зависимости от того, хотите ли вы использовать checked или unchecked – исключения).
Насчет создания рекомендуется придерживаться двух правил:
-
1.
определитесь, исключения какого типа вы хотите использовать для собственных исключений (checked или unchecked) и старайтесь создавать исключения только этого типа;
-
2.
старайтесь максимально использовать стандартные типы исключений и создавать свои типы только в том случае, если существующие типы исключений не отражают суть того исключения, которое вы хотите добавить.
Плохие практики при обработке исключений
Плохие практики при обработке исключений
Ниже представлены действия по обработке ошибок, которые характерны для плохого программиста. Ни в коем случае не рекомендуется их повторять!
-
1.
Указание в блоке catch объекта исключения типа Exception. Существует очень большой соблазн при создании блока
catch
указать тип исключенияException
и, таким образом, перехватывать все исключения, которые относятся к этому классу (а это все исключения, кроме системных ошибок). Делать так крайне не рекомендуется, т.к. вместо того чтобы решать проблему с исключениями, мы фактически игнорируем ее и просто реализуем некоторую «заглушку», чтобы приложение продолжило работу дальше. Кроме того, каждый тип исключения должен быть обработан своим определенным образом. -
2.
Помещение в блок
try
всего тела метода. Следующий плохой прием используется, когда программист не хочет разбираться с кодом, который вызывает исключение и просто, опять же, реализует «заглушку». Этот прием очень «хорошо» сочетается с первым приемом. В блокtry
должен помещаться только тот код, который потенциально может вызвать исключение, а не всё подряд, т.к. лень обрабатывать исключения нормально. -
3.
Игнорирование исключения. Следующий плохой прием состоит в том, что мы просто игнорируем исключение и оставляем блок
catch
пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.
Исключение — ошибка, которая нарушает нормальную работу программы. Java обеспечивает надежный объектно-ориентированный способ обработки исключений. Именно его мы и будем изучать в этом руководстве.
Исключение может возникнуть в разного рода ситуациях: неправильные входные данные, аппаратный сбой, сбоя сетевого соединения, ошибка при работе с базой данных и т.д. Именно поэтому любой Java программист должен уметь правильно обрабатывать исключения, понимать причины их появления и следовать лучшим практикам работы с исключениями даже в небольших проектах.
Java — объектно-ориентированный язык программирования, поэтому всякий раз, когда происходит ошибка при выполнении инструкции, создается объект-исключение, а затем нормальный ход выполнения программы останавливается и JRE пытается найти кого-то, кто может справиться (обработать) это исключение. Объект-исключение содержит много информации об отладке, а именно номер строки, где произошло исключение, тип исключения и т.д.
Что и как происходит, когда появляется ошибка
Когда в методе происходит исключение, то процесс создания объекта-исключения и передачи его в Runtime Environment называется «бросать исключение».
После создания исключения, Java Runtime Environment пытается найти обработчик исключения.
Обработчик исключения — блок кода, который может обрабатывать объект-исключение.
Логика нахождения обработчика исключений проста — прежде всего начинается поиск в методе, где возникла ошибка, если соответствующий обработчик не найден, то происходит переход к тому методу, который вызывает этот метод и так далее.
Пример
У нас есть 3 метода, каждый из которых вызывает друг-друга:
А -> В -> С
(А вызывает В, а В вызывает С). Если исключение появляется в методе C, то поиск соответствующего обработчика будет происходить в обратном порядке:С -> В -> А
(сначала там, где было исключение — в С, если там нет обработчика, то идем в метод В — если тут тоже нет, то идем в А).
Если соответствующий обработчик исключений будет найден, то объект-исключение передаётся обработчику.
Обработать исключение — значит «поймать исключение».
Если обработчик исключений не был найден, то программа завершает работу и печатает информации об исключении.
Обратите внимание, что обработка исключений в Java — это фреймворк, который используется только для обработки ошибок времени выполнения. Ошибки компиляции не обрабатываются рамках обработки исключений.
Основные элементы обработки исключений в Java
Мы используем определенные ключевые слова в для создания блока обработки исключений. Давайте рассмотрим их на примере. Также мы напишем простую программу для обработки исключений.
- Бросить исключение (
throw
) — ключевое слово, которое используется для того, чтобы бросить исключение во время выполнения. Мы знаем, что Java Runtime начинает поиск обработчика исключений как только оно будет брошено, но часто нам самим нужно генерировать исключение в нашем коде, например, в программе авторизации, если какое-то полеnull
. Именно для таких случаем и существует возможность бросить исключение. throws
— когда мы бросаем исключение в методе и не обрабатываем его, то мы должны использовать ключевое словоthrows
в сигнатуре метода для того, чтобы пробросить исключение для обработки в другом методе. Вызывающий метод может обработать это исключение или пробросить его еще дальше с помощьюthrows
в сигнатуре метода. Следует отметить, что пробрасывать можно сразу несколько исключений.- Блок
try-catch
используется для обработки исключений в коде. Словоtry
— это начало блока обработки,catch
— конец блока для обработки исключений. Мы можем использовать сразу несколько блоковcatch
при одномtry
.catch
в качестве параметра принимает тип исключения для обработки. finally
— необязательная завершающая конструкция блокаtry-catch
. Как только исключение остановило процесс исполнения программы, вfinally
мы можем безопасно освободить какие-то открытые ресурсы. Следует отметить, чтоfinally
блок выполняется всегда — не смотря на появление исключительной ситуации.
Давайте посмотрим простую программу обработки исключений в Java.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package ua.com.prologistic; import java.io.FileNotFoundException; import java.io.IOException; public class ExceptionHandling { // в методе main() пробрасывается сразу несколько исключений public static void main(String[] args) throws FileNotFoundException, IOException { // в блоке try-catch перехватываются сразу несколько исключений вызовом дополнительного catch(…) try{ testException(—5); testException(—10); }catch(FileNotFoundException e){ e.printStackTrace(); }catch(IOException e){ e.printStackTrace(); }finally{ System.out.println(«Необязательный блок, но раз уже написан, то выполнятся будет не зависимо от того было исключение или нет»); } testException(15); } // тестовый метод создания, обработки и пробрасывания исключения public static void testException(int i) throws FileNotFoundException, IOException{ if(i < 0){ FileNotFoundException myException = new FileNotFoundException(«число меньше 0: « + i); throw myException; }else if(i > 10){ throw new IOException(«Число должно быть в пределах от 0 до 10»); } } } |
А в консоле эта программа напишет такое:
java.io.FileNotFoundException: число меньше 0: —5 at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:24) at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:10) Необязательный блок, но раз уже написан, то выполнятся будет не зависимо от того было исключение или нет Exception in thread «main» java.io.IOException: Число должно быть в пределах от 0 до 10 at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:27) at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:19) |
Обратите внимание, что метод testException()
бросает исключение, используя ключевое слово throw
, а в сигнатуре метода используется ключевое слово throws
, чтобы дать понять вызывающему методу тип исключений, которые может бросить testException()
.
Важные моменты в обработке исключений:
- Нельзя использовать блоки
catch
илиfinally
без блокаtry
. - Блок
try
также может быть использован только сcatch
блоком, или только сfinally
блоком, или с тем и другим блоком. - Мы можем использовать несколько блоков
catch
только с однимtry
. try-catch
блоки могут быть вложенными — этим они очень похожи наif-else
конструкции.- Мы можем использовать только один, блок
finally
в одномtry-catch
.
Иерархия исключений в Java
Java исключения являются иерархическими, а наследование используется для категоризации различных типов исключений. Throwable
— родительский класс в иерархии Java исключений. Он имеет два дочерних объекта — Error
и Exception
. Исключения далее разделены на проверяемые исключения и исключения времени выполнения.
- Error — это тип ошибок, которые выходят за рамки вашей программы, их невозможно предвидеть или обработать. Это может быть аппаратный сбой, «поломка» JVM или ошибка памяти. Именно для таких необычных ситуаций есть отдельная иерархия ошибок. Мы должны просто знать, что такие ошибки есть и не можем справиться с такими ситуациями. Примеры
Error
:OutOfMemoryError
иStackOverflowError
. - Проверяемые исключения (Checked Exceptions) — тип исключений, которые мы можем предвидеть в программе и попытаться обработать, например,
FileNotFoundException
. Мы должны поймать это исключение и написать внятное и полезное сообщение пользователю о том, что произошло (также желательно логировать ошибки).Exception
— родительский класс всех проверяемых исключений (Checked Exceptions). Если мы бросили проверяемое исключение, то должны поймать его в том же методе или должны пробросить его с помощью ключевого словаthrows
. - Runtime Exception — это ошибки программиста. Например, пытаясь получить элемент из массива, мы должны проверить длину массива, прежде чем пытаться получить элемент — в противном случае это может быть брошен
ArrayIndexOutOfBoundException
.RuntimeException
— родительский класс для всех Runtime исключений. Если мы сами бросаем Runtime Exception в методе, то не обязательно указывать в сигнатуре метода ключевое словоthrows
.
На рисунке 1 представлена иерархия исключений в Java:
Рисунок 1 — Иерархия исключений в Java
Полезные методы в обработке исключений
Класс Exception
и все его подклассы не содержат какие-либо методы для обработки исключений. Все предоставляемые методы находятся в базовом классе Throwable
. Подклассы класса Exception
созданы для того, чтобы определять различные виды исключений. Именно поэтому при обработке исключений мы можем легко определить причину и обработать исключение в соответствии с его типом.
Полезные методы класса Throwable
:
- public String getMessage() — этот метод возвращает сообщение, которое было создано при создании исключения через конструктор.
- public String getLocalizedMessage() — метод, который переопределяют подклассы для локализации конкретное сообщение об исключении. В реализации
Throwable
класса этот метод просто использует методg
etMessage()
, чтобы вернуть сообщение об исключении (Throwable
на вершине иерархии — ему нечего локализировать, поэтому он вызываетgetMessage())
. - public synchronized Throwable getCause() — этот метод возвращает причину исключения или идентификатор в виде
null
, если причина неизвестна. - public String toString() — этот метод возвращает информацию о
Throwable
в форматеString
. - public void printStackTrace() — этот метод выводит информацию трассировки стека в стандартный поток ошибок, этот метод перегружен и мы можем передать
PrintStream
илиPrintWriter
в качестве аргумента, чтобы написать информацию трассировки стека в файл или поток.
Автоматическое управление ресурсами и улучшения блока перехвата ошибок в Java 7
Если вам нужно перехватывать много исключений в одном блоке try-catch
, то блок перехвата будет выглядеть очень некрасиво и в основном будет состоять из избыточного кода. Именно поэтому в Java 7 это было значительно улучшено и теперь мы можем перехватывать несколько исключений в одном блоке catch
.
Это выглядит следующим образом:
catch(IOException | SQLException | Exception ex){ //что-то сделать с перехваченной ошибкой… } |
Как видим, здесь блок catch
перехватывает сразу несколько исключений — это очень красиво, компактно и удобно.
В большинстве случаев мы используем блок finally
для того, чтобы закрыть открытые потоки, подключения или освободить другие ресурсы. Очень часто мы забываем закрыть и получаем runtime исключения. Такие исключения трудно отлаживать. Поэтому в Java 7 был введен try
с ресурсами, где мы можем открыть ресурс в самом try
и использовать его внутри блока try-catch
. Когда программа заканчивает выполнение блока try-catch
, то среда выполнения автоматически закрывает эти ресурсы. Вот пример try-catch
блока с ресурсами:
// try c ресурсами try (MyResource mr = new MyResource()) { System.out.println(«Красивый и компактный код в try c ресурсами»); } catch (Exception e) { e.printStackTrace(); } |
Создание своих классов исключений
Java предоставляет много классов исключений, но иногда нам может понадобиться создать свои «кастомные» классы исключений. Это может понадобиться для того, чтобы уведомить абонента о конкретном типе исключения с соответствующим сообщением. Например, мы напишем метод для обработки только текстовых файлов, поэтому мы можем написать свой класс исключений и передавать соответствующий код ошибки, когда кто-то передает неподходящий тип файла в качестве входных данных.
Вот пример своего класса исключений и его использование:
package ua.com.prologistic; // наследуемся от класс Exception public class MyException extends Exception { private String errorCode = «Unknown_Exception»; public MyException(String message, String errorCode){ super(message); this.errorCode = errorCode; } public String getErrorCode(){ return this.errorCode; } } |
А теперь проверим в работе наш класс MyException:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package ua.com.prologistic; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class CustomExceptionExample { public static void main(String[] args) throws MyException { try { processFile(«file.txt»); } catch (MyException e) { processErrorCodes(e); } } // метод для обработки ошибок private static void processErrorCodes(MyException e) throws MyException { // здесь мы ищем указанный при выбросе исключения код ошибки и сообщаем пользователю что произошло switch(e.getErrorCode()){ case «BAD_FILE_TYPE»: System.out.println(«Неподходящий тип файла»); throw e; case «FILE_NOT_FOUND_EXCEPTION»: System.out.println(«Файл не найден»); throw e; case «FILE_CLOSE_EXCEPTION»: System.out.println(«Ошибка при закрытии файла»); break; default: System.out.println(«Произошла неизвестная ошибка « + e.getMessage()); e.printStackTrace(); } } // метод для работы с файлом, который пробрасывает наш тип исключений private static void processFile(String file) throws MyException { InputStream fis = null; try { fis = new FileInputStream(file); } catch (FileNotFoundException e) { // здесь мы бросаем исключение с указанием кода ошибки throw new MyException(e.getMessage(),«FILE_NOT_FOUND_EXCEPTION»); }finally{ try { if(fis !=null)fis.close(); } catch (IOException e) { // здесь мы бросаем исключение с указанием кода ошибки throw new MyException(e.getMessage(),«FILE_CLOSE_EXCEPTION»); } } } } |
Полезные советы по обработке исключений в Java
- Не используйте для перехвата исключений класс
Exception
. В иерархии исключений есть множество классов на все случаи жизни вашей программы, которые не только эффективно обработают конкретную ошибку, но и предоставят полезную для пользователя и отладки информацию. - Бросайте исключение как можно раньше. Это является хорошей практикой программирования на Java.
- Ловите исключения только тогда, когда сможете эффективно для пользователя и отладки их обработать.
- Освобождайте ресурсы. Перехватывая исключение всегда закрывайте открытые ресурсы. Еще проще и эффективнее это делать с
Java 7
. Используйте try с ресурсами для лаконичного и красивого кода. - Логируйте исключения. Логируйте сообщения, которые предоставляет исключение. В большинстве случаев это даст вам четкое понимание причин и поможет в отладке. Не оставляйте пустым блок
catch
, иначе он будет просто поглощать исключение без каких-либо значимых деталей для отладки. - Один catch для нескольких исключений. Используйте преимущества Java 7 для удобства и красоты вашего кода.
- Используйте свои исключения. Это позволит вам лучше чувствовать свою программу и эффективнее с ней работать.
- Соглашения об именовании. Когда вы создать свои классы исключений, следите за тем, что из самого названия класса будет ясно, что это исключение.
- Используйте исключения с умом. Бросить исключение — достаточно дорогостоящая в Java операция. Возможно, в некоторых случаях будем уместно не бросать исключений, а вернуть, например, логическую переменную, которая обозначала успешное или не успешное выполнение метода.
- Документируйте исключения. Желательно писать javadoc @throws для ваших исключений. Это будет особенно полезно в тех случаях, когда ваша программа предоставляет интерфейс для работы с другими приложениями.
Вот и все, что нужно знать об обработке исключений в Java.
Афоризм
У тебя ноги уже кончились, а платье еще не началось!
Лариса Гузеева
Поддержка проекта
Если Вам сайт понравился и помог, то будем признательны за Ваш «посильный» вклад в его поддержку и развитие
• Yandex.Деньги
410013796724260
• Webmoney
R335386147728
Z369087728698
Исключения, try … catch
В данной статье рассматривается используемый в Java механизм обработки исключений. Исключение в Java —
это объект, который описывает исключительное состояние, возникшее в каком-либо участке программного кода.
Когда возникает исключительное состояние, создается объект класса Exception. Этот объект пересылается
в метод, обрабатывающий данный тип исключительной ситуации. Исключения могут возбуждаться и для того, чтобы
сообщить о некоторых нештатных ситуациях.
Ключевые слова исключений try, catch, throw, throws, finally
Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами:
- try,
- catch,
- throw,
- throws,
- finally.
Ниже приведена общая форма блока обработки исключений.
try { // блок кода } catch (<ExceptionType1> е) { // обработчик исключений типа ExceptionType1 } catch (<ExceptionType2> е) { // обработчик исключений типа ExceptionType2 } finally { // ... }
Типы исключений
В вершине иерархии исключений стоит класс Throwable, который наследуется от Object.
Каждый из типов исключений является подклассом Throwable. Два непосредственных наследника класса Throwable
делят иерархию подклассов исключений на две различные ветви. Иерархия классов представлена на рисунке.
Класс Ехception используется для описания исключительных ситуации, которые должны
перехватываться программным кодом пользователя. Класс Error предназначен для описания
исключительных ситуаций, которые при обычных условиях не должны перехватываться в пользовательской
программе.
Неперехваченные исключения
Объекты-исключения автоматически создаются исполняющей средой Java в результате возникновения
определенных исключительных ситуаций. Пример программы, в которой создаем исключительную ситуацию
при делении на нуль.
package samples; class TestException { public static void main(String args[]) { int d = 0; int a = 42 / d; System.out.println ("a = " + a); } }
В консоль будет выведено следующее сообщение.
Exception in thread "main" java.lang.ArithmeticException: / by zero at samples.TestException.main(TestException.java:8)
Следует обратить внимание на тот факт, что типом возбужденного исключения был не Exception
и не Throwable. Это подкласс класса Exception, а именно: ArithmeticException,
поясняющий, какая ошибка возникла при выполнении программы.
Изменим класс добавлением статического метода subroutine, в котором создадим такую же
исключительную ситуацию.
package samples; public class TestException { static void subroutine() { int d = 0; int a = 10 / d; System.out.println ("a = " + a); } public static void main(String[] args) { TestException.subroutine(); } }
Сообщение выполнения программы показывает, как обработчик исключений исполняющей системы Java выводит
содержимое всего стека вызовов.
Exception in thread "main" java.lang.ArithmeticException: / by zero at samples.TestException.subroutine(TestException.java:8) at samples.TestException.main(TestException.java:14)
Перехват исключений try/catch
Для защиты программного кода от исключений необходимо использовать связанные блоки с ключевыми
словами try catch; catch помещается сразу же после try-блока. В блоке catch задается тип исключения,
которое необходимо обработать.
class TestException { public static void main(String args[]) { try { int d = 0; int a = 42 / d; } catch (ArithmeticException e) { System.out.println("division by zero"); } } }
Целью большинства хорошо сконструированных catch-разделов должна быть обработка возникшей исключительной
ситуации и приведение переменных программы в некоторое разумное состояние — такое, чтобы программу можно было продолжить
так, будто никакой ошибки и не было (в нашем примере выводится предупреждение — division by zero).
Несколько разделов catch
В отдельных случаях блок программного кода может вызвать исключения различных типов. Для того, чтобы локализовать
обработку подобных ситуаций, можно использовать несколько catch-разделов для одного try-блока. Блоки наиболее
специализированных классов исключений должны идти первыми, поскольку ни один подкласс не будет достигнут, если поставить его
после суперкласса.
В следующем примере перехватывается два различных типа исключений, причем за этими двумя специализированными обработчиками
следует раздел catch общего назначения, перехватывающий все подклассы класса Throwable.
class MultiCatch { static int c[] = { 1 }; public static void main(String args[]) { try { int a = args.length; System.out.println("a = " + String.valueOf(a)); int b = 23 / a; c[4] = 33; } catch (ArithmeticException e) { System.out.println("ArithmeticException : " + e.getMessage()); } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "ArrayIndexOutOfBoundsException : " + e.getMessage()); } } }
Данный пример, запущенный без параметров, вызывает возбуждение исключительной ситуации деления на нуль.
Если в командной строке будет определен один или несколько параметров, тем самым установив ‘а’ в значение больше нуля,
то будет возбуждено исключение выхода индекса за границы массива ArrayIndexOutOfBounds. Ниже приведены результаты
работы этой программы, запущенной и тем и другим способом.
а = 0 div by 0: java.lang.ArithmeticException: / by zero a = 1 array index oob: java.lang.ArrayIndexOutOfBoundsException:33
Вложенные операторы try
Операторы try можно вкладывать друг в друга. Если у оператора try низкого уровня нет раздела catch,
соответствующего возбужденному исключению, стек будет развернут на одну ступень выше, и в поисках подходящего обработчика
будут проверены разделы catch внешнего оператора try. Пример вложения двух операторов try catch друг в друга
посредством вызова метода.
class MultiNest { static int c[] = { 1 }; static void checkArray() { try { c[4] = 33; } catch(ArrayIndexOutOfBoundsException e) { System.out.println "ArrayIndexOutOfBoundsException : " + e.getMessage()); } } public static void main(String args[]) { try { int a = args.length(); System.out.println("a = " + a); int b = 23 / a; checkArray(); } catch (ArithmeticException e) { System.out.println("ArithmeticException : " + e.getMessage()); } } }
Возбуждение исключений throw
Программа может явно вызывать исключение, используя оператор throw. После выполнения оператора throw процесс
выполнения программы приостанавливается и последующие операторы не выполняются. JVM просматривает ближайший блоки
try … catch, соответствующий типу исключения, для «передачи управления». Если подходящий блок не будет найден, то
обработчик исключений остановит программу и «распечатает» при этом состояние стека вызовов.
Пример исключения, в котором сначала создается объект-исключение, затем оператор throw возбуждает исключительную ситуацию,
после чего то же исключение возбуждается повторно — на этот раз уже кодом перехватившего его в первый раз раздела catch.
class TestThrow { static void method() { try { throw new NullPointerException("Exception in method"); } catch (NullPointerException e) { System.out.println(e.getMessage()); throw e; } } public static void main(String args[]) { try { method(); } catch(NullPointerException e) { System.out.println("Catch inside main : " + e.getMessage()); } } }
Результат выполнения программы приведен ниже.
Exception in method Catch inside main : Exception in method
Объявление об исключении throws
Если метод может возбуждать исключения, которые сам не обрабатывает, то он должен объявить об этом, чтобы вызывающие его другие
методы могли защитить себя от этих исключений. Для задания списка исключений, которые могут возбуждаться методом, используется
ключевое слово throws.
Если метод в явном виде (т.е. с помощью оператора throw) возбуждает исключение, тип класса исключений должен быть указан
в операторе throws в объявлении этого метода. Принимая данное положение во внимание синтаксис определения метода
должен быть описан следующим образом:
public class TestThrow { static void method() throws IllegalAccessException { try { System.out.println("inside method"); throw new IllegalAccessException ( "Exception in method"); } catch (NullPointerException e) { System.out.println(e.getMessage()); } } public static void main(String args[]) { try { method(); } catch(IllegalAccessException e) { System.out.println("Catch inside main : " + e.getMessage()); } } }
Результат работы примера:
inside method Catch inside main : Exception in method
Ключевое слово finally
В случае, когда необходимо гарантировано выполнить определенный участок кода необходимо использовать ключевое слово finally.
Использование связи try…finally позволяет обеспечить выполнение кода независимо от того, какие исключения были возбуждены и
перехвачены, даже в тех случаях, когда в методе нет соответствующего возбужденному исключению раздела catch.
У каждого раздела try должен быть по крайней мере или один раздел catch или блок finally. Блок finally очень
удобен для закрытия файлов и освобождения любых других ресурсов, захваченных для временного использования в начале выполнения метода.
Ниже приведен пример класса с двумя методами, завершение которых происходит по разным причинам, но в обоих перед выходом выполняется
код раздела finally.
public class TestFinally { static void methodA() { try { System.out.println("inside methodA"); throw new RuntimeException("Exception in methodA"); } finally { System.out.println("finally inside methodA"); } } static void methodB() { try { System.out.println("inside methodB"); return; } finally { System.out.println("finally inside methodB"); } } public static void main(String args[]) { try { methodA(); } catch (Exception e) { System.out.println("Catch exception iinside main"); } methodB(); } }
В тестовом примере в методе methodA возбуждается исключение. Но перед преждевременным выходом из блока try, выполняется
раздел finally. Во втором методе methodB завершается работа в try-блоке оператором return, но и при этом
перед выходом из метода выполняется программный код блока finally. Результат работы тестового примера:
inside methodA finally inside methodA Catch exception iinside main inside methodB finally inside methodB
Обработка исключений в Java предоставляет исключительно мощный механизм для управления сложными программами. Ключевые слова
try, throw, catch позволяют выполнять обработку ошибок и разных нештатных ситуаций в программе.
Наследование исключений
catch — полиморфная конструкция, т.е. catch по типу parent перехватывает исключения любого типа, которые является
Parent’ом.
public class TestException { public static void main(String[] args) { try { System.err.print("level 0"); throw new RuntimeException(); System.err.print("level 1"); } catch (Exception e) { // catch Exception ПЕРЕХВАТ RuntimeException System.err.print("level 2"); } System.err.println("level 3"); } }
В результате в консоли увидим
Error и Exception из параллельных веток наследования от
Throwable, поэтому catch по одному «брату» не может поймать другого «брата».
public class TestError { public static void main(String[] args) { try { System.err.println("level 0"); if (true) { throw new Error(); } System.err.println("level 1"); } catch (Exception e) { System.err.println("level 2"); } System.err.println("level 3"); } }
Результат выполения программы
level 0 Exception in thread "main" java.lang.Error at TestError.main(TestFinally.java:8)
Множественные исключения
Объявление исключений в методе может быть множественным. Пример :
import java.io.EOFException; import java.io.FileNotFoundException; public class MultiException { // объявляем исключения public static void main(String[] args) throws EOFException, FileNotFoundException { if (System.currentTimeMillis() % 2 == 0) { throw new EOFException(); } else { throw new FileNotFoundException(); } } }
Наверх