Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Algorithms (2006)

.pdf
Скачиваний:
255
Добавлен:
17.08.2013
Размер:
9.67 Mб
Скачать

Chapter 13

assertEquals(E.getValue(), _map.get(E.getKey())); assertEquals(5, _map.size());

assertNull(_map.set(F.getKey(), F.getValue())); assertEquals(F.getValue(), _map.get(F.getKey())); assertEquals(6, _map.size());

}

public void testSetExistingKey() { assertEquals(4, _map.size());

assertEquals(C.getValue(), _map.set(C.getKey(), “cvalue2”)); assertEquals(“cvalue2”, _map.get(C.getKey())); assertEquals(4, _map.size());

}

public void testDeleteExisting() { assertEquals(4, _map.size());

assertEquals(B.getValue(), _map.delete(B.getKey())); assertFalse(_map.contains(B.getKey())); assertEquals(3, _map.size());

assertEquals(A.getValue(), _map.delete(A.getKey())); assertFalse(_map.contains(A.getKey())); assertEquals(2, _map.size());

assertEquals(C.getValue(), _map.delete(C.getKey())); assertFalse(_map.contains(C.getKey())); assertEquals(1, _map.size());

assertEquals(D.getValue(), _map.delete(D.getKey())); assertFalse(_map.contains(D.getKey())); assertEquals(0, _map.size());

}

public void testDeleteNonExisting() { assertEquals(4, _map.size()); assertNull(_map.delete(E.getKey())); assertEquals(4, _map.size()); assertNull(_map.delete(F.getKey())); assertEquals(4, _map.size());

}

public void testClear() { assertEquals(4, _map.size()); assertFalse(_map.isEmpty());

_map.clear();

assertEquals(0, _map.size()); assertTrue(_map.isEmpty());

assertFalse(_map.contains(A.getKey())); assertFalse(_map.contains(B.getKey()));

324

Maps

assertFalse(_map.contains(C.getKey())); assertFalse(_map.contains(D.getKey()));

}

public void testIteratorForwards() { checkIterator(_map.iterator());

}

public void testIteratorBackwards() {

checkIterator(new ReverseIterator(_map.iterator()));

}

private void checkIterator(Iterator i) { List entries = new LinkedList();

for (i.first(); !i.isDone(); i.next()) { Map.Entry entry = (Map.Entry) i.current();

entries.add(new DefaultEntry(entry.getKey(), entry.getValue()));

}

try { i.current(); fail();

}catch (IteratorOutOfBoundsException e) {

//expected

}

assertEquals(4, entries.size()); assertTrue(entries.contains(A)); assertTrue(entries.contains(B)); assertTrue(entries.contains(C)); assertTrue(entries.contains(D));

}

}

How It Works

The class AbstractMapTestCase extends TestCase in order to make it a proper JUnit-compatible test class. It also defines some sample entries and a map for testing. The map is assigned a value in the setUp() method, which runs just prior to each test case, and the first four keys from the sample entries are associated with their corresponding value in the map.

The abstract createMap() method is to be implemented in each concrete subclass of AbstractMapTestCase, and is where you will create the specific instance of the map to be tested:

package com.wrox.algorithms.maps;

import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.ReverseIterator; import com.wrox.algorithms.lists.LinkedList;

import com.wrox.algorithms.lists.List; import junit.framework.TestCase;

325

Chapter 13

public abstract class AbstractMapTestCase extends TestCase {

private static final Map.Entry A = new DefaultEntry(“akey”, “avalue”); private static final Map.Entry B = new DefaultEntry(“bkey”, “bvalue”); private static final Map.Entry C = new DefaultEntry(“ckey”, “cvalue”); private static final Map.Entry D = new DefaultEntry(“dkey”, “dvalue”); private static final Map.Entry E = new DefaultEntry(“ekey”, “evalue”); private static final Map.Entry F = new DefaultEntry(“fkey”, “fvalue”);

private Map _map;

protected void setUp() throws Exception { super.setUp();

_map = createMap();

_map.set(C.getKey(), C.getValue()); _map.set(A.getKey(), A.getValue()); _map.set(B.getKey(), B.getValue()); _map.set(D.getKey(), D.getValue());

}

protected abstract Map createMap();

...

}

The contains() method should return true for any key that is contained within the map and false otherwise. You know that four of the sample keys do exist, so in testContainsExisting() you check to ensure that contains() returns true for each one:

public void testContainsExisting() { assertTrue(_map.contains(A.getKey())); assertTrue(_map.contains(B.getKey())); assertTrue(_map.contains(C.getKey())); assertTrue(_map.contains(D.getKey()));

}

Conversely, testContainsNonExisting() ensures that contains() returns false for keys that are known not to exist:

public void testContainsNonExisting() { assertFalse(_map.contains(E.getKey())); assertFalse(_map.contains(F.getKey()));

}

Next, testGetExisting() verifies that get() returns the correct value for each key that was assigned in the setUp() method:

public void testGetExisting() { assertEquals(A.getValue(), _map.get(A.getKey())); assertEquals(B.getValue(), _map.get(B.getKey())); assertEquals(C.getValue(), _map.get(C.getKey())); assertEquals(D.getValue(), _map.get(D.getKey()));

}

326

Maps

Similarly, testGetNonExisting() verifies that null is returned for a few keys known not to exist in the map:

public void testGetNonExisting() { assertNull(_map.get(E.getKey())); assertNull(_map.get(F.getKey()));

}

The testSetNewKey() method verifies whether you can successfully retrieve stored values. After first checking the initial size of the map, two key/value pairs are added. Each time set() is called, the return value is checked to ensure it is null, indicating there was no existing value; get() is called to ensure that the value is associated with the new key, and the size is checked to ensure it has increased by one:

public void testSetNewKey() { assertEquals(4, _map.size());

assertNull(_map.set(E.getKey(), E.getValue())); assertEquals(E.getValue(), _map.get(E.getKey())); assertEquals(5, _map.size());

assertNull(_map.set(F.getKey(), F.getValue())); assertEquals(F.getValue(), _map.get(F.getKey())); assertEquals(6, _map.size());

}

The testSetExistingKey() method first checks the initial size of the map. Then set() is called to associate a new value with an existing key, and the return value is checked to ensure that it matches the original value. A lookup is then performed to ensure that the new value is associated with the key. Finally, the size is checked against the original to verify that it hasn’t changed:

public void testSetExistingKey() { assertEquals(4, _map.size());

assertEquals(C.getValue(), _map.set(C.getKey(), “cvalue2”)); assertEquals(“cvalue2”, _map.get(C.getKey())); assertEquals(4, _map.size());

}

Next, the testDeleteExisting() method calls delete() to remove each of the keys added in setUp(), and the return value is checked to ensure that it is correct. The contains() method is then called to verify that the key no longer exists, and the size is checked to ensure it has been decremented:

public void testDeleteExisting() { assertEquals(4, _map.size());

assertEquals(B.getValue(), _map.delete(B.getKey())); assertFalse(_map.contains(B.getKey())); assertEquals(3, _map.size());

assertEquals(A.getValue(), _map.delete(A.getKey())); assertFalse(_map.contains(A.getKey())); assertEquals(2, _map.size());

327

Chapter 13

assertEquals(C.getValue(), _map.delete(C.getKey())); assertFalse(_map.contains(C.getKey())); assertEquals(1, _map.size());

assertEquals(D.getValue(), _map.delete(D.getKey())); assertFalse(_map.contains(D.getKey())); assertEquals(0, _map.size());

}

After first checking the size of map, the testDeleteNonExisting() method calls delete() to remove a key known not to exist. The return value is then tested to make sure it is null, and the size is checked once again to ensure there has been no change:

public void testDeleteNonExisting() { assertEquals(4, _map.size()); assertNull(_map.delete(E.getKey())); assertEquals(4, _map.size()); assertNull(_map.delete(F.getKey())); assertEquals(4, _map.size());

}

The testClear() method first ensures that the map isn’t already empty. The clear() method is then called and the size is rechecked to confirm that it has been reset to zero. Finally, contains() is called for each of the original keys to verify that none still exist:

public void testClear() { assertEquals(4, _map.size()); assertFalse(_map.isEmpty());

_map.clear();

assertEquals(0, _map.size()); assertTrue(_map.isEmpty());

assertFalse(_map.contains(A.getKey())); assertFalse(_map.contains(B.getKey())); assertFalse(_map.contains(C.getKey())); assertFalse(_map.contains(D.getKey()));

}

Almost all of the work for testing iterator() is done in checkIterator(). This method iterates over all the entries in the map. Each time an entry is returned, the key and value are used to create a DefaultEntry, which is then added to the list. The list is then checked to ensure that the size matches

the expected number of entries; and that each of the expected entries exists. Why not just add the entries as they are returned from the map itself? The answer is rather subtle, and something to be aware of, not only in this instance but when working with interfaces in general.

The contains() method is called to determine whether the expected entries exist in the list, which in turn calls equals() to determine whether the entry being searched for matches any in the list. Now recall that Map.Entry is an interface, so the entries returned from the iterator may be of any class that implements Map.Entry, not necessarily DefaultEntry. This means that there is no guarantee that the

328

Maps

equals() method has been implemented or that it will even work as needed when comparing itself with a DefaultEntry. (For the authoritative discussion on equals(), see Effective Java [Block, 2001].) Therefore, rather than cross your fingers and hope for the best, you’ve instead taken the key/value pairs and added them to the list as instances of DefaultEntry, which you know implements the equals() method and is the same class as the expected entries:

private void checkIterator(Iterator i) { List entries = new LinkedList();

for (i.first(); !i.isDone(); i.next()) { Map.Entry entry = (Map.Entry) i.current();

entries.add(new DefaultEntry(entry.getKey(), entry.getValue()));

}

try { i.current(); fail();

} catch (IteratorOutOfBoundsException e) { // expected

}

assertEquals(4, entries.size()); assertTrue(entries.contains(A)); assertTrue(entries.contains(B)); assertTrue(entries.contains(C)); assertTrue(entries.contains(D));

}

Then, to test forwards iteration, testIteratorForwards() simply obtains an iterator from the map and hands it off to checkIterator():

public void testIteratorForwards() { checkIterator(_map.iterator());

}

Finally, to test reverse iteration, testIteratorBackwards() wraps the iterator in a ReverseIterator (from Chapter 2) before calling checkIterator(). In this way, all calls to first() and next() will be redirected to last() and previous(), respectively — meaning you don’t have to write a separate set of tests:

public void testIteratorBackwards() {

checkIterator(new ReverseIterator(_map.iterator()));

}

A List Map

In the next Try It Out section you create a map that uses a list as the underlying storage mechanism. The implementation is very straightforward and easy to follow; and although it isn’t particularly efficient, it is useful for small data sets.

329

Chapter 13

Try It Out

Testing and Implementing a List Map

Start by creating the ListMapTest as follows:

package com.wrox.algorithms.maps;

public class ListMapTest extends AbstractMapTestCase { protected Map createMap() {

return new ListMap();

}

}

Then create the ListMap class itself:

package com.wrox.algorithms.maps;

import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;

public class ListMap implements Map {

private final List _entries = new LinkedList();

public Object get(Object key) { DefaultEntry entry = entryFor(key);

return entry != null ? entry.getValue() : null;

}

public Object set(Object key, Object value) { DefaultEntry entry = entryFor(key);

if (entry != null) {

return entry.setValue(value);

}

_entries.add(new DefaultEntry(key, value)); return null;

}

public Object delete(Object key) { DefaultEntry entry = entryFor(key); if (entry == null) {

return null;

}

_entries.delete(entry); return entry.getValue();

}

public boolean contains(Object key) { return entryFor(key) != null;

}

public void clear() { _entries.clear();

330

Maps

}

public int size() {

return _entries.size();

}

public boolean isEmpty() { return _entries.isEmpty();

}

public Iterator iterator() { return _entries.iterator();

}

private DefaultEntry entryFor(Object key) { Iterator i = iterator();

for (i.first(); !i.isDone(); i.next()) { DefaultEntry entry = (DefaultEntry) i.current(); if (entry.getKey().equals(key)) {

return entry;

}

}

return null;

}

}

How It Works

Because the test cases themselves have already been created, all you do for ListMapTest is extend AbstractMapTest and implement createMap() to return an instance of your ListMap class:

package com.wrox.algorithms.maps;

public class ListMapTest extends AbstractMapTestCase { protected Map createMap() {

return new ListMap();

}

}

With the tests in place, you then move on to the map implementation itself in the form of the ListMap class. This class holds nothing more than the list that will be used for storing the contained entries. The clear(), size(), isEmpty(), and iterator() methods all just delegate to the methods of the same name:

package com.wrox.algorithms.maps;

import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;

public class ListMap implements Map {

private final List _entries = new LinkedList();

public void clear() {

331

Chapter 13

_entries.clear();

}

public int size() {

return _entries.size();

}

public boolean isEmpty() { return _entries.isEmpty();

}

public Iterator iterator() { return _entries.iterator();

}

...

}

The private entryFor() method obtains an entry (if any exists) for a given key. This method simply iterates through all the entries in the list, comparing the key of the entry to the search key. If a matching entry is found, it is returned; otherwise, null is returned to indicate that no such entry exists:

private DefaultEntry entryFor(Object key) { Iterator i = iterator();

for (i.first(); !i.isDone(); i.next()) { DefaultEntry entry = (DefaultEntry) i.current(); if (entry.getKey().equals(key)) {

return entry;

}

}

return null;

}

Based on this, you implement get() to return the associated value. In this method, entryFor() is called to find the appropriate entry for the given key. If one is found (entry != null), then the value is returned; otherwise, null is returned to indicate that no such key exists:

public Object get(Object key) { DefaultEntry entry = entryFor(key);

return entry != null ? entry.getValue() : null;

}

You also implement contains() in a similar manner by attempting to find an entry for a given key and returning true only if one exists:

public boolean contains(Object key) { return entryFor(key) != null;

}

The set() method first calls entryFor() to determine whether an entry already exists for the given key. If an entry is found, then its value is updated and the old value returned. If no matching entry was found, however, then a new one is added to the end of the underlying list, and null is returned accordingly:

332

Maps

public Object set(Object key, Object value) { DefaultEntry entry = entryFor(key);

if (entry != null) {

return entry.setValue(value);

}

_entries.add(new DefaultEntry(key, value)); return null;

}

Lastly, delete() is called to remove a key/value pair from the map. As with the previous methods, delete() starts by calling entryFor(). In this case, however, if no entry is found, null is returned to indicate that the key did not exist; otherwise, the entry is deleted from the underlying list and the value is returned to the caller:

public Object delete(Object key) { DefaultEntry entry = entryFor(key); if (entry == null) {

return null;

}

_entries.delete(entry); return entry.getValue();

}

There you have it — your first map implementation. The code for the ListMap class is very simple, and most of the work is performed by the underlying list. In this case, the simplicity comes at a price: The performance of ListMap is dependent on the performance of the underlying list, which is O(N). This isn’t particularly efficient, but for relatively small data sets, a list-based map may be enough.

A Hash Map

The next type of map you will create is based on a hash tables (covered in Chapter 11). At this point, you might like to refresh your understanding of hashing concepts — in particular, hash tables that use buckets — and the code for the BucketingHashtable class.

In the next Try It Out section, you start by creating the tests that will ensure the correct behavior before creating the hash map implementation proper.

Try It Out

Testing and Implementing a Hash Map

Create the test class as follows:

package com.wrox.algorithms.maps;

public class HashMapTest extends AbstractMapTestCase { protected Map createMap() {

return new HashMap();

}

}

333