8

I have a "strange" problem about Java generics.

First I list my code:

Service.class

package jse.generics.service;

public interface Service {

}

ServiceProvider.class

package jse.generics.service;

public interface ServiceProvider<T extends Service> {

    public T getService();
}

ServiceProviderRegistry.class

package jse.generics.service;

import java.util.HashMap;
import java.util.Map;

public class ServiceProviderRegistry<T extends Service> {

     private Map<Class<T>, ServiceProvider<T>> map = new HashMap<Class<T>, ServiceProvider<T>>();

     public void register(Class<T> clazz, ServiceProvider<T> provider) {
          map.put(clazz, provider);
     }
}

FooService.class

package jse.generics.service;

public class FooService implements Service {

}

FooServiceProvider.class

package jse.generics.service;

public class FooServiceProvider implements ServiceProvider<FooService> {

     @Override
     public FooService getService() {
          return new FooService();
     }

}

ServiceTest.class

package jse.generics.service;

public class ServiceTest {

     /**
     * @param args
     */
     public static void main(String[] args) {
          ServiceProviderRegistry<? extends Service> registry = new ServiceProviderRegistry<Service>();
          registry.register(FooService.class, new FooServiceProvider());
     }

}

In the ServiceTest class the compiler complaints that the registry.register method is not applicable for arguments passed to it.I really don't know why this happens. So I am looking forward to you guy's help to solve this problem.

Samuel Gong
  • 157
  • 1
  • 1
  • 7

4 Answers4

6

In this case you would be better off if ServiceProviderRegistry were not a parameterized class, but instead make the register method (and presumably the corresponding lookup method) generic. In your current approach a ServiceProviderRegistry<Service> can only register Service.class, not any subclasses of Service.

All you really care about is that the class and the provider passed to register match each other, which is an ideal case for a generic method.

public class ServiceProviderRegistry {
  private Map<Class<?>, ServiceProvider<?>> registry = new HashMap<>();

  public <T extends Service> void register(Class<T> cls, ServiceProvider<T> provider) {
    registry.put(cls, provider);
  }

  @SuppressWarnings("unchecked")
  public <T extends Service> ServiceProvider<T> lookup(Class<T> cls) {
    return (ServiceProvider<T>)registry.get(cls);
  }
}

You will need the @SuppressWarnings annotation - it's impossible to implement this pattern without one in a way that will completely satisfy the compiler, which only has access to the compile-time types. In this case you know the cast will always be safe at runtime because register is the only thing that modifies the registry map, so the @SuppressWarnings is justified. Jon Skeet's answer to this related question sums it up very nicely

Sometimes Java generics just doesn't let you do what you want to, and you need to effectively tell the compiler that what you're doing really will be legal at execution time.

Community
  • 1
  • 1
Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
  • `new HashMap<>()` only works for Java 7 and above. For Java 6 and below you'd need `new HashMap, ServiceProvider>>()` – Rajesh J Advani Jun 07 '13 at 08:33
  • Also, your `register` method is missing the `void` he return type. – Rajesh J Advani Jun 07 '13 at 08:39
  • @RajeshJAdvani thanks, I blame too much programming in groovy :-) – Ian Roberts Jun 07 '13 at 09:47
  • "(ServiceProvider)registry.get(cls)" is unsafe! – Mikhail Jun 07 '13 at 10:55
  • @Noofiz I've added a bit more explanation - this is one of those situations where you need the behaviour contract (that the `registry` map is only modified by `register`) to know that the cast will always be safe at runtime, there's simply no way to convince the compiler purely based on compile-time types. – Ian Roberts Jun 07 '13 at 11:06
  • I was partially wrong, you don't need class parametrization, but collection should be typed. @SuppressWarnings can be avoided. – Mikhail Jun 07 '13 at 11:14
  • @Noofiz not entirely. You could say `Map, ServiceProvider extends Service>>` but you'd still have to cast (with suppressed warning) in `lookup` in order to turn the `ServiceProvider extends Service>` into a `ServiceProvider`. `Class.cast` doesn't help because that only works for plain types, not parameterized ones. You'd need a kind of "quantified" type expression `∀X:Map, ServiceProvider>`, which is not something Java generics can express. – Ian Roberts Jun 07 '13 at 11:20
  • How do you explain that there are no casts and warnings in my code than? – Mikhail Jun 07 '13 at 11:32
  • Try this for your example: "Map, ServiceProvider extends Service>> registry = new HashMap, ServiceProvider extends Service>>();" and remove cast in lookup. – Mikhail Jun 07 '13 at 11:41
  • @Noofiz because you're using the raw `ServiceProvider` type in `ServiceProvider sp = registry.lookup(FooService.class);` and thus `sp.getService()` returns just `Service`, not `FooService`. – Ian Roberts Jun 07 '13 at 13:34
5

Method signature is

 public void register(Class clazz, ServiceProvider provider)

You are passing two Class instance here :

registry.register(FooService.class, FooServiceProvider.class);

You need to pass an instance of a class which implements the ServiceProvider interface as the second argument to the register() method.

AllTooSir
  • 48,828
  • 16
  • 130
  • 164
  • Sorry for the wrong type. I replace the second parameter with new FooServiceProvider() and the complaint still exists. – Samuel Gong Jun 07 '13 at 08:11
0

Your Method signature is like:

 public void register(Class clazz, ServiceProvider provider)

But you are passing two Class instances to the register-Method

registry.register(FooService.class, FooServiceProvider.class);

You have to pass an instance of a class which implements the ServiceProvider interface as second argument to the method.

You should use generic types and no raw types. e.g. Class typed with ? instead of only Class

Holger
  • 496
  • 3
  • 8
  • sry i didn´t refresh the page before making my post. But I give a suggestion to not use raw types and the other post not. – Holger Jun 07 '13 at 08:11
0

Weird, weird generics. Here is my solution:

public class ServiceProviderRegistry<T extends Service> {

    private Map<Class<? extends T>, ServiceProvider<? extends T>> map = new HashMap<Class<? extends T>, ServiceProvider<? extends T>>();

    public void register(Class<? extends T> clazz, ServiceProvider<? extends T> provider) {
        map.put(clazz, provider);
    }

    public ServiceProvider<? extends T> lookup(Class<? extends T> cls) {
        return map.get(cls);
    }
}

and main:

public static void main(String[] args) {
    ServiceProviderRegistry<Service> registry = new ServiceProviderRegistry<Service>();
    registry.register(FooService.class, new FooServiceProvider());
    ServiceProvider sp = registry.lookup(FooService.class);
    Service s = sp.getService(); //safe!
}

safe and still typed

Mikhail
  • 4,175
  • 15
  • 31
  • You're using the raw `ServiceProvider` type on the last line of `main`. But in this implementation `ServiceProvider sp = registry.lookup(FooService.class);` won't compile, because `lookup` doesn't force `lookup(X.class)` to be of type `ServiceProvider` (only `ServiceProvider extends Service>`) – Ian Roberts Jun 07 '13 at 10:03
  • Without cast you cant get ServiceProvider from ServiceProviderRegistry. this is illogical task. Parent type can't be casted to child implicitly. – Mikhail Jun 07 '13 at 10:31
  • That was my point - ServiceProviderRegistry shouldn't be a parameterized class at all, but the register and lookup methods should be generic and enforce the matching of types. – Ian Roberts Jun 07 '13 at 10:40
  • The only difference is that here are no warnings compared to your example. I don't make any casts. Just a cleaner code. I am sure bytecode is 100% equal. – Mikhail Jun 07 '13 at 10:54