一、泛型概述
1、泛型:对要操作的数据类型进行指定。是JDK1.5出现的安全机制。泛型是给编译器使用的技术,用在编译时期,提高了编译的安全性(确保类型安全)
2、向集合中添加元素,public boolean add(E e); 任何类型都可以接收(添加的元素被提升为Object类型)。通常,会将元素取出,对元素进行特有的操作。为了避免后期出现安全隐患,在定义容器时,就需要明确容器中存储元素的类型。而迭代器是从集合获取到的,集合明确了其中的元素类型,迭代器取出的元素类型就是明确的,就没有必要再做强转动作了(编译时会检测类型)
注:迭代器的泛型和获取迭代器对象集合的泛型一致
//定义容器时,明确容器中存储元素的类型
ArrayList<String> list = new ArrayList<String>();
//添加元素时,类型匹配的才可以存入,否则编译报错
list.add("abc");
// list.add(true); //类型不匹配,编译报错
//迭代器取出的元素类型明确
for (Iterator<String> it = list.iterator(); it.hasNext();){
//不用再做强转动作
String value = it.next();
System.out.println(value);
}
3、泛型的好处
(1)将运行时期的问题ClassCastException转到了编译时期
(2)避免了强制转换的麻烦
4、何时使用<> ?
当操作的引用数据类型不确定的时候,就使用<>,将要操作的引用数据类型传入即可。<>是一个用于接收具体引用数据类型的参数范围。在程序中,只要用到了带有<>的类或者接口,就要明确传入的具体引用数据类型
注:<>中接收的都是引用数据类型(类/接口),要么是类名,要么是接口名。只要出现<>,就要向<>中传入类型。泛型中不能传基本数据类型
二、泛型的擦除&补偿
1、泛型的擦除:运行时,会将泛型去掉。即 生成的 .class 文件是不带泛型的
2、运行时为何要擦除泛型?
为了兼容运行时的类加载器。运行时,JVM会去启动类加载器。但以前的类加载器没有使用泛型,如果在类文件中加入泛型,以前的类加载器需要升级。希望以前的类加载器还可以使用,且编译时已经对类型进行了检查,类型已经统一,是安全的,运行时就可以把泛型去掉了。所以,只在编译器中加入了泛型机制
注:类加载器:专门用于读取类文件、解析类文件,并将类文件加载进内存的程序
3、泛型的补偿:在运行时,通过获取元素的类型进行转换动作,使用者无需再进行强制转换
4、编译器只做检查,之后<>中的类型被擦除,add()中传入的类型还是Object。取出时,为了避免强转时出现问题,在类加载器的基础上编写了一个补偿程序。先获取指定元素的类型(通过getClass()方法获取对象所属的类文件,就是<>中被擦除的类型),再对其进行类型转换
三、泛型在集合中的应用
1、API中,只要类或接口后有<>的,使用时都要在后面用<>明确元素类型
2、在定义容器时,就要明确容器中要存储的元素类型
/**
* 实现Comparable<Person>接口,使用泛型,该Comparable接口中只能传入Person类型的对象
*/
public class Person implements Comparable<Person> {
private int age;
private String name;
//......此处省略构造函数和get()、set()方法
/**
* 覆盖Comparable<T>接口中的compareTo(T o)方法,参数类型是Person.因为 implements Comparable<Person> 限制了类型的使用
*
* @param p
* @return
*/
@Override
public int compareTo(Person p) {
//不用再做强转动作,因为只有匹配的类型才能传进来
int temp = this.age - p.age;
return temp == 0 ? this.name.compareTo(p.name) : temp;
}
/**
* 覆盖Object类中的equals(Object obj)方法,参数是Object类型(固定类型),不能修改。因为父类中没有定义过泛型
* @param obj
* @return
*/
@Override
public boolean equals(Object obj){
//......
}
}
3、泛型在集合框架中应用的最多
4、泛型中,当引用数据类型不确定时,用泛型表示。泛型中不能传基本数据类型
5、一般情况下,写泛型要具备的特点是:左右两边的泛型要一致
四、泛型类
1、JDK1.5后,使用泛型来接收类中要操作的引用数据类型 -- 泛型类(自定义泛型类)
2、何时使用?
当类中操作的引用数据类型不确定时,就使用泛型来表示
3、没泛型用Object,但泛型这种替代方式比Object安全得多,只是书写比Object麻烦一点,要写<>
/**
* 存入时,用Object类型接收(向上转型),什么类型都可以存入 -- 提高扩展性
* 取出时,先强转再使用,注意报错:ClassCastException。所以,取出时需要做健壮性判断
*/
public class Tool {
private Object object;
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
}
/**
* 在JDK1.5后,使用泛型来接收类中要操作的引用数据类型 -- 泛型类
* 何时使用?
* 当类中操作的引用数据类型不确定时,就使用泛型来表示
*/
public class Tool<QQ> {
private QQ q;
public QQ getObject() {
return q;
}
public void setObject(QQ object) {
this.q = object;
}
}
注:泛型类取出时不用做强转动作,进一步验证了:(1)泛型将运行时期的问题转移到了编译时期(2)避免了强转的麻烦
五、泛型方法
1、在不明确类型的情况下,Object其实是一种具体类型。只不过它是所有类的父类,导致它可以有多态方式来接收
2、如果不明确类型,用户往里传什么类型,就操作什么类型。这时可以定义泛型,叫做把泛型定义在方法上
eg:public <W> void show(W xxx){ ... }
public class Tool<QQ> {
private QQ q;
public QQ getObject() {
return q;
}
public void setObject(QQ object) {
this.q = object;
}
/**
* 工具类中指定什么类型(Tool<QQ>),方法中就操作什么类型(QQ) -- show()方法的参数的泛型跟着对象走
* 方法的参数用的也是泛型,使用的是类上定义的泛型
*/
public void show(QQ object) {
System.out.println("show: " + object);
}
/**
* 将泛型定义在方法上:自动找类型匹配,什么都能传入 -- 泛型是在方法上自定义的
* <W>是定义参数,W是使用参数
*/
public <W> void show1(W obj) {
System.out.println("泛型:show: " + obj); //等同与obj.toString()方法
}
}
3、当方法静态时,不能访问类上定义的泛型。如果静态方法需要使用泛型,只能将泛型定义在方法上
注:自定义的泛型一定要写在返回值类型前,修饰符后
public class Tool<QQ> {
private QQ q;
public QQ getObject() {
return q;
}
public void setObject(QQ object) {
this.q = object;
}
/**
* 静态是不需要对象的,而泛型得需要对象来明确
* 当方法静态时,不能访问类上定义的泛型。如果静态方法需要使用泛型,只能将泛型定义在方法上
*/
// public static void method(QQ xxx) {
// System.out.println("method: " + xxx);
// }
public static <W> void method(W xxx) {
System.out.println("method: " + xxx);
}
}
4、一旦使用了泛型,变量的类型不确定,就不能使用具体对象的方法。Object中的方法可以使用,因为无论传入什么类型,都是Object的子类对象,都具备着Object中的方法(Object中的方法是所有对象的通用方法)
public void print(QQ str) {
// System.out.println("print: " + str.length()); //报错。使用了泛型,变量str的类型不确定,不能使用具体对象的方法
}
六、泛型接口
1、泛型接口:将泛型定义在接口上(将泛型定义在类上就叫泛型类)
2、定义泛型接口时,一般不会明确具体类型(无意义)
/**
* 泛型接口:定义时,一般不会明确具体类型
*/
interface Inter<T> {
public void show(T t);
}
3、泛型接口一般有两种实现方式。使用哪一种,要看何时能明确类型
(1)实现接口时明确类型
/**
* 实现接口时,明确类型:Inter<T> --> Inter<String>
*/
class InterImpl implements Inter<String> {
@Override
public void show(String str){
System.out.println(str);
}
}
public class Test {
public static void main(String[] args) {
InterImpl in = new InterImpl();
in.show("abc");
}
}
(2)实现接口时类型尚不明确,继续使用泛型。直到使用子类对象时,才明确类型
/**
* 实现接口时,类型尚不明确,继续使用泛型:Inter<T> --> Inter<Q>
*/
class InterImpl<Q> implements Inter<Q> {
@Override
public void show(Q q){
System.out.println(q);
}
}
public class Test {
public static void main(String[] args) {
//使用子类对象时,才明确类型
InterImpl<Integer> in1 = new InterImpl<Integer>();
in1.show(1);
InterImpl<String> in2 = new InterImpl<String>();
in2.show("abc");
}
}