Java泛型到底是什么

Java泛型到底是什么

前言

有一位朋友给我说在她阅读Java内置容器的时候,泛型给她造成了些困扰,想让我当面给她讲讲。我觉得写篇文章效果更好,她想看几遍看几遍。

为什么要学习泛型

在Java中编写通用容器的时候需要使用到大量的泛型类,泛型接口和泛型方法。而这些也正是Java内库容器List<E>, Set<E>,Map<K,V>能够处理各种类型元素的基础,缺乏这方面的知识对阅读Javan中的容器代码会有些障碍。

Java为什么需要泛型

在泛型引入前,程序员在使用一个通用容器的时候需要来处理下面两个事情。

  • 需要想清楚要向容器中放入什么元素
  • 需要对从容器中取出的元素进行手动类型转换

实例代码

public class Fruit {}
public class Apple extends Fruit {
    public void functionInApple() {}
}
public class Orange extends Fruit {
    public void functionInOrange() {}
}
public class FujiApple extends Apple {}
public class ObjectHolder {

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

}
public class ObjectHolderTest {

    public static void main(String[] args) {

        ObjectHolder holder = new ObjectHolder();
        
        // 需要想清楚要向容器中放入什么元素
        // 向holder容器中放入Apple
        holder.setObject(new Apple());
        
        // 需要对从容器中取出的元素进行手动类型转换
        // 我们知道容器中放入的是Apple,拿出容器中的对象,将他转型为Apple
        Apple apple = (Apple) holder.getObject();
        apple.functionInApple();
        
        // 需要对从容器中取出的元素进行转型,但是可能会因为疏忽造成转型错误。 
        // 我误以为这个holder容器中放的是Orange,结果导致程序运行出现异常
        Orange orange = (Orange) holder.getObject();
        orange.functionInOrange();
    }

}

运行结果:从结果中我们可以看到由于取出的Apple错误的转型为Orange然后调用Orange中的orange.functionInOrange()方法遇到了运行时异常。

functionInOrange has been called
Exception in thread "main" java.lang.ClassCastException: com.lin.chen.javatestdemo.Apple cannot be cast to com.lin.chen.javatestdemo.Orange
    at com.lin.generic.ObjectHolderTest.main(ObjectHolderTest.java:24)

Java泛型帮我们做什么

为了解决上面的两个问题,Java的设计者让编译器帮我们处理了下面两件事情。

  • 编译器检查我们放入容器的元素是否满足泛型容器定义的期许,我们只需要告诉编译器我这个容器是一个处理何种类型的容器即可。
  • 编译器为从容器中取出的元素进行自动转型。

在泛型介入后,程序员的关注点由2点变为1点:

  • 定义容器处理的类型,这样放入容器的检查和取出容器的转型都交由编译器来完成。

泛型类的使用

定义方式:

public class ClassName<T>

解决问题

  • 程序员只需要在实例化容器的时候,定义容器允许处理的类型,那么放入容器的检查和取出容器的转型都交由编译器来完成。

实例代码

public class GenericHolder<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    
    public boolean equals (Object obj) {
        return t.equals(obj);
    }

}
public class GenericHolderTest {

    public static void main(String[] args) {

        //通过GenericHolder<Apple>定义容器处理的类型为Apple,之后放入容器和从容器取出数据的检查通通交给编译器
        GenericHolder<Apple> holder = new GenericHolder<Apple>();
        holder.setT(new Apple());
        // 编译错误,编译器检查我们放入容器的元素是否满足泛型容器定义的期许
        // 编译器报错原文:The method setT(Apple) in the type GenericHolder<Apple> is not applicable for the arguments (Orange)
        // holder.setT(new Orange());
        
        //编译器为从容器中取出的元素进行自动转型
        Apple apple = holder.getT();
        apple.functionInApple();
        
        //编译错误:编译器检查出取出的类型不能安全的转型为Orange
        //Type mismatch: cannot convert from Apple to Orange
        //Orange orange = holder.getT();
    }

}

运行结果:

functionInOrange has been called

泛型接口的使用

定义方式:

public interface InterfaceName<T>

解决问题

  • 在没有泛型接口的时候,接口约束的参数类型都只能是具体类型。那么这将是接口设计上的灾难。想象我们有一个叫IntegerIterator的接口约束一个next()方法返回一个具体的Integer类型.
public interface IntegerIterator {
  
    boolean hasNext();

    Integer next();
}

现在情况有变,我们需要用next()方法返回一个String.难道我们还需要再写一个StringIterator吗?显然不是。Java的Iterator接口已经告诉了我们解决方案。这个答案就是使用泛型接口。

public interface Iterator<T> {
  
    boolean hasNext();

    T next();
}

泛型方法

定义方式:

public static <T> void methodName(T t)

解决问题

  • 一个类中的静态方法要使用泛型参数
  • 一个类中的某一个或者某些方法有使用泛型的需求,但是这并不是一个泛型类

一个类中的静态方法要使用泛型参数的例子

public class GenericMethod <T>{
    
        //  泛型类中的静态方法在使用泛型参数的时候遇到了编译错误因为一个静态方法不能使用非静态引用T,这个T是泛型类定义的参数类型
        //  编译器报错原文:Cannot make a static reference to the non-static type T
        //  public static void printClassName(T t) {
        //      System.out.println(t.getClass().getName());
        //  }
    
    //定义泛型方法使类中的静态方法可以使用泛型参数
    public static <T> void printClassName(T t) {
        System.out.println(t.getClass().getName());
    }
    
    public static void main(String[] args) {

        GenericMethod.printClassName(1.0f);
        GenericMethod.printClassName(1);
        GenericMethod.printClassName("I'm String");
        GenericMethod.printClassName(new ArrayList<String>());
    }
}

输出结果

java.lang.Float
java.lang.Integer
java.lang.String
java.util.ArrayList

拥有泛型方法的类,并不一定是泛型类,而泛型类中使用了泛型参数的方法也不一定是泛型方法。
泛型方法和泛型类中使用到泛型的方法的区别:
泛型类中使用到泛型参数的方法:public static void printClassName(T t)
泛型方法:public static <T> void printClassName(T t),注意到static与void之间的斜体加粗 <T> ,这是泛型方法参数列表的定义,也是泛型方法定义的标志。
既然泛型方法并不要存在在泛型类中,那么上面的代码可以做如下修改:public class GenericMethod <T>

public class GenericMethod {

    public static <T> void printClassName(T t) {
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {

        GenericMethod.printClassName(1.0f);
        GenericMethod.printClassName(1);
        GenericMethod.printClassName("I'm String");
        GenericMethod.printClassName(new ArrayList<String>());
    }
}

输出结果

java.lang.Float
java.lang.Integer
java.lang.String
java.util.ArrayList

泛型边界

定义方式:

TeacherHolder <E extends SomeObject>

解决问题

  • 定义一个泛型容器类中持有的是某个类和他的子类

有这样一种场景。我有一个Teacher类和一个装Teacher或者Teacher子类的容器,Teacher有个teach方法,我会在这个容器中,调用这个Teacher类或者他子类的teach方法。那么就会使用到泛型的边界。

public class Teacher {
    //Teacher的teach方法
    public void teach() {}
}
public class TeacherHolder <T>{
    
    private T t;
    
    public TeacherHolder (T t) {
        this.t = t;
    }
    
    //没有边界的泛型类,无法通过编译,因为编译器并不知道这个T应该拥有什么样的方法
    //编译器报错原文:The method teach() is undefined for the type T
    //public  void callTeach() {
    //  t.teach();
    //}
}
public class TeacherHolder <T extends Teacher>{

    private T t;
    
    public TeacherHolder (T t) {
        this.t = t;
    }
    
    //通过定义边界<T extends Teacher>,t拥有了调teach方法的能力
    public  void callTeach() {
        t.teach();
    }
}

泛型通配符

定义

  • 上边界通配符Holder<? extneds T>
  • 下边界通配符Holder<? super T>
  • 无界通配符Holder<?>
    解决问题
  • 解决泛型容器不能支持协变得问题

协变这个概念有可以简单理解为可以使用父类型的地方均可以用子类型替代。Java的数组是支持协变的。举一个例子,Apple是Fruit的子类型,能使用Fruit[] 的地方均可以使用Apple[]代替。而支持Holder<Fruit>的地方并不支持Holder<Apple>,因为在Java的编译器看来Holder<Fruit>与Holder<Apple>完全是不同的类型(最好的佐证就是Holder<Fruit> fruits = new Holder<Apple>是一个让编译器报错的语句),虽然他们在运行时经过泛型的擦除使得他们是同一种类型。
Java泛型容器的协变由泛型的上界通配符来实现

public class CovariantArrays {

    public static void main(String[] args) {
        //定义一个Fruit[]可以将Apple[]赋给Fruit[]的,这就是协变的应用
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new FujiApple();
        fruits[3] = new Fruit();
        fruits[4] = new Orange();
    }

}

泛型类如何进行协变

public class ConvarianGenericHolder {

    public static void main(String[] args) {

        // 编译错误:编译器不会认为GenericHolder<Apple>是GenericHolder<Fruit>的子类
        // 编译器报错原文:Type mismatch: cannot convert from GenericHolder<Apple> to GenericHolder<Fruit> 
                //GenericHolder<Fruit> fruitsHolder = new GenericHolder<Apple>();
            //使用上边界通配符实现Java泛型容器的协变
        GenericHolder<? extends Fruit> fruitsHolder = new GenericHolder<Apple>();

    }

}

各种泛型通配符的使用限制

泛型通配符的使用限制是让初学Java最头疼的问题之一,下面例子为了便于大家理解我直接使用List<E>容器来做讲解,要理解这些限制需要们牢记下面5个知识点:

  1. List<? extends Fruit>, List<? super Fruit>, List<?>中的<? extends>在编译器看来所表达的并不是一个类型范围,而是一个没指定名字的具体类型,注意具体类型4个字。
  2. Object是所有类型的基类。
  3. 编译器认为null是所有类型的子类,由null可以向任何类型赋值可知。相信大家都使用过String str = null, ArrayList<Integer> list = null诸如此类的赋值。
  4. 子类可以安全的转型为超类。
  5. 超类无法安全的转型为子类,因为子类拥有超类并不拥有的信息,例如子类有个超类没有的方法,那么超类无法安全的转型为子类。
    有了这几条基础那么我们可以开始去理解通配符使用的限制了。

<? extends Fruit>的限制

  • 添加:不能添加除null之外的任何元素进入容器。由知识点1知道List<? extends Fruit>是一个Fruit子类的具体类型。既然List<? extends Fruit>是一个具体类型,那么可以理解为<? extends Fruit>为一个不知道名字的具体的水果。用<unknowNameFruit>表示。向一个List<unknowNameFruit>的list添加Apple,Orange元素。就如同向List<Orange>的List添加Apple元素不会被编译器通过。只能添加null时由知识点3推导出来的。
  • 读取:可以从容器中安全的读取Fruit和Fruit的子类并安全的转型为Fruit。由5可知向上转型是安全的,所以这个unknowNameFruit可以安全的从容器中取出并转型为Fruit。
    示例代码:
public class ExtendTypeWildcards {

    public static void main(String[] args) {
        List<? extends Fruit> fruits = new ArrayList<Apple>();
        // 编译错误:这样的行为你可以理解为向一个List<unknowNameFruit>里添加Apple元素会失败一样,就像
        //我们向List<orange>的list中添加Apple会失败一样
        // 编译器报错原文: The method add(? extends Fruit) in the type ? extends Fruit> is not applicable for the arguments (Apple)
        //fruits.add(new Fruit());
        //fruits.add(new Apple());
        //fruits.add(new Orange());
        //fruits.add(new FujiApple());
        fruits.add(null);
        //从容器中取出unknowNameFruit并安全转型为Fruit
        Fruit fruit = fruits.get(0);
    }

}

<? super Fruit>的限制

  • 添加:可以添加Fruit和Fruit的子类进入容器。我们可以理解为<? super Fruit>是一个某中类型的SomeObject,他们的继承关系是 Fruit extends SomeObject, SomeObject extends Object。
  • 读取:从容器中取出的元素只能安全转型为Object.这也是由2&4&5推导出来的。因为这个SomeObject是Fruit的超类,那么将他转型为任何Fruit和Fruit的子类都无法满足知识点5,故只能转型为Object。

示例代码:

public class SuperTypeWildcards {

    public static void write(List<? super Fruit> fruits) {
        //compile error: The method add(? super Fruit) in the type List<? super Fruit> is not applicable for the arguments (Object)
        //fruits.add(new Object());
        //可以成功的添加Fruit和Fruit的子类
        fruits.add(new Fruit());
        fruits.add(new Apple());
        fruits.add(new Orange());
        fruits.add(new FujiApple());
            //无法安全的转型为除Object之外的任何类型
        //T编译器报错原文: cannot convert from capture#5-of ? super Fruit to Fruit
        //Fruit fruit = fruits.get(0);
        //只能安全的转型为Object
        Object object = fruits.get(0);
    }
}

<?>的限制

无界通配符的限制是类统配和父类通配符的并集。

  • 添加:同子类通配符一样。不能添加除null之外的任何元素进入容器。
  • 读取:同父类通配符一样。从容器中取出的元素只能安全转型为Object.

示例代码:

public class Wildcards {

    public void unboundArgsForList(List<?> list) {

        // 编译错误:无法向容器中添加任何除null外的元素
        // 编译器报错原文:The method add(capture#4-of ?) in the type List<capture#4-of ?> is not applicable for the arguments (Fruit)
        //list.add(new Fruit());
        //list.add(new Apple());

        list.add(null);

        // 编译错误:从容器中取出的元素只能安全转型为Object
        // 编译器报错原文:Type mismatch: cannot convert from capture#5-of ? to Apple
        //Apple apple = list.get(0);
        
        Object obj = list.get(0);

    }
}

参考书籍

Java编程思想(第四版)15章-泛型

博文https://blog.csdn.net/s10461/article/details/53941091

一课一练

下面是Java8中Stream这个泛型接口中的部分实现.
问题一:filter和map哪一个是泛型方法,哪一个不是?
问题二:为什么filter的方法中filter(Predicate<? super T> predicate)的Predicate使用<? super T>这个下边界通配符。
为什么map的方法中map(Function<? super T, ? extends R> Function中的T的定义用了下边界通配符<? super T>,而R< ? extends R>使用了上边界通配符,他的目的是什么?

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    Stream<T> filter(Predicate<? super T> predicate);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

请你在留言区写下你的答案,我会选出经过认真思考的留言,然后抽取一位幸运读者,什么礼品都不送。如果你也有朋友为泛型发愁,你可以分享给他,或许对他有所帮助。

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

推荐阅读更多精彩内容

  • “泛型”这个术语的意思是:"适用于许多许多的类型”。如何做到这一点呢,正是通过解耦类或方法与所使用的类型之间的约束...
    王侦阅读 1,124评论 0 0
  • 1.泛型的由来 一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义的类型,如果要编写可以适用于多...
    BigDreamMaker阅读 547评论 0 1
  • JAVA-泛型 sschrodinger 2018/11/15 简介 泛型是Java SE 1.5的新特性,泛型的...
    sschrodinger阅读 514评论 0 2
  • 参数类型的好处 在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Obje...
    杰哥长得帅阅读 872评论 0 3
  • 前言 泛型(Generics)的型变是Java中比较难以理解和使用的部分,“神秘”的通配符,让我看了几遍《Java...
    珞泽珈群阅读 7,804评论 12 51