Java泛型学习
一. 什么是泛型
泛型既类型参数化,是指通过在定义类和接口时
二. 为什么要使用泛型
1.在集合框架中通过定义集合元素类型参数,在编译时进行类型检查,防止在运行时出现类型转化异常,看如下代码片
List list=new ArrayList();
list.add(1);
list.add(2);
list.add("Java");
for (int i = 0; i <list.size(); i++) {
int a=(int) list.get(i);
}
上述代码在一个集合中同时添加了String和int两种数据类型,在JDK1.5没有引入泛型之前,所有添加的数据都会被当做Object类型处理,因此当取出数据时会发生 java.lang.ClassCastException类型转化异常。
2.通过动态的传递类型参数实现程序的可扩展性,看一个JDBC的例子`
操作用户数据的Dao:
public class UserDao {
//在这里初始化DataSource
public BookDao() {
// TODO Auto-generated constructor stub
}
//获取所有用户
@Override
public List<User> retrieveAll(){
// TODO Auto-generated method stub
return users;
}
//获取指定用户
@Override
public User retrieveById(int id){
// TODO Auto-generated method stub
return user;
}
//插入用户数据
@Override
public void insert(User user){
// TODO Auto-generated method stub
}
//删除指定数据
@Override
public void deleteById(int id){
// TODO Auto-generated method stub
}
//更新数据
public int update(Book newBook) {
// TODO Auto-generated method stub
}
}
//在这里关闭数据库连接,游标和数据处理对象
private void close(){
}
}
上面这个Dao中定义了增删改查的方法,并且在构造器中初始化DataSource对象,还定义了关闭数据库资源的方法。假如说程序中拥有n个业务数据对象,则需要定义n个这样的Dao,那么会造成构造器中初始化DataSource的代码和关闭数据库链接的代码大量的重复。可以通过模板模式抽取一个BaseDao来封装公用的操作,因为每个Dao对应不同的JavaBean类,所以只能使用泛型来动态的在编译的时候替换JavaBean的类型,以下是我的提取结果`
public abstract class BaseDao<T> {
protected DataSource dataSource;
//初始化DataSource对象
public BaseDao() {
// TODO Auto-generated constructor stub
}
public abstract List<T> retrieveAll() ;
public abstract T retrieveById(int id);
public abstract boolean insert(T t) ;
public abstract boolean deleteById(int id);
}
三.使用通配符
首先看一段代码
public static void reverse(List<?> list) {
int size = list.size();
if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
for (int i=0, mid=size>>1, j=size-1; i<mid; i++, j--)
swap(list, i, j);
} else {
ListIterator fwd = list.listIterator();
ListIterator rev = list.listIterator(size);
for (int i=0, mid=list.size()>>1; i<mid; i++) {
Object tmp = fwd.next();
fwd.set(rev.previous());
rev.set(tmp);
}
}
}
List<String> list=new ArrayList<String>();
for(int i=0;i<5;i++){
list.add("第"+i+"个元素");
}
这个Collections操作集合工具类中,反转集合中数据元素的方法。该方法的形参声明为List <?> 这个?便是通配符。假入在这里使用List<Object> list,当我们调用该方法传递List<String>类型实参时,无法编译通过,因为List<String>不是List<Object>的子类,尽管String是Object的子类,因为List<String>和List<Object>在编译时会对形参进行替换,会生成同一份字节码文件.
三.设定类型通配符的上限和下限
public class Book<T extends Number>{
private String name;
private T price;
public void setName(String name) {
this.name = name;
}
public void setPrice(T price) {
this.price = price;
}
public String getName() {
return name;
}
public T getPrice() {
return price;
}
}
Book<Object> book=new Book<Object>();
我们定义了一个Book类,这个类的price属性只能是Number类型本身或者Number的子类。然后我们初始化一个Book对象
Book<Object> book=new Book<Object>();
//编译错误
Bound mismatch: The type Object is not a valid substitute for the bounded parameter <T extends Number> of the type Book<T>
当我们传入Object类型实参时,发生编译错误,Object不是一个有效的代替对于边界参数<T extends Number>,因此在这个只能使用Integer,Float等Number类型的子类,这个Number便是通配符的上限。
public static <T> void fill(List<? super T> list, T obj) {
int size = list.size();
if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
for (int i=0; i<size; i++)
list.set(i, obj);
} else {
ListIterator<? super T> itr = list.listIterator();
for (int i=0; i<size; i++) {
itr.next();
itr.set(obj);
}
}
}
这是Collections工具类用指定元素填充指定集合的方法,其中List<? super T> list, T obj,使用super表示集合中的元素类型只能是目标元素类型的父类。
Book<Integer> book=new Book<Integer>();
List<Square> list=new ArrayList<Square>();
Rectangle rectangle=new Rectangle();
Collections.fill(list, rectangle);
class Shape{
}
class Rectangle extends Shape{
}
class Square extends Rectangle{
}
以上代码我们定义了三个类,其中Rectangle继承自Shape,Square继承自Rectangle,当我们在测试代码中向fill中传入元素为Square类型,目标元素为Rectangle类型的实参时,不能编译通过,因为Square是Rectangle的子类,而不是Square的父类。
四.使用泛型方法
现在定义一个方法向一个集合中添加一个元素
public <T> void addData(Collection<T> list,T t){
list.add(t);
}
当方法参数中包含类型参数使,需要在方法修饰符和返回值之间添加类型形参,多个类型形参之间用逗号隔开。
五.擦除和转换
首先看这样一个问题:如何把一个字符串类型的数据插入到List泛型参数为Integer类型中。`
List<Integer> list=new ArrayList<Integer>();
//list.add(e)
Class<List<Integer>> clazz=(Class<List<Integer>>) list.getClass();
try {
Method method=clazz.getDeclaredMethod("add", Object.class);
method.invoke(list, "张三");
method.invoke(list, "张三");
method.invoke(list, "张三");
method.invoke(list, "张三");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for (int i = 0; i < list.size(); i++) {
Object object=list.get(i);
System.out.println(object);
}`
通过以上代码,使用反射技术便可以通过泛型检查,插入字符串类型的数据,由此可以得出结论。
1.泛型检查只在编译期有效
2.clazz.getDeclaredMethod(“add”, Object.class);通过这行代码可以推断出从源文件到字节码阶段把源文件中的add(E e) 中的e替换成立Object
,进而可知把ArrayList中的类型形参E替换为了实参Object。
3.能把String插入List中的根本原因是因为在List集合中存储元素的是 Object[] elementData;这样一个数组元素类型是Object的数组,
因此只要跳过编译期的类型检查,便可以插入各种类型的数据,并且没有类型不对应异常,因为所有类型都是Object的子类。
泛型参数的赋值原则:
在定义类型形参时使用通配符的上限:
public class Book<T extends Interface2&Interface1>{
private String name;
private T price;
public void setName(String name) {
this.name = name;
}
public void setPrice(T price) {
this.price = price;
}
public String getName() {
return name;
}
public T getPrice() {
return price;
}
}
这是一个简单的JavaBean类, 其中Interface1和Interface2是两个接口,Book<T extends Interface2&Interface1>,这中写法中上限可以是
两个接口或者是第一个是接口第二个是类,但不能是两个都是类,或者第一个是接口第二个是类。说明Java允许统配符的允许设置多个接口上限不允许有多个类上限,并且类上限必须位于接口上限之前。
Class<Book> class1=Book.class;
TypeVariable<Class<Book>>[] typeParameters = class1.getTypeParameters();
TypeVariable<Class<Book>> typeVariable=typeParameters[0];
System.out.println(typeVariable);
try {
Field field= class1.getDeclaredField("price");
Type type=field.getType();
System.out.println(type);
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
输出:T
interface com.sunjinxi.spring.test.Interface2
通过上述代码测试发现Book中T的类型总是被替换为Book<T extends Interface2&Interface1>,中靠近extends的类型,由此可以得出结论在使用形参为通配符上限时,实参为靠近extends关键字的类型。
在定义类型形参时使用通配符的下限:
通过测试,在Java中并不支持这么做。
六.泛型数组
在Java中允许定义List<String>[]这样类型的数组,但是不允许创建ArrayList<String>[]这样的数组,说明Java不支持泛型数组,
因为在字节码阶段并不存在所有的泛型都会被擦除。java的泛型设计规范是如果一段代码在编译时系统没有产生【unchecked】未经检查的转换警告,则程序在运行时不会引发ClassCastExceptin异常。假设Java支持
ArrayList这样的类型,
1 List<String> [] lists=new ArrayList<String>[4];
2 List<String> [] lists=new ArrayList[4];
3 List<?>[] lists=new ArrayList<?>[4];
Object[] objects=lists;
List<Integer> list1=new ArrayList<Integer>();
list1.add(1);
objects[0]=list1;
//这样的代码不会有警告,但是会触发ClassCastException
String string=lists[0].get(0);
//使用3号代码,需要进行类型判断才能使代码安全
Object target=lists[0].get(0);
if (target instanceof String) {
String string=(String) lists[0].get(0);
}
则上面的1号代码片段不会有任何警告,但是会引发类型转换异常,这是违反泛型的设计原则的,所以这样的代码编译不通过,如果换成2号代码是可以编译通过的,所以引发异常是可能的不违反规则的。3号代码可以编译通过,但是只允许创建没有通配符上限的ArrayList类型,但是同样可能引发类型转换异常,需要做类型判断。
七.最后的总结
泛型只是在编译期做了类型检查,并且在编译为字节码的时候对泛型形参进行替换,根据替换规则可以知道每个包含泛型参数的Java源文件只会生成一份字节码,所以List<String>和List<Integer>会编译成一份字节码,其中泛型形参E会替换成Object,所以获取他们的Class对象也是同一个Class对象。通过使用泛型可以将要可能发生的错误 在编译期进行控制检查,在程序设计中使用泛型也可以增加结构设计的灵活性,减少重复的代码。关于泛型的总结就到这里,下一篇将对Java中的注解进行总结。