Working with JLists

A JList is a Swing component for displaying lists of items. The items displayed are usually Strings, and you can ask a JList to hand you the item the user has currently selected by calling the JList's getSelectedValue method. That method returns null if no item is currently selected; otherwise, it will return the currently selected Object.

The Likes and Dislikes application is a simple example application that displays two lists of items, foods that you like and foods that you don't like.

In this application the user can click on an item from one list to select it,

and click the corresponding move button to move it to the other list:

The Architecture of the Application

The items displayed on the two lists are modelled internally by objects of type Preference. A Preference consists of a String describing the item in question and a boolean that indicates whether the item is liked or not.

public class Preference {
    private String what;
    private boolean like;
    
    public Preference(String what,boolean likeIt)
    {
        this.what = what;
        this.like = likeIt;
    }
    
    public String getWhat() { return what; }
    
    public boolean isLiked() { return like; }
    public void setLiked(boolean likeIt)
    {
        like = likeIt;
    }
}

The main data class in the application maintains a list of Preference objects in an ArrayList. To determine what items are liked or not liked, you can call the following method in the data class:

   public Object[] getItems(boolean liked){
        ArrayList<String> result = new ArrayList<String>();
        Iterator<Preference> iter = data.iterator();
        while(iter.hasNext()) {
            Preference pref = iter.next();
            if(pref.isLiked() == liked)
                result.add(pref.getWhat());
        }
        return result.toArray();
    }

This method iterates over all of the Preference objects looking for objects that are liked or not liked as specified by the parameter. For each Preference we come across that has the desired liked setting, we place a String in the result ArrayList. At the end of the method we convert that array list to an array and return the array as an array of objects.

The reason that we return an array of objects is that we are going to hand that array to the two JLists in the interface to populate the JLists with items to display. This is the code for the Frame class's constructor showing how these arrays are used to populate the two JLists.

public MainFrame() {
  initComponents();
  try {
    data = new DataMain(new File("prefs.txt"));
  }
  catch(IOException ex)
  {
    JOptionPane.showMessageDialog(this,
                      "Could not open the preferences file.");
    System.exit(1);
  }
  likeList.setListData(data.getItems(true));
  hateList.setListData(data.getItems(false));
}

Here now is the code that gets run when the user selects an item from the "Like" list and clicks the button labeled "> Move >".

private void hateButtonActionPerformed(ActionEvent evt) 
{                                           
  Object toHate = likeList.getSelectedValue();
  if(toHate != null) {
    data.flipPreference(toHate.toString());
   }
  likeList.setListData(data.getItems(true));
  hateList.setListData(data.getItems(false));
}

The code here will get the String form of the object the user has selected in the likeList and tell the data class to flip the preference value of the Preference object that corresponds to that String. After that Preference has been updated, both of the lists get rebuilt by asking the data object to regenerate both lists.

This approach, which uses setListData to populate a JList and getSelectedValue to ask which item the user has selected, is very simple to implement. The only small problem it has is that it is somewhat inefficient. When we want to change a Preference object, we will have to search for it and then flip its preference. Likewise, anytime anything changes both JLists have to be completely rebuilt.

An alternative implementation

The LikesAndDislikesWithModel project contains an alternative implementation of this application. The first difference in this new version is somewhat subtle. Here is the new version of the getItems method in the main data class.

public DefaultListModel getItems(boolean liked){
  DefaultListModel result = new DefaultListModel();
  Iterator<Preference> iter = data.iterator();
  while(iter.hasNext()) {
    Preference pref = iter.next();
    if(pref.isLiked() == liked)
      result.addElement(pref);
  }
  return result;
}

Instead of returning an array of Strings to be put in a JList, this version returns a special object called a DefaultListModel. A DefaultListModel is effectively a container for a set of objects that are going to be displayed in a JList. The code above finds all of the Preference objects whose like value matches the liked value given in the parameter. As it finds Preferences that have the correct liked value, the loop above adds these objects to the DefaultListModel.

The DefaultListModel will use the objects it contains to determine what Strings should appear in some JList. For this to work correctly, the Preference objects have to be modified to allow themselves to be converted to Strings. This is done by adding a toString() method to the Preference class.

public String toString() { return what; }

Here is the code for the MainFrame's constructor showing how to link a DefaultListModel to its JList.

private DataMain data;
private DefaultListModel likeModel;
private DefaultListModel hateModel;
    
public MainFrame() {
  initComponents();
  try {
    data = new DataMain(new File("prefs.txt"));
  }
  catch(IOException ex)
  {
    JOptionPane.showMessageDialog(this,
            "Could not open the preferences file.");
    System.exit(1);
  }
  likeModel = data.getItems(true);
  likeList.setModel(likeModel);
  hateModel = data.getItems(false);
  hateList.setModel(hateModel);
}

The great advantage of using a DefaultListModel is that you can add or remove objects from the model and have the JList updated automatically without having to rebuild the model for the JList. To demonstrate how this works, here is the code that moves an item from the liked list to the hated list:

private void hateButtonActionPerformed(ActionEvent evt) {                                           
  Object toHate = likeList.getSelectedValue();
  if(toHate != null) {
    Preference thePref = (Preference) toHate;
    thePref.setLiked(false);
    likeModel.removeElement(toHate);
    hateModel.addElement(toHate);
  }
}

To update the two JLists, we simply have to update their models by removing the object in question from one model and adding it to the other.

As an added bonus here we don't have to search for the Preference object to modify. Since we are storing Preference objects in the models directly, we simply have to tell the object that the user had selected to change its liked setting to false.