美文网首页
Java编程思想---泛型(2)

Java编程思想---泛型(2)

作者: Cool_Pomelo | 来源:发表于2020-01-09 10:41 被阅读0次

    Java编程思想---泛型(2)

    类型擦除

    先上例子:

    public class ErasedTypeEquivalence {
    
        public static void main(String[] args) {
            Class c1 = new ArrayList<String>().getClass();
    
            Class c2 = new ArrayList<Integer>().getClass();
            
            System.out.println(c1 == c2);
        }
    
    
    }
    
    
    

    ArrayList< String >与ArrayList< Integer >是不同的类型,但是上面的程序输出true,也就是上面的程序认为它们是一样的类型。

    再看接下来这个例子:

    public class Apple {
    }
    
    public class Orange {
    }
    
    public class Pear<P> {
    }
    
    
    public class Watermelon<POSITION,MOMENTUM> {
    
    }
    
    
    
    public class LostInformation {
    
    
        public static void main(String[] args) {
    
    
            List<Apple> apples = new ArrayList<>();
    
            Map<Apple,Orange> map = new HashMap<>();
    
            Pear<Apple> pear = new Pear<>();
    
            Watermelon<Long,String> watermelon = new Watermelon<>();
    
            System.out.println(Arrays.toString(
                    apples.getClass().getTypeParameters()
            ));
    
            System.out.println("______________________________________");
    
            System.out.println(Arrays.toString(
                    map.getClass().getTypeParameters()
            ));
    
            System.out.println("______________________________________");
    
            System.out.println(Arrays.toString(
                    pear.getClass().getTypeParameters()
            ));
    
            System.out.println("______________________________________");
    
            System.out.println(Arrays.toString(
                    watermelon.getClass().getTypeParameters()
            ));
    
        }
    }
    
    

    Class.getTypeParameters()将返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数。

    但是最终的输出结果确实一堆用作参数占位符的标识符,不是我们所想的具体的参数类型的信息。

    最终的事实就是,在Java中,在泛型代码内部,你无法获得任何有关泛型参数类型的信息

    Java泛型是用擦除来实现的,意味着你使用泛型时,任何具体的类型信息都背擦除了,你唯一知道的就是你在使用一个对象。

    因此List< String >和List< Integer >在运行时事实上是相同的类型,这两种形式都被擦除成它们的“原生”类型,即List。

    继续深入

    还是先上例子:

    public class HasF {
    
        public void f(){
            System.out.println("HasF.f");
        }
    }
    
    public class Manipulator<T> {
    
        private T obj;
    
        public Manipulator(T obj) {
            this.obj = obj;
        }
    
    //    ERROR
        public void manipulator(){
    //        obj.f();
        }
    }
    
    public class Manipulation {
    
        public static void main(String[] args) {
    
            HasF hasF = new HasF();
    
            Manipulator<HasF> manipulator =
                    new Manipulator<>(hasF);
    
            manipulator.manipulator();
    
    
    
        }
    }
    
    

    因为擦除,Java编译器无法将manipulator()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实。

    为了解决问题,我们必须给定泛型类的边界,告知编译器只能接受遵循这个边界的类型。

    
    public class Manipulator2<T extends HasF> {
    
        private T obj;
    
        public Manipulator2(T obj) {
            this.obj = obj;
        }
    
        public void  manipulator2(){
            obj.f();
        }
    }
    
    
    

    边界< T extends HasF >声明T必须具有类型HasF或者从HasF导出的类型。

    泛型类型参数将擦除到它的第一个边界(也许有有多个边界)。还有类型参数的擦除,编译器实际上会把类型参数替换为它的擦除,就像上面,T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。

    看第三个版本的Manipulator,我们可以自己去擦除:

    public class Manipulator3 {
    
        private HasF obj;
    
        public Manipulator3(HasF obj) {
            this.obj = obj;
        }
    
        public void manipulator3(){
            obj.f();
        }
    }
    

    擦除带来的问题

    擦除的代价很明显,泛型不能用于显式的引用运行时类型的操作之中,比如转型,instanceof操作,new表达式,因为所有关于参数的类型信息都丢失了。

    所以如果接下来你写了这段代码:

    
    public class Car {
    }
    
    public class Fon<T> {
    
        T var;
    }
    
    public class T1 {
    
        public static void main(String[] args) {
    
            Fon<Car> fon = new Fon<>();
    
    
    
        }
    }
    
    

    class Fon的代码应该工作与Car之上,泛型语法也在提醒我们,但事实并非如此,你应该意识到,这只是一个Object、

    边界

    用泛型表示没有任何意义的事物

    public class ArrayMake<T> {
    
        private Class<T> kind;
    
        public ArrayMake(Class<T> kind) {
            this.kind = kind;
        }
    
        @SuppressWarnings("unchecked")
        T[] create(int size){
            return (T[]) Array.newInstance(kind,size);
        }
    
        public static void main(String[] args) {
    
            ArrayMake<String> stringArrayMake =
                    new ArrayMake<>(String.class);
    
            String[] strings = stringArrayMake.create(10);
    
            System.out.println(Arrays.toString(
                    strings
            ));
    
        }
    
    }
    
    
    

    kind即使被存储了Class< T >,擦除也意味着它实际被存储未Class,无任何参数,所以当你用Array.newInstance创建的时候,其实并未拥有任何kind的类型信息。

    如果换成容器呢?

    public class ListMake<T> {
    
        List<T> create(){
            return new ArrayList<T>();
        }
    
        public static void main(String[] args) {
    
    
            ListMake<String> stringListMake = new ListMake<>();
    
            List<String> stringList = stringListMake.create();
    
            
    
        }
    }
    
    
    

    编译器没有给出警告。

    再看这个例子:

    public class FilledListMake<T> {
    
        List<T> create(T t,int n){
            List<T> res = new ArrayList<>();
    
            for (int i = 0; i < n ; i++) {
    
                res.add(t);
            }
    
            return res;
    
        }
    
        public static void main(String[] args) {
    
            FilledListMake<String> stringFilledListMake =
                    new FilledListMake<>();
    
            List<String> list = stringFilledListMake.create("World",20);
    
            System.out.println(list);
    
        }
    
    }
    
    

    即使擦除在方法或类内部移除了有关实际类型的信息,编译器依旧可以确保在方法或类中使用的类型的内部的一致性。

    补偿擦除

    因为擦除,任何在运行时需要知道的确切类型信息的操作都无法执行:

    public class Erase<T> {
    
        private final int siez = 100;
    
        public static void f(Object o){
    
    
    //        ERROR
    //        if (o instanceof T) {
    //
    //        }
    
    
    //        error
    //        T var = new T();
    
    //        error
    //        T[] array = new T[siez];
    
    
    
    
        }
    }
    
    
    

    如果引入类别标签,那么就可以使用动态的isInstance():

    public class ClassTypeCapture<T> {
    
        Class<T> kind;
    
        public ClassTypeCapture(Class<T> kind) {
            this.kind = kind;
        }
    
        public boolean f(Object arg){
            return kind.isInstance(arg);
        }
    
        public static void main(String[] args) {
    
            ClassTypeCapture<Fruit> classTypeCapture =
                    new ClassTypeCapture<>(Fruit.class);
    
    
            System.out.println(classTypeCapture.f(new Fruit()));
    
            System.out.println("____________________________________");
    
            System.out.println(classTypeCapture.f(new Apple()));
    
            System.out.println("____________________________________");
    
            ClassTypeCapture<Apple> classTypeCapture1 =
                    new ClassTypeCapture<>(Apple.class);
    
            System.out.println(classTypeCapture1.f(new Fruit()));
    
            System.out.println("____________________________________");
    
            System.out.println(classTypeCapture1.f(new Apple()));
    
    
        }
    }
    
    

    使用类型标签的话,你就可以使用newInstance()来创建这个类型的新对象。

    public class ClassFactory<T> {
    
        T x;
    
        public ClassFactory(Class<T> kind) {
            try {
                x = kind.newInstance();
            } catch (Exception e) {
                throw new RuntimeException();
            }
    
        }
    }
    
    public class Apple {
    }
    
    
    public class InstantiateGenericType {
    
        public static void main(String[] args) {
    
            ClassFactory<Apple> classFactory =
                    new ClassFactory<>(Apple.class);
    
            System.out.println("succeed");
    
    
    //        报错
            ClassFactory<Integer> classFactory1 =
                    new ClassFactory<>(Integer.class);
    
    
    
        }
    }
    
    
    

    最后的语句会报错,因为Integer没有任何默认构造函数。

    泛型数组

    正如之前的例子可以看到,不能创建泛型数组,所以一般的思路就是用Arraylist

    public class ListOfGenerics<T> {
    
        private List<T> array = new ArrayList<>();
    
        public void add(T item){
            array.add(item);
        }
    
        public T get(int index) {
            return array.get(index);
        }
    
    
    }
    
    

    假如你还是想创建泛型类型数组,你可以按照编译器喜欢的方式定义一个引用。

    public class Generic<T> {
    }
    
    public class ArrayOfGenericReference {
    
        static Generic<Integer>[] gia;
    
    
    }
    
    

    编译器会接受这个程序,也不会给出任何警告,但是,永远都不能创建这个确切类型的数组(包括类型参数)。

    比如接下来这个,可以编译,但不能运行。

    public class ArrayOfGeneric {
    
        static final int SIZE = 100;
    
        static Generic<Integer>[] gia;
    
        @SuppressWarnings("unchecked")
        public static void main(String[] args) {
    
            gia = (Generic<Integer>[]) new Object[SIZE];
    
            gia = new Generic[SIZE];
    
            System.out.println(gia.getClass().getSimpleName());
    
            gia[0] = new Generic<Integer>();
    
    //        gia[1] = new Object();
    
    //        gia[2] = new Generic<Double>();
    
    
    
        }
    
    
    }
    
    
    

    来看一个泛型数组包装器:

    public class GenericArray<T> {
    
        private T[] array;
    
        @SuppressWarnings("unchecked")
        public GenericArray(int sz){
            array = (T[]) new Object[sz];
        }
    
        public void put(int index,T item){
            array[index] = item;
        }
        
        public T get(int index){
            return array[index];
        }
    
        public T[] rep(){
            return array;
        }
    
        public static void main(String[] args) {
    
            GenericArray<Integer> gai =
                    new GenericArray<>(10);
    
    
    //        error
    //        Integer[] integers = gai.rep();
    
            Object[] objects = gai.rep();
            
        }
        
    }
    
    
    

    我们不能声明T[] array = new T[sz],所以需要创建一个对象数组,然后将其转型

    因为擦除,数组运行时类型就只能时Object[],假设我们立即将其转型为T[ ],那么编译期间数组的实际类型就会丢失。

    看接下来的例子:

    public class GenericArray2<T> {
    
        private Object[] array;
    
        public GenericArray2(int size){
            array = new Object[size];
        }
    
        public void put(int index,T item){
            array[index] = item;
        }
    
        @SuppressWarnings("unchecked")
        public T get(int index){
            return (T) array[index];
        }
    
        @SuppressWarnings("unchecked")
        public T[] rep(){
            return (T[]) array;
        }
    
        public static void main(String[] args) {
    
            GenericArray2<Integer> genericArray2 =
                    new GenericArray2<>(20);
    
            for (int i = 0; i < 20; i++) {
                genericArray2.put(i,i);
    
            }
    
            for (int i = 0; i < 20 ; i++) {
                System.out.println(genericArray2.get(i) + "\n");
    
            }
    
    //        error
    //        Integer[] integers = genericArray2.rep();
    
        }
    
    
    }
    
    
    

    现在内部表示是Object[]而不是T[ ],方法get()会将对象转型为T,这是正确的类型。

    当然传递一个类型标记也是可以尝试的:

    
    public class GenericArrayWithTypeToken<T> {
    
        private T[] array;
    
        @SuppressWarnings("unchecked")
        public GenericArrayWithTypeToken(Class<T> type,int siez){
            array = (T[]) Array.newInstance(type,siez);
        }
    
        public void put(int index,T item){
            array[index] = item;
        }
    
        public T get(int index){
            return array[index];
        }
    
        public T[] rep(){
            return array;
        }
    
        public static void main(String[] args) {
    
            GenericArrayWithTypeToken genericArrayWithTypeToken =
                    new GenericArrayWithTypeToken(Integer.class,30);
    
    //        error
    //        Integer[] ia = genericArrayWithTypeToken.rep();
    
    
        }
    
    }
    
    
    

    类型标记Class< T >被传递到构造函数中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。

    深入边界

    边界使得你可以在用于泛型的参数类型上设置限制条件。

    因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法,但是假设能将这个参数限制为某个类型的子集,那么就可以用这些类型子集来调用方法。

    看下面这个例子,体现边界的基本要素:

    public interface HasColor {
    
        java.awt.Color getColor();
    }
    
    public class Colored<T extends HasColor> {
    
        T item;
    
        public Colored(T item) {
            this.item = item;
        }
    
        public T getItem() {
            return item;
        }
    
        java.awt.Color color(){
            return item.getColor();
        }
    
    }
    
    public class Dimension {
    
        public int x,y,z;
    }
    
    
    
    public class ColoredDimension<T extends Dimension & HasColor> {
    
        T item;
    
        public ColoredDimension(T item) {
            this.item = item;
        }
    
        public T getItem() {
            return item;
        }
    
        java.awt.Color color(){
            return item.getColor();
        }
    
        int getX(){
            return item.x;
        }
    }
    
    
    public interface Weight {
        int weight();
    }
    
    
    public class Solid<T extends Dimension & HasColor & Weight> {
    
        T item;
    
        public Solid(T item) {
            this.item = item;
        }
    
        public T getItem() {
            return item;
        }
    
        java.awt.Color color(){
            return item.getColor();
        }
    
        int getX(){
            return item.x;
        }
    
        int weight(){
            return item.weight();
        }
    }
    
    
    public class Bounded extends Dimension implements HasColor,Weight{
    
        @Override
        public Color getColor() {
            return null;
        }
    
        @Override
        public int weight() {
            return 0;
        }
    }
    
    
    
    public class BasicBounds {
    
        public static void main(String[] args) {
    
    
            Solid<Bounded> solid =
                    new Solid<>(new Bounded());
    
            solid.color();
    
            solid.getX();
    
            solid.weight();
    
        }
    }
    
    
    

    接下来看看如何在继承的每个层次上添加边界限制:

    
    public class HoldItem<T> {
    
        T item;
    
        public HoldItem(T item) {
            this.item = item;
        }
    
        public T getItem() {
            return item;
        }
    }
    
    
    public class Color2<T extends HasColor> extends HoldItem<T> {
    
        public Color2(T item) {
            super(item);
        }
    
        java.awt.Color color(){
            return item.getColor();
        }
    
    
    }
    
    public class ColoredDimension2<T extends Dimension & HasColor> extends Color2<T>{
    
        public ColoredDimension2(T item) {
            super(item);
        }
    
        int getX(){
            return item.x;
        }
    }
    
    
    public class Solid2<T extends Dimension & HasColor & Weight> extends ColoredDimension2<T>{
    
        public Solid2(T item) {
            super(item);
        }
    
        int weight(){
            return item.weight();
        }
    }
    
    
    public class InheritBounds {
    
        public static void main(String[] args) {
    
    
            Solid2<Bounded> solid2 = new
                    Solid2<>(new Bounded());
    
            solid2.color();
    
            solid2.getX();
    
            solid2.weight();
        }
    }
    
    
    

    HoldItem直接持有一个对象,因此这种行为也继承到了Color2中,也要求其参数与HasColor一致。

    通配符

    来考虑数组的一种特殊行为,向导出类型的数组赋予基类型的数组引用。

    public class Fruit {
    }
    
    public class Apple extends Fruit{
    }
    
    
    public class Jonathan extends Apple{
    }
    
    
    public class Orange extends Fruit{
    }
    
    
    public class CovarianArrays {
    
        public static void main(String[] args) {
    
    //      创建了一个Apple数组,并将其赋值给了一个Fruit数组引用,这是有意义的
    //        因为Apple也是一种Fruit。所以Apple数组也是一个Fruit数组
            Fruit[] fruits = new Apple[10];
    
            fruits[0] = new Apple();
    
            fruits[1] = new Jonathan();
    
    //        error
    //        fruits[0] = new Fruit();
    
    //        error
    //        fruits[0] = new Orange();
    
    
    
        }
    }
    
    
    
    

    如果用泛型容器代替数组会怎样:

    public class NonCovariantGenerics {
    
    //    error
    //    List<Fruit> fruits = new ArrayList<Apple>();
    
    
    }
    
    
    

    Apple的List在类型上不等价于Fruit的List,即使Apple是一种Fruit类型。

    那如果我想要在两个类型之间建立某种类型的向上转型关系,这个时候就可以使用通配符了

    
    public class GenericsAndCovariance {
    
        public static void main(String[] args) {
    
    
            List<? extends Fruit> fruits = new ArrayList<Apple>();
    
    //        error
    //        fruits.add(new Apple())
    
    //        error
    //        fruits.add(new Fruit())
    
            fruits.add(null);
    
            Fruit fruit = fruits.get(0);
    
            System.out.println(fruit);
    
    
        }
    }
    
    

    fruits现在是List<? extends Fruit>,你可以将其视作“具有任何从Fruit继承的类型的列表”。

    逆变

    超类型通配符

    声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至或者使用类型参数<? super T>.

    例如:

    public class SuperTypeWildcards {
    
        static void writeTo(List<? super Apple> a){
            a.add(new Apple());
    
            a.add(new Jonathan());
    
    //        error
    //        a.add(new Fruit());
        }
    }
    
    

    参数Apple是Apple的某种基类型的List,这样向其中添加Apple或Apple的子类型是安全的。

    超类型边界放松了在可以向方法传递的参数上所做的限制:

    
    public class GenericWriting {
    
    //    使用了一个确切参数类型(无通配符)
        static <T> void writeExact(List<T> list,T item){
            list.add(item);
        }
    
        static List<Apple> apples = new ArrayList<Apple>();
    
        static List<Fruit> fruits = new ArrayList<Fruit>();
    
        static void f1(){
            writeExact(apples,new Apple());
            writeExact(fruits,new Apple());
        }
    
    //    使用了通配符,因此这个List将持有从T导出的某种具体类型
        static <T> void
        writrWithWildcard(List<? super T> list,T item){
            list.add(item);
        }
    
        static void f2(){
            writrWithWildcard(apples,new Apple());
            writrWithWildcard(fruits,new Apple());
        }
    
        public static void main(String[] args) {
            f1();
            f2();
        }
    
    }
    
    
    

    无界通配符

    无界通配符<?>看起来意味着“任何事物”,因此使用起来就好像等价于使用原生类型。

    public class UnboundedWildcard1 {
    
        static List list1;
    
        static List<?> list2;
    
        static List<? extends Object> list3;
    
        static void assign1(List list){
            list1 = list;
            list2 = list;
            list3 = list;
        }
    
        static void assign2(List<?> list){
            list1 = list;
            list2 = list;
            list3 = list;
        }
    
        static void assign3(List<? extends Object> list){
            list1 = list;
            list2 = list;
            list3 = list;
        }
    
        public static void main(String[] args) {
    
    
            assign1(new ArrayList());
    
            assign2(new ArrayList<>());
    
            assign3(new ArrayList<>());
    
            assign1(new ArrayList<String>());
    
            assign2(new ArrayList<String>());
    
            assign3(new ArrayList<String>());
    
            List<?> wildList = new ArrayList<>();
    
            wildList = new ArrayList<String>();
    
            assign1(wildList);
    
            assign2(wildList);
    
            assign3(wildList);
    
        }
    
    
    }
    
    

    相关文章

      网友评论

          本文标题:Java编程思想---泛型(2)

          本文链接:https://www.haomeiwen.com/subject/pdcyactx.html