The Limitations of Double.parseDouble()

The Double.parseDouble() method is ubiquitous. Most developers will reach for it reflexively whenever they need to convert a String to a double. However, there are some quirky behaviors to be mindful of. I’ll present those as well as some simple replacement methods that are more robust and better suited to certain situations.

Behavior

There are several edge cases when it comes to parsing strings to doubles. Let’s examine a few and see how the Double.parseDouble() method behaves.

Whitespace

double d = Double.parseDouble("10.0 ");

How does parseDouble handle trailing or leading whitespace? As it turns out, whitespace characters are ignored. According to the Javadoc:

Whitespace is removed as if by the String.trim() method; that is, both ASCII space and control characters are removed.

Maybe that’s what you want, but if you are looking for a stricter parser that would fail on whitespace, parseDouble will not work.

Note that any whitespace within the number itself, such as "10. 0", will throw a NumberFormatException.

Scientific Notation

double d = Double.parseDouble("1e3");

Did you know you could enter values in scientific notation and parseDouble would handle it? I know I didn’t realize this for a long time. Again, this is a case that you might not care about, but if you don’t want to allow this type of entry, you’ll need another solution.

Null

double d = Double.parseDouble(null);

Thankfully, parseDouble clearly documents its behavior with null input values – it throws a NullPointerException. That’s exactly what we’d want to see, so nothing to worry about here.

Empty String

double d = Double.parseDouble("");

Another common edge case here, and thankfully parseDouble behaves the way we’d want it to, throwing a NumberFormatException.

Format Specifiers

As a Java programmer, you probably know you can use a trailing format specifier to determine the type of a floating-point literal.

double d = 10.0d;
double d2 = 10.0D;
float f = 10f;
float f2 = 10.0F;

When using parseDouble, a String ending with one of these specifiers will parse without error. Even though it won’t break anything, seeing something like 10F in a GUI is probably not what you want.

Underscores

After reading what happens with format specifiers, you may be worried about underscores as well. Legal since Java 7, underscores can be used when defining numeric literals. They have no effect on the value, but can make them much easier to read if used with discretion.

public static final double AVOGADROS_NUMBER = 6.022_140_857e23;

Fortunately in this case, if we try to pass such a String to parseDouble we will get a NumberFormatException.

NaN

double d = Double.parseDouble("NaN");

Yes, our old friend Not-a-Number. It’s easy to forget about, but you can sneak the string "NaN" through parseDouble. Another case we probably want to add some checks for …

Infinity

double d = Double.parseDouble("Infinity");

Here’s yet another case where we would probably like the parser to fail, but parseDouble lets it through. The same goes for the strings "+Infinity" and "-Infinity".

Min/Max Values

double d = Double.parseDouble("2e308");
System.out.println(d); // prints "Infinity"

d = Double.parseDouble("-2e308");
System.out.println(d); // prints "-Infinity"

On a related note, passing extremely high or low numbers to parseDouble can return the value Double.POSITIVE_INFINITY or Double.NEGATIVE_INFINITY. Depending on your application, you may want to add additional checks on the value that comes back to avoid this case.

Hex Strings

double d = Double.parseDouble("0x1.0p0");

This is the case that really threw me off. The example above parses correctly (to a value of 1.0), but a "plain" hex string such as "0xcafebabe" will throw a NumberFormatException. Who knew?!?

The Javadocs for Double.toHexString() do shed more light on this topic, but this may be another case where we want to modify parseDouble‘s behavior.

Alternatives

Now that we understand more of the detailed behavior of parseDouble, we can start to evaluate alternatives and try to craft a solution that behaves the way we want it to.

Doubles.tryParse

You know how much I love Guava, and as you’d expect there’s a handy method for this particular scenario called Doubles.tryParse. Although it behaves very similarly to parseDouble when it comes to the edge cases we’ve discussed so far, it does have a slight performance advantage in that it returns null instead of throwing an unchecked NumberFormatException upon a failure. This means we can build our enhanced parser around this method and not have to deal with a try-catch block.

Double tmp = Doubles.tryParse(input);

Edge Cases

Based on the cases we explored earlier, we want to build a parser that will fail on:

  1. Whitespace
  2. Format Specifiers
  3. NaN
  4. Infinity
  5. Values above the min/max
  6. Hex Strings

I’m going to allow values in scientific notation for now, but you could easily modify my method if needed.

The good news is that Doubles.tryParse has us covered on whitespace (that’s the only difference compared to parseDouble). It also performs a null check internally, so we don’t have to add that explicitly to our method. All we have to do then is chain together checks for the remaining edge cases:

public final class ParseUtils
{
    private ParseUtils()
    {
    }

    public static double parseDouble(String input)
    {
        Double tmp = Doubles.tryParse(input);
        if (tmp == null                                                                     // standard invalid strings
                || input.contains(Double.toString(Double.POSITIVE_INFINITY))                // infinity
                || tmp == Double.POSITIVE_INFINITY || tmp == Double.NEGATIVE_INFINITY       // values exceeding min/max
                || input.toLowerCase().endsWith("d") || input.toLowerCase().endsWith("f")   // format specifiers
                || input.contains(Double.toString(Double.NaN))                              // NaN
                || input.toLowerCase().contains("x")                                        // hex strings
        )
        {
            // handle error case
        }
        return tmp;
    }
}

The framework for our improved parser works. All the edge conditions we cared about will be detected, but now we need to think about what to do when an invalid string is found.

Handling the Error Case

Our options at this point are:

  1. Return a distinguished value such as null
  2. Provide separate "state-testing" and "state-dependent" methods
  3. Return an empty optional
  4. Throw an unchecked exception
  5. Throw a checked exception

Let’s take a minute to consider each path.

Distinguished Return Values

In the spirit of Doubles.tryParse, we could have our method return null if parsing fails. We avoid the expense of capturing the entire stack trace that throwing an exception would incur, but I’m not a huge fan of this approach for a few reasons.

First, we’d have to make our method return a boxed Double, which is less than preferred (EJ 61). Second, clients must add special-case code to deal with the possibility of a null return. Even with the best of intentions, this type of handling is easy to forget about, which could lead to even more problems:

If a client neglects to check for a null return and stores a null return value away in some data structure, a NullPointerException may result at some arbitrary time in the future, at some place in the code that has nothing to do with the problem.

State-Testing and State-Dependent Methods

Another approach would be to split this out into two separate methods, one that returns a boolean indicating if the String is a valid double, and another that does the actual parsing.

Let’s walk through Joshua Bloch’s advice on the matter to evaluate whether this makes sense for our scenario ( see EJ 69).

Here are some guidelines to help you choose between a state-testing method and an optional or distinguished return value. If an object is to be accessed concurrently without external synchronization or is subject to externally induced state transitions, you must use an optional or distinguished return value, as the object’s state could change in the interval between the invocation of a state-testing method and its state-dependent method.

Thankfully we are dealing with Strings here, which are immutable and thread-safe. Moving on:

Performance concerns may dictate that an optional or distinguished return value be used if a separate state-testing method would duplicate the work of the state-dependent method.

This would actually be an issue for our parser setup based on the way we have it set up now. By using tryParse as our first pass validation, we get both the checking logic and the parsing logic evaluated in one method invocation. If we were to split this into two methods, we’d basically be evaluating the same checking and parsing logic twice.

We could rip off the regular expression pattern that Guava’s tryParse is using under the hood to do its checks into our own checker method (along with the additional logic we added earlier). To create a separate parser that doesn’t redundantly perform the same checks, we’d basically be back to using Double.parseDouble. Yes, we could wrap it inside our own method, but our parser would still pass values that our checker said were invalid without failing. Hmm … let’s keep reading:

All other things being equal, a state-testing method is mildly preferable to a distinguished return value. It offers slightly better readability, and incorrect use may be easier to detect: if you forget to call a state-testing method, the state-dependent method will throw an exception, making the bug obvious; if you forget to check for a distinguished return value, the bug may be subtle.

Again, this is where I worry that our parser would have to duplicate the checks to achieve that "obvious" bug detection. This option isn’t looking great right now, so let’s consider the others.

Empty Optional

This is certainly a case where we could have our method return an Optional (EJ 55), and an OptionalDouble at that. Optionals force the user of an API to confront the fact that something might have gone wrong in the parsing process. They also allow the client to choose what action to take in that case, with simple APIs for specifying default values or throwing exceptions.

An Optional-returning method is more flexible and easier to use than one that throws an exception, and it is less error-prone than one that returns null.

However, this all comes at the expense of object allocation and initialization for the Optional itself. Overall though, this could be a promising solution.

Unchecked Exception

The unchecked exception route is basically what Double.parseDouble takes – if parsing fails, it throws an unchecked NumberFormatException. We could easily mimic that behavior in our enhanced parser by doing the same thing when we detect an error case.

This approach is good in that no failure can go unhandled. However, the best "handling" a client can do would be to wrap the call in a try-catch block. Since we’re dealing with runtime exceptions, the compiler will not force clients to do so. And as we know, anytime the client is not forced to do something, there’s a potential for bugs.

Ultimately, the choice between an unchecked and checked exceptions comes down to the use case. Recalling our trusty API design maxims:

Throw unchecked exceptions unless clients can realistically recover from the failure.

Can a client realistically recover from the case where a String cannot be parsed to a double? This all depends on the situation – is the failure expected (e.g., when parsing a value entered by a user in the UI) or unexpected (e.g., parsing values from a trusted source)? Double parsing is such a common operation that there’s really not a one size fits all solution. Throwing an unchecked exception is a great choice for some applications, but not for all.

Checked Exception

For the cases where clients can realistically recover from the failure, throwing a checked exception does make sense. Callers must handle the exception in a catch clause or propagate it outward, which enhances reliability. A checked exception also includes an error message and other information detailing the cause of the failure which could be used by clients.

Despite this, any Java developer knows how burdensome a checked exception is. They make APIs painful to use and client code more difficult to read. Methods throwing checked exceptions also cannot be used directly in streams.

For these reasons, Joshua Bloch actually advises against using them as a first choice:

If recovery may be possible and you want to force callers to handle exceptional conditions, first consider returning an optional. Only if this would provide insufficient information in the case of failure should you throw a checked exception.

I don’t think our case quite meets the "insufficient information" condition described here, so a checked exception is not the best option.

Final Solution

Each of these choices has its strengths and weaknesses. Because of the varied use cases for a String parsing method, I think it actually makes sense to define two separate methods.

For the case where you are dealing with data from a trusted source and do not expect any errors, throwing an unchecked exception is the best choice. Callers are able to skip over the extra handling of an Optional, but they still get the benefits of the enhanced checks compared to Double.parseDouble.

I recommend adding "unchecked" somewhere in the name of the method so clients are consciously aware of what they are doing when they choose to use it. If they mess up and use it on invalid data, the thread will crash and they’ll have an easier time debugging the issue.

For the common case where invalid strings are expected, I think the choice comes down to an Optional or a checked exception. Both have overhead concerns, and you can make an argument for the checked exception because of the additional failure detail contained in the exception.

For me though, I find the readability and convenience of the Optional API to be the best choice for the second parser method.

Here’s how both methods would look together in a utilities class:

public final class ParseUtils
{
    private ParseUtils()
    {
    }

    public static double parseUnchecked(String input)
    {
        Double tmp = Doubles.tryParse(input);
        if (tmp == null                                                                     // standard invalid strings
                || input.contains(Double.toString(Double.POSITIVE_INFINITY))                // infinity
                || input.toLowerCase().endsWith("d") || input.toLowerCase().endsWith("f")   // format specifiers
                || input.contains(Double.toString(Double.NaN))                              // NaN
                || input.toLowerCase().contains("x")                                        // hex strings
        )
        {
            throw new NumberFormatException("Error parsing decimal value from input: " + input);
        }
        if (tmp == Double.POSITIVE_INFINITY) throw new NumberFormatException("Value is too large: " + input);
        if (tmp == Double.NEGATIVE_INFINITY) throw new NumberFormatException("Value is too small: " + input);
        return tmp;
    }

    public static OptionalDouble parseDouble(String input)
    {
        Double tmp = Doubles.tryParse(input);
        if (tmp == null                                                                     // standard invalid strings
                || input.contains(Double.toString(Double.POSITIVE_INFINITY))                // infinity
                || tmp == Double.POSITIVE_INFINITY || tmp == Double.NEGATIVE_INFINITY       // values exceeding min/max
                || input.toLowerCase().endsWith("d") || input.toLowerCase().endsWith("f")   // format specifiers
                || input.contains(Double.toString(Double.NaN))                              // NaN
                || input.toLowerCase().contains("x")                                        // hex strings
        )
        {
            return OptionalDouble.empty();
        }
        return OptionalDouble.of(tmp);
    }
}

Notice how I moved the min max checking from the original implementation to include slightly more descriptive error messages for those cases in the parseUnchecked method.

Conclusion

I hope you learned as much reading this post as I did writing it! This is a classic API design problem where you really see how the best solution is not always obvious. That’s why this stuff is an art!

I’d love to hear if you have any thoughts on the design or know of any other edge cases I neglected to consider. Please leave a comment if you do!

Leave a Reply