Java编程思想 第十五章:泛型

清疚 2022-04-04 11:56 438阅读 0赞

1. 泛型

  1. “泛型”意思就是适用于许多类型。
  2. 使用泛型的目的之一: 指定容器持有什么类型,让编译器确保正确性,而不是在运行期发现错误。
  3. 这个容器可以看成是有其他类型对象作为成员的类,而不单单只是JDK中的容器类。

2.简单的泛型

2.1 元组

  • 元组是对象,是一个将多个对象打包存储于其中的单一对象。Java中没有元组这个类库,Python中有元组这一概念。
  • 可以通过泛型构建自己的元组类库。

    class TwoTuple{

    1. public final A first;
    2. public final B second;
    3. TwoTuple(A a, B b){
    4. first = a;
    5. second = b;
    6. }
  • 元组允许读取元素,但不能插入新元素,不可以修改元素值,因为元素被设置为final。

  • 元组可以任意长度,可以存储任何类型对象。

2.2 元组泛型的继承

  • 父类的泛型同样可以继承给子类,但要显示的写出父类的泛型

    class ThreeTuple extends TwoTuple{

    1. public final C three;
    2. public ThreeTuple(A a, B b, C c){
    3. super(a,b);
    4. three = c;
    5. }
  • 一个方法只能返回一个对象,但返回一个元组就可以包含多个对象。

    public static TwoTuple f(){

    1. return new TwoTuple<String, Integer>("hi",99);
    2. }

2.3 模拟堆栈

  • 用泛型实现一个自定义堆栈
  • 使用内部链式存储机制

    public class StackDemo {

    1. public static void main(String[] args) {
    2. LinkedStack<String> lss = new LinkedStack<String>();
    3. lss.push("please");
    4. lss.push("say");
    5. lss.push("good");
    6. String s;
    7. while ((s = lss.pop()) != null)
    8. System.out.println(s);
    9. }

    }

    class LinkedStack {

    1. //定义栈结点,结点为一个对象
    2. //结点保存元素类型使用泛型45表示
    3. private class Node<U> {
    4. U item; //要保存的数据
    5. Node<U> next; //指向下一结点的引用
    6. Node() { //默认构造器,构造空结点
    7. item = null;
    8. next = null;
    9. }
    10. //该构造器给结点赋值
    11. Node(U item, Node<U> next) {
    12. this.next = next;
    13. this.item = item;
    14. }
    15. //判断栈是否为空
    16. boolean end() {
    17. return item == null && next == null;
    18. }
    19. }
    20. //设置一个末端哨兵,该哨兵结点为空结点
    21. private Node<T> top = new Node<T>();
    22. //压栈 每次push创建一个Node结点
    23. public void push(T item) {
    24. /*第一次压栈将末端哨兵连接到该结点的next,并且top不再是末端哨兵而是第一个结点 第二次压栈将第一个结点连接在第二个结点的next上,top是第二个结点 ... 以此类推 整个栈完成 */
    25. top = new Node<T>(item, top);
    26. }
    27. //弹栈
    28. public T pop() {
    29. T result = top.item; //得到结点数据
    30. if (!top.end()) //如果本结点不是空元素,则丢弃当前top,指向下一top
    31. top = top.next;
    32. return result;
    33. }

    }

    StackDemo

3.泛型接口

泛型也可以用于接口,例如生成器,生成器是专门负责创建对象类,一般只定义一个方法。

4.泛型方法

4.1 泛型方法定义

泛型参数列表至于返回值前 如:

  • public void f(){}; 这是泛型方法;
  • public int f(T a){};这不是泛型方法,返回值前无泛型。

4.2 泛型方法

  1. 泛型方法所在的类可以是泛型类,也可以不是泛型类,并且泛型标识符可以完全不一样,也就是说泛型方法和泛型类无关。
  2. 普通static方法无法访问泛型类的类型参数,如果要是使用泛型就要定义成泛型静态方法

    public class Gy {

    1. T name;

    / public static T f(T a){ //编译不通过 return a; }/

    1. public static <T> T f(T a){
    2. return a;
    3. }
    4. public T g(T b){
    5. return b;
    6. }

    }

类的泛型要在创建对象时才确定,而类内的静态方法,静态域在类加载时初始化,因此如果使用类的泛型类型则初始化时无法知道具体类型是什么,此时使用泛型方法这样就和类的泛型无关了,这样静态方法初始化时类型只和自身的泛型相关。

  1. 使用泛型方法时编译期会通过类型参数推断来为我们找出具体类型,而不必自己声明时什么类型。
  2. 泛型方法返回值是泛型,那么就返回一个泛型,不能是具体类型,反之亦然。

    public static Set set(){

    1. // return new HashSet<String>(); //不能返回具体类型
    2. }

4.3 显式类型说明

  • 泛型就是为了适用于多种类型,而显式类型说明却指定了泛型的具体类型
  • 显式类型说明用在调用泛型方法上。
    1. 泛型方法调用后如果产生一个泛型结果,则不能将泛型结果传入另一个方法,而必须要这么做时就可以使用它显式类型说明。
    1. 在点操作符和方法名称之间插入<类型> 如果该泛型方法

        1. 和要传入方法(非静态方法)在同一个类要用 this.<>g()
        1. 是静态方法要用 类名.<>g()
        1. 通过对象调用

    public static Set set(){

    1. return new HashSet<T>();
    2. }
    3. public static void f(Set<List> stringSet){ }
    4. public static void main(String[] args) {
    5. f(Gy.<List>set());
    6. }

4.4 可变参数泛型方法

  1. public static void main(String[] args) {
  2. System.out.println(f(1,2,3,4,"juih"));
  3. }
  4. public static <T> List<T> f(T... args){
  5. List<T> result = new ArrayList<T>();
  6. for (T item : args) {
  7. result.add(item);
  8. }
  9. return result;
  10. }

4.5 生成器Generator思想

5.泛型用于匿名内部类

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

  1. import java.util.*;
  2. import net.mindview.util.*;
  3. class Customer {
  4. private static long counter = 1;
  5. private final long id = counter++;
  6. private Customer() { }
  7. public String toString() { return "Customer " + id; }
  8. // A method to produce Generator objects:
  9. public static Generator<Customer> generator() {
  10. return new Generator<Customer>() {
  11. public Customer next() { return new Customer(); }
  12. };
  13. }
  14. }

6.构建复杂模型

7. 擦除

  • jvm并不认识泛型因此需要将泛型擦除。
  • ArrayList 和 ArrayList很容易被认为是不同类型。因为他们有不同的行为,但程序却认为他们是相同的,正是因为擦除的存在。
  • 擦除的结果就是把一个对象变为它的原生类
  • 泛型只是用来检查类型正确性,泛型部分的代码不会参与运行,这是由于泛型的擦除作用。
  • 泛型代码内部无法获得有关泛型参数的类型的信息。

7.1 泛型擦除到第一个边界

  • 上界 意思就是T 只能为HasF或者其子类。
  • 泛型只是在静态类型检查期间出现来验证类型正确性,程序一但运行后泛型都将被擦除,替换成他们的非泛型上界,如List被擦除为List,List被擦除为List, 擦除为

7.2 擦除动机

擦除使得泛化的代码变得具体,因此泛化客户端可以使用非泛化类库,非泛化客户端也可以使用泛化类库。

7.3 擦除的代价

泛型不能当做一个类去操作,如Foo cat不能代表Cat这个类,因为它会被擦除为Object.

7.4 边界处的动作

泛型中创建数:Array.newInstance(类<?> componentType, int length) 并且要强制转型为T[]类型。

  1. public class Test<T> {
  2. private Class<T> kind;
  3. T[] create(int size){
  4. return (T[]) Array.newInstance(kind,size);//必须强转T[]
  5. }
  6. List<T> createList(){
  7. return new ArrayList<T>();
  8. }
  9. }
  • 边界就是对象进入和离开方法的地方,编译期执行类型检查和插入转型代码就是在边界处。
  • 编译期执行了类型检查确保了数据一致性,在离开方法时由编译器为你插入转型代码执行转型,此时转型是安全的。
  • 由于擦除kind实际被存储为Class,因此创建数组无法后知道要转型成什么类型,因此必须强转。但创建容器就不需要强转了,编译期可以保证类型的一致性,如果类型不一致不通过编译。

8.擦除补偿

  • 由于擦除存在,所以任何在 运行时 需要知道泛型代表的类型的 确切类型信息 的操作都无法工作。
  • 解决办法:引入类型标签

    • 给泛型类添加一个标签成员Class kind; 构造器传入类型参数赋值给kind,这样就得到了泛型的类型。

8.1 创建类型实例

  • 创建泛型的实例 不可以 new T() 一来因为擦除,二来因为不能确定T是否有默认构造器.

如果需要创建类型实例,就需要如下方式:

  1. 利用类型标签 可以kind.newInstance()创建对象,但遇到没有默认构造器的类如Integer,运行时就会出错,而编译期无法捕获错误。
  2. Java解决方案是传入一个显示工厂对象来创建实例,并且限制其类型。

    interface Factory{

    1. T create();

    }
    //创建显式工厂并限制类型为Integer
    class IntegerFactory implements Factory{

    1. @Override
    2. public Integer create() {
    3. return new Integer(0);
    4. }

    }

    class Widget{

    1. //创建显式工厂并限制类型为Integer
    2. public static class FactionWidget implements Factory<Widget>{
    3. @Override
    4. public Widget create() {
    5. return new Widget();
    6. }
    7. }

    }
    class Foo2 {

    1. private T x;
    2. public <F extends Factory<T>> Foo2(F factory){
    3. x = factory.create();
    4. }

    }
    public class Test{

    1. public static void main(String[] args) {
    2. // 创建Foo2对象实例 并且x为泛型的实例对象
    3. new Foo2<Integer>(new IntegerFactory());
    4. new Foo2<Widget>(new Widget.FactionWidget());
    5. }

    }

  3. 使用模板方法设计模式

8.2 泛型数组

  • 不能直接创建泛型数组 T[] array = new T[size]
  • 可以定义一个泛型数组的引用 T[ ] array ,但无法使用这个引用。
  • 解决办法
    1. 可以使用ArrayList来代替数组达到相同目的。
    1. 内部创建一个Object[ ] 数组,在需要时将数组内的对象转型为需要的类型,但不能将Object[ ]转型为T[ ],因为没有任何方式可以改变数组底层类型。

    public class Test {

    1. private Object[] array={ "ji"};
    2. public T get(int index) {
    3. return (T) array[index];
    4. }
    5. public static void main(String[] args) {
    6. Test<String> test = new Test<String>();
    7. String s = test.get(0);// String s = (String)test.get(0);编译器自动插入转型代码
    8. }

    }

其实 get内没有任何类型转换 ,T 被擦除成了Object,只是Object转Object了, 创建对象确定T类型后在编译阶段编译器会为你插入转型代码。

9. 边界

  • 作用:
    1. 强制泛型可以使用什么类型
    1. 按边界类型调用方法其方法,无边界的只能调用从Objec继承的方法。

9.1 多边界

  • A B C 之间没有继承关系
  • 多边界擦除到第一个边界A。

10. 通配符

  • 通配符可以允许某种类型向上转型,与普通边界对比:

    • List first = new ArrayList(); 只能使用T
    • List<? extends Fruit> first = new ArrayList(); //可以使用各种Fruit的子类。
    • List<? extends Fruit> 读作具有任何从Fruit继承的类型列表。

10.1 区别一个概念

  • 数组可以向上转型,即导出类型的数组可以赋值给及基本类型数组的引用
  • 而导出类型的泛型确不能赋值给基本类型泛型的引用 如: List list = new ArrayList(); 语法错误
  • Apple 是 Fruit 的子类,但 Apple所在的List却不是Fruit所在的List的子类,故不能这样转型。

10.2 上界和下界

<? extends Fruit> 上界 ?是Fruit 的子类,但具体是什么不知道,因此当调用get方法时返回的对象可以赋值给Fruit引用,而add添加对象时由于不清楚具体要添加什么子类所以无法使用add方法。 <? super Apple > 下界 也称 逆变?是Apple的父类,但具体是什么类型不得而知,因此当调用add方法添加对象时可以添加Apple和其子类对象,但调用get方法时无法确定要返回什么类型,因此不能调用get方法返回具体类型,只能返回Object。 ## 10.3 无界通配符 <?>

表示任何类型,相当于使用原生类 ,他还意味着声明 “我要在这里使用泛型”

原生类List实际上是List,表示持有Object对象的原生类,而List<?>是利用泛型给List持有的对象划了一个具体的范围,虽然范围也是Object,但List<?>确不是原生类。

下面我们来看一下List<?>和看起来等价于List之间的差异?

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

10.4 <?>与上下界之间的区别

  1. 一个方法的参数的类型如是 List ,List<?> ,则可以接收任何形式的List参数,参数是不是泛型无所谓。
  2. 参数的类型如果是List<? extends/super A > ,则只能接收泛型的List参数.
  3. 如果参数的类型是 <?> 或者 <? extends A>,则该方法无法调用
  4. <?>可以向上转型
  5. 多个泛型参数下只有全为?时编译器无法与原生类区分,但只要有过一个参数不是?就会有所区分如Map必须传入map类型的参数,而Map<?,?>可以传入new HashMap();

10.5 捕获转换

向一个<?>方法传入有个原生类型,编译器可以捕获实际类型参数,这个<?>方法调用其他方法时向其他方法传递被捕获的对象时就会传递确切类型。如 A a =new A(); 将a传入f(A<?> s)方法,f可以捕获确切类型 即s=A

11.泛型存在的问题

  1. 基本数据类型无法作为泛型的类型参数,如 T不能是int 可以使用包装器类
  2. 自动包装机制不能用于数组int[ ] 不能成为Integer[ ]
  3. 带有泛型类型参数的转换或者使用instanceof判断类型都没有任何效果
  4. 被擦除后如果产生相同的方法签名那么不允许编译。

12.自限定的类型

13.动态类型安全

14.泛型中的异常

15.混型

16.潜在类型机制

17.对缺乏潜在类型机制的补偿

发表评论

表情:
评论列表 (有 0 条评论,438人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java编程思想:内部类

    可以将一个类的定义放在另一个类的定义的内部,这就是内部类。 起初看内部类只是一种名字隐藏和组织代码的模式。当生成内部类对象时,此对象与制造它的外围对象之间有一种联系,它可以访