Java泛型方法与泛型通配符

203 阅读10分钟

1.泛型方法

当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。
基本语法如下:

public <类型参数> 返回类型 方法名(类型参数 变量名) {
    ...
}

1. 只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。

public class Test<U> {
        // 该方法只是使用了泛型类定义的类型参数,不是泛型方法
        public void testMethod(U u){
                System.out.println(u);
        }
        
        // <T> 真正声明了下面的方法是一个泛型方法
        public <T> T testMethod1(T t){
                return t;
        }
}

2. 泛型方法中可以同时声明多个类型参数。

public class TestMethod<U> {
        public <T, S> T testMethod(T t, S s) {
                return null;
        }
}

3. 泛型方法中也可以使用泛型类中定义的泛型参数。

public class TestMethod<U> {
        public <T> U testMethod(T t, U u) {
                return u;
        }
}

特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的

public class Test<T> {
        public void testMethod(T t) {
                System.out.println(t);
        }
        //泛型方法始终以自己声明的类型参数为准
        public <T> T testMethod1(T t) {
                return t;
        }
}

4. 将静态方法声明为泛型方法
在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。

public class Test2<T> {   
        // 泛型类定义的类型参数 T 不能在静态方法中使用
        // 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
    public static <E> E show(E one) {     
        return null;    
    }    
}  

泛型方法的使用

  • 泛型类,在创建类的对象的时候确定类型参数的具体类型;
    泛型方法,在调用方法的时候再确定类型参数的具体类型。
  • 泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
    当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型。
public class Demo {  
  public static void main(String args[]) {  
    GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象  
    
    String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串  
    int i = d.fun(30);  // 给GenericMethod中的泛型方法传递数字,自动装箱  
    System.out.println(str); // 输出 汤姆
    System.out.println(i);  // 输出 30

        GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
  }  
}

class GenericMethod {
        // 普通的泛型方法
        public <T> T fun(T t) { // 可以接收任意类型的数据  
            return t;
          } 

        // 静态的泛型方法
        public static <E> void show(E one){     
         System.out.println("静态泛型方法 " + one);
    }
}  

2.类型擦除

泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。
举个例子:

public class GenericType {
    public static void main(String[] args) {  
        ArrayList<String> arrayString = new ArrayList<String>();   
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();   
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
    }  
}
  • 在这个例子中,我们定义了两个ArrayList集合,不过一个是ArrayList< String>,只能存储字符串。一个是ArrayList< Integer>,只能存储整型对象。我们通过arrayString对象和arrayInteger对象的getClass()方法获取它们的类信息并比较,发现结果为 true。
  • 这是因为,在编译期间,所有的泛型信息都会被擦除,ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object >类型。

总结

  • 泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析)

  • 在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)

3.泛型通配符

概念

在现实编码中我们有时希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符这个概念。
泛型通配符有3种形式:

<?> :被称作无限定的通配符。
<? extends T> :被称作有上界的通配符。
<? super T> :被称作有下界的通配符。

在引入泛型通配符之后,我们便得到了一个在逻辑上可以表示为某一类型参数范围的父类引用类型。接下来将分别介绍 3 种形式的泛型通配符。

上界通配符 <?extends T>

上界通配符<? extends T>:T 代表了类型参数的上界,<? extends T>表示类型参数的范围是T和T的子类。需要注意的是:<? extends T>也是一个数据类型实参,它和Number、String、Integer 一样都是一种实际的数据类型。
ArrayList< Integer >和ArrayList< Number >之间不存在继承关系。引入上界通配符的概念后,我们便可以在逻辑上将ArrayList<?extends Number>看做是ArrayList< Integer >的父类,但实质上它们之间没有继承关系。例如:

public class GenericType {
    public static void main(String[] args) {  
                ArrayList<Number> list01 = new ArrayList<Integer>();// 编译错误

                ArrayList<? extends Number> list02 = new ArrayList<Integer>();// 编译正确
    }  
}

ArrayList<? extends Number>可以代表 ArrayList< Integer >、ArrayList< Float >、… 、ArrayList< Number >中的某一个集合,但我们不能指定 ArrayList<?extends Number>的数据类型。

public class GenericType {
    public static void main(String[] args) {  
                ArrayList<? extends Number> list = new ArrayList<>();
                
                list.add(new Integer(1));// 编译错误
                list.add(new Float(1.0));// 编译错误
    }  
}
  • 可以这样理解,ArrayList<?extends Number>集合表示了:我这个集合可能是ArrayList< Integer >集合,也可能是 ArrayList< Float > 集合,… ,还可能是ArrayList< Number >集合;但到底是哪一个集合,不能确定;程序员也不能指定。
  • 所以,在上面代码中,创建了一个ArrayList<?extends Number> 集合 list,但我们并不能往list中添加Integer、Float 等对象,这也说明了list集合并不是某个确定了数据类型的集合。

上界通配符 <?extends T> 的用法

public class Test {
    public static void main(String[] args) {
            // 创建一个 ArrayList<Integer> 集合
        ArrayList<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        // 将 ArrayList<Integer> 传入 printIntVal() 方法
        printIntVal(integerList);
                
                // 创建一个 ArrayList<Float> 集合
        ArrayList<Float> floatList = new ArrayList<>();
        floatList.add((float) 1.0);
        floatList.add((float) 2.0);
        // 将 ArrayList<Float> 传入 printIntVal() 方法
        printIntVal(floatList);
    }
    
    public static void printIntVal(ArrayList<? extends Number> list) {
                 // 遍历传入的集合,并输出集合中的元素       
        for (Number number : list) {
            System.out.print(number.intValue() + " ");
        }
        System.out.println();
    }
}

//    输出结果:
//        1 2
//        1 2

需要注意的是:在printIntVal()方法内部,必须要将传入集合中的元素赋值给Number对象,而不能赋值给某个子类对象;是因为根据 ArrayList<?extends Number>的特性,并不能确定传入集合的数据类型(即不能确定传入的是 ArrayList< Integer > 还是 ArrayList< Float >)
现给出错误用法:

public class Test {
        public static void main(String[] args) {
                ArrayList<? extends Number> list = new ArrayList();
                list.add(null);// 编译正确
                list.add(new Integer(1));// 编译错误
                list.add(new Float(1.0));// 编译错误
        }
        
        public static void fillNumList(ArrayList<? extends Number> list) {
                list.add(new Integer(0));//编译错误
                list.add(new Float(1.0));//编译错误
                list.set(0, new Integer(2));// 编译错误
                list.set(0, null);// 编译成功,但不建议这样使用
        }
}

在 ArrayList<?extends Number>集合中,不能添加任何数据类型的对象,只能添加空值null,因为null可以表示任何数据类型

一句话总结:使用 extends 通配符表示可以读,不能写

下界通配符 <?super T>

下界通配符 <?super T>:T 代表了类型参数的下界,<?super T>表示类型参数的范围是T和T的超类,直至Object。需要注意的是:<?super T>也是一个数据类型实参,它和Number、String、Integer一样都是一种实际的数据类型。
ArrayList<? super Integer>在逻辑上表示为Integer类以及Integer类的所有父类,它可以代表ArrayList< Integer>、ArrayList< Number >、ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。

public class GenericType {
    public static void main(String[] args) {  
                ArrayList<Integer> list01 = new ArrayList<Number>();// 编译错误

                ArrayList<? super Integer> list02 = new ArrayList<Number>();// 编译正确
    }  
}

下界通配符<? super T> 的用法

public class Test {
        public static void main(String[] args) {
                // 创建一个 ArrayList<? super Number> 集合
                ArrayList<Number> list = new ArrayList(); 
                // 往集合中添加 Number 类及其子类对象
                list.add(new Integer(1));
                list.add(new Float(1.1));
                // 调用 fillNumList() 方法,传入 ArrayList<Number> 集合
                fillNumList(list);
                System.out.println(list);
        }
        
        public static void fillNumList(ArrayList<? super Number> list) {
                list.add(new Integer(0));
                list.add(new Float(1.0));
        }
}
//输出结果:[1,1.1,0,1.0]
  • 与带有上界通配符的集合 ArrayList<?extends T>的用法不同,带有下界通配符的集合 ArrayList<?super Number> 中可以添加 Number 类及其子类的对象;ArrayList<? super Number>的下界就是 ArrayList

    集合,因此,其中必然可以添加 Number 类及其子类的对象;但不能添加 Number 类的父类对象(不包括 Number 类)。
    现给出错误用法:

public class Test {
    public static void main(String[] args) {
            // 创建一个 ArrayList<Integer> 集合
        ArrayList<Integer> list = new ArrayList<>();
        list.add(new Integer(1));
        // 调用 fillNumList() 方法,传入 ArrayList<Integer> 集合
        fillNumList(list);// 编译错误
    }

    public static void fillNumList(ArrayList<? super Number> list) {
        list.add(new Integer(0));// 编译正确
        list.add(new Float(1.0));// 编译正确
                
                // 遍历传入集合中的元素,并赋值给 Number 对象;会编译错误
        for (Number number : list) {
            System.out.print(number.intValue() + " ");
            System.out.println();
        }
        // 遍历传入集合中的元素,并赋值给 Object 对象;可以正确编译
        // 但只能调用 Object 类的方法,不建议这样使用
        for (Object obj : list) {
            System.out.println(obj);使用
        }
    }
}
  • 注意,ArrayList<?super Number>代表了 ArrayList< Number >、 ArrayList< Object >中的某一个集合,而ArrayList< Integer >并不属于ArrayList<?super Number>限定的范围,因此,不能往fillNumList()方法中传入ArrayList< Integer >集合。
  • 并且,不能将传入集合的元素赋值给Number对象,因为传入的可能是ArrayList< Object >集合,向下转型可能会产生 ClassCastException异常。
  • 不过,可以将传入集合的元素赋值给Object对象,因为Object是所有类的父类,不会产生ClassCastException 异常,但这样的话便只能调用Object类的方法了,不建议这样使用。

一句话总结:使用 super 通配符表示可以写,不能读

无限定通配符 <?>

  • 无界通配符<?>:? 代表了任何一种数据类型,能代表任何一种数据类型的只有null。需要注意的是:<?>也是一个数据类型实参,它和 Number、String、Integer一样都是一种实际的数据类型。
  • 注意:Object本身也算是一种数据类型,但却不能代表任何一种数据类型,所以ArrayList< Object >和ArrayList<?>的含义是不同的,前者类型是Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是ArrayList< Object >逻辑上的父类。
  • ArrayList<?> 在逻辑上表示为所有数据类型的父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系
public static void main(String[] args) {
    ArrayList<Integer> list01 = new ArrayList<>();
    list01.add(1);
    ArrayList<?> list02 = list01; // 安全地向上转型
}

上述代码是可以正常编译运行的,因为ArrayList<?> 在逻辑上是ArrayList< Integer >的父类,可以安全地向上转型

  • ArrayList<?>既没有上界也没有下界,因此,它可以代表所有数据类型的某一个集合,但我们不能指定ArrayList<?>的数据类型
public class GenericType {
        public static void main(String[] args) {
        ArrayList<?> list = new ArrayList<>();
        list.add(null);// 编译正确
        Object obj = list.get(0);// 编译正确

                list.add(new Integer(1));// 编译错误
                Integer num = list.get(0);// 编译错误
    }
}

ArrayList<?> 集合的数据类型是不确定的,因此我们只能往集合中添加null;而我们从ArrayList<?>集合中取出的元素,也只能赋值给Object对象,不然会产生ClassCastException异常