3

I ran into some compiler errors when designing parallel class hierarchies. The issue seems stem from the limitation of Java where (unlike C++) there's no public "typedef" to allow access to type parameter from outside of a generic class. I guess following sample code best illustrates my question.

public interface Bean { }
public class GreenBean implements Bean { }
public class RedBean implements Bean { }

// All BreanCounter classes implement this interface and interact with their
// corresponding concrete bean classes. 
public interface BeanCounter <B extends Bean> {
     public void addBean(B bean);
     public Collection<B> getBeans();
}

// DefaultBeanCounter implements logic shared by GreenBeanCounter and RedBeanCounter
public class DefaultBeanCounter <B extends Bean> implements BeanCounter<B> {

    private List<B> beans = new ArrayList<B>();

    @Override
    public void addBean(B bean) { beans.add(bean); }

    @Override
    public Collection<B> getBeans() { return beans; }
}

public class GreenBeanCounter extends DefaultBeanCounter<GreenBean> { }
public class RedBeanCounter extends DefaultBeanCounter<RedBean> { }

With above setup, compiler complains about the code below:

public class BeanCounterMaster <C extends BeanCounter<? extends Bean>> {
    public void mergeBeans(C des, C src) {
        for (Bean bean : src.getBeans()) {
            des.addBean(bean); //compiler error on this line
        }
    }
}

The error message reads:

The method addBean(capture#2-of ? extends Bean) in the type BeanCounter is not applicable for the arguments (Bean)

This is understandable but awkward, as src and des are obviously of the same type. And yet, we can't move their beans around because we can only use base type "Bean". In C++ world, we would be able to use "C::BeanType" to refer to the concrete bean type. To work around this, I decide to put merge logic into DefaultBeanCounter where bean type is known:

So I added following methods:

public interface BeanCounter <B extends Bean> {
    public void mergeBeans(BeanCounter<B> anotherCounter);
}

public class DefaultBeanCounter <B extends Bean> implements BeanCounter<B> {
    @Override
    public void mergeBeans(BeanCounter<B> anotherCounter) {
        for (B bean : anotherCounter.getBeans())
            addBean(bean);
    }
}

Now BeanCounterMaster looks like:

public class BeanCounterMaster <C extends BeanCounter<? extends Bean>> {
    public void mergeBeans(C des, C src) {
        des.mergeBeans(src); // Still got compiler error
    }
}

The new compiler error reads:

The method mergeBeans(BeanCounter) in the type BeanCounter is not applicable for the arguments (C)

What am I missing here?

mr49
  • 1,053
  • 1
  • 8
  • 26

1 Answers1

2

When using just C extends BeanCounter<? extends Bean>, you lose the information about the specific type of bean. Because of this, when iterating over src.getBeans() you only know you're working with Beans and the compiler can't tell that these should be the "same type" when adding them to des.

A workaround is to give BeanCountMaster two type parameters:

public class BeanCounterMaster <C extends BeanCounter<B>, B extends Bean> {
    public void mergeBeans(C des, C src) {
        for (B bean : src.getBeans()) {
            des.addBean(bean);
        }
    }
}

Note that if you wanted, mergeBeans could just be a static generic method:

public static <B extends Bean> mergeBeans(
        BeanCounter<B> des,
        BeanCounter<? extends B> src
) {
    for (B bean : src.getBeans()) {
        des.addBean(bean);
    }
}

Here I removed C in favor of just taking BeanCounters of B, in case you want to consider that. Notice I also made src a BeanCounter<? extends B>, meaning you can copy e.g. a BeanCounter<FrozenGreenBean> into a BeanCounter<GreenBean> (see What is PECS (Producer Extends Consumer Super)?).

Community
  • 1
  • 1
Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
  • I assume the static method solution still requires the class to take two type parameters, yes? – mr49 Dec 13 '13 at 05:34
  • @mr49 No, that (like any static method) could be declared anywhere - that's why I said it could "just be" a static method, because it removes the need for instantiating an object to call it on. – Paul Bellora Dec 13 '13 at 05:37
  • Ah, I see. In actual code, I cannot truly make this logic static. At some point, I need to call into the static method from non-static method of an instantiated method. There I run into similar issue again. – mr49 Dec 13 '13 at 05:41
  • @mr49 That's understandable. I just meant it as an example of what's possible. – Paul Bellora Dec 13 '13 at 05:45