泛型和注解是框架技术必备的技能
- 5 泛型
- 5.1泛型理解
- 5.1.1 泛型概念
- 5.1.2 泛型的特点
- 5.1.3 如何理解Java中的泛型是伪泛型?
- 5.1.4 泛型的价值
- 5.2 泛型语法
- 5.2.1 泛型类
- 5.2.2 泛型接口
- 3.2.3 泛型方法
- 3.2.4泛型的上下边界
- 3.2.5创建泛型数组
- 5.3泛型应用场景
- 5.3.1数据库操作组件封装
- 5.3.2 数据库分页组件封装
- 5.3.3 第三方中间件
5 泛型
5.1泛型理解
5.1.1 泛型概念
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。本文综合多篇文章后,总结了Java 泛型的相关知识,希望可以提升你对Java中泛型的认知效率。
5.1.2 泛型的特点
泛型只在编译阶段有效。看下面的代码:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();if(classStringArrayList.equals(classIntegerArrayList)){Log.info("泛型测试,类型相同");
}
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
5.1.3 如何理解Java中的泛型是伪泛型?
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
擦除原则
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性
如何擦除类型?
(1) 无限制的转化为Object
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。
(2). 升级为上限
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
(3).擦除方法定义中的类型参数
除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
如何证明被擦除了呢?
看测试代码
@Testpublic void t1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {ArrayList<Integer> list = new ArrayList<Integer>();list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer// list.add("字符串");//编译器语法检查会报错,因为引用了泛型,无法编译通过//通过反射注入值,编译能通过list.getClass().getMethod("add", Object.class).invoke(list, "字符串");for (Object o:list){System.out.println(o);}}
在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型
5.1.4 泛型的价值
既然编译后会擦除泛型,那为什么又要使用泛型呢,不是没事找事吗,有以下原因:
(1)程序的健壮和安全性
以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
有了泛型后,出现不符合预期的代码就会编译不通过。相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。
(2)避免了不必要的装箱、拆箱操作,提高程序的性能
以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型强制转换,大量的开箱拆箱工作将会降低代码性能。
(3)避免重复代码,提升程序优雅性
当我们为处理不同的对象,必须增加不同方法或者类时,用泛型可以避免这些,必须当你需要创建一个通用的数据结构,例如列表、栈、队列、字典等,这些结构可以处理各种类型的数据时,可以使用泛型类,用泛型代替Object
5.2 泛型语法
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
5.2.1 泛型类
泛型类:把泛型定义在类上
语法:把类体里面要用的泛型类型,在类后声明,可以1个或者多个,泛型的名字无限制
public class 类名 <泛型类型1,…> { }
注意事项:
- 泛型类型必须是引用类型(非基本数据类型)
- 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
- 参数名称可以任意
当然,这个后面的参数类型也是有规范的,通常类型参数我们都使用大写的单个字母表示:
实例1:单个泛型
public class Pair<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}
}
使用泛型类
@Testpublic void t2() {Pair<String> pair = new Pair<>();pair.setValue("www");String r = pair.getValue();Pair<Integer> pair1=new Pair<>();Integer i=pair1.getValue();}
实例2:多个泛型
public class MoreGenerics <k,v>{private k id;private v name;public k getId() {return id;}public void setId(k id) {this.id = id;}public v getName() {return name;}public void setName(v name) {this.name = name;}
}
使用
@Testpublic void t4(){MoreGenerics<String,String> mg=new MoreGenerics<>();mg.setId("1");mg.setName("jzk");MoreGenerics<Integer,String> mg2=new MoreGenerics<>();Integer id=mg2.getId();}
5.2.2 泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
public interface 接口名 <泛型类型1,…> { }
例如
public interface GenericeServeice<T> {public T getKey();
}
实现接口的类,有三种方式
- 指定具体类型:就是在实现接口时,明确指定泛型参数的具体类型;
- 保留泛型参数:在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数。
- 保留泛型参数:并增加新的泛型类型
注意语法
class A implements GenericeServeice<String>{}//指定具体类型
class B<T> implements GenericeServeice<T>{}//保留泛型
class C<K,T> implements GenericeServeice<T>{}//保留并新增泛型
接口
ublic interface GenericeServeice<T> {public T getKey();
}
实现类
public class GenericeServeiceImp {//1.就是在实现接口时,明确指定泛型参数的具体类型;注意 A implements B <具体类型>{}class A implements GenericeServeice<String>{@Overridepublic String getKey() { // T getKey() T 用具体类型 String代替return "大太阳";}}//2.在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数class B<T> implements GenericeServeice<T>{private T id;@Overridepublic T getKey() {return id;}public void setId(T id){this.id=id;}}//3.继承了接口的泛型参数,并新增泛型class C<K,T> implements GenericeServeice<T>{private T id;private K name;@Overridepublic T getKey() {return id;}public void setName(K name){this.name=name;}public K getName(){return name;}public void setKey(T id){this.id=id;}}@Testpublic void t1(){A a=new A();String key=a.getKey();B<Integer> b=new B<>();b.setId(3);Integer id=b.getKey();C<String,Integer> c=new C<>();c.setName("奎哥");c.setKey(3);C<Number,Integer> c1=new C<>();c1.setName(34);c1.setKey(3);System.out.println("A.getKey()="+key);System.out.println("B.getKey()="+id);}}
3.2.3 泛型方法
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
注意事项:
- 泛型方法必须标注在方法修饰符和返回值之间<泛型变量…>,和对应类是否时泛型无关,彼此独立
泛型方法声明:
public class GeneralM {/***方法无返回值,入参是一个泛型参数*/public <T> void m(T t){System.out.println(t.getClass().getName());}/***方法无入参,返回值是一个泛型变量*/public <T> T m1(){T t=null;return t;}/***入参和返回值都是一个泛型变量*/public <T> T m2(T t){return t;}/***声明多个泛型*/public <K,V> void m3(K k,V v){System.out.println(k.getClass().getName());System.out.println(v.getClass().getName());}/***Class<T>这个表示泛型T的具体类型是Class*/public <T> T getObj(Class<T> c) throws InstantiationException, IllegalAccessException {T t= c.newInstance();return t;}/***写一个函数把数组转化为List*/public <T> List<T> toList(T[] arrs){List<T> list=new ArrayList<>();for (T arr : arrs) {list.add(arr);}return list;}}
泛型方法使用
@Testpublic void t5() throws InstantiationException, IllegalAccessException {GeneralM gm=new GeneralM();//对应 public <T> void m(T t){}System.out.println("对应 public <T> void m(T t){}=============");gm.m("d");gm.m(new Pair());gm.m(1);//对应 public <T> T m1(){}System.out.println("对应 public <T> T m1(){}=============");String ds= gm.m1();Integer i=gm.m1();//对应 public <T> T m2(T t){}System.out.println("对应 public <T> T m2(T t){}=============");Integer i1=gm.m2(3);String s=gm.m2("d");//对应 public <K,V> void m3(K k,V v){}System.out.println("对应 public <K,V> void m3(K k,V v){}=============");gm.m3(12, "大哥");//对应 public <T> T getObj(Class<T> c)System.out.println("对应 public <T> T getObj(Class<T> c)=============");VO vo= gm.getObj(VO.class);vo.setId(1);vo.setName("dd");System.out.println(vo);//对应 public <T> List<T> toList(T[] arrs)System.out.println("对应 public <T> List<T> toList(T[] arrs)");Integer[] arr1={1,2,3};List<Integer> list=gm.toList(arr1);String[] arr2={"1","2","3"};List<String> list2=gm.toList(arr2);//List<Integer> list3=gm.toList(arr2);//编译报错}
注意:泛型方法使用,并没有泛型类或者接口用<>确定类型,和正常的方法一样,因为方法不会实例化
案例详解
案例说明:
-
定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
-
Class的作用就是指明泛型的具体类型,而Class类型的变量c,可以用来创建泛型类的对象。
为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。 -
泛型方法要求的参数是Class类型,而Class.forName()方法的返回值也是Class,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class类型的对象,因此调用泛型方法时,变量c的类型就是Class,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
当然,泛型方法不是仅仅可以有一个参数Class,可以根据需要添加其他参数。
我们再看解析json的中间件Gson的源码,加深理解:
Gson
泛型类和泛型方法往往都在一起,容易让初学者混淆,查看实例
public class GenericTest {//这个类是个泛型类,在上面已经介绍过public class Generic<T>{ private T key;public Generic(T key) {this.key = key;}//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。//所以在这个方法中才可以继续使用 T 这个泛型。public T getKey(){return key;}/*** 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。public E setKey(E key){this.key = keu}*/}/** * 这才是一个真正的泛型方法。* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T* 这个T可以出现在这个泛型方法的任意位置.* 泛型的数量也可以为任意多个 * 如:public <T,K> K showKeyName(Generic<T> container){* ...* }*/public <T> T showKeyName(Generic<T> container){System.out.println("container key :" + container.getKey());//当然这个例子举的不太合适,只是为了说明泛型方法的特性。T test = container.getKey();return test;}//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。public void showKeyValue1(Generic<Number> obj){Log.d("泛型测试","key value is " + obj.getKey());}//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类public void showKeyValue2(Generic<?> obj){Log.d("泛型测试","key value is " + obj.getKey());}/*** 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。public <T> T showKeyName(Generic<E> container){...} *//*** 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。* 所以这也不是一个正确的泛型方法声明。public void showkey(T genericObj){}*/public static void main(String[] args) {}
}
泛型方法有什么优势
泛型类必须要在实例化对象时指明具体的泛型的替代类型,不同的化必须实例化一个新对象如:
List<String> list=new ArrayList<>();
List<Integer> list=new ArrayList<>();
但泛型方法就更为简单,不需要再实例化对象,也不需要专门用<>声明数据类型,更为灵活,如上面例子提到的:
//对应 public <T> List<T> toList(T[] arrs)System.out.println("对应 public <T> List<T> toList(T[] arrs)");Integer[] arr1={1,2,3};List<Integer> list=gm.toList(arr1);String[] arr2={"1","2","3"};List<String> list2=gm.toList(arr2);//List<Integer> list3=gm.toList(arr2);//编译报错
3.2.4泛型的上下边界
- 上限
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
语法 :泛型变量 extends 具体类型
测试代码:这里泛型参数T只能是Number及其子类
public class Info <T extends Number>{private T id;public Info(T id){this.id=id;}public static void main(String[] args) {Info<Integer> info1=new Info<>(12); //okInfo<Float> info2=new Info<>(12.45f); //ok// Info<String> info3=new Info<>("123"); //编译出错}
}
- 下限
class Info<T>{private T var ; // 定义泛型变量public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}public String toString(){ // 直接打印return this.var.toString() ;}
}
public class GenericsDemo21{public static void main(String args[]){Info<String> i1 = new Info<String>() ; // 声明String的泛型对象Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象i1.setVar("hello") ;i2.setVar(new Object()) ;fun(i1) ;fun(i2) ;}public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类System.out.print(temp + ", ") ;}
}
小结
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
通配符 ?
通配符?表示任意的,一般和泛型的上下界限制一起搭配使用
3.2.5创建泛型数组
看代码
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型 List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建 List<?>[] list15 = new ArrayList<?>[10]; //OK List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
上面都不是创建泛型数组的最佳方式
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下
public class ArrayWithTypeToken<T> {private T[] array;public ArrayWithTypeToken(Class<T> type, int size) {array = (T[]) Array.newInstance(type, size);}public void put(int index, T item) {array[index] = item;}public T get(int index) {return array[index];}public T[] create() {return array;}
}
//...ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
5.3泛型应用场景
泛型最到的特征是在定义类时并不指定类里的具体参数,这样就可以把一些共性抽象出来,在泛型之前,我们只能把具体参数对象抽象成Object类,在使用时再强制转化成具体对象,但其健壮性和安全性存在一定问题,所以泛型在做通用基础组件里得到广泛的应用,在实际业务场景中,我们在结合类的反射,这样就能形成通用的封装类,我们查看很多三方组件,都有大量的泛型应用。
5.3.1数据库操作组件封装
BaseDao定义了基本的数据库增删查改, 之后可以继承该泛型类,实现各自的增删查改,或者使用超类的增删查改,同时每个继承类还能增加自己的操作:
思路:
利用类得反射原理,把传入对象的字段属性和值都读出来,动态生成SQL语句,为简单我们假设类名和属性名称和数据库表与字段一一对应
示意代码
public class BaseDAO <T>{//数据库连接信息private String dbURL="";public void save(T t){Class cls=t.getClass();//利用反射原理获得T的属性和值,动态生成SQL语句}public T getByKey(Integer id,Class<T> c) {try{T o=c.newInstance();//把sql查询值利用反射机制注入objSystem.out.println("执行getByKey()");return o;}catch (Exception e){throw new RuntimeException("映射出错");}}
}
继承基类
public class StudentDAO extends BaseDAO<Student>{
}
public class OrderDAO extends BaseDAO<Order>{
}
使用
@Testpublic void t9() {StudentDAO sdao=new StudentDAO();sdao.save(new Student());Student st=sdao.getByKey(1, Student.class);OrderDAO odao=new OrderDAO ();odao.save(new Order());Student st=sdao.getByKey(1, Order.class);}
5.3.2 数据库分页组件封装
在使用java对数据库操作时候,很常见的一个功能分页操作,java接收的常常是一个count和相应的记录列表,然后,一般的定bean的方法如下:
vo类 如:order
@Data
public class Order {private Integer id;private String orderCoder;private Integer goodId;private Integer buyNum;
}
分页类:
一般包含分页的公共数据和当前记录数据
@Data
public class PageSpitOrder {//分页通用数据private Integer pageNum;//页数private Integer curPage;//当前页private Integer pageSize;//每页条数private List<Order> list;//当前当前页数据
}
如果我们再有商品分页,最简单的方法再定义一个PageSpitGood的封装类,这样肯定就不通用了,泛型以前我们会一定义一个List类来存储当前页数据,在使用类里在强转到具体对象。
用泛型就优雅得多:
代码示意图
public class PageSpitComm<T> {//分页通用数据private Integer pageNum;//页数private Integer curPage;//当前页private Integer pageSize;//每页条数private List<T> list;//当前当前页数据,使用T类型public List<T> getList(){return list;}
}
使用代码
@Testpublic void t8(){PageSpitComm<Order> pg1=new PageSpitComm<>();List<Order> list1=pg1.getList();PageSpitComm<VO> pg2=new PageSpitComm<>();List<VO> list2=pg2.getList();}
5.3.3 第三方中间件
Gson.fromJson,原理解析json字符串,反映射注入类中
mybatis中间件
BaseMapper源码