文章目录
- 一、泛型的理解和好处
- 二、泛型的基本介绍
- 三、泛型基本语法
- 3.1 语法介绍
- 3.2 泛型使用的注意事项和细节
- 四、自定义泛型
- 4.1 自定义泛型类
- 4.2 自定义泛型接口
- 4.3 自定义泛型方法(成员方法 or 静态方法)
- 五、泛型的继承和通配符
- 六、类型擦除
- 6.1 类型擦除机制介绍
- 6.2 类型擦除带来的局限性
- 七、JUnit
- 八、泛型细节
- 8.1 泛型类或者泛型方法中,不接受 8 种基本数据类型
- 8.2 对泛型方法的困惑
- 8.3 Java 不能创建具体类型的泛型数组
一、泛型的理解和好处
package com;import java.util.ArrayList;/*** @author Gao YongHao* @version 1.0*/
public class Generic01 {public static void main(String[] args) {// 使用传统方法来解决 ===> 使用泛型// 解读// 1. 当我们 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是Dog类型(细节后面说)// 2. 如果编译器发现添加的类型,不满足要求,就会报错ArrayList<Dog> dogs = new ArrayList<Dog>();dogs.add(new Dog("旺财", 10));dogs.add(new Dog("发财", 1));dogs.add(new Dog("小黄", 5));// 假如不小心添加了一只猫,编译器会报错
// dogs.add(new Cat("招财猫", 8));for (Dog o : dogs) {System.out.println(o.getName() + "-" + o.getAge());}}
}class Cat {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Cat(String name, int age) {this.name = name;this.age = age;}
}class Dog {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Dog(String name, int age) {this.name = name;this.age = age;}
}
二、泛型的基本介绍
泛型
是一种表示数据类型的数据类型。是数据的多态表现。在类声明时使用,可以把数据类型
作为参数传入类中(即:参数化类型)泛型
的具体数据类型是在编译期间
就确定下来的
public class Genric03{public static void main(String[] args){Person<String> person = new Person<String>("韩顺平教育");/*可以这要理解,上面的Person类class Person{String s; // E 表示 s 的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型public Person(String s){ // E 也可以是参数类型this.s = s;}public String f(){ // 返回类型使用Ereturn s;}
}*/}}class Person<E>{E s; // E 表示 s 的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型public Person(E s){ // E 也可以是参数类型this.s = s;}public E f(){ // 返回类型使用Ereturn s;}
}
三、泛型基本语法
3.1 语法介绍
3.2 泛型使用的注意事项和细节
四、自定义泛型
4.1 自定义泛型类
package com.generic;/*** @author Gao YongHao* @version 1.0*/
public class Generic02 {public static void main(String[] args) {}
}// 解读
// 1. Tiger 后面有泛型,索引我们把 Tiger 成为自定义泛型类
// 2. T,R,M 泛型的标识符,一般是单个大写字母
// 3. 泛型标识符可以有多个
// 4. 普通成员可以使用泛型(属性,方法)class Tiger<T,R,M>{String name;T t; // 属性使用泛型R r;M m;// 因为数组在new的使用不能确定T的类型,就无法在内存开空间
// T[] ts = new T[8];// 因为静态是和类相关的,在类加载时,对象还没有创建// 所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化
// static R r2;
// public static void m1(M m){
//
// }public Tiger(String name, T t, R r, M m) { // 构造器使用泛型this.name = name;this.t = t;this.r = r;this.m = m;}public String getName() {return name;}public void setName(String name) {this.name = name;}public T getT() { // 返回类型可以使用泛型return t;}public void setT(T t) { // 方法使用到泛型this.t = t;}public R getR() {return r;}public void setR(R r) {this.r = r;}public M getM() {return m;}public void setM(M m) {this.m = m;}
}
4.2 自定义泛型接口
package com.generic;/*** @author Gao YongHao* @version 1.0*/
public class Generic03 {public static void main(String[] args) {}
}/*** 泛型接口使用说明* 1. 接口中,静态成员也不能使用泛型* 2. 泛型接口的类型,在继承接口或者实现接口时确定* @param <U>* @param <R>*/
interface IUsb<U, R> {// 普通方法中,可以使用接口泛型R get(U u);void hi(R r);void run(R r1, R r2, U u1, U u2);// 在 jdk8 中,可以在接口中,使用默认方法,也是可以使用泛型default R method(U u) {return null;}}// 实现接口时,直接指定泛型接口的类型
// 给 U 指定 Integer,给 R 指定了 Float
// 所以,当我们实现了IUsb,会使用Integer替换U,使用Float替换 R
class BB implements IUsb<Integer,Float>{@Overridepublic Float get(Integer integer) {return null;}@Overridepublic void hi(Float aFloat) {}@Overridepublic void run(Float r1, Float r2, Integer u1, Integer u2) {}@Overridepublic Float method(Integer integer) {return null;}
}// 在继承接口 指定泛型接口的类型
interface IA extends IUsb<String,Double>{
}// 当我们去实现IA接口时,因为IA在继承IUsu接口时,
// 指定了 U 为 String,R 为Double
class AA implements IA{@Overridepublic Double get(String s) {return null;}@Overridepublic void hi(Double aDouble) {}@Overridepublic void run(Double r1, Double r2, String u1, String u2) {}@Overridepublic Double method(String s) {return null;}
}
4.3 自定义泛型方法(成员方法 or 静态方法)
- 注意:
泛型方法
与使用泛型
的区别
package com.generic;import java.util.ArrayList;/*** @author Gao YongHao* @version 1.0*/
public class Generic04 {public static void main(String[] args) {Car car = new Car();// 当调用方法时,编译器会根据传入参数,确定泛型的具体类型car.fly("宝马", 100);// 测试// T -> String, R -> ArrayFish<String, ArrayList> fish = new Fish<>();fish.hello(new ArrayList(),11.3f);}
}// 泛型方法,可以定义在普通类中,也可以定义在泛型类中
class Car {// 普通类public void run() {} // 普通方法// 说明// 1. <T,R> 就是泛型// 2. 是提供给 fly 使用的public <T, R> void fly(T t, R r) { // 泛型方法}
}class Fish<T, R> { // 泛型类public void run() {}public <U, M> void eat(U u, M m) { // 泛型方法}// 说明// 1. 下面hi方法不是泛型方法// 2. 是 hi 方法使用了类声明的 泛型public void hi(T t) {}// 泛型方法,可以使用类声明的泛型,也可以使用自己声明的泛型public <K> void hello(R r, K k) {}}
五、泛型的继承和通配符
package com.generic;import java.util.List;/*** @author Gao YongHao* @version 1.0*/
public class Generic05 {public static void main(String[] args) {}public static void printCollection1(List<?> c) {for (Object o : c) { // 通配符,取出时,就是ObjectSystem.out.println(o);}}// ? extends AA 表示 上限;可以接受 AA 或者 AA 子类public static void printCollection2(List<? extends AA> c) {for (Object o : c) { // 通配符,取出时,就是ObjectSystem.out.println(o);}}// ? super 子类类名AA:支持AA类以及AA类的父类,不限于直接父类public static void printCollection3(List<? super AA> c) {for (Object o : c) { // 通配符,取出时,就是ObjectSystem.out.println(o);}}}
六、类型擦除
6.1 类型擦除机制介绍
https://blog.csdn.net/briblue/article/details/76736356 Java 泛型,你了解类型擦除吗?
- 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容(之前版本借助
Object
类型与强制转换
作为泛型解决方法) - 这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
- 通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
可能同学会问,那么类型 String 和 Integer 怎么办?
答案是 泛型转译
public class Erasure <T>{T object;public Erasure(T object) {this.object = object;}}
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
打印结果是
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 Erasure<T>
这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
打印结果是
Field name object type:java.lang.Object
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码
public class Erasure <T extends String>{
// public class Erasure <T>{T object;public Erasure(T object) {this.object = object;}}
现在再看测试结果
Field name object type:java.lang.String
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>
则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>
则类型参数就被替换成类型上限。
所以,在反射中。
public class Erasure <T>{T object;public Erasure(T object) {this.object = object;}public void add(T object){}}
add() 这个方法对应的 Method 的签名应该是 Object.class。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){System.out.println(" method:"+m.toString());
}
打印结果是
method:public void com.frank.test.Erasure.add(java.lang.Object)
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class)
否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。
6.2 类型擦除带来的局限性
- 类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性
- 理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
public interface List<E> extends Collection<E>{boolean add(E e);
}
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于
boolean add(Object obj);
那么,利用反射,我们绕过编译器去调用 add 方法。
public class ToolTest {public static void main(String[] args) {List<Integer> ls = new ArrayList<>();ls.add(23);
// ls.add("text");try {Method method = ls.getClass().getDeclaredMethod("add",Object.class);method.invoke(ls,"test");method.invoke(ls,42.9f);} catch (NoSuchMethodException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (SecurityException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalAccessException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IllegalArgumentException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (InvocationTargetException e) {// TODO Auto-generated catch blocke.printStackTrace();}for ( Object o: ls){System.out.println(o);}}}
打印结果是:
23
test
42.9
可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。
七、JUnit
八、泛型细节
8.1 泛型类或者泛型方法中,不接受 8 种基本数据类型
所以,你没有办法进行这样的编码。
List<int> li = new ArrayList<>();
List<boolean> li = new ArrayList<>();
需要使用它们对应的包装类。
List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();
8.2 对泛型方法的困惑
public <T> T test(T t){return null;
}
有的同学可能对于连续的两个 T 感到困惑,其实 <T>
是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 你可以相像一下,如果 test() 这样被调用
test("123");
那么实际上相当于
public String test(String t);
8.3 Java 不能创建具体类型的泛型数组
这句话可能难以理解,代码说明。
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List<Integer>
和 List<Boolean>
在 jvm 中等同于 List<Object>
,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>
类型还是 List<Boolean>
类型。
但是
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于无限定通配符却可以,前面讲过 ?
代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。