这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
?即将学会
泛型的相关知识点
背景
我们先来看看 这两段代码
public int addInt(int x,int y){
return x + y;
}
public float addFloat(float x, float y){
return x + y;
}
复制代码
List list= new ArrayList<String>();
list.add("123");
list.add(123);
list.add(123f);
for (int i = 0; i < list.size(); i++) {
String value = ((String) list.get(i));
System.out.println(value);
}
复制代码
上面两段代码中,我们可以看到这种情况中引起的一些问题。
多种不同数据类型相同的数据操作
List 存储对象时,可以传该类以及该类的子类,该对象的编译类型变为该类型 代码中为Object,但其运行时依然为自身类型String、Int。因此,当从list中取出元素需要人为的强制类型转换为目标类型,很容易出现类型转换异常。
为了解决List中的这种问题,这个类型的不同实例的具体类型可能不同,需要对集合类 类型进行限制,以及对某些数据进行强制类型转换。提出了泛型的概念解决方案
泛型写法
泛型类 写法
public class Wrapper<T> {
T instance ;
//这不叫泛型方法 只是普通方法
public T getInstance() {
return instance;
}
public void setInstance(T instance) {
this.instance = instance;
}
}
复制代码
泛型方法 写法
//声明
<E> E method(E item);
//使用
String newStr = 相关类.<String>method("method");
参考
@SuppressWarnings("TypeParameterUnusedInFormals")
@Override
public <T extends View> T findViewById(@IdRes int id) {
return getDelegate().findViewById(id);
}
复制代码
泛型优势
- 类型检查 自动转型
- 类型约束
这可以使多种数据类型执行相同的方法 ,以及运行时实例化泛型参数,自动转型
虽然这些我们在Java中也可以实现,但是泛型让这个过程更加简单 快捷
比如以下代码块 利用泛型可以这样实现 ·
class GenericList<T> {
public Object[] instances = new Object[0];
public T get(int index) {
return (T) instances[index];
}
public void set(int index,T instance){
instances[index] = instance;
}
public void add(T instance){
instances = Arrays.copyOf(instances,instances.length + 1);
instances[instances.length -1] = instance;
}
}
GenericList<String> stringGenericList = new GenericList<>();
stringGenericList.add("asd");
//会自动报错 当传入非泛型实例类型为String类型的变量
//stringGenericList.add(123);
//当利用泛型取数据的时候 会自动转型为泛型实例化类型
String s = stringGenericList.get(0);
复制代码
我们可以看到 泛型的使用过程中可以让我们对类型进行限制 以及 对某些数据进行强转
不过这种操作 我们不使用泛型也可以实现
class NonGenericList {
public Object[] instances = new Object[0];
public Object get(int index) {
return instances[index];
}
public void set(int index,Object instance){
instances[index] = instance;
}
public void add(Object instance){
instances = Arrays.copyOf(instances,instances.length + 1);
instances[instances.length -1] = instance;
}
}
复制代码
我们可以利用Object实现任意存储,再判断实现特定类型存储,最终在使用时 使用强制类型转换依旧可以实现这种效果
NonGenericList nonGenericList = new NonGenericList();
if ("community" instanceof String){
nonGenericList.add("community");
}
String result = (String) nonGenericList.get(0);
复制代码
但泛型让这其中的操作更加快捷
泛型适用场景
一个类 或者 接口 某些字段的类型 方法的参数类型 返回类型 是不定的
实例类型 不针对 静态类型 静态参数
泛型中的 < T >
//RepairableShop<M>是声明 Shop<M>是实例化
//RepairableShop<M> 是说我有一个泛型参数M 具体是什么类我不知道
//shop<M> 是实例化 虽然M不确定 但是对于shop来说 这个值是确定的 就是M是对Shop泛型的实例化 虽然这个值不确定 但是对于Shop是确定的,就是M
public interface RepairableShop<T> extends Shop<T> {
/**
* 想给之前的泛型接口新增功能 并且保留泛型
* 类型参数是T的接口 继承了Shop接口 并且Shop接口的参数是T 用RepairableShop类型参数T实例化Shop参数T
* 并且在实例化的过程中 shop的参数是T 现在把它的类型参数也继承下来了
*左边的E是Repairable的类型参数的声明;右边的E是Shop的类型参数的实例化
*/
}
复制代码
泛型的约束与限制
不能实例化 类型变量
Type parameter ‘T’ cannot be instantiated directly
//不行
public T get(int index) {
return new T();
}
复制代码
不能使用基本数据类型实例化泛型类型参数
GenericList<Integer> integerGenericList = new GenericList<>();//行
GenericList<int> intGenericList = new GenericList<>();//不行
复制代码
泛型类的静态上下文 类型变量失效
//因为泛型是针对实例的 因此 静态的都是不行的
private static T instacne; /错误的
public static <T> T getInstacne(){
//这个也是有问题的
};
复制代码
不能创建参数化类型的数组
List<String>[] strings = new List<String>[100];//不行
复制代码
泛型类型实例化上界与下界
为什么 ArrayList<Coffee> coffees = new ArrayList<Latte>();
会报错
Coffee
是Latte
的父类,因此,他们的抽象意思是
我声明了一个装咖啡的容器,要什么咖啡都能装的容器
我实例化了一个装Latte的容器 将它赋值给声明的变量。
因此,当我们获取数据时,会拿到咖啡接口或者子类的实例化对象,在这个时候 转换对象会失败,我要一个可以容纳任何咖啡的容器 ,你给我一个能且只能容纳拿铁的容器。
但是我们在实际开发中,这种需求是比较常见的 我们声明了一个咖啡杯 是咖啡杯就好。
通配符?
只能写在泛型实例化的地方
表示 这个类型是什么都可以 ,只要不超过?extend
或者? super
的限制。
虽然用于实例化,但是它表示类型还有待进一步确定,它不能用在类型参数最终确定的时候既new Goods<? extends Fruit>();
ArrayList<Coffee> coffees = new ArrayList<Latte>();
刚才这里时会报错的,当我们使用了?通配符后 我们会发现编译器 已经不报错了
//创建类的上界
public interface CoffeeShop<T,C extends Coffee> extends Shop<T>{
}
//声明类的上界
ArrayList<? extends Coffee> coffees = new ArrayList<Latte>();
复制代码
这个时候,是一种上界,是一种承诺,虽然这个时候这一行不会报错了,但是我们还是不能往里面传入不对的类型。
不能子类的泛型赋值给父类的泛型引用
? extend 传递给方法的参数 必须是X的子类 包括X本身
放宽声明时的要求 可以声明子类
这个时候我们就可以需要咖啡杯的时候,传入任意的咖啡杯
?extend限制
但是,我们在上述情况中,不能调用传入泛型参数的方法 包含一些set等方法
因为编译器不能知晓运行时的实际参数,可能传入子类 类型参数,从而发生错误。
而这种限制下带来的好处是,主要用于安全地访问数据 可以访问X以及其子类型
上述情况我们利用了通配符解决了泛型子类传递给父类引用的情况
那么?为什么只有泛型有这样的情况 以下的代码没有
//我要一个水果实例 你给我一个苹果 苹果是水果
Fruit fruit = new Apple();//1 编译器不报错
//完整的peach替换完整的苹果
fruit = new Peach();//2 编译器不报错
//我要一个装水果的容器 你只给我一个只能装苹果的容器 会出现往只能装苹果 后面插入了peach
ArrayList<Fruit> fruits = new ArrayList<Apple>();//3 编译器报错
List<Fruit> fruits = new ArrayList<Fruit>();//4 编译器不报错 这个应该才是和1处代码匹配的
Fruit[] fruits = new Apple[10];//编译器不报错
//报错 Peach cannot be stored in Array type of Apple[]
fruits[0] = new Peach();//编译器不报错 但这行代码是有问题的 运行时会报错
//编译器不报错 但这行代码是有问题的 不过运行时也不会报错
//因为泛型有类型擦除的特性 运行时其泛型会被擦除
/**
*运行时类型会被变成这样
*ArrayList coffeeArrayList = (ArrayList) new ArrayList();
*public interface Shop <T> {
* T sell();
* float refund(T item);
*}
*会变成以下形态
*public interface Shop{
* Object sell();
* float refund(Object item);
*}
*
*/
ArrayList<Coffee> coffeeArrayList = (ArrayList) new ArrayList<Cappuccino>();
coffeeArrayList.add(new Latte());
ArrayList<Cappuccino> cappuccinoArrayList = (ArrayList) new ArrayList<Cappuccino>();
ArrayList<Coffee> coffeeArrayList = cappuccinoArrayList;
coffeeArrayList.add(new Latte());
//运行时报错 不能将子类的类型对象赋值给父类的类型引用 因为Java类型擦除的特性
//而数组没有类型擦除,可以在运行时第一时间发现问题 而泛型不能 当使用时会发生异常 因此
//才有这样的机制
Cappuccino cappuccino = cappuccinoArrayList.get(0);
复制代码
? extends 的使用
主要用于安全地访问数据,可以访问Coffee及其子类型 主要用于场景化的用途
场景化的用途: 一些方法中 子类传递给父类 不限制函数的接收类型
Shop<? extends Coffee> coffeeShop = new Shop<Latte>(){};
//我们一般不这么用 一般不创建字段
ArrayList<? extends Coffee> coffeeLists = new ArrayList<Cappuccino>();
float totalSugar = 0;
for(Coffee coffer:coffeeLists){
totalSugar += coffer.getSugr();
}
//主要用于这样的场景需求 有具体的需求 声明一下 给别人用
//主要用于获取不含泛型参数的方法 安全地访问数据
float getTotalSugar(ArrayList<? extends Coffee> coffeeLists){
float totalSugar = 0;
for(Coffee coffer:coffeeLists){
totalSugar += coffer.getSugr();
}
return totalSugar;
}
复制代码
? super X
表示传递给方法的参数,必须是X的超类(包括X本身)
? super X 表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。
public class Mocha implements Coffee{
public void addMeToCoffeeList(ArrayList<Mocha> arrayList){
arrayList.add(this);
}
}
//上面的方法 我们发现
ArrayList<Cappuccino> cappuccinoArrayList = new ArrayList<Cappuccino>();
ArrayList<Coffee> coffeeLists = new ArrayList<Coffee>();
Mocha mocha = new Mocha();
mocha.addMeToCoffeeList(cappuccinoArrayList)//正常
mocha.addMeToCoffeeList(coffeeLists)//报错 但这个需求是正常的 咖啡容器加摩卡 只要能装摩卡就好
复制代码
? super XXX 的使用场景
声明这个变量 它的方法参数是这个类型的参数 父类泛型肯定可以接受这个类型
//方法改造
public class Mocha implements Coffee{
public void addMeToCoffeeList(ArrayList<? super Mocha> arrayList){
arrayList.add(this);
}
}
复制代码
泛型方法
泛型方法 自己声明了泛型参数的方法
<O> List<T> recycle(O item);
复制代码
为什么要用泛型方法
场景: 当我们有个类需要引入新的类型数据的时候,比如 商店接口
public interface Shop <T> {
T sell();
float refund(T item);
}
//我们想要新增回收方法 回收方法可以回收任意多种商品 然后回赠我卖的东西
//这个时候 我们可以依据需求 新增 方法
list<T> recycle(G goods);
//因为商品是任意类型的商品 因此 我们在方法中新增泛型 G
//因为G是 新定义的类型 这个时候 我们首先可以在接口中定义 泛型类型 G
此时 代码结构为
public interface Shop <T,G> {
T sell();
float refund(T item);
list<T> recycle(G goods);
}
//这个时候 当我们需要使用的时候 我们需要传入相关的类型
Shop shop = new Shop<Television television>(){}
我们发现 这个时候 我们在创建商店的时候 需要传入相关的类。
这样的话 违背了我们想要实现回收任意物品的需求
为了解决这个需求 于是 我们想到了接口 嗯 构建一个家电接口 让相关物品实现该接口
可是为了这样不受限制 我们不如直接让传入的参数改为Object,
这个时候 我们在使用的时候就可以直接 这样
public interface Shop <T> {
T sell();
float refund(T item);
list<T> recycle(Object goods);
}
我们可以直接 传入相关实例 也不用使用接口了 直接传入相关实例对象 更快捷 也不需要传入泛型
泛型为我们提供类型检查错误 自动转型 。
如果不用泛型也可以满足需求(不用到类型检查错误 和 自动转型)的时候
可以不使用泛型
而当我们换一种需求 此时需要以旧换新 我们需要向方法中传入旧物品 以及 部分 金钱
然后返回 任意新物品
我们可以依据需求 设计方法
因为是换任意商品 类似于刚才的回收任意商品 我们可以参考上面的方法设计 返回 Object 这个时候
Object tradeIn(Object goods,float money);
当我们使用的时候
Object goods = shop.tradeIn(new Television(),1000);
Television tv = (Television)goods;
这个时候 我们可以使用泛型 定义泛型参数
<G>G tradeIn(G goods,float money);
使用的时候 我们可以看到 这个时候不用转型了
传入什么类型的类 通过类型推断 返回的类型就是什么类型。
Television goods = shop.tradeIn(new Television(),1000);
这个G不是针对这个类的 而是针对这个方法的
复制代码
泛型参数的实例化
对象的声明 Shop<Coffee>
对象的创建 new ArrayList<String>()
继承
一次泛型方法的调用
复制代码
依靠泛型方法自动转型
<R> R take();
我们可以通过 shop.take(); 来推断出返回值类型参数 为 Mocha
也可以以这种方式 推断出 返回值参数 Mocha mocha = shop.take() ;
类比findViewById 传入resId, 通过前面的类型推断控件类型
@SuppressWarnings("TypeParameterUnusedInFormals")
@Override
public <T extends View> T findViewById(@IdRes int id) {
return getDelegate().findViewById(id);
}
复制代码
每次调用的时候对泛型参数实例化
回到为什么使用泛型方法
<G>list<T> recycle(G goods);
list<T> recycle(Object goods);
对比这两行代码 我们发现对商店的泛型 来实现任意物品回收
在这个设计过程中,我们可以观察到
泛型只作为参数传入 且只有一个泛型参数 返回值也没有
这个时候 泛型并没有起到作用 是没有必要的
复制代码
泛型方法 和当前的对象本身无关 不局限于非静态方法,
因此可以让静态方法成为泛型方法,和每一次调用有关,
每一次调用进行泛型参数实例化。
泛型的本质
类型检查和自动转型 (表面)
本质
什么时候要类型检查 和 自动转型
对多个不同实可以例锁定类型 稍后锁定 每次使用的时候锁定 实例化
Class String implement Comparable<String>{
}
String 和 Comparable<String> 没有关系
String 实现 Comparable接口 并将泛型参数实例化为String
复制代码
类型约束
泛型可以加多重限制
interface AppleShop<T extends Apple & Serializable>{
T buy();
float refund(T item);
}
复制代码
使用泛型 限定方法参数类型 返回值类型
<p> void merge(List<p> list1, List<p> lisr2)
复制代码
使用情景归纳
T 类名右边 接口名右边 作为类型参数 代称而已
类型参数有两种 类比方法参数
parameter 方法形参声明的时候
argument 方法实参 实际使用的时候传入的实例
-
type parameter
-
泛型的创建 public class Shop;
- 创建一个Shop类,内部使用到一个同一的类型,这个类型称之为 T
-
-
type argument
-
其它地方尖括号里的 Shop appleShop 的Apple;
-
表示那个同一的类型,在这里我们决定是这个类型 如上面为Apple。
-
interface RepairableShop<T> extends Shop<T>{
// 我要对父类进行实例化 确定它类型参数的实际值
// 实例化的具体类型是我的这个类型参数
}
复制代码
而泛型 只要在类的内部 定义 声明的时候才有意义 一旦出了类的内部 就是在使用该类了
?
扩大实例化的时候 实参的范围 extends 创建的时候泛型的上界
<>
做类型的形参 实参 ,对类型进行包裹 分隔符的作用
泛型的重复 与 嵌套
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>{
//又重复 又嵌套
// extends Enum<E> 表示上界 E 需要Enum<E> 的子类
// E 是 Enum 的子类或其本身 当其实例化的时候
// Comparable<E>的实现 需要重写comparaTo(E o)的参数就需要是Enum<E> 的子类 就是表示 必须和自己一样的类作比较
E 是 Enum 的子类或其本身 当其实例化的时候
}
复制代码
类型擦除
我们可以看一下以下两段代码
我们可以看到编译器报错了 下面是报错信息
‘方法(List)’与’方法(List)’冲突;两种方法有相同的擦除
‘method(List)’ clashes with ‘method(List)’;
both methods have same erasure
在Java中,通过类型擦除 List 与 List 都变成了List 因此 这两个方法实际上在运行时是一样的 所以编译器进行了报错
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
而虚拟机在泛型这块 。引入Signature等对参数类型,参数化类型信息进行保存,并利用反射手段 进行了一些类型转型 达到泛型的使用
而在方法中 会有一种 bridge method
//@Override
float refund(Apple item);// 我们重写的
//jvm 加的 实质上是这个才是重写的
@Override
float method(Object item);
复制代码
而对于float method(Object item);
方法 JVM会进行以下处理
@Override
float method(Object item){
return refund((Apple)item);
}
复制代码
类型擦除的影响
List.class 获取不到 因为类型擦除
消除影响
从Signature属性,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,我们能通过反射手段取得参数化类型。
因此 类型擦除 只是在运行时没有 在字节码中还是可以看到类型信息的
所有代码中声明的变量、参数、类、接口,在运行时都可以通过反射获取到泛型信息
因此 我们可以利用反射技术 获得类型信息
但是 在运行时创建的对象,在运行时通过反射也获取不到泛型信息 (Class文件没有 )
来生成对象,这样由于子类在Class文件里,就可以通过反射来拿到运行时所创建对象的泛型信息
GSon等框架就是这样处理泛型信息的