|
10/28 Recitation 8 - Tying Up Loose Ends
- The checkRep() Paradox
- The Comparator Contract
- Anonymous Classes
- MouseAdapter
Earlier in the semester, we told you
when to call checkRep() ,
but there is a subtle problem with our rules which I like to call
The checkRep() Paradox.
The Paradox occurs when you have a private method that may be invoked
while the RI is broken, but the private method needs to call
an innocuous observer method during its execution.
For example, suppose we tried to use a checkRep()
method in the implementation of Map as follows:
public class ExampleMap implements java.util.Map {
/*
* RI =
* (1) size is equal to the total number of Entry
* objects in, or linked to by members of, the buckets array
* (2) size >= (0.25 * buckets.length)
* (3) size <= (0.75 * buckets.length)
*/
private int size;
private Entry[] buckets;
private static final boolean CHECKREP_ENABLED = true;
private void checkRep() {
if (!CHECKREP_ENABLED) return;
if (size < (buckets.length / 4)) {
throw new RuntimeException("number of buckets " + buckets.length +
" is too large for number of entries: " + size);
}
// checks for other conditions of RI
}
public ExampleMap() {
size = 0;
buckets = new Entry[8];
checkRep();
}
public int size() {
checkRep();
return size;
}
public Object remove(Object key) {
checkRep();
try {
if (!containsKey(key)) return null;
Entry oldEntry = removeEntryForKey(key);
return oldEntry.getValue();
} finally {
checkRep();
}
}
private Entry removeEntryForKey(Object key) {
// removes entry from buckets and returns it
// side-effect: updates size field if Entry removed
// calls checkResize() before returning
}
private void checkResize() {
if (size() < (buckets.length / 4)) {
rehash(buckets.length / 2);
} else if (size() > (3 * buckets.length / 4)) {
rehash(buckets.length * 2);
}
}
private void rehash(int newSize) {
// resizes the number of buckets
// and reassigns each Entry in the old buckets array
// to its new bucket
}
// other public methods required to implement Map
}
At first, this looks reasonable, but what would happen when we did the following:
Map map = new ExampleMap();
map.put("1", "Mississippi");
map.put("2", "Mississippi");
map.remove("2");
It turns out that we would get a RuntimeException because the RI was violated
by removeEntryForKey() before it called checkResize()
which invokes size() . Since size() is called while
the RI is violated, it will throw a RuntimeException.
Obviously this is pretty frustrating because you're aware that there may be a problem with size() – that's why you're calling checkResize()
in the first place! So what are the possible solutions?
- Temporarily Disable checkRep()
One option would be to rewrite
checkResize() as follows:
private void checkResize() {
boolean oldCheckRepValue = CHECKREP_ENABLED;
try {
CHECKREP_ENABLED = false;
if (size() < (buckets.length / 4)) {
rehash(buckets.length / 2);
} else if (size() > (3 * buckets.length / 4)) {
rehash(buckets.length * 2);
}
} finally {
CHECKREP_ENABLED = oldCheckRepValue;
}
}
This is verbose and inelegant. Furthermore, if the contents of your
try block invoke other methods where it is important
that checkRep() is enabled, then you may allow subtle bugs
to creep into your code. This is dangerous and is not a good choice.
- Use the
size Field Directly
Another option is to refer to size instead of invoking
size() to avoid the side effect of invoking checkRep() .
This is somewhat undesirable because it eliminates the abstraction that
the size() method provides. For example, size()
does something simple right now, but if it is changed to do something complicated
later, then referencing size directly may miss those effects,
which may be important. Furthermore, it is better to use size() to
access the field in case someone extends ExampleMap and
overrides the size() method to do something important every time
the size field is accurate. For something as simple as
size , this may not seem so critical, but it is possible to create
other scenarios in which this may be a problem.
- Create a Private Method to Access
size()
The best alternative seems to be to create a private accessor method
for size that is used by size() :
public int size() {
checkRep();
return _size();
}
private int _size() {
return size;
}
private void checkResize() {
if (_size() < (buckets.length / 4)) {
rehash(buckets.length / 2);
} else if (_size() > (3 * buckets.length / 4)) {
rehash(buckets.length * 2);
}
}
This doesn't help those clients who override ExampleMap.size() ,
but it does allow all accesses to size to be updated in one place
within ExampleMap . This is certainly cleaner than toggling the
CHECKREP_ENABLED field, and it eliminates duplicate code.
This is likely the best solution to the Paradox.
Hopefully you will not encounter this problem, but when you do,
it is a puzzling one, so hopefully this solution will help you out.
2. The Comparator Contract
Below is the code for a panel called SurprisePanel
that will display a message to the user when she clicks on it:
public class SurpriseComponent extends JComponent {
private String message;
SurpriseComponent(final String secretMessage) {
addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
JOptionPane.showMessageDialog(getParent(), secretMessage);
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
});
}
}
The code shown in boldface constitutes what is called an anonymous class.
It creates an object that implements the MouseListener interface
without having a named, concrete class that explicitly implements the interface.
Since interfaces are not classes, it may seem odd that we can write
new MouseListener() , but this turns out to be a convenient construct:
1. It eliminates the need to create arbitrary classes.
Suppose Java did not have anonymous classes.
In that case, every time we wanted to add a MouseListener to a GUI
widget, we would have to create a new class that implements the
MouseListener interface.
2. It minimizes the scope of local variables (as Bloch advocates).
If there were no anonymous classes, and small helper classes had to be created
to implement listener interfaces all over your code,
then it would be possible to instantiate those helper classes from parts of your
code that have no business instantiating that helper class.
Using an anonymous class limits the scope of the class,
making it impossible to instantiate outside the block in which it is created.
This also makes your code easier to maintain
because it is one less private class whose API he needs to be kept up to date.
Note that the parameter to the SurpriseComponent is
declared final (the code would not compile otherwise).
Normally, the secretMessage parameter would fall out of scope
once the end of the block is reached; however, the anonymous
MouseListener still exists when the end of the block is reached
(because SurpriseComponent has a reference to it),
so a reference to secretMessage needs to be persisted in some way.
One way to resolve this problem is to declare the parameter final .
Alternatively, secretMessage could be assigned to the private
field message and then message could be used as
the parameter to showMessageDialog . Though this does not involve the
use of the final keyword, the reference to message
is persisted because it is a field of SurprisePanel .
Another option would be to create a final local variable:
SurpriseComponent(String secretMessage) {
final String m = secretMessage;
addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
JOptionPane.showMessageDialog(getParent(), m);
}
...
Doing something like this may be necessary if the anonymous class needs to
make a reference to an object that is created inside the method
rather than to an object that is passed as a parameter to the method.
The code above could be simplified greatly by the use of the
MouseAdapter class:
public class SurpriseComponent extends JComponent {
SurpriseComponent(final String secretMessage) {
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
JOptionPane.showMessageDialog(getParent(), secretMessage);
}
});
}
}
MouseAdapter trivially implements the MouseListener
interface; that is, it implements MouseListener by creating
an empty method body for each method in MouseListener .
Thus, you can create a MouseListener by first extending
MouseAdapter , and then overriding only the methods of
MouseListener that interest you. As you can see in the example
above, this saves us the trouble of listing a number of methods that do not
get used.
This trick is used often in the java.awt.event package:
KeyAdapter , MouseAdapter , MouseMotionAdapter ,
and WindowAdapter are just a few examples of classes with trivial
interface implementations. Each adapter has an analogous
listener interface in the same package that it implements.
Ironically, this is not a true use of the Adapter design pattern,
as these adapters do not convert the interface of one class to that of another.
|