Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 108 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
108
Dung lượng
1,37 MB
Nội dung
} } /* Output: Woof! Sitting Click! Clank! Mime cannot speak Pretending to sit *///:~ Here, the classes are completely disjoint and have no base classes (other than Object) or interfaces in common. Through reflection, CommunicateReflectively.perform( ) is able to dynamically establish whether the desired methods are available and call them. It is even able to deal with the fact that Mime only has one of the necessary methods, and partially fulfills its goal. Applying a method to a sequence 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 java.lang.reflect.*; import java.util.*; import static net.mindview.util.Print.*; 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 errors throw 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> { Generics 519 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 Iterable import 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. 520 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Generics 521 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 tradeoff (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, 9 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. Exercise 40: (3) Add a speak( ) method to all the pets in typeinfo.pets. Modify Apply.java to call the speak( ) method for a heterogeneous collection of Pet. 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 Iterable 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.*; 9 See citation at the end of this chapter. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com // 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 0 Contract 1 Contract 2 TitleTransfer 3 TitleTransfer 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. 522 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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); } Generics 523 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com } // 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 0 Coffee 1 Coffee 2 Latte 3 Latte 4 Mocha 5 Mocha 6 Mocha 7 Mocha 8 Latte 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. 524 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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. Exercise 41: (1) Modify Fill2.java to use the classes in typeinfo.pets instead of the Coffee classes. Generics 525 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 526 Thinking in Java Bruce Eckel Using function objects as strategies This final example will create truly generic code using the adapter approach described in the previous section. The example began as an attempt to create a sum over a sequence of elements (of any type that can be summed), but evolved into performing general operations using afunctional style of programming. If you just look at the process of trying to add objects, you can see that this is a case where we have common operations across classes, but the operations are not represented in any base class that we can specify—sometimes you can even use a’+’ operator, and other times there may be some kind of "add" method. This is generally the situation that you encounter when trying to write generic code, because you want the code to apply across multiple classes— especially, as in this case, multiple classes that already exist and that we have no ability to "fix." Even if you were to narrow this case to subclasses of Number, that superclass doesn’t include anything about "addability." The solution is to use the Strategy design pattern, which produces more elegant code because it completely isolates "the thing that changes" inside of a function object. 10 A function object is an object that in some way behaves like a function—typically, there’s one method of interest (in languages that support operator overloading, you can make the call to this method look like an ordinary method call). The value of function objects is that, unlike an ordinary method, they can be passed around, and they can also have state that persists across calls. Of course, you can accomplish something like this with any method in a class, but (as with any design pattern) the function object is primarily distinguished by its intent. Here the intent is to create something that behaves like a single method that you can pass around; thus it is closely coupled with—and sometimes indistinguishable from—the Strategy design pattern. As I’ve found with a number of design patterns, the lines get kind of blurry here: We are creating function objects which perform adaptation, and they are being passed into methods to be used as strategies. Taking this approach, I added the various kinds of generic methods that I had originally set out to create, and more. Here is the result: //: generics/Functional.java import java.math.*; import java.util.concurrent.atomic.*; import java.util.*; import static net.mindview.util.Print.*; // Different types of function objects: interface Combiner<T> { T combine(T x, T y); } interface UnaryFunction<R,T> { R function(T x); } interface Collector<T> extends UnaryFunction<T,T> { T result(); // Extract result of collecting parameter } interface UnaryPredicate<T> { boolean test(T x); } public class Functional { // Calls the Combiner object on each element to combine // it with a running result, which is finally returned: public static <T> T 10 You will sometimes see these called functors. I will use the term function object rather than^unctor, as the term "functor" has a specific and different meaning in mathematics. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com reduce(Iterable<T> seq, Combiner<T> combiner) { Iterator<T> it = seq.iterator(); if(it.hasNext()) { T result = it.next(); while(it.hasNext()) result = combiner.combine(result, it.next()); return result; } // If seq is the empty list: return null; // Or throw exception } // Take a function object and call it on each object in // the list, ignoring the return value. The function // object may act as a collecting parameter, so it is // returned at the end. public static <T> Collector<T> forEach(Iterable<T> seq, Collector<T> func) { for(T t : seq) func.function(t); return func; } // Creates a list of results by calling a // function object for each object in the list: public static <R,T> List<R> transform(Iterable<T> seq, UnaryFunction<R,T> func) { List<R> result = new ArrayList<R>(); for(T t : seq) result.add(func.function(t)); return result; } // Applies a unary predicate to each item in a sequence, // and returns a list of items that produced "true": public static <T> List<T> filter(Iterable<T> seq, UnaryPredicate<T> pred) { List<T> result = new ArrayList<T>(); for(T t : seq) if(pred.test(t)) result.add(t); return result; } // To use the above generic methods, we need to create // function objects to adapt to our particular needs: static class IntegerAdder implements Combiner<Integer> { public Integer combine(Integer x, Integer y) { return x + y; } } static class IntegerSubtracter implements Combiner<Integer> { public Integer combine(Integer x, Integer y) { return x - y; } } static class BigDecimalAdder implements Combiner<BigDecimal> { public BigDecimal combine(BigDecimal x, BigDecimal y) { return x.add(y); } } static class BigIntegerAdder implements Combiner<BigInteger> { public BigInteger combine(BigInteger x, BigInteger y) { return x.add(y); Generics 527 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com } } static class AtomicLongAdder implements Combiner<AtomicLong> { public AtomicLong combine(AtomicLong x, AtomicLong y) { // Not clear whether this is meaningful: return new AtomicLong(x.addAndGet(y.get())); } } // We can even make a UnaryFunction with an "ulp" // (Units in the last place): static class BigDecimalUlp implements UnaryFunction<BigDecimal,BigDecimal> { public BigDecimal function(BigDecimal x) { return x.ulp(); } } static class GreaterThan<T extends Comparable<T>> implements UnaryPredicate<T> { private T bound; public GreaterThan(T bound) { this.bound = bound; } public boolean test(T x) { return x.compareTo(bound) > 0; } } static class MultiplyingIntegerCollector implements Collector<Integer> { private Integer val = 1; public Integer function(Integer x) { val *= x; return val; } public Integer result() { return val; } } public static void main(String[] args) { // Generics, varargs & boxing working together: List<Integer> li = Arrays.asList(1, 2, 3, 4, 5, 6, 7); Integer result = reduce(li, new IntegerAdder()); print(result); result = reduce(li, new IntegerSubtracter()); print(result); print(filter(li, new GreaterThan<Integer>(4))); print(forEach(li, new MultiplyingIntegerCollector()).result()); print(forEach(filter(li, new GreaterThan<Integer>(4)), new MultiplyingIntegerCollector()).result()); MathContext mc = new MathContext(7); List<BigDecimal> lbd = Arrays.asList( new BigDecimal(1.1, mc), new BigDecimal(2.2, mc), new BigDecimal(3.3, mc), new BigDecimal(4.4, mc)); BigDecimal rbd = reduce(lbd, new BigDecimalAdder()); print(rbd); print(filter(lbd, new GreaterThan<BigDecimal>(new BigDecimal(3)))); // Use the prime-generation facility of BigInteger: List<BigInteger> lbi = new ArrayList<BigInteger>(); 528 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... 0.19 0.52 0.27 0. 26 0.05 0.8 0. 76 Float: 0.53 0. 16 0.53 0.4 0.49 0.25 0.8 0.11 0.02 0.8 Long: 767 4 8804 8950 78 26 4322 8 96 8033 2984 2344 5810 Integer: 8303 3141 7138 60 12 9 966 868 9 7185 69 92 57 46 39 76 Short: 3358 20592 284 267 91 12834 -8092 1 365 6 29324 -1423 5327 String: bkInaMe sbtWHkj UrUkZPg wsqPzDy CyRFJQA HxxHvHq XumcXZJ oogoYWM NvqeuTp nXsgqia Character: x x E A J J m z M s Byte: -60 -17 55 -14... = new ArrayList(); for(int i = 0; i < 5; i++) sphereList.add(new BerylliumSphere()); print(sphereList); print(sphereList.get(4)); int[] integers = { 0, 1, 2, 3, 4, 5 }; print(Arrays.toString(integers)); print(integers[4]); List intList = new ArrayList( Arrays.asList(0, 1, 2, 3, 4, 5)); intList.add(97); print(intList); print(intList.get(4)); } } /* Output: [Sphere... String[] a9 = new String[size]; Arrays.fill(a1, true); print("a1 = " + Arrays.toString(a1)); Arrays.fill(a2, (byte)11); print("a2 = " + Arrays.toString(a2)); Arrays.fill(a3, ‘x’); print("a3 = " + Arrays.toString(a3)); Arrays.fill(a4, (short)17); print("a4 = " + Arrays.toString(a4)); Arrays.fill(a5, 19); print("a5 = " + Arrays.toString(a5)); Arrays.fill(a6, 23); print("a6 = " + Arrays.toString(a6));... int[] primitive(Integer[] in) { int[] result = new int [in. length]; for(int i = 0; i < in. length; i++) result[i] = in[ i]; return result; } public static long[] primitive(Long[] in) { long[] result = new long [in. length]; for(int i = 0; i < in. length; i++) result[i] = in[ i]; return result; } public static float[] primitive(Float[] in) { float[] result = new float [in. length]; for(int i = 0; i < in. length; i++)... primitive(Byte[] in) { byte[] result = new byte [in. length]; for(int i = 0; i < in. length; i++) result[i] = in[ i]; return result; } public static short[] primitive(Short[] in) { short[] result = new short [in. length]; for(int i = 0; i < in. length; i++) result[i] = in[ i]; 552 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com return result; } public static int[]... http://www.simpopdf.com Autoboxing also works with array initializers: //: arrays/AutoboxingArrays .java import java. util.*; public class AutoboxingArrays { public static void main(String[] args) { Integer[][] a = { // Autoboxing: { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, { 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 }, { 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 }, { 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 }, }; System.out.println(Arrays.deepToString(a));... size)); print("a7 = " + Arrays.toString(a7)); double[] a8 = ConvertTo.primitive(Generated.array( Double.class, new RandomGenerator.Double(), size)); print("a8 = " + Arrays.toString(a8)); } } /* Output: a1 = [true, false, true, false, false, true] a2 = [104, -79, - 76, 1 26, 33, -64 ] a3 = [Z, n, T, c, Q, r] a4 = [-13408, 2 261 2, 15401, 15 161 , -28 466 , -1 260 3] a5 = [7704, 7383, 77 06, 575, 8410, 63 42] a6 = [ 767 4,... using new Here’s a three-dimensional array allocated in a new expression: //: arrays/ThreeDWithNew .java import java. util.*; public class ThreeDWithNew { public static void main(String[] args) { // 3-D array with fixed length: int[][][] a = new int[2][2][4]; System.out.println(Arrays.deepToString(a)); } } /* Output: [[[0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0]]] *///:~ 540 Thinking in Java. .. String() {} public String(int length) { super(length); } } public static class Short implements Generator { public java. lang.Short next() { return (short)r.nextInt(); } } public static class Integer implements Generator { private int mod = 10000; public Integer() {} public Integer(int modulo) { mod = modulo; } public java. lang.Integer next() { return r.nextInt(mod); } }... (Trailing comma is optional in both cases) print("a.length = " + a.length); print("b.length = " + b.length); print("c.length = " + c.length); print("d.length = " + d.length); a = d; print("a.length = " + a.length); // Arrays of primitives: int[] e; // Null reference int[] f = new int[5]; // The primitives inside the array are // automatically initialized to zero: print("f: " + Arrays.toString(f)); int[] . creating your adapter by using inheritance, as you can see in AddableSimpleQueue. 524 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com In. http://www.simpopdf.com 5 26 Thinking in Java Bruce Eckel Using function objects as strategies This final example will create truly generic code using the adapter approach described in the previous. class IntegerAdder implements Combiner<Integer> { public Integer combine(Integer x, Integer y) { return x + y; } } static class IntegerSubtracter implements Combiner<Integer>