Java Exceptions
Exceptions in Java
Exceptions in Java
Before we learn about how to detect and handle exceptions in Java, let’s review some of the common exceptions and errors we may see in our programs. Each of the headers below is the name of an exception in Java, which is represented by a particular class in the Java programming language.
Every exception that can be handled in Java is a subtype of the Exception class. So, when we aren’t sure which type of exception to expect, we can always use the Exception class to make sure we catch all of them.
An ArithmeticException can occur whenever our program attempts to perform a calculation that would result in an undefined value. In Java, this is the case when we divide by 0. So, the following code would generate an ArithmeticException:
int a = 10;
int b = 0;
int c = a / b; // throws ArithmeticException
However, when using floating point values, the result is different:
double a = 5.0;
double b = 0.0;
double c = a / b; // Infinity
In short, this is because the standard for floating point numbers defines Infinity as 1.0 / 0.0
, so it is a valid value according to the standard definition for floating point numbers.
An ArrayIndexOutOfBoundsException happens when we try to access an array index that does not exist. Here’s a great example:
int[] array = new int[5];
array[5] = 10; // throws ArrayIndexOutOfBoundsException
In this example, we are trying to access the 6th element in the array, which is at array index 5. However, since the size of the array is only 5, we’ll get an ArrayIndexOutOfBoundsException when we try to execute this code since there is no 6th element.
Similar to the ArrayIndexOutOfBoundsException above, a StringIndexOutOfBoundsException occurs when we try to access an invalid index of a character in a string. Here’s a couple of examples:
String s = "abc";
char c = s.charAt(3); // throws StringIndexOutOfBoundsException
char d = s.charAt(-1); // throws StringIndexOutOfBoundsException
In this example, we are first trying to access the character at index 3. Just like with arrays, that would be the 4th character in the string. Since the string only contains 3 characters, it would result in an exception. Likewise, we cannot access a negative character index in Java, so it would also cause this exception to be thrown.
This exception occurs when the program is trying to open a file that does not exist. This is one of the more common errors that programmers must deal with when using files for input, and it is relatively simple to correct. In most cases, we can simply ask the user to provide another file. Here’s an example of some code that may cause this exception:
String filename = "";
Scanner scanner = new Scanner(new File(filename)); // throws FileNotFoundException
In this case, we are providing a blank filename, which causes the program to throw a FileNotFoundException.
An IOException is thrown whenever the program encounters a problem performing some input or output operation. This can be due to an error in the file it is reading from, a bug in the underlying operating system, or many other problems that can occur while a program is reading or writing data.
For example, reading input using a Scanner object can result in an IOException, both when reading from the terminal using System.in
, or when reading from a file. So, in general, anytime we are reading input from any source, we must always consider the fact that an IOException can occur.
Thankfully, these errors are very rare on most modern computers, but they are something that can be dealt with.
A NumberFormatException is used in Java when we try to convert a string to a number but it doesn’t work. See the sample code below to see how this could occur:
String s = "abc";
int x = Integer.parseInt(s); // throws NumberFormatException
double d = Double.parseDouble(s); // throws NumberFormatException
This exception is really useful when we are asking the user to provide a number that fits a particular format. By detecting this exception, we know that the user entered an invalid number, so we may have to prompt the user for another input.
A NoSuchElementException happens when a Scanner object tries to read input when there is no input available to read.
Scanner reader = new Scanner(System.in);
int x = reader.nextInt(); // throws NoSuchElementException if the input is empty;
This exception is typically used by developers to indicate that a particular input to a program is invalid. Many developers also use it in their own code as a custom exception.
Java 8 API Reference
Programming with Assertions
An AssertionError happens when our code reaches an assertion that fails. An assertion is a check added by the programmer to verify that a particular situation doesn’t occur in the code, such as a variable being negative or an array being too large. Java supports the use of a special keyword assert
that can be used to add these assertions to our code. By default, the Java runtime environment ignores any assert
keywords, but they can easily be enabled by adding -ea
as an argument to the java
command when running a program. This is a helpful step when debugging a new application. We’ll learn more about assertions and how they can be used effectively in a later chapter.
For now, here’s a quick example of an assertion that would produce an AssertionError:
int x = 5;
int y = 3;
assert x + y < 7; // throws AssertionError if enabled
Beyond exceptions, there are a few unrecoverable errors that may occur in our Java code. Recall that errors are special types of exceptions that should not be handled by our programs, and should instead be allowed to cause our programs to crash. Some examples are:
In addition, the Java compiler helps us detect many other errors, such as syntax and type errors that would cause our code to be unusable. Other languages, such as Python, have to deal with these errors at runtime, so there are many more unrecoverable errors in that language compared to Java.
Beyond the difference between exceptions and errors we’ve already discussed, Java also makes a distinction between two types of exceptions, checked exceptions and unchecked exceptions. Let’s look more closely at each of these groups below.
A checked exception is detected by the Java compiler. When it detects a checked exception, the compiler then looks to see if the exception will be detected and handled within the code. If not, it will then check to see if the method containing that code includes a throws
clause1 at the end of the method declaration that includes this particular exception. If neither of those is the case, then the compiler will output an error and refuse to compile the code. So, the developer must add one of these two things to the code in order for the compiler to be satisfied.
error: unreported exception someException; must be caught or declared to be thrown
If you get this type of message from the compiler, you have 2 choices.
throws someException
to the signature linepublic void someMethod() throws someException {}
In this class you will generally handle the exception.
An unchecked exception is one that is not detected by the Java compiler. These are exceptions that can occur at runtime, but would be difficult or impossible to detect when compiling the code. However, in many cases they can easily be detected or prevented from within the program itself. So, the compiler does not require us to add code to either handle those exceptions or declare them to be thrown in to our method declaration.
An ArithmeticException is a good example of an unchecked exception. Any division operation could result in an ArithmeticException, but the compiler does not require us to add additional code to handle that exception or declare it to be thrown. However, it is still good practice to either detect and handle this error, or use code or assertions to prevent it from occurring in the first place.
As discussed before, errors are a special type of exception that cannot be easily handled by the program. These are usually due to issues in the underlying operating system, and typically cause the program to crash.
throws
is a keyword in Java. It is only used to tell the compiler to ignore the lack of a handler for the checked exception ↩︎
Programs written in Java can have many exceptions occur on a regular basis. Thankfully, as we learned earlier, it is possible to detect and handle these exceptions in our code directly, in order to prevent our program crashing and causing the user more stress. Let’s see how we can perform this step in Java. A Try-Catch construct is synonymous with “handler”.
In Java, we use a Try-Catch construct to detect and handle exceptions in our code. Let’s look at a quick example, and then we can discuss how it works.
import java.util.Scanner;
import java.lang.Exception;
public class Example{
public static void main(String[] args){
Scanner reader;
try{
reader = new Scanner(System.in);
int x = Integer.parseInt(reader.nextLine());
System.out.println(x);
}catch(Exception e){
System.out.println("Error!");
}
}
}
First, we use the try
keyword, followed by curly braces {}
around a block of code. If any exceptions occur in the code contained in the try
statement, we can add catch
statements to handle that exception.
Directly following the closing curly brace after the try
block, we must include at least one catch
statement. The catch
keyword is followed by the name of an exception and a variable for that exception in parentheses ()
. In this example, we are just catching the generic Exception
type, which will match any catchable exception. We’ll see how we can detect specific exceptions in a later example. Then, we have one more block of code contained in curly braces {}
that is executed when we detect an exception. We can use the variable for the exception, in this case e
, to access additional details about the exception we’ve detected.
Finally, notice that we also must add import java.lang.Exception
to the top of our file. We’ll need to import any exceptions we want to catch in order to use them.
When writing code, it can sometimes be very difficult to even know which exceptions to expect from a particular piece of code. Thankfully, the Java 8 API Reference includes quite a bit of information, including which exceptions can be thrown by a particular method.
Let’s return to our earlier example. Here is the code contained in the try
block:
reader = new Scanner(System.in);
int x = Integer.parseInt(reader.nextLine());
System.out.println(x);
While this may look like a very simple few lines of code, there are actually several exceptions that could be produced here. Below is a list of them, followed by a link to the Java 8 API reference for the source of that exception:
When writing truly bulletproof code, it is a good idea to attempt to catch and handle all of these exceptions if possible. You can always refer to the official Java 8 API reference to see what exceptions could be produced by any methods you use that are a part of the main Java language. Later on in this chapter we’ll discuss some best practices when it comes to detecting and handling exceptions in code.
Of course, we can add multiple catch
statements after any try
block to catch different types of exceptions.
import java.util.Scanner;
import java.io.IOException;
import java.lang.NumberFormatException;
public class Example{
public static void main(String[] args){
Scanner reader;
try{
reader = new Scanner(System.in);
int x = Integer.parseInt(reader.nextLine());
System.out.println(x);
}catch(IOException e){
System.out.println("Error: IO Exception!");
}catch(NumberFormatException e){
System.out.println("Error: Input Does Not Match Expected Format!");
}
}
}
In the example code above, we see three different catch
statements, each of which will handle a different type of exception. When an exception occurs in the code contained in a try
block, the Java runtime will create the exception, and then it will search for the first handler that matches. So, it will begin with the first catch
statement, and see if the exception created matches the type of the exception in parenthesis. If so, it will execute the code inside of that catch
block. If not, it will continue to the next catch
statement. If none of the catch
statements match the exception, then it will be thrown and cause the program to stop executing.
The exceptions in Java form a hierarchical structure, meaning that an exception may match multiple types. For example, all exceptions that programs can catch are based on the generic Exception type. So, FileNotFoundException and ArrayIndexOutOfBoundsException would both match the Exception type.
In addition, many exceptions are descended from the IOException type. For example, FileNotFoundException is based on IOException, which itself is based on the Exception type. So, a FileNotFoundException would match any of those three types of exceptions. It can make things a bit tricky!
So, how can we know what types of exceptions we are dealing with, and what the hierarchy is? Thankfully, the Java 8 API Reference contains information about all possible exceptions, including the entire hierarchy of that exception.
For example, here is a screenshot from the FileNotFoundException page showing its hierarchy:
Because of this, we must be careful about how we order the catch statements. In general, we want to place the more specific exceptions first (the ones further down the hierarchy), and the more generic exceptions later.
Let’s look at an example of poor ordering of exception handlers:
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.NumberFormatException;
public class Example{
public static void main(String[] args){
Scanner reader;
try{
reader = new Scanner(new File("input.txt"));
int x = Integer.parseInt(reader.nextLine());
System.out.println(x);
}catch(IOException e){
System.out.println("Error: IO Exception!");
}catch(FileNotFoundException e){s
System.out.println("Error: File Not Found!");
}catch(NumberFormatException e){
System.out.println("Error: Input Does Not Match Expected Format!");
}
}
}
In the code above, we are catching the IOException first. So, if the code produces a FileNotFoundException, it will be caught and handled by the first catch
statement, since FileNotFoundException is also an IOException. Therefore, we wouldn’t want to order our catch
statements in this way.
This chapter introduces the basic code for reading files using a Scanner
object. It is mainly presented as a way to introduce some of the more common exceptions related to file input and output. We’ll cover the actual process of reading and writing from files in a later chapter.
Let’s see if we can write a very simple program to catch and handle a few common exceptions. Here’s some code to start with:
import java.util.Scanner;
import java.io.File;
public class Try{
public static void main(String[] args){
Scanner reader;
reader = new Scanner(System.in);
int x = Integer.parseInt(reader.nextLine());
int y = Integer.parseInt(reader.nextLine());
int z = x / y;
System.out.println(z);
}
}
For this example, place this starter code in Try.java
, open to the left, and modify it to catch and handle the following exceptions by printing the given error messages:
Any other exceptions can be ignored. Don’t forget to add import statements at the top of the file for each type of exception we need to catch!
Beyond catching exceptions, there are a few other important concepts to learn related to exceptions. Let’s go over a couple of them.
When the Java compiler finds a checked exception, we must either surround that code in a Try-Catch statement, or we must declare the exception to be thrown by adding throws
to our method declaration, followed by the exception type.
Here’s an example:
import java.util.Scanner;
import java.io.File;
public class Throw{
public static void main(String[] args){
Scanner reader;
reader = new Scanner(new File("input.txt"));
int x = Integer.parseInt(reader.nextLine());
System.out.println(x);
}
}
The code above handles user inputs. However, it does not includes the throws
keyword. So, when we try to compile this code, we’ll get the following error message.
8j-except/Throw.java:11: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
reader = new Scanner(new File(args[0]));
^
1 error
To allow this code to compile, we must simply add throws FileNotFoundException
to the end of our method declaration to tell the compiler that we aren’t going to handle that exception in our code. We’ll also have to add import java.io.FileNotFoundException
to the top of the file. Notice that we aren’t using throws Exception
this time, since we now know more about how exceptions work. As we’ll discuss later in this chapter, it is always better to include a specific exception instead of a generic one whenever possible.
Of course, we may also want to use a Try-Catch statement to handle this error directly in our code. We’ll discuss those tradeoffs later as well.
We can also generate our own exceptions using the throw
keyword. This allows us to create new exceptions whenever needed. Let’s look at an example:
import java.util.Scanner;
import java.io.File;
import java.lang.ArithmeticException;
import java.io.FileNotFoundException;
public class Throw{
public static void main(String[] args) throws FileNotFoundException{
Scanner reader;
reader = new Scanner(new File("input.txt"));
double x = Double.parseDouble(reader.nextLine());
double y = Double.parseDouble(reader.nextLine());
if(y == 0.0){
throw new ArithmeticException("Divide by Zero!");
}else{
System.out.println(x / y);
}
}
}
In this code, we are asking the user to input two floating point numbers. Then, we will output the first number divided by the second. However, we want to avoid the situation where the second number, the divisor, is $0$. Since Java doesn’t generate an ArithmeticException when dividing by $0$ using floating point numbers, we can do that ourselves using an If-Then statement and the throw
keyword.
Following the throw
keyword, we must create a new Exception. In this case, we are creating a new ArithmeticException, but any of the exception types we’ve learned about so far will work the same way. Inside of the parentheses, we can provide a helpful error message to go along with this exception.
Let’s see if we can use these keywords in another example. We’ll start with this code:
import java.util.Scanner;
import java.io.File;
public class Throw{
public static void main(String[] args){
Scanner reader;
reader = new Scanner(new File(args[0]));
// *** ADD CODE HERE ***
}
}
Place this code in Throw.java
and modify it to do the following:
args[0]
does not exist, it should throw a FileNotFoundException
. We’ll need to add the throws
keyword and additional information in the correct place.trim()
method to remove any extra whitespace!StringIndexOutOfBoundsException
when it tries to access the first character of the string. (Hint: Will the String.charAt()
method throw this exception for us? We can check the Java Documentation to find out)In the example above, we are using the code args[0]
to access the first command-line argument provided to the program. As you can tell by the data type in the parameter of the main
method, args
is an array of strings.
When running a Java program from the terminal, we can provide command-line arguments after the name of the file, as in this example:
java Throw input.txt
In this example, the string "input.txt"
will be stored in args[0]
as the first command-line argument, and it will be accessible in the program. This is a great way to run a program by providing an input file to read from.
More command-line arguments can be included, separated by spaces:
java Throw input.txt output.txt
In this example, "input.txt"
would be stored in args[0]
and "output.txt"
would be stored in args[1]
.
When dealing with exceptions in our code, sometimes we have an operation that must be executed, even if the earlier code throws an exception. In that case, we can use the finally
keyword to make sure that the correct code is executed.
To understand how the finally
keyword works, let’s take a look at an example:
import java.util.Scanner;
import java.lang.NumberFormatException;
import java.lang.IllegalArgumentException;
public class Finally{
public static void main(String[] args){
Scanner reader;
reader = new Scanner(System.in);
try{
int x = Integer.parseInt(reader.nextLine());
if(x < 0){
throw new IllegalArgumentException("Input must be greater than 0!");
}
}catch(NumberFormatException e){
System.out.println("Error: Must input an integer!");
}catch(IllegalArgumentException e){
System.out.println(e.getMessage());
}finally{
System.out.println("Finally Block");
}
System.out.println("After Try");
}
}
This program will read an integer from the terminal. If the integer is greater than or equal to 0, it will do nothing except print the “Finally Block” and “After Try” messages. However, if the input is less than 0, it will throw an IllegalArgumentException. Finally, if the input is not a number, then an InputMismatchException will be thrown and handled. In each of those cases, it will also print the “Finally Block” message.
Let’s run this program a few times and see how it works. First, we’ll provide the input “5”:
$ java Finally
5
Finally Block
After Try
Here, we can see that the code in the finally
block always runs when the program is finished executing the statements in the try
block.
Let’s run it again, this time with “-5” as the input:
$ java Finally
-5
Input must be greater than 0!
Finally Block
After Try
Here, we can see that it prints the error message caused by the IllegalArgumentException, then proceeds to the finally
block. So, even if an exception is thrown while inside of the try
block, the code in the finally
block is always executed once the try
block and any exception handlers are finished.
Here’s one more example, this time with “abc” as the input:
$ java Finally
abc
Error: Must input an integer!
Finally Block
After Try
Once again, we see that the code first handles the NumberFormatException, and then it will execute the code in the finally
block.
In a later chapter, we’ll learn more about how to use the finally
block to perform important tasks such as closing open files and making sure we don’t leave the system in an unstable state when we handle an exception.
Lastly, Java includes a special type of Try-Catch statement, known as a Try with Resources statement, that can perform some of the work typically handled by the finally
block.
Let’s look at an example of a Try with Resources statement:
import java.util.Scanner;
import java.io.File;
import java.lang.NumberFormatException;
import java.io.FileNotFoundException;
public class Resources{
public static void main(String[] args){
try(
Scanner reader = new Scanner(new File("input.txt"))
){
int x = Integer.parseInt(reader.nextLine().trim();
System.out.println(x + 5);
}catch(NumberFormatException e){
System.out.println("Error: Invalid Number Format!");
}catch(FileNotFoundException e){
System.out.println("Error: File Not Found!");
}
}
}
In this example, there is a new statement after the try
keyword, surrounded by parentheses. Inside of that statement, we are declaring and initializing a new Scanner object to read data from a file. That Scanner object is the resource that we are using in our Try with Resources statement. We can add multiple resources to that section, separated by semicolons ;
.
When the code in the try
statement throws an exception, Java will automatically try to close the resources declared in the Try with Resources statement. So, when we are reading input from a file, it would close that file and make sure that it isn’t damaged or left open when our program crashes.
In addition, any additional exceptions thrown when trying to close the file are suppressed by the system, so we only see the exception that caused the initial error. This is much better than using a finally
statement to close the file, since we’d have to run the risk of throwing a second exception inside of the finally
statement.
Any Java class that implements the AutoCloseable interface can be used as a resource in this way. The Java 8 API documentation for AutoCloseable lists all known classes that implement that interface. We’ll discuss interfaces more later in this course.
Specifically, this type of statement is great when writing programs that will handle large amounts of input, either by connecting to a database, reading from a file, or using the Internet to communicate with a server. A Try with Resources statement is a great way to make sure those programs are able to handle exceptions without leaving the system in an unstable state.
Try-with resources should never be used in concert with Scanners Readers or Buffers connected to System resources. For example
// attach scanner to System stdin
Scanner reader = new Scanner( System.in);
// use as part of try with resources
try (Scanner scan = reader){...
} catch (Exception e){
}
// now System.in is closed and unavailable to rest of program
is a bad construct.
Now that we’ve seen lots of information about how to throw, catch, and handle exceptions in Java, it is a great time to discuss some of the best practices we can apply in our code to make it as readable and bulletproof as possible. While these aren’t strict rules that must be followed all the time, they are great things to keep in mind as we write code that deals with exceptions.
As much as possible, we should write our code to handle any exception we can reasonably expect our users to run into when using our programs. We cannot assume that users will always provide the correct input in the correct format, and a single typo by a user should not cause our entire program to crash.
Instead, we should always use Try-Catch statements whenever possible to detect and handle those errors as simply as possible. In addition, if the program is interactive, we can combine that approach with the use of loops to prompt the user to provide new input to resolve the error.
The Java 8 API is a great resource to determine which exceptions could be thrown by any methods used in our code.
You can go “try catch” crazy. Exception handling is powerful, but also really, really slow.
For example, this statement using exceptions is very slow:
try{
ratio = x / y;
}catch ArithmeticException e{
System.out.println("Cannot divide by zero");
}
but this version of the statement using a simple conditional statement:
if (y !=0){
ratio = x / y;
}else{
System.out.println("Cannot divide by zero");
}
executes a lot more efficiently. In this example the if statement is about 60 times faster than the try-catch statement.
In fact, most unchecked exceptions can be avoided through value checking. In this course, we may direct you to throw and catch exceptions of this type for practice in exception coding.
It is BAD practice to have implied control flow embedded and disguised as try catch
. For example, if we are to accept input from either a file given as a command line argument or the keyboard. This first code block is acceptable:
Scanner reader;
if args.length() == 1{
try{
reader = new Scanner(new File(args[0]));
}
catch Exception e{
System.out.println("error accessing file");
return;
}
} else {
reader = new Scanner(System.in);
}
This next code block is bad style:
try{
scanner = new Scanner(new File(args[0]));
}catch(FileNotFoundException e){
System.out.println("FileNotFoundException: " + e.getMessage());
scanner = new Scanner(System.in);
}
}
Most readers of your code will not expect “flow logic” to be in the catch blocks and may miss it.
We should also always strive to use specific exceptions in our catch
statements whenever we can. So, if we are opening a file, we should include a catch
statement for a FileNotFoundException, and not just a single generic Exception
. Even though this means we may have to include several catch
statements, it is much better in the long run because it allows us to know exactly what happened when our code has a problem.
In addition, we should always make sure we catch the most specific exceptions first, before any more generic exceptions. As we saw earlier, if we try to catch an IOException before a FileNotFoundException, we won’t be able to tell when we’ve reached a FileNotFoundException at all, since it is a subtype of the IOException.
The Java Exception class includes some very helpful methods we can use to get additional information when we encounter an exception.
Here’s an example to show what we can learn:
import java.lang.ArithmeticException;
public class StackTrace{
public static void main(String[] args){
try{
int x = 5 / 0;
}catch(ArithmeticException e){
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
}
When this code is executed, it will produce the following output:
$ java StackTrace
Error: / by zero
java.lang.ArithmeticException: / by zero
at StackTrace.main(Finally.java:6)
The first line will use e.getMessage()
to get the short version of the error, in this case the text “/ by zero”. The second line uses e.printStackTrace()
to print a full stack trace showing the location of the error in code.
In general, it is best to only show the short error message to users, but in some cases it may be better to add our own message. The text “/ by zero” isn’t very clear, even to developers. Instead, we could say “Error: divisor cannot be zero” to make the error message clearer.
The information contained in the stack trace is very helpful to developers when trying to debug and fix problems in the code, but that information is very confusing to users. As we learn how to build more advanced programs, we’ll see how to record and log those error messages and stack traces for debugging, but hide them from our users.
To see what other methods are available, refer to the Throwable entry in the Java 8 API reference.
Another big problem is that many developers tend to catch exceptions, only to ignore them so their program doesn’t crash. Here’s a quick example:
int x = 0;
Scanner reader = new Scanner(System.in);
try{
x = Integer.parseInt(reader.nextLine());
}catch(Exception e){
//do nothing
}
In this code, if the user inputs something that can’t be converted to an integer, the code just silently ignores the exception and proceeds with x
still storing the value $0$. While that may not cause issues, it can also make it very frustrating to debug later issues or changes in this program. So, it is always best to output an error message when an exception occurs, even if it can be easily ignored without additional input from the user. This will make it easier to debug additional issues later on in development.
Here’s a simple program that asks the user for input, and will repeat the question until a valid input is received. This code is designed to handle many common situations and exceptions:
import java.util.Scanner;
import java.lang.NumberFormatException;
import java.util.NoSuchElementException;
public class HandleInput{
public static void main(String[] args){
int x = 0;
try(
Scanner reader = new Scanner(System.in)
){
while(x <= 0){
try{
System.out.print("Please input a positive integer: ");
x = Integer.parseInt(reader.nextLine());
if(x <= 0){
System.out.println("Error: Negative Integer Detected!");
}
}catch(NumberFormatException e){
System.out.println("Error: Integer Not Found!");
reader.nextLine(); // bypass bad input and try again
}catch(NoSuchElementException e){
System.out.println("Error: No Input Found!");
}catch(Exception e){
System.out.println("Error: Unknown Input Error!");
return; //probably can't recover, so stop executing
}
}
System.out.println("You entered " + x);
}catch(Exception e){
System.out.println("Error: Unable to Open Scanner!");
}
}
}
There are several items in this code that help make it very bulletproof:
catch
block to handle any exceptions that arise from opening the Scanner. This prevents those exceptions from being thrown to the user. If we are opening a file, we’ll also need to handle the FileNotFoundException here.next()
to successfully read the next token from input before we continue, or else it will try to read the same thing again.return
keyword to stop executing the program, since it may be difficult to continue to read input in that case. Thankfully, since we are using a Try With Resources block, the Scanner object will be correctly closed for us automatically. In addition, any finally
blocks would be executed before the program stops.We should not use System.exit()
to exit our program, which you may find on many online resources. The System.exit()
method directly ends the Java Virtual Machine, or JVM, without properly closing any resources or executing finally
blocks. It can also make your code much more difficult to maintain and test. This can be very dangerous!
Of course, there are many things that can be done differently in this code, depending on our preferences and how we’d like our program to function. This is just one possible way to build useful code that handles several exceptions.
Let’s work through an entire example program together to see how we can build programs that handle exceptions quickly and easily, without allowing the program to crash when invalid input is received.
Consider the following problem statement for a driver-class program Example.java
:
Write a program to accept a list of numbers, one per line from the terminal.
The numbers can either be positive whole numbers or positive floating point numbers. The program should continue to accept input until a negative number is received. Zero is an acceptable input.
Once a negative number is received, the program should print the largest and smallest number provided as input (not including the the negative number end input). Sample outputs are shown below.
The program should catch and handle all common exceptions. When an exception is caught, the program should print the name of the exception. It should then continue to accept input (if possible) or exit the program (if not possible).
Here’s an example of an expected input for the program:
5.0
3
8.5
7
k
2.3.6
-1
Here is the correct output for that input:
NumberFormatException
NumberFormatException
Maximum: 8.5
Minimum: 3.0
Pause a moment to think about control flow, loops and exceptions you might need to handle.
When first starting out in coding and exception handling, it is often best to first write the code without exception handling, test it, then add the exception stuff. Exception handlers can hide logic errors.
We’ll start out with the code to “attach” a Scanner to the correct input source.
import java.util.Scanner;
import java.lang.NumberFormatException;
import java.util.NoSuchElementException;
public class Example{
public static void main (String[] args) {
Scanner scanner = new Scanner(System.in);
}
}
Next let’s use a do-while loop to handle input until there is a negative number. We will treat all inputs as doubles.
double input;
do{
input = Double.parseDouble(scanner.nextLine().trim());
}while(input >= 0.0);
If the input is negative, we do nothing. Otherwise we keep track of the minimum and maximum number seen. Note, that the first number input is both the min and max number seen. We’ll handle this by observing that an acceptable entered number cannot be less than zero so initial values for the min and max can bet set to -1.0
as a sentinel value.
double max = -1.0;
double min = -1.0;
double input = 0.0;
do{
input = Double.parseDouble(scanner.nextLine().trim());
if (input >= 0.0 ){
if (min > input || min < 0.0){
min = input;
}
if (max < input || max < 0.0){
max = input;
}
}
} while (input >= 0.0);
Lastly we would print the min and max out.
double max = -1.0;
double min = -1.0;
double input = 0.0;
do{
input = Double.parseDouble(scanner.nextLine().trim());
if (input >= 0.0 ){
if (min > input || min < 0.0){
min = input;
}
if (max < input || max < 0.0){
max = input;
}
}
} while (input >= 0.0);
System.out.println("Maximum: " + max);
System.out.println("Minimum: " + min);
We leave it for you to finish the “without exception handling” program and test it from the terminal.
Although rare, a Scanner
object connected to System.in
can throw a NoSuchElementException
when nextLine()
is called. This can occur when:
System.in
(more on this is discussed in the chapter on Files)In Linux, the <
operator can be used at the terminal to send the contents of a file in place of stdin
.
java Example < tests/example1.txt
You can see this “trick” if you have Example.java
working. The command java Example < tests/example1.txt
will provide the input from the file example1.txt
that is stored in the tests
folder to the program, just as if it was typed directly in the terminal by hand.
Let’s catch the NoSuchElementException
,
double max = -1.0;
double min = -1.0;
double input = 0.0;
do{
try{
input = Double.parseDouble(scanner.nextLine().trim());
}
catch (NoSuchElementException e){
System.out.println("NoSuchElementException");
return; //exits the program
}
if (input >= 0.0 ){
if (min > input || min < 0.0){
min = input;
}
if (max < input || max < 0.0){
max = input;
}
}
} while (input >= 0.0);
System.out.println("Maximum: " + max);
System.out.println("Minimum: " + min);
Here we catch the Exception and exit the program. Remember to catch a specific Exception you must import it.
The other thing that can go awry is the parsing. Reading a String that would not parse into a Double, which will throw a NumberFormatException
. However, from the example inputs it is clear that we should not exit on this condition but instead keep accepting input. continue
will reset us to the next loop.
double max = -1.0;
double min = -1.0;
double input = 0.0;
do{
try{
input = Double.parseDouble(scanner.nextLine().trim());
}
catch (NoSuchElementException e){
System.out.println("NoSuchElementException");
return; //exits the program
}
catch(NumberFormatException e ){
System.out.println("NumberFormatException");
continue;
}
if (input >= 0.0 ){
if (min > input || min < 0.0){
min = input;
}
if (max < input || max < 0.0){
max = input;
}
}
} while (input >= 0.0);
System.out.println("Maximum: " + max);
System.out.println("Minimum: " + min);
Finally, we may want to do some quick testing of our program. In theory, it should handle any inputs provided. When testing, we should always see if we can find some input that breaks our program, then adjust the program as needed to prevent that problem. Of course, we should always make sure that it produces the correct answers, too.