java泛型
为了让集合记住其元素的数据类型而不是都作为object处理,参数化类型,aka,泛型从java5开始引进。
泛型类、泛型接口、泛型方法、类型通配符、逆变、协变等!
java7以前构造器后面还是需要在尖括号后面加上类型的,7之后不需要了,支持菱形语法。9开始可以在创建匿名内部类时使用菱形语法。
使用var声明变量时,程序无法使用菱形语法。
泛型定义
所谓泛型就是允许在定义类、接口、方法时使用类型形参(泛型),这个类型参数将在声明变量、创建对象、调用方法时动态地指定(类型实参)。
示例:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MenuReverseController<Y> {
private Y info;
public static void main(String[] args) {
MenuReverseController<String> a1 = new MenuReverseController<>("橘子");
MenuReverseController<BigDecimal> a2 = new MenuReverseController<>(new BigDecimal("3.14"));
System.out.println(a1.getInfo());
System.out.println(a2.getInfo());
}
}
对于自定义泛型声明类的构造器,构造器还是原来的类名,不加菱形。
从泛型类派生子类
当使用这些泛型声明的接口或父类时,不能再包含泛型形参。下面是错的
如:
public class Menu extends MenuReverseController<Y>{}
<泛型参数> 应该删去或者传入具体的类型来代替Y。以下两种方式均可。
public class Menu extends MenuReverseController<String>{}
//原始类型//原始类型会被当做Object类型
public class Menu extends MenuReverseController{}
class Banana extends MenuReverseController<String>{
@Override
public String getInfo(){
return "子类"+super.getInfo();
}
Banana(String info){
super(info);
}
}
并不存在泛型类
ArrayList<String>不是新类;
MenuReverseController<String> a1 = new MenuReverseController<>("橘子");
MenuReverseController<BigDecimal> a2 = new MenuReverseController<>(new BigDecimal("3.14"));
System.out.println(a1.getClass()==a2.getClass());
输出为true;因此都会被当做同一个类处理,因此关于类的声明(静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。)
由于系统并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。
类型通配符
List是个泛型接口,使用时要传入泛型实参,但是List<String>不是List<Object>的子类,java泛型的设计原则是,只要代码在编译时候没有出现警告,就不会遇到运行时ClassCastException异常。
加入Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G<Foo>不是G<Bar>的子类型。Foo[]自动向上转型为Bar[]的方式称为型变。数组支持型变,集合不支持。
Integer[] aa = new Integer[3];
Number[] bb = aa;
bb[0]=3;
//这个会报错
bb[1]=0.5;
System.out.println(Arrays.toString(bb));
使用类型通配符
为了表示各种泛型List的父类,可以使用类型通配符,即问号?
将一个问号作为实参进行传递,但是它仅仅表示它是各种泛型的父类,并不能把元素加入其中。只可以加入null(所有引用类型的实例)。
List<?> kk =new ArrayList<>();
kk.add("1");
另一方面,程序可以返回List<?>指定索引处的元素,返回值肯定是一个Object。因此把返回值赋值给一个Object类型的变量。
设定类型通配符上限
有时候会想要设定某一类泛型List的父类。List<? extend Shape>上限为Shape,这个可以表示Listxxx的父类,因为无法确定这个受限制的通配符的具体类型,所以不能把shape对象或其子类对象加入到这个泛型集合中。主要是程序无法确定类型是哪个!
这种指定通配符上限的集合只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器无法确定集合元素实际是哪种子类型)。
这种型变方式称为“协变”!
协变泛型只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是只出不进。没有指定通配符上限,相当于上限为Object。
设定类型通配符下限
跟上面的上限相反,用A<? super Shape>。
程序可将Shape的父类泛型赋值过去。这种型变方式称为逆变!
编译器只知道集合元素是下限的父类型,但具体哪种父类类型不确定,此类泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当做Object类型处理(编译器无法确定取出的到底是哪个父类对象)。
逆变的泛型只能调用泛型类型作为参数的方法,而不能调用泛型类型作为返回值类型的方法。口诀是只进不出。
泛型方法的定义:在修饰符与返回值之间用尖括号定义,见示例代码:
public class Utils {
public static <Y> Y copy(Collection<? super Y> dest, Collection<Y> src){
Y last = null;
for(Y ele:src){
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> num = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
intList.add(66);
Integer copy = copy(num, intList);
System.out.println(num);
}
}
设定泛型形参上限
不仅允许在使用通配符时候设定上限,还可以在定义泛型形参时候设置上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
在一种极端情况下,程序需要为泛型形参设定多个上限(至多一个父类上限,多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。类上限必须位于第一位。
public class Utils<T extends Number & java.io.Serializable> {}
泛型方法
泛型方法的定义
泛型方法是指在声明方法时,定义一个或多个泛型形参。语法格式为:
修饰符 <T,S> 返回值类型 方法名(形参){
…………
}
方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型可以在整个接口、类中使用。
方法中的泛型参数无需显式传入实际类型参数,而是根据实参推断出泛型所代表的类型。
为了避免类型推断错误,可以使用通配符上限,比如:
static <T> void test(Collection<? extends T> from ,Collection<T> to){}
List<Object> ao;
List<String> as;
test(as,ao);
问:何时使用泛型方法,何时使用类型通配符呢?二者区别是什么?
泛型方法与类型通配符的区别
大多数时候都可以用泛型方法来代替类型通配符,比如:
public interface Collection<E>{
Boolean containsAll(Collection<?> c);
Boolean addAll(Collection<? extends E> c);
//转化为泛型方法:
<T> Boolean containsAll(Collection<T> c);
<T extends E> Boolean addAll(Collection<T> c);
}
虽然可以转化,但方法中的泛型形参T只使用了一次,唯一效果是可以在不同的调用点传入不同的实际类型,对于这种情况,应该使用通配符,它毕竟是被设计用来支持灵活的子类化的。
泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不该使用泛型方法。
ps:如果有需要,也可以同时使用泛型方法与通配符。支持协变的集合可以安全地取出元素。比如:Collections.copy()方法
public static <T> void copy(List<T> dest, List<? extends T> src){}
//可以改编为泛型方法
public static <T,E extends T> void copy(List<T> dest, List<E> src){}
菱形语法与泛型构造器
java也允许在构造器签名中声明泛型形参,类似于在方法签名中声明泛型形参一样。这样就产生了所谓的泛型构造器。例如:
class Foo{
public <T> Foo(T t){
System.out.println(t);
}
}
public class GenericConstructor{
public static void main(String[] args) {
//泛型构造器中T为String,自动推断
new Foo("讲义");
new Foo(200);
//显式指定泛型构造器中的T类型为String
//实际传给Foo构造器的实参也是String对象,正确!
new <String> Foo("讲义");
//显式指定泛型构造器中的T类型为String
//但实际传给Foo构造器的实参是Integer对象,错误!
new <String> Foo(55);
}
}
前面介绍了菱形语法:它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息,但是如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用菱形语法,例如:
class Foo<E>{
public <T> Foo(T t){
System.out.println(t);
}
}
public class GenericConstructor{
public static void main(String[] args) {
//构造器方法泛型未显式声明,自动推断
Foo<String> foo1 = new Foo<>(5);
//构造器方法泛型显式声明,不使用菱形语法,但可以补全类级别的泛型实际类型
Foo<String> foo2 = new <Integer> Foo<String>(200);
//xxxxxxxxx构造器方法泛型显式声明,不能使用菱形语法
Foo<String> foo3 = new <Integer> Foo<>(200);
}
}
泛型方法与方法重载
重载(两同一不同):同一个类中,同一方法名,形参列表不同才是重载!
重写(两同两小一大):
- 同一个方法名,同一个形参列表;
- 子类方法返回值与子类方法声明抛出的异常类应比父类方法返回类型更小或相等,比父类方法声明抛出的异常类更小或相等;
- 子类方法的访问权限要比父类大或相等。
因为泛型允许设定通配符上限与通配符下限,从而允许在一个类里包含如下两个方法定义:
public class MyUtils{
public static <T> void copy(Collection<T> dest, Collection<? extends T> src){}
public static <T> T copy(Collection<? super T> dest, Collection<T> src){}
}
编译器无法确定函数调用了哪个copy方法,会引起编译错误。
类型推断
类型推断主要有如下两个方面:
可通过调用方法的上下文来推断泛型目标类型。
可在方法调用链中,将推断得到的泛型传递到最后一个方法。
public class Utils<E>{
public static <Z> Utils<Z> nil(){
return null;
}
public static <Z> Utils<Z> cons(Z head, Utils<Z> tail){
return null;
}
E head(){
return null;
}
static class InterfaceTest{
public static void main(String[] args) {
//通过方法赋值的目标参数(等号前面的)来推断类型为String
Utils<String> ls = Utils.nil();
//无须使用下面语句在调用nil方法时指定泛型的类型
Utils<String> ls1 = Utils.<String>nil();
//可调用cons方法所需的参数类型来推断泛型未Integer
Utils.cons(42, Utils.nil());
//无须使用下面的语句在调用nil方法时指定泛型的类型
Utils.cons(42, Utils.<Integer>nil());
//虽然增强了泛型推断能力,但是推断不是万能的,比如下面不可:
//String s = Utils.nil().head();
//必须显式指定泛型的实际类型
String s = Utils.<String>nil().head();
}
}
}
擦除与转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。为了与老的java代码保持一致,需要使用原始类型,默认是声明该泛型形参时指定的第一个上限类型。
擦除
当把一个具有泛型信息的对象赋值给另一个没有泛型信息的变量时,所有尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转化为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。例如
public class Utils<T extends Number>{
T size;
public Utils(){}
public Utils(T size){
this.size=size;
}
public void setSize(T size){
this.size=size;
}
public T getSize(){
return this.size;
}
}
public class InterfaceTest{
public static void main(String[] args) {
Utils<Integer> a = new Utils<>(6);
Integer as = a.getSize();
//把a对象赋值给Utils变量,丢失尖括号里面的类型信息
Utils b = a;
Number size1 = b.getSize();
//下面这样会引起编译错误,因为b已经是number类型了。
//Integer size2 = b.getSize();
}
}
转换
逻辑上看List<Sting>是List的子类,如果直接把一个List对象赋值给一个List<String>对象应该引起编译错误,但实际上不会。
对于泛型而言。可以直接把一个List对象赋值给一个List<String>对象,编译器仅仅提示“未经检查的转换”,如
class InterfaceTest{
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
li.add(12);
li.add(26);
//擦除
List miAn = li;
//转换 引起未经检查的转换警告,编译运行完全正常。
List<String> ls = miAn;
//这样是对的,不会报错
//List<Integer> ls = miAn;
//但是要访问ls里的元素,会引起运行时异常。miAn变量实际引用的是List<Integer>集合,
//所以当试图把集合中的元素当成String类型的对象取出时引发ClassCastException
//System.out.println(ls.get(0));
}
}
泛型与数组
数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但是可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象。(数组可以类型向上转化)
java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],
class InterfaceTest{
public static void main(String[] args) {
List<?>[] lsp = new ArrayList<?>[10];
Object[] oa = lsp;
List<Integer> li = new ArrayList<>();
li.add(26);
oa[1]=li;
Object target = lsp[1].get(0);
if(target instanceof Integer){
Integer ss = (Integer)target;
System.out.println(ss);
}
}
}
与此类似,创建元素类型是泛型类型的数组对象也将导致编译错误,如:
<T> T[] makeArray(Collection<T> coll){
return new T[coll.size()];
}
报错:new T的T不能直接被实例化。
如何使用泛型数组?
class InterfaceTest{
public static void main(String[] args) {
GenericArray1<String> values = new GenericArray1(String.class, 2);
String[] values1 = values.getValues();
}
}
class GenericArray1 <T>{
private T[] values;
public GenericArray1(Class<T> type,int length){
values= (T[]) Array.newInstance(type, length);
}
public void setValue(T t,int position){
values[position] = t;
}
public T getValue(int position){
return (T)values[position];
}
public T[] getValues(){
return values;
}
}
反射与泛型
从jdk5之后,java的class类增加了泛型功能,从而允许使用泛型来限制class类,例如string.class的类型实际上是class<string>。如果class类型暂时未知,则使用class<?>。通过在反射中使用泛型可以避免使用反射生成的对象需要强制类型转换。
泛型和class类
使用class<T>可以避免强制类型转换,例如下面这个工厂类
public class CrabFactory{
public static Object getInstance(String clsName){
try {
//创建指定类对应的Class对象
Class cls = Class.forName(clsName);
//返回使用该class对象创建的实例
return cls.getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
但是这个对象类型是Object,因此需要转化
Date d = (Date)CrabFactory.getInstance("java.util.Date");
将上面的程序改写为使用泛型的class即可避免上述情况:
public class CrabFactory{
public static <T> T getInstance(Class<T> cls){
try {
return cls.getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
前面讲过使用Array类来创建数组时,有如下代码:
Object arr = Array.newInstance(String.class,10);
使用不方便,还得强制类型转换。该函数的定义为:
public static Object newInstance(Class<?> componentType,int... dimensions);
改写为:
public static <T> T[] newInstance(Class<T> componentType,int length);
这样就可以在调用方法后无需强制类型转换了,缺点是,这个方法只能创建一维数组,不能利用可变参数的优势了。为了利用泛型的优势,可以对Array的newInstance方法进行包装:
public class CrabFactory{
@SuppressWarnings("unchecked")
public static <T> T[] newInstance(Class<T> componentType, int length){
return (T[])Array.newInstance(componentType,length);
}
public static void main(String[] args) {
//创建一维数组
String[] arr = CrabFactory.newInstance(String.class, 10);
//创建二维数组
int[][] intArr = CrabFactory.newInstance(int[].class,5);
arr[5] = "科技大";
intArr[1] = new int[]{12,13};
System.out.println(arr[5]);
System.out.println(intArr[1][1]);
}
}
使用反射获取泛型信息
通过指定类对应的class对象,可以获取该类里包含的所有成员变量,不管该变量是使用private还是public修饰。获得了成员变量对应的Field对象后,可以容易获取成员变量的数据类型,即如下代码:
Class<?> a = f.getType();
但是这个方式只适用于普通类型的成员变量,如果该成员变量的类型是有泛型类型的类型,如Map<String,Integer>类型,则不能准确地得到该成员变量的泛型参数。
为了获取指定成员变量的泛型类型,先使用下面方法获取该成员变量的泛型类型:
Type gType = f.getGenericType();
然后将Type对象强制类型转化为ParameterizedType对象,ParameterizedType对象代表被参数化的类型,也就是增加了泛型限制的类型。该类提供了两个方法:
- getRawType():返回没有泛型信息的原始类型。
- getActualTypeArguments():返回泛型参数的类型。
public class GenericTest{
private Map<String,Integer> score;
public static void main(String[] args) throws Exception {
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
//直接使用getType()取出类型只对普通类型的成员变量有效
Class<?> a = f.getType();
//下面将只看到java.util.Map
System.out.println("score类型是:"+a);
Type gType = f.getGenericType();
//如果gType是ParameterizedType对象
if(gType instanceof ParameterizedType){
ParameterizedType pType = (ParameterizedType) gType;
//获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:"+rType);
//取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型信息是:");
for(int i=0;i<tArgs.length;i++){
System.out.println("第"+i+"个泛型类型是:"+tArgs[i]);
}
}else{
System.out.println("获取泛型类型出错!");
}
}
}
输出结果为:
score类型是:interface java.util.Map
原始类型是:interface java.util.Map
泛型信息是:
第0个泛型类型是:class java.lang.String
第1个泛型类型是:class java.lang.Integer
ps:Class是Type接口的实现类!
Type反射包下面的一个接口,该接口代表所有类型的公共高级接口。Type包含原始类型、参数化类型、数组类型、基本类型、类型变量等。