3

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?

VeeAyeInIn
  • 193
  • 1
  • 12

3 Answers3

3

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.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
2

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.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • Looking at the interface of `Number` we could provide an addition `BiFunction` for every primitive type. But since `Number` isn't final there might be other subclasses which need dedicated addition `BiFunction`s. Thus this approach will work for primitive types and known subtypes of `Number`. So if we know what we are dealing with this approach will work. (+1) – LuCio Aug 25 '18 at 21:15
  • The only issue here is that the `BiFunction` returns this error in the IDE: `Wrong number of type arguments: 1; required: 3`, so `BiFunction` sadly doesn't work. – VeeAyeInIn Aug 25 '18 at 21:34
  • You're right. It should be `BeFunction` since the `adder` has to add two instance of the type `T` which is the generic type of the `Vector`. The third `T`is the result type of the `adder`. It has to be `T` too in order to store the result in the `Vector`. – LuCio Aug 25 '18 at 22:31
  • @VeeAyeInIn misremembered the API. I meant `BinaryOperator` (which is a subinterface of `BinaryFunction`). – Andy Turner Aug 26 '18 at 04:42
  • @AndyTurner Well even with the `BinaryFunction`, that helped me a long ways, as I got this: https://hastebin.com/azafobefum.java , which I am rather happy about. – VeeAyeInIn Aug 26 '18 at 18:52
1

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.

davidxxx
  • 125,838
  • 23
  • 214
  • 215