泛型,即参数化类型。
泛型的本质是为了参数化类型
参数化类型,即将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)
在没有泛型之前,我们只能定义一个类型为Object的集合,使得该集合可以存放任意的数据类型对象
然后在我们使用该集合之后,我们必须明确知道这个集合存储元素的数据类型,否则很容易引发ClassCastException异常,此时只能使用强制类型转换
但有了泛型之后,我们在定义类时使用泛型,这样子也使得这个类可以存放任意的数据类型对象
当使用这个类时通过给泛型赋值,就确定了这个类中存取的数据类型,这样子如果我们存放的数据类型不正确时,会直接在编译中报错提醒程序员写错了
由上面介绍,很自然的,泛型的好处为:
class 类名 <泛型标识, 泛型标识...> { private 泛型标识 变量名; ... }
interface 接口名 <泛型标识, 泛型标识...> { 泛型标识 方法名(); // 返回值为泛型的方法 ... }
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>(); // 从jdk1.7开始,后面的<>中具体的数据类型可省略不写: 类名<具体的数据类型> 对象名 = new 类名<>(); // 示例:使用集合类 List<String> list = new ArrayList<>();
泛型类的派生子类:
1.如果子类也是泛型类,则父类和子类的泛型类型要一致(可以扩容):
class Child<T> extends Parent<T>
2.如果子类不是泛型类,则子类继承时要指定父类泛型的具体数据类型:
class Child extends Parent<String>
泛型接口的使用(实现类):
1.实现类也是泛型类,则实现类和接口的泛型类型要一致(可以扩容):
class Child<T,E> implement 接口名<T> { // 扩容的例子 // 可以定义泛型变量与实现方法了 }
2.如果实现类不是泛型类,则要明确接口泛型的数据类型:
class Child implement 接口名<String>
泛型类的注意事项:
如果使用泛型类定义对象时,没有指定具体的数据类型,则默认数据类型为Object
泛型的类型参数只能是类类型,不能是基本数据类型
泛型类型在逻辑上可以看成多个不同的类型,但实际上都是相同类型(运行前会进行类型擦除)
因此也就不能使用泛型类型不一样的方法重载了(也可以理解为类型擦除之后都一样)
同时不能对确切的泛型类型使用instanceof操作
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public E set(int index, E element) { Objects.checkIndex(index, size); E oldValue = elementData(index); elementData[index] = element; return oldValue; } private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } }
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法能将整个类泛型化,那么就应该使用泛型方法。另外对于一个
static
的方法而言,无法访问泛型类型的参数。所以如果static
方法要使用泛型能力,就必须使其成为泛型方法。
静态方法不可以访问类上定义的泛型
如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上。
泛型类与泛型方法的区别:
泛型类:是在实例化该类时指定泛型的具体类型
泛型方法:是在调用方法时指定泛型的具体类型
权限修饰符 <T,E...> 返回值类型 方法名(形参列表){ ... } // 示例: // 表示返回值类型为E,参数类型也为E的方法: public <E> E fun(E e){ }
由于泛型是java1.5之后才引入的概念,在此之前是没有泛型的
那为什么泛型代码能向下兼容呢?
原因是泛型信息只存在代码编译阶段,在进入JVM之前与泛型相关的信息都会被擦除,也就是说,泛型信息不会进入到运行时阶段
public class A<T> { private T key; public T getKey(){ return key; } public void setKey(T key){ this.key = key; } } // 泛型擦除后: public class A { private Object key; public Object getKey(){ return key; } public void setKey(Object key){ this.key = key; } }
public class A<T extends Number> { private T key; public T getKey(){ return key; } public void setKey(T key){ this.key = key; } } // 泛型擦除后: public class A { private Number key; public Number getKey(){ return key; } public void setKey(Number key){ this.key = key; } }
public <T extends Number> T getValue(T value){ return value; } // 泛型擦除后: public Number getValue(Number value){ return value; }
public interface Info<T> { T info(T var); } public class InfoImpl implement Info<Integer> { @Override public Integer info(Integer var) { return var; } } // 类型擦除后: public interface Info { Object info(Object var); } public class InfoImpl implement Info { public Integer info(Integer var) { return var; } // 桥接方法,保证接口与类的实现关系 @Override public Object info(Object var) { return info( (Integer)var ); } }
List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); Class classStrList = strList.getClass(); Class classIntList = intList.getClass(); if(classStrList.equals(classIntList)){ System.out.println("泛型类型实质是相同的"); }
输出结果:
泛型类型实质是相同的
我们知道Ingeter是Number的一个子类,同时在类型擦除中我们也验证过Generic<Ingeter>与Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实参传入呢?在逻辑上类似于Generic<Number>和Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?
实际上,这种类似多态的形参实参传入方法是错误的,因为同一种泛型可以对应多个版本(因为参数类型是不确定的),而不同版本的泛型类实例是不兼容的。
对此,Java引入了泛型类型通配符:?
,类型通配符可以代替具体的类型实参
也就是说,?
是一个类型实参,而不是类型形参
以ArrayList构造器示例:
public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // defend against c.toArray (incorrectly) not returning Object[] // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
我们注意到,在ArrayList构造器中的参数中,写法是<? extends E>,而E是ArrayList定义类时使用的泛型标识符,那这个extends又是什么意思呢?这就涉及了通配符的上限与下限了:
语法:
类/接口<? extends 实参类型1>
这表示该泛型的类型只能是实参类型1或者是实参类型1的子类,这也就限定了类型通配符的上限
代码示例:
public void show(List<? extends Number> list) { ... // 如果只使用?,不使用extends,则定义变量list时list中的泛型参数默认为Object,此时list中的元素变成其他类型需要强转 // 但使用extends之后,要求list中的泛型参数默认为上限Number,下限则可以是任意Number的子类 }
语法:
类/接口<? super 实参类型2>
表示该泛型的类型只能是实参类型2,或者是实参类型2的父类类型
当创建泛型数组的时候,可以说明带泛型的数组引用(即等号前面),但是不能直接创建带泛型的数组对象
因为数组在编程期需要全程知道数据类型,而泛型在编译期中会有类型擦除
但是可以通过匿名数组的形式将数组引用传递给泛型数组:
ArrayList<String>[] listarr = new ArrayList<>[5]; // 报错 // 通过匿名数组引用传递给泛型数组: ArrayList<String>[] listarr = new ArrayList[5];// 后半部分为匿名非泛型数组 // 相当于: ArrayList[] list = new ArrayList[5];// 注意两个的后半部分 ArrayList<String>[] listarr = list; // 以上相当于这俩句 /** 但是要使用匿名数组引用传递给泛型数组 而不能创建普通数组再传递(防止因为类型被改来改去不一致) */
可以通过java.lang.reflect.Array的newInstance(Class, int)来创建T<>数组(Array.newInstance)
(但是在真正开发中尽量使用泛型集合代替泛型数组)
返回值 | 方法 | 描述 |
---|---|---|
static Object | newInstance(类<?> componentType(成分类型, int length) | 创建具有指定组件类型和长度的新数组 |
static Object | newInstance(类<?> componentType, int… dimensions) | 创建具有指定组件类型和尺寸的新数组 |
注意: 因为返回Object所以需要强转!
T[] arr = new T(5); // 报错,因为还不知道T类型 // 只能通过Array.newInstance来创建T<>数组 // 示例: public class Fruit<T> { private T[] arr; // 不能直接初始化 // 通过构造方法调用Array.newInstance创建泛型数组 public Fruit(Class<T> clz, int len){ arr = (T[])Array.newInstance(clz, len); } // 填充数组放入元素 public void put(int index, T item){ arr[index] = item; } // 获取数组元素 public T get(int index){ return arr[index]; } // 获取数组 public T[] getArr(){ return arr; } } // 使用上面写的类 public class Test{ public static void main(String[] agrs){ Fruit<String> fruit = new Fruit<>(Stirng.class, 3); // 创建String类型,将String.class传进去 // 接着通过上面创建的三个方法使用 } }