等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与++模板机制实现方式之间的重要区别。
2.1 🐙 擦除机制
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
类型擦除的主要过程如下:
- 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
- 移除所有的类型参数。
那么再白话一点,就是一个类的泛型<>中如果用不同类型(比如String,比如Integer)创建出不同的对象,那他们这里的<>中编译器最后都会转化为Object类型处理。
还不能理解?好吧,上代码
/**
* @author Gremmie102
* @date 2022/4/27 15:44
* @purpose : 类型擦除的实验
*/
class MyGeneric<T> {
T value;
public MyGeneric(){
}
public MyGeneric(T val){
value = val;
}
public T getValue(){
return value;
}
public void setValue(T val){
value = val;
}
}
public class TestDemo2 {
public static void main(String[] args) {
MyGeneric<String> generic1 = new MyGeneric<>("Gremmie");
MyGeneric<Integer> generic2 = new MyGeneric<>(19);
if (generic1.getClass()==generic1.getClass()){
System.out.println("Their class is same");
}else {
System.out.println("Their class is different");
}
}
}
运行结果:
C:\Users\lenovo\.jdks\openjdk-18\bin\java.exe "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=63584:E:\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath E:\JAVAcode\gyljava\GenericTest\out\production\GenericTest TestDemo2
Their class is same
Process finished with exit code 0
再举个例子:
数组要记住元素类型, 类型擦除会使得数组忘记自己定义的原来的泛型类型是什么, 放到List中就是这样的
List list = new ArrayList();
list.add(“a”);
list.add(1);
String o1 = (String) list.get(0);
Integer o2 = (Integer) list.get(1);
这段能正常执行, 相当于在ArrayList中放了int类型, 就是因为类型擦除导致的,
那么我们从上面的代码运行结果可以看出,两次分别传入了不同的类型,一个是String,另外一个是Integer,但是最后我们通过getClass获取两个对象的信息时,发现两个对象的类是一样的,这就是泛型的擦除机制,关于反射什么的,我也不太懂,生硬讲解反而不好,等以后学到相关知识再来完善。
2.2 🤖泛型不能创建数组
这里我找了一篇文章,讲解的很好,大家可以去看一看这位大佬的观点
👉为什么泛型不能创建数组
那么我为大家总结一下
如果允许创建泛型数组,将能在数组p里存放任何类的对象,并且能够通过编译,因为在编译阶段p被认为是一个Object[ ],也就是p里面可以放一个int,也可以放一个Pair,当我们取出里面的int,并强制转换为Pair,调用它的info()时会怎样?java.lang.ClassCastException!这就违反了泛型引入的原则。所以,Java不允许创建泛型数组。
因为 在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。
List<Integer> [] lists = new ArrayList<>[];
//后面的这个<>写上去是报错的
//正确的写法应该是:
List<Integer> [] lists1 = new ArrayList[];
//如果允许第一种写方法的存在,那么多态的存在会导致运行时出现ClassCast异常
//Object类是所有类的父类,所以可以:
lists = [list1,list2,....]其中的list都是Integer泛型的
Object[] objects = lists;
listStr = new ArrayList<String>;
listStr.add("...")
//此时如果把listStr赋值到objects的第一个里,原则上是没有任何问题的
//因为Object是所有类的父类
//但是这个地方应该是Integer泛型的ArrayList,如果这样允许存在,
// 要么运行时出现类转化异常
// 要么lists定义时的泛型推断就失去了意义
2.3 🦚泛型的优点
- 泛型是将数据类型参数化,进行传递
- 使用 表示当前类是一个泛型类。
- 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换
看不懂没关系,继续往下翻,会有更新的理解!
3. 😺 泛型类
那么终于来到我们实打实的操作了
3.1 🐺 泛型类的使用
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
MyArray list = new MyArray();
泛型只能接受类,所有的基本数据类型必须使用包装类!
我们也可以这样写
MyArray list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String
编译器将自动为我们推导出后面new 出来的对象中<>应该是什么类型
3.2 🐱 上界
我们在写一个带有泛型的类时,通常都希望传进<>的类有着一定的限制,比如父类子类继承关系,那我们将用到extends来定义一个上界,来限制泛型
class 泛型类名称<类型形参 extends 类型边界> {
...
}
这里即为只接受 Number 的子类型作为 E 的类型实参
有界的类型参数:
可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界
3.3 🐶 裸类型
裸类型我们用的不多,只是用来适应一些比较老的API的
例如:MyArray list = new MyArray();
尽量规范语句,不要轻易使用裸类型
3.4 🦊 泛型标记符
常用的 有T,E,K,V,?
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的:
? 表示不确定的 java 类型
T (type) 表示具体的一个java类型
K V (key value) 分别代表java键值中的Key Value
E (element) 代表Element
那么?这个泛型通配符下面会详细讲
4. 🐯 泛型方法
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
/**
* @author Gremmie102
* @date 2022/4/27 16:48
* @purpose :
*/
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组
System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组
System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组
}
}
5. 🦒 泛型接口
/**
* author : northcastle
* createTime:2021/10/20
* 自定义普通类,用来做泛型的实际参数
*/
public class Cat {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
public interface SayHello<T> {
//声明了一个带有泛型类型的方法
T printT(T t);
}
public class SayHelloImplA<T,E> implements SayHello<T>{
private E e;
@Override
public T printT(T t) {
System.out.println("This is SayHelloImplA "+t);
return t;
}
//getter/setter方法
public E getE() {
return e;
}
public void setE(E e) {
this.e = e;
}
//自定义普通的方法
public void sayHelloE(){
System.out.println("Hello E : "+e);
}
}
public class Application {
public static void main(String[] args) {
//1.创建一个 泛型实现类的 对象
SayHelloImplA<String, Integer> implA = new SayHelloImplA<>();
implA.printT("我是泛型类-实现类");
implA.setE(100);
implA.sayHelloE();
System.out.println("==================");
//2.创建一个 普通实现类的 对象
CommonImplB commonImplB = new CommonImplB();
Cat huahua = new Cat("huahua");
commonImplB.printT(huahua);
}
}
6. 🐮 泛型通配符”?”
那么我们创建出这么几个类
class A{
}
class B extends A{
}
class C extends A{
}
类B和类C都继承于类A。
那么再来实例化出两个List变量
List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();
我们这样做,都会报错
listA = listB;
listB = listA;
在 listA 中你可以插入 A类的实例,或者A类子类的实例(比如B和C)。如果下面的语句是合法的:
List listB = listA;
那么listA 里面可能会被放入非B类型的实例。
泛型通配符可以解决这个问题。泛型通配符主要针对以下两种需求:
- 从一个泛型集合里面读取元素
- 往一个泛型集合里面插入元素
这里有三种方式定义一个使用泛型通配符的集合(变量)。
List<?> listUknown = new ArrayList();
List<? extends A> listUknown = new ArrayList();
List<? super A> listUknown = new ArrayList();
List<?> 的意思是这个集合是一个可以持有任意类型的集合,它可以是List,也可以是List,或者List等等。
List、List 可以看成是不同的类型,这里的类型指的是集合的类型(如List、List),而不是集合所持有的类型(如A、B),但集合所持有元素的类型会决定集合的类型。
6.1 🐷 ?上界和下界
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
上界 <? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。
下界 <? super Apple>,表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。
上界和下界的特点:
- 上界的list只能get,不能add(确切地说不能add出除null之外的对象,包括Object)
- 下界的list只能add,不能get
import java.util.ArrayList;
import java.util.List;
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
//上界
List<? extends Fruit> flistTop = new ArrayList<Apple>();
flistTop.add(null);
//add Fruit对象会报错
//flist.add(new Fruit());
Fruit fruit1 = flistTop.get(0);
//下界
List<? super Apple> flistBottem = new ArrayList<Apple>();
flistBottem.add(new Apple());
flistBottem.add(new Jonathan());
//get Apple对象会报错
//Apple apple = flistBottem.get(0);
}
}
泛型就解释到这里,后面可能还会有补充
大家如果有什么补充直接dd我哦
希望能帮到你
感谢阅读~