4

We are trying to create a rudimentary implementation of a Cache in Java. For this, we have created an annotation @Cached defined as:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cached {

    <T> Class<T> keyClass();

    <V> Class<V> valueClass();

    String cacheId();
}

This leads to an error: @interface members may not have type parameters. This however is needed as we would like to typecast the values of the returned method and store them in the cache. We are using AspectJ for intercepting the method calls whose results need to be cached. What is the other way in which we can achieve the result? Since not all methods have the same signature, we have to rely on the methods marked @Cached for our cache implementation.

UPDATE Here is the class that stores data in the cache:

public class Cache<K extends Serializable, V> {
   private Map<K, CacheEntry<V> cache;
   // Some other fields and accessors
}

The class CacheEntry is defined as:

public class CacheEntry<V> {
   private V value;
   // Some other fields used for invalidating the cache entry and accessors
}

Now when accessing the cache, I would like to do something like this:

cache.getCache().values().stream()
        .map(value -> cached.valueClass().cast(value))
        .collect(Collectors.toList());

In above code, cached is the reference of @Cached annotation used on the method as:

@Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
List<Person> findAll();
Prashant
  • 4,775
  • 3
  • 28
  • 47
  • 2
    *This however is needed as we would like to typecast the values of the returned method*: can you show *how* you intend to do that? Even if you could do this, the type argument would still be erased. You probably need to stick to `Class>`. And btw, why don't you use the method's declared return type? – ernest_k Oct 23 '19 at 08:10
  • Do you know about [Type erasure in Generics](https://stackoverflow.com/a/339708/5413565) at compile time? You can't determine what was `T` and `V` at runtime, this information is discarded at execution time. – Mushif Ali Nawaz Oct 23 '19 at 08:17
  • 1
    I would be very concerned about writing a custom caching mechanism, unless there's a very good reason you can't use an existing one. You're most likely to get it wrong, and you'll be spending valuable developer time on it. – Kayaman Oct 23 '19 at 08:23
  • Yes true, I do understand that. But the question asks if there is another way to implement the requirement. – Prashant Oct 23 '19 at 08:24
  • 2
    @Kayaman i understand your concern. This is not a production code just a hobbyist one where I am trying to learn the use of some design patterns and annotations together. – Prashant Oct 23 '19 at 08:26

1 Answers1

1

cached.valueClass().cast(value) is unable to help you in this case, primarily because there's no way for the compiler to enforce that cached.valueClass() is compatible with Cached.valueClass().

Your best way to help cache clients with type casting is to make them specify the target type at read time:

public <V> List<V> getAllCachedValues(Class<V> valueClass) {
    return cache.getCache().values().stream()
    .map(value -> valueClass.cast(value))
    .collect(Collectors.toList());
}

or with this unchecked cast:

public <V> List<V> getAllCachedValues() {
    return cache.getCache().values().stream()
    .map(value -> (V) value)
    .collect(Collectors.toList());
}

Here are some uses to show how unrealistic it would be to attempt to enforce type safety using the annotation:

  1. You can't enforce compatibility between the return type and the annotation metadata:

    @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
    List<Banana> findAll();
    
  2. The client gets to pick the T and V associated with the actual cache instance (otherwise this would be pointless, right?). With that said, this is possible:

    @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
    
    //and then
    Cache<Long, Banana> cacheInstance = cacheManager.getCache("personCache");
    

    Of course, you could add Class parameters to getCache:

    Cache<Long, Banana> cacheInstance = cacheManager.getCache("personCache", 
                        Long.class, Banana.class);
    

    And validate using the actual Class instances. But what would be the point of doing this when you can take the first approach (cast at retrieval)... The bottom line is the caller will ultimately ensure type compatibility, and annotation metadata doesn't help.

In my opinion, @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache") may help with typing if you're serializing cached values to such formats as JSON and XML, and need the original class just to deserialize them when the cache is hit (expecting the client to cast, of course).

ernest_k
  • 44,416
  • 5
  • 53
  • 99