Java编程思想(一)第1~13、16章
Java编程思想(二)第14章-类型信息
Java编程思想(三)第15章-泛型
Java编程思想(四)第17章-容器深入研究
Java编程思想(五)第18章-Java IO系统
Java编程思想(六)第19章-枚举类型
Java编程思想(七)第20章-注解
Java编程思想(八)第21章-并发
泛型(generics)的概念是Java SE5的重大变化之一。泛型实现了参数化类型(parameterized types)的概念,使代码可以应用于多种类型。“泛型”这个术语的意思是:“适用于许多许多的类型”。
泛型方法与其所在的类是否是泛型没在关系,即泛型方法所在的类以是泛型类也可以不是泛型类。
static
方法而言,无法访问泛型类的类型参数,所以,如果static
方法需要使用泛型能力,就必须使其成为泛型方法。使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。
在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内,即显式的类型说明。
根据JDK文档的描述,
Class.getTypeParameters()
将“返回一个TypeVariable
对象数组,表示有泛型声明的类型参数…..”,这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中看到,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息。
因此,你可以知道诸如泛型参数标识符和泛型类型边界这类信息——你却无法知道创建某个特定实例的实际的类型参数。……,在使用Java泛型工作时它是必须处理的最基本的问题。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此
List
和List
在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生类型,即List
。
它怎么知道
f()
方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator
被实例化的这一刻,它看到HasF
拥有一个方法f()
。如果情况并非如此,就会得到一个编译期错误,这样类型安全就得到了保障。
// Templates.cpp
#include
using namespace std;
template<class T> class Manipulator{
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF{
public:
void f() { cout << "HasF::f()" << endl; }
};
int main(){
HasF hf;
Manipulator manipulator(hf);
manipulator.manipulate();
}
由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。
package net.mrliuli.generics.erase;
/**
* Created by li.liu on 2017/12/7.
*/
/**
* 由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
* @param
*/
class Manipulator{
private T obj;
public Manipulator(T x){ obj = x; }
// Error: Cannot resolve method 'f()'
//public void manipulate(){ obj.f(); }
}
/**
* 为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。
* @param
*/
class Manipulator2{
private T obj;
public Manipulator2(T x){ obj = x; }
public void manipulate(){ obj.f(); }
}
public class Manipulation {
public static void main(String[] args){
HasF hf = new HasF();
Manipulator manipulator = new Manipulator<>(hf);
//manipulator.manipulate();
Manipulator2 manipulator2 = new Manipulator2<>(hf);
manipulator2.manipulate();
}
}
我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。
T
擦除到了HasF
,就好像在类的声明中用HasF
替换了T
一样。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性。
因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。
擦除的代码是显著的。
如果编写了下面这样的代码:
class Foo<T>{ T var; }
那么看起来当你在创建Foo
的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo
中的代码应该知道现在工作于Cat
之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”class GenericBase<T>{}
class Derived1<T> extends GenericBase<T>{}
class Derived2 extends GenericBase{} // No warning
有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
java.lang.ArrayStoreException
异常。 // Compile Error: incompatible types:
List list = new ArrayList();
与数组不同,泛型没有内建的协变类型。即*协变性对泛型不起作用。
package net.mrliuli.generics.wildcards;
import java.util.*;
/**
* Created by leon on 2017/12/8.
*/
public class GenericsAndCovariance {
public static void main(String[] args){
// Compile Error: incompatible types:
//List list = new ArrayList();
// Wildcards allow covariance:
List extends Fruit> flists = new ArrayList();
// But, 编译器并不知道flists持有什么类型对象。实际上上面语句使得向上转型,丢失掉了向List中传递任何对象的能力,甚至是传递Object也不行。
//flists.add(new Apple());
//flists.add(new Fruit());
//flists.add(new Object());
flists.add(null); // legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flists.get(0);
}
}
List extends Fruit>
,set()
方法不能工作于 Apple
和 Fruit
,因为 set() 的参数也是 ? extends Furit
,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。equals()
方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。 super MyClass>
,甚至或者使用类型参数: super T>
。这使得你可以安全地传递一个类型对象到泛型类型中。package net.mrliuli.generics.wildcards;
import java.util.*;
public class SuperTypeWildcards {
/**
* 超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。
* @param apples 参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。
*/
static void writeTo(List super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
//apples.add(new Fruit()); // Error
}
}
writeExact(fruitList, new Apple());
在JDK1.7中没有报错,说明进入泛型方法 writeExact()
时 T
被识别为 Fruit
,书中说报错,可能JDK1.5将 T
识别为 Apple
。package net.mrliuli.generics.wildcards;
import java.util.*;
/**
* Created by li.liu on 2017/12/8.
*/
public class GenericWriting {
static void writeExact(List list, T item){
list.add(item);
}
static List appleList = new ArrayList();
static List fruitList = new ArrayList();
static void f1(){
writeExact(appleList, new Apple());
writeExact(fruitList, new Apple());
}
static void writeWithWildcard(List super T> list, T item){
list.add(item);
}
static void f2(){
writeWithWildcard(appleList, new Apple());
writeWithWildcard(fruitList, new Apple());
}
public static void main(String[] args){
f1();
f2();
}
}
原生泛型Holder
与Holder>
原生
Holder
将持有任何类型的组合,而Holder>
将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。
以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用
f2()
的过程中被捕获,因此它可以在对f1()
的调用中被使用。
package net.mrliuli.generics.wildcards;
/**
* Created by leon on 2017/12/9.
*/
public class CaptureConversion {
static void f1(Holder holder){
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder> holder){
f1(holder); // Call with captured type
}
public static void main(String[] args){
Holder raw = new Holder(1);
f1(raw);
f2(raw);
Holder rawBasic = new Holder();
rawBasic.set(new Object());
f2(rawBasic);
Holder> wildcarded = new Holder(1.0);
f2(wildcarded);
}
}
我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上。