VeeAyeInIn
VeeAyeInIn

Reputation: 193

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?

Upvotes: 3

Views: 405

Answers (3)

Andy Turner
Andy Turner

Reputation: 140309

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.

Upvotes: 2

davidxxx
davidxxx

Reputation: 131326

The object should be a Double after the cast, right?

Never because casting (with or without generics) never changes the runtime type. It just changes the declared type that you manipulate.

In addNumbers() you actually perform an uncheck casts : BigDecimal to T.
The compiler warns you of the uncheck cast but accepts it as BigDecimal is compatible with T that has as upper-bounded wildcard : Number.
The contained elements of the generic class instance :

private T x;
private T y;

refer now the BigDecimal type and no more Double type.

Upvotes: 1

Louis Wasserman
Louis Wasserman

Reputation: 198023

You basically can't do things like this in Java. (T) someBigDecimal will work if and only if T is a BigDecimal itself. The way erasure works may hide that from you temporarily, but Number has no special magic about being able to add two Numbers or cast one to another.

In general, there's not really any way to generify in Java over different kinds of numbers and then be able to do numerical things with them.

Upvotes: 3

Related Questions