Thinking in java——Generics Applying a method to a sequence

来源:互联网 发布:数据库实验报告 编辑:程序博客网 时间:2024/05/11 00:20

Reflection provides some interesting possibilities, but it relegates all the type checking to run time, and is thus undesirable in many situations. If you can achieve compile-time type checking, that's usually more desirable. But is it possible to have compile-time type checking and latent typing?

Let's look at an example that explores the problem. Suppose you want to create an apply( ) method that will apply any method to every object in a sequence. This is a situation where interfaces don't seem to fit. You want to apply any method to a collection of objects, and interfaces constrain you too much to describe "any method." How do you do this in Java? 

Initially, we can solve the problem with reflection, which turns out to be fairly elegant because of Java SE5 varargs: 

//: generics/Apply.java// {main: ApplyTest}import static net.mindview.util.Print.print;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;public class Apply {public static <T, S extends Iterable<? extends T>> void apply(S seq,Method f, Object... args) {try {for (T t : seq)f.invoke(t, args);} catch (Exception e) {// Failures are programmer errorsthrow new RuntimeException(e);}}}class Shape {public void rotate() {print(this + " rotate");}public void resize(int newSize) {print(this + " resize " + newSize);}}class Square extends Shape {}class FilledList<T> extends ArrayList<T> {public FilledList(Class<? extends T> type, int size) {try {for (int i = 0; i < size; i++)// Assumes default constructor:add(type.newInstance());} catch (Exception e) {throw new RuntimeException(e);}}}class ApplyTest {public static void main(String[] args) throws Exception {List<Shape> shapes = new ArrayList<Shape>();for (int i = 0; i < 10; i++)shapes.add(new Shape());Apply.apply(shapes, Shape.class.getMethod("rotate"));Apply.apply(shapes, Shape.class.getMethod("resize", int.class), 5);List<Square> squares = new ArrayList<Square>();for (int i = 0; i < 10; i++)squares.add(new Square());Apply.apply(squares, Shape.class.getMethod("rotate"));Apply.apply(squares, Shape.class.getMethod("resize", int.class), 5);Apply.apply(new FilledList<Shape>(Shape.class, 10),Shape.class.getMethod("rotate"));Apply.apply(new FilledList<Shape>(Square.class, 10),Shape.class.getMethod("rotate"));SimpleQueue<Shape> shapeQ = new SimpleQueue<Shape>();for (int i = 0; i < 5; i++) {shapeQ.add(new Shape());shapeQ.add(new Square());}Apply.apply(shapeQ, Shape.class.getMethod("rotate"));}} /* (Execute to see output) */// :~

In Apply, we get lucky because there happens to be an Iterable interface built into Java which is used by the Java containers library. Because of this,the apply( ) method can accept anything that implements the Iterable interface, which includes all the Collection classes such as List. But it can also accept anything else, as long as you make it Iterable—for example, the SimpleQueue class defined here and used above in main( ): 

//: generics/SimpleQueue.java// A different kind of container that is Iterableimport java.util.*;public class SimpleQueue<T> implements Iterable<T> {  private LinkedList<T> storage = new LinkedList<T>();  public void add(T t) { storage.offer(t); }  public T get() { return storage.poll(); }  public Iterator<T> iterator() {    return storage.iterator();  }} ///:~

In Apply.java, exceptions are converted to RuntimeExceptions because there's not much of a way to recover from exceptions—they really do represent programmer errors in this case. 

Note that I had to put in bounds and wildcards in order for Apply and FilledList to be used in all desired situations. You can experiment by taking these out, and you'll discover that some applications of Apply and FilledList will not work. 

FilledList presents a bit of a quandary. In order for a type to be used, it must have a default (no-arg) constructor. Java has no way to assert such a thing at compile time, so it becomes a runtime issue. A common suggestion to ensure compile-time checking is to define a factory interface that has a method that generates objects; then FilledList would accept that interface rather than the "raw factory" of the type token. The problem with this is that all the classes you use in FilledList must then implement your factory interface.Alas, most classes are created without knowledge of your interface, and therefore do not implement it. Later, I'll show one solution using adapters. 

But the approach shown, of using a type token, is perhaps a reasonable trade-off (at least as a first-cut solution). With this approach, using something like FilledList is just easy enough that it may be used rather than ignored. Of course, because errors are reported at run time, you need confidence that these errors will appear early in the development process.

Note that the type token technique is recommended in the Java literature,such as Gilad Bracha's paper Generics in the Java Programming Language,where he notes, "It's an idiom that's used extensively in the new APIs for manipulating annotations, for example." However, I've discovered some inconsistency in people's comfort level with this technique; some people strongly prefer the factory approach, which was presented earlier in this chapter.  

Also, as elegant as the Java solution turns out to be, we must observe that the use of reflection (although it has been improved significantly in recent versions of Java) may be slower than a non-reflection implementation, since so much is happening at run time. This should not stop you from using the solution, at least as a first cut (lest you fall sway to premature optimization),but it's certainly a distinction between the two approaches. 

When you don't happen to have the right interface

The above example benefited because the Iterable interface was already built in, and was exactly what we needed. But what about the general case,when there isn't an interface already in place that just happens to fit your needs? 

For example, let's generalize the idea in FilledList and create a parameterized fill( ) method that will take a sequence and fill it using a Generator. When we try to write this in Java, we run into a problem,because there is no convenient "Addable" interface as there was an Iterabl interface in the previous example. So instead of saying,"anything that you can call add( ) for," you must say, "subtype of Collection." The resulting code is not particularly generic, since it must be constrained to work with Collection implementations. If I try to use a class that doesn't implement Collection, my generic code won't work. Here's what it looks like: 

//: generics/Fill.java// Generalizing the FilledList idea// {main: FillTest}import java.util.*;// Doesn't work with "anything that has an add()." There is// no "Addable" interface so we are narrowed to using a// Collection. We cannot generalize using generics in// this case.public class Fill {  public static <T> void fill(Collection<T> collection,  Class<? extends T> classToken, int size) {    for(int i = 0; i < size; i++)      // Assumes default constructor:      try {        collection.add(classToken.newInstance());      } catch(Exception e) {        throw new RuntimeException(e);      }  }}class Contract {  private static long counter = 0;  private final long id = counter++;  public String toString() {    return getClass().getName() + " " + id;  }}class TitleTransfer extends Contract {}class FillTest {  public static void main(String[] args) {    List<Contract> contracts = new ArrayList<Contract>();    Fill.fill(contracts, Contract.class, 3);    Fill.fill(contracts, TitleTransfer.class, 2);    for(Contract c: contracts)      System.out.println(c);    SimpleQueue<Contract> contractQueue =      new SimpleQueue<Contract>();    // Won't work. fill() is not generic enough:    // Fill.fill(contractQueue, Contract.class, 3);  }} /* Output:Contract 0Contract 1Contract 2TitleTransfer 3TitleTransfer 4*///:~

This is where a parameterized type mechanism with latent typing is valuable,because you are not at the mercy of the past design decisions of any particular library creator, so you do not have to rewrite your code every time you encounter a new library that didn't take your situation into account (thus the code is truly "generic"). In the above case, because the Java designers (understandably) did not see the need for an "Addable" interface, we are constrained within the Collection hierarchy, and SimpleQueue, even though it has an add( ) method, will not work. Because it is thus constrained to working with Collection, the code is not particularly "generic." With latent typing, this would not be the case.  

Simulating latent typing with adapters

So Java generics don't have latent typing, and we need something like latent typing in order to write code that can be applied across class boundaries (that is, "generic" code). Is there some way to get around this limitation? 
What would latent typing accomplish here? It means that you could write code saying, "I don't care what type I'm using here as long as it has these methods." In effect, latent typing creates an implicit interface containing the desired methods. So it follows that if we write the necessary interface by hand (since Java doesn't do it for us), that should solve the problem. 
Writing code to produce an interface that we want from an interface that we have is an example of the Adapter design pattern. We can use adapters to adapt existing classes to produce the desired interface, with a relatively small amount of code. The solution, which uses the previously defined Coffee hierarchy, demonstrates different ways of writing adapters: 
//: generics/Fill2.java// Using adapters to simulate latent typing.// {main: Fill2Test}import generics.coffee.*;import java.util.*;import net.mindview.util.*;import static net.mindview.util.Print.*;interface Addable<T> { void add(T t); }public class Fill2 {  // Classtoken version:  public static <T> void fill(Addable<T> addable,  Class<? extends T> classToken, int size) {    for(int i = 0; i < size; i++)      try {        addable.add(classToken.newInstance());      } catch(Exception e) {        throw new RuntimeException(e);      }  }  // Generator version:  public static <T> void fill(Addable<T> addable,  Generator<T> generator, int size) {    for(int i = 0; i < size; i++)      addable.add(generator.next());  }}// To adapt a base type, you must use composition.// Make any Collection Addable using composition:class AddableCollectionAdapter<T> implements Addable<T> {  private Collection<T> c;  public AddableCollectionAdapter(Collection<T> c) {    this.c = c;  }  public void add(T item) { c.add(item); }}// A Helper to capture the type automatically:class Adapter {  public static <T>  Addable<T> collectionAdapter(Collection<T> c) {    return new AddableCollectionAdapter<T>(c);  }}// To adapt a specific type, you can use inheritance.// Make a SimpleQueue Addable using inheritance:class AddableSimpleQueue<T>extends SimpleQueue<T> implements Addable<T> {  public void add(T item) { super.add(item); }}class Fill2Test {  public static void main(String[] args) {    // Adapt a Collection:    List<Coffee> carrier = new ArrayList<Coffee>();    Fill2.fill(      new AddableCollectionAdapter<Coffee>(carrier),      Coffee.class, 3);    // Helper method captures the type:    Fill2.fill(Adapter.collectionAdapter(carrier),      Latte.class, 2);    for(Coffee c: carrier)      print(c);    print("----------------------");    // Use an adapted class:    AddableSimpleQueue<Coffee> coffeeQueue =      new AddableSimpleQueue<Coffee>();    Fill2.fill(coffeeQueue, Mocha.class, 4);    Fill2.fill(coffeeQueue, Latte.class, 1);    for(Coffee c: coffeeQueue)      print(c);  }} /* Output:Coffee 0Coffee 1Coffee 2Latte 3Latte 4----------------------Mocha 5Mocha 6Mocha 7Mocha 8Latte 9*///:~

Fill2 doesn't require a Collection as Fill did. Instead, it only needs something that implements Addable, and Addable has been written just for Fill—it is a manifestation of the latent type that I wanted the compiler to make for me. 
In this version, I've also added an overloaded fill( ) that takes a Generator rather than a type token. The Generator is type-safe at compile time: The compiler ensures that you pass it a proper Generator, so no exceptions can be thrown. 
The first adapter, AddableCollectionAdapter, works with the base type Collection, which means that any implementation of Collection can be used. This version simply stores the Collection reference and uses it to implement add( ). 
If you have a specific type rather than the base class of a hierarchy, you can write somewhat less code when creating your adapter by using inheritance, as you can see in AddableSimpleQueue. 
In Fill2Test.main( ), you can see the various types of adapters at work.First, a Collection type is adapted with AddableCollectionAdapter. A second version of this uses a generic helper method, and you can see how the generic method captures the type so it doesn't have to be explicitly written— this is a convenient trick that produces more elegant code. 
Next, the pre-adapted AddableSimpleQueue is used. Note that in both cases the adapters allow the classes that previously didn't implement Addable to be used with Fill2.fill( ). 
Using adapters like this would seem to compensate for the lack of latent typing, and thus allow you to write genuinely generic code. However, it's an extra step and something that must be understood both by the library creator and the library consumer, and the concept may not be grasped as readily by less experienced programmers. By removing the extra step, latent typing makes generic code easier to apply, and this is its value. 

0 0
原创粉丝点击