Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

187Cost of Causing Exceptions

Author: Dr. Heinz M. KabutzDate: 2010-08-31Java Version: 6Category: Performance
 

Abstract: Many years ago, when hordes of C++ programmers ventured to the greener pastures of Java, some strange myths were introduced into the language. It was said that a "try" was cheaper than an "if" - when nothing went wrong.

 

Welcome to the 187th issue of The Java(tm) Specialists' Newsletter. Summer is rapidly ending here in Crete. We have not had a drop of rain since June, but the temperature is now a chilly 28 degrees Celsius. I will need to soon check that my central heating system is operational. Our kids are still on holiday for a couple of weeks, having broken up for their summer break in the middle of June. If I ever had a chance be a child again, I would choose Greece as the place to grow up. Sun, sea and every three years on holiday for one (on average, not counting strikes by teachers or pupils).

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Cost of Causing Exceptions

A few weeks ago, Herman Lintvelt sent me a discussion he had with some of the Java programmers he was mentoring. One of the programmers had read that a try/catch was for free and so he was doing things like:

try {
  return policy.calculateInterest();
} catch(NullPointerException ex) {
  // policy object was null
  return 0.0f;
}

instead of using an if-else construct:

if (policy != null) {
  return policy.calculateInterest();
} else {
  // policy object was null
  return 0.0f;
}

This led to Java code that was hard to understand. However, it was equally difficult to persuade Herman's prodigy that we should not write such code. After all, he kept pointing out that try was faster than if.

In this newsletter, we will try to persuade you that it is a bad coding practice to cause exceptions unnecessarily. We will then explain why these two approaches take almost the same amount of time.

Causing Exceptions: Style

Here are some reasons why it is better to check conditions rather than cause exceptions:

1. Exceptions should be used to indicate an exceptional or error condition. We should not use them to change the control flow of our program. When we use them to avoid an if-else for null, we are effectively using them to steer the control flow of our program. This makes it difficult to understand the code.

2. It is not at all clear that the NullPointerException is happening due to policy being null. What if the method calculateInterest() causes a NullPointerException internally due to a programming bug? All we would see is that we calculate zero interest, hiding a potentially serious error.

3. Debuggers are often set up to pause whenever an exception is generated. By causing exceptions in our code, we make debugging of our code almost impossible.

This list is by no means complete. There are many other reasons why we should not code like this. In fact, it would be better to use the Null Object Pattern instead of testing for null, that way the behaviour for when policy is null is well defined. Something like:

return policy.calculateInterest();

Where policy might be an instance of NullPolicy:

public class NullPolicy implements Policy {
  public float calculateInterest() {
    return 0.0f;
  }
}

Performance

The first time I saw this strange construct was in 1999, whilst mentoring C++ programmers on this new wonder called "Java". In order to make Java run faster, they wrote equals() methods that assumed everything would be fine, returning false when a RuntimeException occurred.

For many years, it was faster to just do a try, rather than an if statement (unless of course an exception occurred, in which case it was much much slower). Even though the code was ugly, it did perform a tiny little bit faster.

Constructing exceptions is very expensive, we know that. For most objects, the cost of construction in terms of time complexity is constant. With exceptions, it is related to the call stack depth. Thus it is more expensive to generate exceptions deep down in the hierarchy.

However, a few months ago, I tried to demonstrate to some Java Master students how costly it really was to construct exceptions. For some reason, my test did not work. It in fact demonstrated that it is extremely fast to cause exceptions. We had run out of time for that day, so I was unable to investigate the exact reasons for our weird results.

Herman also tried to write some code that clearly demonstrated that it was much slower to occasionally cause exceptions, rather than use an if-else statement all the time. In his code, he had however used Math.random() to decide whether an object would be null or not. The call to Math.random() swamped the rest of his results, so that he could not get clear results.

I rewrote his test slightly by creating an array of random objects and then calling the methods on the objects. According to some factor, I would have a certain percentage of null values in the array, which would occasionally cause NullPointerException. I expected the cost of creating the exceptions to be much greater than the cost of the if statements. However, it turns out that the speed for both was roughly the same.

After causing the same NullPointerException a number of times, Java starts returning the same exception instance to us. Here is a piece of code that demonstrates this nicely:

import java.util.*;

public class DuplicateExceptionChecker {
  private final IdentityHashMap<Exception, Boolean> previous =
      new IdentityHashMap<Exception, Boolean>();

  public void handleException(Exception e) {
    checkForDuplicates(e);
  }

  private void checkForDuplicates(Exception e) {
    Boolean hadPrevious = previous.get(e);
    if (hadPrevious == null) {
      previous.put(e, false);
    } else if (!hadPrevious) {
      notifyOfDuplicate(e);
      previous.put(e, true);
    }
  }

  public void notifyOfDuplicate(Exception e) {
    System.err.println("Duplicate Exception: " + e.getClass());
    System.err.println("count = " + count(e));
    e.printStackTrace();
  }

  private int count(Exception e) {
    int count = 0;
    Class exceptionType = e.getClass();
    for (Exception exception : previous.keySet()) {
      if (exception.getClass() == exceptionType) {
        count++;
      }
    }
    return count;
  }
}

We would expect to eventually run out of memory, as obviously all new exceptions would be brand new objects? I was surprised to discover that this was not true for the server HotSpot compiler. After a relatively short while, it began returning the same exception instance, with an empty stack trace. You might have seen empty stack traces in your logs. This is why they occur. Too many exceptions are happening too closely together and eventually the server HotSpot compiler optimizes the code to deliver a single instance back to the user.

Here is a class that uses the DuplicateExceptionChecker to check for NullPointerException duplicates. What we do is occasionally set the array elemenet to null, which causes the NullPointerException when we call randomObjects[j].toString().

import java.util.*;

public class NullPointerTest extends DuplicateExceptionChecker {
  private final Object[] randomObjects =
      new Object[1000 * 1000];

  private final String[] randomStrings =
      new String[1000 * 1000];

  public static void main(String[] args) {
    NullPointerTest npt = new NullPointerTest();
    npt.fillArrays(0.01);
    npt.test();
  }

  public void notifyOfDuplicate(Exception e) {
    super.notifyOfDuplicate(e);
    System.exit(1);
  }

  private void fillArrays(double probabilityObjectIsNull) {
    Random random = new Random(0);
    for (int i = 0; i < randomObjects.length; i++) {
      if (random.nextDouble() < probabilityObjectIsNull) {
        randomObjects[i] = null;
      } else {
        randomObjects[i] = new Integer(i);
      }
    }
    Arrays.fill(randomStrings, null);
  }

  private void test() {
    for (int i = 0; i < 100; i++) {
      for (int j = 0; j < randomObjects.length; j++) {
        try {
          randomStrings[j] = randomObjects[j].toString();
        } catch (NullPointerException e) {
          randomStrings[j] = null;
          handleException(e);
        }
      }
    }
  }
}

After a short while, I see output such as:

Duplicate Exception: class java.lang.NullPointerException
count = 228
java.lang.NullPointerException

This is true also for other exceptions that you could cause with the JVM. For example, the ClassCastException:

import java.util.*;

public class ClassCastTest extends DuplicateExceptionChecker {
  private final Object[] randomObjects =
      new Object[1000 * 1000];

  private final String[] randomStrings =
      new String[1000 * 1000];

  public static void main(String[] args) {
    ClassCastTest npt = new ClassCastTest();
    npt.fillArrays(0.01);
    npt.test();
  }

  public void notifyOfDuplicate(Exception e) {
    super.notifyOfDuplicate(e);
    System.exit(1);
  }

  private void fillArrays(double probabilityObjectIsNull) {
    Random random = new Random(0);
    for (int i = 0; i < randomObjects.length; i++) {
      if (random.nextDouble() < probabilityObjectIsNull) {
        randomObjects[i] = new Float(i);
      } else {
        randomObjects[i] = new Integer(i);
      }
    }
    Arrays.fill(randomStrings, null);
  }

  private void test() {
    for (int i = 0; i < 100; i++) {
      for (int j = 0; j < randomObjects.length; j++) {
        try {
          randomStrings[j] = ((Integer)randomObjects[j]).toString();
        } catch (ClassCastException e) {
          randomStrings[j] = null;
          handleException(e);
        }
      }
    }
  }
}

We can see that this also causes an exception that eventually is replaced with a single instance exception.

Duplicate Exception: class java.lang.ClassCastException
count = 263
java.lang.ClassCastException

Lastly we can see that even an ArrayIndexOutOfBoundsException is eventually replaced with a single instance without a stack trace:

import java.util.*;

public class ArrayBoundsTest extends DuplicateExceptionChecker {
  private static final Object[] randomObjects =
      new Object[1000 * 1000];

  private static final int[] randomIndexes =
      new int[1000 * 1000];

  private static final String[] randomStrings =
      new String[1000 * 1000];

  public static void main(String[] args) {
    ArrayBoundsTest test = new ArrayBoundsTest();
    test.fillArrays(0.01);
    test.test();
  }

  public void notifyOfDuplicate(Exception e) {
    super.notifyOfDuplicate(e);
    System.exit(1);
  }

  private void fillArrays(double probabilityIndexIsOut) {
    Random random = new Random(0);
    for (int i = 0; i < randomObjects.length; i++) {
      randomObjects[i] = new Integer(i);
      randomIndexes[i] = (int) (Math.random() * i);
      if (random.nextDouble() < probabilityIndexIsOut) {
        randomIndexes[i] = -randomIndexes[i];
      }
    }
    Arrays.fill(randomStrings, null);
  }

  private void test() {
    for (int i = 0; i < 100; i++) {
      for (int j = 0; j < randomObjects.length; j++) {
        try {
          int index = randomIndexes[j];
          randomStrings[index] = randomObjects[index].toString();
        } catch (ArrayIndexOutOfBoundsException e) {
          randomStrings[j] = null;
          handleException(e);
        }
      }
    }
  }
}

The question still remains - what is faster? I will not answer that question in this newsletter. Since we do not have the cost of object creation during the exception, the number of instructions would be roughly the same.

There is thus a difference between causing and creating exceptions. When we create them ourselves, they are very expensive to initialize due to the fillInStackTrace() method. But when we cause them inside the Java Virtual Machine, they may end up eventually not costing anything, depending on the exception caused. This is an optimization that we could of course add to our own code, but just remember that the stack trace is one of the most important parts of the exception. Leave that out and you might as well not throw it.

History Lesson

When I discovered this new feature, I was amazed as to how clever the Sun engineers had been to sneak this into the very latest Java 6 server HotSpot. Wanting to find out at exactly which point this was added, I went back in time and tried earlier versions of Java 6, then Java 5, 1.4.2, 1.4.1 and 1.4.0. Imagine my surprise when every single version that I tried had this feature? It was actually a bit depressing that the JVM has been doing this since at least February 2002 and that I did not know about it. I have also never read about this anywhere.

Turning Off Fast Exceptions

Ervin Varga sent me a JVM option that you can use to control this behaviour in the JVM, if you ever need to. Use the -XX:-OmitStackTraceInFastThrow to turn it off.

Thank you very much for reading this newsletter. I hope you enjoyed it.

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...