6

I have this basic News interface

interface News {
    String getHeader();
    String getText();
}

and concrete classes like SportsNews and FinancialNews to provide specific methods like getStockPrice(), getSport() and so on. News are intended to be dispatched to a

interface Subscriber<N extends News> {
    void onNews(N news);
}

The problem is how to register and maintain subscriptions. The first approach I tried was using a central Aggregator, keeping a map between Class<T> objects and Set<Subscriber<T>>, but soon this approach revealed unviable. Here is the desired API

public class Aggregator {

    public <N extends News> void subscribe(Subscriber<N> subscriber) {
        // TODO somehow (super type token) extract N and 
        // add the item to the set retrieved by getSubscribersFor()
    }

    public <N extends News> void dispatch(N news) {
        for (Subscriber<N> subscriber: getSubscribersFor(news.getClass())) {
            subscriber.onNews(news);
        }
    }

    private <N extends News> Set<Subscriber<N>> getSubscribersFor(Class<N> k) {
        // TODO retrieve the Set for the specified key from the Map
    }
}

Is there any alternative to be type safe? Can Java solve this problem at all? I put this little demo online to help you better understand what the problem really is.

UPDATE

An alternative would be to make Aggregator itself parameterized with the actual news type. This would be ok, except that it's a chicken and egg problem: now one needs to find a way to retrieve the aggregator. In Java there's no way to express the following

interface News {
    static Aggregator<CurrentClass> getAggregator();
}
  • static method can't be abstract
  • there's no way to reference the current type in a type argument
Raffaele
  • 20,627
  • 6
  • 47
  • 86
  • Nothing is wrong. The `Aggregator` doesn't compile because `getSubscribersFor()` doesn't return anything and it should, but that's the point of the question... – Raffaele Oct 23 '12 at 20:13
  • I get error in `dispatch`: incompatible types/found: Subscriber/required: Subscriber – Miserable Variable Oct 23 '12 at 21:12
  • @MiserableVariable `Aggregator` doesn't compile. I put the signatures to describe the desired API, but definitely I asked this question because I can't compile it... Either one method works, or the other, that's the problem – Raffaele Oct 23 '12 at 21:13
  • Perhaps I misunderstood. I thought you said `Aggregator` does not compile because `getSubscribersFor`. That is not the only reason it does not compile, another (more important) error is that `getSubscribersFor(news.getClass()` does not return a `Set>`, because `news` can be any other subtype of `N` – Miserable Variable Oct 23 '12 at 21:31
  • Sorry, I edited the code a number of times, depending on my current attempt. Forget *why* it doesn't compile, and just make it run :) Keep the API (`subscribe` and `dispatch`), all the rest can be freely changed – Raffaele Oct 23 '12 at 21:34

4 Answers4

4

Here's what I would do. If you can use Guava (a Google library written and used by Google), I recommend scrolling down and looking at the other solution first.

Vanilla Java

First, start by adding a method to get the class from your subscribers:

public interface Subscriber<N extends News> {
    void onNews(N news);
    Class<N> getSupportedNewsType();
}

Then when implementing:

public class MySubscriber implements Subscriber<MyNews> {

    // ...

    public Class<MyNews> getSupportedNewsType() {
        return MyNews.class;
    }
}

In your aggregator, include a map where the keys and values aren't typed:

private Map<Class<?>, Set<Subscriber<?>> subscribersByClass = ... ;

Also note that Guava has a multimap implementation that will do this key to multiple values stuff for you. Just Google "Guava Multimap" and you'll find it.

To register a subscriber:

public <N extends News> void register(Subscriber<N> subscriber) {
    // The method used here creates a new set and puts it if one doesn't already exist
    Set<Subscriber<?>> subscribers = getSubscriberSet(subscriber.getSupportedNewsType());
    subscribers.add(subscriber);
}

And to dispatch:

@SuppressWarnings("unchecked");
public <N extends News> void dispatch(N news) {
    Set<Subscriber<?>> subs = subscribersByClass.get(news.getClass());
    if (subs == null)
        return;

    for (Subscriber<?> sub : subs) {
        ((Subscriber<N>) sub).onNews(news);
    }
}

Notice the cast here. This will be safe because of the nature of the generics between the register method and the Subscriber interface, provided no one does something ridiculously wrong, like raw-typing such as implements Subscriber (no generic argument). The SuppressWarnings annotation suppresses warnings about this cast from the compiler.

And your private method to retrieve subscribers:

private Set<Subscriber<?>> getSubscriberSet(Class<?> clazz) {
    Set<Subscriber<?>> subs = subscribersByClass.get(news.getClass());
    if (subs == null) {
        subs = new HashSet<Subscriber<?>>();
        subscribersByClass.put(subs);
    }
    return subs;
}

Your private methods and fields do not need to be type safe. It won't cause any problems anyway since Java's generics are implemented via erasure, so all of the sets here will be just a set of objects anyway. Trying to make them type safe will only lead to nasty, unnecessary casts that have no bearing on its correctness.

What does matter is that your public methods are type safe. The way the generics are declared in Subscriber and the public methods on Aggregator, the only way to break it is via raw types, like I stated above. In short, every Subscriber passed to register is guaranteed to accept the types that you're registering it for as long as there's no unsafe casts or raw typing.


Using Guava

Alternatively, you can take a look at Guava's EventBus. This would be easier, IMO, for what you're trying to do.

Guava's EventBus class uses annotation-driven event dispatching instead of interface-driven. It's really simple. You won't have a Subscriber interface anymore. Instead, your implementation will look like this:

public class MySubscriber {
    // ...

    @Subscribe
    public void anyMethodNameYouWant(MyNews news) {
        // Handle news
    }
}

The @Subscribe annotation signals to Guava's EventBus that it should remember that method later for dispatching. Then to register it and dispatch events, use an EventBus isntance:

public class Aggregator {
    private EventBus eventBus = new EventBus();

    public void register(Object obj) {
        eventBus.register(obj);
    }

    public void dispatch(News news) {
        eventBus.dispatch(news);
    }
}

This will automatically find the methods that accept the news object and do the dispatching for you. You can even subscribe more than once in the same class:

public class MySubscriber {
    // ...

    @Subscribe
    public void anyMethodNameYouWant(MyNews news) {
        // Handle news
    }

    @Subscribe
    public void anEntirelyDifferentMethod(MyNews news) {
        // Handle news
    }
}

Or for multiple types within the same subscriber:

public class MySubscriber {
    // ...

    @Subscribe
    public void handleNews(MyNews news) {
        // Handle news
    }

    @Subscribe
    public void handleNews(YourNews news) {
        // Handle news
    }
}

Lastly, EventBus respects hierarchical structures, so if you have a class that extends MyNews, such as MyExtendedNews, then dispatching MyExtendedNews events will also be passed to those that care about MyNews events. Same goes for interfaces. In this way, you can even create a global subscriber:

public class GlobalSubscriber {
    // ...

    @Subscribe
    public void handleAllTheThings(News news) {
        // Handle news
    }
}
Brian
  • 17,079
  • 6
  • 43
  • 66
  • It's for an Android project, and Guava would make the package too big. Anyway, +1. Just a couple of points: 1. `getSupportedNewsType()` isn't needed, since the aggregator can easily extract the type parameter itself programmatically, so one doesn't need to override that in each subscriber 2. Since I can't use a `Multimap`, I need to find a way to get the appropriate `Set>` from my `Map`. That's what my private method `getSubscribersFor(Class)` was for – Raffaele Oct 23 '12 at 21:42
  • To put it plainly, on runtime, Java doesn't care about what type is on the subscriber. What you want is compile-time verification. (You won't get runtime verification because of erasure). It doesn't matter what your `getSubscribersFor` method returns because ultimately, it's just going to be a set of objects that all have an `onNews` method that accepts objects. As long as your public method signatures are type-safe, the underlying method of storing them doesn't matter. I'll update my answer to reflect this a little better. – Brian Oct 23 '12 at 21:46
  • I think you got the point. The whole thing of using generics instead of raw types everywhere is to guarantee that if there's something wrong, it fails as soon as possible (ie at compile time). In this case, it seems I can only use generics to make the `Subscriber` API look nicer, because everywhere else I used unchecked casts (potentially unsafe) – Raffaele Oct 23 '12 at 21:52
  • I accepted your answer, but I think you should clean it up and organize all of your code into a single compilable and executable unit ;) Currently, there are some typos and the work can't overall be used by others for future reference. Again, I suggest to drop the `getSupportedNewsTypes()` since it can be achieved via reflection (and there are some annoyances with `getSubscribersFor(Class)` and subclasses, but this is another point) – Raffaele Oct 23 '12 at 22:20
  • 1
    @Brian interesting. how well does Guava handle generics? for example, `class Parent{ @Subscribe void foo(T){...} } class Child extends Parent{}`. now register a `new Child()`, then dispatch an `Integer`, what happens? – irreputable Oct 24 '12 at 00:38
  • 1
    @irreputable `EventBus` uses the types available at runtime, not compile-time, so `void foo(T)` would be `void foo(Object)` on runtime. This means that `EventBus` wouldn't handle it as `Number`, but as `Object`. – Brian Nov 07 '12 at 19:43
  • Seems like a lot of things in Java go "First, start by adding a method to get the class from ____" :) – Dmitry Minkovsky Aug 01 '15 at 02:57
1

You will need to send the class parameter to dispatch. The following compiles for me, not sure if that meets your needs:

import java.util.Set;

interface News {
    String getHeader();
    String getText();
}

interface SportsNews extends News {}

interface Subscriber<N extends News> {
    void onNews(N news);
}


class Aggregator {

    public <N extends News> void subscribe(Subscriber<N> subscriber, Class<N> clazz) {
        // TODO somehow (super type token) extract N and 
        // add the item to the set retrieved by getSubscribersFor()
    }

    public <N extends News> void dispatch(N item, Class<N> k) {
        Set<Subscriber<N>> l = getSubscribersFor(k);
        for (Subscriber<N> s : l) {
            s.onNews(item);
        }
    }

    private <N extends News> Set<Subscriber<N>> getSubscribersFor(Class<N> k) {
        return null;
        // TODO retrieve the Set for the specified key from the Map
    }
}
Miserable Variable
  • 28,432
  • 15
  • 72
  • 133
  • I don't understand this. The `Class` arguments arent't needed at all: in `subscribe` you can extract the argument via [`ParameterizedType`](http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/ParameterizedType.html#getActualTypeArguments()), and in `dispatch` you simply cast to `Class` the object returned by `item.getClass()`. Am I missing something? – Raffaele Oct 23 '12 at 21:48
  • I am not sure if I missing something as well :) but the fact that `item` may be of a *subtype* of `N` means that you cannot simply cast it to `Class` – Miserable Variable Oct 23 '12 at 22:16
  • I see - then the missing thing are specs, it's my fault :) this code is intended to be used in a simple project, so it's ok to assume that item **can't** be a subtype of `N`. However this answer makes a point that effectively messed up some tests I wrote, so + 1 – Raffaele Oct 23 '12 at 22:26
  • Glad it was of some use, I l learned something new about generics too but I am not quite sure what :) – Miserable Variable Oct 23 '12 at 22:31
0

You can get the super types of subscriber.getClass(), by Class.getGenericSuperclass/getGenericInterfaces(), then inspect them to extract which N really is, by ParameterizedType.getActualTypeArguments()

For example

public class SportsLover implements Subscriber<SportsNews>
{
    void onNews(SportsNews news){ ... }
}

if subscriber is an instance of SportsLover

Class clazz = subscriber.getClass();   // SportsLover.class

// the super type: Subscriber<SportsNews>
Type superType = clazz.getGenericInterfaces()[0];  

// the type arg: SportsNews
Type typeN = ((ParameterizedType)superType).getgetActualTypeArguments()[0];  

Class clazzN = (Class)typeN;  

That works for simple cases.

For more complicated case, we'll need more complicated algorithms on types.

irreputable
  • 44,725
  • 9
  • 65
  • 93
  • Yes, I read that in the [Gafter's article](http://gafter.blogspot.it/2006/12/super-type-tokens.html). That's not the hard part, at the moment. Can you elaborate on the rest? – Raffaele Oct 23 '12 at 20:12
  • your problem doesn't need "type token" – irreputable Oct 23 '12 at 20:18
  • I don't know what my problem needs. Reflection? Generics? Polymorphism? I'm open minded, I'd just like `Aggregator` to compile. Your solution suggests how to extract the type argument: this is fine, but it's not the only problem. Try to [make it work](http://pastebin.com/LVbcw0Nv) – Raffaele Oct 23 '12 at 20:28
  • You need reflection, to extract N for any given Subscriber object. that's what my code demonstrated, extract `classN` from `subscriber`. – irreputable Oct 23 '12 at 20:32
  • OK, but this is just the first step to solve the problem. Now I need to somehow keep a map between `Class`es and `Set`s, and dinamically put and retrieve `Subscriber` in the approprate set in a type safe manner: **that** is the real problem. Check the provided link – Raffaele Oct 23 '12 at 20:36
0

A way could be use a TypeToken to hold N

 Type typeOfCollectionOfFoo = new TypeToken<Collection<Foo>>(){}.getType() 

To make your code work

Declare your class as

public static class Aggregator<N extends News>

Change method signature to

 private Set<Subscriber<N>> getSubscribersFor() {

And you are done.

Amit Deshpande
  • 19,001
  • 4
  • 46
  • 72
  • That is not the problem. I don't need an external class because I already know that real subscribers implement a generic interface, and can use that to extract the type. To answer the question, you need to make [this](http://pastebin.com/LVbcw0Nv) work – Raffaele Oct 23 '12 at 20:29
  • `Aggregator` is not supposed to be parameterized, it should be a container of heterogeneous news. The alternative (maybe I forgot to mention it) is making a `static` method in each `News` subtype to return the parameterized aggregator, but still there's no way of enforcing this at the language level - I mean, `News` should declare something like `abstract static Aggregator getAggregator()`, which in Java is prohibited in a number of ways – Raffaele Oct 23 '12 at 20:45