Richard Robinson
Richard Robinson

Reputation: 997

Java Reflection with Annotation Class not working

I'm pretty experienced with Java, however a novice to using Reflection and Annotation classes, which I'm trying to learn for fun. To get some practice, I made an Identifiable class which is designed to add several helpful methods to any class it inherits.

Here is the full class:

abstract class Identifiable<T, K extends Comparable<K>> implements Comparable<Identifiable<T, K>> {

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Identifier { }

    private static Method getMethodAnnotatedWith(final Class<?> type) {
        return Arrays.stream(type.getDeclaredMethods())
                .filter(m -> m.isAnnotationPresent(Identifier.class))
                .findFirst()
                .orElse(null);
    }

    private K id;

    @SuppressWarnings("unchecked")
    public Identifiable(Class<T> clazz) {
        var m = getMethodAnnotatedWith(clazz);
        if (m == null) throw new IllegalArgumentException(
            clazz.toString() + " does not have a method annotated by @Identifier"
        );

        try {
            id = (K) m.invoke(this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public int compareTo(@NotNull Identifiable<T, K> i) {
        return id.compareTo(i.id);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Identifiable<?, ?> that = (Identifiable<?, ?>) o;
        return id == that.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

And here is how I am trying to design it to work:

class Foo extends Identifiable<Foo, Integer> {
    private final int i;

    Foo(int i) {
        super(Foo.class);
        this.i = i;
    }

    @Identifier
    int getI() {
        return i;
    }
}

However, id is always 0 for some reason, so I'm not sure if it's a problem with my Identifier annotation class or the way I'm using reflection. I'm pretty sure it's the latter since while debugging, I found that it is able to access the method with the annotation. Any help would be appreciated, thanks!

Upvotes: 0

Views: 662

Answers (1)

Andreas
Andreas

Reputation: 159086

Don't call the annotated method during construction.

If the identifier value is immutable (final), just pass the value to the super constructor.

public Identifiable(K id) {
    this.id = id;
}
Foo(int i) {
    super(i);
    this.i = i;
}

If the identifier value is mutable, you need to change the logic to invoke the method when you need the value, not cache the value during construction.

abstract class Identifiable<T, K extends Comparable<K>> implements Comparable<Identifiable<T, K>> {

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Identifier {/**/}

    private Method idGetter;

    protected Identifiable(Class<T> type) {
        this.idGetter = Arrays.stream(type.getDeclaredMethods())
                .filter(m -> m.isAnnotationPresent(Identifier.class))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(type.getName() + " does not have a method annotated by @Identifier"));
    }

    @SuppressWarnings("unchecked")
    private final K getIdentifiableKey() {
        try {
            return (K) this.idGetter.invoke(this);
        } catch (IllegalAccessException e) {
            throw new IllegalAccessError(e.getMessage());
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public int compareTo(Identifiable<T, K> that) {
        return this.getIdentifiableKey().compareTo(that.getIdentifiableKey());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        Identifiable<?, ?> that = (Identifiable<?, ?>) o;
        return this.getIdentifiableKey().equals(that.getIdentifiableKey()); // Call equals(), don't use ==
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.getIdentifiableKey());
    }
}

Alternatively, use a functional interface and supply it with a method reference.

abstract class Identifiable<T extends Identifiable<T, K>, K extends Comparable<K>> implements Comparable<Identifiable<T, K>> {

    private Function<T, K> idGetter;

    protected Identifiable(Function<T, K> idGetter) {
        this.idGetter = Objects.requireNonNull(idGetter);
    }

    @Override
    @SuppressWarnings("unchecked")
    public int compareTo(Identifiable<T, K> that) {
        return this.idGetter.apply((T) this).compareTo(that.idGetter.apply((T) that));
    }

    @Override
    @SuppressWarnings("unchecked")
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        Identifiable<T, K> that = (Identifiable<T, K>) o;
        return this.idGetter.apply((T) this).equals(that.idGetter.apply((T) that));
    }

    @Override
    @SuppressWarnings("unchecked")
    public int hashCode() {
        return Objects.hash(this.idGetter.apply((T) this));
    }
}
class Foo extends Identifiable<Foo, Integer> {
    private final int i;

    Foo(int i) {
        super(Foo::getI);
        this.i = i;
    }

    int getI() {
        return i;
    }
}

Upvotes: 2

Related Questions