1. 关系
在 Java
中,可以给一个对象赋值另一个兼容的对象,例如,我们可以把 Integer
赋值给 Object
,因为 Object
是 Integer
的超类。
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在面向对象语言中,这种关系称为 "is a",即 Integer is a Object
。 但是, Integer
同时也是 Number
的一个子类,所以以下的代码也是允许的。
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
同样的,在泛型中,如果类型是和 Number
兼容的,则可以调用 add
方法添加。
public class Box<T>{
public void add(T t) {
}
}
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
接下来思考一个问题
public void boxTest(Box<Number> n) { /* ... */ }
这个函数会接收一个什么类型的参数?
我们可以直接看到它需要的是 Box<Number>
,但是,当我们传入一个 Box<Integer>
或者 Box<Double>
时,这是否允许?
答案是否定的,原因是 Box<Integer>
和 Box<Double>
都不是 Box<Number>
的子类。
这是一个关于泛型通常会陷入的误区。
关系图从上面的关系图中可以看出,尽管 Integer
是 Number
的子类,但是 Box<Integer>
并不是 Box<Number>
的子类,它们是没有关系的。
2. 边界
有时我们可能需要限制参数的类型,比如在一个只操作数字的方法中,可能只需要 Number
类型或者其子类的参数,这就是边界参数的作用。
public class Box<T extends Number> {
private T t;
public void set(T t) {
this.t = t;
}
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.set(new Integer(10));
Box<String> box1 = new Box<String>(); //报错
}
}
在上面的例子中,通过 extends
关键词,约束了 Box
的上边界为 Number
。
因此,在实例化 Box
时,指定的类型必须是 number
的子类。Integer
是 Number
的子类,所以能通过编译,String
不是,会编译报错。
多边界约束
<T extends B1 & B2 & B3>
具有多个边界的类型变量是该边界中列出的所有类型的子类型。如果其中一个边界是类,则必须首先指定它。例如:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
下边这个会编译报错
class D <T extends B & A & C> { /* ... */ }
3. 通配符
在代码中,通配符使用 ?
字符,用来表示未知的类型。
通配符可以用于多种情况:作为参数、字段或局部变量的类型甚至有时作为返回类型。
上界通配符
声明一个上界通配符,只需要在 ?
后面跟上 extends
关键词,比如 <? extends A>
。
如果我们想定义一个函数,其接收一个 Number
的 list
或者是 Number
的子类比如 Integer
,Double
,Float
等的,我们看看下面的写法:
public class Box {
public double sumOfList1(List<Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
public double sumOfList2(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
}
这里定义了两个函数,分别是 sumOfList1(List<Number> list)
、 sumOfList2(List<? extends Number> list)
,不同之处是后者使用了通配符,那他们的使用有什么区别呢?
public static void main(String[] args) {
Box box = new Box();
List<Number> listNumber = Arrays.asList(1, 2, 3);
List<Integer> listInteger = Arrays.asList(1, 2, 3);
//调用使用了通配符的函数
double result = box.sumOfList2(listNumber);
double result1 = box.sumOfList2(listInteger);
//调用未使用通配符的函数
double result2 = box.sumOfList1(listNumber);
double result3 = box.sumOfList1(listInteger); //编译报错
}
上面调用的代码可以看出,使用了通配符的函数, list
包裹的类型可以是 Number
及其子类的,而未使用通配符的,只能使用 Number
类型的。上界通配符放松对变量的限制。
无限通配符
无限通配符是对通配符 (?
) 的一种特殊使用,用法如, List<?>
,这个 list
的类型是未知的。
无限通配符的两个引用场景:
- 编写的方法能直接使用
Object
提供的方法实现的。 - 当使用泛型类的方法不依赖于泛型参数时。比如,
List.size, List.clear
。当类Class<T>
的大多数方法都不依赖于T
时,更常使用CLass<?>
。
比如:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
这个函数的目的是打印 list
的所有的类型,但是现在它只能打印 Object
实例的列表,它是不能打印 List<Integer>
, List<String>
, List<Double>
等类型的列表的,因为它们都不是 List<Object>
的子类,无法传入。
要想实现此目的,我们可以使用无限通配符。
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
因为多余任一实现类型的 A
,List<A>
都是 List<?>
的子类。
下界通配符
上界通配符通过 <? extends A>
的形式,将未知类型限制其为 A
的子类。相似的,下界通配符是将未知类型限制为某个类型的父类。
声明一个下界通配符,只需要在 ?
后面跟上 super
关键词,比如 <? super A>
。
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
上面的函数中,List<? super Integer>
限制了只能传入 List<Integer>
, List<Number>
, 或者 List<Object>
,因为 Integer
的父类只有 Number
, Object
。
需要注意的,可以使用通配符指定上界,也可以指定下界,但不能两者同时指定。
参考:
相关文章:
网友评论