第15章 泛型

即使使用了接口,就要求代码必须使用特定的接口,对程序的约束也还是太强了。我们希望达到的目的是编写更通用的代码,要使代码能够应用与“某种不具体的类型”,而不是一个具体的接口或类。泛型这个术语的意思就是适用于许多许多的类型。

  • 15.1 与C++的比较

T就是类型参数,Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。

  • 15.2.1 一个元祖类库

元祖:将一组对象直接打包存储于其中的一个单一对象。

通过final关键字保证安全性,可以随心所欲的使用这两个对象,却无法改变这两个对象。

  • 15.3 泛型接口

例如生成器,是工厂方法设计模式的一种应用。

注意:基本类型无法作为类型参数,不过Java SE5具备了自动打包和自动拆包的功能,可以很方便地在基本类型和其相应的包装器类型之间进行转换。

  • 15.4 泛型方法

是否拥有泛型方法,与其所在的类是否是泛型没有关系,可以是泛型类,也可以不是泛型类。

如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更加清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数。

调用:

使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数判断。这就好像是f()被无限次的重载过。如果f()调用时传入了基本类型,那么自动打包机制就会接入其中,将基本类型的值包装为对应的对象。

  • 15.4.3 用于Generator的泛型方法

  • 15.5 匿名内部类

  • 15.7 擦除的神秘之处

输出为true。

Java泛型是使用擦除来实现的,在泛型代码内部,无法获得任何有关泛型参数类型的信息。因此List和List在运行时事实上是相同的类型。

因为泛型内部是没有类型信息的,所以要调用t.f()方法时,也是不可以的,解决方法是给定泛型类的边界,如此,就可以调用t.f().

编译器会把类型参数替换为它的擦除,上述的例子T擦除到了HasF.

如果自己去执行擦除,那么前一个例子可以简单的创建出一个没有泛型的类:

但是泛型可以返回确切的类型,而用Object的话必须强转:

  • 15.7.2 迁移兼容性

泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上届,例如,List这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。

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

  • 15.8 擦除的补偿

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

  • 15.8.1 创建类型实例

C++中可以直接创建,Java中的解决方法是传递一个工厂对象,并用它创建新的实例,最便利的工厂对象就是Class对象:

但是如果传入的是Integer.class,可以编译但是最终却catch到了异常,因为Integer没有任何默认的构造器。因此Sun建议使用显示的工厂。

或者是模板方法设计模式,下面的示例中,create()就是在子类中定义的、用来产生子类类型的对象:

  • 15.8.2 泛型数组 不能直接创建泛型数组,一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:

既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来应该能创建一个Object数组,并将其转型为所希望的数组类型,事实上这可以编译,但是不能运行:

数组将跟踪它们的实际类型,而这个类型是在数组被创建的时候确定的,因此,即使gia已经被转型为Generic[],但这个信息只存在于编译器,在运行时,它仍旧是Object数组。

gia = (Generic[]) new Object[SIZE]将报java.lang.Object; cannot be cast to [Lcom.whu.fly.Chapter15.GenericInterface.Generic的错误。而gia = new Generic[SIZE]将报Error:创建泛型数组的错误。

第一个错误原因显而易见,第二个错误原因我觉得是无法建立泛型数组,所以用了通用的Generic[]类型,但是gia中并不能放其他类型的,gia[1] = new Generic()会报错无法将Double插入到Integer中。

成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型:

rep()返回的是T[],但是尝试左右Integer[]引用来捕获,依然会报错,这是因为实际的运行时类型是Object[]。

因为有了擦除,数组的运行时类型就只能是Object[],如果我们立即将其转型为T[],那么在编译器该数组的实际类型就将丢失,而编译器可能会错误某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[]:

依然不能将gai.rep()转为Integer[],它只能是Object[]。

如果确实需要具体的类型,可以传递一个类型标记:

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

  • 15.9 边界

如15.7所讲,边界使得你可以在用于泛型的参数类型上设置限制条件,可以按照自己的边界类型来调用方法。

边界可以设置多个,但是class必须在前面,然后是接口(接口可以是多个)。

  • 15.10 通配符

数组可以由一个父类的引用来持有子类的数组,例如:

因为它有一个Fruit[]的引用,所以它没有理由不允许将Fruit对象或者任何其子类加入其中,因此,在编译器这是允许的,但是它的实际类型是Apple[],在运行时,数组机制知道它处理的是Apple[],因此会抛出异常。

在使用泛型容器时,这种“向上转型”就不允许了:

这实际上根本不是向上转型,Apple的List不是Fruit的List。

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

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

但是此时就丢失了向其中传递任何对象的能力,甚至是Object:

List,可以读作“具有任何从Fruit继承的类型的列表”,但是,这实际上并不意味着这个List将持有任何类型的Fruit,可以想象,T变成了? extends Fruit,因此不管添加什么都是不允许的(add()),因此编译器并不能了解这里需要Fruit的哪个具体子类型。

创建了一个Holder,可以将其向上转型为Holder,如果此时调用get(),它只会返回一个Fruit,这就是在给定“任何扩展自Fruit的对象”这一边界之后,它所能知道的一切了。但是此时set的参数为“? extends Fruit”,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性,所以任何set都不行。

  • 15.10.2 逆变

超类型通配符:声明通配符是由某个特定类的任何基类来界定的。

  • 15.10.3 无界通配符

无界通配符<?>,是在声明:我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。

由于泛型参数将擦除到它的第一个边界,因此List<?>看起来等价于List<Object>,而List实际上也是List<Object>–除非这些语句都不为真。List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么”。

原生的List可以持有任何类型的ArrayList,并且可以add,可以get,具体原因以后看看源码是怎么进行转变的,原生的ArrayList也能被任何类型的List持有,都可以运行,只不过会有uncheck的警告:

完全可以正常运行,可以添加,可以强制类型转换后(get()返回的是Object)调用它的方法。

<Fruit>可以是任何,限制跟普通的new ArrayList<Fruit>一样。

上面的示例中包含了各种Holder作为参数的用法,它们都具有不同的形式,包括原生类型,具体的类型参数以及无界通配符参数。

总结如下:

  1. 只要使用了原生类型,都会放弃编译期的检查,在rawArgs()中,可以将任何类型的对象传递给set(),这个对象会被向上转型为Object。
  2.  在unboundedArg()中可以看出<?>与原生类型的区别,set(Object)会报错,因为原生Holder将持有任何类型的组合,而<?>将持有某种具体类型的同构集合,因此不能只是向其中传递Object。
  3. 对于迁移兼容性,rawArgs()将接受所有Holder的不同变体而不会产生警告,unboundedArg()也可以接受所有Holder的不同变体。
  4. SubType与Supertype展示了两个不同的星位,一个是可以get()(返回的对象起码是基类),一个是可以set()(持有的是基类)。
  5. 使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数,因此,必须诸葛情况地权衡利弊,找到更适合的方法。
  6.  最后的wildSupertype()而wildSubType()却能正常运行没有理解,为什么(Holder<? extends T>, T>)能用(Holder<capture of ?>, Long),而(Holder<? super T>, T>)却不行?一个我想到的解释是如果后者也可以,那就是Holder<?>或者Holder<? extends Long>可以执行set()方法,这显然是不对的。
  • 15.10.4

捕获转换:如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法:

最后一个是根据(1.0)判断的,如果换成了1.0f,那么就会输出Float。

此处,f1()中的类型参数都是确切的,没有通配符或者边界。在f2()中,Holder参数是一个无界通配符,因此它看起来是未知的。但是,在f2()中,f1()被调用,而f1()需要一个已知参数。这里所发生的的是:参数类型在调用f2()的过程中被捕获。

  • 15.11.2 实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口:

如果从Payable的两种用法中都移除掉泛型参数,这段代码就可以编译。

  • 15.11.4 重载

由于擦除的原因,重载方法将产生相同的类型签名,因此必须提供明显有区别的方法名,f1(),f2()。

  • 15.12 自限定的类型

基类用导出类替代其参数,这意味着泛型基类变成了一种其所有导出类的公共的模板。

自限定的参数的意义:它可以保证类型参数必须与正在被定义的类相同,即这个类所用的类型参数将与使用这个参数的类具有相同的基类型。

Telegram频道已经开通,关注flyzythink,随手分享正能量,了解VPS优惠与补货
Telegram群组已经开通,加入flyzy小站,FREE TO TALK
QQ群开通:780593286 flyzy小站
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注