Java 泛型
一、什么是泛型
Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然而 Java 中的泛型使用了类型擦除,所以只是伪泛型。
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); }
这里list存储的是object类型对象,但却出现了类型转换异常。
1.2 概念
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。泛型的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数。
1.3 好处
- 类型安全
- 消除了强制类型的转换
1.4 类型
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(表示Java 类,包括基本的类和我们自定义的类)
- K - Key(表示键,比如Map中的key)
- V - Value(表示值)
- N - Number(表示数值类型)
- ? - (表示不确定的java类型)
- S、U、V - 2nd、3rd、4th types
二、泛型类、接口
2.1 泛型类
(1)使用语法 类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
(2)Java1.7以后,后面的<>中的具体的数据类型可以省略不写 类名<具体的数据类型> 对象名 = new 类名<>(); 菱形语法
2.2 泛型类注意事项
- 泛型类,如果没有指定具体的数据类型,此时,操作类型是Object
- 泛型的类型参数只能是类类型,不能是基本数据类型
- 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型(字节码文件相同)
定义一个泛型类
/** * 泛型类的定义 * @param <T> 泛型标识--类型形参 * T 创建对象的时候里指定具体的数据类型。 */ public class Generic<T> { //T,是由外部使用类的时候来指定。 private T key; public Generic(T key) { this.key = key; } public T getKey() { return key; } public void setKey(T key) { this.key = key; } @Override public String toString() { return "Generic{" + "key=" + key + '}'; } }
test类
//泛型类在创建对象的时候,来指定操作的具体数据类型。 Generic<String> strGeneric = new Generic<>("abc"); String key1 = strGeneric.getKey(); System.out.println("key1:" + key1); System.out.println("-----------------------------------"); Generic<Integer> intGeneric = new Generic<>(100); int key2 = intGeneric.getKey(); System.out.println("key2:" + key2); System.out.println("-----------------------------------"); //泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作。 Generic generic = new Generic("ABC"); Object key3 = generic.getKey(); System.out.println("key3:" + key3); //泛型类,不支持基本数据类型。 //Generic<int> generic1 = new Generic<int>(100); System.out.println("-----------------------------------"); //同一泛型类,根据不同的数据类型创建的对象,本质上是同一类型。 System.out.println(intGeneric.getClass()); System.out.println(strGeneric.getClass()); System.out.println(intGeneric.getClass() == strGeneric.getClass());
2.3 从泛型类派生子类
- 子类也是泛型类,子类和父类的泛型类型要一致
class ChildGeneric extends Generic
//父类 public class Parent<E> { private E value; public E getValue() { return value; } public void setValue(E value) { this.value = value; } } /** * 泛型类派生子类,子类也是泛型类,那么子类的泛型标识要和父类一致。 * 子类的泛型可以是多个 * @param <T> */ public class ChildFirst<T> extends Parent<T> { @Override public T getValue() { return super.getValue(); } }
- 子类不是泛型类,父类要明确泛型的数据类型
class ChildGeneric extends Generic
/** * 泛型类派生子类,如果子类不是泛型类,那么父类要明确数据类型 */ public class ChildSecond extends Parent<Integer> { @Override public Integer getValue() { return super.getValue(); } @Override public void setValue(Integer value) { super.setValue(value); } }
2.4 泛型接口
泛型接口的定义语法:
interface 接口名称 <泛型标识,泛型标识,…> { 泛型标识 方法名(); ..... }
- 实现类不是泛型类,接口要明确数据类型
/** * 实现泛型接口的类,不是泛型类,需要明确实现泛型接口的数据类型。 */ public class Apple implements Generator<String> { @Override public String getKey() { return "hello generic"; } }
- 实现类也是泛型类,实现类和接口的泛型类型要一致
/** * 泛型接口 * @param <T> */ public interface Generator<T> { T getKey(); } /** * 泛型接口的实现类,是一个泛型类, * 那么要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识 * @param <T> * @param <E> */ public class Pair<T,E> implements Generator<T> { private T key; private E value; public Pair(T key, E value) { this.key = key; this.value = value; } @Override public T getKey() { return key; } public E getValue() { return value; } }
测试
Apple apple = new Apple(); String key = apple.getKey(); System.out.println(key); System.out.println("---------------------------------"); Pair<String,Integer> pair = new Pair<>("count",100); String key1 = pair.getKey(); Integer value = pair.getValue(); System.out.println(key1 + "=" + value);
三、泛型方法
3.1 用法
泛型方法是在调用方法的时候指明泛型的具体类型。
(泛型类,是在实例化类的时候指明泛型的具体类别)
3.2 语法
修饰符 返回值类型 方法名(形参列表) { 方法体... }
3.3 说明
- public与返回值中间< T >非常重要,可以理解为声明此方法为泛型方法。
- 只有声明了< T >的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
- < T >表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
- 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
3.4 泛型方法与可变参数
public <E> void print(E... e){ for (E e1 : e) { System.out.println(e); } } //可变参数指的是参数可以随便指定 //Apple.print(1,2,3,4,5); //Apple.print("a","b","c","d","e");
3.5 泛型在静态方法和静态类中的问题
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
举例说明:
public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } }
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
但是要注意区分下面的一种情况:
public class Test2<T> { public static <T >T show(T one){ //这是正确的 return null; } }
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。
3.6 泛型方法总结
- 泛型方法能使方法独立于类而产生变化
- 如果static方法要使用泛型能力,就必须使其成为泛型方法
/** * 静态的泛型方法,采用多个泛型类型 * @param t * @param e * @param k * @param <T> * @param <E> * @param <K> */ 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()); }
3.7 总结
这里总结一个应用泛型类和泛型方法的例子。公司年会,要抽奖,有奖励礼物和奖励现金两种方式,构造一个泛型抽奖器。
/** * 抽奖器 * @param <T> */ public class ProductGetter<T> { static Random random = new Random(); //奖品 private T product; //奖品池 ArrayList<T> list = new ArrayList<>(); /** * 添加奖品 * @param t 奖品 */ public void addProduct(T t) { list.add(t); } /** * 抽奖 * @return */ public T getProduct() { product = list.get(random.nextInt(list.size())); return product; } /** * 定义泛型方法 * @param list 参数 * @param <T> 泛型标识,具体类型,由调用方法的时候来指定。 * @return */ public <T> T getProduct(ArrayList<T> list) { return list.get(random.nextInt(list.size())); } /** * 静态的泛型方法,采用多个泛型类型 * @param t * @param e * @param k * @param <T> * @param <E> * @param <K> */ 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()); } /** * 泛型可变参数的定义 * @param e * @param <E> */ public static <E> void print(E... e){ for (int i = 0; i < e.length; i++) { System.out.println(e[i]); } } }
测试方法:
public class Test06 { public static void main(String[] args) { ProductGetter<Integer> productGetter = new ProductGetter<>(); int[] products = {100,200,400}; for (int i = 0; i < products.length; i++) { productGetter.addProduct(products[i]); } //泛型类的成员方法的调用 Integer product = productGetter.getProduct(); System.out.println(product + "\t" + product.getClass().getSimpleName()); System.out.println("---------------------------------------------------"); ArrayList<String> strList = new ArrayList<>(); strList.add("笔记本电脑"); strList.add("苹果手机"); strList.add("扫地机器人"); //泛型方法的调用,类型是通过调用方法的时候来指定。 String product1 = productGetter.getProduct(strList); System.out.println(product1 + "\t" + product1.getClass().getSimpleName()); System.out.println("---------------------------------------------------"); ArrayList<Integer> intList = new ArrayList<>(); intList.add(1000); intList.add(5000); intList.add(3000); Integer product2 = productGetter.getProduct(intList); System.out.println(product2 + "\t" + product2.getClass().getSimpleName()); System.out.println("---------------------------------------------------"); //调用多个泛型类型的静态泛型方法 ProductGetter.printType(100,"java",true); ProductGetter.printType(false,false,true); System.out.println("---------------------------------------------------"); //可变参数的泛型方法的调用 ProductGetter.print(1,2,3,4,5); System.out.println("---------------------------------------------------"); ProductGetter.print("a","b","c"); } }
四、类型通配符
4.1 什么是类型通配符
- 类型通配符一般是使用"?"代替具体的类型实参。
- 所以,类型通配符是类型实参,而不是类型形参。
public class Test { public static void main(String[] args) { Box<Number> box = new Box<Number>(); box.setFirst(100); showBox(box); // 泛型里的类型不能多态 虽然Integer继承自Number,但是不能表现为多态 // Box<Integer> box1 = new Box<Integer>(); // box1.setFirst(10); // showBox(box1); } public static void showBox(Box<Number> box) { Number first = box.getFirst(); System.out.println(first); } //可以通过统配符?来表示 public static void showBox1(Box<? extends Number> box) { Object first = box.getFirst(); System.out.println(first); } }
4.2 类型通配符的上限
- 语法:
类/接口
<? extend 实参类型> 要求该泛型的类型,只能是实参类型,或实参类型的子类类型。
public static void showBox1(Box<? extends Number> box) { Object first = box.getFirst(); System.out.println(first); }
4.3 类型通配符的下限
- 语法: 类/接口<? super 实参类型> 要求该泛型的类型,只能是实参类型,或实参类型的父类类型。
public static void showBox1(Box<? super Integer> box) { Object first = box.getFirst(); System.out.println(first); }
这里举一个例子,TreeSet源码中比较器使用了下线类型通配符。
这里做一个 继承体系,Animal作为父类,cat继承自Animal,MiniCat继承自cat。
public class Test08 { public static void main(String[] args) { //TreeSet<Cat> treeSet = new TreeSet<>(new Comparator2()); // 添加外部比较器 TreeSet<Cat> treeSet = new TreeSet<Cat>(new Comparator1()); treeSet.add(new Cat("jerry",20)); treeSet.add(new Cat("amy",22)); treeSet.add(new Cat("frank",35)); treeSet.add(new Cat("jim",15)); for (Cat cat : treeSet) { System.out.println(cat); } } } //按Animal名字比较 class Comparator1 implements Comparator<Animal> { @Override public int compare(Animal o1, Animal o2) { return o1.name.compareTo(o2.name); } } //按Cat年龄比较 class Comparator2 implements Comparator<Cat> { @Override public int compare(Cat o1, Cat o2) { return o1.age - o2.age; } } //按Minicat level比较 class Comparator3 implements Comparator<MiniCat> { @Override public int compare(MiniCat o1, MiniCat o2) { return o1.level - o2.level; } }
Comparator1和Comparator2都是可以的。
五、类型擦除
5.1 概念
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); } }
在这个例子中,我们定义了两个ArrayList
数组,不过一个是ArrayList<String>
泛型类型的,只能存储字符串;一个是ArrayList<Integer>
泛型类型的,只能存储整数,最后,我们通过list1
对象和list2
对象的getClass()
方法获取他们的类的信息,最后发现结果为true
。说明泛型类型String
和Integer
都被擦除掉了,只剩下原始类型。
5.2 分类
- 无限制类型擦除
- 有限制类型擦除
- 擦除方法中类型定义的参数
- 桥接方法
这里举个例子,来深入理解。
泛型类 Erasure
public class Erasure<T extends Number> { private T key; public T getKey() { return key; } public void setKey(T key) { this.key = key; } /** * 泛型方法 * @param t * @param <T> * @return */ public <T extends List> T show(T t) { return t; } }
泛型接口info
/** * 泛型接口 * @param <T> */ public interface Info<T> { T info(T t); }
接口实现类infoImpl
public class InfoImpl implements Info<Integer> { @Override public Integer info(Integer value) { return value; } }
利用反射来测试
public class Test09 { Erasure<Integer> erasure = new Erasure<>(); //利用反射,获取Erasure类的字节码文件的Class类对象 Class<? extends Erasure> clz = erasure.getClass(); //获取所有的成员变量 Field[] declaredFields = clz.getDeclaredFields(); for (Field declaredField : declaredFields) { //打印成员变量的名称和类型 System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName()); } System.out.println("----------------------------------------------------------------"); //获取所有的方法 Method[] declaredMethods = clz.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { //打印方法名和方法的返回值类型。 System.out.println(declaredMethod.getName() + ":" + declaredMethod.getReturnType().getSimpleName()); } System.out.println("----------------------------------------------------------------"); Class<InfoImpl> infoClass = InfoImpl.class; //获取所有的方法 Method[] infoImplMethods = infoClass.getDeclaredMethods(); for (Method method : infoImplMethods) { //打印方法名和方法的返回值类型。 System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName()); } } }
运行结果
六、泛型与数组
数组相比于Java 类库中的容器类是比较特殊的,主要体现在三个方面:
-
数组创建后大小便固定,但效率更高
-
数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查
-
数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了
那么当数组遇到泛型会怎样? 能否创建泛型数组呢?
6.1 泛型数组的创建
- 可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象
ArrayList<String>[] listArr = new ArrayList<5>(); //会报错 ArrayList[] list = new ArrayList[5]; ArrayList<String>[] listArr = list; //不会报错 // 或者 ArrayList<String>[] listArr = new ArrayList[5];
由于泛型具有擦除机制,在运行时的类型参数会被擦除,Java只知道存储的对象是一个Object而已,而对于Java的数组来说,他必须知道它持有的所有对象的具体类型,而泛型的这种运行时擦除机制违反了数组安全检查的原则。
- 可以通过java.lang.reflect.Array的newInstance(Class,int)创建T[]数组
public class Fruit<T> { private T[] array; public Fruit(Class<T> clz, int length){ //通过Array.newInstance创建泛型数组 array = (T[])Array.newInstance(clz, length); } /** * 填充数组 * @param index * @param item */ public void put(int index, T item) { array[index] = item; } /** * 获取数组元素 * @param index * @return */ public T get(int index) { return array[index]; } public T[] getArray() { return array; } }
在上面的代码中,泛型数组的创建是创建一个 Object 数组,然后转型为 T[]。但数组实际的类型还是 Object[]。在调用 getArray()方法的时候,就报 ClassCastException 异常了,因为 Object[] 无法转型为 Integer[]。那创建泛型数组的代码 array = (T[])Array.newInstance(clz, length)为什么不会报错呢?我的理解和前面介绍的类似,由于类型擦除,相当于转型为 Object[],看上去就是没转,但是多了编译器的参数检查和自动转型。
测试:
Fruit<String> fruit = new Fruit<>(String.class,3); fruit.put(0,"苹果"); fruit.put(1,"西瓜"); fruit.put(2,"香蕉"); System.out.println(Arrays.toString(fruit.getArray())); String s1 = fruit.get(2); System.out.println(s1);
七、泛型和反射
反射允许运行时分析任意对象,如果对象是泛型类的实例,关于泛型类型参数你将得不到太多信息,因为它们已经被擦除了。
- 反射常用的泛型类 Class< T > Constructor< T >
public class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } /** * 泛型与反射 */ public class Test11 { public static void main(String[] args) throws Exception { Class<Person> personClass = Person.class; Constructor<Person> constructor = personClass.getConstructor(); Person person = constructor.newInstance(); } }