📑即将学会
泛型的相关知识点
背景
我们先来看看 这两段代码
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 coffees = new ArrayList();
会报错
Coffee
是Latte
的父类,因此,他们的抽象意思是 我声明了一个装咖啡的容器,要什么咖啡都能装的容器 我实例化了一个装Latte的容器 将它赋值给声明的变量。 因此,当我们获取数据时,会拿到咖啡接口或者子类的实例化对象,在这个时候 转换对象会失败,我要一个可以容纳任何咖啡的容器 ,你给我一个能且只能容纳拿铁的容器。
但是我们在实际开发中,这种需求是比较常见的 我们声明了一个咖啡杯 是咖啡杯就好。
通配符?
只能写在泛型实例化的地方
表示 这个类型是什么都可以 ,只要不超过?extend
或者? super
的限制。 虽然用于实例化,但是它表示类型还有待进一步确定,它不能用在类型参数最终确定的时候既new Goods();
ArrayList coffees = new ArrayList();
刚才这里时会报错的,当我们使用了?通配符后 我们会发现编译器 已经不报错了
//创建类的上界 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 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等框架就是这样处理泛型信息的