多态和泛型的区别是:是否解耦类或方法与所使用的类型之间的约束。
1实现泛化的方法
- 使用多态:将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。
- 使用泛型:暂时不指定类型,而是稍后再决定具体使用什么类型。
//使用多态时
public class Holder{
private Object a;
public holder(Object a){this.a = a;}
public void set(Object a) {this.a = a;}
public static void main(String[] args){
Holder h2 = new Holder(new Automobile());
Automobile a = (Automobile)h2.get();
h2.set("Not an Automobile");
String s = (String)h2.get();
h2.set(1);
Integer x = (Integer)h2.get();
}
}
//使用泛型的时候
public class Holder2<T>{
private T a;
public Holder2(T a){this.a = a;}
public void set(T a){this.a = a;}
public T get(){return a;}
public static void main(String[] args){
Holder2<Automoblie> h2 = new Holder2<Automobile>(new Automobile());
Automobile a = h2.get();
}
}
java泛型本质上只有类泛型一种,因为所有的方法泛型都可以转换成类泛型,并且方法泛型依赖于类泛型。
java泛型可以简单的看成是“带类型参数的类”。
编译器处理泛型的3个步骤: 1.类型检查,2.类型擦除,3.添加转型代码。
2类泛型和方法泛型。
java泛型的核心概念是:告诉编译器你想使用的类型,然后编译器帮你处理一切细节。
- 类泛型:声明时,在类名的后面使用类型参数,并用尖括号把类型参数括起来。在创建这个类的实例的时候,指定具体要使用的类型,然后编译器会处理一切。
- 方法泛型:需要将泛型参数列表置于返回值之前(从中可以看出泛型是带类型参数的类)。当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为“类型参数推断”(type argument inference)。
- 还可以对泛型使用边界。(T extends MyClass)。
泛型方法相对与泛型类的优势:
泛型方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否用于泛型方法,与其所在的类是否是泛型没有关系。所以,原则上,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
注意:对于一个static方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
3实例化泛型类和调用泛型方法
除了可以在创建这个类的实例的时候指定具体要使用的类型,还可以在继承的时候指定具体要使用的类型。
public class Test {
static class GenericBase<T> {
private T element;
public void set(T arg){element = arg;}
public T get(){return element;}
}
// static class GenericBase {
// private Object element;
// public void set(Object arg){element = arg;}
// public Object get(){return element;}
// }
//但是如果被继承的类中包含带?的类型参数就会报异常
static class Derived extends GenericBase<String>{
}
//如果没有指定T,编译时会使用Object来替代T.
static class Devived2 extends GenericBase{}
public static void main(String[] args) {
Devived2 devived2 = new Devived2();
devived2.set(new Devived2());
//运行时可以获取它本来的类型
System.out.println(devived2.get().getClass().getSimpleName());
//但是在编译时,会报不兼容类型异常,因为编译器检测到的deviced2.get()方法返回的类型是Object。
// Devived2 devived21 = devived2.get();
Derived derived = new Derived();
String s = derived.get();
}
}
3-1.擦除机制
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
}
}
//结果:
true
//编译器虽然使用了擦除机制,但是泛型的类型信息还是会存放到字节码文件中,并存放到Class对象中。对于泛型类的定义,编译器并不执行擦除,
//编译器会执行这3个步骤:先进行类型检查,然后进行类型擦除(替换为它们的非泛型上界),最后添加强制类型转换代码。
//擦除指的是移除方法的类型信息。“对象”进入和离开方法的地点称为擦除边界,边界是编译器进行类型检查和插入转型代码的地方。
//非泛型上界:T可以被指定为MyClass不带泛型参数的(被擦除为Object),也可以被指定为MyClass<String>带类型参数的(被擦除为MyClass),还可以被指定为? extends MyClass(被擦除为MyClass)。
[E]
[K, V]
擦除主要的正当理由是从非泛型代码到泛型代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入java代码。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直到客户端准备好用泛型重写这些代码。
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了。
public class Erased<T> {
private final static int SIZE = 100;
public void f(Object arg){
if (arg instanceof T){} //Error
T var = new T(); //error
T[] array = new T[SIZE]; //error
T[] arrys = (T[])new Object[SIZE]; //unchecked warning
}
}
注意:因为擦除在方法中移除了类型信息,所以边界(对象进入和离开方法的地点)是编译器执行类型检查并插入转型代码的地方。
3-2通配符
注意:通配符被限制为单一边界。
//You can do this
List<? extends SuperHearing> audioBoys;
//But you can't do this
List<? extends SuperHearing&SuperSmell> dogBoys;
不能把一个涉及Apple的泛型赋给一个涉及Fruit的泛型。因为我们讨论的是容器的类型而不是容器持有的类型。
//编译时会报错
List<Fruit> fList = new ArrayList<Apple>();
与数组不同,泛型没有内建的协变类型。这时因为数组在语言中是安全定义的,因此内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想使用类型参数做些什么,以及应该采用什么样的规则。
但是,有时你想要在两个类型之间建立某种类型的向上转型关系,这正是通配符的作用:
//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); //legal but unintersting
Fruit f = fList.get(0);
注意:
List<? extends Fruit>读作:可以获取任何继承Fruit的类型的List。
通配符可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
编译器会直接拒绝对参数列表中涉及通配符的方法的调用。所以,为了禁止对在类型中使用了通配符的情况下的函数调用,我们需要在参数列表中使用类型参数。
超类型通配符
可以使用<? super MyClass>和<? super T>
根据如何能够向一个泛型类型“写入”以及如何能够从一个泛型类型“读取”,来着手思考子类型和超类型边界。
无界通配符
4使用Collections工具类对泛型进行检查
因为可以向JavaSE5之前的代码传递泛型容器,所以旧式代码仍旧有可能会破坏你的容器,JavaSE5的java.util.Collections中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是静态方法:checkedCollection()、checkedList()、checkedMap()、checkedStoredMap()、checkedStoredSet()这些方法每一个都会将你希望动态检查的容器当做第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
这些方法会在你试图插入类型不正确的对象时抛出ClassCastException,这与泛型之前的容器形成了对比。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CheckedList {
static class Pet{}
static class Cat extends Pet{}
static class Dog extends Pet{}
static void oldStyleMethod(List probablyDogs){
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<>();
oldStyleMethod(dogs1);
List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
try{
oldStyleMethod(dogs2);
}catch (Exception e){
System.out.println(e);
}
List<Pet> pets = Collections.checkedList(new ArrayList<>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
}