3

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 {}
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
SunFlow
  • 43
  • 5
  • 4
    Are you sure generics is the right solution here, that `do()` method looks likes something you should not need in a generic class. – Joakim Danielson Oct 25 '20 at 15:28
  • 1
    What do those "supported classes" have in common? Why would you want to have a "generic" class that only supports a limited set of classes that have nothing in common? – tucuxi Oct 25 '20 at 15:34
  • You could try making adapters for each of these classes (`class BlaAdapter`, `class BlaEnum extends BlaAdapter`) but I would follow tucuxi's advice first – user Oct 25 '20 at 16:02
  • I would like to use Bla as a Message class and want to use `T foo` as an identifiier with a getter `T getID()`, that gives me my specified type. The reason i would like to use generics is so i am not bound to use just a String or int for all messages. And dont have cast it to (String, int, ...) after `Object getID()`. – SunFlow Oct 25 '20 at 16:25
  • @SunFlow please write it as an answer, I am interested to see your opinion here – Iman Nia Oct 25 '20 at 16:31
  • Please be aware that there is no such thing as primitive classes in Java. Enum, Boolean, Integer, String etc are normal classes. The only primitives Java has are boolean, int, short, byte, long, float, double and char, which are not classes. – Mark Rotteveel Oct 25 '20 at 17:31
  • 2
    Related: [Is there a Union in Java Generics?](https://stackoverflow.com/questions/1697562/is-there-a-union-in-java-generics) – Mark Rotteveel Oct 25 '20 at 17:33

3 Answers3

2

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);
    }

    // ...
}
Andreas
  • 154,647
  • 11
  • 152
  • 247
1

The common supertype of the classes you want to support — and considering your Supportable type — is Object however, if you define your class to inherit from Object (which is by default) and implements Supportable there is no way you can restrict your generic class this way.

if I were you, I would have used a Factory pattern and write a method that accepts any thing (subclass of Object) and its type to instantiate a relevant instance of Bla . The Create method should be generic but you can not impose any restriction here only you may be able to throw an exception if the type is not acceptable. I know, it is not the type of answer that you are probably expecting. But there is no other way (in your case).

P.S For those who think OP is doing something wrong, and we should not have such a design in real world. I would like to introduce an example. Assume you are about to write a class which suppose to create an in-memory table (like what we do in databases but with java data types). You also want to support user’s data types! So how would you do that?

Iman Nia
  • 2,255
  • 2
  • 15
  • 35
  • 1
    I'm currently looking into implementing the factory pattern here. So far it works for Integers and i it looks okay, but I'm not sure if I want to overload the create method for each supported type or have one method were i test which type the provided parameter is, once I've finished it for all of my types, I'll post an answer on how I did it. – SunFlow Oct 25 '20 at 17:49
1

So to resume my progress.

After the tip of @Iman to use the factory pattern, i started to implement it using static overloaded factory methods. Here is my resulting code using Enum, Boolean and the Supportable interface as examples. It's similar to the code @Andreas posted.

public static <E extends Enum<E>> Bla<E> createBla(E id) {
    return new Bla.EnumBla<E>((Class<E>) id.getClass()).setID(id);
}
public static Bla<Boolean> createBla(boolean id) {
    return new Bla.BooleanBla().setID(id);
}
public static <S extends Supportable> Bla<S> createBla(S id) {
    return new Bla.SupportableBla<S>((Class<S>) id.getClass()).setID(id);
}

I decided to make static subclasses of Bla for the types i want to support.

public abstract class Bla<T>   {
    private T foo;
    private Bla() {}
    public T getFoo() { return foo; }
    public Bla<T> setFoo(T foo) { this.foo = foo; return this; }
    public abstract void do();

    public static class EnumBla<E extends Enum<E>> extends Bla<E> {
        private final Class<E> fooClazz;
        public EnumBla(Class<E> fooClazz) { super(); this.fooClazz = fooClazz; }
        @Override
        protected void do() { // Do Enum specific code}
    }
    public static class BooleanBla extends Bla<Boolean> {
        public BooleanBla() { super(); }
        @Override
        protected void do() { // Do Boolean specific code }
    }
    public static class SupportableBla<S extends Supportable> extends Bla<S> {
        private final Class<S> fooClazz;
        public SupportableBla(Class<S> fooClazz) { super(); this.fooClazz = fooClazz; }
        @Override
        protected void do() { if(super.id != null) super.id.doSpecific(this); }
    }
}

I don't have a have fooClazz in BooleanBla since it's not needed there. Also i can't completely remove "nested" if statements, because i want to give the ability to create Bla without an instance of the wanted foo type.

    public static <C, E extends Enum<E>, S extends Supportable> Bla<C> createBla(Class<C> fooClazz) throws UnsupportedDataTypeException {
        if (Enum.class.isAssignableFrom(fooClazz))
            return (Bla<C>) new Bla.EnumBla<E>((Class<E>) fooClazz);
        if (fooClazz == Boolean.class || fooClazz == boolean.class)
            return (Bla<C>) new Bla.BooleanBla();
        if (Supportable.class.isAssignableFrom(idClazz))
            return (Bla<C>) new Bla.SupportableBla<S>((Class<S>) fooClazz);
        throw new UnsupportedDataTypeException(
                "[" + fooClazz+ "] is not a supported Bla foo\n\t\t" +
                    "supported types are " +
                    "[" + Enum.class + "] " +
                    "[" + Boolean.class + "] " +
                    "[" + Supportable.class + "]");
    }
SunFlow
  • 43
  • 5