The beginning of this chapter introduced the idea of writing code that can be applied as generally as possible. To do this, we need ways to loosen the constraints on the types that our code works with, without losing the benefits of static type checking. We are then able to write code that can be used in more situations without change—that is, more "generic" code.

Java generics appear to take a further step in this direction. When you are writing or using generics that simply hold objects, the code works with any type (except for primitives, although as you've seen, autoboxing smoothes this over). Or, put another way, "holder" generics are able to say, "I don't care what type you are." Code that doesn't care what type it works with can indeed be applied everywhere, and is thus quite "generic."

As you've also seen, a problem arises when you want to perform manipulations on generic types (other than calling Object methods), because erasure requires that you specify the bounds of the generic types that may be used, in order to safely call specific methods for the generic objects in your code. This is a significant limitation to the concept of "generic" because you must constrain your generic types so that they inherit from particular classes or implement particular interfaces. In some cases you might end up using an ordinary class or interface instead, because a bounded generic might be no different from specifying a class or interface.

One solution that some programming languages provide is called latent typing or structural typing. A more whimsical term is duck typing, as in, "If it walks like a duck and talks like a duck, you might as well treat it like a duck." Duck typing has become a fairly popular term, possibly because it doesn't carry the historical baggage that other terms do. 
Generic code typically only calls a few methods on a generic type, and a language with latent typing loosens the constraint (and produces more generic code) by only requiring that a subset of methods be implemented, not a particular class or interface. Because of this, latent typing allows you to cut across class hierarchies, calling methods that are not part of a common 
interface. So a piece of code might say, in effect, "I don't care what type you are as long as you can speak( ) and sit( )." By not requiring a specific type,your code can be more generic. 
 
Latent typing is a code organization and reuse mechanism. With it you can write a piece of code that can be reused more easily than without it. Code organization and reuse are the foundational levers of all computer programming: Write it once, use it more than once, and keep the code in one place. Because I am not required to name an exact interface that my code 
operates upon, with latent typing I can write less code and apply it more easily in more places. 
Two examples of languages that support latent typing are Python (freely downloadable from www.Python.org) and C++(The Ruby and Smalltalk languages also support latent typing).Python is a dynamically typed language (virtually all the type checking happens at run time) and C++ is a statically typed language (the type checking happens at compile time), so latent typing does not require either static or dynamic type checking.

If we take the above description and express it in Python, it looks like this: 

Python uses indentation to determine scope (so no curly braces are needed),and a colon to begin a new scope. A '#' indicates a comment to the end of the line, like'// ' in Java. The methods of a class explicitly specify the equivalent of the this reference as the first argument, called self by convention.Constructor calls do not require any sort of "new" keyword. And Python allows regular (non-member) functions, as evidenced by perform( ).
In perform(anything), notice that there is no type for anything, and anything is just an identifier. It must be able to perform the operations that perform( ) asks of it, so an interface is implied. But you never have to explicitly write out that interface—it's latent. perform( ) doesn't care about the type of its argument, so I can pass any object to it as long as it supports 
the speak( ) and sit( ) methods. If you pass an object to perform( ) that does not support these operations, you'll get a runtime exception. 
We can produce the same effect in C++: 
 //: generics/DogsAndRobots.cpp

 class Dog {
public:
void speak() {}
void sit() {}
void reproduce() {}
}; class Robot {
public:
void speak() {}
void sit() {}
void oilChange() {
}; template<class T> void perform(T anything) {
anything.speak();
anything.sit();
} int main() {
Dog d;
Robot r;
perform(d);
perform(r);
} ///:~

In both Python and C++, Dog and Robot have nothing in common, other than that they happen to have two methods with identical signatures. From a type standpoint, they are completely distinct types. However, perform( ) doesn't care about the specific type of its argument, and latent typing allows it to accept both types of object.

C++ ensures that it can actually send those messages. The compiler gives you an error message if you try to pass the wrong type (these error messages have historically been terrible and verbose, and are the primary reason that C++ templates have a poor reputation). Although they do it at different times—C++ at compile time, and Python at run time—both languages ensure that types cannot be misused and are thus considered to be strongly typed(Because you can use casts, which effectively disable the type system, some people argue that C++ is weakly typed, but that's extreme. It's probably safer to say that C++ is "strongly typed with a trap door.").Latent typing does not compromise strong typing.

Because generics were added to Java late in the game, there was no chance that any kind of latent typing could be implemented, so Java has no support for this feature. As a result, it initially seems that Java's generic mechanism is "less generic" than a language that supports latent typing(The implementation of Java's generics using erasure is sometimes referred to as second-class generic types.).For instance, if we try to implement the above example in Java, we are forced to use a class or an interface and specify it in a bounds expression:

//: generics/Performs.java

public interface Performs {
void speak();
void sit();
} ///:~
//: generics/DogsAndRobots.java
// No latent typing in Java
import typeinfo.pets.*;
import static net.mindview.util.Print.*; class PerformingDog extends Dog implements Performs {
public void speak() { print("Woof!"); }
public void sit() { print("Sitting"); }
public void reproduce() {}
} class Robot implements Performs {
public void speak() { print("Click!"); }
public void sit() { print("Clank!"); }
public void oilChange() {}
} class Communicate {
public static <T extends Performs>
void perform(T performer) {
performer.speak();
performer.sit();
}
} public class DogsAndRobots {
public static void main(String[] args) {
PerformingDog d = new PerformingDog();
Robot r = new Robot();
Communicate.perform(d);
Communicate.perform(r);
}
} /* Output:
Woof!
Sitting
Click!
Clank!
*///:~

However, note that perform( ) does not need to use generics in order to work. It can simply be specified to accept a Performs object:

//: generics/SimpleDogsAndRobots.java
// Removing the generic; code still works. class CommunicateSimply {
static void perform(Performs performer) {
performer.speak();
performer.sit();
}
} public class SimpleDogsAndRobots {
public static void main(String[] args) {
CommunicateSimply.perform(new PerformingDog());
CommunicateSimply.perform(new Robot());
}
} /* Output:
Woof!
Sitting
Click!
Clank!
*///:~

In this case, generics were simply not necessary, since the classes were already forced to implement the Performs interface.

Compensating for the lack of latent typing

Although Java does not support latent typing, it turns out that this does not mean that your bounded generic code cannot be applied across different type hierarchies. That is, it is still possible to create truly generic code, but it takes some extra effort.

Reflection

One approach you can use is reflection. Here's a perform( ) method that uses latent typing:

//: generics/LatentReflection.java
// Using Reflection to produce latent typing.
import java.lang.reflect.*;
import static net.mindview.util.Print.*; // Does not implement Performs:
class Mime {
public void walkAgainstTheWind() {}
public void sit() { print("Pretending to sit"); }
public void pushInvisibleWalls() {}
public String toString() { return "Mime"; }
} // Does not implement Performs:
class SmartDog {
public void speak() { print("Woof!"); }
public void sit() { print("Sitting"); }
public void reproduce() {}
} class CommunicateReflectively {
public static void perform(Object speaker) {
Class<?> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
} catch(NoSuchMethodException e) {
print(speaker + " cannot speak");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch(NoSuchMethodException e) {
print(speaker + " cannot sit");
}
} catch(Exception e) {
throw new RuntimeException(speaker.toString(), e);
}
}
} public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
}
} /* 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> {
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.

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.

 

thinking in java Generics Latent typing的更多相关文章

  1. Thinking in java——Generics

    ​Ordinary classes and methods work with specific types: either primitives or class types. If you are ...

  2. Java Generics and Collections-2.4-2.5

    2.4 The Get and Put Principle Get and Put Principle: 用于取对象的泛型集合,声明为 <? extends T> 用于存对象的泛型集合,声 ...

  3. Java Generics and Collections-2.3

    2.3 Wildcards with super 这里就直接拿书上的例子好了,这是Collections里面的一个方法: public static <T> void copy(List& ...

  4. Java Generics and Collections-2.2

    2.2 Wildcards with extends 前面介绍过List<Integer>不是List<Number>的子类,即前者不能替换后者, java使用? extend ...

  5. Java Generics and Collections-2.1

    2.1 子类化以及替换原理 为什么List<Integer> 不是List<Number> 的子类? 首先看下面的代码,这段代码是编译不过的 package java_gene ...

  6. Java Generics and Collections-8.1

    8.1 Take Care when Calling Legacy Code 通常,泛型都是在编译时检查的,而不是运行时.便意识检查可以提早通知错误,而不至于到运行时才出问题. 但有时后编译时检查不一 ...

  7. Flink -- Java Generics Programming

    Flink uses a lot of generics programming, which is an executor Framework with cluster of executor ha ...

  8. Thinking in Java——笔记(15)

    Generics The term "generic" means "pertaining or appropriate to large groups of class ...

  9. Java基础之十五 泛型

    第十五章 泛型 一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大. 在面对对象编程语言中,多态算是一种泛化机 ...

随机推荐

  1. cocos2d-x 扩充引擎基类功能 引起的头文件重复包含问题的分析

    c++ 头文件包含 原因的分析:   c++  头文件的循环引用是指: .h 里面的里面的头文件的相互包含的,引起的重复引用的问题.cpp 里面包含头文件是不存在重复引用的问题(因为CPP没有#ifn ...

  2. 函数重载二义性:error C2668: 'pow' : ambiguous call to overloaded function

    2013-07-08 14:42:45 当使用的函数时重载函数时,若编译器不能判断出是哪个函数,就会出现二义性,并给出报错信息. 问题描述: 在.cpp代码中用到pow函数,如下: long int ...

  3. SpringMVC中对Controller使用AOP

    转自http://usherlight.iteye.com/blog/1306111 正确配置spring aop,在controller中使用AOP 在controller中使用AOP的问题主要在于 ...

  4. zoj 3351 Bloodsucker(概率 dp)

    题目:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=4530 dp[i]表示现在存在i个吸血鬼要达成目标(全为吸血鬼)天数的数学 ...

  5. 关于C#控制台传递参数和接收参数

    前言: 写了这么久程序,今天才知道的一个基础知识点,就是程序入口 static void Main(string[] args) 里的args参数是什么意思 ?惭愧... 需求: 点击一个button ...

  6. php简单实现MVC

    在PHP中使用MVC越来越流行了,特别是在一些开源的框架当中.MVC足以应对大多数的情况,但还有一些情况是其不太适合的,如比较简单的个人博客,对于只有几百篇文章量级的博客,使用MVC让人觉得有些太复杂 ...

  7. C# 实例化顺序

    static class Program { static void Main() { BaseB baseb = new BaseB(); baseb.MyFun(); Console.ReadKe ...

  8. (一)学习C#之浮点类型float小结

    类型:float 大小:32位 范围a:±3.4E38  MSDNhttp://msdn.microsoft.com/zh-cn/library/b1e65aza.aspx 范围b: ±1.5E45~ ...

  9. Spring--通过注解来配置bean

    Spring通过注解配置bean 基于注解配置bean 基于注解来配置bean的属性 在classpath中扫描组件 组件扫描(component scanning):Spring能够从classpa ...

  10. sql 统计用的sql

    mh:工时   mhtype:工时类型(6种) 字段:userid      mhtype    mh       001          1        5       001          ...