接上文
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信息

java泛型对象怎么获取属性的值 java获取泛型类型class_java

简单来讲, 编译器并不认为Box<Integer>是 Box<Number>的子类。 如果某个方法的参数类型是 Box<Number>, 则不能传入Box<Integer>, 即使Integer是Number的子类, 这里多态无效。

既然这样, 我们尝试重载printBox方法, 写多1个对于Box<Integer>的方法

但是编译又出错了

java泛型对象怎么获取属性的值 java获取泛型类型class_java泛型对象怎么获取属性的值_02

这次编译器认为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信息

java泛型对象怎么获取属性的值 java获取泛型类型class_泛型_03


意思就是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>

java泛型对象怎么获取属性的值 java获取泛型类型class_List_04

/**
     * 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)
就编译出错了。

其实也很容易理解, 父类不能用子类的比较规则来排序, 因为子类可能会用其新属性来排序。

但反过来是可以的, 子类可以用父类的比较规则来排序, 父类的属性子类都会继承了!