java泛型之使用通配符参数

类型安全虽然有用,但是有时可能会影响代码结构,使得无法被完全接受。例如,对于上一节的Stats类,假设希望添加方法sameAvg(),该方法用于判定两个Stats对象包含的数组的平均值是否相同,而不考虑每个对象包含的数值数据的具体类型。例如,如果一个对象包含double值1.0、2.0、和3.0,另一个对象包含整数值2、1和3,那么平均值是相同的。实现sameAvg()方法的一种方式是传递Stats参数,然后根据调用对象比较参数的平均值,只有当平均值相同时才返回true。例如:

......
Integer inums[] = {1,2,3,4,5};
Double dnums[] = {1.1,2.2,3.3,4.4,5.5};
Stats<Integer> iob = new Stats<Integer>(inums);
Stats<Double> dob = new Stats<Double>(dnums);
if(iob.sameAvg(dob))
    System.out.println("Averages are the same.");
else
    System.out.println("Averages differ.");

首先,创建sameAvg()方法看起来是一个简单的问题。因为Stats是泛型化的,并且它的average()方法可以使用任意类型的Stats对象,看起来创建sameAvg()方法将会很直观。不幸的是,一旦试图声明Stats类型的参数,麻烦就开始了。Stats是参数化类型,当声明这种类型的参数时,将Stats的类型参数指定为什么好呢?

乍一看,你可能会认为解决方案与下面类似,其中的T用作类型参数:

......
public boolean sameAvg(Stats<T> ob) {
    if(average() == ob.average())
        return true;
    else false;
}

这种尝试存在的问题是:只有当其他Stats对象的类型和调用对象相同时才能工作。例如,如果调用对象是Stats<Integer>类型,那么参数ob也必须是Stats<Integer>类型。不能用于比较Stats<Double>类型对象的平均值和Stats<Short>类型对象的平均值。所以,这种方式的适用范围很窄,无法得到通用的(即泛型化的)解决方案。

为了创建泛型化的sameAvg()方法,必须使用Java泛型的另一个特性:通配符(wildcard)参数。通配符参数是由“?”指定的,表示未知类型。下面是使用通配符编写的sameAvg()方法:

......
public boolean sameAvg(Stats<?> ob) {
    if(average() == ob.average())
        return true;
    return false;
}
......

在此,Stats<?>和所有Stats对象匹配,允许任意两个Stats对象比较它们的平均值。我们来看看完整的示例:

package test;

public class Stats<T extends Number> {

    private T[] nums;
    
    public Stats(T[] o) {
        nums = o;
    }
    public double average() {
        double sum = 0d;
        for(int i = 0; i < nums.length; i++)
            sum += nums[i].doubleValue();    
        return sum /nums.length;
    }
    public boolean sameAvg(Stats<?> ob) {
        if(average() == ob.average()) 
            return true;
        return false;
    }
}
package test;

public class WildcardDemo {
    public static void main(String[] args) {
        Integer[] inums = {1,2,3,4,5};
        Stats<Integer> iob = new Stats<>(inums);
        double v = iob.average();
        System.out.println("iob average is " + v);
        
        Double[] dnums = {1.1,2.2,3.3,4.4,5.5};
        Stats<Double> dob = new Stats<>(dnums);
        double w = dob.average();
        System.out.println("dob average is " + w);
        
        Float[] fnums = {1.0F,2.0F,3.0F,4.0F,5.0F};
        Stats<Float> fob = new Stats<>(fnums);
        double x = fob.average();
        System.out.println("fob average is " + x);
        
        System.out.println("Averages of iob and dob");
        if(iob.sameAvg(dob))
            System.out.println("are the same.");
        else
            System.out.println("differ.");
        
        System.out.println("Averages of iob and fob");
        if(iob.sameAvg(fob))
            System.out.println("are the same.");
        else 
            System.out.println("differ.");
    }
}

最后一点:通配符不会影响能够创建什么类型的Stats对象,理解这一点很重要。这是由Stats声明中的extends子句控制的。通配符只是简单地匹配所有有效的Stats对象。

有界通配符

可以使用与界定类型参数大体相同的方式界定通配符参数。对于创建用于操作类层次的泛型来说,有界通配符很重要。为了理解其中的原因,看下面这个例子:

package test;

public class TwoD {
    int x,y;
    TwoD(int a,int b) {
        x = a;
        y = b;
    }
}

class ThreeD extends TwoD {
    int z;
    ThreeD(int a,int b,int c) {
        super(a,b);
        z = c;
    }
}

class FourD extends ThreeD {
    int t;
    FourD(int a,int b,int c,int d) {
        super(a, b, c);
        t = d;
    }
}

在这个类层次的顶部是TwoD,该类封装了二维坐标(X,Y坐标)。ThreeD派生自TwoD,该类添加了第3维,创建XYZ坐标。FourD派生自ThreeD,该类添加了第4维(时间),生成4维坐标。

下面显示的是泛型类Coords,该类存储了一个坐标数组:

class Coords<T extends TwoD> {
    T[] coords;
    Coords(T[] o) {coords = o;}
}

注意Coords指定了一个由TwoD界定的类型参数。这意味着在Coords对象中存储的所有数据,将包含TwoD类或其子类的对象。

现在,假设希望编写一个方法,显示在Coords对象的cooreds数组中,每个元素的X和Y坐标。因为所有Coords对象的类型都至少有两个坐标XY,所以使用通配符很容易实现,如下例:

......
static void showXY(Coords<?>  c) {
    System.out.println("X Y Coordinates:");
    for(int i=0;i<c.coords.length;i++) {
        System.out.println(c.coords[i].x + " " + c.coords[i].y);
    }
}
......

因为Coords是有界的泛型类,并且将TwoD指定为上界,所以能够用于创建Coords对象的所有对象都将是TwoD类及其子类。因此,showXY()方法可以显示所有Coords对象的内容。

但是,如果希望创建显示ThreeD或FourD对象的X,Y和Z坐标的方法,该怎么办呢?麻烦是,并非所有Coords对象都有3个坐标,因为Coords<TwoD>对象只有X和Y坐标。所以,如何编写能够显示Coords<ThreeD>和Coords<FourD>对象的X、Y和Z坐标的方法,而又不会阻止该方法使用Coords<TwoD>对象呢?答案是使用有界的通配符参数。

有界的通配符为类型参数指定上界下界,从而可以限制方法能够操作的对象类型。最常用的有界通配符的上界,是使用extends子句创建的,具体方式和用于创建有界类型的方式大体相同。

如果对象实际拥有3个坐标的话,使用有界通配符,可以很容易创建出显示Coords对象中X、Y和Z坐标的方法。例如下面的showXYZ()方法,如果Coords对象中存储的元素的实际类型是ThreeD(或派生自ThreeD),那么showXYZ()方法将显示这些元素的X、Y和Z坐标:

......
static void showXYZ(Coords<? extends ThreeD> c) {
    System.out.println("X Y Z Coordinates:");
    for(int i=0; i<c.coords.length; i++) {
        System.out.println(c.coords[i].x + " " +
            c.coords[i].y + " " + 
            c.coords[i].z);
    }
}

注意,在参数c的声明中为通配符添加了extends子句。这表明“?”可以匹配任意类型,只要这些类型为ThreeD或其派生类即可。因此,extends子句建立了“?”能够匹配的上界。因为这个界定,可以使用对Coords<ThreeD>或Coords<FourD>类型对象的引用调用showXYZ()方法,但不能使用Coords<TwoD>类型的引用进行调用。如果试图使用Coords<TwoD>引用调用showXYZ()方法,就会导致编译时错误,从而确保了类型安全。

下面是演示使用有界通配符参数的整个程序:

package test;

class TwoD {
    int x,y;
    TwoD(int a,int b) {
        x = a;
        y = b;
    }
}

class ThreeD extends TwoD {
    int z;
    ThreeD(int a,int b,int c) {
        super(a,b);
        z = c;
    }
}

class FourD extends ThreeD {
    int t;
    FourD(int a,int b,int c,int d) {
        super(a, b, c);
        t = d;
    }
}
package test;

public class Coords<T extends TwoD> {

    T[] coords;
    Coords(T[] o) {coords = o;}
}
package test;

public class BoundedWildcard {
    static void showXY(Coords<?> c) {
        System.out.println("X Y Coordinates:");
        for(int i=0;i<c.coords.length;i++)
            System.out.println(c.coords[i].x + " " + c.coords[i].y);
        System.out.println();
    }
    static void showXYZ(Coords<? extends ThreeD> c) {
        System.out.println("X Y ZCoordinates:");
        for(int i=0;i<c.coords.length;i++)
            System.out.println(c.coords[i].x +" " + 
                    c.coords[i].y + " " + 
                    c.coords[i].z);
        System.out.println();
    }
    static void showAll(Coords<? extends FourD> c) {
        System.out.println("X Y Z T Coordinates: ");
        for(int i=0; i<c.coords.length;i++)
            System.out.println(c.coords[i].x + " " +
                    c.coords[i].y + " " + 
                    c.coords[i].z + " " +
                    c.coords[i].t);
        System.out.println();
    }
    public static void main(String[] args) {
        TwoD[] td = {
                new TwoD(0,0),
                new TwoD(7,9),
                new TwoD(18,4),
                new TwoD(-1,-23)
        };
        Coords<TwoD> tdlocs = new Coords<>(td);
        System.out.println("Contents of tdlocs.");
        showXY(tdlocs);
        FourD[] fd = {
                new FourD(1,2,3,4),
                new FourD(6,8,14,8),
                new FourD(22,9,4,9),
                new FourD(3,-2,-23,17)
        };
        Coords<FourD> fdlocs = new Coords<>(fd);
        System.out.println("Contents of fdlocs.");
        showXY(fdlocs);
        showXYZ(fdlocs);
        showAll(fdlocs);
    }
}

一般来说,要为通配符建立上界,可以使用如下所示的通配符表达式:

<? extends superclass>

其中,superclass是作为上界的类的名称。记住,这是一条包含子句,因为形成上界(由superclass指定的边界)的类也位于边界之内。

还可以通过为通配符添加一条super子句,为通配符指定下界。下面是一般形式:

<? super subclass>

对于这种情况,只有subclass的超类是可接受参数。这是一条排除子句,因此与subclass指定的类不相匹配。

java泛型之使用通配符参数

时间: 2024-09-28 01:58:47

java泛型之使用通配符参数的相关文章

JAVA 泛型中的通配符 T,E,K,V,?

前言 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型带来的好处 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的.对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这

【转】聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

原文:https://juejin.im/post/5d5789d26fb9a06ad0056bd9 前言 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型带来的好处 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类

java泛型中的通配符

概述 在学习java中泛型这块内容的时候,刚开始非常容易理解,但是,到通配符的时候,我就晕了,因为学习java这块内容的指导思想,是比对着.net来的,可是java中的通配符,.net中没有这样的概念,于是花时间学习了一下,下面通过例子向大家讲解通配符 实例代码 package com.tgb.mydemo; import java.util.ArrayList; import java.util.List; public class Test { public static void main

java 泛型声明与 通配符的使用

一:泛型类 把泛型定义在类上:作用域是整个类的内部 格式:public class 类名<泛型类型1,-> 注意:泛型类型必须是引用类型 import java.util.Arrays; public class GenericClassDemo { public static void main(String[] args) { MyArrayList<String> list = new MyArrayList<String>(); list.add("j

Java泛型中的通配符的使用

package com.srie.testjava; import java.util.ArrayList; import java.util.List; public class TestClassDefine2<T, S extends T> { public static void main(String[] args) { List<String> slist = new ArrayList<String>(); List<Integer> iLis

Java泛型 通配符? extends与super

本文来源:https://i.cnblogs.com/EditPosts.aspx?opt=1 感谢博主.本文仅供参考学习. Java 泛型 关键字说明 ? 通配符类型 <? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类 <? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object extends 示例 static class Food{} static class Frui

java泛型中的上、下界通配符

java泛型中,通配符?表示未知类型,等同于<? extends Object>,<? extends T>是上边界限定通配符,<? super T>是下边界限定通配符. 一.区别 在一个list中,上下界通配符能够存放和读取的对象类型如下图所示: 二.原则 上下界通配符的使用应当遵循PECS原则:Producer Extends,Consumer Super. 限定通配符总是包括自己 上界类型通配符:add方法受限 下界类型通配符:get方法受限 如果你想从一个数据类

Java 泛型解析,太难了,认真读才能理解

Java 泛型 关键字说明 ? 通配符类型 <? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类 <? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object extends 示例 static class Food{} static class Fruit extends Food{} static class Apple extends Fruit{} static clas

Java泛型中的协变和逆变

Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛型中引入通配符这个概念的时候,Java 其实是支持协变和逆变的. 看下面几行代码: // 不可变 List<Fruit>fruits =newArrayList<Apple>();// 编译不通过 // 协变 List<?extendsFruit>wildcardFruit