Java基础-泛型的使用及泛型实现原理

在泛型出现以前,类和方法只能接受具体的类型。假设我们自己实现一个简单的ArrayList,用来持有类A的实例,它可能是这样子的:

class A {}

class ArrayListA {
    private int size = 0;

    private A[] array = new A[100];
    
    public void add(A a) {
        array[size++] = a;
    }
    
    public A get(int index) {
        return array[index];
    }
}

现在如果需要一个ArrayList来持有类B的实例,由于没有泛型,那只能把同样的代码再写一遍,并将其中的A全部换成B。难道我们要为每一个类都写一个ArrayList吗,显然是不可能的。在泛型出现以前,jdk的ArrayList使用的方法是用Object作为类型参数,这样使用者需要自己做转型,就像下面这样:

public class GenericLearn {

    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add("aaa");
        String str = (String) arrayList.get(0);
    }

}

这样的向下转型,既不方便,也不安全。有没有一种办法,能让类型作为一种可选参数,使得一套代码能复用于多个类,且不需要自己做转型等动作,这便是泛型要解决的问题。

泛型的使用

1. 泛型类

class Holder<T> {
    
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    
    public void print() {
        System.out.println(t);
    }
}

上面是一个简单的泛型类,用一个<>来指明参数化类型。现在我们在使用Holder类时就可以指明类型,一旦类型被确定,它就不能用于其他类:

Holder<String> holder = new Holder<>();
// 编译错误
holder.setT(1);

2. 泛型接口

interface Handler<T> {

   void handle(T t);
}

class StringHandler implements Handler<String> {

   @Override
   public void handle(String s) {
       // doNothing
   }
}

泛型用于接口和用于类的方式差不多。

3. 泛型方法

泛型也可以直接用于方法:

class BatchUtil {
    
    public static <T> void batchExec(List<T> list, int batchSize, Consumer<List<T>> action) {
        for (int i = 0; i < list.size(); i += batchSize) {
            int endIndex = i + batchSize > list.size() ? list.size() : i + batchSize;
            List<T> tempList = list.subList(i, endIndex);
            action.accept(tempList);
        }
    }
}

以上方法定义了一个批量操作的工具类,你可以像这样使用它:

public class GenericLearn {

    public static void main(String[] args) {
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
        BatchUtil.batchExec(list, 3, System.out::println);
    }
}

输出:

[1, 2, 3]
[4, 5, 6]
[7]

4. 泛型边界

利用extends关键字,可以为泛型参数限定上边界。

public class GenericLearn {

    public static void main(String[] args) {
        Holder<A> holder1 = new Holder<>();
        Holder<B> holder2 = new Holder<>();
        // 编译错误
        Holder<String> holder3 = new Holder<>();
    }
}

class Holder<T extends A> {
    
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

class A {}

class B extends A{}

限定了边界后,泛型的类型就只能是指定类型及其子类。

5. 通配符

上面介绍的泛型类、接口、方法等都是如何定义泛型,至于使用泛型,最通常的就是指定泛型参数,如List<String>,指定了泛型参数为String。但是有时候我们会碰到这样的情况:

public class GenericLearn {

    public static void main(String[] args) {
        List<A> listA = new ArrayList<>();
        List<B> listB = new ArrayList<>();
        print(listA);
        // 编译错误
        print(listB);
    }
    
    public static void print(List<A> list) {
        for (A a : list) {
            System.out.println(a);
        }
    }
}

class A {}

class B extends A{}

方法接受参数List<A>,却无法将List<B>作为入参,这是因为虽然B可以向上转型为A,List<B>却无法向上转型为List<A>,为了解决这一问题,引入了通配符。

    public static void print(List<? extends A> list) {
        for (A a : list) {
            System.out.println(a);
        }
    }

将方法改写为这样后,可以编译运行。但是由此也会带来一个副作用:

public static void print(List<? extends A> list) {
        for (A a : list) {
            System.out.println(a);
        }
        // 编译错误
        list.add(new A());
        list.add(new B());
        list.add(new Object());
}

用了通配符后,无法对List进行add操作,这是因为List的add方法,其参数是泛型类,而通配符仅仅指定了泛型类的上界,因此任何以泛型类为入参的方法都无法使用。这是可以理解的,因为假如我们传入的是List<B>,那就不能往其中加入A的实例。

这是用通配符指定上界的情况,通配符也可以指定下界,还是以List为例:

public static void main(String[] args) {
        List<? super B> list = new ArrayList<>();
        list.add(new B());
        Object obj = list.get(0);
        // 编译错误
        list.add(new A());
        list.add(new Object());
        B b = list.get(0);
        A a = list.get(0);
}

用通配符指定下界后,可以执行add操作,但是只可以add类B的实例,因为我们不知道List持有的具体类型是什么,只知道它是B或其超类,在这样的条件下,只有往其中加入类B的实例是安全的,并且从其中拿到的对象只能当作Object来使用。

通配符也可以不指定边界,称为无界通配符,以List为例,对List<?>,不能执行add操作,从中取出的对象只能当作Object来使用。

对于通配符的使用有一个PECS原则(Producer Extends, Consumer Super),即如果将泛型类作为生产者使用,例如使用List的get方法,则用上界通配符;如果将泛型类作为消费者使用,例如使用List的add方法,则用下界通配符。

泛型的实现原理

很多人把Java的泛型称为伪泛型,因为Java的泛型只是编译期的泛型,一旦编译成字节码,泛型就被擦除了,即在Java中使用泛型,我们无法在运行期知道泛型的类型,因此像下面这样的操作是不行的。

class Holder<T> {
    // 编译错误
    private T t = new T();
    private T[] array = new T[0];
    private Class<T> clazz = T.class;
}

因为在jvm运行程序时,类Holder是不带有泛型类T的具体类型的,所以任何需要具体类型的操作,比如实例化对象等,都无法进行。

为了直观的理解泛型擦除,我们可以看一下Holder的字节码:

class Holder {


  // access flags 0x2
  // signature TT;
  // declaration: T
  private Ljava/lang/Object; t

  @groovyx.ast.bytecode.Bytecode
  void <init>() {
    aload 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    return
  }

  @groovyx.ast.bytecode.Bytecode
  public Object getT() {
    aload 0
    getfield 'Holder.t','Ljava/lang/Object;'
    areturn
  }

  @groovyx.ast.bytecode.Bytecode
  public void setT(Object a) {
    aload 0
    aload 1
    putfield 'Holder.t','Ljava/lang/Object;'
    return
  }
}

可以看到一旦编译成字节码,泛型将被取代为Object。比较一下直接使用Object类和使用泛型的区别:

public class GenericLearn {

    public static void main(String[] args) {
        Holder holder = new Holder();
        holder.setT("abc");
        String str = (String) holder.getT();
    }

}

class Holder {

    private Object t;

    public Object getT() {
        return t;
    }

    public void setT(String t) {
        this.t = t;
    }
}
@groovyx.ast.bytecode.Bytecode
  public static void main(String[] a) {
    _new 'Holder'
    dup
    INVOKESPECIAL Holder.<init> ()V
    astore 1
    aload 1
    ldc "abc"
    INVOKEVIRTUAL Holder.setT (Ljava/lang/String;)V
    aload 1
    INVOKEVIRTUAL Holder.getT ()Ljava/lang/Object;
    checkcast 'java/lang/String'
    astore 2
    return
  }

下面将Holder加入泛型:

public class GenericLearn {

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.setT("abc");
        String str =  holder.getT();
    }

}

class Holder<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
@groovyx.ast.bytecode.Bytecode
  public static void main(String[] a) {
    _new 'Holder'
    dup
    INVOKESPECIAL Holder.<init> ()V
    astore 1
    aload 1
    ldc "abc"
    INVOKEVIRTUAL Holder.setT (Ljava/lang/Object;)V
    aload 1
    INVOKEVIRTUAL Holder.getT ()Ljava/lang/Object;
    checkcast 'java/lang/String'
    astore 2
    return
  }

可以看到,两段代码的字节码基本是完全一样的,注意字节码中的checkcast,在不使用泛型时,我们需要将Object手动转型成String,而在使用泛型后,我们不需要自己转型,但实际上我们get到的对象仍然是Object类型的,只不过编译器会自动帮我们加入这个转型动作。

再来看一下使用泛型上界的情况:

class Holder<T extends A> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

class A {}
class Holder {


  // access flags 0x2
  // signature TT;
  // declaration: T
  private LA; t

  @groovyx.ast.bytecode.Bytecode
  void <init>() {
    aload 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    return
  }

  @groovyx.ast.bytecode.Bytecode
  public A getT() {
    aload 0
    getfield 'Holder.t','LA;'
    areturn
  }

  @groovyx.ast.bytecode.Bytecode
  public void setT(A a) {
    aload 0
    aload 1
    putfield 'Holder.t','LA;'
    return
  }
}

在有了泛型上界后,泛型将被擦除成定义的上界。

现在可以对Java的泛型做一个总结:Java的泛型只存在于编译期,一旦编译成字节码,泛型将被擦除。泛型的作用在于在编译阶段保证我们使用了正确的类型,并且由编译器帮我们加入转型动作,使得转型是不需要关心且安全的。

Java之所以用擦除来实现泛型,是因为Java是在1.5引入的泛型,为了兼容性,即以前没有泛型的程序能运行在新一代的jvm上,且让开发者可以以自己的进度将代码加入泛型特性,而选择了擦除这一办法。具体可以参考《Java编程思想》的泛型章节或知乎上的这篇回答:https://www.zhihu.com/question/28665443/answer/118148143

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

推荐阅读更多精彩内容