泛型入门食用指南

简介: 面试都要求我们有扎实的Java语言基础。这其中,对泛型的理解是不可或缺的 ,故带大家对泛型进行一定的入门学习与使用。以求大家对泛型有一定的认知与理解

📑即将学会

泛型的相关知识点

背景

我们先来看看 这两段代码

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();会报错

CoffeeLatte的父类,因此,他们的抽象意思是 我声明了一个装咖啡的容器,要什么咖啡都能装的容器 我实例化了一个装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等框架就是这样处理泛型信息的

目录
相关文章
|
6月前
|
存储 安全 Java
源码必备知识:泛型
源码必备知识:泛型
42 2
|
存储 Java
Java数据结构之第十四章、泛型进阶
Java数据结构之第十四章、泛型进阶
79 0
|
6月前
|
Java 编译器
【JAVA杂货铺】一文带你走进面向对象编程|继承|重载|重写|期末复习系列 | (中4)
【JAVA杂货铺】一文带你走进面向对象编程|继承|重载|重写|期末复习系列 | (中4)
40 0
|
6月前
|
Java
【JAVA杂货铺】一文带你走进面向对象编程|构造方法调用 | 代码块分类| 期末复习系列 | (中3)
【JAVA杂货铺】一文带你走进面向对象编程|构造方法调用 | 代码块分类| 期末复习系列 | (中3)
36 0
|
Java 编译器 C++
不懂泛型,怎么装逼,一文把泛型说的明明白白,安排!!!
泛型是Java中的高级概念,也是构建框架必备技能,比如各种集合类都是泛型实现的,今天详细聊聊Java中的泛型概念,希望有所收获。记得点赞,关注,分享哦。
130 0
不懂泛型,怎么装逼,一文把泛型说的明明白白,安排!!!
|
算法 安全 前端开发
|
机器学习/深度学习 安全 算法
面向对象 这一文拿捏了 ⭐ (建议收藏)
面向对象中级部分已整理完成,建议收藏 🔴🟢🟡
116 0
面向对象 这一文拿捏了 ⭐ (建议收藏)
|
C# C++
C#(五十)之泛型
泛型( Generic ) 允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。可以理解为就是C++中的模板。
122 0
C#(五十)之泛型
|
JavaScript
手摸手一起学习Typescript第六天 - 泛型 Generics / 泛型约束 / 泛型与类和接口
手摸手一起学习Typescript第六天 - 泛型 Generics / 泛型约束 / 泛型与类和接口
|
JavaScript 前端开发
知其然,知其所以然:TypeScript 中的协变与逆变
## 前言 在前一篇文章《淘宝店铺 TypeScript ESLint 规则集考量》中,我们提到了这一条规则:**method-signature-style**,它的作用是对 interface 中不同的函数声明方式进行约束,这里的声明方式主要有两种,_method_ 与 _property_,区别如下: ```typescript // method interface T1 { fu