Java 泛型

泛型,就是将数据类型作为参数的代码规则

泛型的一些名词定义

在泛型类 Response<T> 中,Response 是泛型的原生类型,T 为类型参数,Response<T> 称为泛型。

在泛型方法 public <T1> T1 getGenerics()中,<T1> 即为泛型,没有原生类型,T1 为类型参数

为什么需要泛型

1、同一段代码,适配多种数据类型

2、实现1的基础上,编译规范数据类型,避免运行时因数据类型不匹配而报错

泛型类、泛型接口与泛型方法的定义与辨析

<T> 来定义泛型的存在

可以将泛型看作一个 final 的类型变量,其中 <> 内的字符 T 为泛型的类型参数变量申明,而其他没有尖括号的 T,则为类型使用

泛型的定义,一般使用大写的字母,例如: T(type 单个泛型)、KV(key、value 键值对)、E(element 元素)、R(return 返回类型);但实际上,如果你想用其他自定义的字符也没问题,就是代码看起来没那么规范,类似变量名用中文拼音。

泛型方法不一定定义在泛型类里;定义在泛型类里包含泛型的方法,也不一定就是泛型方法

// 泛型类 有<T>
public class Response<T> {
    private T data;

    // 将 listener 的泛型 T,与类的泛型 T 关联起来
    private ResponseListener<T, String> listener;
    // 如果不用类型的泛型 T,也可以给 listener 的泛型 T 赋值为 Integer 或者其他
    private ResponseListener<Integer, String> listener2;
  
    // 定义泛型接口,此处的T,与类型的 T 并没有关系,只是刚好同名的变量名
    interface ResponseListener<T, R> {
        R transformResponse(T data);
    }
        
    // 注意:以下定义方式编译器会报错,必须要添加多个泛型类型再传参中转
//    interface ResponseListener2<R> {
//        R transformResponse(T data);
//    }
  
  
    // 非泛型方法,参数为类的泛型,已固定
    public void setData(T data) {
        this.data = data;
    }

    // 非泛型方法,返回类型为类的泛型,已固定
    public T getData() {
        return data;
    }
    
    // 泛型方法,此处 T 与类定义中的 T 不是同一个
    // 相当于在类中定义了一个成员属性 data 后,在方法参数中又定义了一个 data
    // 此外,这个方法定义在普通类里面也是可以的,泛型方法可以存在于普通类中
    public <T> T getFirst(ArrayList<T> array) {
        return array.get(0);
    }
  
    // 实际开发中,为了不造成阅读误解,尽量不要用上面的命名方式,改成:
    public <T2> T2 getFirst(ArrayList<T2> array) {
        return array.get(0);
    }
}
private void usage() {
    Response<Integer> intResponse = new Response<>();
    // 此处会自动进行拆包,将 Integer 类型转化为 int
    // 获取到的 data,为定义时的 Integer
    int responseData = intResponse.getData();
    ArrayList<String> stringArray = new ArrayList<>();
    stringArray.add("first");
    // stringArray 的泛型为 String,而 intResponse 的泛型为 Integer
    // 尽管泛型类中的泛型方法与类所用的泛型参数同样为 T,但二者不同
    String first = intResponse.getFirst(stringArray);
}

限定类型变量

在实际开发中,定义泛型时,经常需要加入一些类型的限定,此时需要用到 extend 关键字。

NOTE:无论是限定的父类(类定义时,用extends)还是接口(类定义时,用implements),在泛型限定时,用的,都只有 extend

在泛型的限定中,可以用 & 连接符限定一个类加多个接口。java 虚拟机并非真的实现了泛型,而是在泛型定义后,在编译时,使用了类型擦除,如果有 extends 限定泛型参数继承,则将类型擦除为 extends 后第一个类型;否则将类型擦除为 Object。因而,如果有限定类型,类型必须放在第一个

public class Food {
    // 食物入口时的感觉
    public String tasteComment() {
        return "It's food.";
    }
}

public interface Yummy {
    // 太好吃了,所以要表达
    void comment();
}

public class Meat extends Food {

    @Override
    public String tasteComment() {
        return "Wow, meat!";
    }
}

public class Steak extends Meat implements Yummy {
    @Override
    public String tasteComment() {
        return "Steak is juicy and delicious!";
    }

    @Override
    public void comment() {
        System.out.println(tasteComment());
    }
}

// 如设置餐厅主打餐时,必须是食物,同时得好吃,所以同时派生自 Food 和 Yummy
// 其中 Food 为 class,是父类,必须摆放在第一个
// 如果还派生自其他接口,如 Serializable 等,接口的位置无需在意
// 如:<T extends Food & Yummy & Serializable> 或 <T extends Food & Serializable & Yummy>
private <T extends Food & Yummy> void setDailyMainDish(T dish) {
    // 从单一职责角度出发,尽量拆分方法
//    clientReact(dish);
    // 拆分为两个方法:
    clientHaveFood(dish);
    clientReact(dish);
}

// 实际开发中,尽量遵循单一职责,建议将此方法,拆分为下面两个方法:
//private <T extends Food & Yummy> void clientReact(T dish) {
//    System.out.println("client thought:" + dish.tasteComment());
//    dish.comment();
//}
private <T extends Food> void clientHaveFood(T dish) {
    System.out.println("client thought:" + dish.tasteComment());
}
private <T extends Yummy> void ClientReactYummyFood(T dish) {
    dish.comment();
}

public static void main(String[] args) {
    Restaurant restaurant = new Restaurant();
    // Steak 是 Food 的子类,且满足了 Yummy 接口
    restaurant.setDailyMainDish(new Steak());
    // Meat 继承自 Food,但没有满足 Yummy 接口,因而编译器会报错
//    restaurant.setDailyMainDish(new Meat());
}

泛型中的约束

  • 泛型的类型参数不能直接构造出类

不同类型支持的构造方法不同,因而泛型的类型参数无法直接构造

  • 静态域中不能有类型参数的变量和方法

虚拟机创建对象时,会先执行 static 相关代码,再执行构造方法。而在构造方法还没有执行时,静态域中的变量和方法无法获悉泛型类型参数的具体类型,因而不能在静态域中使用类型参数

  • 泛型的类型参数不能为基本类型

泛型支持的参数类型必须是对象,而在 JDK 中,基本类型不是对象

public class Response<T> {
    private T data;

    Response() {
    // 不可以用泛型的类型参数直接构造实例
//        data = new T();
    }

    // 不可以用类型参数定义静态变量 或 静态方法
//    private static T defaultData;
//    public static void setDefaultData(T data) {
//        defaultData = data;
//    }
    
    // 但是泛型方法可以是静态的
    public static <T> void setDefaultData(T data) {
        
    }
}

private void usage() {
    // 泛型中,不允许基础类型参数,必须使用其对应包装类
//    Response<int> intResponse = new Response<>();
    Response<Integer> intResponse = new Response<>();
    // 此处会自动进行拆包,将 Integer 类型转化为 int
    // 获取到的 data,为定义时的 Integer
    int responseData = intResponse.getData();
}

泛型类型的局限性

  • JDK 对泛型的类型判定,只能到其原生类型

获取泛型的类型,或是对类型进行判断,都只支持到其原生类型。即,在上面的 Response<T> 中,getClass() 返回的,只有 Response

  • 可以定义泛型的数组,但初始化只能用原生类初始化
public static void main(String[] args) {
    Response<Integer> intResponse = new Response<>();
    Response<String> stringResponse = new Response<>();
    // 虽然 intResponse 和 stringResponse 的泛型不同,但其类型判定只到原生类型,因而是相同的
    boolean isSameClass = intResponse.getClass() == stringResponse.getClass();
    
    // 对于 instanceOf 关键字的判断,只能到其原生类型,后面两种泛型的判断并不支持
    boolean isResponse = intResponse instanceof Response;
//        boolean isResponse2 = intResponse instanceof Response<Integer>;
//        boolean isResponse3 = intResponse instanceof Response<T>;

    // 定义数组时,可以添加泛型。但数组的初始化,只能使用原生类
    Response<Integer>[] arrays = new Response[10];
//    Response<Integer>[] arrays = new Response<Integer>[10];
    arrays[0] = intResponse;
    // 虽然构建时,没有传入参数类型,但在定义时规定了,因而会报错
//    arrays[1] = stringResponse;
}
  • 方法重载时,无法识别泛型的类型参数
// 以下两个方法无法兼容,尽管入参的泛型不同
// 但是入参泛型的基本类型相同,在经过类型擦除后就是同一类型
// java 编译器对重载的检查只有方法名和入参类型,不包括返回类型
//private String getFirst(ArrayList<String> stringList) {
//    return stringList.get(0);
//}
//
//private Integer getFirst(ArrayList<Integer> intList) {
//    return intList.get(0);
//}

// 以上两个方法可合并为:
private <T> T getFirst(ArrayList<T> list) {
    return list.get(0);
}
  • 泛型类结合 Throwable 时的局限性

泛型类不能继承自 Throwable,且无法 catch 泛型,但可以 throws 泛型类

public class GenericsException {
    // 编译器主动报异常,并提示:Generic class may not extend 'java.lang.Throwable'
//    class GException<T> extends Exception
//    class GException<T> extends Throwable
    
    
    public <T extends Throwable> void handleThrowable(T throwable) throws T {
        try {
            // do something
            
        // 无法 catch 泛型
//        } catch (T t) { 
            
        // 但可以catch throwable,然后抛出一个泛型的 throwable
        } catch (Exception e) {
            throw throwable;
        }
    }
}

泛型类型的继承规则

泛型的类型参数不一致时,泛型是两个不同的类型,泛型的继承关系与 类型参数 的继承关系无关;

泛型的继承关系可以理解为原生类的继承关系。当原生类有继承关系可循,原生类一致时,类型参数一致,则两个泛型一致,存在继承关系;原生类一致时,类型参数不一致,则两个泛型不一致,不存在继承关系

因而:当泛型的原生类型有继承关系,且类型参数一致时,泛型间存在继承关系,否则不存在

// 定义原生类 SuccessResponse 继承自 Response
public class SuccessResponse<T> extends Response<T> {
}

public static void main(String[] args) {
    // 在上面的示例中,定义了 Steak extend Food
    Food food = new Food();
    Steak steak = new Steak();
    Response<Food> foodResponse = new Response<Food>();
    Response<Steak> steakResponse = new Response<Steak>();
    SuccessResponse<Food> foodSuccessResponse = new SuccessResponse<Food>();
    SuccessResponse<Steak> steakSuccessResponse = new SuccessResponse<Steak>();
    // 由于存在继承关系,可以将 steak 赋值给 food
    food = steak;
    // 原生类存在继承,类型参数一致,泛型存在继承关系
    foodResponse = foodSuccessResponse;
    steakResponse = steakSuccessResponse;
    // 原生类一致,类型参数不同,因而泛型不一致,不存在继承关系
//    foodResponse = steakResponse;
    // steakSuccessResponse 转化为父类后是 steakResponse,与 foodResponse不一致,因而不能进行转换
//    foodResponse = steakSuccessResponse;
}

统配符类型

泛型的继承关系与 类型参数 的继承关系无关,但是在实际开发中,经常需要限制 类型参数,因而引入了通配符:

<? extends ClassName> : ClassName 为类型参数的父类,即为继承关系中的上界限

<? super ClassName> : ClassName 为类型参数的子类,即为继承关系的下界限

public class Meal<T extends Food> {
    // 套餐内的食物
    private T food;

    public T getFood() {
        return food;
    }

    public void setFood(T food) {
        this.food = food;
    }
}
public static void main(String[] args) {
    // 通配符意义:可能为 Meat 的任意子类,包括Meat
    Meal<? extends Meat> orderMeal1 = new Meal<>();
    // set 方法中,需要传入一个适用于 Meat 所有子类的类(不存在)
    // 以下所有的 setFood 方法,都编译不过,因为没有一个可以表示所有子类的
//    orderMeal1.setFood(new Food());
//    orderMeal1.setFood(new Meat());
//    orderMeal1.setFood(new Steak());
  
    // 赋值时,需要赋值一个父类,可以用子类代替
    Food food1 = orderMeal1.getFood();   // Food <- Meat 可以
    Meat meat1 = orderMeal1.getFood();   // Meat <- Meat 可以
    // steak 是 meat 的子类,父类不可以赋值给子类
//    Steak steak1 = orderMeal1.getFood();  // Steak <- Meat 不行
    
    // 通配符意义: Meat 所有父类,包括Meat
    Meal<? super Meat> orderMeal2 = new Meal<>();
    // Food 是 Meat 的父类,但不一定包含所有父类属性,因而无法通过编译
//    orderMeal2.setFood(new Food());
    // Meat 是 Meat 类型,一定包含所有 Meat 父类的属性
    orderMeal2.setFood(new Meat());
    // Steak 是 Meat 子型,一定包含所有 Meat 父类的属性
    orderMeal2.setFood(new Steak());
      
    // java内万物皆对象,Object 是所有 java 类的父类
    // 如果Meal 定义时,public class Meal<T>,getFood,只支持Object
    Object object = orderMeal2.getFood();
    // Meal 定义时,public class Meal<T extends Food>
    // 类型参数的最高父类即为 Food,所以 Food 可以兼容 <? super Meat> 的所有类
    Food food2 = orderMeal2.getFood();
    // <? super Meat> 可以为Food,而 Food 不一定是 Meat
//    Meat meat2 = orderMeal2.getFood();
    // 父类无法转换为子类
//    Steak steak2 = orderMeal2.getFood();
}

如在下方的 Meal<Food> 中,需要调用所有的 FoodtasteComment() 方法,此时则需要用到通配符:

<? extend Food> —— (继承关系中)向下兼容 类型参数,即指定类型的子类

class Restaurant {
    private String getFoodTasteComment(Meal<Food> meal) {
        return meal.getFood().tasteComment();
    }

    private String getFoodTasteComment2(Meal<? extends Food> meal) {
        return meal.getFood().tasteComment();
    }
}

public static void main(String[] args) {
    Restaurant restaurant = new Restaurant();
    Meal<Food> foodMeal = new Meal<>();
    Meal<Steak> steakMeal = new Meal<>();
    restaurant.getFoodTasteComment(foodMeal);
    // steakMeal 和 foodMeal 不是继承关系,因而无法在此兼容
//    restaurant.getFoodTasteComment(steakMeal);
  
    restaurant.getFoodTasteComment2(foodMeal);
    // 通过<? extends Food> 的通配符,兼容了foodMeal 和 steakMeal 的传参
    restaurant.getFoodTasteComment2(steakMeal);
}

除了获取 类型参数 的类型属性外,还可能需要设置,例如餐馆周年庆,所有与肉有关的 Meal 都可以免费升级为 Meal<Steak>,此时,需要用到通配符。

<? super Food> —— (继承关系中)向上兼容 类型参数,即指定类型的父类

public class Bread extends Food{
    @Override
    public String tasteComment() {
        return "Bread, not bad.";
    }
}

class Restaurant {
    // 此方法不兼容 Meal<Meat> 类型入参
    private void updateMeal(Meal<Food> meal) {
        meal.setFood(new Steak());
    }
  
    // 入参定义为 Meal<? extends Food> meal 时,无法实现赋值
    // 因为 extends Food 的类型可能会有与 Steak 无关的类型,如 Meal<Bread> 之类
//    private void updateMeal2(Meal<? extends Food> meal) {
//        meal.setFood(new Steak());
//    }
  
    private void updateMeal3(Meal<? super Steak> meal) {
        meal.setFood(new Steak());
    }
}

public static void main(String[] args) {
    Restaurant restaurant = new Restaurant();
    Meal<Food> foodMeal = new Meal<>();
    Meal<Meat> meatMeal = new Meal<>();
    Meal<Steak> steakMeal = new Meal<>();
    Meal<Bread> breadMeal = new Meal<>();
    restaurant.updateMeal(foodMeal);
    // 此方法的入参为 Meal<Food>,无法兼容 Meal<Meat> 或 Meal<Bread>
//    restaurant.updateMeal(meatMeal);
//    restaurant.updateMeal(breadMeal);
    restaurant.updateMeal3(foodMeal);
    restaurant.updateMeal3(meatMeal);
    restaurant.updateMeal3(steakMeal);
    // bread 不在 steak 的父类中,因而也不兼容
//    restaurant.updateMeal3(breadMeal);
}

那如果,要设置为所有套餐(包括 Bread 套餐)都能升级为 Meal<Steak> 呢?

那就必须修改整个泛型而不是只单单类型参数了,编译器不兼容,即便编译器兼容了,在运行时也可能会出错。例如,Bread 可能自定义了一个 pubilc 方法,而 Steak 没有,在运行时就会出现崩溃

class Restaurant {
    private Meal<Steak> updateMeal4(Meal<? extends Food> meal) {
        return new Meal<Steak>();
    }
}

public static void main(String[] args) {
    Restaurant restaurant = new Restaurant();
    Meal<? extends Food> orderMeal = new Meal<>();
    orderMeal = restaurant.updateMeal4(orderMeal);
}

虚拟机如何实现泛型 总结

在虚拟机中,并没有真正实现泛型,即 Meal<Bread>Meal<Meat> 在虚拟机中,并不是两个明确不同的类。

虚拟机用类型擦除的方式,间接地实现了泛型。即编译完成后,会将参数类型变为所有类的父类,即Object。擦除后,泛型变成: Meal<Object>;类里面定义的参数类型会转变为 Object,然后再使用时,编译器自动给参数类型的参数加入了对应的类型强转

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • ArrayList就是个泛型类,我们通过设定不同的类型,可以往集合里面存储不同类型的数据类型(而且只能存储设定的数...
    dinel阅读 477评论 0 2
  • 泛型的目的 在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类...
    风月寒阅读 347评论 0 2
  • 大纲一、为什么需要泛型?泛型的优点二、泛型定义三、限定"类型变量"四、泛型中的约束和局限性五、泛型类型的继承规则和...
    芒果味的你呀阅读 557评论 1 1
  • Java 泛型 1、泛型的精髓是什么 2、泛型方法如何使用 概述: 泛型在java中具有重要地位,在面向对象编程模...
    SeanJX阅读 479评论 0 0
  • 1.泛型的由来 一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义的类型,如果要编写可以适用于多...
    BigDreamMaker阅读 544评论 0 1