泛型
一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大.
简单泛型
- 有许多原因促成了泛型,而最引人注目的一个原因就是为了创造容器类.
- 泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且编译器来保证类型的正确性.
- 在使用泛型时,我们更喜欢暂时不指定类型,而是稍后决定具体使用什么类型,要达到这个目的,需要使用类型参数,放在类名后面.然后在使用这个类的时候.再用实际的类型替换此类型参数.在例子中T就是类型参数.类型参数简单理解就是一个类的引用
- 一个元组类库概念元组:它是将一组对象直接打包存储于其中的一个单一对象.这个容器对象允许读取其中元素,但是不允许向其中存放新的对象.(这个概念也称为数据传送对象或信使)
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A a,B b){
first = a;
second = b;
}
public String toString(){
return "(" + first + " , " + second + ")";
}
}
上面程序的安全性:客户端程序可以读取first和second对象,然后可以随心所欲地使用这两个对象.但是它们无法将其他值赋予first或second.因为final声明为你买了相同的安全保险,而且这种格式更简洁明了.
public class LinkedStack<T> {
public static class Node<U>{
U item;
Node<U> next;
Node(){
item = null;
next = null;
}
Node(U item,Node<U>next){
this.item = item;
this.next = next;
}
boolean end(){
return item == null && next == null;
}
}
private Node<T> top = new Node<T>();//End sentinel
public void push(T item){
top = new Node<T>(item,top);
}
public T pop(){
T result = top.item;
if (!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<String>();
for (String s :
"Phaster on stun!".split(" ")) {
lss.push(s);
}
String s;
while((s = lss.pop()) != null)
{
System.out.println(s);
}
}
}
15.3泛型接口
泛型也可以应用于接口.例如生成器(generator),这是一种专门负责创建对象的类.这是工厂方法设计模式的一种应用.不过,当生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数.也就是说,生成器无需额外的信息就知道如何创建对象.
例:public interface Generator\ {T next();}
15.4泛型方法
要定义泛型方法,只需将泛型参数列表置于返回值之前
例:public \ void f(T x){}
- 是否拥有泛型方法,与其所在的类是否是泛型没有关系.
- 泛型方法使得该方法能够独立于类而产生变化.
原则: 无论何时,只要你能做到,就应该尽量使用泛型方法.也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚.另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以如果static方法需要使用泛型能力,就必须使其成为泛型方法.
- 类型推断只对赋值操作有效,其他时候并不起作用.如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断.这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量.
public class LimitsOfInference{
static void f(Mat<Person,List<? extends Pet>> petPeople){
}
public static void main(String[] args){
//f(New.map());//Does not compile
f(New.<Person,List<Pet>>map());//显式的类型说明,必须在点操作符之前使用this关键字,如果使用static方法,必须在点操作之前加上类名.(很少使用显式)
}
}
- 一个通用的Generator
public interface Generator<T> {
T next();
}
//可以看到只要传入一个类.class运行next方法就会创建该类的对象
//这样大大减少了我们要写的代码.java泛型要求传入Class对象,以便也可以在create()方法中用它进行类型判断
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type){
this.type = type;
}
@Override
public T next() {
try {
//Assumes type is a public class
return type.newInstance();
}catch (Exception e){
throw new RuntimeException(e);
}
}
public static <T> Generator<T> create(Class<T> type){
return new BasicGenerator<>(type);
}
}
15.5匿名内部类
15.6构造复杂模型
class Product {
private final int id;
private String description;
private double price;
public Product(int IDnumber, String descr, double price){
id = IDnumber;
description = descr;
this.price = price;
System.out.println(toString());
}
public String toString() {
return id + ": " + description + ", price: $" + price;
}
public void priceChange(double change) {
price += change;
}
public static Generator<Product> generator =
new Generator<Product>() {
private Random rand = new Random(47);
public Product next() {
return new Product(rand.nextInt(1000), "Test",
Math.round(rand.nextDouble() * 1000.0) + 0.99);
}
};
}
class Shelf extends ArrayList<Product> {
public Shelf(int nProducts) {
Generators.fill(this, Product.generator, nProducts);
}
}
class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves, int nProducts) {
for(int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
}
class CheckoutStand {}
class Office {}
public class Store extends ArrayList<Aisle> {
private ArrayList<CheckoutStand> checkouts =
new ArrayList<CheckoutStand>();
private Office office = new Office();
public Store(int nAisles, int nShelves, int nProducts) {
for(int i = 0; i < nAisles; i++)
add(new Aisle(nShelves, nProducts));
}
public String toString() {
StringBuilder result = new StringBuilder();
for(Aisle a : this)
for(Shelf s : a)
for(Product p : s) {
result.append(p);
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(new Store(14, 5, 10));
}
} /* Output:
258: Test, price: $400.99
861: Test, price: $160.99
868: Test, price: $417.99
207: Test, price: $268.99
551: Test, price: $114.99
278: Test, price: $804.99
520: Test, price: $554.99
140: Test, price: $530.99
...
*///:~
正如我们在Store.toString()中看到的,其结果是许多层容器,但是它们是类型安全且安全且可管理的.
15.7擦除的神秘之处
当开始深入研究泛型时,会发现大量的东西乍看是没有意义的.例:可以声明ArrayList.class,但是不能声明ArrayList\.class
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob,Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> p = new Particle<Long,Double>();
System.out.println(Arrays.toString(
list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(
p.getClass().getTypeParameters()));
}
} /* Output:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*///:~
在泛型代码内部,无法获得任何有关泛型参数类型的信息.
你可以知道诸如类型参数标识符和泛型类型边界这类的信息–你却无法知道用来创建某个特定实例的实际的类型参数.
java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象.因此List\和List\在运行时事实上是相同类型.这两种形式都被擦除成它们的”原生”类型,即List.
- 迁移兼容性:因为泛型不是在Java1.0中的一部分,所以需要擦除来实现.擦除减少了泛型的泛化性.
- 在基于擦除的实现中,泛型类型被当做第二类型处理,即不能在某些重要的上下文环境中使用类型,泛型类型只有在静态类型检查期间才出现.在此之后程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界.例如List\这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object
- 擦除的核心动机是它使得泛化的刻画短可以使用非泛化的类库来使用.反之亦然.被称为”迁移兼容性”
- 通过允许非泛型代码和泛型代码共存,擦除使得这种向着泛型的迁移称为可能.
- 擦除的问题:擦除的代价是显著的.泛型不能用于显式地引用运行时类型的操作之中,例如转型instanceof操作和new表达式.因为关于参数的类型信息都丢失了.
- 使用在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已.
//由于上面的原因尽可能这样写代码.不要直接写出具体类型
class GenericBase<T> {
private T element;
public void set(T arg){arg = element;}
public T get(){return element;}
}
class Derived1<T> extends GenericBase<T>{}
class Derived2 extends GenericBase{}//No waring
//class Derived3 extends GenericBase<?>{}
//Strange error;
// unexpected type found:?
// required: class or interface without bounds
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj);
}
}
//擦除相等版(所产生的字节码是相同的)对进入set()的类型检查是不需要的因为这将由编译器执行.
//由于所产生的get()和set()字节码相同,所以在泛型中的所有动作都发生在边界处--对于传进来的值进行额外的编译期检查,并插入对传递出去的值的转型.
//这有助于澄清对擦除的混淆,记住,"边界就是发生动作的地方".
public class SimpleHolder {
private Object obj;
public void set(Object obj){
this.obj = obj;
}
public Object get(){
return obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String )holder.get();
}
}
public class GenericHolder<T> {
private T obj;
public void set(T obj){this.obj = obj;}
public T get() {return obj;}
public static void main(String[] args) {
GenericHolder <String> holder =
new GenericHolder<>();
holder.set("Item");
String s = holder.get();
}
}
15.8擦除的补偿
擦除丢失了泛型代码中执行某些操作的能力.任何在运行时需要知道确切类型信息的操作都将无法工作
有时必须通过引入类型标签来对擦出进行补偿.
//把两个代码中有用的内容拿出来比较
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz){
array = (T[])new Object[sz];
}
}
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz){
array = new Object[sz];
}
public void put(int index,T item){
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index){
return (T)array[index];
}
@SuppressWarnings("unchecked")
public T[] rep(){
return (T[])array;//Warning: unchecked cast
}
}
- 初看起来,这好像没有什么变化,只是转型挪了地方.如果没有\@SuppressWarnings注解,仍旧会得到unchecked警告,但是,现在的内部表示是Object[]而不是T[].
- 当get()被调用时,他将对象转型为T,这实际上是正确的类型,因此这是安全的.然而如果你调用rep(),它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,在运行时产生异常.因此没有任何方式可以推翻底层的数组类型,他只能是Object[].在内部将array当作Object[]而不是T[]处理的优势时:我们不太可能忘记这个数组的运行时类型,从而意外地引入缺陷(尽管大多数也可能是所有这类缺陷都可以在运行时快速地检测到)
个人理解补充:下面这样的程序是错误的不能创建泛型数组,如果T没有无参构造的话这样建立对象是错误的方法
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
if(arg instanceof T) {} // Error
T var = new T(); // Error
T[] array = new T[SIZE]; // Error
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
} ///:~
15.9边界
边界使得你可以在用于泛型的参数类型上设置限制条件.尽管这使得你可以强制规定泛型可以引用的类型,但是潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法.
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法.为了执行能够将这个参数限制为某个类型的子集,从而用这些类型子集来调用方法.Java泛型重用了extends关键字.
注意:要理解extends关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的.
interface HasColor{java.awt.Color getColor();}
class Colored<T extends HasColor>{
T item;
Colored(T item){this.item = item;}
T getItem(){return item;}
//The bound allows you to call a method;
java.awt.Color color(){return item.getColor();}
}
//在继承的每个层次上添加边界限制
class HoldItem<T>{
T item;
HoldItem (T item){this.item = item;}
T getItem() {return item;}
}
class Colored2<T extends HasColor> extends HoldItem<T>{//()中的extends声明了边界,后面的extends继承了HoldItem<T>继承了T然后在T的基础上前面的extends边界会做一个判断所以也就是在继承边界的基础上又添加了一个边界
Colored2(T item){super(item);}
java.awt.Color color(){return item.getColor();}
}
15.10通配符
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class CovarianArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
//Runtime type is Apple[],not Fruit[] or Orange[]
try {
//所以这里的向上转型是不合适的
//Compiler allows you to add Fruit:
fruit[0] = new Fruit();
}catch (Exception e){
System.out.println(e);
}
try {
//Compiler allows you to add Oranges;
fruit[0] = new Orange();
}catch (Exception e){
System.out.println(e);
}
}
}
- 与数组不同,泛型没有内建的协变类型.这是因为数组在语言中是完全定义的,因此可以内建了编译和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用什么类型做些什么,以及该采用什么样的规则.想要在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的.
public class GenericsAndCovariance {
public static void main(String[] args) {
//Wildcards allow covariance
List<? extends Fruit> flist = new ArrayList<Apple>();
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null);
//We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
- List\
//总结性代码并注意英文注释
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns 'Object'
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
- 调用get(),它只会返回一个Fruit–这就是在给定”任何扩展自Fruit的对象”这一边界之后,它所能知道的一切了.
- set()方法不能工作于Apple或Fruit,因为set()参数也是”?Extends Fruit”,这意味着它可以是 任何事物,而编译器无法验证”任何事物“的类型安全性.
- 但是,equals()方法工作良好,因为它将接受Object类型而并非T类型的参数.因此编译器只关注传递进来和要返回的对象类型,他并不会分析代码,以查看是否执行了任何实际的写入和读取操作.
逆变
- 调用get(),它只会返回一个未知对象,这意味着它可以是 任何事物,而编译器无法验证”任何事物“的类型安全性.只能用Object来接收get对象
- set()方法能工作于Apple或Fruit,因为set()参数是”? super Fruit”,”任何扩展自Fruit的对象”这一边界之后,它所能知道的一切了.
无界通配符
\