Anda di halaman 1dari 14

J D C

T E C H

T I P S
TIPS, TECHNIQUES, AND SAMPLE CODE

WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips,


September 4, 2001. This issue covers:
* Making Defensive Copies of Objects
* Using Iterators
These tips were developed using Java(tm) 2 SDK, Standard Edition,
v 1.3.
You can view this issue of the Tech Tips on the Web at
http://java.sun.com/jdc/JDCTechTips/2001/tt0904.html
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MAKING DEFENSIVE COPIES OF OBJECTS
Suppose that you are doing some graphical programming using the
Rectangle class in the AWT. In your program, a Rectangle object
is passed to a constructor for a class. The constructor checks
the width and height of the Rectangle to make sure that the
values are positive. If width and height are positive, then the
volume of the rectangle (width * height) is guaranteed to be
positive. In the program, you also take other steps to ensure
that objects of your class reference only valid Rectangles. For
example, you make the class final. In this way, no subclass can
invalidate the error checking you do in the program.
The code looks like this:
import java.awt.Rectangle;
final class TestRect {
private final Rectangle rect;
public TestRect(Rectangle r) {
// check for width/height at least 1
if (r.width < 1 || r.height < 1) {
throw new IllegalArgumentException();
}
rect = r;
}
public int getVolume() {
return rect.width * rect.height;
}
}
public class CopyDemo1 {
public static void main(String args[]) {
// create a Rectangle and a TestRect

Rectangle r = new Rectangle(0, 0, 5, 10);


TestRect t = new TestRect(r);
// set width to an invalid value and
// compute volume
//r.width = -59;
System.out.println("Volume = " +
t.getVolume());
}
}
So is this code foolproof? Can a bad Rectangle object somehow get
through the checks and be referenced from within TestRect code?
The answer is yes. If you uncomment the line in the CopyDemo1
program that reads "r.width = -59", and then run the program, the
result is:
Volume = -590
which is probably not what you had in mind. The problem is that
when you pass an object as an argument to a method, you're really
passing a reference (pointer) to the object. This means that a
single object can be shared across various parts of a program.
See the December 5, 2000 Tech Tip "Returning Multiple Values From
a Method" for a further explanation of how arguments are passed
to a method.
How can you fix the problem illustrated above? One way is to make
a copy of the Rectangle passed to TestRect:
import java.awt.Rectangle;
final class TestRect {
private final Rectangle rect;
public TestRect(Rectangle r) {
// use copy constructor to copy Rectangle
// object
rect = new Rectangle(r);
// check for valid width/height
if (rect.width < 1 || rect.height < 1) {
throw new IllegalArgumentException();
}
}
public int getVolume() {
return rect.width * rect.height;
}
}
public class CopyDemo2 {
public static void main(String args[]) {
// create Rectangle and TestRect objects

Rectangle r = new Rectangle(0, 0, 5, 10);


TestRect t = new TestRect(r);
// set width to an invalid value
r.width = -59;
// compute volume
System.out.println("Volume = " +
t.getVolume());
}
}
In this approach, you make a copy using the copy constructor for
Rectangle, the constructor that takes a Rectangle argument and
creates a new object with the same values. You make the copy
before validity checking. The validity checking is done on the
copy to avoid subtle problems in multi-threaded programs. One of
these subtle problems is the possibility that another thread
could change the passed-in argument after it has been checked but
before it has been copied.
If you run the CopyDemo2 program, the result is:
Volume = 50
Because you made a copy of the argument within the constructor,
changing the width to -59 outside of the constructor has no
effect on the result. Note that making copies in this way has
some cost in speed and space, especially if you copy big objects.
Another way to apparently solve this problem is to use a clone()
method. But this approach doesn't always work, as the following
example illustrates:
import java.awt.Rectangle;
final class TestRect {
private final Rectangle rect;
public TestRect(Rectangle r) {
// clone the Rectangle object
rect = (Rectangle)r.clone();
// check for valid width/height
if (rect.width < 1 || rect.height < 1) {
throw new IllegalArgumentException();
}
}
public int getVolume() {
return rect.width * rect.height;
}
}
// subclass of Rectangle with bogus clone() method

class MyRectangle extends Rectangle {


public MyRectangle(int x, int y, int w, int h) {
super(x, y, w, h);
}
public Object clone() {
return this;
}
}
public class CopyDemo3 {
public static void main(String args[]) {
// create MyRectangle and TestRect objects
Rectangle r = new MyRectangle(0, 0, 5, 10);
TestRect t = new TestRect(r);
// set width to an invalid value
r.width = -59;
// compute volume
System.out.println("Volume = " +
t.getVolume());
}
}
Rectangle is not a final class, so it can be subclassed. In this
example, the class MyRectangle is defined with a degenerate clone
method, one that simply returns a reference to the object being
cloned. A clone method like this could either be malicious or
just poorly written. In either case, you don't get the desired
result. The output of the CopyDemo3 program is the same as the
CopyDemo1 program:
Volume = -590
The problem of shared access to objects can show up in other
contexts. Suppose you add an accessor method to TestRect
that returns the Rectangle reference stored within the object.
A user can then change the object in an arbitrary way, and the
assumptions you made about the object's validity no longer hold.
Another way of fixing the problem is to pass the width and height
values as integers, and not pass in a Rectangle object reference.
In general, you can also fix the problem by making a class
immutable, that is, objects of the class cannot be changed after
creation. But Rectangle is mutable, and so you need to find
another way to solve the problem.
Another situation where you might want to make a defensive copy
is illustrated in the following program:
class TestNames {
public static final String names[] =
{"red", "green", "blue"};
}

public class CopyDemo4 {


public static void main(String args[]) {
TestNames.names[0] = "purple";
System.out.println(TestNames.names[0]);
}
}
The fact that the names array is final means that you can't
assign to it. But you can still change the value of a particular
array slot. When you run this program, you get the result:
purple
In other words, you have effectively overwritten what is supposed
to be a read-only array.
How do you solve this problem? One way is to copy the array using
clone, another is to create a read-only view of the array using
the Collections Framework. Here's an illustration that shows both
approaches:
import java.util.*;
final class TestNames {
private static final String names[] =
{"red", "green", "blue"};
// return a copy of the names array
public static String[] getNames() {
//return names;
return (String[])names.clone();
}
// return a read-only List view of the array
public static List getNamesList() {
return Collections.unmodifiableList(
Arrays.asList(names));
}
}
public class CopyDemo5 {
public static void main(String args[]) {
// attempt to modify names[0] and then
// print its value
TestNames.getNames()[0] = "purple";
System.out.println(TestNames.getNames()[0]);
// get names array as a read-only list and
// print the value in the first slot
List list = TestNames.getNamesList();
System.out.println(list.get(0));
// attempt to modify the read-only list

//list.set(0, "purple");
}
}
Notice that this program makes the names array private, and
defines an accessor method for it, getNames(). However, simply
going this far does not solve the problem. You can still change
the first slot in the array. So the program clones the array.
After the cloning, if you change the first slot of the array, it
only affects the copy. Of course, cloning a large array has
a speed and space cost attached to it.
Notice too the Collections Framework mechanisms used in the
program. Arrays.asList() creates a list backed by an array.
Collections.unmodifiableList then creates a read-only view of the
list. The array is not copied in this case, but instead a
read-only List view is built on top of it.
These solutions involve either a shallow copy or no copy of the
underlying array. If the array contains mutable objects, then
there is still a problem because errant user code can change the
value of an individual object within the array. In the CopyDemo5
program above, an array of strings is used, and String objects
are immutable.
The need for defensive copying also shows up in other contexts,
such as after an object has been deserialized, or when an object
is added to a Set or Map. For example, one of the properties of
a set is that it contains no duplicate elements. What if a
mutable object is added to a set, and then it later changes so
that equals() is true between that object and another one in the
set? At this point, the set is corrupt. To avoid this problem,
a defensive copy should have been made.
For more information about making defensive copies of objects,
see item 24 "Make defensive copies when needed" in "Effective
Java Programming Language Guide" by Joshua Bloch
(http://java.sun.com/docs/books/effective/).
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING ITERATORS
An iterator is a mechanism used to step through the elements in
a data structure. The interface java.util.Iterator is part of
the Collections Framework, and specifies three methods:
boolean hasNext();

return true if more elements

Object next();

return the next element

void remove();

delete the last element returned

This interface replaces the older java.util.Enumeration. There is


also an interface ListIterator that extends Iterator, and
provides facilities for traversing lists, both forwards and
backwards, and modifying the list during iteration.
Why would you want to use an iterator? Let's look at a simple
example:

import java.util.*;
public class IterDemo1 {
public static void main(String args[]) {
List list = new ArrayList();
list.add("test1");
list.add("test2");
list.add("test3");
//for (int i = 1; i <= list.size(); i++) {
// System.out.println(list.get(i));
//}
Iterator iter = list.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
}
}
In this example, there is a list containing three strings, and
you want to print out the value of each string. You might do
this using the random-access properties of lists. In this case,
you call get() to return arbitrary elements. If you uncomment the
code in IterDemo1 that does this, and then rerun the program, you
get the following results:
test2
test3
Exception in thread "main"
java.lang.IndexOutOfBoundsException:
Index: 3, Size: 3
This shows that the program skipped the first string, and tried
to access index 3 in a list whose valid indices are in the range
0-2. So you get an "off-by-one" error.
This trivial example illustrates one reason why iterators are
important -- they handle the details of data structure traversal.
You don't have to remember, for example, that a list starts at
index 0 instead of 1. This is even more important for complex
structures such as trees, where the right way to traverse the
structure isn't always obvious.
Another advantage of iterators is that they provide a uniform
interface across different data types. For example, here is
a small program that displays the elements in any type of
collection or map:
import java.util.*;
public class IterDemo2 {
static void dumpElements(Object o) {
// if have a Map, pick up the set of
// key/value pairs
if (o instanceof Map) {

o = ((Map)o).entrySet();
}
// dump elements of collection
if (o instanceof Collection) {
Collection c = (Collection)o;
Iterator iter = c.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
}
// give error if not a Collection
else {
System.err.println(
"Not a valid collection");
}
}
public static void main(String args[]) {
// Lists
List list = new ArrayList();
list.add("test1");
list.add("test2");
dumpElements(list);
// Sets
Set set = new TreeSet();
set.add("test3");
set.add("test4");
dumpElements(set);
// Maps
Map map = new HashMap();
map.put("test5key", "test5value");
map.put("test6key", "test6value");
dumpElements(map);
}
}
Lists and Sets extend the Collection interface. Map does not, but
it's possible to obtain the "entry set" for a Map, and then
iterate over that. When you run the IterDemo2 program, the output
is:
test1
test2
test3
test4
test6key=test6value
test5key=test5value
How do you write your own iterator? Let's look at an example, one
that defines an iterator for a BitSet:

import java.util.*;
class BitSetIterator implements Iterator {
// underlying BitSet
private final BitSet bitset;
// current index into BitSet
private int index;
// constructor
public BitSetIterator(BitSet bitset) {
this.bitset = bitset;
}
// return true if has more elements
public boolean hasNext() {
return index < bitset.length();
}
// return next element
public Object next() {
if (index >= bitset.length()) {
throw new NoSuchElementException();
}
boolean b = bitset.get(index++);
return new Boolean(b);
}
// remove element
public void remove() {
throw new UnsupportedOperationException();
}
}
public class IterDemo3 {
public static void main(String args[]) {
// create BitSet and set some bits
BitSet bitset = new BitSet();
bitset.set(1);
bitset.set(19);
bitset.set(47);
// go through and display True/False values
Iterator iter = new BitSetIterator(bitset);
while (iter.hasNext()) {
Boolean b = (Boolean)iter.next();
String tf = (
b.booleanValue() ? "T" : "F");
System.out.print(tf);
}
System.out.println();
}
}
A BitSetIterator object keeps track of the underlying BitSet
object reference, along with an index into the set of bits. The
IterDemo3 program uses BitSet.length() to obtain the highest

numbered bit that is set. The program then iterates until the
index reaches this value. Each element is returned as a Boolean
object. Note that it is legal to call hasNext() repeatedly
without calling next(). It's also legal to call next() without
calling hasNext(). And next() has to handle the situation where
there are no additional elements to return.
When you run this program, the output is:
FTFFFFFFFFFFFFFFFFFTFFFFFFFFFFFFFFFFFFFFFFFFFFFT
What happens if the underlying data structure that the iterator
is working on changes during the iteration? Let's look at a
simple example:
import java.util.*;
class BitSetIterator implements Iterator {
private final BitSet bitset;
private int index;
public BitSetIterator(BitSet bitset) {
this.bitset = bitset;
}
public boolean hasNext() {
return index < bitset.length();
}
public Object next() {
if (index >= bitset.length()) {
throw new NoSuchElementException();
}
boolean b = bitset.get(index++);
return new Boolean(b);
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public class IterDemo4 {
public static void main(String args[]) {
// create Bitset and set bit 0
BitSet bitset = new BitSet();
bitset.set(0);
// set up iterator
Iterator iter = new BitSetIterator(bitset);
//BitSet clone = (BitSet)bitset.clone();
//Iterator iter = new BitSetIterator(clone);
// display True/False bits
while (iter.hasNext()) {
bitset.clear(0);

Boolean b = (Boolean)iter.next();
String tf = (
b.booleanValue() ? "T" : "F");
System.out.print(tf);
}
System.out.println();
}
}
The IterDemo4 program creates a BitSet, sets one bit in it, sets
up an iterator, and calls hasNext(). Then, before next() is
invoked, the program clears the bit that was set. At this point,
no bits are set in the BitSet. So when next() is called, the
result is a NoSuchElementException.
One way to get around this type of problem is to copy the data
structure, and then create an iterator for the copy. In the
IterDemo4 example, there's an alternative approach using clone()
to copy the BitSet (the code for this approach is commented out).
Recall that the tip "Making Defensive Copies of Objects" stated
that it can be dangerous to call clone if you have a subclass
with a malicious clone method. But in the IterDemo4 example,
there's an actual BitSet object, not an object of a subclass.
Here's another example of modifying a list while iterating over
it:
import java.util.*;
public class IterDemo5 {
public static void main(String args[]) {
List list = new ArrayList();
list.add("test1");
list.add("test2");
list.add("test3");
list.add("test4");
Iterator iter = list.iterator();
for (
int index = 0; iter.hasNext(); index++) {
String next = (String)iter.next();
if (next.equals("test2")) {
list.remove(index);
//iter.remove();
}
}
}
}
If you run this program, you get a
ConcurrentModificationException. This is caused by an attempt to
remove an element from the list, while iterating over the list.
The list and iterator implementations work together to detect
this problem. You can, however, safely remove the element by
calling the remove() method defined for the iterator
implementation. This type of iterator goes by the name
"fail-fast".
A final example shows how you can use an iterator to filter the
output of another iterator:

import java.util.*;
class FilterNumbers implements Iterator {
// underlying iterator
private final Iterator iter;
// current Number object
private Object nextnum;
public FilterNumbers(Iterator iter) {
this.iter = iter;
}
public boolean hasNext() {
// if already have a Number object, return
// true
if (nextnum != null) {
return true;
}
// scan for a Number object
while (iter.hasNext()) {
nextnum = iter.next();
if (nextnum instanceof Number) {
return true;
}
}
// didn't find one
nextnum = null;
return false;
}
public Object next() {
// either already have a Number object,
// or must scan for one via hasNext()
if (nextnum == null && !hasNext()) {
throw new NoSuchElementException();
}
// return Number object
Object savenum = nextnum;
nextnum = null;
return savenum;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public class IterDemo6 {

public static void main(String args[]) {


// create a list
List list = new ArrayList();
// add various types of elements to list
list.add(null);
list.add("test1");
list.add(new Integer(37));
list.add(new Double(12.34));
list.add(null);
list.add("test2");
list.add(new Long(12345));
list.add(null);
list.add(new Byte((byte)125));
// filter and display the Number objects
Iterator iter = new FilterNumbers(
list.iterator());
while (iter.hasNext()) {
System.out.println(iter.next());
}
}
}
In the IterDemo6 program, there is an iterator of some sort, and
you want to add another iterator on top of it. The iterator you
want to add finds all the objects of classes that implement the
Number interface, such as Byte and Double.
The filter iterator scans ahead and returns all the Number
objects to the caller. When you run this program, the result
is:
37
12.34
12345
125
For more information about using iterators, see Section 16.2,
Iteration, and section 16.10, Writing Iterator Implementations,
in "The Java(tm) Programming Language Third Edition"
by Arnold, Gosling, and Holmes
http://java.sun.com/docs/books/javaprog/thirdedition/
. . . . . . . . . . . . . . . . . . . . . . .
- NOTE
Sun respects your online time and privacy. The Java Developer
Connection mailing lists are used for internal Sun Microsystems(tm)
purposes only. You have received this email because you elected
to subscribe. To unsubscribe, go to the Subscriptions page
http://developer.java.sun.com/subscription/
uncheck the appropriate checkbox, and click the Update button.

- SUBSCRIBE
To subscribe to a JDC newsletter mailing list, go to the
Subscriptions page
http://developer.java.sun.com/subscription/
choose the newsletters you want to subscribe to, and click
Update.
- FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2001 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
- LINKS TO NON-SUN SITES
The JDC Tech Tips may provide, or third parties may provide,
links to other Internet sites or resources. Because Sun has no
control over such sites and resources, You acknowledge and agree
that Sun is not responsible for the availability of such external
sites or resources, and does not endorse and is not responsible
or liable for any Content, advertising, products, or other
materials on or available from such sites or resources. Sun will
not be responsible or liable, directly or indirectly, for any
damage or loss caused or alleged to be caused by or in connection
with use of or reliance on any such Content, goods or services
available on or through any such site or resource.
This issue of the JDC Tech Tips is written by Glen McCluskey.
JDC Tech Tips
September 4, 2001
Sun, Sun Microsystems, Java, and Java Developer Connection are
trademarks or registered trademarks of Sun Microsystems, Inc. in
the United States and other countries.

Anda mungkin juga menyukai