Skip to main content

Wildcards in the Wild

Posted by cayhorstmann on December 13, 2012 at 10:31 AM PST

Nothing new here...just keep moving. I refreshed an older blog to fix some awful formatting issues that the java.net blogging system introduced when deciding to convert all &lt; to <, which makes any HTML document about generics a bit hard to read :-)

People kvetch about wildcards—those pesky ? extends T and ? super T in Java generics. Personally, I didn't think they are so bad. For example,

void doSomethingWith(List<? extends Person> people)

means that people is a List<Person>, List<Employee>, and so on. Any kind of Person is fair game.

Conversely,

int countMatches(Filter<? super Employee> filter)

means that filter can be a Filter<Employee>, Filter<Person>, or even a Filter<Object>.

Easy as pie. Or so I thought, until today, when I decided to do the right thing and add the proper generics to some code using JComboBox.

In the olden days, a combo box managed a collection of objects. Why not strings? You might want to have, say, a combo box filled with java.awt.Color objects. Add color objects, install a renderer that renders the color, and the getSelectedItem method returns the color that the user selected. No need to fuss with conversions between strings and colors.

.

So, in the days of generics, that would be a JComboBox<Color>. Why does getSelectedItem still return Object and not Color? Oh, there is the pesky editor. If the combo box is editable, one can specify one's own items. By default, there is an editor that yields the string that the user types in. But you can install your own editor. For example a color editor could present a JColorChooser.

If you keep the default editor, the selected item in an editable JComboBox<Color> could be a Color or a String. So, the return type of getSelectedItem has to be Object. (If your combo box isn't editable, you can call getItemAt(getSelectedIndex()), and you get a Color without casting.)

So far, so good. Now I wanted to clean up code for a

public class LocaleCombo extends JComboBox<Locale>

It renders all the locales in the currently selected language, so that an English user sees "English / German / Chinese", a German user sees "Englisch / Deutsch / Chinesisch", and a Chinese user sees "英文 / 德文 / 中文". So, the renderer takes each locale item, calls item.getDisplayName(currentLocale), and passes that string to the default renderer.

Let's talk renderers.

The renderer of a JComboBox<E> has type ListCellRenderer<? super E>. Ok, why not? If I have a JComboBox<Employee>, I can use a ListCellRenderer<Person> to render all my employees.

In particular, the default renderer is a ListCellRenderer<Object>. It takes any object, calls toString on it, and returns a component showing that string.

In my code, I delegate to that renderer.

final ListCellRenderer originalRenderer = comboBox.getRenderer();
ListCellRenderer localeRenderer = new ListCellRenderer()
{
   public Component getListCellRendererComponent(JList list,
      Object item, int index, boolean isSelected, boolean cellHasFocus)
   {
      String renderedValue = item.getDisplayName(currentLocale);
      return originalRenderer.getListCellRendererComponent(list, renderedValue, index,
         isSelected, cellHasFocus);
   }
};

I added generics:

final ListCellRenderer<? super Locale> originalRenderer = comboBox.getRenderer();
ListCellRenderer<Locale> localeRenderer = new ListCellRenderer()
{
   public Component getListCellRendererComponent(JList<? extends Locale> list,
      Locale item, int index, boolean isSelected, boolean cellHasFocus)
   {
      String renderedValue = item.getDisplayName(currentLocale);
      return originalRenderer.getListCellRendererComponent(list, renderedValue, index,
         isSelected, cellHasFocus);
   }
};

My reward was this error message:

The method getListCellRendererComponent(JList, capture#3-of ? super Locale, int, boolean, boolean) in the type ListCellRenderer is not applicable for the arguments (JList, String, int, boolean, boolean)

Next, I did what I always tell my students not to do. I randomly changed originalRenderer to

final ListCellRenderer<?> originalRenderer = comboBox.getRenderer();

The error message changed subtly, but it was no more comprehensible.

The method getListCellRendererComponent(JList, capture#3-of ?, int, boolean, boolean) in the type ListCellRenderer is not applicable for the arguments (JList, String, int, boolean, boolean)

It took me a long time to figure out how to deal with this. At first, I was highly suspicious of the Swing implementors. I read this thread and felt that they did it wrong—they should have used two type parameters, one for the rendered type and one for the editor type.

I started writing an indignant blog. In the process of writing it, I realized that the editor had nothing to do with my problem, and that the renderer was properly contravariant. The difficulty is with the getRenderer method that returns the only thing it can—a ListCellRenderer<? super E>. It has no idea what that renderer can render.

But I did—I knew the default renderer can render anything. So I had to tell my program, with a cast, since that's what you do in Java when you know more than the compiler does:

@SuppressWarnings("unchecked") 
final ListCellRenderer<Object> originalRenderer
   = (ListCellRenderer<Object>) comboBox.getRenderer();

The @SuppressWarnings is there because the Java runtime can't enforce the cast due to erasure.

What's the difference between a ListCellRenderer<Object> and a ListCellRenderer<?>? The former can render anything. The latter can render something.

And the compiler doesn't know what that something is. In particular, it doesn't know that the something is Object or String, which is really required in my case.

So, are generics evil? I suppose, since I wasted a couple of hours decorating code that worked perfectly well. Then again, it is nice not to have to cast the selected item of a combo box. And if generics had been there from the start, the combo box designers might have done a better job controlling the editor and renderer types.

At any rate, I learned a new trick for solving tricky generics issues—start writing an indignant blog.

Related Topics >>

Comments

I think the secret is that comboBox.getRenderer() should ...

I think the secret is that comboBox.getRenderer() should return a ListCellRenderer<E>. There is no way of getting the original type back out of the JComboBox<E> class, but you do know that any ListCellRenderer you fed to that class can at least render Es. This should be enough type information to implement localeRenderer.

In Scala, we'd define the ListCellRenderer class as ListCellRenderer[+E], which means the class is covariant. The Scala type system treats ListCellRenderer[Locale] as a subclass of ListCellRenderer[Object], and you wouldn't need to add wildcards on the JComboBox class.