前言:自JDK 1.5 之后,Java 引入泛型解决了集合容器类型安全这一问题。泛型在Java中有很重要的地位,在集合容器及各种设计模式中有非常广泛的应用。一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。如果要编写可以应用多种类型的代码,这种刻板的限制对代码得束缚会就会很大。——《Thinking in Java》
泛型就是参数化类型,把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。
1、一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型,把类型当作是参数一样传递。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,然后泛型在实际调用时指定具体类型。
泛型归根到底就是“模版”或"广泛的类型",适用于多种数据类型执行相同的代码(代码更通用化)。
这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2、在 Java 中以 "
泛型中的泛指代表T可用任意字母替代
通常我们使用 T : Type E : Element K : Key V : Value R : Return
可以代表任意类型的数据类型,可以限制集合中存放元素的类型
相关术语:
ArrayList
中的E称为类型参数变量
ArrayList
中的Integer称为实际类型参数整个称为
ArrayList
泛型类型整个
ArrayList
称为参数化的类型ParameterizedType
3、泛型擦除
泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上,即挡住向集合中插入非法数据。但编译器编译完带有泛形的Java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”
1、我们在操作数据时,有时不知道要操作的数据是什么类型。
2、若集合中不使用泛型,意味着集合中可以存储任意类型的对象,若需要具体到某一个数据类型时,需要强制类型转换,可能引发ClassCastException
Java 5.0以前是使用Object来代表任意类型的,但是向下转型有强制转换的问题,这样程序就不太安全。
首先,我们来试想一下:没有泛型,集合会怎么样
Collection、Map集合对元素的类型是没有任何限制的。本来我的Collection集合装载的是全部的Dog对象,但是外边把Cat对象存储到集合中,是没有任何语法错误的,编译能通过。
把对象扔进集合中,集合是不知道元素的类型是什么的,仅仅知道是Object。因此在get()的时候,返回的是Object。外边获取该对象,还需要强制转换,容易忽略导致出现ClassCastException异常。
有了泛型以后:
代码更加简洁【不用强制转换】
程序更加健壮【泛型提供编译检查,只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
可读性和稳定性【在编写集合的时候,就限定了类型】
//集合中泛型的相关使用:在创建集合的时候,我们明确了List集合的类型
List list=new ArrayList<>();
list.add("hello");//add()只能添加String
list.add(10);//泛型提供编译检查,compile不通过
1、使用泛型能写出更加灵活通用的代码
泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨。
2、 泛型将代码安全性检查提前到编译期
泛型被加入Java语法中,还有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,从而将运行时ClassCastException
转移到编译时。
举个栗子:
List dogs =new ArrayList();
dogs.add(new Dog());
dogs.add(new Cat());
在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误ClassCastException
。但是加入泛型后:
List dogs=new ArrayList<>();//限制数据类型为一个,只作编译时限制,当执行时不作读取。通过查看.class文件,<>是不存在的
dogs.add(new Cat());//Error Compile
会在编译的时候就检查出来。
3、泛型能够省去类型强制转换
在JDK1.5之前,Java容器都是通过将类型向上转型为Object
类型来实现的,因此在从容器中取出来的时候需要手动的强制转换。
Dog dog=(Dog)dogs.get(1);
加入泛型后,由于编译器知道了具体的类型,因此编译期就会限定集合的输入类型,这样得到的都是合法类型,不用强制转换了,使得代码更加优雅。
a、何时定义:当我们在写时不确定以后要用的是什么类型,但知道肯定要有这个元素
b、何时确定:使用时确定
1、泛型类
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来。这样的话,用户明确了什么类型,该类就代表着什么类型。用户在使用的时候就不用担心忽略强制转换导致运行时的ClassCastException。
/*
1:把泛型定义在类上
2:类型变量定义在类上,方法中也可以使用
*/
public class ObjectTool {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
用户想要使用哪种类型,就在创建的时候指定类型。使用的时候,该类就会自动转换成用户想要使用的类型:
public static void main(String[] args)
{
//创建对象并指定元素类型
ObjectTool tool = new ObjectTool<>();
tool.setObj(new String("建国70周年快乐"));
String s = tool.getObj();
System.out.println(s);
//创建对象并指定元素类型
ObjectTool objectTool = new ObjectTool<>();
/**
* 如果我在这个对象里传入的是String类型的,它在编译时期就通过不了了.
*/
objectTool.setObj(10);
int i = objectTool.getObj();
System.out.println(i);
}
2、泛型方法
前面已经介绍了泛型类,在类上定义的泛型,在方法中也可以使用.....现在,我们可能就仅仅在某一个方法上需要使用泛型.,外界仅仅是关心该方法,不关心类其他的属性。这样的话,我们在整个类上定义泛型,未免就有些大题小作了。
当写方法时返回值不确定,就可以定义泛型方法。泛型是先定义后使用的:
//定义泛型方法
public void show(T t)
{
System.out.println(t);
return t;
}
用户传递进来的是什么类型,返回值就是什么类型
public static void main(String[] args) {
//创建对象
ObjectTool tool = new ObjectTool();
//调用方法,传入的参数是什么类型,返回值就是什么类型
String str = tool.show("hello");
Interger int = tool.show(12);
tool.show(12.5);
}
3、泛型接口
前面我们已经定义了泛型类,泛型类是拥有泛型这个特性的类,它本质上还是一个Java类,那么它就可以被继承
那它是怎么被继承的呢?这里分两种情况:
子类明确泛型类的类型参数变量
子类不明确泛型类的类型参数变量
/*
* 把泛型定义在接口上
*/
public interface Inter
{
public abstract void show(T t);
}
/**
* 1、子类明确泛型接口的类型参数变量
*/
public class InterImpl implements Inter
{
@Override
public void show(String s)
{
System.out.println(s);
}
}
/**
* 2、子类不明确泛型类的类型参数变量:
* 实现类也要定义出类型的
*
*/
public class InterImpl implements Inter
{
@Override
public void show(T t) {
System.out.println(t);
}
}
Java中,容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate
Java泛型中的通配符机制的目的:让一个持有特定类型(如A类型)的集合能够强制转换为持有A的子类或父类型的集合!
List
List>, 这个 ? 是一个实参,这是Java定义的一种特殊类型,比Object更特殊,就像一个影子。比如List
这里有三种方式定义一个使用泛型通配符的集合(变量)。如下:
List> listUknown = new ArrayList();
List extends A> listUknown = new ArrayList();
List super A> listUknown = new ArrayList();
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.。
?号通配符表示可以匹配任意Java类型。List> 的意思是这个集合是一个可以持有任意类型的集合,它可以是List,也可以是List,或者List
等等。因为你不知道集合是哪种类型,所以你只能够对集合进行读操作。并且你只能把读取到的元素当成 Object 实例来对待。
public void test(List> list){
for(int i=0;i
所以我们要注意:当我们使用?号通配符的时候,就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。因为直到外界使用才知道具体的类型是什么。也就是说,在上面的List集合,我是不能使用add()方法的。因为add()方法是把对象丢进集合中,而现在我是不知道对象的类型是什么。
使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
2.1>现在,我想接收一个List集合,它只能操作数字类型的元素【Float、Integer、Double、Byte等数字类型都行】,怎么做?
我们学习了通配符,但是如果直接使用通配符的话,该集合就不是只能操作数字了。因此我们需要用到设定通配符上限
List extends Number>//代表的是一个可以持有Number及其子类(如Float和Double)的实例的List集合。
当集合所持有的实例是A或者A的子类的时候,此时从集合里读出元素并把它强制转换为A是安全的,但是不能加入任何对象。
2.2>对于上界通配符 extends T>,我们无法向其中加入任何对象,但是我们可以进行正常的取出。
Plate<? extends Fruit>
翻译成人话就是:一个能放水果以及一切是水果派生类的盘子。再直白点就是:啥水果都能放的盘子。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。
Plate extends Fruit> p=new Plate(new Apple());
extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。比如下面例子里两个set()方法,插入Apple和Fruit都报错。
Plate extends Fruit> p=new Plate(new Apple());
//不能存入任何元素
p.set(new Fruit()); //Error
p.set(new Apple()); //Error
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get(); //Error
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate
所以通配符>和类型参数
public List fill(T... t);
但通配符>没有这种约束,Plate>单纯的就表示:盘子里放了一个东西,是什么我不知道。
使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.
List super A> //List集合list,它可以持有A及其父类的实例。
当你知道集合里所持有的元素类型都是A及其父类的时候,此时往list集合里面插入A及其子类(B或C)是安全的。因为此时我们可以确定传入的list集合里的元素是A及其父类,所以我们往这个集合里插入A及其子类是兼容的(向上转型)。
3.2>对于 super 通配符,我们可以存入T 类型对象或 T 类型的子类对象,但是我们取出的时候只能用 Object 类变量指向取出的对象。
Plate super Fruit> p=new Plate(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。
4、extends 通配符和super 通配符总结
extends 通配符偏向于内容的获取,而 super 通配符更偏向于内容的存入。
5、通配符?和T的用法区别
? 与 T 的第一个区别:不关心List里面的元素,或者只需要用到List里面元素的最顶层父元素的方法的时候,可以用List>来简化代码的书写;
? 与 T 的第二个区别:使用extends限定类型子集的时候,?不能多重继承,T 可以;
? 与 T 的第三个区别: 使用super限定父集的时候,? 可以, T 不可以
参考下面链接:对通配符还懵逼的可以看下面第一个链接,该博主举的栗子形象易懂
JDK5提出了泛型这个概念,但是JDK5以前是没有泛型的。也就是泛型是需要兼容JDK5以下的集合的。
当把带有泛型特性的集合赋值给老版本的集合时候,会把泛型给擦除了。值得注意的是:它保留的就类型参数的上限。
List list = new ArrayList<>();
//类型被擦除了,保留的是类型的上限,String的上限就是Object
List list1 = list;
如果我把没有类型参数的集合赋值给带有类型参数的集合赋值,这又会怎么样??
List list = new ArrayList();
List list2 = list;
它也不会报错,仅仅是提示“未经检查的转换”。
当我们写网页的时候,常常会有多个DAO,我们每次都要写好几个DAO,这样会有点麻烦。
那么我们想要的效果是什么呢?只写一个抽象DAO,别的DAO只要继承该抽象DAO,就有对应的方法。
要实现这样的效果,肯定是要用到泛型的。因为在抽象DAO中,是不可能知道哪一个DAO会继承它自己,所以是不知道其具体的类型的。而泛型就是在创建的时候才指定其具体的类型。
public abstract class BaseDao
{
//模拟hibernate....
private Session session;
private Class clazz;
//哪个子类调的这个方法,得到的class就是子类处理的类型(非常重要)
public BaseDao()
{
Class clazz = this.getClass(); //拿到的是子类
//BaseDao
ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass();
clazz = (Class) pt.getActualTypeArguments()[0];
System.out.println(clazz);
}
public void add(T t){
session.save(t);
}
public T find(String id){
return (T) session.get(clazz, id);
}
public void update(T t){
session.update(t);
}
public void delete(String id){
T t = (T) session.get(clazz, id);
session.delete(t);
}
}
CategoryDao
public class CategoryDao extends BaseDao {
}
BookDao
public class BookDao extends BaseDao {
}