CSE190L Notes for Monday, 5/14/07

We continued our discussion of adding serialization to the Clicker program. I mentioned that I was seeing a bug in DrJava that was not manifesting itself when I run Java from the command line. It apparently has to do with the different compiler settings that DrJava uses. So I used emacs and the command line to complete the example.

Where we had left off is that we had a Save button that would store the model to a file and a Restore button that would read it back in. But we found that it didn't quite work. We had briefly discussed part of the reason at the end of Friday's lecture. When we restore, we end up with a second version of the model that has the same state as when we gave the save command. But our panel doesn't know about this new model. It's still talking to the old model. Someone suggested that we could call the update method that is part of the ClickerListener interface:

        restore.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    FileInputStream fis = new FileInputStream("clicker.dat");
                    ObjectInputStream input = new ObjectInputStream(fis);
                    model = (ClickerModel) input.readObject();
                } catch (Exception error) {
                    throw new RuntimeException(error.toString());        
                }
                panel.update(model);
            }
        });
The only problem with this is that in our ClickerPanel code, we never used the model that was passed as a parameter. In the old version, we only had one model floating around, so that made sense. But to make this new version work, we had to add a line of code in the panel's update method to reassign the model before it calls repaint:

        public void update(ClickerModel model) {
            this.model = model;
            repaint();
        }
When we ran this version of the program, it behaved better. When we selected restore, we saw that the panel reverted to the previous version. But then we found that clicks after that were not displayed properly. But when we resized the window, we saw lots of things change. I did this more slowly, calling restore and then clicking twice. Then I stretched the window just slightly and the two circles appeared where I had clicked.

So what could be wrong? This is the problem we started with when we first talked about model/view/controller. This is the update problem. The view is not updating when a change occurs. Our mechanism for solving that problem is that we registered the view with the model and the model would contact the view when an update occurred. So why isn't that working now?

The answer comes from understanding how serialization works. When we ask Java to write out the model, it can't write out just a single object. It has to write out every object that can be reached from the model. For example, it wouldn't make much sense to write out the model object without also writing out its map of points. And that map of points includes two sets of points that also need to be written out.

The key thing to keep in mind is that Java looks for every single object that can be reached starting with the object you are writing out and it writes out each one of those objects. It turns out that in our case, it is writing out the panel. That happens because the model has a list of its listeners and the panel is included in that list. So the serialization process eventually finds that it should write out the panel.

On the other side, when we read this back in, Java recreates a new panel that the new model is talking to. But we need the new model to be talking to the panel we have up on the screen, not this newly constructed panel that is a copy of the original.

We were able to fix our immediate problem by adding this line of code after we read in the model:

        model.addListener(panel);
We are, in effect, registering the panel as a listener a second time. That's because we have a new model to work with now and we have to register our panel with that model as well.

This version of the program behaved much better. After a call on restore, we got back the old panel and we could click on new locations and those would appear on the screen. But this new version had an interesting problem with it. I had included a special method in the ClickerModel class to allow us to explore this:

        public void showListeners() {
            for (ClickerListener listener : listeners) {
                System.out.println(listener);
                System.out.println(listener.hashCode());
            }
            System.out.println();
        }
This method simply prints out the various listeners for the model. It also prints out their hash code. I included that as a way to identify the actual object, versus the state of the object. I added a call on this method in our action listener for restore. When we clicked on save and then restore, we got output like this:

        ClickerPanel[,0,0,600x355,invalid,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
        2490106
        ClickerPanel[,0,0,600x355,invalid,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
        12014584
There are two different panels here that seem to be in the same state according to their toString output, but they have different hash codes. That's because it's two copies of the same panel. I showed that as we type save, then restore, then save, then restore, then save, then restore, etc, this list of panels just grew and grew. And when we checked the file size of clicker.dat, we saw that it was growing as well. That's because in our current implementation, we are accumulating extra copies of the panel because it is being serialized along with the model.

Someone asked if there is a way to stop the listeners from being serialized. There is a mechanism for doing this. You can declare a field to be transient, in which case Java does not serialize it. So we went into the ClickerModel class and indicated that the listeners should be transient:

        public class ClickerModel implements Serializable {
            private Map> playerPoints;
            transient private List listeners;

            ...
        }
When we ran this version, we found that we got a NullPointerException because the new model needs to have this field set up. When you declare a field to be transient, you often have to include your own custom code to manipulate that field yourself. In our case, we just needed some code for the reading (deserialization) process. This is described in chapter 12 and in the api documentation for Serializable. We added a private method called readObject in which we reconstructed the listener list:

        private void readObject(ObjectInputStream in) {
            listeners = new ArrayList();
        }
This almost worked, but now it failed to construct the other field. When you write a custom read object, you have to remember to include code to read the non-transient parts of the class:

        private void readObject(ObjectInputStream in) throws Exception {
            in.defaultReadObject();
            listeners = new ArrayList();
        }
We had to add the throws clause in the header because the call on defaultReadObject throws two checked exceptions.

This version of the program continued to function properly and it didn't create extra copies of the panel. In fact, the file clicker.dat shrunk from being several thousand bytes long to being just a few hundred bytes long.

Someone pointed out that we could have avoided a lot of this mess by serializing just the map of points and not attempting to serialize the entire model. That's a good point and it is probably a better overall approach, although writing the code the way we have is instructive to learn how to do custom serialization if you need it.

We spent the last part of class modifying the class to use a menu rather than a set of buttons. I mentioned that in putting together a menu, you have three levels of structure to deal with:

We changed the JButtons for save and restore in JMenuItems and wrote the following code:

	JMenuBar bar = new JMenuBar();
	JMenu menu = new JMenu("File");
	menu.add(save);
	menu.add(restore);
	bar.add(menu);
	frame.add(bar);
The code has almost the right logic. We have two menu items that we add to a menu. We add that menu to a menu bar. And then we add the menu bar to the frame. But this gave us an odd result. Our panel disappeared and we had a menu in its place. The problem is that for menus, we need to call a different method than add. The last line of code above should instead be:

	frame.setJMenuBar(bar);
When we made this change, the menu started functioning properly. I then mentioned that with menu items, we have the ability to set them to be enabled or disabled. To be able to do this throughout the class, we had to elevate the save and restore menu items to be fields.

I said that we shouldn't allow a restore command if the file "clicker.dat" isn't available. So we added this line of code after constructing the restore item:

	restore.setEnabled(new File("clicker.dat").exists());
We also decided that in the save action, we should enable restore and disable save:

        restore.setEnabled(true);
        save.setEnabled(false);
By disabling save in the save action, we prevent the user from saving several times in a row when no changes have occurred. But then we had to turn save back on inside handleClick:

        private void handleClick(MouseEvent e) {
            // ignore user clicks that happened while the computer was moving
            if (!clickOkay)
                return;
    	    save.setEnabled(true);
            ...
        }
All of these changes are available as a complete set of classes in a zip file that I have labeled as handout #20.

Stuart Reges
Last modified: Wed May 16 22:32:00 PDT 2007