Nov 16, 2001 12:00 AM PT

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

package com.javaworld.jpitfalls.article5;
import java.io.*;
public class BadFileRename
{
    public static void main(String args[])
    {       
        try
        {
            // Check if test file in current dir
            File f = new File("dummy.txt");
            String name = f.getName();
            if (f.exists())
                System.out.println(f.getName() + " exists.");
            else
                System.out.println(f.getName() + " does not exist.");
            // Attempt to rename to an existing file
            File f2 = new File("dummy.bin");
            // Issue 1: boolean status return instead of Exceptions
            if (f.renameTo(f2))
                System.out.println("Rename to existing File Successful.");
            else
                System.out.println("Rename to existing File Failed.");
            // Attempt to rename with a different extension
            int dotIdx = name.indexOf('.');
            if (dotIdx >= 0)
                name = name.substring(0, dotIdx);
            name = name + ".tst";
            String path = f.getAbsolutePath();
            int lastSep = path.lastIndexOf(File.separator);
            if (lastSep > 0)
                path = path.substring(0,lastSep);
            System.out.println("path: " + path);
            File f3 = new File(path + File.separator + name);
            System.out.println("new name: " + f3.getPath());
            if (f.renameTo(f3))
                System.out.println("Rename to new extension Successful.");
            else
                System.out.println("Rename to new extension failed.");
            
            // Delete the file
            // Issue 2: Is the File class a file?
            if (f.delete())
                System.out.println("Delete Successful.");
            else
                System.out.println("Delete Failed.");
            // Assumes program not run from c drive
            // Issue 3: Behavior across operating systems?
            File f4 = new File("c:\\" + f3.getName());
            if (f3.renameTo(f4))
                System.out.println("Rename to new Drive Successful.");
            else
                System.out.println("Rename to new Drive failed.");        
        } catch (Throwable t)
         {
            t.printStackTrace();   
         }       
    }        
}

When Listing 11.1 runs from a drive other than C and with the file dummy.txt in the current directory, it produces the following output:

E:\classes\com\javaworld\jpitfalls\article5>java com.javaworld.jpitfalls.article5.BadFileRename
dummy.txt exists.
Rename to existing File Failed.
path: E:\classes\com\javaworld\jpitfalls\article5
new name: E:\classes\com\javaworld\jpitfalls\article5\dummy.tst
Rename to new extension Successful.
Delete Failed.
Rename to new Drive Successful.

Listing 11.1 raises three specific issues, which are called out in the code comments. At least one is accurately characterized as a pitfall; the others should be considered poor design:

  1. First, returning a boolean error result does not provide enough information about the failure's cause. That proves inconsistent with exception use in other classes and should be considered poor design. For example, the failure above could have been caused by either attempting to renameTo() a file that already exists or attempting to renameTo() an invalid file name. Currently, we have no way of knowing.
  2. The second issue is the pitfall: attempting to use the initial File object after a successful rename. What struck me as odd in this API is the use of a File object in the renameTo() method. At first glance, you assume that you only want to change the filename. So why not just pass in a String? In that intuition lies the pitfall's source.

    The pitfall is the assumption that a File object represents a physical file and not a file's name. In the least, that should be considered poor class naming. For example, if the object merely represents a filename, then the object should be called Filename instead of File. Thus, poor naming directly causes this pitfall, which we stumble over when trying to use the initial File object in a delete() operation after a successful rename.

  3. The third issue is File.renameTo()'s different behavior on different operating systems. The renameTo() works on Windows even across filesystems (as shown here) and fails on Solaris (reported in Sun's bug parade and not shown here). The debate revolves around the meaning of Write Once, Run Anywhere (WORA). The Sun programmers verifying reported bugs contend that WORA simply means a consistent API. That is a cop-out. A consistent API does not deliver WORA; there are numerous examples in existing APIs where Sun went beyond a consistent API to deliver consistent behavior. The best-known example of this is Sun's movement beyond the Abstract Windowing Toolkit's consistent API to Swing's consistent behavior. If you claim to have a platform above the operating system, then a thin veneer of an API over existing OS functionality will not suffice. A WORA platform requires consistent behavior, otherwise "run anywhere" means "maybe run anywhere." To avoid this pitfall, check the os.name System property and code renameTo() differently for each platform.

Out of these three issues, we can currently fix only the proper way to delete a file after a successful rename, as Listing 11.2 demonstrates. Because the other two issues result from Java's design, only the Java Community Process (JCP) can initiate fixes for them.

Listing 11.2. GoodFileRename.java

package com.javaworld.jpitfalls.article5;
import java.io.*;
public class GoodFileRename
{
    public static void main(String args[])
    {       
        try
        {
            // Check if test file in current dir
            File f = new File("dummy2.txt");
            String name = f.getName();
            if (f.exists())
                System.out.println(f.getName() + " exists.");
            else
                System.out.println(f.getName() + " does not exist.");
            // Attempt to rename with a different extension
            int dotIdx = name.indexOf('.');
            if (dotIdx >= 0)
                name = name.substring(0, dotIdx);
            name = name + ".tst";
            String path = f.getAbsolutePath();
            int lastSep = path.lastIndexOf(File.separator);
            if (lastSep > 0)
                path = path.substring(0,lastSep);
            System.out.println("path: " + path);
            File f3 = new File(path + File.separator + name);
            System.out.println("new name: " + f3.getPath());
            if (f.renameTo(f3))
                System.out.println("Rename to new extension Successful.");
            else
                System.out.println("Rename to new extension failed.");
            
            // Delete the file
            // Fix 1: delete via the "Filename" not File
            if (f3.delete())
                System.out.println("Delete Successful.");
            else
                System.out.println("Delete Failed.");
        } catch (Throwable t)
         {
            t.printStackTrace();   
         }       
    }        
}

A run of Listing 11.2 produces the following output:

E:\classes\com\javaworld\jpitfalls\article5>java com.javaworld.jpitfalls.article5.GoodFileRename
dummy2.txt exists.
path: E:\classes\com\javaworld\jpitfalls\article5
new name: E:\classes\com\javaworld\jpitfalls\article5\dummy2.tst
Rename to new extension Successful.
Delete Successful.

Thus, you shouldn't use the File class as if it represents a file instead of the filename. With that in mind, once the file is renamed, operations such as delete() work only on the new filename.

Experience helps you escape Java traps

In summary, gaining familiarity in Java programming is key to avoiding Java pitfalls. We fixed the traps presented in this article by simply learning the proper functions of certain classes. First, remember to position and size components when adding them to a layer in a JLayeredPane. Second, remember that enumerating over a Vector does not work in conjunction with the remove() operation, as Iterator does. Finally, do not use the File class for operations after a successful renameTo().

I would like to thank everyone who emailed me about this column over the past year. The feedback and support has been wonderful. This column has been a rewarding experience, and I will return after a break.

Until then ... best wishes.

Michael C. Daconta is the director of Web and technology services for McDonald Bradley, where he conducts training seminars and develops advanced systems with Java, JavaScript, and XML. Over the past 15 years, Daconta has held every major development position, including chief scientist, technical director, chief developer, team leader, systems analyst, and programmer. He is a Sun-certified Java programmer and coauthor of Java Pitfalls (John Wiley & Sons, 2000; ISBN: 0471361747), Java 2 and JavaScript for C and C++ Programmers (John Wiley & Sons, 1999; ISBN: 0471327190), and XML Development with Java 2 (Sams Publishing, 2000; ISBN: 0672316536). In addition, he is the author of C++ Pointers and Dynamic Memory Management (John Wiley & Sons, 1995; ISBN: 0471049980).

Learn more about this topic