likes
comments
collection
share

谈谈泛型

作者站长头像
站长
· 阅读数 22

引言

本文深入理解Java泛型,内容主要参考于《On Java》和《Effective Java》。

PS: github.com/WeiXiao-Hyy…整理了后端开发的知识网络,欢迎Star!

协变,逆变,不变性

协变

协变性(Covariance):指的是能够使用子类型(子类或者更具体的类型)替换父类型(父类或者更抽象的类型)。

在Java中数组是协变的。如果Cat是Animal的子类型,那么Cat[]也是Animal[]的子类型。

逆变

逆变,也称为逆协变,从名字也可以看出来,它与协变的性质是相反的,指的是能够使用父类型替换子类型。

泛型的不变性

泛型是不变性的,也就是说List<Cat>并不是List<Animal>的子类型。

泛型的类型擦除

泛型代码内部并不存在有关泛型参数类型的可用信息。Java泛型是通过类型擦除实现的,意味着在使用泛型时,任何具体的类型信息都将被擦除。因此List<String>List<Integer>是在运行时是相同的类型,两者的类型都被“擦除”为它们的原始类型List

为什么泛型这样实现

类型擦除的核心初衷是,希望让泛化的调用方程序可以依赖于非泛化的库正常使用即迁移兼容性。

上界和下界

这里的上界和下界,本质上指的是,在定义泛型的时候,子类型的边界。即在运行时真正的类型。并且为了最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。

PECS法则

PECS代表producer-extends,consumer-super。同时需要记住所有的comparable和comparator都是消费者。

如果参数化类型表示一个生产者T,就使用<? extends T>; 如果表示一个消费者,就使用<? super T>

任意类型通配符

<?>代表任意类型通配符,对于任意类型X,List<X>是List<?>的子类型。但是List<?>不能add,get出来也是Object类型。

类型擦除的补偿

由于类型擦除的缘故,失去了在泛型代码中执行某些操作的能力。如何需要在运行时知道确切类型的操作都无法运行。

创建类型实例

传入工厂对象Class

@NoArgsConstructor
class Building {
}

@NoArgsConstructor
class House extends Building {
}

public class ClassTypeCapture<T> implements Supplier<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }

    @Override
    public T get() {
        try {
            return kind.getConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                 NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building())); // true
        System.out.println(ctt1.f(new House())); // true
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building())); // false
        System.out.println(ctt2.f(new House())); // true

        System.out.println(ctt2.get()); // House
    }
}

问题在于如果使用ClassTypeCapture<Integer>则会失败,因为Integer并没有无参构造器。建议使用显式工厂,并对类型进行限制,使其仅能接收实现了该工厂的类。

使用显式工厂Supplier

class IntegerFactory implements Supplier<Integer> {
    private int i = 0;

    @Override
    public Integer get() {
        return ++i;
    }
}

class Foo2<T> {
    private List<T> x = new ArrayList<>();

    Foo2(Supplier<T> factory) {
        Suppliers.fill(x, factory, 5);
    }

    @Override
    public String toString() {
        return x.toString();
    }
}

public class FactoryConstraint {
    public static void main(String[] args) {
        System.out.println(new Foo2<>(new IntegerFactory()));
    }
}

使用模版方法

在子类中被重写以生成该类型的对象。

class X {
}

class XCreator extends GenericWithCreate<X> {

    @Override
    X create() {
        return new X();
    }

    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

public abstract class GenericWithCreate<T> {
    final T element;

    GenericWithCreate() {
        element = create();
    }

    abstract T create();

    public static void main(String[] args) {
        XCreator xc = new XCreator();
        xc.f();
    }
}

泛型单例工厂

有时候可能需要创建一个不可变但又适用于许多不同类型的对象,由于泛型是通过擦除实现的,可以给所有必要的类型参数使用单个对象,但是需要编写一个静态工厂方法,让它重复地给每个必要的类型参数分发对象,即泛型单例工厂(往往泛型单例工厂不包含转换)

@SuppressWarnings("rawtypes")
public static final Set EMPTY_SET = new EmptySet<>();

// Collections.emptySet
@SuppressWarnings("unchecked")
public static final <T> Set<T> emptySet() {
    return (Set<T>) EMPTY_SET;
}

协变override

观察到ServiceImpl实现ServiceA接口,但是返回值却从Iterable变成了List(为Iterable子类型)满足条件。

interface ServiceA {
    Iterable<String> selectAll();
}

class ServiceAImpl implements ServiceA{
    @Override
    public List<String> selectAll() {
        return new ArrayList<>();
    }
}

自限定

目前用的较多的是Builder以及Comparable中的应用。

@Builder的问题

遇到继承情况时,POJO2无法调用POJO1的id方法,所以是@Builder无法解决的问题。

public class Anno {
    public static void main(String[] args) {
        POJO2 pojo2 = POJO2.builder()
                .note("this is pojo b")
                .build();
    }
}

@Data
@Builder
class POJO1 {
    String id;
}


@Data
@Builder
class POJO2 extends POJO1 {
    String note;
}

可以使用@SuperBuilder来解决问题(不建议在生产环境中使用),主要关注POJO1Builder<C extends POJO1, B extends POJO1Builder<C, B>>

@Data
class POJO1 {
    String id;

    public POJO1() {
    }

    // builder -> build() -> 获取示例
    protected POJO1(POJO1Builder<?, ?> b) {
        this.id = b.id;
    }

    // 获取自限定 builder
    public static POJO1Builder<?, ?> builder() {
        return new POJO1BuilderImpl();
    }

    // 请仔细理解此处泛型参数的含义
    public static abstract class POJO1Builder<C extends POJO1, B extends POJO1Builder<C, B>> {
        private String id;

        public B id(String id) {
            this.id = id;
            return self();
        }

        // 以下两个方法理解为builder生命周期下的回调函数/钩子
        // 获取自己,只因在继承体系下使用 self 获取的类型不完全
        protected abstract B self();

        public abstract C build();

        public String toString() {
            return "POJO1.POJO1Builder(id=" + this.id + ")";
        }
    }

    // 实现类:指定泛型参数+回调实现
    private static final class POJO1BuilderImpl extends POJO1Builder<POJO1, POJO1BuilderImpl> {
        private POJO1BuilderImpl() {
        }

        protected POJO1BuilderImpl self() {
            return this;
        }

        public POJO1 build() {
            return new POJO1(this);
        }
    }
}

几个注解

SafeVarargs

SafeVarargs注解表示我们承诺不会对变量参数列表做出任何修改。如果没有这个注解,编译器则会产生警告。

@SafeVarargs
public static <T> List<T> makeList(T... args) {
    List<T> result = new ArrayList<>();
    for (T item : args) {
        result.add(item);
    }
    return result;
}

SuppressWarnings("unchecked")

要尽可能地消除每一个非受检警告, 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有在这种情况下,可以用一个@SupperssWarning("unchecked")注解来禁止这条警告。

参考资料

转载自:https://juejin.cn/post/7389164547028009001
评论
请登录