자바의 예외 처리

1. 개요

이 튜토리얼에서는 Java에서 예외 처리의 기본 사항과 몇 가지 문제점을 살펴볼 것입니다.

2. 첫 번째 원칙

2.1. 그것은 무엇입니까?

예외와 예외 처리를 더 잘 이해하기 위해 실제 비교를 해보겠습니다.

온라인으로 제품을 주문했지만 배송 중 배송에 실패했다고 상상해보십시오. 좋은 회사는이 문제를 처리하고 패키지가 제 시간에 도착하도록 우아하게 경로를 다시 지정할 수 있습니다.

마찬가지로 Java에서 지침을 실행하는 동안 코드에 오류가 발생할 수 있습니다. 좋은 예외 처리 는 오류를 처리하고 프로그램을 정상적으로 다시 라우팅하여 사용자에게 여전히 긍정적 인 경험을 제공 할 수 있습니다 .

2.2. 왜 그것을 사용합니까?

우리는 일반적으로 이상적인 환경에서 코드를 작성합니다. 파일 시스템에는 항상 파일이 포함되고 네트워크는 정상이며 JVM에는 항상 충분한 메모리가 있습니다. 때때로 우리는 이것을“행복한 길”이라고 부릅니다.

그러나 프로덕션에서는 파일 시스템이 손상되고 네트워크가 중단되고 JVM의 메모리가 부족해질 수 있습니다. 코드의 웰빙은 "불행한 경로"를 처리하는 방법에 달려 있습니다.

이러한 조건은 애플리케이션의 흐름에 부정적인 영향을 미치고 예외를 형성하기 때문에 처리해야합니다 .

public static List getPlayers() throws IOException { Path path = Paths.get("players.dat"); List players = Files.readAllLines(path); return players.stream() .map(Player::new) .collect(Collectors.toList()); }

이 코드는 IOException 을 처리하지 않도록 선택하여 대신 호출 스택에 전달합니다. 이상적인 환경에서는 코드가 잘 작동합니다.

그러나 players.dat 가 없으면 프로덕션에서 어떤 일이 발생할 수 있습니까?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist at sun.nio.fs.WindowsException.translateToIOException(Unknown Source) at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source) // ... more stack trace at java.nio.file.Files.readAllLines(Unknown Source) at java.nio.file.Files.readAllLines(Unknown Source) at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12 at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

이 예외를 처리하지 않으면 정상적인 프로그램이 모두 실행을 중지 할 수 있습니다! 우리는 코드가 일이 잘못 될 때에 대한 계획을 가지고 있는지 확인해야합니다.

또한 여기에서 예외에 대한 한 가지 이점이 더 있습니다. 바로 스택 추적 자체입니다. 이 스택 추적으로 인해 디버거를 연결하지 않고도 문제가되는 코드를 찾아 낼 수 있습니다.

3. 예외 계층

궁극적으로 예외Throwable 에서 확장되는 Java 객체 일뿐입니다 .

 ---> Throwable  Exception Error | (checked) (unchecked) | RuntimeException (unchecked)

예외 조건에는 세 가지 주요 범주가 있습니다.

  • 확인 된 예외
  • 확인되지 않은 예외 / 런타임 예외
  • 오류

런타임 및 확인되지 않은 예외는 동일한 것을 참조합니다. 우리는 종종 서로 바꿔서 사용할 수 있습니다.

3.1. 확인 된 예외

확인 된 예외는 Java 컴파일러가 처리해야하는 예외입니다. 호출 스택에 예외를 선언적으로 던지거나 직접 처리해야합니다. 잠시 후이 두 가지에 대해 자세히 알아보십시오.

오라클의 문서는 메서드 호출자가 복구 할 수있을 것으로 합리적으로 예상 할 수있을 때 확인 된 예외를 사용하도록 알려줍니다.

확인 된 예외의 몇 가지 예는 IOExceptionServletException입니다.

3.2. 확인되지 않은 예외

확인되지 않은 예외는 Java 컴파일러가 처리 할 필요 가 없는 예외입니다 .

간단히 말해서, RuntimeException 을 확장하는 예외를 생성하면 체크 해제됩니다. 그렇지 않으면 확인됩니다.

이것이 편리하게 들리지만 Oracle의 문서는 상황 오류 (확인 됨)와 사용 오류 (확인되지 ​​않음)를 구분하는 것과 같이 두 개념에 모두 좋은 이유가 있음을 알려줍니다.

확인되지 않은 예외의 몇 가지 예는 NullPointerException, IllegalArgumentExceptionSecurityException 입니다.

3.3. 오류

오류는 라이브러리 비 호환성, 무한 재귀 또는 메모리 누수와 같은 심각하고 일반적으로 복구 할 수없는 조건을 나타냅니다.

또한 RuntimeException을 확장하지 않더라도 체크되지 않습니다.

대부분의 경우 Errors 를 처리, 인스턴스화 또는 확장하는 것은 이상 할 것 입니다. 일반적으로 우리는 이것들이 끝까지 전파되기를 원합니다.

오류의 몇 가지 예는 StackOverflowErrorOutOfMemoryError 입니다.

4. 예외 처리

Java API에는 일이 잘못 될 수있는 곳이 많이 있으며 이러한 곳 중 일부는 서명 또는 Javadoc에서 예외로 표시됩니다.

/** * @exception FileNotFoundException ... */ public Scanner(String fileName) throws FileNotFoundException { // ... }

앞서 언급했듯이 이러한 "위험한"메서드를 호출 할 때 확인 된 예외를 처리 해야 하며 확인 되지 않은 예외를 처리 할 있습니다. Java는이를 수행하는 몇 가지 방법을 제공합니다.

4.1. 던지다

예외를 "처리"하는 가장 간단한 방법은 예외를 다시 발생시키는 것입니다.

public int getPlayerScore(String playerFile) throws FileNotFoundException { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); }

FileNotFoundException 은 확인 된 예외 이기 때문에 이것은 컴파일러를 만족시키는 가장 간단한 방법이지만, 우리 메서드를 호출하는 모든 사람이 이제 처리해야한다는 것을 의미합니다!

parseIntNumberFormatException을 던질 수 있지만 체크되지 않았기 때문에 처리 할 필요가 없습니다.

4.2. 시도 잡기

예외를 직접 시도하고 처리하려면 try-catch 블록을 사용할 수 있습니다 . 예외를 다시 던져서 처리 할 수 ​​있습니다.

public int getPlayerScore(String playerFile) { try { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException noFile) { throw new IllegalArgumentException("File not found"); } }

또는 복구 단계를 수행하여 :

public int getPlayerScore(String playerFile) { try { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch ( FileNotFoundException noFile ) { logger.warn("File not found, resetting score."); return 0; } }

4.3. 드디어

이제 예외 발생 여부에 관계없이 실행해야하는 코드가있는 경우가 있는데, 바로 여기에 finally 키워드가 있습니다.

In our examples so far, there ‘s been a nasty bug lurking in the shadows, which is that Java by default won't return file handles to the operating system.

Certainly, whether we can read the file or not, we want to make sure that we do the appropriate cleanup!

Let's try this the “lazy” way first:

public int getPlayerScore(String playerFile) throws FileNotFoundException { Scanner contents = null; try { contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } finally { if (contents != null) { contents.close(); } } } 

Here, the finally block indicates what code we want Java to run regardless of what happens with trying to read the file.

Even if a FileNotFoundException is thrown up the call stack, Java will call the contents of finally before doing that.

We can also both handle the exception and make sure that our resources get closed:

public int getPlayerScore(String playerFile) { Scanner contents; try { contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException noFile ) { logger.warn("File not found, resetting score."); return 0; } finally { try { if (contents != null) { contents.close(); } } catch (IOException io) { logger.error("Couldn't close the reader!", io); } } }

Because close is also a “risky” method, we also need to catch its exception!

This may look pretty complicated, but we need each piece to handle each potential problem that can arise correctly.

4.4. try-with-resources

Fortunately, as of Java 7, we can simplify the above syntax when working with things that extend AutoCloseable:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException e ) { logger.warn("File not found, resetting score."); return 0; } }

When we place references that are AutoClosable in the try declaration, then we don't need to close the resource ourselves.

We can still use a finally block, though, to do any other kind of cleanup we want.

Check out our article dedicated to try-with-resources to learn more.

4.5. Multiple catch Blocks

Sometimes, the code can throw more than one exception, and we can have more than one catch block handle each individually:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (IOException e) { logger.warn("Player file wouldn't load!", e); return 0; } catch (NumberFormatException e) { logger.warn("Player file was corrupted!", e); return 0; } }

Multiple catches give us the chance to handle each exception differently, should the need arise.

Also note here that we didn't catch FileNotFoundException, and that is because it extends IOException. Because we're catching IOException, Java will consider any of its subclasses also handled.

Let's say, though, that we need to treat FileNotFoundException differently from the more general IOException:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile)) ) { return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException e) { logger.warn("Player file not found!", e); return 0; } catch (IOException e) { logger.warn("Player file wouldn't load!", e); return 0; } catch (NumberFormatException e) { logger.warn("Player file was corrupted!", e); return 0; } }

Java lets us handle subclass exceptions separately, remember to place them higher in the list of catches.

4.6. Union catch Blocks

When we know that the way we handle errors is going to be the same, though, Java 7 introduced the ability to catch multiple exceptions in the same block:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (IOException | NumberFormatException e) { logger.warn("Failed to load score!", e); return 0; } }

5. Throwing Exceptions

If we don't want to handle the exception ourselves or we want to generate our exceptions for others to handle, then we need to get familiar with the throw keyword.

Let's say that we have the following checked exception we've created ourselves:

public class TimeoutException extends Exception { public TimeoutException(String message) { super(message); } }

and we have a method that could potentially take a long time to complete:

public List loadAllPlayers(String playersFile) { // ... potentially long operation }

5.1. Throwing a Checked Exception

Like returning from a method, we can throw at any point.

Of course, we should throw when we are trying to indicate that something has gone wrong:

public List loadAllPlayers(String playersFile) throws TimeoutException { while ( !tooLong ) { // ... potentially long operation } throw new TimeoutException("This operation took too long"); }

Because TimeoutException is checked, we also must use the throws keyword in the signature so that callers of our method will know to handle it.

5.2. Throwing an Unchecked Exception

If we want to do something like, say, validate input, we can use an unchecked exception instead:

public List loadAllPlayers(String playersFile) throws TimeoutException { if(!isFilenameValid(playersFile)) { throw new IllegalArgumentException("Filename isn't valid!"); } // ... } 

Because IllegalArgumentException is unchecked, we don't have to mark the method, though we are welcome to.

Some mark the method anyway as a form of documentation.

5.3. Wrapping and Rethrowing

We can also choose to rethrow an exception we've caught:

public List loadAllPlayers(String playersFile) throws IOException { try { // ... } catch (IOException io) { throw io; } }

Or do a wrap and rethrow:

public List loadAllPlayers(String playersFile) throws PlayerLoadException { try { // ... } catch (IOException io) { throw new PlayerLoadException(io); } }

This can be nice for consolidating many different exceptions into one.

5.4. Rethrowing Throwable or Exception

Now for a special case.

If the only possible exceptions that a given block of code could raise are unchecked exceptions, then we can catch and rethrow Throwable or Exception without adding them to our method signature:

public List loadAllPlayers(String playersFile) { try { throw new NullPointerException(); } catch (Throwable t) { throw t; } }

While simple, the above code can't throw a checked exception and because of that, even though we are rethrowing a checked exception, we don't have to mark the signature with a throws clause.

This is handy with proxy classes and methods. More about this can be found here.

5.5. Inheritance

When we mark methods with a throws keyword, it impacts how subclasses can override our method.

In the circumstance where our method throws a checked exception:

public class Exceptions { public List loadAllPlayers(String playersFile) throws TimeoutException { // ... } }

A subclass can have a “less risky” signature:

public class FewerExceptions extends Exceptions { @Override public List loadAllPlayers(String playersFile) { // overridden } }

But not a “more riskier” signature:

public class MoreExceptions extends Exceptions { @Override public List loadAllPlayers(String playersFile) throws MyCheckedException { // overridden } }

This is because contracts are determined at compile time by the reference type. If I create an instance of MoreExceptions and save it to Exceptions:

Exceptions exceptions = new MoreExceptions(); exceptions.loadAllPlayers("file");

Then the JVM will only tell me to catch the TimeoutException, which is wrong since I've said that MoreExceptions#loadAllPlayers throws a different exception.

Simply put, subclasses can throw fewer checked exceptions than their superclass, but not more.

6. Anti-Patterns

6.1. Swallowing Exceptions

Now, there’s one other way that we could have satisfied the compiler:

public int getPlayerScore(String playerFile) { try { // ... } catch (Exception e) {} // <== catch and swallow return 0; }

The above is calledswallowing an exception. Most of the time, it would be a little mean for us to do this because it doesn't address the issue and it keeps other code from being able to address the issue, too.

There are times when there's a checked exception that we are confident will just never happen. In those cases, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { // this will never happen } }

Another way we can “swallow” an exception is to print out the exception to the error stream simply:

public int getPlayerScore(String playerFile) { try { // ... } catch (Exception e) { e.printStackTrace(); } return 0; }

We've improved our situation a bit by a least writing the error out somewhere for later diagnosis.

It'd be better, though, for us to use a logger:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { logger.error("Couldn't load the score", e); return 0; } }

While it's very convenient for us to handle exceptions in this way, we need to make sure that we aren't swallowing important information that callers of our code could use to remedy the problem.

Finally, we can inadvertently swallow an exception by not including it as a cause when we are throwing a new exception:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { throw new PlayerScoreException(); } } 

Here, we pat ourselves on the back for alerting our caller to an error, but we fail to include the IOException as the cause. Because of this, we've lost important information that callers or operators could use to diagnose the problem.

We'd be better off doing:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { throw new PlayerScoreException(e); } }

Notice the subtle difference of including IOException as the cause of PlayerScoreException.

6.2. Using return in a finally Block

Another way to swallow exceptions is to return from the finally block. This is bad because, by returning abruptly, the JVM will drop the exception, even if it was thrown from by our code:

public int getPlayerScore(String playerFile) { int score = 0; try { throw new IOException(); } finally { return score; // <== the IOException is dropped } }

According to the Java Language Specification:

If execution of the try block completes abruptly for any other reason R, then the finally block is executed, and then there is a choice.

If the finally block completes normally, then the try statement completes abruptly for reason R.

If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S (and reason R is discarded).

6.3. Using throw in a finally Block

Similar to using return in a finally block, the exception thrown in a finally block will take precedence over the exception that arises in the catch block.

This will “erase” the original exception from the try block, and we lose all of that valuable information:

public int getPlayerScore(String playerFile) { try { // ... } catch ( IOException io ) { throw new IllegalStateException(io); // <== eaten by the finally } finally { throw new OtherException(); } }

6.4. Using throw as a goto

Some people also gave into the temptation of using throw as a goto statement:

public void doSomething() { try { // bunch of code throw new MyException(); // second bunch of code } catch (MyException e) { // third bunch of code } }

This is odd because the code is attempting to use exceptions for flow control as opposed to error handling.

7. Common Exceptions and Errors

Here are some common exceptions and errors that we all run into from time to time:

7.1. Checked Exceptions

  • IOException – This exception is typically a way to say that something on the network, filesystem, or database failed.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException – this exception means that we tried to access a non-existent array index, like when trying to get index 5 from an array of length 3.
  • ClassCastException – this exception means that we tried to perform an illegal cast, like trying to convert a String into a List. We can usually avoid it by performing defensive instanceof checks before casting.
  • IllegalArgumentException – this exception is a generic way for us to say that one of the provided method or constructor parameters is invalid.
  • IllegalStateException – This exception is a generic way for us to say that our internal state, like the state of our object, is invalid.
  • NullPointerException – This exception means we tried to reference a null object. We can usually avoid it by either performing defensive null checks or by using Optional.
  • NumberFormatException – This exception means that we tried to convert a String into a number, but the string contained illegal characters, like trying to convert “5f3” into a number.

7.3. Errors

  • StackOverflowError – 이 예외는 스택 추적이 너무 큼을 의미합니다. 이것은 때때로 대규모 애플리케이션에서 발생할 수 있습니다. 그러나 일반적으로 코드에서 무한 재귀가 발생한다는 것을 의미합니다.
  • NoClassDefFoundError –이 예외는 클래스 경로에 있지 않거나 정적 초기화 실패로 인해 클래스로드에 실패했음을 의미합니다.
  • OutOfMemoryError –이 예외는 JVM에 더 많은 개체에 할당 할 수있는 메모리가 더 이상 없음을 의미합니다. 때때로 이것은 메모리 누수로 인한 것입니다.

8. 결론

이 기사에서는 예외 처리의 기본 사항과 우수 사례 및 불량 사례를 살펴 보았습니다.

항상 그렇듯이이 기사에서 찾은 모든 코드는 GitHub에서 찾을 수 있습니다!