但行好事  莫问前程

泛型系列(二):深入泛型

所谓泛型,就是允许在定义类,接口,方法时使用类型形参,这个类型形参将在声明变量,创建对象,调用方法时动态指定(即传入实际的类型参数,也可称为类型实参)。java5改写了集合框架的全部接口和类,为这些接口,类增加了泛型支持,从而可以在声明集合变量,创建集合对象时传入类型实参。

1.1 定义泛型接口,类

下面是Java5改写后的List接口,Iterator接口,Map的代码片段。


//定义接口时指定了一个类型形参,该形参名为E
public interface List<E>{
    //在该接口里,E可作为类型使用
    //下面方法可以使用E作为参数类型
    void add(E x);
    Iterator<E> iterator(); //①
}

//定义接口时指定了一个类型形参,该形参名为E
public interface Iterator<E>{
    //在该接口里,E完全可作为类型使用
    E next();
    boolean hashNext();
    ...
}

public interface Map<K,V>{
    //在该接口里,K,V完全可作为类型使用
    Set<K> keySet();//②
    V put(K key,V value);
    ...
}

上面三个接口声明是比较简单的,除了尖括号中的内容,这就是泛型的实质:允许在定义接口,类时声明类型形参,类型形参在整个接口,类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。

除此之外,我们发现 发现①②处方法声明返回值类型是Iterator<E>,Set<K>,这表明Set<K>形式是一种特殊的数据类型,是一种与Set不同的数据类型,可以认为是Set类型的子类。

例如,在使用List类型时,为E形参传入String类型实参,则产生了一个新的类型:List<String>类型,我们可以把List<String>想象成E被全部替换成String的特殊List子接口。

//List<String>接口等同于如下接口
public interface ListString extends List{
    //原来的E形参全部变成String类型实参
    void add(String x);
    Iterator<String> iterator();
    ...
}

通过这种方式,虽然程序只定义了一个List<E>接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。必须指出:List<String>绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。

注意:
包含泛型声明的类型可以在定义变量,创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但是这种子类在物理上面并不存在。

通过上面的介绍发现,我们可以为任何类,接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个Apple类,这个Apple类可以包含一个泛型声明。

//定义Apple类时使用了泛型声明
public class Apple<T>
{
    // 使用T类型形参定义实例变量
    private T info;
    public Apple(){}
    // 下面方法中使用T类型形参来定义构造器
    public Apple(T info)
    {
        this.info = info;
    }
    public void setInfo(T info)
    {
        this.info = info;
    }
    public T getInfo()
    {
        return this.info;
    }
    public static void main(String[] args)
    {
        // 因为传给T形参的是String实际类型,
        // 所以构造器的参数只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());
        // 因为传给T形参的是Double实际类型,
        // 所以构造器的参数只能是Double或者double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}

上面程序定义了一个带泛型声明的Apple<T>类(不要理会这个类型形参是否具有实际意义),使用Apple<T>类时就可以为T类型形参传入实际类型,这样就可以生成如Apple<String>,Apple<Double>…形式的多个逻辑子类(物理上面并不存在)。

注意:
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如:为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数,Java7提供了菱形语法,允许省略<>中的类型实参。

1.2 从泛型类派生子类

当创建了带泛型声明的接口,父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些接口,父类时不能再包含类型形参。例如下面的代码就是错误的:

//定义A类继承Apple类,Apple类不能跟类型形参
public class A extends Apple<T>{ }

提示:
方法中的形参代表变量,常量,表达式等数据,这里我把它们称为形参,或者称为数据形参。定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是,定义类,接口,方法时可以声明类型形参,使用类,接口,方法时应该为类型形参传入实际的类型。

如果想从Apple类派生一个子类,则可以改成如下的代码:

//使用Apple类时为T形参传入String类型
public class A extends Apple<String>

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类,接口时可以不为类型形参传入实际的参数值,即下面的代码也是正确的。

//使用Apple类时,没有为T传入实际的类型参数
public class A extends Apple

如果从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将继承到StringgetInfo()void setInfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意这一点。下面程序示范了这一点。

public class A1 extends Apple<String>
{
    // 正确重写了父类的方法,返回值
    // 与父类Apple<String>的返回值完全相同
    public String getInfo()
    {
        return "子类" + super.getInfo();
    }
    /*
    // 下面方法是错误的,重写父类方法时返回值类型不一致
    public Object getInfo()
    {
        return "子类";
    }
    */
}

如果使用Apple类时没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作,这就是泛型检查的警告。如果希望看到该警告提示的更详细信息,则可以通过为ujavac命令增加-Xlint:unchecked选项来实现。此时。系统会把Apple<T>类里面的T形参当成Object类型处理,如下程序所示:

public class A2 extends Apple
{
    // 重写父类的方法
    public String getInfo()
    {
        // super.getInfo()方法返回值是Object类型,
        // 所以加toString()才返回String类型
        return super.getInfo().toString();
    }
}

上面程序都是从带泛型声明的父类来派生子类,创建带泛型声明的接口的实现类与此几乎完全一样。

1.3 并不存在泛型类

前面提到可以把ArrayList<String>类当成ArrayList的子类,事实上,ArrayList<String>类也确实像一种特殊的ArrayList类,这个ArrayList<String>对象只能添加String对象作为集合元素。但是实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。看看下面代码的打印结果:

// 分别创建List<String>对象和List<Integer>对象
        List<String> l1 = new ArrayList<>();
        List<Integer> l2 = new ArrayList<>();
        // 调用getClass()方法来比较l1和l2的类是否相等
        System.out.println(l1.getClass() == l2.getClass());

运行上面的代码片段,可能有的人会认为应该输出false,但是实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

不管为泛型的类型形参传入哪一种类型实参,对于java类型来说,他们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法,静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。下面代码演示了这种错误:

public class R<T>
{
    // 下面代码错误,不能在静态Field声明中使用类型形参
//  static T info;
    T age;
    public void foo(T msg){}
    // 下面代码错误,不能在静态方法声明中使用类型形参
//  public static void bar(T msg){}
}

由于系统中并不会真正生成泛型,所以instanceof运算符后不能使用泛型类。例如,下面代码是错误的:

Collection cs = new ArrayList<String>();
//下面代码编译时引起错误,Instanceof运算符后不能使用泛型
if(cs instanceof List<String>){{...}

附:源代码示例

github地址:点击查看
码云地址:点击查看

打赏
欢迎关注人生设计师的微信公众账号
公众号ID:longjiazuoA

未经允许不得转载:人生设计师 » 泛型系列(二):深入泛型

分享到:更多 ()

人生设计师-接受不同的声音

联系我关于我