Exception Handling: File CRUD Operations Example


Introduction to Exception Handling

This is a supplement to my previous blog Creating Custom Exception Classes in Java. I recommend you read that one first and then read this one.

Java provides many Exception (and Error) Handling classes. Both of these categories implement the Throwable interface. Although it is not imperative that you develop your own custom exception classes, it is often a good idea to do so; particularly when dealing with large systems. The main reason to create custom exception classes is to quickly identify which custom class is being affected or causing these custom exceptions. For example, referencing a null object and trying to use it will result in a NullPointerException. However, in your code, there could be literally hundreds or even thousands of statements which could result in such exception.

Designing your Custom Exception Class

Suppose that you are designing a class that will serve as a message receiver. For simplicity, let’s call it MessageReceiver.java. This message receiver class is designed to receive a message and process it. The UML below illustrates this basic concept. This is the basic design pattern that you should follow to implement custom exception classes.
  1. Create a Custom Exception class named some custom class ONLY IF THE JAVA PROVIDED CLASSES ARE NOT EXPLICIT ENOUGH FOR YOUR CASE. No sense in reinventing the wheel. Only create those classes that you need.
  2. Your custom class must extend the Java Exception class; which is the parent class of all exception classes.
  3. Add at least four constructors as illustrated in the UML below. No additional methods are needed.



Suppose that the received message (which is an object) is null. Attempting to access this null object could result in some NullPointerException that is too general to effectively troubleshoot. However, you can design your custom exception class to add enough detail to this exception so that exception messages are more helpful. Just the fact that you will see on the program’s stack trace that a MessageReceiverException is being thrown is more helpful than the general NullPointerException in this case. You can further expand the user-friendliness of your custom exception class by generating your own set of messages.


package demo;

public interface IMessageReceiver
{
    public void onMessage(Message message) throws MessageReceiverException;
}



package demo;

public class Message
{
    public String extractMessage() {
        return "Hello!";
    }
}



public class MessageReceiver implements IMessageReceiver {

    public void onMessage(Message message) {
        processMessage(message);
    }

    private void processMessage(Message message) {
        System.out.println("Message: " + message.extractMessage());
    }

    public static void main(String[] args){
        Message message = null;
        IMessageReceiver receiver = new MessageReceiver();
        receiver.onMessage(message);
    }
}
The stack trace if this program is executed will look something like this:
run:
Exception in thread "main" java.lang.NullPointerException
at demo.MessageReceiver.processMessage(MessageReceiver.java:19)
at demo.MessageReceiver.onMessage(MessageReceiver.java:15)
at demo.MessageReceiver.main(MessageReceiver.java:25)
Java Result: 1


By looking at the main method used to test this class, you can quickly see how this execution will result in a NullPointerException that is not handled by the message receiver. This exception is not very informative. In a large system it would be very hard to find the root cause of such generic exception messages. If we create a custom Message Receiver exception class following the procedure already outlined in this document, we could make the following modifications to the code to make it more specific:


public class MessageReceiver2 implements IMessageReceiver {

    public void onMessage(Message message) throws MessageReceiverException{
        try {
            processMessage(message);
        } catch (MessageReceiverException ex) {
            System.out.println("ERROR: " + ex.getClass() + " caught.");
            System.out.println("CAUSE: " + ex.getMessage());
        }
    }

    private void processMessage(Message message) throws MessageReceiverException {
        try {
            System.out.println("Message: " + message.extractMessage());
        } catch (NullPointerException e) {
            throw new MessageReceiverException("Message object was null");
        }
    }

    public static void main(String[] args){
        Message message = null;
        IMessageReceiver receiver = new MessageReceiver2();
        receiver.onMessage(message);
    }
}
The console output of this implementation is much more user friendly:


run:
ERROR: class demo.MessageReceiverException caught.
CAUSE: Message object was null

The key to making this user friendly is how the original exception is handled. In this case, the process message method attempted to extract the message contents from an empty object which resulted in a null pointer exception. Instead of replicating this same (non user-friendly) exception, it actually transformed this exception into our custom Message Receiver Exception with a custom message that stated that the object being manipulated was null. In turn, the on message method caught this exception and displayed a well-constructed, user-friendly message to the console that should simplify determining what went wrong during execution. These print statements could have been logged in a file as well or instead.

File CRUD Operations Example


package demo;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This is to demonstrate CRUD (Create, Read, Update, Delete) on a File object
 * @author Hector Fontanez
 */
public class FileManipulationDemo {

    public static File create (String path) throws IOException {
        FileWriter writer = null;
        File file = new File(path); //Created in memory
        try {
            writer = new FileWriter(file); //This is when the file is committed to file system
            writer.write("Welcome to Java!");
            writer.write("\nHello World!");
            writer.close(); //Closes the stream (FileWriter instance cannot be used anymore)
        } catch (IOException ex) {
            Logger.getLogger(FileManipulationDemo.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
        return file;
    }

    public static void read(File file) throws IOException {
        String line = null;
        FileReader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);

        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        //Reclaim resources whenever you can
        fr.close();
        br.close();
    }

    public static void update(File file) throws IOException {
        FileWriter writer = null;
        try {
            writer = new FileWriter(file,true); //The second argument is the APPEND flag
            writer.write("\nThis should update the file");
            writer.flush();//Contents is saved but stream is not closed
            writer.close();//Contents is saved AND stream is closed
        } catch (IOException ex) {
            Logger.getLogger(FileManipulationDemo.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
    }

    public static void delete(File file) {
        file.delete();
    }

    public static void main(String[] args) {

        try {
            File file = create("C:/Users/Hector/Desktop/myfile.txt");
            read(file);
            update(file);
            read(file); // Should show the update
            delete(file);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(FileManipulationDemo.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(FileManipulationDemo.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Conclusion

This example demonstrates basic file operations (create, read, update, and delete) and makes use of exception handling for efficient handling of unexpected situations. It also illustrates another good point about exception handling: When handling different types of exceptions, pay attention to the hierarchy. You can see that the portion of the code dealing with reading files could result in two types of exceptions: IOException or FileNotFoundException. These two exception are related in the sense that FileNotFoundException is a subclass of IOException. Therefore, it should be listed in the catch block before IOException. Otherwise, IOException will handle any FileNotFoundException that may occur and will make the FileNotFoundException block unreachable; resulting in a compile error.

There may be cases when it doesn't make sense to catch all of the exception of a particular type because the same action to be taken might apply to all cases. For instance, a FileNotFoundException might be handled the same way as the more generic IOException; like in this example. Since the same action is taking place, it is OK to remove the most specific FileNotFoundException and leaving the most general IOException to handle all cases. Keep in mind that this is not going to be the case all the time. There might be situations that might require a different type of action. For example, a file not existing is a completely different case than a file existing but it is locked by another user. It is very possible that the latter could be handled by invoking some method that would allow access to a copy of the original file for reading purposes. On the other hand, a file that doesn't exist could be handled by simply creating a blank file to replace it. So, in those cases, the bodies of these catch blocks would be totally different.

For the sake of rapid development, you may decide to handle everything generically at first and, as the system matures and you gain more knowledge of the system and the requirements get refined, you may decide to add more specific exception handling to your code. Obviously, the preferred solution is to add as much refinement as possible based on the existing requirements. If there is something my experience have taught me is that sometimes it is very difficult to add such refinements to the system once it is considered complete; especially if no bugs have been reported.

Lastly, and almost a direct contradiction to the point just made, you do not want to start by making everything to catch the Exception class. In my opinion, that is extremely poor programming (lazy programming) that could lead to more headaches than it is worth. Even in the initial stages of software design, you should have sufficient insight into what is being build to make better decisions with regards to exception handling.

Comments

Popular posts from this blog

Implementing Interfaces with Java Records

Customizing Java Records