2

There are many questions about the issue of combining generics with varargs. This would require generic arrays which don't exist when actual code tries to instantiate them. Moreover, there's a good amount of documentation on the compiler-vagueness of warnings from varargs methods with non-reifiable parameters. Because of type erasure this creates potential heap pollution, hence the warning (in Java 6 at the caller). However, my question is not about these problems themselves. I think I understand that some things aren't possible. What I'd like to know is the way to elegantly workaround these problems in my complex case.

Links for related topics:

My case

I have a BookItemSearchAddTask that extends from the Android AsyncTask but somewhere along its inheritance hierarchy has been made generic, more abstract at higher levels:

At a higher level it's SearchAddTask, which contains the method start() to execute the task, called from a client that knows that it passes a BookItem product in.

public abstract class SearchAddTask<ProductToAdd extends Product & NamedProduct> 
        extends AddTask<ProductToAdd, ProductToAdd> {

    public void start(ViewActivity context, ProductToAdd product) throws SpecificAddTaskDomainException, TaskExistsException, TaskUnavailableException {
        super.start(context, product);
            //more stuff ...
            execute(product);
    }
}

A level lower it's an ItemSearchAddTask. Here the method doInBackground is implemented, as required by the AsyncTask API. It can still use generics.

public abstract class ItemSearchAddTask extends SearchAddTask<I> {

    public I doInBackground(I... params) {
            I product = params[0];
            //do stuff ...
            return product;
    }
}

Finally BookItemSearchAddTask is ItemSearchAddTask<BookItem>. A BookItem therefore is an Item, is a Product. The "I" is linked to the class in which this nested task class, ItemSearchAddTask, finds itself:

public abstract class ItemSearchAddWindow<I extends Item & ImageRepresentedProduct & NamedProduct> extends ViewActivity implements View.OnClickListener,
        AdapterView.OnItemClickListener {}

The problem

Now, when I run this code I get the following error:

Caused by: java.lang.ClassCastException: [Lnet.lp.collectionista.domain.Product;
    at net.lp.collectionista.ui.activities.items.book.ItemSearchAddWindow$ItemSearchAddTask.doInBackground(ItemSearchAddWindow.java:1)

Note the "[L".

I also get compile time warnings at "execute(product);": "Type safety: A generic array of ProductToAdd is created for a varargs parameter"

The cause

To my understanding, the JVM finds that in the doInBackground vararg it gets a Product[] passed in, rather than the Item[] (I[]) it expects. Apart from the generic arrays, which are hard to think about, I think what's going on is the case of the lion cage at http://docs.oracle.com/javase/tutorial/java/generics/subtyping.html. Not by my code, but because the generated generic array of ProductToAdd (which basically extends Product) was created for the varargs param.

I checked that if I pass no argument to execute, that it works. Also using "execute((ProductToAdd[])new MusicCDItem[]{new MusicCDItem()});" worked mostly (don't ask, a MusicCDItem is an Item, just like a BookItem is).

A solution?

However in start() I can't know that I need to pass in a BookItem. I only know about "I". Since that is a generic, I can't create the generic array that is required to pass as the varargs param. I think what is complex about my case is the different levels of using generics, as well as the parallel hierarchies.

How do I work around this feature gap? I considered:

  • Removing generics. A bit drastic.
  • Holding on the varargs param everywhere in all the generics bits of code (i.e. change the method signature of start()), until we reach the client code, and only there do I pass one product element only, and that element is of a real type, BookItem. I will get the same warning there but the compiler will be able to generate the correct generic array.
  • Duplicating the AsyncTask code and changing doInBackground to not use a varargs param because I currently may not need one. I prefer not to do this so I get the benefits when AsyncTask is updated in the future.
  • Perhaps some reflection code in start(). Ugly.

Is there anything shorter and more local?

Either this, or there is some really stupid typo in my code.

Community
  • 1
  • 1
pjv
  • 10,658
  • 6
  • 43
  • 60
  • Just to let you know I went with solution 2 "Holding on the varargs param everywhere" for now and it seems to work. – pjv Jan 16 '12 at 22:40

1 Answers1

1

You noted that you get an "Unchecked generic array" warning in SearchAddTask.start() when it calls execute(). However, the actual warning is slightly misleading. What it says is A generic array of ProductToAdd is created for a varargs parameter, but what it really means is, ProductToAdd is a type variable and at run-time I can't create an array of those, so I'll just have to use my best guess.

If you step through into execute() in the debugger, you'll see that the array that was created for the P... declaration is a Product[1] -- exactly what you'd expect from the class cast exception you got. At run time, this is the best the JVM can do, because it's the closest un-erased ancestor of ProductToAdd.

Unfortunately, in ItemSearchAddTask, the JVM's also done the best it can, which is convert the I... declaration into an Item[] (the closest un-erased ancestor of I); thus the ClassCastException when execute() tries to call doInBackground().

The least awful way I can think of offhand to get around this is to sidestep Java's type erasure by keeping ProductToAdd's concrete class around at run time and creating the args array (of the correct type) yourself:

abstract class SearchAddTask<ProductToAdd extends Product & NamedProduct> 
  extends AddTask<ProductToAdd, ProductToAdd> {

    private final Class<ProductToAdd> productClass;

    SearchAddTask(Class<ProductToAdd> productClass) {
        this.productClass = productClass;
    }

    public void start(ViewActivity context, ProductToAdd product) {
        super.start(context, product);
        ProductToAdd[] argsArray = (ProductToAdd[]) Array.newInstance( productClass, 1 );
        argsArray[0] = product;
        execute( argsArray );
    }
}

It does mean you have to ensure that BookItem.class gets passed in, probably when you create the AddWindow<BookItem>, but it keeps the ugliness contained.

David Moles
  • 48,006
  • 27
  • 136
  • 235