【Java基础系列】
【Java基础01】基础概述
【Java基础02】常用类
【Java基础03】时间类
【Java基础04】异常
【Java基础05】枚举类
【Java基础06】泛型
【Java基础07】注解
【Java基础08】反射
【Java基础09】代理
【Java基础10】IO流
泛型
文章概述:总结描述Java基础中泛型的定义和使用,包括泛型的定义、泛型的使用优缺点和使用方法、泛型及其通配符、及泛型的实现原理。
一、泛型的定义
泛型(generics)是JDK5引入的新特性,是通用设计上必不可少的元素,在开原框架和JDK源码中都能看到它。
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二、为什么使用泛型
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型的作用:
- 保证类型安全性
- 消除强制转换
- 提升性能,避免了不必要的装箱、拆箱操作
- 提高代码重用性
保证类型安全性
消除强制转换
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,容易出现类型转换错误。有了泛型后,相当于告诉编译器每个集合接收的对象类型是什么,使得程序更加安全,增强了程序的健壮性。
没有泛型:
ArrayList names = new ArrayList();
names.add("luo");
names.add(123); //编译正常
String s = (String) list.get(0);
使用泛型:
ArrayList<String> names = new ArrayList<>();
names.add("luo");
names.add(123); //编译不通过
String s = list.get(0); // no cast
提升性能,避免了不必要的装箱、拆箱操作
在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高。
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
提高代码重用性
三、如何使用泛型
3.1 泛型类
泛型类:把泛型定义在类上
public class 类名 <泛型类型1,...> {
}
泛型类型必须是引用类型(非基本数据类型)
定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
public class GenericClass<ab,a,c> {}
public static void main(String[] args) {
ArrayListT<String> listT = new ArrayListT<String>(5);
listT.add("123");
System.out.println(listT.get(0));
}
// T是指我们的数据类型,当我们在实例化的时候必须要指明他的类型
static class ArrayListT<T> {
private Object[] elementDate;
private int size = 0;
//initialCapacity初始容量
public ArrayListT(int initialCapacity){
this.elementDate = new Object[initialCapacity];
}
//这里的T就是我们要添加数据的类型,其必须与最开始定义的类型相同
public void add(T t) {
this.elementDate[size++] = t;
}
public T get(int num) {
return (T) this.elementDate[num];
}
}
3.2 泛型接口
泛型接口概述:把泛型定义在接口上
public interface 类名 <泛型类型,...> {
}
方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。
public class GenericsInterfaceDemo {
public interface GenericInterface<T> {
void show(T value);
}
class StringShowImpl implements GenericsInterfaceDemo.GenericInterface<String> {
@Override
public void show(String value) {
System.out.println(value);
}
}
class NumberShowImpl implements GenericsInterfaceDemo.GenericInterface<Integer> {
@Override
public void show(Integer value) {
System.out.println(value);
}
}
}
3.3 泛型方法
泛型方法概述:把泛型定义在方法上
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
public class GenericsFunctionDemo {
public static void main(String[] args) {
GenericsFunctionDemo genericsFunctionDemo = new GenericsFunctionDemo();
String s = genericsFunctionDemo.soutInfo("luo");
Integer integer = genericsFunctionDemo.soutInfo(123);
System.out.println(s);
System.out.println(integer);
}
public <T> T soutInfo(T t) {
System.out.println(t.getClass());
return t;
}
}
输出
class java.lang.String
class java.lang.Integer
luo
123
四、泛型及通配符
4.1 常用的 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.2 泛型通配符
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
<?> 无边界的通配符 <? extend E> 固定上边界的通配符 <? super E> 固定下边界的通配符 ```java //表示类型参数可以是任何类型 public class Apple<?>{}//表示类型参数必须是A或者是A的子类
public class Apple{}
//表示类型参数必须是A或者是A的超类型
public class Apple{}
为什么要使用通配符而不是简单的泛型呢?下面看一段代码
```java
static int countLegs(List<? extends Animal> animals) {
int retVal = 0;
for (Animal animal : animals) {
retVal += animal.countLegs();
}
return retVal;
}
static int countLegs1(List<Animal> animals) {
int retVal = 0;
for (Animal animal : animals) {
retVal += animal.countLegs();
}
return retVal;
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs(dogs);
// 报错
countLegs1(dogs);
}
class Animal {
public Integer countLegs() {
return 1;
}
}
class Dog extends Animal {
}
像 countLegs 方法中,限定了上限,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。其原因是在于,要是使用了<? extends Animal>玩意的话我们的范围就变成了Animal及其子类
上界通配符 < ? extends E>
用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
下界通配符 < ? super E>
用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
4.3 T 和 ?的区别
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行
// 可以
T t = operate();
// 不可以
? car = operate();
区别:
通过 T 来 确保 泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List<? extends Number> dest, List<? extends Number> src)
通配符可以使用超类限定而类型参数不行
类型参数 T 只具有 一种 类型限定方式:
T extends A
但是通配符 ? 可以进行 两种限定:
? extends A
? super A
4.4 Class 和 Class<?> 区别
前面介绍了 ? 和 T 的区别,那么对于,Class<T>
和 <Class<?>
又有什么区别呢?
常用的反射代码:
// 通过反射的方式生成 multiLimit
// 对象,这里比较明显的是,我们需要使用强制类型转换
MultiLimit multiLimit = (MultiLimit)
Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();
如果反射的类型不是 MultiLimit 类,那么一定会报 java.lang.ClassCastException 错误。
对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:
Class<T>
在实例化的时候,T 要替换成具体类。Class<?>
它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。
// 可以
public Class<?> clazz;
// 不可以,因为 T 需要指定类型
public Class<T> clazzT;
所以当不知道定声明什么类型的 Class 的时候可以定义一 个Class<?>。
那如果也想 public Class<T> clazzT;
这样的话,就必须让当前的类也指定 T
public class Test3<T> {
public Class<?> clazz;
// 不会报错
public Class<T> clazzT;
}
五、泛型实现原理
泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
例子:
public class Generics<T> {
private T t;
}
定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。
反编译一下这个类:
public class Generics{
public Generics(){}
private Object num;
}
发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。
那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型
public class Generics<T extends String> {
private T num;
}
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。
这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Generics 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
extends String> {
private T num;
}
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。
这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Generics 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。