简体   繁体   中英

Using Generics to retrieve a list of classes of type Generic

I'm relatively new to generics in Java, so I apologize if this is something common that gets taught in schools (I'm pretty much self-taught). Let's say I have the interface and abstract class below

public interface IChallenge<T> {
    boolean handle(T e);
    Class<? extends T> getType();
}
public abstract class AbstractChallenge<T> implements IChallenge<T> {
    protected Class<T> clazz;
    
    @Override
    public Class<? extends T> getType() {
        return this.clazz;
    }
}

For every class that extends AbstractChallenge, the handle method takes in the parameter that is specified for the generic. So if I had an event class that gets triggered when Event happens, I would have

public class EventChallenge extends AbstractChallenge<Event> {
    public EventChallenge() {
        super(Event.class);
    }

    @Override
    public boolean handle(Event e) {}
}

My problem comes when I'm trying to pass a specific class to the handle method. Since the generic can be any class, and there can be multiple challenges with the same type, I have the challenges stored in a map with their type as the key.

private Map<Something, List<AbstractChallenge<Something>> challenges = new HashMap<>();

With the ultimate hope of achieving something along the lines of

List<AbstractChallenge<A>> specificChallenges = this.challenges.get(A.class);
specificChallenges.removeIf(challenge -> challenge.handle(A));

But I'm having a hard time figuring out what goes in the 'Something'. If I put the wildcard ? symbol, then IntelliJ says that handle must take in a parameter of the requirement: capture of? when I pass it class A. The best I've gotten to is to not specify the type for AbstractChallenge but I'd like a better solution.

Any ideas? Thanks!

What you seek is something like that (I took the comment here):

private Map<Class<?>, List<IChallenge<?>> challenges = new HashMap<>();

A a = ...;
challenges.get(a.getClass())
          .removeIf(challenger -> challenger.handle(a));

This is unsafe as it can be and you can't do much because you don't know the actual type of T (the compiler does not, so the much it can do is infer it, and in this case, the type would be Object ):

  • The key can be any type, for example Integer.class
  • The value can be any type IChallenge<T> and if T is not Integer (or Number , Object , eg: any type in the hierarchy of T ), it may fail if the implementation use the object it handles and do some cast.

When you add:

challenges.get(Integer.class).add((Number a) -> a.intValue() > 10); // #1
challenges.get(Integer.class).add((Integer a) -> a.intValue() > 10); // #2
challenges.get(Integer.class).add((Object a) -> a != null); // #3
challenges.get(Integer.class).add((String a) -> a.length() > 10); // #4

Here is an example:

Integer a = Integer.valueOf(5);
// #1 -> ok: a is a Number
challenges.get(a.getClass()).removeIf(c -> c.handle(a));
// #2 -> ok: a is an Integer
challenges.get(a.getClass()).removeIf(c -> c.handle(a));
// #3 -> ok: a is an Object
challenges.get(a.getClass()).removeIf(c -> c.handle(a));
// #4 ->ko: a is not a String
challenges.get(a.getClass()).removeIf(c -> c.handle(a));

If you wish to avoid that, but still be able to handle anything challenge, you should ensure that the class holding/building the challenges do it correctly:

public <T> void addChallenge(Class<T> type, IChallenge<T> challenge) {
  challenges.computeIfAbsent(type, ignored -> new ArrayList<>()).add(challenge);
}

While you could use the getType() your defined in IChallenge , I wanted to show you how to enforce that the type (the key) and the IChallenge (the value) can be secured: normally, unless you gave write access to the map to other classes, this should be safe because the compiler validate the type at insertion.

Therefore, when you remove them, you should never have a ClassCastException due to the type parameter of IChallenge .

You could also try playing with ? super T ? super T and ? extends T ? extends T but that's another challenge.

--

Regarding your comment:

I'm not entirely sure how to go about using the addChallenge method you specified. Right now, I have a list of Class> for every challenge created, and when a specific challenge should be loaded, the program instantiates using.newInstance(). Should I be doing it differently? I only need a certain amount of challenges loaded at once, not all – DeprecatedFrank

I am not telling to load all challenges at once, I am merely telling your to use OOP to ensure that no one, but your challenge holder (let call it ChallengeHolder ) manage the map, and manage it so that you avoid generics pitfall:

class ChallengeHolder {
  private final Map<Class<?>, List<IChallenge<?>>> challenges;
  public ChallengeHolder() {
    this.challenges = new HashMap<>();
  }

  public <T> void addChallenge(Class<T> type, IChallenge<T> challenge) {      
      challenges.computeIfAbsent(type, ignored -> new ArrayList<>()).add(challenge);
  }

  public boolean handle(Object a) {
    List<IChallenge<T>> challengers = challenges.get(a);
    if (challengers == null) return false;
    return challengers.removeIf(c -> c.handle(a));
  }   

}

Since there are no public access to challenges beyond what the ChallengeHolder class provides, there should be no problem with using Object or Class<T> .

If you need to create IChallenge on demand, then you could perhaps an implementation like this:

public class LazyChallenge<T> implements IChallenge<T> {
  private final Class<IChallenge<T>> impl;
  private IChallenge<T> value;

  public LazyChallenge(IChallenge<T> impl) {
    this.impl = impl;
  }

  public boolean handle(T o) {
    if (value == null) {
      try {
        value = impl.getConstructor().newInstance();
      } catch (java.lang.ReflectiveOperationException e) { // ... a bunch of exception your IDE will fill in ...
        throw new IllegalStateException(e);
      }
    }
    return value.handle(o);
  }
}

You would then add it to ChallengeHolder :

challengeHolder.addChallenge(String.class, new LazyChallenge<>(StringChallenge.class));

Or you could use lambda to avoid the reflection:

public class LazyChallenge<T> implements IChallenge<T> {
  private final Class<IChallenge<T>> supplier;
  private IChallenge<T> value;

  public LazyChallenge(Supplier<IChallenge<T>> supplier) {
    this.supplier = supplier;
  }

  public boolean handle(T o) {
    if (value == null) {
      value = supplier.get();
    }
    return value.handle(o);
  }
}

And:

challengeHolder.addChallenge(String.class, new LazyChallenge<>(StringChallenge::new));

And after though, you may directly use Supplier in place of IChallenge in ChallengeHolder :

class ChallengeHolder {
  private final Map<Class<?>, List<Supplier<IChallenge<?>>>> challenges;
  public ChallengeHolder() {
    this.challenges = new HashMap<>();
  }

  public <T> void addChallenge(Class<T> type, Supplier<IChallenge<T>> challenge) {      
      challenges.computeIfAbsent(type, ignored -> new ArrayList<>()).add(challenge);
  }

  public boolean handle(Object a) {
    List<IChallenge<T>> challengers = challenges.get(a);
    if (challengers == null) return false;
    return challengers.removeIf(c -> c.get().handle(a));
  }   

}

StringChallenge existing = ... ;
// always reuse an existing
challengeHolder.addChallenge(String.class, () -> existing);
// bring a new challenge each time that ChallengeHolder::handle is called
challengeHolder.addChallenge(String.class, StringChallenge::new);

If I were to implements it, I would use the lambda way because you avoid reflection pitfalls (the try catch, the visibility problems especially given that Java 9++ introduced modules, ...).

The LazyChallenge defined above may help to avoid creating the StringChallenge more than one. In that case, it would be best to have it implements Supplier<T> instead of IChallenge<T> .

This whole digression does not change what I pointed out earlier: ensure that only ChallengeHolder read/write the map.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM