美文网首页
Java 编程思想笔记:Learn 11

Java 编程思想笔记:Learn 11

作者: 智勇双全的小六 | 来源:发表于2018-05-23 17:17 被阅读0次

    第15章 泛型

    一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

    在面向对象编程语言中,多态算是一种泛化机制。例如,你可以经方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。

    但是有时候,拘泥于单继承体系,也会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了很多。因为任何试了该接口的类都能满足该方法,这也包含了暂时还不存在的类。

    但是有时候接口的限制也是太强了。因为一旦指明接口,就必须实现该接口。有时,我们希望编写更通用的代码,要使代码能够适用于 “某种不具体的类型”,而不是一个具体的接口或类。

    泛型实现了参数化类型的概念,泛型的概念是适用于许多类型。

    15.2 简单泛型

    泛型出现的原因很多,最主要的就是创造容器类。
    持有单个容器的类:

    public class Holder1 {
        private Automobile a;
    
        public Holder1(Automobile a){
            this.a = a;
        }
    
        Automobile get(){
            return a;
        }
    }
    
    class Automobile {}
    

    这个类直接持有 Object 类型的对象:

    class Holder2{
        private Object a;
    
        public Holder2(Object a){
            this.a = a;
        }
    
        public void setA(Object a) {
            this.a = a;
        }
    
        public Object getA() {
            return a;
        }
    
        public static void main(String[] args){
            Holder2 holder2 = new Holder2(new Automobile());
            Automobile automobile = (Automobile) holder2.getA();
            holder2.setA("Not an Automobile");
            String s = (String) holder2.getA();
            holder2.setA(1);
            Integer x = (Integer) holder2.getA();
        }
    }
    

    有时,我们并不希望使用 Object , 我们更喜欢暂时不指定类型,而是稍后决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号参数,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数.

    public class Holder3<T>{
      private T a;
      public Holder3(T a){
        this.a = a;
      }
      public void set(T a){
        this.a = a;
      }
      public T get(){
        return a;
      }
      public static void main(String[] args){
        Holder3<String> h3 = new Holder3<String>(new String());
      }
    }
    

    15.2.1 一个元组类库

    因为 return对象只能返回单个对象,为了返回多个对象,就需要返回一个对象,用它来持有想要返回的多个对象。因为有泛型,能够一次性地解决该问题,这个概念被称为元组(tuple)。

    元组是将一组对象直接打包存储于其中的一个单一对象,这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也被称为数据传送对象,或信使)。

    通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。

    public class TwoTuple<A,B> {
    
        public final A first;
    
        public final B second;
    
        public TwoTuple(A a, B b){
            first = a;
            second = b;
        }
    
        @Override
        public String toString() {
            return "TwoTuple{" +
                    "first=" + first +
                    ", second=" + second +
                    '}';
        }
    }
    

    构造器捕获了要存储的对象,而 toString() 是一个便利函数,用来显示列表中的值。注意,元组隐藏地保持了其中元素的次序。

    客户端程序员可以读取 first 和 second 对象,但是无法将其他值赋予 first 或 second。因为这两个对象都声明为final.

    如果客户端程序员确实需要改变 first 或 second 所引用的对象,需要他们另外创建一个新的 TwoTuple 对象。我们可以利用继承机制实现长度更长的元组。

    public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
    
        public final C third;
    
        public ThreeTuple(A a, B b, C c){
            super(a, b);
            third = c;
        }
    
        @Override
        public String toString() {
            return "ThreeTuple{" +
                    "third=" + third +
                    '}';
        }
    }
    
    15.2.2 一个堆栈类

    传统的下推堆栈。现在不使用 LinkedList, 实现自己的内部链式存储机制

    public class LinkedStack<T> {
    
        private static class Node<U>{
            U item;
    
            Node<U> next;
    
            Node(){
                item = null;
                next = null;
            }
    
            Node(U item, Node<U> next){
                this.item = item;
                this.next = next;
            }
    
            boolean end(){
                return item == null && next == null;
            }
        }
    
        private Node<T> top = new Node<T>();
    
        public void push(T item){
            top = new Node<T>(item, top);
        }
    
        public T pop(){
            T result = top.item;
            if(!top.end()){
                top = top.next;
            }
            return result;
        }
    
        public static void main(String[] args){
            LinkedStack<String> lss = new LinkedStack<>();
            for(String s : "Phasers or run!".split(" ")){
                lss.push(s);
            }
            String s;
            while ((s = lss.pop()) != null){
                System.out.println(s);
            }
        }
    }
    

    内部类 Node 也是一个泛型,它拥有自己的类型参数。

    这个例子使用了一个末端哨兵(end sentinel)来判断堆栈何时为空。这个末端哨兵是在构造 LinkedStack 时创建的。然后,每调用一次 push 方法,就会创建一个 Node<T> 对象,并将其链接到前一个Node<T>对象。当你调用 pop() 方法时,总是会返回 top.item, 然后丢弃当前 top 所指的 Node<T>, 并将 top 转移到下一个 Node<T>, 除非你已经碰到了末端哨兵,这时候就不再移动 top了。如果已经到了末端,客户端还继续调用 pop 方法,它只能得到 null,说明堆栈已经空了。

    15.2.3 RandomList

    作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用其上的 select 方法时,它就可以随机地选取一个元素,就需要使用泛型:

    public class RandomList<T> {
    
        private List<T> storage = new ArrayList<>();
    
        private Random random = new Random(47);
    
        public void add(T item){
            storage.add(item);
        }
    
        public T select(){
            return storage.get(random.nextInt(storage.size()));
        }
    
        public static void main(String[] args){
            RandomList<String> rs = new RandomList<>();
            for(String s : ("The quick brown fox jumped".split(" "))){
                rs.add(s);
            }
            for(int i =0; i < 11; i++){
                System.out.println(rs.select());
            }
        }
    }
    

    15.3 泛型接口

    泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。

    一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next方法。

    public interface Generator<T> {T next();}
    

    方法 next() 返回类型是参数化的T。接口使用泛型和类使用泛型没啥区别。为了演示如何实现 Generator 接口,我们还需要一些别的类,例如 Coffee 类层次结构如下:

    public class Coffee {
        private static long counter = 0;
        private final long id = counter++;
    
        public String toString(){
            return getClass().getSimpleName() + " " + id;
        }
    }
    
    
    class Latte extends Coffee{}
    
    class Mocha extends Coffee{}
    
    class Cappuccino extends Coffee{}
    
    class Americano extends Coffee{}
    
    class Breve extends Coffee{}
    
    class CoffeeGenerator implements Generator<Coffee>, Iterator<Coffee>{
        private Class[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class};
        private static Random rand = new Random(47);
        public CoffeeGenerator() {}
    
        // For iteration
        private int size = 0;
    
        public CoffeeGenerator(int size){
            this.size = size;
        }
    
        public Coffee next(){
            try{
                return (Coffee) types[rand.nextInt(types.length)].newInstance();
            } catch (Exception e){
                throw new RuntimeException(e);
            }
        }
    
        class CoffeeIterator implements Iterator<Coffee>{
            int count = size;
    
            public boolean hasNext(){
                return count > 0;
            }
    
            public Coffee next(){
                count--;
                return CoffeeGenerator.this.next();
            }
    
            public void remove(){
                throw new UnsupportedOperationException();
            }
        }
    
        public Iterator<Coffee> iterator(){
            return new CoffeeGenerator();
        }
    
        public static void main(String args){
            CoffeeGenerator gen = new CoffeeGenerator();
            for(int i = 0; i < 5; i++){
                System.out.println(gen.next());
            }
            for(Coffee c : new CoffeeGenerator(5)){
                System.out.println(c);
            }
        }
    }
    

    Java 泛型的局限性就是基本类型不能作为泛型。

    public class Fibonacci implements Generator<Integer> {
        private int count = 0;
    
        public Integer next(){
            return fib(count++);
        }
    
        private int fib(int n){
            if(n < 2){
                return 1;
            }
            return fib(n-2) + fib(n-1);
        }
    
        public static void main(String[] args){
            Fibonacci gen = new Fibonacci();
            for(int i = 0; i < 18 ; i++){
                System.out.println(gen.next() + " ");
            }
        }
    }
    

    如果还想更进一步,编写一个实现了 Iterable 的 Fibonacci 生成器。一个选择是重写这个类,令其实现 Iterable 接口。不过,有时没有这个代码的控制权。另外一个选择就是创建适配器( adapter ) 来实现所需的接口。

    有多种方式可以实现适配器,例如,可以通过继承来创建适配器类。

    15.4 泛型方法

    泛型方法使得该方法能够独立于类而产生变化。一个基本的指导原则:无论何时,只要你能做到,就应该尽量使用泛型方法。也就说,如果使用泛型类的地方如果能够被泛型方法所取代,那么就应该只使用泛型方法。

    对于一个 static 的方法而言,无法访问泛型类的类型参数,所以如果 static 方法需要使用泛型能力,就必须使用泛型方法。定义一个泛型方法,只需要将泛型参数列表置于返回值之前:

    public class GenericMethods {
        public <T> void f(T x){
            System.out.println(x.getClass().getName());
        }
    
        public static void main(String[] args){
            GenericMethods genericMethods = new GenericMethods();
            genericMethods.f("");
            genericMethods.f(1);
            genericMethods.f(1.0);
            genericMethods.f(1.0f);
            genericMethods.f('c');
            genericMethods.f(genericMethods);
        }
    }
    

    当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这被称为类型参数推断(type argument inference)。我们可以像调用普通方法一样调用 f(), 而且就好像 f() 被无限次地重载过。

    15.4.1 杠杆利用类型参数推断

    人们对泛型有一个抱怨,使用泛型有时候需要向程序添加更多的代码。如果要创建一个持有 List 的Map,就像做如下操作:

    public class GenericMethods {
        public <T> void f(T x){
            System.out.println(x.getClass().getName());
        }
    
        public static void main(String[] args){
            GenericMethods genericMethods = new GenericMethods();
            genericMethods.f("");
            genericMethods.f(1);
            genericMethods.f(1.0);
            genericMethods.f(1.0f);
            genericMethods.f('c');
            genericMethods.f(genericMethods);
        }
    }
    

    ? extends Integer 的方式是什么意思?

    Map<String, List<? extends Integer>> people = new HashMap<>();
    

    注意返回值的书写方式

    public class New {
        public static <K,V> Map<K,V> map(){
            return new HashMap<>();
        }
    
        public static <T> List<T> list(){
            return new ArrayList<>();
        }
    
        public static <T>LinkedList<T> linkedList(){
            return new LinkedList<>();
        }
    
        public static <T> Set<T> set(){
            return new HashSet<>();
        }
    
        public static <T> Queue<T> queue(){
            return new LinkedList<>();
        }
    
        public static void main(String[] args){
            Map<String, List<String>> sls = New.map();
            List<String> ls = New.list();
            LinkedList<String> lls = New.linkedList();
            Queue<String> qs = New.queue();
        }
    }
    

    赋值时,Java 能对泛型做类型推断。如果你将一个泛型方法调用的结果(例如New.map())作为参数,传递给另外一个方法,这时编译器不会进行类型推断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋予一个 Obeject 类型的变量。

    显式的类型说明

    在泛型方法中,可以显式地指明类型,不过这种语法很少使用。要显示指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号中。
    那我就不学了。。。

    15.4.2 可变参数与泛型方法

    public class GenericVarargs {
    
        public static <T> List<T> makeList(T... args){
            List<T> result = new ArrayList<>();
            for(T item : args){
                result.add(item);
            }
            return result;
        }
    
        public static void main(String[] args){
            List<String> ls = makeList("A");
            System.out.println(ls);
            ls = makeList("A", "B", "C");
            System.out.println(ls);
        }
    }
    

    泛型方法与可变参数列表能够很好的共存。

    15.4.3 用 Generator 的泛型方法

    pass

    15.4.4 一个通用的 Generator

    public class BasicGenerator<T> implements Generator<T> {
        private Class<T> type;
    
        public BasicGenerator(Class<T> type){
            this.type = type;
        }
    
        public T next(){
            try{
                return type.newInstance();
            } catch (Exception e){
                throw new RuntimeException(e);
            }
        }
    
        public static <T> Generator<T> create(Class<T> type){
            return new BasicGenerator<>(type);
        }
    }
    

    CountedObject 类能够记录它创建了多少个 CountedObject 实例,并通过 toString() 方法告诉我们其编号。

    public class CountedObject {
        private static long counter = 0;
        private final long id = counter++;
    
        public long id(){
            return id;
        }
    
        public String toString(){
            return "CountedObject " + id;
        }
    }
    

    使用 BasicGenerator ,可以很容易为 CountedObject 创建一个 Generator:

    15.5 匿名内部类

    泛型还可以应用于内部类以及匿名内部类。下面的示例使用匿名内部类实现了 Generator 接口:

    15.7 擦除的神秘之处

    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> 是不同的类型,但是他们的 class 却是相同的。因为 Java 泛型是使用擦除来实现的,这意味着当你使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List<String> 和 List<Integer> 在运行时事实上是相同的类型。这两种形式都被擦除成它们的 “原生”类型,即 list。

    15.7.1 C++ 的方式

    pass

    15.7.2 迁移兼容性

    如果泛型在 Java 1.0 中就已经是其中一部分了,那么这个特性就不会使用擦除来实现了,它将使用具体化,使类型参数保持为第一类实体,那么就能在类型参数上执行基于类型的语言操作和反射操作。但是为了保持兼容性,Java 5 才加入这个特性,所以擦除实现泛型是一种折中的做法。

    在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中所有的泛型类型都将被擦除,替换为它们的非泛型上界。如,List<T>这样的类型注解会被擦除为 List,而普通的类型变量在未指定边界的情况下将被擦除为 Object

    擦除的核心动机是它们使得泛化的客户端可以使用非泛化的类库来使用,反之亦然。这被称为 “迁移兼容性”。

    15.7.3 擦除的问题

    因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入 java 语言。擦除使得现有的非泛型客户端能够在不改变的情况下继续使用,直至客户端准备好使用泛型重写这些代码。这样的做法不会破坏现有的代码。

    擦除的代价是显著的,泛型不能用于显示地引用运行时类型的操作中,例如转型、instanceof 操作 和 new 表达式。因为所有关于参数的类型信息都丢失了。当编写泛型代码时,必须提醒自己,你只是看起来好像拥有了有关参数的类型信息而已。

    class Foo<T>{
      T var;
    }
    

    当创建 Foo 实例时,

    Foo<Cat> f = new Foo<Cat>();
    

    class Foo 中的代码并不知道现在基于 Cat 之上,这只是一个 Object,代码并不知道T 是 Cat。

    15.7.4 边界处的动作

    在泛型中,创建数组使用 Array.newInstance(kind, size) 的方式。

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

    但是如果是容器使用泛型,跟平常一样使用就好了。

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

    即使编辑器无法知道有关 create() 中 T 的任何信息,但是它仍然可以在编译期间确保result 对象中具体 T 类型。因此,即使在擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一性。

    public class FilledListMaker<T> {
    
        List<T> create(T t, int n){
            List<T> result = new ArrayList<>();
            for(int i = 0; i < n; i++){
                result.add(t);
            }
            return result;
        }
    
        public static void main(String[] args){
            FilledListMaker<String> stringFilledListMaker = new FilledListMaker<>();
            List<String> list = stringFilledListMaker.create("hello", 4);
            System.out.println(list);
        }
    }
    

    因为擦除在方法中体现移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地方。这些正是编译器在编译期执行类型检查并插入转型代码的地点。请考虑以下非泛型示例:

    
    

    然后是一些关于字节码的事情,先略过

    15.8 擦除的补偿

    擦除丢失了在泛型代码中执行某些操作的能力,擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道的确切类型信息的操作都无法工作:

    public class Erased<T> {
        private final int SIZE = 100;
        private Class<T> kind;
    
        public void setKind(Class<T> kind) {
            // 调用的时候,setKind(String.class)
            this.kind = kind;
        }
    
        public void f(Object arg){
            // 以下的操作都是不允许的
            if(arg instanceof T){}
            T var = new T();
            T[] array = new T[SIZE];
            // 这个样子是允许的
            T[] array2 = (T[]) Array.newInstance(kind,SIZE);
            // 这个会有警告
            T[] array3 = (T[]) new Object[SIZE];
        }
    }
    

    偶尔可以绕过这些问题来编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显示地传递你的类型的 Class 对象,以便你可以在类型表达式中使用它。

    例如,在前面示例中对使用 instanceof 的尝试最终失败了,因为其类型信息已经被擦除了。如果引入类型标签,就可以转而使用动态的 isInstance():

    class Building {}
    
    class House extends Building{}
    
    
    public class ClassTypeCapture<T> {
        private 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<Building> classTypeCapture = new ClassTypeCapture<Building>(Building.class);
            System.out.println(classTypeCapture.f(new Building()));
            System.out.println(classTypeCapture.f(new House()));
        }
    }
    

    当显示地传递类型时,泛型就可以调用类型了。

    15.8.1 创建类型实例

    在 Erased.java 中对创建一个 new T() 的尝试将无法实现,部分原因是因为擦除,而另外一部分原因是因为编译器不能验证 T 具有默认(无参)构造器。

    在Java 中解决方法是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是 Class 对象,因此如果使用类型标签,那么就可以使用 newInstance() 来创建这个类型的新对象:

    class ClassAsFactory<T>{
        T x;
        public ClassAsFactory(Class<T> kind){
            try{
                x = kind.newInstance();
            }catch (Exception e){
                throw new RuntimeException(e);
            }
        }
    }
    
    class Employee {}
    
    public class InstantiateGenericType {
    
        public static void main(String[] args){
            ClassAsFactory<Employee> classAsFactory = new ClassAsFactory<>(Employee.class);
            System.out.println("ClassAsFactory<Employee> succeeded");
            try{
                ClassAsFactory<Integer> factory = new ClassAsFactory<>(Integer.class);
            }catch (Exception e){
                // newInstance 是 Class 的方法,
                // Integer 与 Class 这个类没有瓜葛, 所以就失败了
                System.out.println("ClassAsFactory<Integer> failed");
            }
        }
    }
    

    还有另外一种方式创建泛型的实例,这种是由Sun 公司建议的

    public class FactoryConstraint {
        public static void main(String[] args){
            new Foo2<Integer>(new IntegerFactory());
            new Foo2<Widget>(new Widget.Factory());
        }
    }
    
    // 泛型接口
    interface FactoryI<T>{
        T create();
    }
    
    class Widget{
        public static class Factory implements FactoryI<Widget>{
            public Widget create(){
                return new Widget();
            }
        }
    }
    
    class IntegerFactory implements FactoryI<Integer>{
        @Override
        public Integer create() {
            return new Integer(0);
        }
    }
    
    class Foo2<T>{
        private T x;
        public <F extends FactoryI<T>> Foo2(F factory){
            x = factory.create();
        }
    }
    

    这种通过实现接口的方式,使得在编译期间就能对类型进行检查。

    另一种方式是模板方法设计模式,在下面的实例中,get() 是模板方法,而 create() 是在子类中定义的、用来产生子类类型的对象:

    public class CreatorGeneric {
        public static void main(String[] args){
            Creator c = new Creator();
            c.f();
        }
    }
    
    
    class Creator extends GenericWithCreate<X>{
        X create(){
            return new X();
        }
    
        void f(){
            System.out.println(element.getClass().getSimpleName());
        }
    }
    
    
    class X {}
    
    abstract class GenericWithCreate<T>{
        final T element;
    
        GenericWithCreate(){
            element = create();
        }
    
        abstract T create();
    }
    

    这个模板方法,说白了就是使用抽象函数,然后具体函数继承(extends)抽象类,实现抽象类中的方法。

    15.8.2 泛型数组

    一般情况下,不能创建泛型数组。一般的解决方案是在使用数组的地方,使用容器 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);
        }
    }
    

    这样就可以获得数组的行为,以及由泛型提供的编译期的类型安全。

    有时,就是想创建数组,(例如,ArrayList 内部使用的是数组)。可以按照编译器喜欢的方式定义一个引用,如:
    不知道讲的啥鬼,掠过

    15.9 边界

    边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用办法。

    因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用 Object 调用的方法。但是,如果能够将这个参数限制为某个类型的子集,那么你就可以用这些类型子集来调用方法。为了执行这种限制,Java 泛型重用了 extends 关键字。对你来说有一点很重要,即要理解 extends 关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。下面展示了边界的基本要素:
    没看懂,掠过

    15.10 通配符

    可以向到处类型的数组赋予基类型的数组引用:

    public class CovarianArrays {
        public static void main(String[] args){
            Fruit[] fruits = new Apple[10];
            fruits[0] = new Apple();
            fruits[1] = new Jonathan();
            try{
                fruits[0] = new Orange();
            }catch (Exception e){
                System.out.println(e);
            }
        }
    }
    
    class Fruit {}
    class Apple extends Fruit{}
    class Jonathan extends Apple{}
    class Orange extends Fruit{}
    

    main() 中的第一行创建了一个 Apple 数组,并将其赋值给一个 Fruit 数组引用。这是有意义的,因为 Apple 也是一种 Fruit,因此 Apple 数组应该也是一个 Fruit 数组。

    但是,如果实际的数组类型是 Apple[], 你应该只能在其中放置 Apple 或 Apple 的子类型,这在编译期和运行时都可以工作。但是,编译器允许你将 Fruit 放置到这个数组中,这对于编译器来说是有意义的,因为它有一个 Fruit[] 引用。在编译期,这是允许的。但是,运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时会抛出异常:

    java.lang.ArrayStoreException: com.zzjack.rdsapi_demo.javathought.Orange
    

    实际上,向上转型不合适用在这里。你真正做的是将一个数组赋值给另一个数组。数组的行为应该是它可以持有其他对象。这里只是因为我们能够向上转型而已。所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象是有意识的。因此在编译期间检查和运行时检查之间,不能滥用他们。

    对数组的这种赋值并不那么可怕,因为在运行时可以发现已经插入了不正确的类型。但是泛型的主要目标之一是将这种错误检测移入到编译期

    List<Fruit> fruits2 = new ArrayList<Apple>();
    

    第一阅读这段代码时会认为,“不能将一个 Apple 容器赋值给Fruit容器”。实际上,根本就不是这么回事,Apple 的List 不是 Fruit 的List。Apple 的List 将持有Apple和Apple的子类型,而Fruit 的List 将持有任何类型的 Fruit,这包括Apple在内。这两种List 的类型完全是不等价的。

    真正的问题在于容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想利用类型做什么,以及应该采用什么样的规则。

    但是,有时你想要在两个类型之间搭建某种类型的向上转型关系,这正是通配符所允许的:

    List<? extends Fruit> flist = new ArrayList<>();
    

    flist 的类型现在是 List<? extends Fruit>,你可以将其读作 “具有任何从 Fruit 继承的类型的列表”。但是,这并不意味着这个List将持有任何类型的 Fruit。通配符引用的是明确的类型,因此它意味着 “某种 flist 引用没有指定的具体类型”。因此这个被赋值的 List 必须持有 Fruit 或 Apple 这样的某种类型,但是为了向上转型为 flist,这个类型是什么并没有人关心。

    如果调用一个返回 Fruit 的方法,则是安全的,因为在这个 List 中任何对象至少具有 Fruit 类型

        public List<? extends Fruit> test(){
            List<Apple> apples = new ArrayList<>();
            return apples;
        }
    

    15.10.1 编译器有多聪明

    public class CompilerIntelligence {
    
        public static void main(String[] args){
            List<? extends Fruit> flist = Arrays.asList(new Apple());
            Apple a = (Apple) flist.get(0);
            flist.contains(new Apple());
            flist.indexOf(new Apple());
            flist.indexOf(new String());
        }
    }
    

    我觉得作者说的太罗嗦了,掠过

    15.10.2 逆变

    超类型通配符,通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至使用类型参数 <? super T>(尽管你不能对泛型参数给出一个超类型边界,即不能声明<T super MyClass>). 这个特性可以这么使用:

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

    凡是 Apple 的字类型都可以被使用。

    15.10.3 无界通配符

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

    public class UnboundedWildCards1 {
        static List list1;
        static List<?> list2;
        static List<? extends Object> list3;
    
        static void assign1(List list){
            list1 = list;
            list2 = list;
            list3 = list; // unchecked error
        }
    
        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){}
    }
    

    上面这段代码,看起来<?>与<? extends Object> 的作用是差不多的。

    下面这段代码将展示通配符的重要作用,

    试了一下,这段代码据说会产生警告。我觉得没啥必要了

    15.11 使用泛型可能会出现的问题

    1. java 泛型中,不能使用基本类型用作类型参数。解决办法,使用装箱类型就好了

    15.12 自限定的类型

    class SelfBounded<T extends SelfBounded<T>>
    class A extends SelfBounded<A>{}
    

    自限定类型适用于,泛型把自己当作边界条件

    15.14 异常

    由于擦除的原因,将泛型应用于异常是非常受限的。catch语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable
    。。。

    相关文章

      网友评论

          本文标题:Java 编程思想笔记:Learn 11

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