泛型(2) - 泛型方法 , 泛型通配符

泛型方法

  • 当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}

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

public class Test<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}

// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}
  1. 泛型方法中可以同时声明多个类型参数。
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
  1. 泛型方法中也可以使用泛型类中定义的泛型参数。
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}

// 上面可见,我们可以使用泛型类中定义的泛型参数U作为返回值以及形参,也可以使用该方法外部定义的泛型
  1. 泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}

public <T> T testMethod1(T t) {
return t;
}
}

上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。

注意事项:

1. < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。
2. 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
3. 与泛型类的类型参数定义一样,此处泛型方法中的 T 可以写为`任意标识`,常见的如 T、E、K、V 等形式的参数常用于表示泛型。

补充一点:将静态方法声明为泛型方法

  • 前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。
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);
}
}

泛型方法中的类型推断

  • 在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
  1. 当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
  2. 在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
public class Test {

// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}

public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是 Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);

//(2)传入的两个实参一个是 Integer,另一个是 Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);

// 传入的两个实参一个是 Integer,另一个是 String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");

// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为 Integer 对象
int a = Test.<Integer>add(1, 2);

//(2)指定了<T> = <Integer>,所以不能传入 Float 对象
int b = Test.<Integer>add(1, 2.2);// 编译错误

//(3)指定<T> = <Number>,所以可以传入 Number 对象
// Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}

泛型通配符

  • 在现实编码中,确实有这样的需求,希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此 Java 引入了泛型通配符这个概念。

泛型通配符有 3 种形式:

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

1. 上界通配符 <? 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>();// 编译正确
}
}
// 因为 Number是Integer的父类,这里的泛型可以使用Number的子类包括Number,即以Number为上界限的子类可以使用

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 中添加 IntegerFloat 等对象,这也说明了 list 集合并不是某个确定了数据类型的集合。

完整的用法展示

  • 下面展示一段代码,设置一个方法(泛型方法),可以接受Number类的子类集合,接收后将该集合中的元素转化为Int型数据循环输出
package chapter06;

import java.util.ArrayList;

public class Fanxing_01 {
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);
}

// 设置一个静态方法,输出集合中的数据(可以接收Number子类创建的动态数组集合)
public static void printIntVal(ArrayList<? extends Number> list) {
// 遍历传入的集合,并输出集合中的元素
for (Number number : list) {
System.out.print(number.intValue() + " "); // 强制转换为int数据输出
}
System.out.println();
}
}

错误用法:

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>也是一个数据类型实参,它和 NumberStringInteger 一样都是一种实际的数据类型。

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 Integer>表示只能用 Integer 的父类,例如 Number

与上界通配符一相反下界通配符同样能用于赋值操作,用于指定某一个集合在接收某类型参数的范围

public class GenericType {
public static void main(String[] args) {
ArrayList<? super Number> list = new ArrayList<>();

list.add(new Integer(1));// 编译正确
list.add(new Float(1.0));// 编译正确

// Object 是 Number 的父类
list.add(new Object());// 编译错误
}
}
  • 这里奇怪的地方出现了,为什么和ArrayList<? extends Number> 集合不同, ArrayList<? super Number> 集合中可以添加 Number 类及其子类的对象呢?
  • 其原因是, ArrayList<? super Number> 的下界是 ArrayList< Number > 。因此,我们可以确定 Number 类及其子类的对象自然可以加入 ArrayList<? super Number> 集合中; 而 Number 类的父类对象就不能加入 ArrayList<? super Number> 集合中了,因为不能确定 ArrayList<? super Number> 集合的数据类型。

完整例子展示:

package chapter06;

import java.util.ArrayList;

public class Fanxing_01 {
public static void main(String[] args) {
// 创建一个 ArrayList<? super Number> 集合
ArrayList<Number> list = new ArrayList();
// 往集合中添加 Number 类及其子类对象
list.add(new Integer(1)); // 推荐使用 -> list.add(1); 来添加Integer数据
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));
}
}

错误例子展示:

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 通配符表示可以写,不能读。

无限定通配符 <?>

  • 我们已经讨论了<? extends T><? super T>作为方法参数的作用。实际上,Java 的泛型还允许使用无限定通配符<?>,即只定义一个?符号
  • 无界通配符:? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。需要注意的是: 也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。

  • 注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList 是 ArrayList< Object > 逻辑上的父类。

ArrayList<?> 在逻辑上表示为所有数据类型的父类,它可以代表 ArrayList< Integer>ArrayList< Number >ArrayList< Object >中的某一个集合,但实质上它们之间没有继承关系。

public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<>(123, 456);
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 异常(原因可以结合上界和下界通配符理解)。
*/

大多数情况下,可以用类型参数 < T > 代替 <?> 通配符。

static <?> void isNull(ArrayList<?> list) {
...
}
// 替换如下:
static <T> void isNull(ArrayList<T> list) {
...
}