Practice makes perfect

Experience is often your best defense against Java pitfalls

This month's article focuses on hard-won experience: both my experience and the experiences of this column's readers. Many new Java developers fall into traps simply because they lack familiarity with the language. Through this column and the book Java Pitfalls, I strive to ease the learning curve for Java newbies by sharing the experiences that other developers and I have gained. In this article (my last, as I am now busy writing a second volume to accompany Java Pitfalls), I share with you pitfalls that I and other developers encountered in JLayeredPane, Enumeration, and File.renameTo().

Pitfall 9: Assuming too much from JLayeredPane

While working on the jXUL project (an open source effort to integrate XUL (Extensible User Interface Language) with Java) for the book Essential XUL Programming, I ported a Pac-Man arcade game clone called Pagman to a Java-based XulRunner platform. The XulRunner Java class executes XUL applications; it's similar to the JDK's AppletRunner. Figure 1 provides a screenshot of the current version of the Pagman port, which successfully allows the ghost sprites to move on a JLayeredPane's top layer. The sprites move over the background images, which exist in a layer beneath. (Many thanks to my coauthor Kevin Smith, who worked through these pitfalls with me to bring Pagman to fruition.)

Figure 1. Pagman screen in XulRunner

Instead of examining the pitfall encountered in the XulRunner code, which is rather large, we will examine a simpler example that demonstrates the problem. Those interested in the Pagman code can download it from the jXUL Website.

Our simple BadLayeredPane example attempts to create a frame that has a colored panel in a background layer and a button in a foreground layer with a JLayeredPane:

Listing 9.1. BadLayeredPane.java

package com.javaworld.jpitfalls.article5;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class BadLayeredPane extends JFrame
{
    public BadLayeredPane()
    {
        // Error 1: using the root layered pane       
        JLayeredPane lp = getLayeredPane();
        // Set the size of this pane        
        lp.setPreferredSize(new Dimension(100,100));
        
        // Add a colored Panel 
        JPanel jpnl = new JPanel();
        jpnl.setSize(100,100);
        jpnl.setOpaque(true);
        jpnl.setBackground(Color.red);
        
        // Error 2: these MUST be of type integer.
        lp.add(jpnl, 2);
        
        // Put a Button on top
        Button b = new Button("Hi!");
        // Error 3: adding button wrong
        lp.add(b, 1);        
    }
    public static void main(String [] args)
    {
        JFrame frame = new BadLayeredPane();
        frame.addWindowListener(
        new WindowAdapter() 
        {
            public void windowClosing(WindowEvent e) 
            {
                System.exit(0);
            }
        });
        frame.pack();
        frame.setVisible(true);            
    }
}

When Listing 9.1 runs, it produces the screen in Figure 2.

Figure 2. Run of BadLayeredPane

Our JLayeredPane isn't just working improperly; it also has no size! We must first work through the size problem before we can approach the heart of our pitfall.

Listing 9.1 features three errors (called out in the comments); I'll tackle the first two now and address the third later. First, the JLayeredPane that is part of the JFrame's JRootPane causes our size problem. When you examine JRootPane's source code, you see that the JRootPane's RootLayout does not use the JLayeredPane to calculate its size; JLayeredPane only calculates the size of the content pane and the menu bar. Second, when adding components to our JLayeredPane, we use integers instead of Integer objects.

With this knowledge, let's examine our second attempt at displaying our two simple layers:

Listing 9.2. BadLayeredPane2.java

package com.javaworld.jpitfalls.article5;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class BadLayeredPane2 extends JFrame
{
    public BadLayeredPane2()
    {
        // Fix 1: Create a JLayeredPane
        JLayeredPane lp = new JLayeredPane();
        
        // Set the size of this pane        
        lp.setPreferredSize(new Dimension(100,100));
                        
        // Add a colored Panel 
        JPanel jpnl = new JPanel();
        jpnl.setSize(100,100);
        jpnl.setOpaque(true);
        jpnl.setBackground(Color.red);
        
        // Fix 2: use Integer objects
        lp.add(jpnl, new Integer(2));
        
        // Put a Button on top
        Button b = new Button("Hi!");
        lp.add(b, new Integer(1));        
        // Part of Fix 1
        getContentPane().add(lp);
    }
    public static void main(String [] args)
    {
        JFrame frame = new BadLayeredPane2();
        frame.addWindowListener(
        new WindowAdapter() 
        {
            public void windowClosing(WindowEvent e) 
            {
                System.exit(0);
            }
        });
        frame.pack();
        frame.setVisible(true);            
    }
}

We'll first study the fixes applied and then the results. There are two fixes in Listing 9.2 (called out in the comments). First, we create a new JLayeredPane, which we add to the ContentPane. The RootLayout manager uses the ContentPane to calculate the frame's size, so now the JFrame is packed properly. Second, we correctly add components to the JLayeredPane using an Integer object to specify the layer. Figure 3 shows the result of these fixes.

Figure 3. Run of BadLayeredPane2

Figure 3 clearly demonstrates that we have not yet accomplished our goal. Though the colored panel displays, the button fails to appear on the layer above the panel. Why? Because we assume that we add components to a JLayeredPane in the same way that we add components to Frames and Panels. This assumption is our third error and the JLayeredPane pitfall. Unlike Frame and Panel, the JLayeredPane lacks a default LayoutManager; thus, the components have no sizes or positions provided for them by default. Instead, you must explicitly set the size and position of a component before adding it to the JLayeredPane, which Fix 1 achieves in Listing 9.3:

Listing 9.3. GoodLayeredPane.java

package com.javaworld.jpitfalls.article5;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class GoodLayeredPane extends JFrame
{
    public GoodLayeredPane()
    {
        JLayeredPane lp = new JLayeredPane();
        
        // Set the size of this pane        
        lp.setPreferredSize(new Dimension(100,100));
                        
        // Add a colored Panel 
        JPanel jpnl = new JPanel();
        jpnl.setSize(100,100);
        jpnl.setOpaque(true);
        jpnl.setBackground(Color.red);
        
        lp.add(jpnl, new Integer(1));
        
        // Put a Button on top
        Button b = new Button("Hi!");
        // Fix 1: set the size and position
        b.setBounds(10,10, 80, 40);
        lp.add(b, new Integer(2));        
        getContentPane().add(lp);
    }
    public static void main(String [] args)
    {
        JFrame frame = new GoodLayeredPane();
        frame.addWindowListener(
        new WindowAdapter() 
        {
            public void windowClosing(WindowEvent e) 
            {
                System.exit(0);
            }
        });
        frame.pack();
        frame.setVisible(true);            
    }
}

When run, Listing 9.3 produces the correct result, shown in Figure 4.

Figure 4. Run of GoodLayeredPane

In summary, the key pitfall in our JLayeredPane example is the incorrect assumption that the JLayeredPane has a default LayoutManager, as JFrame and JPanel do. Experience tells us to eliminate that assumption and position and size the components for each layer. Once we do so, the JLayeredPane works fine.

Pitfall 10: How not to visit a Vector

Reader Win Harrington noticed a pitfall in the Enumeration implementation class, where its behavior differs from Iterator (both visit each element in a collection). Listing 10.1 demonstrates the behavior in question by removing an element while it iterates over the collection:

Listing 10.1. BadVisitor.java

package com.javaworld.jpitfalls.article5;
import java.util.*;
public class BadVisitor
{
    public static void main(String args[])
    {
        Vector v = new Vector();
        v.add("one"); v.add("two"); v.add("three"); v.add("four");
        
        Enumeration enum = v.elements();
        while (enum.hasMoreElements())
        {
            String s = (String) enum.nextElement();
            if (s.equals("two"))
                v.remove("two");
            else
            {
                // Visit
                System.out.println(s);
            }
        }
                
        // See what's left
        System.out.println("What's really there...");
        enum = v.elements();
        while (enum.hasMoreElements())
        {
            String s = (String) enum.nextElement();
            System.out.println(s);            
        }
    }    
}

When run, Listing 10.1 produces the following output:

E:\classes\com\javaworld\jpitfalls\article5>java com.javaworld.jpitfalls.article5.BadVisitor
one
four
What's really there...
one
three
four

We expect to have visited elements one, three, and four, but instead we receive only visited elements one and four. The problem: We assume the Enumeration implementation and the Vector class work in sync; that is not the case. The Vector.remove() method goes against our expectations; it doesn't modify the index integer (called count).

Listing 10.2 demonstrates how an Iterator removes an item while iterating:

Listing 10.2. BadVisitor2.java

package com.javaworld.jpitfalls.article5;
import java.util.*;
public class BadVisitor2
{
    public static void main(String args[])
    {
        Vector v = new Vector();
        v.add("one"); v.add("two"); v.add("three"); v.add("four");
        
        Iterator iter = v.iterator();
        while (iter.hasNext())
        {
            String s = (String) iter.next();
            if (s.equals("two"))
                v.remove("two");
            else
            {
                // Visit
                System.out.println(s);
            }
        }
                
        // See what's left
        System.out.println("What's really there...");
        iter = v.iterator();
        while (iter.hasNext())
        {
            String s = (String) iter.next();
            System.out.println(s);            
        }
    }    
}

When run, Listing 10.2 produces the following output:

E:\classes\com\javaworld\jpitfalls\article5>java com.javaworld.jpitfalls.article5.BadVisitor2
one
Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:445)
        at java.util.AbstractList$Itr.next(AbstractList.java:418)
        at com.javaworld.jpitfalls.article5.BadVisitor2.main(BadVisitor2.java:15)

As the output shows, while we iterate, the class implementing the Iterator interface specifically checks for modification outside the iteration implementation class and throws an Exception. It would be nice if the Enumeration implementation class were upgraded with this same behavior.

Now let's examine the correct way to remove an item while iterating. Listing 10.3 demonstrates both visiting and modifying with an Iterator:

Listing 10.3. GoodVisitor.java

package com.javaworld.jpitfalls.article5;
import java.util.*;
public class GoodVisitor
{
    public static void main(String args[])
    {
        Vector v = new Vector();
        v.add("one"); v.add("two"); v.add("three"); v.add("four");
        
        Iterator iter = v.iterator();
        while (iter.hasNext())
        {
            String s = (String) iter.next();
            if (s.equals("two"))
                iter.remove();
            else
            {
                // Visit
                System.out.println(s);
            }
        }
                
        // See what's left
        System.out.println("What's really there...");
        iter = v.iterator();
        while (iter.hasNext())
        {
            String s = (String) iter.next();
            System.out.println(s);            
        }
    }    
}

When Listing 10.3 runs, it produces the following output:

E:\classes\com\javaworld\jpitfalls\article5>java com.javaworld.jpitfalls.article5.GoodVisitor
one
three
four
What's really there...
one
three
four

Notice that the Iterator implementation class, not the Vector class, calls the remove() method.

Pitfall 10 is caused by the expectation that a class will do more than it does. Specifically, the Vector implementation of the Enumeration interface is assumed to work in conjunction with the underlying collection's modification. Implementations of Iterator fixed the pitfall by including a remove() method in the Iterator interface.

Pitfall 11: When File.renameTo() doesn't

Craig Minton, another reader, suggested that the File.renameTo() method suffered pitfalls in both design and implementation. Listing 11.1 demonstrates that method's behavior and pitfalls:

Listing 11.1. BadFileRename.java

1 2 Page 1
Page 1 of 2