疯狂Java讲义(九)

wuchangjian2021-11-03 01:36:22编程学习

本章内容:

        本章的知识可以与前一章的内容补充阅读,因为Java 5增加泛型支持在很大程度上都是为了让集合能记住其元素的数据类型。在没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastExeception异常。
        增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误。增加泛型后的集合,可以让代码更加简洁,程序更加健壮(Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常)。除此之外,Java泛型还增强了枚举类、反射等方面的功能,泛型在反射中的用法,将在第18章中介绍
        本章不仅会介绍如何通过泛型来实现编译时检查集合元素的类型,而且会深入介绍Java泛型的详细用法,包括定义泛型类、泛型接口,以及类型通配符、泛型方法等知识。

1.泛型入门

        Java集合有个缺点——把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
        Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

下面将深入介绍编译时不检查类型可能引发的异常,以及如何做到在编译时进行类型检查。

  (1) 编译时不检查类型的异常

下面程序将会看到编译时不检查类型所导致的异常。

        上面程序创建了一个List集合,而且只希望该List集合保存字符串对象——但程序不能进行任何限制,如果程序在①处“不小心”把一个Integer对象“丢进”了List集合中,这将导致程序在②处引发ClassCastException异常,因为程序试图把一个Integer对象转换为String类型

  (2) 使用泛型

        Java 5以后,Java引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,正如在第8章的ShowHand.java程序中见到的 List<String>,这表明该List 只能保存字符串类型的对象。Java的参数化类型被称为泛型(Generic)。
        对于前面的ListErr.java程序,可以使用泛型改进这个程序。

         上面程序成功创建了一个特殊的List集合: strList,这个 List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。注意①处的类型声明,它指定 strList 不是一个任意的List,而是一个String类型的List,写作: List<String>。可以称List是带一个类型参数的泛型接口,在本例中,类型参数是String。在创建这个ArrayList对象时也指定了一个类型参数。
        上面程序将在②处引发编译异常,因为strList集合只能添加String对象,所以不能将Integer对象“丢进”该集合。
        而且程序在③处不需要进行强制类型转换,因为 strList对象可以“记住”它的所有集合元素都是String类型。
        上面代码不仅更加健壮,程序再也不能“不小心”地把其他对象“丢进”strList集合中;而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。这一切,都是因为Java 5提供的泛型支持。

  (3) Java 9增强的“菱形”语法

        Java 7以前如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。例如如下两条语句:

        上面两条语句中的粗体字代码部分完全是多余的,在Java7以前这是必需的,不能省略。从Java 7开始,Java 允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:

        把两个尖括号并排放在一起非常像一个菱形,这种语法也就被称为“菱形”语法。下面程序示范了Java 7的菱形语法。

        上面程序中三行粗体字代码就是“菱形”语法的示例。从该程序不难看出,“菱形”语法对原有的泛型并没有改变,只是更好地简化了泛型编程。
        Java9再次增强了“菱形”语法它甚至允许在创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型。下面程序示范了在匿名内部类中使用菱形语法

        上面程序先定义了一个带泛型声明的接口,接下来三行粗体字代码分别示范了在匿名内部类中使用菱形语法。第一行粗体字代码声明变量时明确地将泛型指定为String 类型,因此在该匿名内部类中T类型就代表了String类型;第二行粗体字代码声明变量时使用通配符来代表泛型(相当于通配符的上限为Object),因此系统只能推断出T代表Object,所以在该匿名内部类中T类型就代表了Object类型;第三行粗体字代码声明变量时使用了带上限(上限是Number)的通配符,因此系统可以推断出T代表Number类。
        无论哪种方式,Java 9都允许在使用匿名内部类时使用菱形语法

2. 深入泛型

        所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是在前面程序中看到的List<String>和ArrayList<String>两种类型。

  (1) 定义泛型接口、类

        上面三个接口声明是比较简单的,除了尖括号中的内容-——这就是泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参
        除此之外,①②处方法声明返回值类型是Iterator<E>、Set<K>,这表明Set<K>形式是一种特殊的数据类型,是一种与Set不同的数据类型——可以认为是Set类型的子类
        例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型:List<String>类型,可以把List<String>想象成E被全部替换成String的特殊List子接口。

        通过这种方式,就解决了9.1.2节中的问题——虽然程序只定义了一个List<E>接口,但实际使用时可以产生无数多个List 接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。必须指出:List<String>绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。

        可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个Apple类,这个Apple类就可以包含一个泛型声明。

        上面程序定义了一个带泛型声明的Apple<T>类(不要理会这个泛型形参是否具有实际意义),使用Apple<T>类时就可为T形参传入实际类型,这样就可以生成如 Apple<String>、Apple<Double>…形式的多个逻辑子类(物理上并不存在)。这就是9.1节可以使用List<String>、ArrayList<String>等类型的原因——JDK在定义List、ArrayList 等接口、类时使用了泛型声明,所以在使用这些类时为之传入了实际的类型参数。

  (2) 从泛型类派生子类

        当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类需要指出的是,当使用这些接口、父类时不能再包含泛型形参。例如,下面代码就是错误的。

        方法中的形参代表变量、常量、表达式等数据,本书把它们直接称为形参,或者称为数据形参。定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型
        如果想从Apple类派生一个子类,则可以改为如下代码:

        调用方法必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。

        像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)
        如果从Apple<String>类派生子类,则在Apple类中所有使用T类型的地方都将被替换成String类型,即它的子类将会继承到String getInfo()和 void setInfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意这一点。下面程序示范了这一点。

        如果使用Apple类时没有传入实际的类型(即使用原始类型),Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告,读者在前一章中应该多次看到这样的警告。如果希望看到该警告提示的更详细信息,则可以通过为javac命令增加-Xlint.unchecked选项来实现。此时,系统会把Apple<T>类里的T形参当成Object类型处理。如下程序所示。

        上面程序都是从带泛型声明的父类来派生子类,创建带泛型声明的接口的实现类与此几乎完全一样,此处不再赘述。

  (3) 并不存在泛型类

        前面提到可以把ArrayList<String>类当成ArrayList 的子类,事实上,ArrayList<String>类也确实像一种特殊的ArrayList类:该ArrayList<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。
        看下面代码的打印结果是什么?

        运行上面的代码片段,可能有读者认为应该输出false,但实际输出true因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)
        不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。下面程序演示了这种错误。

        由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。例如,下面代码是错误的。

3. 类型通配符

        正如前面讲的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?
        考虑如下代码:

        上面程序当然没有问题:这是一段最普通的遍历List 集合的代码。问题是上面程序中List是一个有泛型声明的接口,此处使用List接口时没有传入实际类型参数,这将引起泛型警告。为此,考虑为List接口传入实际的类型参数——因为List集合里的元素类型是不确定的,将上面方法改为如下形式:

        表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入的实际参数值时可能不是我们所期望的,例如,下面代码试图调用该方法。

        上面程序出现了编译错误,这表明List<String>对象不能被当成List<Object>对象使用,也就是说,List<String>类并不是List<Object>类的子类

        与数组进行对比,先看一下数组是如何工作的。在数组中,程序可以直接把一个Integer[]数组赋给一个Number[]变量。如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。例如如下程序。

        在Java的早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此Java在泛型设计时进行了改进,它不再允许把List<Integer>对象赋值给List<Number>变量。例如,如下代码将会导致编译错误(程序清单同上)。

        Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

  (1) 使用类型通配符

        为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作: List<?>(意思是元素类型未知的List()。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。可以将上面方法改写为如下形式:

        现在使用任何类型的List 来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object.

        但这种带通配符的List仅表示它是各种泛型List 的父类,并不能把元素加入到其中。例如,如下代码将会引起编译错误。

        因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现: add()方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合唯一的例外是null,它是所有引用类型的实例
        另一方面,程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。因此,把 get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。

  (2) 设定类型通配符的上限

        当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List 的父类,只希望它代表某一类泛型List 的父类。考虑一个简单的绘图程序,下面先定义三个形状类。

        上面定义了三个形状类,其中 Shape是一个抽象父类,该抽象父类有两个子类:Circle和 Rectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢?考虑如下的Canvas 实现类。

        注意上面的drawAll()方法的形参类型是List<Shape>,而 List<Circle>并不是List<Shape>的子类型,因此,下面代码将引起编译错误。

        关键在于List<Circle>并不是List<Shape>的子类型,所以不能把List<Circle>对象当成List<Shape>使用为了表示 List<Circle>的父类,可以考虑使用List<?>,但此时从List<?>集合中取出的元素只能被编译器当成Object 处理。为了表示 List集合的所有元素是Shape 的子类,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:

        将Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends Shape>使用。即 List<?extends Shape>可以表示List<Circle>、List<Rectangle>的父类—-只要List后尖括号里的类型是Shape的子类型即可。
        List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把 Shape称为这个通配符的上限(upper bound)。
        类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。

        与使用普通通配符相似的是,shapes.add()的第二个参数类型是? extends Shape,它表示Shape 未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。
        简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)
        对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A<Bar>就相当于A<? extends Foo>的子类,可以将A<Bar>赋值给A<? extends Foo>类型的变量,这种型变方式被称为协变
        对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!

  (3) 设定类型通配符的下限

        除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反
        指定通配符的下限就是为了支持类型型变。比如Foo是Bar的子类,当程序需要一个A<? super Bar>变量时,程序可以将A<Foo>、A<Object>赋值给A<? super Bar>类型的变量,这种型变方式被称为逆变

        对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)
        假设自己实现一个工具方法:实现将src集合中的元素复制到 dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。
        对于上面的copy()方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或者是前者的父类即可,此时通配符的下限就有了用武之地。下面程序采用通配符下限的方式来实现该copy)方法。

        使用这种语句,就可以保证程序的①处调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。

        实际上,Java集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示。

        正如前一章所介绍的,TreeSet会对集合中的元素按自然顺序或定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数c就是进行定制排序的Comparator对象。
        Comparator接口也是一个带泛型声明的接口:

        通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个 TreeSet<String>集合,并传入一个可以比较String 大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>一—只要尖括号里传入的类型是String 的父类型(或它本身)即可。

        通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparato作为参数传入,从而增加了程序的灵活性。当然,不仅TreeSet有这种用法,TreeMap也有类似的用法,具体的请查阅Java的API文档。

  (4) 设定泛型形参的上限

        Java泛型不仅允许在使用通配符形参时设定上限而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类。下面程序示范了这种用法。

        上面程序定义了一个Apple泛型类,该Apple类的泛型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。上面程序在①处将引起编译错误:类型T的上限是Number类型,而此处传入的实际类型是String类型,既不是Number类型,也不是Number类型的子类型,所以将会导致编译错误。
        在一种更极端的情况下,程序需要为泛型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。如下代码所示。

        与类同时继承父类、实现接口类似的是,为泛型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为泛型形参指定类上限,类上限必须位于第一位。

4. 泛型方法

        前面介绍了在定义类、接口时可以使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用。在另外一些情况下﹐定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,这也是可以的,Java 5还提供了对泛型方法的支持。

  (1) 定义泛型方法

        假设需要实现这样一个方法——该方法负责将一个Object数组的所有元素添加到一个Collection集合中。考虑采用如下代码来实现该方法。

        上面定义的方法没有任何问题,关键在于方法中的c形参,它的数据类型是Collection<Object>。正如前面所介绍的,Collection<String>不是Collection<Object>的子类型——所以这个方法的功能非常有限只能将object[]数组的元素复制到元素为Object (Object 的子类不行)的Collection 集合中,即下面代码将引起编译错误。

        可见上面方法的参数类型不可以使用Collection<String>,那使用通配符Collection<?>是否可行呢?显然也不行,因为Java不允许把对象放进一个未知类型的集合中。
        为了解决这个问题,可以使用Java 5提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个泛型形参。泛型方法的语法格式如下:

        把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了泛型形参声明,泛型形参声明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,所有的泛型形参声明放在方法修饰符和方法返回值类型之间
        采用支持泛型的方法,就可以将上面的 fromArrayToCollection方法改为如下形式:

 下面程序示范了完整的用法。

        上面程序定义了一个泛型方法,该泛型方法中定义了一个T泛型形参,这个T类型就可以在该方法内当成普通类型使用。与接口、类声明中定义的泛型不同的是,方法声明中定义的泛型只能在该方法里使用而接口、类声明中定义的泛型则可以在整个接口、类中使用。
        与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数如上面程序所示,当程序调用fromArrayToCollection()方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道为泛型实际传入的类型,因为编译器根据实参推断出泛型所代表的类型,它通常推断出最直接的类型。例如,下面调用代码:

        上面代码中 cs是一个Collection<String>类型,与方法定义时的 fromArrayToCollection(T[] a,Collection<T>c)进行比较——只比较泛型参数,不难发现该T类型代表的实际类型是String类型。
        对于如下调用代码:

        上面的cn是Collection<Number>类型,与此方法的方法签名进行比较——只比较泛型参数,不难发现该T类型代表了Number类型。
        为了让编译器能准确地推断出泛型方法中泛型的类型,不要制造迷惑!系统一旦迷惑了,就是你错了!看如下程序。

        上面程序中定义了test()方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参from、to 的类型都是Collection<T>这要求调用该方法时的两个集合实参中的泛型类型相同,否则编译器无法准确地推断出泛型方法中泛型形参的类型。
        上面程序中调用test方法传入了两个实际参数,其中 as 的数据类型是List<String>,而 ao的数据类型是List<Object>,与泛型方法签名进行对比:test(Collection<T> a,Collection<T> c),编译器无法正确识别T所代表的实际类型。为了避免这种错误,可以将该方法改为如下形式:

        上面代码改变了test()方法签名,将该方法的前一个形参类型改为Collection<? extends T>,这种采用类型通配符的表示方式,只要test()方法的前一个Collection集合里的元素类型是后一个Collection集合里元素类型的子类即可
        那么这里产生了一个问题:到底何时使用泛型方法﹖何时使用类型通配符呢﹖接下来详细介绍泛型方法和类型通配符的区别。

  (2) 泛型方法和类型通配符的区别

        大多数时候都可以使用泛型方法来代替类型通配符。例如,对于Java的Collection接口中两个方法定义:

        上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示。

        上面方法使用了<T extendsE>泛型形式,这时定义泛型形参时设定上限(其中E是Collection接口里定义的泛型,在该接口里E可当成普通类型使用)。
        上面两个方法中泛型形参T只使用了一次,泛型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的
        泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

         如果有需要,也可以同时使用泛型方法和通配符,如Java 的 Collections.copy()方法。

        上面copy方法中的dest和 src存在明显的依赖关系,从源List 中复制出来的元素,必须可以“丢进”目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法
        提示:简而言之,指定上限的类型通配符支持协变,因此这种协变的集合可以安全地取出元素(协变只出不进),因此无须使用泛型方法。
        当然,也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如下所示。

        这个方法签名可以代替前面的方法签名。但注意上面的泛型形参S,它仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那泛型形参S就没有存在的必要,即可以用通配符来代替S使用通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰和准确,因此 Java设计该方法时采用了通配符,而不是泛型方法。
        类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法中显式声明
 

  (3) Java 7的“菱形”语法与泛型构造器

        正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器
        一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让 Java根据数据参数的类型来“推断"泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。如下程序所示。

        上面程序中①号代码不仅显式指定了泛型构造器中的泛型形参T的类型应该是String,而且程序传给该构造器的参数值也是String类型,因此程序完全正常。但在②号代码处,程序显式指定了泛型构造器中的泛型形参T的类型应该是String,但实际传给该构造器的参数值是Double类型,因此这行代码将会出现错误。
        前面介绍过Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。如下程序所示。

        上面程序中粗体字代码既指定了泛型构造器中的泛型形参是Integer类型,又想使用“菱形”语法,所以这行代码无法通过编译。

  (4) 泛型方法与方法重载

        因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义。

        上面的MyUtils类中包含两个copy()方法这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

        上面程序中粗体字部分调用copy()方法,但这个copy()方法既可以匹配①号copy()方法,此时泛型T表示的类型是Number;也可以匹配②号copy()方法,此时泛型T表示的类型是Integer。编译器无法确定这行代码想调用哪个copy()方法,所以这行代码将引起编译错误。

  (5) Java 8改进的类型推断

        Java8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。

  • 可通过调用方法的上下文来推断泛型的目标类型
  • 可在方法调用链中,将推断得到的泛型传递到最后一个方法

如下程序示范了Java 8对泛型方法的类型推断。

        上面程序中前两行粗体字代码的作用完全相同,但第1行粗体字代码无须在调用MyUtil类的nil()方法时显式指定泛型参数为String,这是因为程序需要将该方法的返回值赋值给MyUtik<String>类型,因此系统可以自动推断出此处的泛型参数为String 类型。
        上面程序中第3行与第4行粗体字代码的作用也完全相同,但第3行粗体字代码也无须在调用MyUtil类的 nil()方法时显式指定泛型参数为Integer,这是因为程序将nil()方法的返回值作为了MyUtil类的cons()方法的第二个参数,而程序可以根据cons()方法的第一个参数(42)推断出此处的泛型参数为Integer类型。
        需要指出的是,虽然Java8增强了泛型推断的能力,但泛型推断不是万能的,例如如下代码就是错误的。

5. 擦除和转换

        在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java 代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

        当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。下面程序示范了这种擦除。

        上面程序中定义了一个带泛型声明的Apple类,其泛型形参的上限是Number,这个泛型形参用来定义Apple类的size变量。程序在①处创建了一个Apple对象,该Apple对象的泛型代表了Integer类型,所以调用a的 getSize()方法时返回Integer类型的值。当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即所有尖括号里的信息都会丢失——因为Apple 的泛型形参的上限是Number类,所以编译器依然知道b的getSize()方法返回Number类型,但具体是Number的哪个子类就不清楚了。
        从逻辑上来看,List<String>是List 的子类,如果直接把一个List对象赋给一个List<String>对象应该引起编译错误,但实际上不会对泛型而言,可以直接把一个List对象赋给一个List<String>对象,编译器仅仅提示“未经检查的转换”,看下面程序。

        上面程序中定义了一个List<Integer>对象,这个List<Integer>对象保留了集合元素的类型信息。当把这个 List<Integer>对象赋给一个List类型的list后,编译器就会丢失前者的泛型信息,即丢失 list集合里元素的类型信息,这是典型的擦除。Java又允许直接把 List对象赋给一个List<Type> (Type可以是任何类型)类型的变量,所以程序在①处可以编译通过,只是发出“未经检查的转换”警告。但对list变量实际上引用的是 List<Integer>集合,所以当试图把该集合里的元素当成String 类型的对象取出时,将引发ClassCastException异常
        下面代码与上面代码的行为完全相似。

        程序从li中获取一个元素,并且试图通过强制类型转换把它转换成一个String,将引发运行时异常。前面使用泛型代码时,系统与之存在完全相似的行为,所以引发相同的ClassCastException异常。

6. 泛型与数组

        Java泛型有一个很重要的设计原则——如果一段代码在编译时没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象
        假设Java支持创建ArrayList<String>[10]这样的数组对象,则有如下程序:

        在上面代码中,如果粗体字代码是合法的,经过中间系列的程序运行,势必在①处引发运行时异常,这就违背了Java泛型的设计原则。
        如果将程序改为如下形式:

        上面程序粗体字代码行声明了List<String>[]类型的数组变量,这是允许的;但不允许创建List<String>[]类型的对象,所以创建了一个类型为ArrayList[10]的数组对象,这也是允许的。只是把ArrayList[ 10]对象赋给List<String>[]变量时会有编译警告“[unchecked]未经检查的转换”,即编译器并不保证这段代码是类型安全的。上面代码同样会在①处引发运行时异常,但因为编译器已经提出了警告,所以完全可能出现这种异常。
        Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],因此也可以将第一段代码改为使用无上限的通配符泛型数组在这种情况下,程序不得不进行强制类型转换。正如前面所介绍的,在进行强制类型转换之前应通过instanceof运算符来保证它的数据类型。将上面代码改为如下形式(程序清单同上):

        由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器在粗体字代码处报错。

本章小结

        本章主要介绍了Java提供的泛型支持,还介绍了为何需要在编译时检查集合元素的类型,以及如何编程来实现这种检查,从而引出 Java泛型给程序带来的简洁性和健壮性。本章详细讲解了如何定义泛型接口、泛型类,以及如何从泛型类、泛型接口派生子类或实现类,并深入讲解了泛型类的实质。本章介绍了类型通配符的用法,包括设定类型通配符的上限、下限等;本章重点介绍了泛型方法的知识,包括如何在方法签名时定义泛型形参,以及泛型方法和类型通配符之间的区别与联系。本章最后介绍了Java不支持创建泛型数组,并深入分析了原因

相关文章

没对象啊?new一个咯。

现存的编成语言有很多,每种语言都有其优劣和特点。传统结构化开发面向功能去划...

Leetcode 1228.最长定差子序列

给你一个整数数组 arr 和一个整数 difference,请你找出并返回...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。