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.)
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.
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 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 Frame
s and Panel
s. 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.
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:classescomjavaworldjpitfallsarticle5>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:classescomjavaworldjpitfallsarticle5>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:classescomjavaworldjpitfallsarticle5>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:classescomjavaworldjpitfallsarticle5>java com.javaworld.jpitfalls.article5.BadFileRename
dummy.txt exists.
Rename to existing File Failed.
path: E:classescomjavaworldjpitfallsarticle5
new name: E:classescomjavaworldjpitfallsarticle5dummy.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:
- 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 torenameTo()
an invalid file name. Currently, we have no way of knowing. -
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 aFile
object in therenameTo()
method. At first glance, you assume that you only want to change the filename. So why not just pass in aString
? 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 calledFilename
instead ofFile
. Thus, poor naming directly causes this pitfall, which we stumble over when trying to use the initialFile
object in adelete()
operation after a successful rename. - The third issue is
File.renameTo()
‘s different behavior on different operating systems. TherenameTo()
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 theos.name
System
property and coderenameTo()
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:classescomjavaworldjpitfallsarticle5>java com.javaworld.jpitfalls.article5.GoodFileRename
dummy2.txt exists.
path: E:classescomjavaworldjpitfallsarticle5
new name: E:classescomjavaworldjpitfallsarticle5dummy2.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.