Incomparable Puzzles in Java

Here are a few puzzles for you to solve in Java. The source is available here: UncomparablePuzzles.java.

Puzzle 1: Comparing long and double

Try running the following code to reproduce the output below. See if you can work out why these results occur:

long a = (1L << 54) + 1;
double b = a;
System.out.println("b == a is " + (b == a));
System.out.println("(long) b < a is " + ((long) b < a));

When executed, it produces:

b == a is true
(long) b < a is true

Analysis

This puzzle highlights the precision limitations when converting between long and double.

  1. Precision Loss During Conversion The long value a is (1L << 54) + 1, which is 18014398509481985. When cast to double, b becomes 18014398509481984.0. Due to double 's 53-bit mantissa, it cannot accurately represent every long value beyond this range, resulting in precision loss.

  2. Equality Comparison (b == a) Despite b losing precision, b == a evaluates to true because a exceeds the precision range of double. Both a and b effectively represent the same double value.

  3. Casting Back to long and Comparison Casting b back to long truncates the decimal part, yielding 18014398509481984, which is less than the original a. Hence, (long) b < a returns true.

Puzzle 2: Large double to long Conversion

Examine the following code:

double c = 1e19;
long d = 0;
d += c;
System.out.println("\nd < c is " + (d < c));
System.out.println("d < (long) c is " + (d < (long) c));

Produces:

d < c is true
d < (long) c is false

Analysis

This puzzle demonstrates the intricacies of type conversion and arithmetic operations in Java.

  1. Initial Assignment c is assigned 1e19, a value that exceeds the maximum value a long can hold (Long.MAX_VALUE is approximately 9.22e18). Adding c to d results in d becoming Infinity due to overflow.

  2. Comparisons

    • d < c: Since d is now Infinity, and c is 1e19, the comparison Infinity < 1e19 is false.

    • d < (long) c: Casting c to long results in overflow, yielding Long.MIN_VALUE. Thus, Infinity < Long.MIN_VALUE is false.

Puzzle 3: Double Object Comparisons

Consider the following code:

Double e = 0.0;
Double f = 0.0;
System.out.println("\ne <= f is " + (e <= f));
System.out.println("e >= f is " + (e >= f));
System.out.println("e == f is " + (e == f));

Produces:

e <= f is true
e >= f is true
e == f is false

Analysis

This puzzle explores the behaviour of object comparisons in Java.

  1. Comparison Operators (<= and >=) These operators compare the primitive double values of e and f, both 0.0, resulting in true for both comparisons.

  2. Equality Operator (==) The == operator checks for reference equality. Since e and f are distinct Double objects, e == f evaluates to false despite representing the same numeric value.

Puzzle 4: BigDecimal Equality and Comparison

Examine the following code:

BigDecimal x = new BigDecimal("0.0");
BigDecimal y = BigDecimal.ZERO;
System.out.println("\nx == y is " + (x == y));
System.out.println("x.doubleValue() == y.doubleValue() is " + (x.doubleValue() == y.doubleValue()));
System.out.println("x.equals(y) is " + x.equals(y));
System.out.println("x.compareTo(y) == 0 is " + (x.compareTo(y) == 0));

Produces:

x == y is false
x.doubleValue() == y.doubleValue() is true
x.equals(y) is false
x.compareTo(y) == 0 is true

Analysis

This puzzle delves into the comparison mechanics of the BigDecimal class.

  1. Reference Equality (x == y) x and y are different objects; hence, x == y is false.

  2. Primitive Equality (doubleValue()) Both x and y convert to the same primitive double value 0.0, making x.doubleValue() == y.doubleValue() true.

  3. equals Method BigDecimal.equals() considers both value and scale. x has a scale of 1, while y has a scale of 0, leading to x.equals(y) being false.

  4. compareTo Method compareTo only considers the numeric value, ignoring scale differences. Therefore, x.compareTo(y) == 0 is true.

Bonus Puzzle: The Shrinking Collections

This bonus puzzle involves a collection of BigDecimal values. Take a look at the following code:

List<BigDecimal> bds = Arrays.asList(
    new BigDecimal("1"),
    new BigDecimal("1.0"),
    new BigDecimal("1.00"),
    BigDecimal.ONE
);
System.out.println("bds.size()=    " + bds.size());

Set<BigDecimal> bdSet = new HashSet<>(bds);
System.out.println("bdSet.size()=  " + bdSet.size());

Set<BigDecimal> bdSet2 = new TreeSet<>(bds);
System.out.println("bdSet2.size()= " + bdSet2.size());

Output

bds.size()=    4
bdSet.size()=  3
bdSet2.size()= 1

Explanation

At first glance, it might seem surprising that the sizes of the collections are shrinking. Here’s why this happens:

1. bds.size()= 4

The bds list contains four distinct BigDecimal objects, so bds.size() returns 4. However, we need to understand why the set sizes shrink.

2. bdSet.size()= 3

A HashSet in Java removes duplicates based on the equals() method. In this case, BigDecimal uses the equals() method to check for equality. The values new BigDecimal("1"), new BigDecimal("1.0"), and new BigDecimal("1.00") are considered equal because they all represent the same numeric value: 1.0. So, the HashSet eliminates two of the four entries, leaving only three elements in the set.

3. bdSet2.size()= 1

A TreeSet uses the compareTo() method to determine uniqueness and respects objects' natural ordering. In this case, BigDecimal compares the values numerically, so all the BigDecimal objects in the list are considered equal because they all represent the value 1.0. Therefore, only one element is retained in the TreeSet, and the size is 1.

Key Takeaways

  • Converting between long and double can lead to precision loss, affecting equality and comparison operations.

  • Operators like == behave differently for object references versus primitive values.

  • The equals() method considers scale, while compareTo() does not, impacting how BigDecimal instances are treated in collections.

  • Different Set implementations (HashSet vs. TreeSet) handle object uniqueness based on their respective equality mechanisms.

By understanding the behaviour of the HashSet and TreeSet with respect to the BigDecimal class, you can better predict the results of such operations in your code.

Conclusion

In Java, the choice of collection can lead to surprising results when working with classes with custom equality and comparison behaviour. In this puzzle, the BigDecimal class reveals its nuances when used with HashSet and TreeSet, leading to shrinking collection sizes.

About the Author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology that empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Follow Peter on BlueSky or Mastodon.

Comments

Popular posts from this blog

Java is Very Fast, If You Don’t Create Many Objects

System wide unique nanosecond timestamps

What does Chronicle Software do?