• Thu. Sep 19th, 2024

Exceptions in Java: Advanced features and types

Byadmin

Sep 19, 2024



My example revealed only one cause. Exceptions thrown from non-trivial real-world applications may contain extensive chains of many causes. You can access these causes by employing a loop such as the following:
catch (Exception exc)
{
Throwable t = exc.getCause();
while (t != null)
{
System.out.println(t);
t = t.getCause();
}
}

Try-with-resources
Java applications often access files, database connections, sockets, and other resources that depend on related system resources (e.g., file handles). The scarcity of system resources implies that they must eventually be released, even when an exception occurs. When system resources aren’t released, the application eventually fails when attempting to acquire other resources, because no more related system resources are available.
In my introduction to exception handling basics, I mentioned that resources (actually, the system resources on which they depend) are released in a finally block. This can lead to tedious boilerplate code, such as the file-closing code that appears below:
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
// ignore exception
}
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
// ignore exception
}
}
Not only does this boilerplate code add bulk to a classfile, the tedium of writing it might lead to a bug, perhaps even failing to close a file. JDK 7 introduced try-with-resources to overcome this problem.
The basics of try-with-resources
The try-with-resources construct automatically closes open resources when execution leaves the scope in which they were opened and used, whether or not an exception is thrown from that scope. This construct has the following syntax:
try (resource acquisitions)
{
// resource usage
}
The try keyword is parameterized by a semicolon-separated list of resource-acquisition statements, where each statement acquires a resource. Each acquired resource is available to the body of the try block, and is automatically closed when execution leaves this body. Unlike a regular try statement, try-with-resources doesn’t require catch blocks and/or a finally block to follow try(), although they can be specified.
Consider the following file-oriented example:
try (FileInputStream fis = new FileInputStream(“abc.txt”))
{
// Do something with fis and the underlying file resource.
}
In this example, an input stream to an underlying file resource (abc.txt) is acquired. The try block does something with this resource, and the stream (and file) is closed upon exit from the try block.

Using ‘var’ with ‘try-with-resources’
JDK 10 introduced support for var, an identifier with special meaning (i.e., not a keyword). You can use var with try-with-resources to reduce boilerplate. For example, you could simplify the previous example to the following:
try (var fis = new FileInputStream(“abc.txt”))
{
// Do something with fis and the underlying file resource.
}

Copying a file in a try-with-resources context
In the previous article, I excerpted the copy() method from a file-copy application. This method’s finally block contains the file-closing boilerplate presented earlier. Listing 8 improves this method by using try-with-resources to handle the cleanup.
Listing 8. Copy.java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Copy
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println(“usage: java Copy srcfile dstfile”);
return;
}

try
{
copy(args[0], args[1]);
}
catch (IOException ioe)
{
System.err.println(“I/O error: ” + ioe.getMessage());
}
}

static void copy(String srcFile, String dstFile) throws IOException
{
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(dstFile))
{
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
}
}
copy() uses try-with-resources to manage source and destination file resources. The round bracketed-code following try attempts to create file input and output streams to these files. Assuming success, its body executes, copying the source file to the destination file.
Whether an exception is thrown or not, try-with-resources ensures that both files are closed when execution leaves the try block. Because the boilerplate file-closing code that was shown earlier isn’t needed, Listing 8’s copy() method is much simpler and easier to read.
Designing resource classes to support try-with-resources
The try-with-resources construct requires that a resource class implement the java.lang.Closeable interface or the JDK 7-introduced java.lang.AutoCloseable superinterface. Pre-Java 7 classes like java.io.FileInputStream implemented Closeable, which offers a void close() method that throws IOException or a subclass.
Starting with Java 7, classes can implement AutoCloseable, whose single void close() method can throw java.lang.Exception or a subclass. The throws clause has been expanded to accommodate situations where you might need to add close() methods that can throw exceptions outside of the IOException hierarchy; for example, java.sql.SQLException.
Listing 9 presents a CustomARM application that shows you how to configure a custom resource class so that you can use it in a try-with-resources context.
Listing 9. CustomARM.java
public class CustomARM
{
public static void main(String[] args)
{
try (USBPort usbp = new USBPort())
{
System.out.println(usbp.getID());
}
catch (USBException usbe)
{
System.err.println(usbe.getMessage());
}
}
}

class USBPort implements AutoCloseable
{
USBPort() throws USBException
{
if (Math.random()
Listing 9 simulates a USB port in which you can open and close the port and return the port’s ID. I’ve employed Math.random() in the constructor so that you can observe try-with-resources when an exception is thrown or not thrown.
Compile this listing and run the application. If the port is open, you’ll see the following output:
port open
some ID
port close
If the port is closed, you might see this:
unable to open port
Suppressing exceptions in try-with-resources
If you’ve had some programming experience, you might have noticed a potential problem with try-with-resources: Suppose the try block throws an exception. This construct responds by invoking a resource object’s close() method to close the resource. However, the close() method might also throw an exception.
When close() throws an exception (e.g., FileInputStream‘s void close() method can throw IOException), this exception masks or hides the original exception. It seems that the original exception is lost.
In fact, this isn’t the case: try-with-resources suppresses close()‘s exception. It also adds the exception to the original exception’s array of suppressed exceptions by invoking java.lang.Throwable‘s void addSuppressed(Throwable exception) method.
Listing 10 presents a SupExDemo application that demonstrates how to repress an exception in a try-with-resources context.
Listing 10. SupExDemo.java
import java.io.Closeable;
import java.io.IOException;

public class SupExDemo implements Closeable
{
@Override
public void close() throws IOException
{
System.out.println(“close() invoked”);
throw new IOException(“I/O error in close()”);
}

public void doWork() throws IOException
{
System.out.println(“doWork() invoked”);
throw new IOException(“I/O error in work()”);
}

public static void main(String[] args) throws IOException
{
try (SupExDemo supexDemo = new SupExDemo())
{
supexDemo.doWork();
}
catch (IOException ioe)
{
ioe.printStackTrace();
System.out.println();
System.out.println(ioe.getSuppressed()[0]);
}
}
}
Listing 10’s doWork() method throws an IOException to simulate some kind of I/O error. The close() method also throws the IOException, which is suppressed so that it doesn’t mask doWork()‘s exception.
The catch block accesses the suppressed exception (thrown from close()) by invoking Throwable‘s Throwable[] getSuppressed() method, which returns an array of suppressed exceptions. Only the first element is accessed because only one exception is suppressed.
Compile Listing 10 and run the application. You should observe the following output:
doWork() invoked
close() invoked
java.io.IOException: I/O error in work()
at SupExDemo.doWork(SupExDemo.java:16)
at SupExDemo.main(SupExDemo.java:23)
Suppressed: java.io.IOException: I/O error in close()
at SupExDemo.close(SupExDemo.java:10)
at SupExDemo.main(SupExDemo.java:24)

java.io.IOException: I/O error in close()

Multiple catch blocks (multi-catch)
Starting in JDK 7, it is possible to codify a single catch block that catches more than one type of exception. The purpose of this multi-catch feature is to reduce code duplication and reduce the temptation to catch overly broad exceptions (for instance, catch (Exception e)).
Suppose you’ve developed an application that gives you the flexibility to copy data to a database or file. Listing 11 presents a CopyToDatabaseOrFile class that simulates this situation, and demonstrates the problem with catch block code duplication.
Listing 11. CopyToDatabaseOrFile.java
import java.io.IOException;

import java.sql.SQLException;

public class CopyToDatabaseOrFile
{
public static void main(String[] args)
{
try
{
copy();
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
// additional handler code
}
catch (SQLException sqle)
{
System.out.println(sqle.getMessage());
// additional handler code that’s identical to the previous handler’s
// code
}
}

static void copy() throws IOException, SQLException
{
if (Math.random()
JDK 7 overcomes this code duplication problem by letting you specify multiple exception types in a catch block where each successive type is separated from its predecessor by placing a vertical bar (|) between these types:
try
{
copy();
}
catch (IOException | SQLException iosqle)
{
System.out.println(iosqle.getMessage());
}
Now, when copy() throws either exception, the exception will be caught and handled by the catch block.
When multiple exception types are listed in a catch block’s header, the parameter is implicitly regarded as final. As a result, you cannot change the parameter’s value. For example, you cannot change the reference stored in the previous code fragment’s iosqle parameter.

Shrinking bytecode
The bytecode resulting from compiling a catch block that handles multiple exception types will be smaller than compiling several catch blocks that each handle only one of the listed exception types. A catch block that handles multiple exception types contributes no duplicate bytecode during compilation. In other words, the bytecode doesn’t contain replicated exception handlers.

Final re-throw
Starting in JDK 7, the Java compiler is able to analyze re-thrown exceptions more precisely than in previous Java versions. This feature only works when no assignments are made to a re-thrown exception’s catch block parameter, which is considered to be effectively final. When a preceding try block throws an exception that’s a supertype/subtype of the parameter’s type, the compiler throws the caught exception’s actual type instead of throwing the parameter’s type (as was done in previous Java versions).
The purpose of this final re-throw feature is to facilitate adding a try–catch statement around a block of code to intercept, process, and re-throw an exception without affecting the statically determined set of exceptions thrown from the code. Also, this feature lets you provide a common exception handler to partly handle the exception close to where it’s thrown, and provide more precise handlers elsewhere that handle the re-thrown exception. Consider Listing 12.
Listing 12. MonitorEngine.java
class PressureException extends Exception
{
PressureException(String msg)
{
super(msg);
}
}

class TemperatureException extends Exception
{
TemperatureException(String msg)
{
super(msg);
}
}

public class MonitorEngine
{
public static void main(String[] args)
{
try
{
monitor();
}
catch (Exception e)
{
if (e instanceof PressureException)
System.out.println(“correcting pressure problem”);
else
System.out.println(“correcting temperature problem”);
}
}

static void monitor() throws Exception
{
try
{
if (Math.random() 0.9)
throw new TemperatureException(“temperature too high”);
else
System.out.println(“all is well”);
}
catch (Exception e)
{
System.out.println(e.getMessage());
throw e;
}
}
}
Listing 12 simulates the testing of an experimental rocket engine to see if the engine’s pressure or temperature exceeds a safety threshold. It performs this testing via the monitor() helper method.
monitor()‘s try block throws PressureException when it detects a pressure extreme, and throws TemperatureException when it detects a temperature extreme. (Because this is only a simulation, random numbers are used — the java.lang.Math class’s static double random() method returns a random number between 0.0 and (almost) 1.0.) The try block is followed by a catch block designed to partly handle the exception by outputting a warning message. This exception is then re-thrown so that monitor()‘s calling method can finish handling the exception.
Before JDK 7 you couldn’t specify PressureException and TemperatureException in monitor()‘s throws clause because the catch block’s e parameter is of type java.lang.Exception and re-throwing an exception was treated as throwing the parameter’s type. JDK 7 and successor JDKs have made it possible to specify these exception types in the throws clause because their compilers can determine that the exception thrown by throw e comes from the try block, and only PressureException and TemperatureException can be thrown from this block.
Because you can now specify static void monitor() throws PressureException, TemperatureException, you can provide more precise handlers where monitor() is called, as the following code fragment demonstrates:
try
{
monitor();
}
catch (PressureException pe)
{
System.out.println(“correcting pressure problem”);
}
catch (TemperatureException te)
{
System.out.println(“correcting temperature problem”);
}
Because of the improved type checking offered by final re-throw in JDK 7, source code that compiled under previous versions of Java might fail to compile under later JDKs. For example, consider Listing 13.
Listing 13. BreakageDemo.java
class SuperException extends Exception
{
}

class SubException1 extends SuperException
{
}

class SubException2 extends SuperException
{
}

public class BreakageDemo
{
public static void main(String[] args) throws SuperException
{
try
{
throw new SubException1();
}
catch (SuperException se)
{
try
{
throw se;
}
catch (SubException2 se2)
{
}
}
}
}
Listing 13 compiles under JDK 6 and earlier. However, it fails to compile under later JDKs, whose compilers detect and report the fact that SubException2 is never thrown in the body of the corresponding try statement. This is a small problem that you are unlikely to encounter in your programs, and a worthwhile trade-off for having the compiler detect a source of redundant code. Removing redundancies results in cleaner code and smaller classfiles.

StackWalker and the StackWalking API
Obtaining a stack trace via Thread‘s or Throwable‘s getStackTrace() method is costly and impacts performance. The JVM eagerly captures a snapshot of the entire stack (except for hidden stack frames), even when you only need the first few frames. Also, your code will probably have to process frames that are of no interest, which is also time-consuming. Finally, you cannot access the actual java.lang.Class instance of the class that declared the method represented by a stack frame. To access this Class object, you’re forced to extend java.lang.SecurityManager to access the protected getClassContext() method, which returns the current execution stack as an array of Class objects.

JDK 9 introduced the java.lang.StackWalker class (with its nested Option class and StackFrame interface) as a more performant and capable alternative to StackTraceElement (plus SecurityManager). To learn about StackWalker and its related types, see my introduction to the StackWalking API.

In conclusion
This article completes my two-part introduction to Java’s exception handling framework. You might want to reinforce your understanding of this framework by reviewing Oracle’s Exceptions lesson in the Java Tutorials. Another good resource is Baeldung’s Exception handling in Java tutorial, which includes anti-patterns in exception handling.



Source link