javaclassgenericsbounded-wildcard

Can you restrict a type parameter <T> to multiple specific classes


I am writing a generic class Bla with type parameter T.

Can I restrict T, so that only classes I want to support can be used?

public class Bla<T> {
    private T foo;
    private Class<T> fooClazz;
}

I want Bla to support most primitive classes (Enum, Boolean, Integer, String, ...), and also my own interface Supportable.

public interface Supportable {
    void doSpecific(Bla _bla);
}

Bla has a method do(), which handles the supported classes, or throws an exception if a class that I don't support is used.

public void do() { 
    if (Enum.class.isAssignableFrom(fooClazz)) {
        // Do Enum specific code
    } else if (Boolean.class.isAssignableFrom(fooClazz)) {
        // Do Boolean specific code
    } else if (Integer.class.isAssignableFrom(fooClazz)) {
        // Do Integer specific code
    } else if (String.class.isAssignableFrom(fooClazz)) {
        // Do String specific code
    } else if (Supportable.class.isAssignableFrom(fooClazz)) {
        ((Supportable) foo).doSpecific();
    } else {
        throw new UnsupportedDataTypeException(fooClazz + "is not supported");
    }
}

I know I can do this.

public class Bla<T extends Number> {}

So only classes that extend Number can be used, but is there something like this?

public class Bla<T extends Number | String> {}

So that also a String is possible?

The only solution I can think of, is to make multiple Bla classes for the different types.

public class BlaEnum {}
public class BlaBoolean {}
public class BlaInteger {}
public class BlaString {}
public class BlaSupportable {}

Solution

  • One way to restrict it, is to use static overloaded factory methods to construct the object.

    public class Bla<T> {
        private final T foo;
        private final Class<T> fooClazz;
    
        private Bla(T foo, Class<T> fooClazz) { // Must be private
            this.foo = foo;
            this.fooClazz = fooClazz;
        }
    
        @SuppressWarnings("unchecked")
        public static <E extends Enum<E>> Bla<E> of(E foo) { // Caveat: Cannot handle null
            return new Bla<>(foo, (Class<E>) foo.getClass());
        }
        public static Bla<Boolean> of(Boolean foo) {
            return new Bla<>(foo, Boolean.class);
        }
        public static Bla<Integer> of(Integer foo) {
            return new Bla<>(foo, Integer.class);
        }
        public static Bla<String> of(String foo) {
            return new Bla<>(foo, String.class);
        }
        public static Bla<Supportable> of(Supportable foo) {
            return new Bla<>(foo, Supportable.class);
        }
    
        public void do() {
            // ...
        }
    
        // ...
    }
    

    It changes how the caller constructs an instance, but actually simplifies it too, since the caller doesn't have to pass in a Class<T>, e.g.

    // With constructor (old way)
    Bla<MyEnum> e2 = new Bla<>(MyEnum.A, MyEnum.class);
    Bla<Boolean> b2 = new Bla<>(true, Boolean.class);
    Bla<Integer> i2 = new Bla<>(42, Integer.class);
    Bla<String> s2 = new Bla<>("", String.class);
    Bla<Supportable> su2 = new Bla<>(supportable, Supportable.class);
    
    // With static factory method (new way)
    Bla<MyEnum> e1 = Bla.of(MyEnum.A);
    Bla<Boolean> b1 = Bla.of(true);
    Bla<Integer> i1 = Bla.of(42);
    Bla<String> s1 = Bla.of("");
    Bla<Supportable> su1 = Bla.of(supportable);
    
    // Unsupported types are not allowed
    Bla<Double> i1 = Bla.of(3.14); // Error: The method of(E) in the type Bla is not applicable for the arguments (double)
    

    However, rather than using a multi-way if statement in the do() method, it should be using subclasses. The subclasses are hidden from the caller, so it makes no external difference, but it eliminates the need for multi-way if statement / switch statement:

    public abstract class Bla<T> {
        private final T foo;
        private final Class<T> fooClazz;
    
        private Bla(T foo, Class<T> fooClazz) { // Must be private
            this.foo = foo;
            this.fooClazz = fooClazz;
        }
    
        @SuppressWarnings("unchecked")
        public static <E extends Enum<E>> Bla<E> of(E foo) { // Caveat: Cannot handle null
            return new Bla<>(foo, (Class<E>) foo.getClass()) {
                @Override
                public void do() {
                    // Do Enum specific code
                }
            };
        }
        public static Bla<Boolean> of(Boolean foo) {
            return new Bla<>(foo, Boolean.class) {
                @Override
                public void do() {
                    // Do Boolean specific code
                }
            };
        }
        public static Bla<Integer> of(Integer foo) {
            return new Bla<>(foo, Integer.class) {
                @Override
                public void do() {
                    // Do Integer specific code
                }
            };
        }
        public static Bla<String> of(String foo) {
            return new Bla<>(foo, String.class) {
                @Override
                public void do() {
                    // Do String specific code
                }
            };
        }
        public static Bla<Supportable> of(Supportable foo) {
            return new Bla<>(foo, Supportable.class) {
                @Override
                public void do() {
                    foo.doSpecific(this);
                }
            };
        }
    
        public abstract void do(); // Is now abstract
    
        // ...
    }
    

    You can of course create (private) static nested classes, or (package-private) top-level classes, instead of the anonymous classes, if you prefer.

    Using subclasses allow fooClass-specific actions in multiple methods. If you only have one method, you can use lambdas expressions and/or method references instead:

    public class Bla<T> {
        private final T foo;
        private final Class<T> fooClazz;
        private final Consumer<Bla<T>> doImpl;
    
        private Bla(T foo, Class<T> fooClazz, Consumer<Bla<T>> doImpl) { // Must be private
            this.foo = foo;
            this.fooClazz = fooClazz;
            this.doImpl = doImpl;
        }
    
        @SuppressWarnings("unchecked")
        public static <E extends Enum<E>> Bla<E> of(E foo) { // Caveat: Cannot handle null
            return new Bla<>(foo, (Class<E>) foo.getClass(), bla -> {
                // Do Enum specific code
            });
        }
        public static Bla<Boolean> of(Boolean foo) {
            return new Bla<>(foo, Boolean.class, bla -> {
                // Do Boolean specific code
            });
        }
        public static Bla<Integer> of(Integer foo) {
            return new Bla<>(foo, Integer.class, bla -> {
                // Do Integer specific code
            });
        }
        public static Bla<String> of(String foo) {
            return new Bla<>(foo, String.class, bla -> {
                // Do String specific code
            });
        }
        public static Bla<Supportable> of(Supportable foo) {
            return new Bla<>(foo, Supportable.class, foo::doSpecific);
        }
    
        public void do() {
            doImpl.accept(this);
        }
    
        // ...
    }