javagenericsclasscastexceptionunchecked-cast

Removing unchecked cast warnings with generics


I just got into generics with Java, so I set up a little project for myself. I wanted to make a Vector / Point where you could specify the Number (e.g. Double, Integer, Long, etc).

I ended up getting a decent class object for it, however noticed some issues regarding the methods.

import java.math.BigDecimal;

@SuppressWarnings("WeakerAccess") // Suppresses weaker access warnings
public class Vector<T extends Number> {

    private T x;
    private T y;

    public Vector() {}

    public Vector(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }

    public void dislocate(T offsetX, T offsetY) {
        this.setX(addNumbers(getX(), offsetX));
        this.setY(addNumbers(getY(), offsetY));
    }

    public void dislocate(Vector vector) {
        this.setX(addNumbers(getX(), vector.getX()));
        this.setY(addNumbers(getY(), vector.getY()));
    }

    @SuppressWarnings("unchecked") // Suppresses cast unchecked warnings
    private T addNumbers(Number... numbers) {
        BigDecimal bd = new BigDecimal(0);

        for(Number number : numbers) {
            bd = bd.add(new BigDecimal(number.toString()));
        }

        return (T) bd;
    }
}

The final method, which is the adding numbers method, throws an unchecked cast warning. After I did some research, I figured out it was behaving oddly due to generics, which I'm relatively new in and unable to properly troubleshoot.

What about return (T) bd; creates the warning? T has to be an instance of a Number, so it should be cast-able to a BigDecimal, right?

So I created my little testing method,

Vector<Double> vec = new Vector<>(1.0, 3.0);
Vector<Double> vec2 = new Vector<>(2.2, 3.9);
vec.dislocate(1.0, 2.7);
System.out.println(vec.getX() + " " + vec.getY());
vec.dislocate(vec2);
System.out.println(vec.getX() + " " + vec.getY());

It works great, printing out 2.0 5.7 and 4.2 9.6.

The issue then, is when I use a method from Double, like Double#isNaN(). It then throws out the ClassCastException, Exception in thread "main" java.lang.ClassCastException: java.base/java.math.BigDecimal cannot be cast to java.base/java.lang.Double.

This seemed pretty common with other issues people have had with this, however, despite going over the resources, I don't understand why the error is thrown using the Double methods. The object should be a Double after the cast, right?


Solution

  • To solve this, you need to provide some means of adding Ts.

    For example, a BinaryOperator<T> is something that takes in two Ts, and returns a T. So, you can define ones for adding, for example:

    BinaryOperator<Double> addDoubles = (a, b) -> a+b;
    BinaryOperator<BigDecimal> addBigDecimals = (a, b) -> a.add(b);
    

    Now, you actually need to supply an instance of this to your Vector when you create it, e.g. as a constructor parameter:

    public Vector(BinaryOperator<T> adder) {
      this.adder = adder; // define a field, too.
    }
    

    And now use the BiFunction to add the numbers:

    private T addNumbers(T a, T b) {
      return adder.apply(a, b); // or you could just invoke this directly.
    }
    

    I simplified your addNumbers always to take two parameters, since you only invoke with two parameters. To do it generically, you'd either need to provide a "generic zero", i.e. a value of type T which is zero for that type, or simply to start from the first element in the varargs array.