JAVA泛型教程

泛型

什么是泛型

在强类型语言中,可以先不设置参数类型,用某个符号作为占位符.最后在运行时指定参数类型来替换.

为什么要使用泛型

  • 动态化参数,代码编写可以更加灵活、复用性高
  • 类型安全,避免手动的类型转换.保证在运行时出现的错误能提早放到编译时检查
  • 解耦类或方法所使用的类型之间的约束

java的泛型

要求

java的泛型是从jdk1.5之后引入的.所以使用泛型
的最低要求是jdk1.5

为什么会在jdk1.5之后引入泛型

最主要的原因就是为了重写容器相关的类(Collection).如果没有泛型,都使用Object来代替,那么就会出现大量的类型转换与模板代码,复用性低.

使用场景

  • 暂时不指定类型,而是稍后再决定具体使用什么类型
  • 限制其类型,使类型需要保持一直
  • 大量的样板重复代码,只是类型不同

泛型语法

在类上编写泛型

用<>表示泛型,T表示占位符类型,可以自定义不一定叫T.这样就可以先不指定类型,最后在调用时指定.

  • 示例

public class Holder<T> {

    // 先不指定类型
    private T value;

    public void set(T val) {
        value = val;
    }

    public T get() {
        return value;
    }

    public static void main(String[] args) {
        // 在调用时指定类型
        Holder<String> holder = new Holder<>();
        holder.set("test");
        System.out.println(holder.get());
    }

}

在方法上编写泛型

摘自Thinking in java泛型设计的基本原则,如果泛型方法能够代替泛型类,应该尽量优先使用泛型方法.因为在类上定义泛型是全局的,在方法上定义作用在方法上,作用范围更小.这样就为类上预留了泛型参数,适合以后扩展.

  • 示例

与类上定义泛型的语法不同的是,只要在返回值之前定义<T>,就能在方法上定义泛型

public class HolderUtils {

    public static <T> T getHolder(Holder<T> holder) {
        return holder.get();
    }

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.set("test");
        System.out.println(HolderUtils.getHolder(holder));
    }
}

泛型的工作原理

从上面的例子中看出,感觉java的泛型就是在运行时指定类型就像 Holder<String> holder = new Holder<>();把指定的类型String 动态的替换成占位符T.实际本非如此.在java中泛型参数类型都会转成Object,擦除实际的类型.

  • 下面是刚才例子的反编译的结果
javap -c Holder.class
Compiled from "Holder.java"
public class generics.Holder<T> {
  public generics.Holder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       // 传入的T转换成Object,类型被擦除
       2: putfield      #2                  // Field value:Ljava/lang/Object;
       5: return

  public T get();
    Code:
       0: aload_0
        // 返回的T也为Object,类型被擦除
       1: getfield      #2                  // Field value:Ljava/lang/Object;
       4: areturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generics/Holder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String test
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1
      18: invokevirtual #8                  // Method get:()Ljava/lang/Object;
      // 需要时类型检查,确保类型安全,然后强制类型转换
      21: checkcast     #9                  // class java/lang/String
      24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return
}

从上面的结果可以看出泛型的工作原理为

  • 编译期检查类型
  • 编译之后把实际类型替换成Object类型,参数实际类型
  • 在需要时,编译器自动先做类型检查然后强制转成需要的类型

java 泛型的局限性

java的泛型不是纯粹的.在使用泛型时,因为擦除无法获取具体的类型.不能使用泛型来执行类型相关的例如new instanceof 反射等操作.

// 因为获取不到真实的类型,转成Object类型了,则会出现以下几个问题
// 1. 泛型是不能new对象的
T t  = new T(); // error

// 2. 泛型不能用instanceof
t instanceof // error

public static void main(String[] args) {

    Holder<String> holder = new Holder<>();
    holder.set("test");
    
    // 3.使用getClass().getTypeParameters() 只能获取占位符 [T], 而不是实际的类型   
    System.out.println(Arrays.toString(
            holder.getClass().getTypeParameters()));
}

什么是擦除

java的泛型是通过擦除来实现的.在使用泛型时,任何类型信息都会被擦除.在泛型代码内部,无法获得有关泛型参数类型的信息.只能获取到定义泛型的占位标识符

使用擦除的原因

  • java的泛型不是从jdk1.0就已经存在的,泛型是从1.5开始的,需要向下兼容

  • 老类库需要升级泛型的兼容性.例如有x、y类库, x 依赖于 y.
    这时y升级使用了泛型,x没有使用泛型.那么势必不能对调用y的x产生影响.所以x应该不具备感知y使用泛型的能力.所以当依赖的类库使用了泛型,则不能对现有类库造成影响.所以泛型不是强制的,则类型信息必须被擦除.

如何解决擦除的问题

1. 定义泛型边界

因为泛型的擦除,我们获取不到实际的类型,可以通过泛型边界来获取实际的类型.如果定义边界,泛型将会擦除到第一个边界

语法
使用extends关键字,来定义边界,表示传入的类型必须是其边界的类型或者子类

例子:

public class HolderUtils {

    public static <T extends Person> T getHolder(Holder<T> holder) {
        T t = holder.get();
        t.run();
        return t;
    }

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.set("test");
        // System.out.println(HolderUtils.getHolder(holder)); 
       // error 类型不匹配,已经限定了只能是Person以及Person的子类

        Holder<Person> holderPerson = new Holder<>();
        holderPerson.set(new Person());
        System.out.println(HolderUtils.getHolder(holderPerson));
    }
}

class Person {

    public void run() {
        System.out.println("run ...");
    }
}


因为定义了边界所以擦除到第一个边界类型Person.
这样就不会使用Object来代替,就能够使用边界类型的方法与属性.
解决了一部分的擦除问题

2. 传入类型标识

通过传入类型标识Class<T> tClass来确保泛型类型,
使用 class.newInstance()来创建对象,t就能确定其类型信息,
能够执行类型相关的操作例如instanceof等.这样就彻底解决了擦除的带来的类型问题.

public class HolderUtils {

    public static <T extends Person> T getHolder(Holder<T> holder) {
        T t = holder.get();
        t.run();
        return t;
    }

    public static <T> T newHolder(Class<T> tClass) throws IllegalAccessException, InstantiationException {
        T t = tClass.newInstance();
        if (t instanceof Person) {
            System.out.println("type is Person...");
        }
        return t;
    }

    public static void main(String[] args) throws Exception{
        Person person = newHolder(Person.class);
    }
}

class Person {

    public void run() {
        System.out.println("run ...");
    }
}


控制台输出:type is Person...

通配符

在讲通配符之前,先要弄清2个知识点

  • 什么是泛型容器类型
  • 什么是泛型持有类型

泛型容器类型是可以自动向上转型的

// 泛型容器类型 
List list = new ArrayList();
ArrayList -> List OK
表示ArrayList是List的某一种类型

泛型持有类型是不支持向上转型的

class Fruit {}
class Apple extends Fruit {}
// 泛型持有类型
List<Fruit> fruit = new ArrayList<Apple>();
ArrayList -> List error 编译不通过

虽然Apple能够自动向上转型为Fruit,但是装有苹果的篮子不代表就能装水果.当泛型容器类型定义持有对象类型时,容器类型就不能向上转型.

为什么容器类型定义持有对象时就不能自动向上转型了

主要原因是会出现类型转换错误的问题,泛型的目的就是为了让运行期出现的问题,放到了编译器处理.增加了代码的健壮性,避免了类型转换错误.

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
// 数组可以编译通过,但是在运行期出现异常
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
try {
  fruit[0] = new Orange();// ArrayStoreException
} catch(Exception e) { System.out.println(e); }

控制台输出: java.lang.ArrayStoreException: generics.Orange

// 而泛型直接编译不通过,避免了运行期的类型转换错误

List<Fruit> fruit = new ArrayList<Apple>(); // 编译不通过

以上例子就说明了,当装有水果的篮子引用了只能装苹果篮子的实例,而去装橘子时,就会出现类型转换错误.而泛型的好处就是从运行期的错误提前到了编译器.
那有什么办法可以让泛型容器在定义了持有对象时即能够向上转型又能够保证类型安全呢?这时候就可以定义通配符

什么是通配符

通配符就是定义泛型容器的上下界,使其泛型容器类型可以做到类型安全的自动类型转换

通配符的语法

使用 <?> 来表示类型范围

上界

使用 <? extend Fruit> 来表示上界.表示该集合都继承于Fruit,都能返回Fruit的集合.所以读取该集合的元素是类型安全的.添加元素是类型不安全的.

List<? extends Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // ok
Fruit apple = new Apple();
fruit.add(apple); // error

下界

使用 <? super Fruit> 来表示下界.表示该集合父类至少是Fruit的集合.所以添加元素是类型安全.读取元素是类型不安全的,只能返回Object.

List<? super Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // error
Fruit apple = new Apple();
fruit.add(apple) // ok

无界

使用<?>来表示无界,表示该集合可以是任何类型,但是很遗憾,如果使用无界则不能添加元素,因为元素是任何类型,在读取时只能是Objcet,将丢失添加的类型,向下转型有风险.所以是类型不安全的.

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

推荐阅读更多精彩内容