接上文
Java 泛型进阶
1. 一个例子
我们自己写1个Box类, 可以往里面添加一个元素, 而我们不限定元素的类型, 用泛型符号T代替具体类型:
class Box<T>{
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
而在测试类中, 我们写1个方法printBox用于打印某个Box 对象的元素
@Slf4j
class TestBox{
public void printBox(Box<Number> box){
Number e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
}
注意点事, 我们printBox方法的参数里, 使用Box<Number> 来限制, 传参的类型。
我们再写1个测试方法去调用这个printBox类
public void test(){
Box<Number> box1 = new Box<>();
box1.setItem(3);
printBox(box1);
}
可以见到这个车程序是能正常运行的
[main] INFO com.home.javacommon.study.generics.simplewildcard.TestBox - item is: 3
毕竟传入的参数box1 是 属于Box<Number>的对象
问题来了
如果我写多1个方法, 尝试利用多态, 尝试给方法printBox传如一个Box<Integer>对象, 毕竟Integer 是 Number的子类。
public void test2(){
Box<Integer> box2 = new Box<>();
box2.setItem(5);
printBox(box2);
}
但是其实这么写连编译都不通过, 我们会收到下面的error信息
简单来讲, 编译器并不认为Box<Integer>是 Box<Number>的子类。 如果某个方法的参数类型是 Box<Number>, 则不能传入Box<Integer>, 即使Integer是Number的子类, 这里多态无效。
既然这样, 我们尝试重载printBox方法, 写多1个对于Box<Integer>的方法
但是编译又出错了
这次编译器认为Box<Integer> 与 Box<Number>是同 一种类。不允许这样重载。
这时我们还有两个方案, 让printBox 能接受Box<Integer>对象的参数。
方案1:
在printBox的参数中, Box不使用泛型
public void printBox(Box box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
里面的元素当Object处理, 这时的确程序能被编译和执行。
但是,在编辑器中我们仍然会收到下面的warning信息
意思就是Box是1个泛型类, 在形参中应该指定泛型的具体类型。
方案2:
使用泛型通配符
public void printBox(Box<?> box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
这次我们用?来代替具体的类型。
而这次printBox终于可以支持Box<Integer> 与 Box<Number>的对象参数了。
从这个例子中, 类型通配符?解决了1个实际的问题。
2. 泛型类型通配符?的一些总结
1. 在方法的形参中, 多态并不适用于泛型参数
例如对于类A , A1, B这3个类,
有1个方法f(B<A> b)定义在在其他类。
则f(B<A> b) 并不能传入参数B<A1>的对象, 即使A1是A的子类。
2. ?泛型类型通配符就是为了解决上面的问题, 令f(B<?>)同时支持多个B<A>, B<A1>多种类型的参数对象。
3. ?泛型类型通配符, 只适用于方法的参数中, 并不是适用于泛型类和泛型接口的定义。
也就是讲, 下面的写法是不能通过编译的
class Box2<?>{
private ? item;
...
4. ?泛型类型通配符, 也不能用于直接取代类型的参数, 而是取代类型某个参数的泛型类型。
下面的写法也是错误的:
public void printBox2(? box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
所以?多数情况下用于集合, 例如List<?>, Set<?> 等, 上面的Box类型其实也是1个集合。
5. ?泛型通配符用于取代方法题的实参, 并不适用于形参
也就是讲? 这个字符不应该出现在方法体中。
形参定义:方法定义的参数和方法体内的参数就是形参
实参定义:具体调用的参数
例如:
void f(int a){
a = a + 1;
}
void g(){
a = 3;
f(a);
System.out.println(String.valueOf(a));
}
这个例子中, f()内的a是形参, g方法的a是f()的实参。
6. 使用?泛型通配符的普通方法和 泛型方法的区别。
其实我觉得它们两者都是为了实现同一目的,就是令一个方法可以同事支持若干不同的类型的参数。
其实上面的例子中, 还有第三种方案, 就是用泛型方法:
public <T extends Box<?>> void printBox(T box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
泛型方法会更加灵活, 不过其实上面也用到泛型通配符?, 当然不用也会通过编译, 但是同样会有第一种方法的warning:
意思就是Box是1个泛型类, 在形参中应该指定泛型的具体类型。
3. 泛型类型通配符?的上界限
我们look back上面例子的第2个方案:
public void printBox(Box<?> box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
其实Box<?> 的?是可以被任何类型取代的。 但如果我们想限定?只能被Number or其自子类 被传入printBox参数。 则只需要把? 改成 ?extends Number就ok了
public void printBox(Box<? extends Number> box){
Object e = box.getItem();
log.info("item is: {}", String.valueOf(e));
}
3.1 指定泛型通配符上界限的语法
类/接口<? extends 实参类型>
注意了, 上面提过, 只适用于方法参数体中。
3.2 另1个例子
为了加深印象, 我们用另1个例子来体会类型通配符的上界限
class Automobile{
public int getId(){
return 0;
};
}
@AllArgsConstructor
@Data
class Truck extends Automobile{
private int id;
public String toString(){
return MessageFormat.format("{0}({1}:{2})", this.getClass().getSimpleName(),"id",id);
}
}
class PickupTruck extends Truck{
public PickupTruck(int i) {
super(i);
}
}
上面定义类了1个类,
汽车类
卡车类 继承 汽车类
皮卡类 继承 卡车类
重写了卡车类的toString方法, 令卡车和皮卡的对象可以把id show出来。
这时我们写1个方法,可以把1个基于卡车数组里的所有卡车print出来
就可以利用?的上界限了
class Test{
public void printTruck(List<? extends Truck> list){
for (Truck t: list){
System.out.println(t);
}
}
}
我们再写一些测试代码:
public void test1(){
List<Automobile> automobiles = new ArrayList<>();
automobiles.addAll(Arrays.asList(new Truck(1), new Truck(2)));
List<Truck> trucks = new ArrayList<>();
trucks.addAll(Arrays.asList(new Truck(3), new Truck(4)));
List<PickupTruck> pickupTrucks = new ArrayList<>();
pickupTrucks.addAll(Arrays.asList(new PickupTruck(1), new PickupTruck(2)));
//printTruck(automobiles); // not allow due to ? extends Truck
printTruck(trucks);
printTruck(pickupTrucks);
}
输出:
list : Truck - Truck(id:3)
list : Truck - Truck(id:4)
list : PickupTruck - PickupTruck(id:1)
list : PickupTruck - PickupTruck(id:2)
可以见到printTuck方法可以接受List<Truck> 和 List<==PickupTruck ==> 的对象, 但是不能接受List<Automobile> 对象, 实现了本例子的需求
可能有人会觉得多勾余, 直接在printTruck()的参数体用List<Truck>不就行了吗, 为何要有用List<? extends Truck>呢。
还是那句, 编译不过, 因为 一旦参数类型被定义List<Truck> , 则不能传入List<PickupTruck> 的对象!
到这里, 我们的例子都是关于集合的(Box/List)
有没有普通类的泛型通配符例子? 下面就是
3.3 再1个例子
class Automobile{
public int getId(){
return 0;
};
}
@Data
@AllArgsConstructor
class Goods{
private int goodsId;
public String toString(){
return MessageFormat.format("{0}({1}:{2})", this.getClass().getSimpleName(),"goodsId",goodsId);
}
}
class Glass extends Goods{
public Glass(int goodsId) {
super(goodsId);
}
}
@AllArgsConstructor
@Data
class Truck<T> extends Automobile{
private int id;
private T goods;
public String toString(){
return MessageFormat.format("{0}({1}:{2})", this.getClass().getSimpleName(),"id",id);
}
}
class Test{
public void test1(){
Truck<Glass> t = new Truck<>(1, new Glass(2));
System.out.println(MessageFormat.format("{0}''s goods is {1}", t, t.getGoods()));
}
public void printGoods(Truck<? extends Goods> truck){
System.out.print(truck);
}
public void printTruck(List<? extends Automobile> list){
for (Automobile a: list){
System.out.println("list : ".concat(a.getClass().getSimpleName()).concat(" - ").concat(a.toString()));
}
}
}
上面我们令到卡车可以带1个货物,定义了Goods 类和Glass类, 其中Glass继承Goods类。
我们写了1个方法printGoods 可以把卡车的货物打印出来,同时support Glass 和 Goods, 这时就必须用到泛型通配符Truck<? extends Goods>了。只用Truck<Goods>是不行的
不过其实这时,Truck其实也变成1个容器和集合了。。
所以为什么讲类型通配符基本上都是应用于集合!
3.4 List.addAll()方法例子
在3.2 的例子中我们用到list.add()方法来加入元素
List<Automobile> automobiles = new ArrayList<>();
automobiles.addAll(Arrays.asList(new Truck(1), new Truck(2)));
List<Truck> trucks = new ArrayList<>();
trucks.addAll(Arrays.asList(new Truck(3), new Truck(4)));
List<PickupTruck> pickupTrucks = new ArrayList<>();
pickupTrucks.addAll(Arrays.asList(new PickupTruck(1), new PickupTruck(2)));
可以看出, addAll方法support多种List泛型,它肯定是用了泛型通配符, 让我们看看他的源代码:
/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
果然它用了Collection<? extends E> , 泛型通配符, 因为ArrayList本身是泛型类,
所以对于ArrayList<E>, 它的addAll()方法的参数只能是List<? extends E>了
这就是为什么需要泛型通配符和它的上界限!
4. 使用了泛型通配符上界限的集合不能二次添加元素,下界限可以
我们继续参考3.2 的例子:
在printTuck方法中, 加入我们在方法体再添加1个Truck对象
public void printTruck2(List<? extends Truck> list){
list.add(new Truck(23)); // not allow
for (Truck t: list){
System.out.println("list : ".concat(t.getClass().getSimpleName()).concat(" - ").concat(t.toString()));
}
}
这时是不能通过编译的, 会有错误
The method add(capture#4-of ? extends Truck) in the type List<capture#4-of ? extends Truck> is not applicable for the arguments (Truck)
为什么呢, 因为List<? extends Truck> list 是不知道你会传什么鬼进来的, 如果传的是List<PickupTruck>的对象进来, 你再往里添加其父类Truck的对象就会出错。
所以编译器直接禁止使用泛型通配符上界限的集合形参,在方法体里添加元素。
但是基对于用下界限的形参, 是可以添加元素的
应为传入的所有泛型都是 Truck的祖先。
public void printTruck2(List<? supper Truck> list){
list.add(new Truck(23));、、 allow
for (Truck t: list){
System.out.println("list : ".concat(t.getClass().getSimpleName()).concat(" - ").concat(t.toString()));
}
}
5. 泛型类型通配符?的下界限
语法:
类/接口<? super 实参类型>
对于下界限只不过是把extends关键字改成super。
但问题来了
对于一个方法f(A a), 在方法体内我们会调用对象a里的成员or成员方法, 对于A类的所有子孙, 其实都会继承A的成员和成语方法的。
所以我们为什么要限制f() 方法不能适用于某1代子孙呢。
只有一种可能, 就是f()参数体内某一代子孙开始, 其新增的成员会影响f()逻辑执行。
我们用TreeSet作1个例子。
Tree set 中有1个构造函数TreeSet<Comparator<? super E> comparator>
/**
* Constructs a new, empty tree set, sorted according to the specified
* comparator. All elements inserted into the set must be <i>mutually
* comparable</i> by the specified comparator: {@code comparator.compare(e1,
* e2)} must not throw a {@code ClassCastException} for any elements
* {@code e1} and {@code e2} in the set. If the user attempts to add
* an element to the set that violates this constraint, the
* {@code add} call will throw a {@code ClassCastException}.
*
* @param comparator the comparator that will be used to order this set.
* If {@code null}, the {@linkplain Comparable natural
* ordering} of the elements will be used.
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
可以在创建TreeSet 对象时同时指定TreeSet 的排序规则。
我们再重写下3.1 例子汽车的3个类:
@AllArgsConstructor
@Data
class Automobile{
private int id;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Automobile other = (Automobile) obj;
if (id != other.id)
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
}
@Getter
class Truck extends Automobile{
private int weight;
public Truck(int id, int weight) {
super(id);
this.weight = weight;
}
@Override
public String toString() {
return "Truck [id=" + this.getId() + ", weight=" + weight + "]";
}
}
@Getter
class PickupTruck extends Truck{
private int speed;
public PickupTruck(int id, int weight, int speed) {
super(id, weight);
this.speed = speed;
}
}
令到汽车类有id属性, 卡车类新增重量属性, 皮卡类新增速度属性
也就是讲汽车可以比较id, 卡车可以比较id 和重量, 皮卡还可以比较速度。
我们尝试创建基于卡车的TreeSet<Truck>对象, 用Comparator参数那个构造方法。
在此之前,我们先写3个Comparator 对象
Comparator<Automobile> aComparator = (o1, o2) -> o1.getId() - o2.getId();
Comparator<Truck> truckComparator = (o1, o2) -> o1.getWeight() - o2.getWeight();
Comparator<PickupTruck> pComparator = (o1, o2) -> o1.getSpeed() - o2.getSpeed();
可以见到, 我们为3个类指定了不同的排序规则
然后我们再创建TreeSet<Truck>对象
public void test1(){
TreeSet<Truck> ts = new TreeSet<>(aComparator);
TreeSet<Truck> ts2 = new TreeSet<>(truckComparator);
//TreeSet<Truck> ts3 = new TreeSet<>(pComparator); // allow due to public TreeSet(Comparator<? super E> comparator)
ts.addAll(Arrays.asList(new Truck(3, 99),new Truck(2, 200),new Truck(1, 19)));
ts2.addAll(Arrays.asList(new Truck(3, 99),new Truck(2, 200),new Truck(1, 19)));
System.out.println(ts);
System.out.println(ts2);
}
第2行
TreeSet<Truck> ts = new TreeSet<>(aComparator); 是正确的, 因为
public TreeSet(Comparator<? super E> comparator)
限制的是Truck的下界限, 我们可以用Automobile的比较规则
第3行也是ok的
TreeSet<Truck> ts2 = new TreeSet<>(truckComparator);
用回本类的比较规则
但是第4行
//TreeSet<Truck> ts3 = new TreeSet<>(pComparator); // allow due to public TreeSet(Comparator<? super E> comparator)
就编译出错了。
其实也很容易理解, 父类不能用子类的比较规则来排序, 因为子类可能会用其新属性来排序。
但反过来是可以的, 子类可以用父类的比较规则来排序, 父类的属性子类都会继承了!