十三、泛型
文章目录
- 一、泛型的理解和好处
- 二、泛型的基本介绍
- 三、泛型基本语法
- 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(){ // 返回类型使用E
return s;
}
}
*/
}
}
class Person<E>{
E s; // E 表示 s 的数据类型,该数据类型在定义Person对象的时候指定,即在编译期间,就确定E是什么类型
public Person(E s){ // E 也可以是参数类型
this.s = s;
}
public E f(){ // 返回类型使用E
return 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>{
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float aFloat) {
}
@Override
public void run(Float r1, Float r2, Integer u1, Integer u2) {
}
@Override
public Float method(Integer integer) {
return null;
}
}
// 在继承接口 指定泛型接口的类型
interface IA extends IUsb<String,Double>{
}
// 当我们去实现IA接口时,因为IA在继承IUsu接口时,
// 指定了 U 为 String,R 为Double
class AA implements IA{
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double aDouble) {
}
@Override
public void run(Double r1, Double r2, String u1, String u2) {
}
@Override
public 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 -> Array
Fish<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) { // 通配符,取出时,就是Object
System.out.println(o);
}
}
// ? extends AA 表示 上限;可以接受 AA 或者 AA 子类
public static void printCollection2(List<? extends AA> c) {
for (Object o : c) { // 通配符,取出时,就是Object
System.out.println(o);
}
}
// ? super 子类类名AA:支持AA类以及AA类的父类,不限于直接父类
public static void printCollection3(List<? super AA> c) {
for (Object o : c) { // 通配符,取出时,就是Object
System.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 block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.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() 操作,这个在前面通配符的内容小节中已经讲过。