Array
There are three issues that distinguish arrays from other types of containers: efficiency, type, and the ability to hold primitives. The array is the most efficient way that Java provides to store and randomly access a sequence of object references. The array is a simple linear sequence, which makes element access fast,but you pay for this speed; when you create an array object, its size is fixed and cannot be changed for the lifetime of that array object. In Java, you get bounds checking regardless of whether you're using an array or a container; you'll get a RuntimeException if you exceed the bounds, thus you don't need to check for it in your code.
You need to build only one container, and any Java object will go into that container.(Except for primitives, which can be placed in containers as constants using the Java primitive wrapper classes, or as changeable value by wrapping in your own class)
Arrays are first-class objects
Part of the array object(in fact, the only field or method you can access)is the read-only length member that tells you how many elements can be stored in that array object.
The example shows the various ways that an array can be initialized, and how the array references can be assigned to different array objects. It also shows that arrays of objects and arrays of primitives are almost identical in their use.
The only difference is that arrays of objects hold references, but arrays of primitives hold the primitive values directly.
//c11:ArraySize.java
Weeble[] a; // Local uninitialized variable
Weeble[] b = new Weeble[5]; // Null references
Weeble[] c = new Weeble[4];
for(int i = 0; i < c.length; i++)
if(c[i] == null) // Can test for null reference
c[i] = new Weeble();
// Aggregate initialization:
Weeble[] d = {
new Weeble(), new Weeble(), new Weeble()
};
// Dynamic aggregate initialization:
a = new Weeble[] {
new Weeble(), new Weeble()
};
The expression:
a = d;
shows how you can take a reference that's attached to one array object and assign it to another array object, just as you can do with any other type of object reference. Now both a and d are pointing to the same array object on the heap.
Containers of primitives
Container classes can hold only references to Objects. An array, however, can be created to hold primitives directly, as well as references to Objects. It is possible to use the "wrapper" classes, such as Integer, Double, etc., to place primitive values inside a container, but the wrapper classes for primitives can be awkward to use. In addition, it's much more efficient to create and access an array of primitives than a container of wrapped primitives.
Returning an array
Java takes a similar approach, but you just "return an array". Unlike C++, with Java you never worry about responsibility for that array-it will be arround as long as you need it, and the garbage collector will clean it up when you're done.
//c11:IceCream.java
String[] fl = flavorSet(flavors.length);
for(int j = 0; j < fl.length; j++)
System.out.println("/t" + fl[j]);
The Arrays class
In java.util, which holds a set of static methods that perform utility functions for arrays. There are four basic methods: equals(), to compare two arrays for equality; fill(), to fill an array with a value; sort(), to sort the array; and binarySearch(), to find an element in a sorted array. All of these methods are overloaded for all the primitive types and Objects. In addition, there's a single asList() method that takes any array and turns it into a List container.
Although useful, the Arrays class stops short of being fully functional. For example, it would be nice to be able to easily print the elements of an array without having to code a for loop by hand every time. And as you'll see, the fill() method only takes a single value and places it in the array, so if you wanted, for example, to fill an array with randomly generated numbers, fill() is no help.
Thus it makes sense to supplement the Arrays class with some additional utilties, which will be placed in the package com.bruceeckel.util for convenience. These will print an array of any type and fill an array with values or objects that are created by an object called a generator that you can define.
//: com:bruceeckel:util:Generator.java package com.bruceeckel.util; public interface Generator { Object next(); } ///:~
Arrays2 contains a variety of toString() methods, overloaded for each type. There methods allow you to easily print an array. The toString() code introduces the use of StringBuffer instead of String object, finally, the result is coverted to a String as the return value.
//: com:bruceeckel:util:Arrays2.java
// A supplement to java.util.Arrays, to provide additional
// useful functionality when working with arrays. Allows
// any array to be converted to a String, and to be filled
// via a user-defined "generator" object.
To fill an array of elements using a generator, the fill() method takes a reference to an appropriate generator interface, which has a next() method that will somehow produce an object of the right type(depending on how the interface is implemented). The fill() method simply calls next() until the desired range has been filled. Now you can create any generator by implementing the appropriate interface and use your generator with fill().
//c11:TestArrays2.java
//Test and demonstrate Arrays2 utilities.
Filling an array
The Java standard library Arrays also has a fill() method, but that is rather trivial; It only duplicates a single value into each location, or in the case of objects, copies the same reference into each location.
//c11:FillingArrays.java
//Using java.util.Arrays.fill()
Copying an array
The Java standard library provides a static method, System.arraycopy(), which can make much faster copies of an array than if you use a for loop to perform the copy by hand. System.arraycopy() is overloaded to handle all types.
//c11:CopyingArrays.java
//Using System.arraycopy()
int[] i=new int[7];
int[] j=new int[10];
Arrays.fill(i, 47);
Arrays.fill(j, 99);
System.arraycopy(i, 0, j, 0, i.length);
The arguments to arraycopy() are the source array, the offset into the source array from whence to start copying, the destination array, the offset into the detination array where the copying begins, and the number of elements to copy. Naturally, any violation of the array boundaries will cause an exception.
Comparing arrays
Arrays provides the overloaded method equals() to compare entire arrays for equality. Again, there are overloaded for all the primitives and for Object. To be equal, the arrays must have the same number of elements, and each element must be equivalent to each corresponding element in the other array, using the equals() for each element.
//c11:ComparingArrays.java
//Using Arrays.equals()
String[] s1=new String[5];
Arrays.fill(s1, "Hi");
String[] s2={"Hi", "Hi", "Hi", "Hi", "Hi"};
System.out.println(Arrays.equals(a1, a2); //true
All the elements of s1 point to the same object, but s2 has five unique objects. However, array equality is based on contents(via Object.equals()), so the result is "true".
Array element comparisons
A primary goal of programming design is to "separate things that change from things that stay the same," and here, the code that stays the same is the general sort algorithm, but the thing that changes from on use to next is the way objects are compared. So instead of placing the comparison code into manydifferent sort routines, the technique of the callback is used. With a callback, the part of the code that varies from case to case is separated, and the part of the code that's always the same will call back to the code that changes.
The first way is imparted to a class by implementing the java.lang.Comparable interface. This is a very simple interface with a single method, compareTo(). This method takes another Object as an argument and produces a negative value if the current object is less than the argument, zero if they are equal, and a positive value if the current object is greater than the argument.
//c11:CompType
public int compareTo(Object rv){
int rvi=((CompType)rv).i; //i is class member variable
return (i
}
The Collections class(which we'll look at more later)contains a single Comparator that reverses the natural sorting order. This can be applied easily to the CompType:
//c11:Reverse.java
//The Collections.reverseOrder() Comparator
Arrays.sort(Object[] o, Collections.reverseOrder());
The second starategy design pattern is used. With a strategy, the part of the code that varies is encapsulated(压缩) inside its own class(the strategy object). You hand a strategy object to the code that's always the same, which uses the strategy to fulfill its algorithm. That way, you can make different objects to express different ways of comparison and feed them to the same sorting code. Here, you create a strategy by defining a separate class that implements an interface called Comparator. This has two methods, compare() and equals(). However, you don't have to implement equals() except for special performance needs, because anytime you create a class, it is implicitly inherited from Object, which has an equals(). So you can just use the default Object equals() and satisfy the contract imposed by the interface.
The following Comparator compares CompType object based on their j values rather than their i values:
//c11:ComparatorTest.java
class CompTypeComparator implements Comparator{
public int compare(Object o1, Object o2){
int j1=((CompType)o1).j;
int j2=((CompType)o2).j;
return (j1
}
}
Arrays.sort(Object[] a, new CompTypeComparator());
Sorting an array
Believe it or not, there was no support in Java 1.0 or 1.1 for sorting Strings!
//c11:StringSorting.java
//Sorting an array of Strings
One thing you'll notice about the output in the String sorting algorithm is that it's lexicographic, so it puts all the words starting with uppercase letters first, followed by all the words starting with lowercase letters. You may also want to group the words together regardless of case, and you can do this by defining a Comparator class, thereby overriding the default String Comparable behavior. For reuse, this will be added to the util package.
//: com:bruceeckel:util:AlphabeticComparator.java
// Keeping upper and lowercase letters together.
public class AlphabeticComparator implements Comparator {
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
return s1.toLowerCase().compareTo(s2.toLowerCase());
}
} ///:~
Here's a test using AlphabeticComparator:
//c12:AlphabeticSorting.java
//Keeping upper and lowercase letter together.
Arrays.sort(String[] sa, new AlphabeticComparator());
You shouldn't need to spend any time worrying about performance unlees you profiler points you to the sorting process as a bottleneck.
Searching a sorted array
Once an array is sorted, you can perform a fast search for a particular item by using Arrays.binarySearch(). However, it's very important that you do not try to use binarySearch() on an unsorted array; the results will be unpredictable.
//c11:ArraySearching.java
//Using Arrays.binarySearch()
int location=Arrays.binarySearch(int[] a, int key)
//location is index of the search key.
If it's not found, it produces a negative value representing the place that the element should be inserted if you are maintaining the sorted array be hand. the value produced is
-(insertion point)-1
If the array contains duplicate elements, there is no guarantee which one will be found.
If you have sorted an object array using a Comparator(primitive arrays do not allow sorting with a Comparator), you must include that same Comparator when you perform a binarySearch()(using the overloaded version of the method that's provided).
//c11:AlphabeticSearch.java
//Search with a Comparator.
AlphabeticComparator comp=new AlphabeticComparator();
Arrays.sort(String[] sa, comp);
//The Comparator must be passed to the overloaded binarySearch() as the third argument.
int index=Arrays.binarySearch(sa, sa[10], comp);
Array summary
Your first and most efficient choice to hold a group of objects should be an array, and you're forced into this choice if you want to hold a group of primitives.
Introduction to containers
Collection: a group of individual elements, often with some rule applied to them. A List must hold the elements in a particular sequence, and a Set cannot have any duplicate elements. (A bag, which is not implemented in the Java container library-since Lists provide you with enough of that functionality-has no such rules.)
Map: a group of key-value object pairs. At first glance(乍看), this might seem like it ought to be a Collection of pairs, but when you try to implement it that way the design get awkward, so it's clearer to make it a separate concept. On the other hand, it's convenient to look at portions of a Map by creating a Collection to represent that portion. Thus,
a Map can return a Set of its keys, a Collection of its values, or a Set of pairs.
Maps, like arrays, can easily be expanded to multiple dimension without adding new concepts; you simply make a Map whose value are Maps(and the values of those Maps can be Maps, etc.).
Printing containers
Unlike arrays, the containers print nicely without any help.
//c11:PrintingContainers.java
//Containers print themselves automatically.
System.out.println(ArrayList al);
System.out.println(HashSet hs);
System.out.println(HashMap hm);
List: holds a group of items in a specified sequence.
Set: only allows the adition of one item of each type.
Map: holds key-value pairs, rather like a mini database.
Filling containers
Just like Arrays, there is a companion class called Collections containing static utility methods, including one called fill(). This fill() also just duplicates a single object reference throughout the container, and
also only works for List objects and not Sets or Maps.
//c11:FillingLists.java
//The Collections.fill() method
To be able to create interesting example, here is a complementary Collections2 library(part of com.bruceeckel.util for convenience) with a fill() method that uses a generator to add elements and allows you to specify the number of elements you want to add().
//com.bruceeckel.util.Pair.java
//com.bruceeckel.util.MapGenerator.java
//com.bruceeckel.util.Collections2.java
//To fill any type of container using a generator object
public static void fill(Collection c, Generator gen, int count) {
for(int i=0; i
c.add(gen.next());
}
public static void fill(Map m, MapGenerator gen, int count) {
for(int i=0; i
Pair p=gen.next();
m.put(p.key, p.value);
}
}
This is simple test using the fill() methods and generators:
//c11:FillTest.java
Container disadvantage:unknown type
The "disadvantage" to using the Java containers is that you lose type information when you put an object into a container. The container holds references to Object, which is the root of all the classes, so it holds any type.(
Of course, this doesn't include primitive types, since they aren't real object, and thus, are not inherited form anything.)
- Because the type information is thrown away when you put an object reference into a container, there's no restriction on the type of object that can be put into your container, even if you mean it to hold only, say, cats. Someone could just as easily put a dog into the container.
- Because the type information is lost, the only thing the container knows that it holds is a reference to an object. You must perform a cast to the correct type before you use it.
You'll get a
RuntimeException when you pull the dog reference out of the cat container and try to cast it to a cat.
ArrayList: you can think of ArrayList as "an array that automatically expands itself." Using an ArrayList is straightforward: create one, put object in using add(), and get them out with get() using an index-just like you would with an array, but without the square brackets. ArrayList also has a method size() to let you know how many elements have been added so you don't inadvertently run off the end and cause an exception.
//c11:Cat.java
//c11:Dog.java
//c11:CatsAndDogs.java
//Simple container example
Sometimes it works anyway
The String class has some extra help from the compiler to make it work smoothly. Whenever the compiler expects a String object and it hasn't got one, it will automatically call the toString() method that's defined in Object and can be overridden by any Java class.
//c11:Mouse.java
//Overriding toString()
//c11:MouseTrap.java
static void caughtYa(Object m){
Mouse mouse=(Mouse)m; //Cast from Object
}
//c11:WorksAnyway.java
//In special cases, things just seem to work correctly.
//No cast necessary, automatic call to Object.toString():
System.out.println("Free mouse: "+ mice.get(i));
MouseTrap.caughtYa(mice.get(i));
You can see toString() overridden in Mouse. In the second for loop in main() you find the statement:
System.out.println("Free mouse: "+mice.get(i));
After the "+" sign the compiler expects to see a String object. get() produces an Object, so to get the desired String, the compiler implicitly calls toString(). Unfortunately, you can work this kind of magic only with String; it isn't available for any other type.
A second approach to hiding the cast has been placed inside mouseTrap. So no cast is necessary.
Making a type-conscious(类型自觉) ArrayList
It will accept only your type and produce only your type:
//c11:MouseList.java
//A type-conscious List
private List list=new ArrayList();
public void add(Mouse m){list.add(m);}
public Mouse get(int index){
return (Mouse)list.get(index);
}
public int size(){return list.size();}
Because a MouseList will accept only a Mouse, if you say: mice.add(new Pigeon());
you will get an error message at compile time. Note that no cast is necessary when using get(); it's always a Mouse.
Parameterized types
Iterators
ArrayList is quite flexible, but if you want to start thinking at a higher level, there's a drawback: You need to know the exact type of the container in order to use it. This might not seem bad at first, but what if you start out using an ArrayList, and later on you discover that because of the features you need in the container you actually need to use a Set instead? Or suppose you'd like to write a piece of generic code that doesn't know or care what type of container it's working with, so that it could be used on different types of containers without rewriting that code?
The concept of an iterator (yet another design pattern)can be used to achieve this abstraction. An iterator is an object whose job is to move through a sequence of objects and select each object in that sequence without the client programmer knowing or caring about the underlying structure of that sequence. In addition, an iterator is usually what's called a "light-weight" object: one that's cheap to create. For that reason, you'll often find seemingly strange constraints for iterators; for example, some iterators can move in only one direction.
The Java Iterator is an example of an iterator with these kinds of constraints. There's not much you can do with one except:
- Ask a container to hand you an Iterator using a method called iterator(). This Iterator will be ready to return the first element in the sequence on your first call to its next() method.
- Get the next object in the sequence with next().
- See if there are any more objects in the sequence with hasNext().
- Remove the last element returned by the iterator with iterator with remove().
//c11:CatsAndDogs2.java
//Simple container with Iterator.
Iterator e=cats.iterator(); //cats is a ArrayList
while(e.hasNext())
((Cat)e.next()).id();
Creation of a general-purpose printing method:
//c11:Printer.java
//Using an Iterator.
The example is even more generic, since it implicitly uses the Ojbect.toString() method. The println() method is overloaded for all the primitive types as well as Object; in eache case, a String is automatically produced by calling the appropriate toString() method.
//c11:Hamster.java
//c11:HamsterMaze.java
Printer.printAll(list.iterator());
//You could write printAll() to accept a Collection object instead of an Iterator, but the latter provides better decoupling.
Unintended recursion
Inside ArrayList, for example, the toString() steps through the elements of the ArrayList and calls toString() for each one. Suppose you'd like to print the address of your class. It seems to make sense to simply refer to
this.
//c11:InfinteRecursion.java
//Accidental recursion.
public String toString(){
return "InfiniteRecursion address: "+ this +"/n";
}
public static void main(String[] args){
List v=new ArrayList();
.........
System.out.println(v);
}
The compiler sees a String followed by a '+' and something that's not a String, so it tries to convert
'this' to a String. It does this conversion by calling toString(), which produces a recursive call.
If you really do want to print the address of the object in this case, the solution is to call the
Object toString() method, which does just that. So instead of saying
this, you'd say
super.toString().
Container taxonomy
Collections and Maps may be implemented in different ways according to your programming needs. It's helpful to look at a diagram of the Java containers(as of JDK 1.4)
You'll see that there are really only three container components-Map, List and Set-and only two or three implementations of eache one. The containers that you will generally use most of the time have heavy black lines around them.
The dotted boxes represent interfaces, the dashed boxes represent abstract classes, and the solid boxes are regular(concrete)classes.
The interfaces that are concerned with holding objects are Collection, List, Set, and Map. Ideally, you'll write most of your code to talk to these interfaces, and the only place where you'll specify the precise type you're using is at the point of creation. So you can create a List like this:
List
x=new LinkedList();
Of course, you can also decide to make x a LinkedList(instead of a generic List)and carry the precise type information around with x. The beauty(and the intent)of using the interface is that if you decide you want to change your implementation, all you need to do is change it at the point of creation, like this:
List x=new ArrayList();
The rest of your code can remain untouched.
If you were making your own container, because the containers library contains enough functionality to satisfy your needs virtually all the time. So for our purposes, you can ignore any class that begins with "Abstract" classes.
You'll typically make an object of a concrete class, upcast it to the corresponding interface, and then use the interface throughout the rest of your code. In addition, you do not need to consider the legacy elements when writing new code. Therefore, the diagram can be greatly simplified to look like this:
Note that the WeakHashMap and the JDK1.4 IdentityHashMap are not included on this diagram, because they are special-purpose tools that you will rarely use.
//c11:SimpleCollection.java
//A simple example using Java 2 Collections.
//Upcast because we just want to work with Collection features
Collection c=new ArrayList();
for(int i=0; i<10; i++)
c.add(Integer.toString(i));
Iterator it=c.iterator();
while(it.hasNext())
System.out.println(it.next());
Collection functionality
The following table shows everything you can do with a Collection(not including the methods that automactically come throuth with Object). Maps are not inherited form Collection and will be treated separately.
boolean add(Object)
|
Ensures that the container holds the argument. Returns false if it doesn’t add the argument. (This is an “optional” method, described later in this chapter.)
|
boolean addAll(Collection)
|
Adds all the elements in the argument. Returns true if any elements were added. (“Optional.”)
|
void clear( )
|
Removes all the elements in the container. (“Optional.”)
|
boolean contains(Object)
|
true if the container holds the argument.
|
boolean containsAll(Collection)
|
true if the container holds all the elements in the argument.
|
boolean isEmpty( )
|
true if the container has no elements.
|
Iterator iterator( )
|
Returns an Iterator that you can use to move through the elements in the container.
|
boolean remove(Object)
|
If the argument is in the container, one instance of that element is removed. Returns true if a removal occurred. (“Optional.”)
|
boolean removeAll(Collection)
|
Removes all the elements that are contained in the argument. Returns true if any removals occurred. (“Optional.”)
|
boolean retainAll(Collection)
|
Retains only elements that are contained in the argument (an “intersection” from set theory). Returns true if any changes occurred. (“Optional.”)
|
int size( )
|
Returns the number of elements in the container.
|
Object[] toArray( )
|
Returns an array containing all the elements in the container.
|
Object[] toArray(Object[] a)
|
Returns an array containing all the elements in the container, whose type is that of the array a rather than plain Object (you must cast the array to the right type).
|
Noctic that there's no get() method for random-access element selection. That's because Collection also inclueds Set, which maintains its own internal ordering. Thus, if you want to examine the elements of a Collection, you must use an iterator.
//c11:Collection1.java
//Things you can do with all Collections.
List functionality
There are acutally two types of List: the basic ArrayList, which excels at randomly accessing elements, and the much more powerful LinkedList, which is not designed for fast random access, but has a much more general set of methods.
List (interface)
|
Order is the most important feature of a List; it promises to maintain elements in a particular sequence. List adds a number of methods to Collection that allow insertion and removal of elements in the middle of a List. (This is recommended only for a LinkedList.) A List will produce a ListIterator, and by using this you can traverse the List in both directions, as well as insert and remove elements in the middle of the List.
|
ArrayList*
|
A List implemented with an array. Allows rapid random access to elements, but is slow when inserting and removing elements from the middle of a list. ListIterator should be used only for back-and-forth traversal of an ArrayList, but not for inserting and removing elements, which is expensive compared to LinkedList.
|
LinkedList
|
Provides optimal sequential access, with inexpensive insertions and deletions from the middle of the List. Relatively slow for random access. (Use ArrayList instead.) Also has addFirst( ), addLast( ), getFirst( ), getLast( ), removeFirst( ), and removeLast( ) (which are not defined in any interfaces or base classes) to allow it to be used as a stack, a queue, and a deque.
|
The methods in the following example each cover a different group of activities: things that every list can do (basicTest()), moving around with an Iterator(iterMotion()) versus changing things with an Iterator(iterManipulation()), seeing the effects of List manipulation(testVisual()), and operations available only to LinkedLists.
//c11:List1.java
//Things you can do with Lists.
Remember that a container is only a storage cabinet to hold objects. If that cabinet solves all of your needs, it doesn't really matter how it is implemented(a basic concept with most types of objects). If you're working in a programming environment that has built-in overhead due to other factors, then the cost difference between an ArrayList and LinkedList might not matter. You might need only one type of sequence. You can even imagine the "perfect" container abstraction, which can automatically change its underlying implementation according to the way it is used.
Making a stack from a LinkedList
A stack is sometimes referred to as a "last-in, first-out(LIFO) container.
The LinkedList has methods that directly implement stack functionality, so you can also just use a LinkedList rather than making a stack class.
//c11:StackL.java
//Making a stack from a LinkedList
public void push(Object v) { list.addFirst(v);}
public Object top() {return list.getFirst();}
public Object pop() {return list.removeFirst();}
.......
System.out.println(stack.top());
System.out.println(stack.top());
System.out.println(stack.pop());
//results: "CHAD","CHAD","CENTRAL AFRICAN REPUBLIC"
Making a queue form a LinkedList
A queue is a "first-in, first-out"(FIFO) container.
//c11:Queue.java
//Making a queue from a LinkedList.
public void put(Object v) {list.addFirst(v);}
public Object get() {return list.removeLast();}
public boolean isEmpty() {return list.isEmpty();}
......
You can also easily create a deque(double-ended queue) from a LinkedList. This is like a queue, but you can add and remove elements from either end.
Set functionality
Set has exactly the same interface as Collection, so there isn't any extra functionality like there is with the two different Lists. Instead, the Set is exactly a Collection-it just has different behavior.
Set (interface)
|
Each element that you add to the Set must be unique; otherwise, the Set doesn’t add the duplicate element. Objects added to a Set must define equals( ) to establish object uniqueness. Set has exactly the same interface as Collection. The Set interface does not guarantee that it will maintain its elements in any particular order.
|
HashSet*
|
For Sets where fast lookup time is important. Objects must also define hashCode( ).
|
TreeSet
|
An ordered Set backed by a tree. This way, you can extract an ordered sequence from a Set.
|
LinkedHashSet (JDK 1.4)
|
Has the lookup speed of a HashSet, but maintains the order in which you add the elements (the insertion order), internally using a linked list. Thus, when you iterate through the Set, the results appear in insertion order.
|
//c11:Set1.java
//Things you can do with Sets. demonstrates the behavior that makes a Set unique
You'll see that the Set has accepted only one instance of each value.
Each Set has a different way of Storing elements so they can be located later.(
TreeSet keeps elements sorted into a red-black tree data structure, whereas
HashSet uses a hashing function, which is designed specifically for rapid lookups.
LinkedHashSet uses hashing internally for lookup speed, but appears to maintain elements in insertion order using a linked list.)
When creating your own types, be aware that a Set needs a way to maintain a storage order, which means that you must implement the Comparable interface and define the compareTo() method.
//c11:Set2.java
//Putting your own type in a Set
The form for the definitions for equals() and hashCode() will be described later in this chapter. You must define an equals() in both cases, but the hasCode() is absolutely necessary only if the class will be placed in a HashSet(which is likely, since that should generally be your first choice as a Set implementation). However, as a programming style, you should always override hashCode() when you override equals(). This process will be fully detailed later in this chapter.
In the compareTo(), note that I did not use the "simple and obvious" from
return i - i2. Although this is a common programming error, it would only work properly if i and i2 were "unsigned"
ints.
SortedSet
If you have a SortedSet( of which TreeSet is the only one available),the elements are guaranteed to be in sorted order, which allows additional functionality to be provided with these methods in the SortedSet interface:
Comparator comparator(): Produces the Comparator used for this Set, or null for natural ordering.
Object first(): Produces the lowest element.
Object last(): Produces the highest element.
SortedSet subSet(fromElement, toElement): Produces a view of this Set with elements from fromElement, inclusive, to toElement, exclusive.
SortedSet headSet(toElement): Produces a view of this Set with elements less than toElement.
SortedSet tailSet(fromElement): Produces a view of this Set with elements greater than or equal to fromElement.
//c11:SortedSetDemo.java
//What you can do with a TreeSet.
Note that SortedSet means "sorted according to the comparison function of the object," not "insertion order".
Map functionality
The concept shows up in Java as the Map interface. The
put(Object key, Object value) method adds a value(the thing you want)and associates it with a key(the thing you look it up with).
get(Object key)produces the value given the corresponding key. You can also test a Map to see if it contains a key or a value with
containsKey() and
containsValue().
The standard Java library contains different types of Maps: HashMap, TreeMap, LinkedHashMap, WeakHashMap, and IdentityHashMap.
Map (interface)
|
Maintains key-value associations (pairs) so you can look up a value using a key.
|
HashMap*
|
Implementation based on a hash table. (Use this instead of Hashtable.) Provides constant-time performance for inserting and locating pairs. Performance can be adjusted via constructors that allow you to set the capacity and load factor of the hash table.
|
LinkedHashMap (JDK 1.4)
|
Like a HashMap, but when you iterate through it, you get the pairs in insertion order, or in least-recently-used (LRU) order. Only slightly slower than a HashMap, except when iterating, where it is faster due to the linked list used to maintain the internal ordering.
|
TreeMap
|
Implementation based on a red-black tree. When you view the keys or the pairs, they will be in sorted order (determined by Comparable or Comparator, discussed later). The point of a TreeMap is that you get the results in sorted order. TreeMap is the only Map with the subMap( ) method, which allows you to return a portion of the tree.
|
WeakHashMap
|
A map of weak keys that allow objects referred to by the map to be released; designed to solve certain types of problems. If no references outside the map are held to a particular key, it may be garbage collected.
|
IdentityHashMap (JDK 1.4)
|
A hash map that uses == instead of equals( ) to compare keys. Only for solving special types of problems; not for general use.
|
Hashing is the most commonly used way to store elements in a map. Sometions you'll need to know the details of how hashing works, so we'll look at that a little later.
//c11:Map1.java
//Things you can do with Maps.
The
keySet() method produces a Set backed by the keys in the Map. Similar treatment is given to
values(), which produces a Collection containing all the values in the Map.
//c11:Statistics.java
//Simple demonstration of HashMap
Map hm = new HashMap();
for(int i=0; i<10000; i++){
Integer r=new Integer(rand.nextInt(20));
if(hm.containsKey(r))
((Counter)hm.get(r)).i++;
else
hm.put(r, new Counter());
}
You might wonder at the necessity of the class Counter, which seems like it doesn't even have the functionality of the wrapper class Integer. Why not use int or Integer? Well, you can't use an int because all of the containers can hold only Object references. After you've seen containers, the wrapper classes might begin to make a little more sense to you, since you can't put any of the primitive types in containers. However, the only thing you can do with the Java wrappers is initialize them to a particular value and read that value. That is, there's no way to change a value once a wrapper object has been created. This make the Integer wrapper immediately useless to solve the problem, so we're forced to create a new class that does satisfy the need.
SortedMap
TreeMap is the only one available of a SortedMap
Comparator comaparator(): Produces the comparator used for this Map, or null for natural ordering.
Object firstKey(): Produces the lowest key.
Object lastKey(): Produces the highest key.
SortedMap subMap(fromKey, toKey): Produces a view of this Map with keys from fromKey, inclusive, to toKey, exclusive.
SortedMap headMap(toKey): Produces a view of this Map with keys less than toKey.
SortedMap tailMap(fromKey): Produces a view of this Map with keys greater than or equal to fromKey.
//c11:SimplePairGenerator.java
//Example that's similar to SortedSetDemo.java and shows this additional behavior of TreeMaps
LinkedHashMap
A LinkedHashmap can be configured in the constructor to use a least-recently-used(LRU) algorithm based on accesses,
so elements that haven't been accessed(and thus are candidates for removal) appear at the front of the list. This allows easy creation of programs that do periodic cleanup in order to save space.
//c11:LinkedHashMapDemo.java
//What you can do with a LinkedHashMap
linkedMap=new LinkedHashMap(16, 0.75f, true);
for(int i = 0; i < 7; i++) // Cause accesses:
linkedMap.get(SimplePairGenerator.gen.items[i].key);
System.out.println(linkedMap);
//Result: "{eight=H, nine=I, ten=J, one=A, two=B, " ,"three=C, four=D, five=E, six=F, seven=G}"
Hashing and hash codes
HashMap worked because it has all the necessary wiring to make it behave correctly as a key. But a common pitfall occurs with HashMaps when you create your own classes to be sued as keys.
//c11:Groundhog.java
//Looks plausible, but doesn't work as a HashMap key.
//c11:Prediction.java
//Predicting the weather with groundhogs.
//c11:SpringDetector.java
//What will the weather be?
//Uses a Groundhog or class derived from GroundHog:
public static void detectSpring(Class groundHogClass) throws Exception{
Constructor ghog=groundHogClass.getConstructor(new Class[] {int.class});
Map map=new HashMap();
for(int i=0;i<10;i++)
map.put(ghog.newInstance(new Object[]{new Integer(i)}), new Prediction());
System.out.println("map = "+ map);
......
}
A HashMap is filled with Groundhogs and their associated Predictions. The HashMap is printed so that you can see it has been filled. Then a Groundhog with an identity number of 3 is used as a key to lookup the prediction for Groundhog #3(which you can see must be in the Map).
It seems simple enough, but it doesn't work. The problem is that Groundhog is inherited from the common root class Object(which is what happens if you don't specify a base class, thus all classes are ultimately inherited from Object). It is Object's hashCode()method that is used to generate the hash code for each object, and by default it just uses the address of its object. Thus, the first instance of Groundhog(3) does not produce a hash code equal to the hash code for the second instance of Groundhog(3) that we tried to use as a lookup.
You need to write appropriate override for hashCode() and equals().
A proper equals() must satisfy the following five conditions:
- Reflexive: for any x, x.equals(x) should return true.
- Symmetric: for any x and y, x.equals(y)should return true if only if y.equals(x) return true.
- Transitive: for any x, y, and z, if x.equals(y)returns true and y.equals(z) return true, then x.equals(z) should return true.
- Consistent: for any x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the object is modified.
- For any non-null x, x.equals(null)should return false.
Again, the default Object.equals() simply compares object addresses, so one Groundhog(3) is not equal to another Groundhog(3).
//c11:Groundhog2.java
//A class that's used as a key in a HashMap must override hasCode() and equals().
public class Groundhog2 extends Groundhog{
public Groundhog2( int n) {super(n);}
public int hashCode() {return number; }
public boolean equals(Object o){
return (o instanceof Groundhog2) && (number==((Groundhog2)o).number);
//using the instanceof keyword, which was explained in Chapter 10
}
}
//c11:SpringDecector2.java
//A working key.
public static void main(String[] args) throws Exception{
SpringDetector.detectSpring(Groundhog2.class);
......
}
......
The hashCode() is not required to return a unique identifier, but the equals() method must be able to strictly determine whether two objects are equivalent.
Understanding hashCode()
If you want to look up an object using another ojbect. But you can accomplish this with a TreeSet or TreeMap, too. To do so, the Map.entrySet() method must be supplied to produce a set of Map.Entry objects. In order for it to be placed in a TreeSet, it must implement equals() and be Comparable
//c11:MPair.java
//A new type of Map.Entry.
The follow example implements a Map using a pair of ArrayLists
//c11:SlowMap.java
//A Map implemented with ArrayLists.
Hashing allows the lookup to happen quickly. Since the bottleneck is in the speed of the key lookup, one of the solutions to the problem could be to keep the keys sorted and then then use Collections.binarySearch() to perform the lookup(an exercise at the end of this chapter will walk you through this process).
The array points not directly to a value because it must has fixed size, but instead to a list of values. There values are searched in a linear fashion using the equals() method. Of course, this aspect of the search is much slower, but if the hash function is good, there will only be a few values in each slot. so instead of searching through the entire list, you quickly jump to a slot where you have to compare a few entries to entries to find the value. This is much faster, which is why the HashMap is so quick.
//c11:SimpleHashMap.java
// A demonstration hashed Map.
private static final int SZ = 997;
private LinkedList[] bucket = new LinkedList[SZ];
HashMap performance factors
Capacity: The number of buckets in the table.
Initial capacity: The number of buckets when the table is created. Hashmap and HashSet have constructors that allow you to specify the initial capacity.
Size: The number of entries currently in table.
Load factor: size/capacity
Rehashing: when the load factor is reached, the container will automatically increase the capacity(the number of buckets) by roughly doubling it and will redistribute the existing objects into the new set of buckets.
If you know that you'll be storing many entries in a HashMap, creating it with an appropriately large initial capacity will prevent the overhead of automatic rehashing.
Overriding hashCode()
In Effective Java(Addison-Wesley 2001), Joshua bloch gives a basic recipe for generating a decent hashCode():
- Store some constant nonzero value, say 17, in a intvariable called result.
- For each significant field f in your object(each field taken into account by the equals(), that is), calculate and int hash code c for the field.
Field type
|
Calculation
|
boolean
|
c = (f ? 0 : 1)
|
byte, char, short, or int
|
c = (int)f
|
long
|
c = (int)(f ^ (f >>>32))
|
float
|
c = Float.floatToIntBits(f);
|
double
|
long l = Double.doubleToLongBits(f);c = (int)(l ^ (l >>> 32))
|
Object, where equals( ) calls equals( ) for this field
|
c = f.hashCode( )
|
Array
|
Apply above rules to each element |
- Combine the hash code(s) computed above: result=37 * result + c;
- Return result;
- Look at the resulting hashCode() and make sure that equal instances have equal hash codes.
//c11:ContedString.java
//Creating a good hashCode().
public int hashCode() {
int result=17;
result=37*result + s.hashCode();
result=37*result + id;
return result;
}
CountedString includes a String and an id that represents the number of CountedString objects that contain an identical String. The counting is accomplished in the constructor by iterating through the static ArrayList where all the Strings are stored.
Both hashCode() and equals() produce results based on both fields; if they were just based on the String alone or the id alone, there would be duplicate matches for distinct value.
Holding references
The java.lang.ref library contains a set of classes that allow greater flexibility in garbage collection. Especially useful when you have large objects that may cause memory exhaustion. There are three classes inherited from the abstract class Reference: SoftReference, WeakReference, and PhantomReference. Each of these provides a different level of indirection for the garbage collector if the object in question is only reachable through one of these Reference objects.
You might also have a reference to an object that has a reference to the object in question; there could be many intermediate links. If an object is reachable, the garbage collector cannot release it because it's still in use by your program. If an object isn't reachable, there's no way for your program to use it, so it's safe to garbage collect that object.
You use Reference objects when you want to continue to hold onto a reference to that object; you want to be able to reach that object, but you also want to allow the garbage collector to release that object. Thus, you have a way to go on using the object, but if memory exhaustion is imminent, you allow that object to be released.