Java泛型

泛型的好处

使用泛型的好处我觉得有两点:1:类型安全 2:减少类型强转

下面通过一个例子说明:

假设有一个Test类,通用的实现是:

class Test {
    private Object o;

    public Test(Object o) {
        this.o = o;
    }

    public Object getObject() {
        return o;
    }

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

我们可以这样使用它:

public static void main(String[] args) {
    Test test = new Test(new Integer(1));
    //编译时不报错
    //运行时报  java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    String o = (String) test.getObject();
}

看一个使用泛型的例子:

class Test<T> {
    private T o;

    public Test(T o) {
        this.o = o;
    }

    public T getObject() {
        return o;
    }

    public void setObject(T o) {
        this.o = o;
    }
}

public static void main(String[] args) {
    Test1<Integer> test = new Test1<Integer>(new Integer(1));
    //编译时报错,无法通过编译
    //String o = test.getObject();

    //正常运行
    Integer o = test.getObject();
}

从上面的对比中能够看出两点:

1.使用泛型之后在编译时报错而非运行时,减少了出错的几率;

2.使用泛型之后编译器不再要求强转

定义泛型

泛型的机制能够在定义类、接口、方法时把类型参数化,也就是类似于方法的形参一样,把类型当做参数使用。

泛型参数部分使用<>包裹起来,比如<T>,T声明了一种类型,习惯上,这个类型参数使用单个大写字母来表示,指示所定义的参数类型。有如下惯例:

E:表示元素

T:表示类型

K:表示键

V:表示值

N:表示数字

定义泛型类

上面的例子就是一个很好的演示

class Test<T> {
    private T o;

    public Test(T o) {
        this.o = o;
    }

    public T getObject() {
        return o;
    }

    public void setObject(T o) {
        this.o = o;
    }
}

使用泛型定义类之后,泛型参数 T 可以运用到该类中:可以声明成员变量类型、可以声明成员函数返回值类型、可以声明成员函数参数类型。

要注意:泛型参数T不能用于声明静态变量,同时也不能用于new一个对象比如:T o = new T();

下面的类型擦除会说到原因。

定义泛型接口

interface Test<T> {
    public T test(T t);
}

使用泛型定义接口之后,泛型参数 T 可以运用到该接口中:可以声明接口函数返回值类型、可以声明接口函数参数类型。

定义泛型方法

可以单独给方法使用泛型,而不是泛化整个类:

public static <T> T getT(T t){
    return t;
}

使用泛型定义方法后,泛型参数 T 可以声明该方法的返回值类型、可以声明该方法的参数类型。

要注意,定义方法所用的泛型参数需要在修饰符之后添加。

定义多个泛型参数

以接口为例:

interface Test<T, S> {
    public T testT(T t);
    public S testS(S s);
}

public static void main(String[] args) {
    //编译时报错
    //getT("s");
}

多个泛型参数在尖括号中使用逗号隔开。类的泛化与方法的泛化类似。

泛型参数的界限

定义泛型参数界限有这样两种意义:

1.有时候我们希望限定这个泛型参数的类型为某个类的子类或者超类;

2.上面的例子中可以看到,我们定义了泛型参数,向方法中传入某种类型,这种类型是未知的,因此我们无法使用这种类型定义的变量,不能够调用它的方法。

public static <T extends Number> Integer getT(T t) {
    return  new Integer(t.intValue());
}

上面例子中,<T extends A>表示T是A或A的子类,他限定了传入泛型方法参数的类型必须为A或A的子类,同时,在方法体中我们也可以使用t这个实参就像使用A的实例一样,调用Number具有的public方法。

除了<T extends A>限定T是A或A的子类外,还可以使用<T super A>这种方式来限定T是A或A的超类。

A可以是某个类或者接口。

除此以外,还可以为泛型参数限定多个限制范围,如<T extends A & B & C>,限定范围中最多只能有一个类(某个类只能有一个父类~~),并且他必须是限定列表中的第一个。

Class A { // }
interface B { // }
interface C { // }

//正确
class D <T extends A & B & C> { // }
//编译时报错
class D <T extends A & B & C> { // }

泛型的继承

看一下jdk中List的泛型继承例子:

public interface List<E> extends Collection<E>{//...}

List<String> 就是 Collection<String> 的子类。

假如定义自己的接口:

interface MyList<E,P> extends List<E> {
  void setPay(E e,P p);
  //...
}

MyList<String,String>MyList<String,Integer>MyList<String,Exception>都是List<String>的子类。

使用泛型

上面的例子中已经列举了一些使用泛化类或者泛化函数的例子,但是还有一些问题需要指出:

1.泛型参数只接受引用类型,不适用于基本类型

比如:

class Test<T> {}

public static void main(String[] args) {
    //无法通过编译,不接受int类型的泛化参数
    //Test<int> test = new Test();
}

而我们使用泛化函数时:

public static void main(String[] args) {
    getT(1);
}
public static <T> void getT(T t) {
}

是没有问题的,通过查看生成的字节码,发现getT(1)这个方法的字节码中1被自动装箱为Integer类型。

2.通配符的使用

考虑下面的情况:

class Test<T> {}
public static void getT(Test<Number> t) {
}

public static void main(String[] args) {
    Test<Double> test = new Test();
    //编译时报错
    //getT(test);
}

报错的原因很好理解,虽然Double是Number的子类,但Test<Double>并不是Test<Number>的子类,故类型检查无法通过。这一点一定要明白。

那么如果我们确实想要传入一个Test<Double>类型的形参呢?可以使用通配符:

class Test<T> {}
public static void main(String[] args) {
        Test<Double> test = new Test();
        //正常运行
        getT(test);
}
public static void getT(Test<? extends Number> t) {
}

Test<? extends Number>扩展了形参的类型,可以是Test<Double>Test<Integer>等,尖括号中的类型必须是Number或继承于Number。同样的,通配符也适用于super,如Test<? super A>

如果类型参数中既没有extends 关键字,也没有super关键字,只有一个?,代表无限定通配符。

Test<?>Test<Object>并不相同,无论T是什么类型,Test<T>Test<?>的子类,但是,Test<T> 不是 Test<Object> 的子类,想想上面的例子。

通常在两种情况下会使用无限定通配符:

  1. 如果正在编写一个方法,可以使用Object类中提供的功能来实现

  2. 代码实现的功能与类型参数无关,比如List.clear()与List.size()方法,还有经常使用的Class<?>方法,其实现的功能都与类型参数无关。

一般情况下,通配符<? extends Number>只是出现在使用泛型的时候,而不是定义泛型的时候,就像上面的例子那样。而<T extends Number>这种形式出现在定义泛型的时候,而不是使用泛型的时候,不要搞混了。

结合泛型的继承和通配符的使用,理解一下泛型的类型系统,也就是泛型类的继承关系:

以下内容来自:Java深度历险(五)——Java泛型


引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>Collection<Integer>等。如此循环下去,ArrayList<Long>HashSet<Double>等也都算是Collection<? extends Number>的子类型。
如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。


关于通配符的理解可以参考Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?

类型擦除

类型擦除发生在编译阶段,对于运行期的JVM来说,List<int>List<String>就是同一个类,因为在编译结束之后,生成的字节码文件中,他们都是List类型。

1.java编译器会在编译前进行类型检查

java编译器承担了所有泛型的类型安全检查工作。

2.类型擦除后保留的原始类型

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。

3.自动类型转换

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?

比如:

public class Test {  
    public static void main(String[] args) {  
        ArrayList<Date> list=new ArrayList<Date>();  
        list.add(new Date());  
        Date myDate=list.get(0);  
    }
}  

Date myDate=list.get(0);这里我们并没有对其返回值进行强转就可以直接获取Date类型的返回值。原因在于在字节码当中,有checkcast这么一个操作帮助我们进行了强转,这是java自动进行的。

更多的关于类型擦除的知识,参考 java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

实践

最近在重构公众号服务器的过程中,用到了泛型编程的知识。

public interface BaseServiceContext<T extends ReqBaseMessage, R> {
    public void selectService(T reqMeg);

    public R executeRequest();
}

上面是一个选择service的上下文接口,接收到用户请求后通过这个接口选择对应的service并且执行service。这个接口相当于一个工厂和策略模式的结合体。下面是这个接口的一种实现:

//请求为文本类型,返回string类型的处理结果
public class TextServiceContext implements BaseServiceContext<ReqTextMessage,String> {

    @Override
    public void selectService(ReqTextMessage reqMeg) {
        //.....
    }

    @Override
    public String executeRequest() {
        //.....
    }
}

可以看到,BaseServiceContext<ReqTextMessage,String>限定了selectService方法的参数类型和executeRequest方法的返回值类型,使其能够灵活的支持各种类型的参数和返回值。

看一下在没有学习泛型之前,这个接口是怎么实现的:

public interface BaseServiceContext {
    public void selectService(ReqBaseMessage reqMeg);

    public Object executeRequest();
}

public class TextServiceContext implements BaseServiceContext {

    @Override
    public void selectService(ReqBaseMessage reqMeg) {
        //根据业务逻辑对reqMeg进行强转,需要程序员自己判断
        //很有可能强转失败
    }

    @Override
    public Object executeRequest() {
        //返回类型为object,在调用方法的外部强转为需要的类型
        //很有可能强转失败
    }
}

可以看到没有使用泛型接口的情况下,类型不安全且增大了强转失败的风险。同时也需要程序员根据业务逻辑去判断该强转成什么类型。使用泛型接口之后就没有了这些问题,只需要在使用接口时声明好他的泛型参数就o了。

上面只是我在开发过程中体会到泛型的一个好处,类似的例子还有很多。

注意事项

参考

java 泛型编程(一)

泛型:工作原理及其重要性

Java深度历险(五)——Java泛型

java泛型详解

Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?

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

推荐阅读更多精彩内容

  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 990评论 0 3
  • 为什么需要泛型? 通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到...
    一只好奇的茂阅读 1,231评论 2 39
  • 我们知道,使用变量之前要定义,定义一个变量时必须要指明它的数据类型,什么样的数据类型赋给什么样的值。 假如我们现在...
    今晚打肉山阅读 928评论 0 1
  • 一、泛型简介1.引入泛型的目的 了解引入泛型的动机,就先从语法糖开始了解。 语法糖 语法糖(Syntactic S...
    Android进阶与总结阅读 1,013评论 0 9
  • 大人有吃夜宵的习惯,但是我们都明白吃夜宵对身体危害很大,孩子更是要从小培养好的饮食和生活习惯。 有的孩子睡觉晚,肚...
    米妮老师说儿推阅读 406评论 0 0