Java干货程序员java进阶干货

Java泛型

2017-11-25  本文已影响44人  德彪

泛型的好处

使用泛型的好处我觉得有两点: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 有何不同?

上一篇 下一篇

猜你喜欢

热点阅读