Home > database >  Return type based on final field of input
Return type based on final field of input

Time:08-08

I have an enum whose values have an associated Class<?> field, like so:

public enum Example {
    A(int.class),
    B(String.class),
    C(int.class);
    
    public final Class<?> type;
    private Example(Class<?> type) {
        this.type = type;
    }
}

I want to pass an Example to a function in another class and return based on the type field in Example, like so:

public <T> T getValue(Example ex) {
    return foo(ex); // Will return a value of type ex.type
}

How can I do this properly? The best I've found so far is this:

public <T> T getValue(Class<T> c, Example ex) {
    return (T)foo(ex); // Will cause problems if T is not ex.type, although that can be checked and an exception thrown
}

But this isn't great as it doesn't enforce ex.type.equals(c) at compiler-time, and it seems like I shouldn't have to explicitly pass the type when I'm implicitly passing it through the other parameter.

CodePudding user response:

The core issue at hand here is that, unfortunately, enums do not support generics. You cannot write enum Foo<E> {}.

However, you need that generics to exist to do it 'properly'. You can make enums implement interfaces, and interfaces can have generics, but you can't bind different types to that interface on a per-enum-value basis, so that's still not a solution. You can implement, say, SomeInterface<Integer>, but that would mean all the enum values all implement that. So you can't have Foo.A implement SomeInterface<Integer> and Foo.B implement SomeInterface<String>.

As far as I can tell, you have only two options.

Hack it

And you already discovered the best of a bad bunch: Have callers pass in the class. There is no compile time check if they pass in the wrong class. At best, you can improve on the implementation of the body of the getValue class you showed; use return (T) type.cast(ex) which will at least end up throwing an exception at runtime if someone tries to pull that stunt - now if the c value is not identical to what the enum intended you to do, you get an exception instead of silent heap corruption.

No enums

Enums are, in effect, almost entirely syntax sugar. The amount of places in popular java APIs that demand an enum are scarce (you can switch on them, that's nice - and a few APIs such as DB SQL abstractions might do something special, and there's EnumSet, but that is about it). Enums are just objects that all implement/extend a common type, along with a bunch of infrastructure that ensures that only one can exist for any given enum value even in the face of reflection, multi-threaded shenanigans, and serialization. All of these things can be easily reproduced. You're just missing out on the notion that you can switch on them or make EnumSets out of them.

Once you write 'your own' enum, you can make each value have its own generics value, which 'fixes' the problem.

Write-your-own enum syntax sugar

The 'enum syntax sugar' essentially follows to the letter what's in Effective Java prior to enums existing. If you have that book, you can look it up. It's not particularly complicated - it gets a little hairy if you care about serialization, but virtually nobody does, so if you don't, it's really just 'private constructor' and that's all you need:

class Suit {
  private final String name;
  private final Color color;

  public static final Suit DIAMONDS = new Suit("DIAMONDS", Color.RED);
  public static final Suit HEARTS = new Suit("HEART", Color.RED);
  public static final Suit CLUBS = new Suit("CLUBS", Color.BLACK);
  public static final Spades SPADES = new Spades();

  Suit(String name, Color color) { // package private
    this.name = name;
    this.color = color;
  }

  public final String name() {
    return name;
  }

  public Color getColor() {
    return color;
  }

  public boolean isTrumps() {
    return false;
  }
}

public final class Spades extends Suit {
  Spades() {
    super("SPADES", Color.BLACK);
  }

  @Override public boolean isTrumps() {
    return true;
  }
}

is essentially identical to:

enum Suit {
  DIAMONDS(Color.RED),
  HEARTS(Color.RED),
  CLUBS(Color.BLACK),
  SPADES(Color.BLACK) {
    @Override public boolean isTrumps() { return true; }
  };

  private final Color color;
  Suit(Color color) {
    this.color = color;
  }

  public Color getColor() {
    return color;
  }

  public boolean isTrumps() {
    return false;
  }
}

in almost all ways that matter. Once you've 'refactored' your enum into this construction, you are free to define e.g. A as public static final Foo<Integer> A = ...; and B as public static final Foo<String> B = ... and go from there.

A few language features might be relevant here:

  • In very very recent versions of java (16 , I think), you can make so-called 'sealed' interfaces and classes - these allow extension only by types explicitly enumerated on the base type. So, you can make your Suit type sealed, listing out that only the Diamonds, Hearts, Clubs, and Spades types are its subtypes and no others exist (no other type can be implements or extends Suit). In addition, the far more advanced pattern matching forms of switch introduced in the most recent few versions of java know what this means and let you switch on them after all, regaining you the loss of switching-on-enums.
  • Instead of (T) foo, if you happen to have a Class<T> from elsewhere (for example, because one of your fields is a Class<T>), you can write type.cast(foo) - this avoids the generics warning and will in fact throw a ClassCastException on the spot if the types do no match, whereas (T) foo gets you a compiler warning and will do absolutely nothing if the types don't match up. Instead, some code somewhere later is going to throw that ClassCastException. Errors should be as localized as possible (thrown at the point of problem, not thrown 5 days later in unrelated code - that leads to hours of debugging instead of 10 seconds of debugging).

Of course, given that generics are erased, you need that Class<T> to be made elsewhere. You can't create a Class<T> instance from just the fact that your type/method has a T typevar. That's the 'point' of generics erasure - that you can't do that.

  •  Tags:  
  • java
  • Related