24

Assuming I have a class with two instance variables of generic type, how can I force those types to be the same?

I haven't found any similar questions elsewhere (let alone answers), which may be because I'm using terms wrong. There's this question, but I'm not sure how to apply it to my situation.

In short, how can I get this last line of code to not compile?

class Foo<T> {
    private T value;
}

class Bar {
    private Foo var1;
    private Foo var2;

    public Bar(Foo var1, Foo var2) {
        this.var1 = var1;
        this.var2 = var2;
    }

    public static void main(String[] args) {
        Foo<String> var1 = new Foo<>();
        Foo<Integer> var2 = new Foo<>();
        Bar b = new Bar(var1, var2); // this should not work
    }
}
vvye
  • 1,208
  • 1
  • 10
  • 25

2 Answers2

34

Make Bar also generic:

class Bar<T> {
    private Foo<T> var1;
    private Foo<T> var2;

    public Bar(Foo<T> var1, Foo<T> var2) {
        this.var1 = var1;
        this.var2 = var2;
    }

    public static void main(String[] args) {
        Foo<String> var1 = new Foo<>();
        Foo<Integer> var2 = new Foo<>();
        Bar<String> b = new Bar<>(var1, var2); // this does not work
    }
}

By using a class level generic parameter T for Bar you can enforce the same types for both instance variables.

This eliminates also the warning of using raw types (which should never be used) as mentioned by @daniu in the comments.


Now if you happen to not use raw types but want to allow not the same types, you can work with wildcards:

With upper bounded, which only allow typed reading (var1 and var2 will always produce an implementation of type T):

class Bar<T> {
    private Foo<? extends T> var1;
    private Foo<? extends T> var2;

    public Bar(Foo<? extends T> var1, Foo<? extends T> var2) {
        this.var1 = var1;
        this.var2 = var2;
    }

    public static void main(String[] args) {
        Foo<String> var1 = new Foo<>();
        Foo<Integer> var2 = new Foo<>();
        Bar<Object> b = new Bar<>(var1, var2); // this does now work
    }
}

And with lower bounded, which only allow typed writing (var1 and var2 will always consume any implementation of type T):

class Bar<T> {
    private Foo<? super T> var1;
    private Foo<? super T> var2;

    public Bar(Foo<? super T> var1, Foo<? super T> var2) {
        this.var1 = var1;
        this.var2 = var2;
    }

    public static void main(String[] args) {
        Foo<Integer> var1 = new Foo<>();
        Foo<Number> var2 = new Foo<>();
        Bar<Integer> b = new Bar<>(var1, var2); // this does now work
    }
}

For more on that topic you can read: What is PECS (Producer Extends Consumer Super)?

Lino
  • 19,604
  • 6
  • 47
  • 65
  • 2
    The `var1` and `var2` declared with raw `Foo` type give a compiler warning in the OP's code anyway, this is the only way around it. – daniu Jun 15 '18 at 12:09
3

Lino's answer is good. I want to add one fact:

If you don't have to keep track of the type of the vars, but only verify that they are of the same type, then you can move the type parameter from the Bar class to its constructor:

class Bar {
    // These will always have the same type, but
    // that's not visible here
    private Foo<?> var1;
    private Foo<?> var2;

    // The parameter types of the construct ensures that
    // the vars are always of the same type
    public <T> Bar(Foo<T> var1, Foo<T> var2) {
        this.var1 = var1;
        this.var2 = var2;
    }

    public static void main(String[] args) {
        Foo<String> var1 = new Foo<>();
        Foo<Integer> var2 = new Foo<>();

        Bar b1 = new Bar(var1, var1); // Works fine
        Bar b2 = new Bar(var1, var2); // Compilation error
    }
}

This technique works for all kinds of methods.

Lii
  • 11,553
  • 8
  • 64
  • 88
  • 1
    Good point! Although you can't really do anything with `var1` and `var2` because from the `value`-member of `Foo` you'll just get `Object` when you read it, and you can't write to it (Not a problem if you just invoke methods from `Foo`) – Lino Jun 18 '18 at 11:47