Java Tip 142: Pushing JButtonGroup

Build a better ButtonGroup

Swing has many useful classes that make graphical user interface (GUI) development easy. Some of these classes, however, are not well implemented. One example of such a class is ButtonGroup. This article explains why ButtonGroup is poorly designed and offers a replacement class, JButtonGroup, which inherits from ButtonGroup and fixes some of its problems.

Note: You can download this article's source code from Resources.

ButtonGroup holes

Here's a common scenario in Swing GUI development: You build a form to gather data about items that someone will enter into a database or save to a file. The form might contain text boxes, check boxes, radio buttons, and other widgets. You use the ButtonGroup class to group all radio buttons that need single selection. When the form design is ready, you start to implement the form data. You encounter the set of radio buttons, and you need to know which button in the group was selected so you can store the appropriate information into the database or file. You're now stuck. Why? The ButtonGroup class does not give you a reference to the button currently selected in the group.

ButtonGroup has a getSelection() method that returns the selected button's model (as a ButtonModel type), not the button itself. Now, this might be okay if you could get the button reference from its model, but you can't. The ButtonModel interface and its implementing classes do not allow you to retrieve a button reference from its model. So what do you do? You look at the ButtonGroup documentation and see the getActionCommand() method. You recall that if you instantiate a JRadioButton with a String for the text displayed next to the button, and then you call getActionCommand() on the button, the text in the constructor returns. You might think you can still proceed with the code because even if you don't have the button reference at least you have its text and still know the selected button.

Well, surprise! Your code breaks at runtime with a NullPointerException. Why? Because getActionCommand() in ButtonModel returns null. If you bet (as I did) that getActionCommand() produces the same result whether called on the button or on the model (which is the case with many other methods, such as isSelected(), isEnabled(), or getMnemonic()), you lost. If you don't explicitly call setActionCommand() on the button, you don't set the action command in its model, and the getter method returns null for the model. However, the getter method does return the button text when called on the button. Here is the getActionCommand() method in AbstractButton, inherited by all button classes in Swing:

    public String getActionCommand() {
        String ac = getModel().getActionCommand();
        if(ac == null) {
            ac = getText();
        }
        return ac;
    }

This inconsistency in setting and getting the action command is unacceptable. You can avoid this situation if setText() in AbstractButton sets the model's action command to the button text when the action command is null. After all, unless setActionCommand() is called explicitly with some String argument (not null), the button text is considered the action command by the button itself. Why should the model behave differently?

When your code needs a reference to the currently selected button in the ButtonGroup, you need to follow these steps, none of which involves calling getSelection():

  • Call getElements() on ButtonGroup, which returns an Enumeration
  • Iterate through the Enumeration to get a reference to each button
  • Call isSelected() on each button to determine whether it's selected
  • Return a reference to the button that returned true
  • Or, if you need the action command, call getActionCommand() on the button

If this looks like a lot of steps just to get a button reference, read along. I believe ButtonGroup's implementation is fundamentally wrong. ButtonGroup keeps a reference to the selected button's model when it should actually keep a reference to the button itself. Furthermore, since getSelection() retrieves the selected button's method, you might think the corresponding setter method is setSelection(), but it's not: it's setSelected(). Now, setSelected() has a big problem. Its arguments are a ButtonModel and a boolean. If you call setSelected() on a ButtonGroup and pass a button's model that isn't part of the group and true as arguments, then that button becomes selected, and all buttons in the group become unselected. In other words, ButtonGroup has the power to select or unselect any button passed to its method, even though the button has nothing to do with the group. This behavior occurs because setSelected() in ButtonGroup does not check whether the ButtonModel reference received as an argument represents a button in the group. And because the method enforces single selection, it actually deselects its own buttons to select one unrelated to the group.

This stipulation in the ButtonGroup documentation is even more interesting:

There is no way to turn a button programmatically to 'off' in order to clear the button group. To give the appearance of 'none selected,' add an invisible radio button to the group and then programmatically select that button to turn off all the displayed radio buttons. For example, a normal button with the label 'none' could be wired to select the invisible radio button.

Well, not really. You can use any button, sitting anywhere in your application, visible or not, and even disabled. Yes, you can even use the button group to select a disabled button outside the group, and it will still deselect all of its buttons. To get references to all the buttons in the group, you have to call the ludicrous getElements(). What "elements" has to do with ButtonGroup is anybody's guess. The name was probably inspired by the Enumeration class's methods (hasMoreElements() and nextElement()), but getElements() clearly should have been named getButtons(). A button group groups buttons, not elements.

Solution: JButtonGroup

For all these reasons I wanted to implement a new class that would fix the errors in ButtonGroup and provide some functionality and convenience to the user. I had to decide whether the class should be a new class or inherit from ButtonGroup. All the previous arguments suggest creating a new class rather than a ButtonGroup subclass. However, the ButtonModel interface requires a method setGroup() that takes a ButtonGroup argument. Unless I was ready to reimplement button models as well, my only option was to subclass ButtonGroup and override most of its methods. Speaking of the ButtonModel interface, notice the absence of a method called getGroup().

One other issue I haven't mentioned is that ButtonGroup internally keeps references to its buttons in a Vector. Thus, it unnecessarily gets the synchronized Vector's overhead, when it should use an ArrayList, since the class itself is not thread safe and Swing is single threaded anyway. However, the protected variable buttons is declared a Vector type, and not List as you might expect of good programming style. Thus, I could not reimplement the variable as an ArrayList; and because I wanted to call super.add() and super.remove(), I could not hide the superclass variable. So I abandoned the issue.

I propose the class JButtonGroup, in tone with most of the Swing class names. The class overrides most methods in ButtonGroup and provides additional convenience methods. It keeps a reference to the currently selected button, which you can retrieve with a simple call to getSelected(). Thanks to ButtonGroup's poor implementation, I could name my method getSelected(), since getSelection() is the method that returns the button model.

Following are JButtonGroup's methods.

First, I made two modifications to the add() method: If the button to be added is already in the group, the method returns. Thus, you can't add a button to a group more than once. With ButtonGroup, you can create a JRadioButton and add it 10 times to the group. Calling getButtonCount() will then return 10. This should not happen, so I do not allow duplicate references. Then, if the added button was previously selected, it becomes the selected button (this is the default behavior in ButtonGroup, which is reasonable, so I did not override it). The selectedButton variable is a reference to the currently selected button in the group:

public void add(AbstractButton button)
{   
  if (button == null || buttons.contains(button)) return;
  super.add(button);
  if (getSelection() == button.getModel()) selectedButton = button;
}

The overloaded add() method adds a whole array of buttons to the group. It is useful when you store button references in an array for block processing (i.e., setting borders, adding action listeners, etc.):

public void add(AbstractButton[] buttons)
{
  if (buttons == null) return;
  for (int i=0; i<buttons.length; i++)
  {
    add(buttons[i]);
  }
}

The following two methods remove a button or an array of buttons from the group:

public void remove(AbstractButton button)
{
  if (button != null)
  {
    if (selectedButton == button) selectedButton = null;
    super.remove(button);
  }
}
public void remove(AbstractButton[] buttons)
{
  if (buttons == null) return;
  for (int i=0; i<buttons.length; i++)
  {
    remove(buttons[i]);
  }
}

Hereafter, the first setSelected() method lets you set a button's selection state by passing the button reference instead of its model. The second method overrides the corresponding setSelected() in ButtonGroup to assure that the group can only select or unselect a button that belongs to the group:

public void setSelected(AbstractButton button, boolean selected)
{
  if (button != null && buttons.contains(button))
  {
    setSelected(button.getModel(), selected);
    if (getSelection() == button.getModel()) selectedButton = button;
  }
}
        
public void setSelected(ButtonModel model, boolean selected)
{
  AbstractButton button = getButton(model);
  if (buttons.contains(button)) super.setSelected(model, selected);
}

The getButton() method retrieves a reference to the button whose model is given. setSelected() uses this method to retrieve the button to be selected given its model. If the model passed to the method belongs to a button outside the group, null is returned. This method should exist in the ButtonModel implementations, but unfortunately it does not:

public AbstractButton getButton(ButtonModel model)
{
  Iterator it = buttons.iterator();
  while (it.hasNext())
  {
    AbstractButton ab = (AbstractButton)it.next();
    if (ab.getModel() == model) return ab;
  }
  return null;
}

getSelected() and isSelected() are the simplest and probably most useful methods of the JButtonGroup class. getSelected() returns a reference to the selected button, and isSelected() overloads the method of the same name in ButtonGroup to take a button reference:

public AbstractButton getSelected()
{
  return selectedButton;
}
public boolean isSelected(AbstractButton button)
{
  return button == selectedButton;
}

This method checks whether a button is part of the group:

public boolean contains(AbstractButton button)
{
  return buttons.contains(button);
}

You would expect a method named getButtons() in a ButtonGroup class. It returns an immutable list containing references to the buttons in the group. The immutable list prevents button addition or removal without going through the button group's methods. getElements() in ButtonGroup not only has a totally uninspired name, but it returns an Enumeration, which is an obsolete class you shouldn't use. The Collections Framework provides everything you need to avoid enumerations. This is how getButtons() returns an immutable list:

public List getButtons()
{
  return Collections.unmodifiableList(buttons);
}

Improve ButtonGroup

The JButtonGroup class offers a better and more convenient alternative to the Swing ButtonGroup class, while preserving all of the superclass's functionality.

Daniel Tofan is as a postdoctoral associate in the Chemistry Department at State University of New York, Stony Brook. His work involves developing the core part of a course management system with application in chemistry. He is a Sun Certified Programmer for the Java 2 Platform and holds a PhD in chemistry.

Learn more about this topic