Using a wildcard-bounded generic type, as suggested in other answers, isn't a great idea.
If you declare your class like this:
public interface Cache<K extends CacheKey<?>, V extends CacheValue<?>> {
}
then you would never be able to usefully invoke get() on the key or value, because they would only ever return Serializable. And Serializable is a useless class (it's barely different to Object actually inside your code).
Instead, I would simply declare the class like:
public interface Cache<K extends Serializable, V extends Serializable> {
and then declare that the put method takes a CacheKey<K> and a CacheValue<V>:
void put(CacheKey<K> key, CacheValue<V> value);
because, ultimately, all implementations of CacheKey<K> and CacheValue<V> should be indistinguishable.
If you really want to force the CacheKey<K> and CacheValue<V> to be of specific types, you need to add more type variables:
public interface Cache<
K extends Serializable, CK extends CacheKey<K>,
V extends Serializable, CV extends CacheValue<V>> {
void put(CK key, CV value);
}
but this is really quite gross, as you would have to carry around all of these 4 type variables wherever you use a Cache type directly.
Of course, you can specialize the interface to hide some of these type variables:
interface StringKey extends CacheKey<String> {}
interface IntegerValue extends CacheValue<Integer> {}
interface StringIntegerCache extends Cache<String, StringKey, Integer, IntegerValue> {}
and then just use StringIntegerCache. The usefulness of doing so depends on your application.