泛型的由来
什么是泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型的好处
- 好处一:编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
- 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”;
- “任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的;
- 对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
自定义泛型结构
泛型结构包括泛型类、泛型接口和泛型方法
泛型类、泛型接口
泛型类与泛型接口的区别其实就是类与接口的区别,这里以泛型类为例:
- 类中声明的泛型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,但是在静态方法中不能使用类的泛型。因为泛型类型是在创建类的时候指定的,而静态方法加载的时候,类的实例并没有创建。
- 异常类不能是泛型的。
泛型方法
以JDK中的Collection接口为例,区分普通方法和泛型方法:
public interface Collection<E> extends Iterable<E> {
//普通方法
boolean add(E data);
//泛型方法,泛型方法的类型区别于类或接口中的类型,普通类中也可以含有泛型方法
<T> T[] toArray(T[] a);
}
注意:区别于泛型类中带有类型符号的普通方法的是,泛型方法可以是静态的。具体说来就是,上面的add方法不能是静态的,因为E是在类创建时类型才确定下来;而toArray方法可以是静态的,因为T是在方法调用时就确定下来了。
泛型与面向对象
泛型与继承
- 在此定义三个类,Father、Son和Grandson
- Grandson extends Son
- Son extends Father
类的继承
//Son不是泛型类,它继承了Father中的Integer
public class Son extends Father<Integer> {
}
//Son<T> 仍然是泛型类
public class Son<T> extends Father<T> {
}
参数类型的继承
java中的向上转型有如下几种形式
//1.对象,编译通过
Object abj = null;
String str = null;
abj = str;
//2.数组,编译通过
Object[] objArr = null;
String[] strArr = null;
objArr = strArr
//3.泛型1,编译通过,其实就是平常写的List<String> list = new ArrayList<>();
List<String> list1 = null;
ArrayList<String> list2 = null;
list1 = list2
//4.泛型2,编译不通过,此时的objList和strList的类型不具有子父类关系
List<Object> objList = null;
List<String> strList = null;
objList = strList;
## 第四种情况会导致一些问题,比如如下方法,遍历集合中的元素
public void foreach(List<Object> list) {
}
## List<String>类型的变量就无法用这个方法遍历,因为List<Object>类型和List<String>类型
不具备子父类关系,无法使用多态,这样就需要重载foreach方法
总结:List<Object>
是ArrayList<Object>
的父类;List<Object>
不是List<String>
的父类。针对上述的第四种情况,泛型通配符可以解决。
泛型通配符
示例
泛型通配符用?
来表示,在泛型中,List<?>
是List<String>
和List<Object>
的父类,可以声明一下方法解决上述第四种情况
public void foreach(List<?> list) {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
}
通配符上下限
-
? extends Son
,其中Son是通配符?
的上限,可以理解为?
的类只能是Son
及其子类,即小于等于 -
? super Son
,其中Son是通配符?
的下限,可以理解为?
的类只能是Son
及其父类,即大于等于 - 对于通配符上下限,如
List<? extends Son>
和List<? super Son>
,声明以下对象做测试
List<? extends Son> list1 = new ArrayList<>();
List<? super Son> list2 = new ArrayList<>();
List<Grandson> list3 = new ArrayList<>();
List<Son> list4 = new ArrayList<>();
List<Father> list5 = new ArrayList<>();
//测试一
list1 = list3;//(编译成功)
list1 = list4;//(编译成功)
list1 = list5;//(编译失败)
//结论:G<? extends A>可以作为G<A>和G<B>的父类,其中B是A的子类
//测试二
list2 = list3;//(编译失败)
list2 = list4;///(编译成功)
list2 = list5;///(编译成功)
//结论:G<? super A>可以作为G<A>和G<B>的父类,其中B是A的父类
泛型通配符读写要求
- 对于普通的通配符
List<?>
List<String> listStr = new ArrayList<>();
List<Integer> listInteger = new ArrayList<>();
List<?> list = null;
list = listStr ;
list = listInteger ;
//不能添加(只能添加null),编译失败
list.add("1");
list.add(1);
//只能读取,读取的是object类型的对象
Object o = list.get(0);
- 对于通配符上限
List<? extends Son>
//读取数据
list1 = list4;//用list1 = list3同理
//由上面的测试可知,list1中的类型最大为Son,这里用了最大的类型,保证了多态的特性(父类引用指向子类对象)
Son s = list1.get(0);
//写入数据
//无法确定list1的下限,传入的Son类型不能确定是?的子类还是父类,不满足多态特性
list1.add(new Son());//(编译失败)
无法写入数据,那具体怎么使用呢?通配符在声明局部变量时没什么意义,但是作为形参,限制传入的参数类型时,它非常重要
- 对于通配符下限
List<? super Son>
//读取数据
list2 = list4;
//只能返回Object,因为list2中的对象最小类型是Son,但是上限没有定,只能用*父类Object才能保证多态特性
Object o = list2.get(0);
//写入数据
//list2的下限是Son,可以添加Son及其子类,以满足多态特性
list2.add(new Son());
list2.add(new Grandson());//(编译成功)
使用案例:TreeSet
的构造器中传入的比较器使用了Comparator<? super E>
类型擦除
定义
- 泛型是java1.5版本才引入的概念,在这之前没有泛型那个。但是,泛型代码可以很好的兼容以前的版本,那是因为泛型信息只存在于代码编译阶段。在进入JVM之前,与泛型相关的信息会被擦除,我们称之为类型擦除
演示
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getSimpleName());//ArrayList
System.out.println(intList.getClass().getSimpleName());//ArrayList
System.out.println(strList.getClass() == intList.getClass());//true
种类
- 无限制的类型擦除
- 有限制的类型擦除
- 类的类型擦除
- 方法的类型擦除
- 类的类型擦除
- 桥接方法
在jvm中,对接口进行类型擦除后,生成的info方法,在实现类中为了保持接口和类的实现关系,也需要有这个方法
类型擦除中的一些坑
//todo
泛型与反射
一些区别
?和T的区别
- ?是一个不确定的类型,通常用于接收返回值或者作为形参
- T是一个确定的类型,通常用于泛型类和泛型方法的定义
class<?>和class<T>的区别
-
class<?>
作为通配泛型,当我们不知道确切类型的时候,可以做以下声明:
public class<?> clazz;
-
class<T>
和List<T>
类似,一般用在反射中,具体用法如下:
// 通过反射的方式生成 Son
// 对象,这里比较明显的是,我们需要使用强制类型转换,强制类型转换可能在运行期报ClassCastException
Son son= (Son) Class.forName("com.test.model.Son").newInstance();
//使用class<T>优化
//当传入的类型不是T时,无法创建T类型的对象,且在编译期就会报错
public static <T> T createInstance(class<T> clazz) throw Exception {
return clazz.newInstance();
}
泛型数组
创建泛型数组
- 可以声明带泛型数组的应用,但是不能直接创建带泛型的数组对象
//编译失败
new ArrayList<String>[5];
//编译成功
ArrayList[] list = new ArrayList[5];
ArrayList<String>[] listArr = list;
- 不能使用new E[],但是可以E[] elements = (E[]) new Object[capacity],参考ArrayList源码中声明Object[] elementData,而非泛型数组。
public class Fruit<T> {
//编译成功, 可以声明T[]
private T[] array;
//public Fruit(int length) {
//编译失败
//array = new T[length];
//}
//可以使用反射加类型强转
public Fruit(Class<T> clazz, int length) {
array = (T[]) Array.newInstance(clazz, length);
}
public void put(int index, T item) {
array[index] = item;
}
}
泛型的使用场景
DAO
数据库中的一张表对应java中的一个类型,对数据库数据的增删改查,都可以归结为操作这个类,针对多张不同的表,映射到多个类,所要进行的操作都相同(CRUD),因此可以使用泛型。具体用法体现在:
public class BaseDAO<T> {
//带有类型参数T的增删改查
public T get(Query query) {
...
}
...
}
public class XXXDAO extends BaseDAO<XXX> {
//直接继承增删改查方法
}