一、什么是泛型
1.1 背景
java 推出泛型以前,程序员可以构建一个元素类型为 Object 的集合,该集合能够存储任意的数据类型对象,而在使用该集合的过程中,需要程序员明确知道存储每个元素的数据类型,否则很容易引发 ClassCastException 异常。如下:
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("java");
list.add(100);
list.add(true);
for (int i=0;i<list.size();i++){
Object o = list.get(i);
String str = (String)o;
System.out.println(str);
}
}
1.2 泛型的概念
java 泛型是 JDK5 中引入的特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时监测到非法的类型数据结构。
泛型的本质是类型参数化,也就是所操作的数据类型被指定为一个参数。
// 编译期间检查类型,减少了数据类型转换
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for (int i=0;i<list.size();i++){
String str = list.get(i);
System.out.println(str);
}
}
1.3 泛型的好处
1.3.1 类型安全
因为我们指定了集合存储的具体类型,那么在编译的时候编译器就会自动检查集合存储的数据类型是否符合要求。
1.3.2 消除了强制类型的转换
因为我们指定了集合存储的具体类型,所以在接收的时候直接使用具体类型就可以了,消除了强制类型的转换。
二、泛型类和泛型接口
2.1 泛型类
2.1.1 语法定义
class 类名称 <泛型标识,泛型标识,.....>{
private 泛型标识 变量名;
.........
}
接下来我们定义一个简单的泛型类,代码如下:
// T 为泛型标识
// T 需要在创建对象的时候指定具体的数据类型
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
// setter、getter、toString()
}
2.1.2 使用语法
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
jdk1.7 以后,后面的 <> 中的具体的数据类型可以省略不写。
类名<具体的数据类型> 对象名 = new 类名<>();
public static void main(String[] args) {
// 泛型类在创建对象的时候,来指定操作的具体数据类型
Generic<String> stringGeneric = new Generic<>("abc");
String key1 = stringGeneric.getKey();
System.out.println(key1);
// 指定任何非基本数据类型都可以
Generic<Integer> IntegerGeneric = new Generic<>(100);
int key2 = IntegerGeneric.getKey();
System.out.println(key2);
// 泛型如果在创建对象的时候没有指定类型,将按照 Object 类型来操作
Generic generic = new Generic("ABC");
Object key3 = generic.getKey();
System.out.println(key3);
// 泛型类不支持基本数据类型,因为基本数据类型没有继承 Object,底下这么写编译器会报错
// Generic<int> IntegerGeneric = new Generic<int>(100);
// 同一泛型类,根据不同数据类型创建的对象,本质上是同一类型。输出为 true
System.out.println(stringGeneric.getClass() == IntegerGeneric.getClass());
}
2.1.3 注意事项
1、泛型类,如果没有指定具体的数据类型,此时的操作类型为 Object。
2、泛型的类型参数只能是类类型,不能是基本数据类型。
3、泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同的类型。
2.1.4 实战演练
接下来我们使用泛型来模拟一个抽奖器的场景,代码如下:
public class ProductGetter<T> {
Random random = new Random();
// 奖品
private T product;
// 奖品池
ArrayList<T> list = new ArrayList<>();
// 添加奖品
public void addProduct(T t) {
list.add(t);
}
// 抽奖
public T getProduct() {
product = list.get(random.nextInt(list.size()));
return product;
}
}
抽奖的代码如下所示:
public class ProductGetterTest {
public static void main(String[] args) {
// 创建抽奖器对象,指定数据类型
ProductGetter<String> productGetter = new ProductGetter<>();
String [] strProducts = {"苹果手机","华为手机","扫地机器人","咖啡机"};
// 为抽奖器填充奖品
for (int i=0;i<strProducts.length;i++){
productGetter.addProduct(strProducts[i]);
}
// 抽奖
String product = productGetter.getProduct();
System.out.println("恭喜您抽中了:"+product);
// 创建抽奖器对象,指定数据类型
ProductGetter<Integer> IntegerProductGetter = new ProductGetter<>();
Integer [] IntegerProducts = {2000,10000,500,300};
// 为抽奖器填充奖金
for (int i=0;i<IntegerProducts.length;i++){
IntegerProductGetter.addProduct(IntegerProducts[i]);
}
// 抽奖
Integer product2 = IntegerProductGetter.getProduct();
System.out.println("恭喜您抽中了:"+product2);
}
}
2.1.5 泛型类派生子类
第一种情况:子类也是泛型类,子类和父类的泛型类型要一致
class ChildGeneric<T> extends Generic<T>
先创建一个父类,代码如下:
public class Parent<E> {
private E value;
// setter、getter
}
// 此时如果不指定父类的类型参数,则父类的参数默认是 Object 类型
public class ChildFirst<T> extends Parent {
@Override
public Object getValue() {
return super.getValue();
}
}
指定父类的泛型,子类的写法如下:
// 泛型类的派生子类,子类和父类的泛型类要一致
public class ChildFirst<T> extends Parent<T> {
@Override
public T getValue() {
return super.getValue();
}
}
// 如果进行泛型扩展也没事,只需要确保扩展里面的类型有一个和父类一致就行
public class ChildFirst<T,E,K,V> extends Parent<T> {
@Override
public T getValue() {
return super.getValue();
}
}
// 使用代码如下
public static void main(String[] args) {
ChildFirst<String> childFirst = new ChildFirst<>();
childFirst.setValue("ABC");
String value = childFirst.getValue();
System.out.println(value);
}
第二种情况:子类不是泛型类,父类要明确泛型的数据类型
class ChildGeneric extends Generic<String>
public class Parent<E> {
private E value;
// setter、getter
}
// 此时如果不指定父类的类型参数,则父类的参数默认是 Object 类型
public class ChildSecond extends Parent {
@Override
public Object getValue() {
return super.getValue();
}
}
指定父类的泛型,子类的写法如下:
// 需要在这个地方明确父类的参数类型
public class ChildSecond extends Parent<String>{
@Override
public String getValue() {
return super.getValue();
}
}
// 此时是按照普通类进行操作
public static void main(String[] args) {
ChildSecond childSecond = new ChildSecond();
childSecond.setValue("ABC");
String value = childSecond.getValue();
System.out.println(value);
}
2.2 泛型接口
2.2.1 语法定义
interface 接口名称 <泛型标识,泛型标识,.....>{
泛型标识 方法名();
.........
}
2.2.2 注意事项
接下来我们创建一个简单的泛型接口,代码如下:
// 定义一个泛型接口
public interface Generator<T> {
T getKey();
}
第一种情况:实现类不是泛型类,接口要明确数据类型。
// 此时如果不指定父类的类型参数,则父类的参数默认是 Object 类型
public class Apple implements Generator{
@Override
public Object getKey() {
return null;
}
}
// 实现泛型接口的类不是泛型类,则需要明确实现泛型接口的数据类型
public class Apple implements Generator<String>{
@Override
public String getKey() {
return "hello World";
}
}
// 调用代码如下
public static void main(String[] args) {
Apple apple = new Apple();
String key = apple.getKey();
System.out.println(key);
}
第二种情况:实现类也是泛型类,实现类要和接口的泛型类一致。
// 实现类也是泛型类,实现类要和接口的泛型类一致
public class Pair<T> implements Generator<T>{
private T key;
@Override
public T getKey() {
return key;
}
}
// 如果进行泛型扩展也没事,只需要确保实现接口的泛型类泛型标识包含接口的泛型标识
public class Pair<T,V> implements Generator<T>{
private T key;
private V value;
public Pair(T key, V value) {
this.key = key;
this.value = value;
}
@Override
public T getKey() {
return key;
}
public V getValue(){
return value;
}
}
// 使用代码如下
public static void main(String[] args) {
Pair<String,Integer> pair = new Pair<>("count",100);
String key = pair.getKey();
Integer value = pair.getValue();
System.out.println("key:"+key+" value:"+value);
}
三、泛型方法
泛型类是在实例化类的时候指明泛型的具体类型。而泛型方法是在调用方法的时候指明泛型的具体类型。
3.1 语法定义
修饰符 <T,E,...> 返回值类型 方法名(形参列表){
方法体....
}
1、修饰符和返回值类型中间的 <T> 非常重要,可以理解为声明此方法为泛型方法。
2、只有声明了<T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3、<T> 表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T。
4、与泛型类的定义一样,此处的 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型。
public class ProductGetter<T> {
Random random = new Random();
// 奖品
private T product;
// 奖品池
ArrayList<T> list = new ArrayList<>();
// 添加奖品
public void addProduct(T t) {
list.add(t);
}
// 此方法非泛型方法,是成员方法,只不过是返回值类型为泛型
public T getProduct() {
product = list.get(random.nextInt(list.size()));
return product;
}
// 此方法为泛型方法
// E 是泛型标识,具体类型由调用方法的时候指定
public <E> E getProduct(ArrayList<E> list){
return list.get(random.nextInt(list.size()));
}
}
调用测试的代码如下:
public static void main(String[] args) {
ProductGetter<Integer> productGetter = new ProductGetter<>();
// 测试泛型类的成员方法
Integer [] ips = {100,200,300};
for (int i=0;i<ips.length;i++){
productGetter.addProduct(ips[i]);
}
Integer pt = productGetter.getProduct();
System.out.println(pt+"\t"+pt.getClass().getSimpleName());
System.out.println("---------------------------------------------");
// 测试泛型方法的类型为 String
// 泛型方法的调用,类型是通过调用方法的时候指定的
// 泛型方法的类型是独立于泛型类的
ArrayList<String> list = new ArrayList<>();
list.add("苹果手机");
list.add("扫地机器人");
list.add("华为手机");
String product = productGetter.getProduct(list);
System.out.println(product+"\t"+product.getClass().getSimpleName());
System.out.println("---------------------------------------------");
// 测试泛型方法的类型为 Integer
ArrayList<Integer> integerList = new ArrayList<>();
integerList.add(100);
integerList.add(200);
integerList.add(300);
Integer product2 = productGetter.getProduct(integerList);
System.out.println(product2+"\t"+product2.getClass().getSimpleName());
}
3.2 静态泛型方法
泛型的成员方法是不可以用 static 关键字进行修复的。而泛型方法是支持静态调用的,即可以用 static 关键字修饰,如下:
// 定义一个静态的泛型方法,而且泛型的成员列表可以有多个
public static <T,E,K> void printType(T t,E e,K k){
System.out.println(t+"\t"+t.getClass().getSimpleName());
System.out.println(e+"\t"+e.getClass().getSimpleName());
System.out.println(k+"\t"+k.getClass().getSimpleName());
}
调用代码如下:
public static void main(String[] args) {
// 调用多个泛型类型的静态泛型方法
ProductGetter.printType("love",520,false);
ProductGetter.printType(true,true,false);
}
3.3 泛型方法与可变参数
我们创建一个可变参数的泛型方法,如下所示:
// 定义泛型可变参数
public static <E> void print(E...e){
for(int i=0;i< e.length;i++){
System.out.println(e[i]);
}
}
调用代码如下:
public static void main(String[] args) {
ProductGetter. print(1,2,3,4,5);
ProductGetter. print("a","b","c");
}
3.4 总结
泛型方法能使方法独立于类而产生变化。
如果 static 方法要使用泛型能力,就必须使其成为泛型方法。
四、类型通配符
4.1 定义
类型通配符一般是使用 "?" 代替具体的类型实参。
所以,类型通配符是类型实参 ,而不是类型形参。
4.2 使用语法
// 创建一个泛型类
public class Box<E> {
private E first;
// setter 和 getter
}
// 测试类的代码如下
public class TestBox {
public static void main(String[] args) {
Box<Integer> box2 = new Box<>();
box2.setFirst(200);
showBox(box2);
Box<String> box3= new Box<>();
box3.setFirst("java");
showBox(box3);
}
// 创建一个类型通配符的方法,这里的 ? 表示实参,不是形参。
public static void showBox(Box<?> box){
Object first = box.getFirst();
System.out.println(first);
}
}
4.3 类型通配符上限
语法如下:
类/接口 <? extends 实参类型>
要求该泛型的类型,只能是实参类型或实参类型的子类类型。
// 创建三个类,分别两两继承。
public class Animal {
}
public class Cat extends Animal{
}
public class MiniCat extends Cat{
}
// 调用的测试代码如下
public class Test01 {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
// 只有调用这个方法是显示编译错误的
// showAnimal(animals);
showAnimal(cats);
showAnimal(miniCats);
}
// 方法的参数为 arrayList,里面的泛型采用了上限通配符。
// 泛型上限通配符,传递的集合类型,只能是 Cat 或 Cat 的子类类型。
public static void showAnimal(ArrayList<? extends Cat> list){
// 泛型上面的通配符里面的 list 是无法往里面添加元素的,因为不确定具体往里面添加的到底是哪种数据类型。
// 下面的两个 add 方法是显示编译错误的
// list.add(new Cat());
// list.add(new MinCat());
for (int i = 0; i <list.size() ; i++) {
Cat cat = list.get(i);
System.out.println(cat);
}
}
}
4.4 类型通配符下限
语法如下:
类/接口 <? super 实参类型>
要求该泛型的类型,只能是实参类型或实参类型的父类类型。
public class Test01 {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
ArrayList<Cat> cats = new ArrayList<>();
ArrayList<MiniCat> miniCats = new ArrayList<>();
showAnimal(animals);
showAnimal(cats);
// 只有调用这个方法是显示编译错误的
// showAnimal(miniCats);
}
// 类型通配符下限,要求集合只能是 Cat 或 Cat 的父类类型
public static void showAnimal(List<? super Cat> list){
// 下限通配符可以填充元素,但是不保证元素数据类型的约束要求
list.add(new Cat());
list.add(new MinCat());
// 使用 Object 来接收
for (Object o:list) {
System.out.println(o);
}
}
}
五、类型擦除
5.1 概念
泛型是 Java1.5 版本才引进的概念,在这之前是没有泛型的,但是泛型代码能够很好地和之间版本的代码兼容。那是因为泛型信息只存在于代码编译阶段,再进入 JVM 之前,与泛型相关的信息会被擦除掉,我们称之为类型擦除。
5.2 无限制类型擦除
针对于泛型类,里面的泛型在编译的过程中,即生成字节码文件的过程中,T 会被 Object 来代替,也就是类型擦除掉了。如下图。
接下来用代码来测试下,如下:
// 定义一个泛型类
public class Erasure<T> {
private T key;
// setter、getter
}
public class ErasureTest {
public static void main(String[] args) {
Erasure<Integer> erasure = new Erasure<>();
// 利用反射获取当前类的字节码文件的 Class 对象
Class<? extends Erasure> aClass = erasure.getClass();
// 获取所有的成员变量
Field [] declaredFields = aClass.getDeclaredFields();
for (Field declaredField:declaredFields) {
// 打印成员变量的名称和类型
System.out.println(declaredField.getName()+":"+declaredField.getType().getSimpleName());
}
}
}
虽然我们在使用的时候给 T 赋的 Integer 类型,但是真正编译结束以后就进行了类型擦除,变成了 Object 类型,输出如下所示:
5.3 有限制类型擦除
如果我们在创建泛型类的时候指定了泛型上限,里面的泛型在编译的过程中,即生成字节码文件的过程中,T 会被泛型上限来代替,也就是类型擦除掉了。如下图。
接下来用代码来测试下,如下:
// 定义一个泛型类,并设定泛型上限为 Number
public class Erasure<T extends Number> {
private T key;
// setter、getter
}
public class ErasureTest {
public static void main(String[] args) {
Erasure<Integer> erasure = new Erasure<>();
// 利用反射获取当前类的字节码文件的 Class 对象
Class<? extends Erasure> aClass = erasure.getClass();
// 获取所有的成员变量
Field [] declaredFields = aClass.getDeclaredFields();
for (Field declaredField:declaredFields) {
// 打印成员变量的名称和类型
System.out.println(declaredField.getName()+":"+declaredField.getType().getSimpleName());
}
}
}
虽然我们在使用的时候给 T 赋的 Integer 类型,但是真正编译结束以后就进行了类型擦除,变成了泛型的上限 Number 类型,输出如下所示:
5.4 泛型方法参数类型擦除
如果我们在创建泛型方法的时候指定了泛型上限,里面的泛型在编译的过程中,即生成字节码文件的过程中,T 会被泛型上限来代替,也就是类型擦除掉了。如下图。
如果我们不指定泛型方法的泛型上限,那么最终的 T 会被 Object 取代。
接下来用代码来测试下,如下:
// 定义一个泛型类,并设定泛型上限为 Number
public class Erasure<T extends Number> {
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
// 定义一个泛型方法
public <T extends List> T show(T t){
return t;
}
}
public class ErasureTest {
public static void main(String[] args) {
Erasure<Integer> erasure = new Erasure<>();
// 利用反射获取当前类的字节码文件的 Class 对象
Class<? extends Erasure> aClass = erasure.getClass();
// 通过反射获取所有的方法
Method [] declaredMethods = aClass.getDeclaredMethods();
for ( Method declaredMethod:declaredMethods) {
// 打印方法和方法的返回值类型
System.out.println(declaredMethod.getName()+":"+declaredMethod.getReturnType().getSimpleName());
}
}
}
一共有三个方法,分别打印出来了,其中 getKey() 方法返回值类型为 Number,因为 T 为泛型类的标识并设置了泛型上限 Number。
show() 方法的返回值类型为 List,是因为类型擦除为泛型的上限 List。
setKey() 方法的返回值为 void,这个没什么可说的。
5.5 桥接方法
桥接方法和泛型接口有关,我们在给泛型接口做擦除的时候,泛型接口里面的 T 会被转换成 Object 类型。
而接口的实现类一方面会有一个 Integer 类型的方法,另一方面会生成一个 Object 类型的桥接方法,因为接口编译完是 Object 类型的,而我们是实现这个接口,必须要对接口里面的方法进行重写。而这个桥接方法就是为了保证接口和类的实现关系的。
接下来用代码来测试下,如下:
// 定义一个泛型接口
public interface Info<T> {
T info(T t);
}
// 定义一个实现类,对泛型接口进行实现
public class InfoImpl implements Info<Integer>{
@Override
public Integer info(Integer value) {
return value;
}
}
public class ErasureTest {
public static void main(String[] args) {
Class<InfoImpl> infoClass = InfoImpl.class;
// 通过反射获取所有的方法
Method [] declaredMethods = infoClass.getDeclaredMethods();
for (Method method : declaredMethods) {
// 打印方法和方法的返回值类型
System.out.println(method.getName()+":"+method.getReturnType().getSimpleName());
}
}
}
可以看到,InfoImpl 里面有两个 info() 方法,其中 Integer 类型的 info() 方法是原有的实现方法,而 Object 类型的 info() 方法是为了保持接口实现的规范,编译器自动帮我们生成的桥接方法。
六、泛型和数组
6.1 泛型数组的创建
可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象。
泛型在编译期会做类型的擦除,而数组在整个编译器都会持有它初始的数据类型。二者在设计上就是相互冲突的,所以不允许我们创建带泛型的数组。
public static void main(String[] args) {
// 可以声明带泛型的数组引用,下面这种写法不会报编译错误
ArrayList<String>[] listArr2;
// 不能直接创建带泛型的数组对象,下面的代码报编译错误
// ArrayList<String>[] listArr = new ArrayList<String>[5];
// 使用原生的数据类型创建是没有问题的,它是非泛型的
ArrayList[] listArr3 = new ArrayList[5];
// 将匿名对象直接赋给泛型对象的引用,一般在使用这种方式创建泛型数组
ArrayList<String> [] listArr = new ArrayList[5];
ArrayList<String> strList = new ArrayList<>();
strList.add("abc");
listArr[0] = strList;
System.out.println(listArr[0]);
}
可以通过 java.lang.reflect.Array 的 newInstance(Class<T> int ) 创建 T [] 数组。
public class Fruit <T>{
// 底下这么写会报错,因为连 T 的类型都不知道,无法创建对象
// private T [] array = new T [3];
private T [] array;
public Fruit(Class<T> clz,int length){
// 通过 Array.newInstance 创建泛型数组
array = (T[])Array.newInstance(clz,length);
}
// 填充数据
public void put(int index,T item){
array[index] = item;
}
// 获取数组元素
public T getIndex(int index){
return array[index];
}
// 获取全部的元素
public T [] getArray(){
return array;
}
}
测试代码如下:
public class Test1 {
public static void main(String[] args) {
Fruit<String> fruit = new Fruit<>(String.class,3);
fruit.put(0,"苹果");
fruit.put(1,"西瓜");
fruit.put(2,"香蕉");
System.out.println(Arrays.toString(fruit.getArray()));
System.out.println(fruit.getIndex(0));
}
}
七、泛型和反射
反射常用的泛型类:Class<T>、Constructor<T> 等,如下。
public static void main(String[] args) throws Exception{
// 编写反射程序会更加方便,三步就可以创建出 person 对象
Class<Person> personClass = Person.class;
Constructor<Person> constructor = personClass.getConstructor();
Person person = constructor.newInstance();
// 非泛型使用反射会更麻烦,还得强制转换
Class personClass2 = Person.class;
Constructor constructor2 = personClass2.getConstructor();
Object o = constructor2.newInstance();
Person person2 = (Person)o;
}