第十五章笔记——泛型

记录一下好的描述以及不知道的问题(会附带书中页码)


总述
  • (P352)为什么要解决泛型:
       ①
    普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大;
       ②而拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中;
       ③即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
  • (P352)泛型实现了参数化类型的概念,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。
  • (P352)编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。
  • (P353)在学习Java语言时,泛型是一个很有益的补充:因为你在实例化一个类型参数时,编译器会负责转型并确保类型的正确性。
  • (P354)泛型的目的:用来约定容器要存储(或者称之为持有)什么类型的对象,并且通过编译器确保规约得以满足。
  • (P354)Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。

简单的泛型
  • (P354)为什么要有元组:有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。
  • (P354)元组的概念:它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
       1.元组的性质:
         ①元组可以具有任意长度
         ②元组中的对象可以是不同类型的
       2.如何使用元组:
         使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可
// generics/TupleTest.java
import onjava.*;
public class TupleTest {
    static Tuple2<String, Integer> f() {
        // 47 自动装箱为 Integer
        return new Tuple2<>("hi", 47);
    }

    static Tuple3<Amphibian, String, Integer> g() {
        return new Tuple3<>(new Amphibian(), "hi", 47);
    }

    public static void main(String[] args) {
        Tuple2<String, Integer> ttsi = f();
        System.out.println(ttsi);
        // ttsi.a1 = "there"; // 编译错误,因为 final 不能重新赋值
        System.out.println(g())
    }
}
/* 输出:
 (hi, 47)
 (Amphibian@1540e19d, hi, 47)
 */

泛型接口
  • (P358)泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。实际上,这是 工厂方法 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。
    (关于 工厂模式推荐一篇文章,很生动形象 https://www.cnblogs.com/toutou/p/4899388.html
  • (P360)Java 泛型的一个局限性:基本类型无法作为类型参数。不过 Java 5 具备自动装箱和拆箱的功能,可以很方便地在基本类型和相应的包装类之间进行转换。

泛型方法
  • (P361)泛型方法使用的一个基本的指导原则:请“尽可能”使用泛型方法。
  • (P362) static 方法无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
  • (P362)类型参数推断(type argument inference): 对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。
       作用范围: 只对赋值操作有效

擦出的神秘之处

擦除是泛型学习中特别需要注意的一个地方,当一下遇到时希望着重思考

  • (P373)首先思考下面的问题
// generics/ErasedTypeEquivalence.java

import java.util.*;

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);
    }

}
/* Output:
true
*/

ArrayListArrayList 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList 中放入一个 Integer,所得到的行为(失败)和向 ArrayList 中放入一个 Integer 所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。
所以可以看出一个残酷的现实是:
   在泛型代码内部,无法获取任何有关泛型参数类型的信息。
   Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List 和 List 在运行时实际上是相同的类型。它们都被擦除成原生类型 List。

  • (P376)不能在某些重要的地方使用泛型类型。因为泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object。
  • (P376)擦除的核心动机:你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为 “迁移兼容性”
  • (P376)为什么要设计擦除?
        答:因为要支持向后兼容性和迁移兼容性。
  • (P376)何时会擦除: 泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,
  • (P377)继承泛型的类可以不用实现泛型。
  • (P378)边界: 即对象进入和离开方法的地点。(下面给你来个人话)这些正是编译器在编译期执行类型检查并插入转型代码的地点。
  • (P378)
// generics/ListMaker.java

import java.util.*;

public class ListMaker<T> {
    List<T> create() {
        return new ArrayList<T>();
    }

    public static void main(String[] args) {
        ListMaker<String> stringMaker = new ListMaker<>();
        List<String> stringList = stringMaker.create();
    }
}

List create()方法中返回一个新的ArrayList() ,<T>虽然会被擦出,但是不可以丢,原因有两点:
   ①编译器会发出警告
   ②留着的话,编译器仍可以确保方法或类中使用的类型的内部一致性

-(P380)泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。记住:“边界就是动作发生的地方”。


擦除的补偿

既然使用泛型会发生擦除现象,那么该如何解决擦除带来的影响呢呢?

  • (P381)首先来看下面的代码:
public class Erased<T> {
    private final int SIZE = 100;

    public void f(Object arg) {
        // error: illegal generic type for instanceof
        if (arg instanceof T) {
        T var = new T();// error: unexpected type
        T[] array = new T[SIZE];// error: generic array creation     
        T[] array = (T[]) new Object[SIZE];// warning: [unchecked] unchecked cast
    }
}

new T() 是行不通的,原因有两个:
    ①部分原因是由于擦除
    ②部分原因是编译器无法验证 T 是否具有默认(无参)构造函数
那如何解决呢?
   方法有两个:
     ①传递工厂对象(建议使用显示的工厂)
     ②模板方法设计模式

  • (P384)成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。
  • (P385)由于擦除,数组的运行时类型只能是 Object[] 。 如果我们立即将其转换为 T[] ,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 Object[] ,并在使用数组元素时向 T 添加强制类型转换。
       原因: 数组的底层数据结构只能是Object[ ]
  • (P391)首先提到一点就是关于协变的概念:
    (引用https://www.cnblogs.com/en-heng/p/5041124.html
    强烈推荐大家有兴趣可以看看这个博主写的)
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
根据那个博主的文章,引用一些笔记中可以用到的观点:
  	① extends确定了泛型的上界
  		 super确定了泛型的下界
  	②<? extends >实现了泛型的协变
  	   eg:List<? extends Number> list = new ArrayList<<Integer>Integer>();
  	 <? super >实现了泛型的逆变
  	   eg:List<? super Number> list = new ArraList<<Object>Object>();
  	③ 泛型是不变的
  	  数组是协变的
  • (P396)List 实际上表示“持有任何 Object 类型的原生 List ”,而 List<?> 表示“具有某种特定类型的非原生 List ,只是我们不知道类型是什么。
  • (P399)有一种特殊情况需要使用 <?> 而不是原生类型,那就是捕获转换。
      定义:如果向一个使用 <?> 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。
      举例:
// generics/CaptureConversion.java
public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static void f2(Holder<?> holder) {
        f1(holder); // Call with captured type
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<>(1);
        f1(raw);
        // warning: [unchecked] unchecked method invocation:
        //     f1(raw);
        f2(raw); // No warnings
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic); // No warnings
        // Upcast to Holder<?>, still figures it out:
        Holder<?> wildcarded = new Holder<>(1.0);
        f2(wildcarded);
    }
}
/* Output:
Integer
Integer
Object
Double
*/

f1() 中的类型参数都是确切的,没有通配符或边界。在 f2() 中,Holder 参数是一个无界通配符,因此它看起来是未知的。但是,在 f2() 中调用了 f1(),而 f1() 需要一个已知参数。这里所发生的是:在调用 f2() 的过程中捕获了参数类型,并在调用 f1() 时使用了这种类型。
   你可能想知道这项技术是否可以用于写入,但是这要求在传递 Holder<?> 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 f2() 中返回 T,因为 T 对于 f2() 来说是未知的。捕获转换十分有趣,但是非常受限。


问题
本节记录Java泛型在使用时会出现的5种问题
  • 任何基本类型都不能作为类型参数 (P400)
  • 实现参数化接口: 一个类不能实现同一个泛型接口的两种变体 (P401)
  • 转型和警告: 使用带有泛型类型参数的转型或 instanceof 不会有任何效果。 (P402)
  • 重载: 由于擦除导致特定重载方法产生了相同的类型签名 (P403)
  • 基类劫持接口: 一旦为接口确定了其泛型的参数,那么任何实现类实现接口中的任意方法的形参只能是确定好的参数。(P404)
      虽然第一条提到不能将基本类型用作类型参数。因此,不能创建 ArrayList<int>之类的东西。但还是有解决方法的:
       解决方法是使用基本类型的包装器类以及自动装箱机制。
    (注意:自动包装机制不能应用数组)

自限定的类型

先来看一下什么叫做自限定的类型(定义):

class SelfBounded<T extends SelfBounded<T>> { // ...

SelfBounded 类接受泛型参数 T,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的 SelfBounded。

  • (P405)从简单的了解下:看一看古怪的循环泛型(GRG)
class GenericType<T> {}

public class CuriouslyRecurringGeneric
  extends GenericType<CuriouslyRecurringGeneric> {}

定义: 指类相当古怪地出现在它自己的基类中这一事实。
  结果: 它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 Object 的类型。(因为Java 中的泛型关乎参数和返回类型)
  本质: 基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。
下面举一个例子:

// generics/BasicHolder.java

public class BasicHolder<T> {
    T element;
    void set(T arg) { element = arg; }
    T get() { return element; }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}


// generics/CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}
public class CRGWithBasicHolder {
    public static void main(String[] args) {
        Subtype st1 = new Subtype(), st2 = new Subtype();
        st1.set(st2);
        Subtype st3 = st1.get();
        st1.f();
    }
}
/* Output:
Subtype
*/

注意,这里有些东西很重要:新类 Subtype 接受的参数和返回的值具有 Subtype 类型而不仅仅是基类 BasicHolder 类型。 依旧其的本质可知:在Subtype 中,传递给 set() 的参数和从 get() 返回的类型都是确切的 Subtype。

  • (P406)自限定的步骤: 强制泛型当作其自身的边界参数来使用。
    例如:
    class A extends SelfBounded<A>{}
        自限定的参数的意义: 它可以保证类型参数必须与正在被定义的类相同。
        自限定的使用范围: 这能强制作用于继承关系(类继承与泛型方法继承)。
        自限定的意义:
           1.提高了可读性,更加明确使用的对象.更方便调用.
           2.提高了安全性,防止对象的转换出错。
           3.提高效率,不然如果定一下成Object的话.还需要进行一步强转在继续使用(Object可能会进行装包、拆包或强转操作)
           4.产生协变参数类型: 方法参数类型会随子类而变化

动态类型安全
  • (P409)Java 5 的 java.util.Collections 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:
    checkedCollection() 、checkedList()、 checkedMap() 、 checkedSet() 、checkedSortedMap()和 checkedSortedSet()。
    这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
  • (P410)好处: 如果使用受检查的集合,就可以发现谁在试图插入不良对象。

异常
  • (P410)泛型应用于异常的2个局限:
       (1)catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。
       (2) 泛型类也不能直接或间接继承自 Throwable(这将进一步阻止你去定义不能捕获的泛型异常)
  • (P410)泛型会在哪里使用异常: 类型参数可能会在一个方法的 throws 子句中用到。这使得你可以编写随检查型异常类型变化的泛型代码

混型
  • (P412)最基本概念: 混合多个类的能力,以产生一个可以表示混型中所有类型的类。
    (P412)价值之一: 它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点面向切面编程 (AOP) 的味道,而切面经常被建议用来解决混型问题。
  • (P414)装饰器模型介绍: https://www.cnblogs.com/jzb-blog/p/6717349.html#commentform
    (P414)装饰器与混型的区别: 装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。

潜在类型机制
  • (P417)在泛型类型接口上执行操作的缺陷: 还是正如你所见到的,当要在泛型类型上执行操作(即调用 Object 方法之外的方法)时,就会产生问题。擦除强制要求指定可能会用到的泛型类型的边界,以安全地调用代码中的泛型对象上的具体方法。这是对“泛化”概念的一种明显的限制,因为必须限制你的泛型类型,使它们继承自特定的类,或者实现特定的接口。在某些情况下,你最终可能会使用普通类或普通接口,因为限定边界的泛型可能会和指定类或接口没有任何区别。
  • (P417)代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。
  • (P417)潜在机制在Pyhton和C++上的区别: 支持潜在类型机制的语言包括 Python、C++、Ruby、SmallTalk 和 Go。Python 是动态类型语言(几乎所有的类型检查都发生在运行时),而 C++ 和 Go 是静态类型语言(类型检查发生在编译期),因此潜在类型机制不要求静态或动态类型检查。
  • (P418)Java使用擦除的泛型实现有时被称为“第二类泛型类型。”

总结
  • (P431)泛型解读: 它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值