Parameterize a class with one of a fixed set of ty

2019-05-18 04:10发布

问题:

Say I have a generic class Foo which can hold an object of type T. Furthermore, let's say I only want to be able to instantiate the class with objects that are one of two types. Finally, let's say that the lowest common upper bound of these two types is a type that has many more subclasses than those two types that I want to allow, so I can't simply specify an upper bound for the type parameter (as in class Foo<T extends Something>), because then I would allow to instantiate the class with other types than the two I expect.

For illustration, let's say I want Foo to hold only either a String or an Integer. The lowest common upper bound is Object, so specifying an upper bound won't do the trick.

Certainly, I could do something along the lines of

class Foo<T> {

    private T obj;

    public Foo(T obj) throws IllegalArgumentException {
        if (!(obj instanceof String || obj instanceof Integer)) {
            throw new IllegalArgumentException("...");
        }
        this.obj = obj;
    }
}

However, in this case, I can still call the constructor with any object; if I try to instantiate it with something that is neither a String nor an Integer, I will get an exception at runtime.

I would like to do better. I would like the compiler to infer statically (i.e., at compile time) that I can only instantiate this class with objects that are either String or Integer.

I was thinking something along those lines might do the trick:

class Foo<T> {

    private T obj;

    public Foo(String s) {
        this((T) s);
    }

    public Foo(Integer i) {
        this((T) i);
    }

    private Foo(T obj) {
        this.obj = obj;
    }
}

This works, but it looks really, really odd. The compiler warns (understandably) about unchecked casts. Of course I could suppress those warnings, but I feel this is not the way to go. In addition, it looks like the compiler can't actually infer the type T. I was surprised to find that, with the latter definition of class Foo, I could do this, for instance:

Foo<Character> foo = new Foo<>("hello");

Of course, the type parameter should be String here, not Character. But the compiler lets me get away with the above assignment.

  1. Is there a way to achieve what I want, and if yes, how?
  2. Side question: why does the compiler let me get away with the assignment to an object of type Foo<Character> above without even so much as a warning (when using the latter definition of class Foo)? :)

回答1:

Try using static factory method to prevent compiler warning.

class Foo<T> {

    private T obj;

    public static Foo<String> of(String s) {
        return new Foo<>(s);
    }

    public static Foo<Integer> of(Integer i) {
        return new Foo<>(i);
    }

    private Foo(T obj) {
        this.obj = obj;
    }
}

Now you create instance using:

Foo<String> foos = Foo.of("hello");

Foo<Integer> fooi = Foo.of(42);

Foo<Character> fooc = Foo.of('a'); // Compile error

However the following are still valid since you can declare a Foo of any type T, but not instantiate it:

Foo<Character> fooc2;

Foo<Character> fooc3 = null;

Foo<Object> fooob1;

Foo<Object> fooob2 = null;


回答2:

  1. one word: interface. You want your Z to wrap either A or B. Create an interface implementing the smallest common denominator of A and B. Make your A and B implement that interface. There's no other sound way to do that, AFAIK. What you already did with your constructors etc. is the only other possibility, but it comes with the caveats you already noticed (having to use either unchecked casts, or static factory wrappers or other code smells).

note: If you can't directly modify A and/or B, create wrapper classes WA and WBfor them beforehand.

example:

interface Wrapper {
    /* either a marker interface, or a real one - define common methods here */
}

class WInt implements Wrapper {
    private int value;
    public WInt( int value ) { this.value = value; }
}

class WString implements Wrapper {
    private String value;
    public WString( String value ) { this.value = value; }
}

class Foo<T> {   
    private Wrapper w;   
    public Foo(Wrapper w) { this.w = w; }
}
  1. because you call your private Foo(T obj) due to diamond type inference. As such, it's equal to calling Foo<Character> foo = new Foo<Character>("hello");


回答3:

  1. Long story short: You are trying to create a union of two classes in java generics which is not possible but there are some workarounds. See this post

  2. Well the compiler uses the Character class in T parameter. Then the String constructor is used where String is casted to T (Character in this case). Trying to use the private field obj as a Character will most likely result in an error as the saved value is an instance of the final class String.



回答4:

Generics is not suitable here.

Generics are used when any class can be used as the type. If you only allow Integer and String, you should not use generics. Create two classes FooInteger and FooString instead.

The implementations should be pretty different anyway. Since Integers and Strings are very different things and you would probably handle them differently. "But I am handling them the same way!" you said. Well then what's wrong with Foo<Double> or Foo<Bar>. If you can handle Integer and String with the same implementation, you probably can handle Bar and Double and anything else the same way as well.

Regarding your second question, the compiler will see that you want to create a Foo<Character>, so it tries to find a suitable overload. And it finds the Foo(T) overload to call, so the statement is perfectly fine as far as the compiler is concerned.