Java泛型学习笔记

泛型

  • 泛型实现了参数化类型的概念,使得代码可以引用与多种类型
    学习泛型的难点:了解Java泛型的局限是什么,以及为什么会有这些限制,Java泛型的边界在哪里。理解边界所在,才能成为程序高手
    Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节

缘起

ArrayList list = new ArrayList() ;
list.add(new Integer(32));
Integer num = (Integer)list.get(0);
  • Jdk1.5版本以前的写法

    list在编译期无法进行错误类型检查,因为它存储的都是Object
    如果我们想要在ArrayList中存储单一一种数据类型(例如Integer),这样的写法就会存在运行时抛出异常的危险,而泛型的主要目标之一就是要将这种错误检查上升到编译期

    public class ArrayList 泛型引入后当你想要使用ArrayList存储指定类型的数据,就要指定的类型置于尖括号内
    ArrayList list = new ArrayList() ; list.add(32); Integer num
    = list.get(0); 现在,在创建ArrayList的时候我们指定了只能存储Integer类型的数据,如果添加的数据类型不是Integer编译器会报错

泛型类

public class Holder<T>{
private T item ;
public T get(){return item ;}
public void set(T v){item = v;}
public static void main(String[] args) {
    Holder<Integer> holder = new Holder<>() ; //在使用的时候指定其类型
 }
}

泛型方法

  • 类中可以包含参数化方法,这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法与它所在的类是否是泛型没有关系。
  • 使用泛型方法的基本准则:如果使用泛型方法可以取代整个类的泛型化,那么就应该尽量使用泛型方法,因为它可以使得事情更加清楚明白

对于一个static方法而言,它无法访问泛型类的泛型参数,所以,如果static方法需要实现泛型能力,就必须使其成为一个泛型方法
在这里插入图片描述

擦除的神秘之处

  • JVM没有泛型类型对象——所有的对象都属于普通类,在泛型代码内部,无法获取任何有关泛型参数类型的信息。
    Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都会被擦除。例如:List和List在运行时事实上是相同的类型。这两个形式都会被擦除成它们的原生类型”List”
    泛型类型参数将擦除到它的第一个边界(无指定边界则擦除为Object类型)
    在这里插入图片描述
    在这里插入图片描述

    向上述例子一样,T擦除到了Object,就好像在类中声明用Object替换了T一样

擦除的问题

  • 擦除主要的正当理由是从非泛化代码到泛化代码的转换过程,在不破坏现有类库的情况下,将泛型融入到Java语言中

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

    如果你编写了如下代码:
    class Foo{T var;}
    那么,当你在创建Foo实例的时候
    Foo foo = new Foo<>();
    看起来Foo中的代码应该知道工作于Cat之上,而且泛型语法也在给出这样的强烈暗示:在整个类的各个地方,T都将会被替换为Cat,但是事实并且如此!无论何时,当你在编写这个类的时候,都必须提醒自己“它(T)只是一个Object 或者是该类的第一个边界类型”

边界处的动作

看下面的例子

public class SimpleHolder {
    private Object obj;
    public void set(Object obj){this.obj = obj;}
    public Object get(){return obj ;}
public static void main(String[] args) {
    	SimpleHolder holder = new SimpleHolder();
    	holder.set(new String("a"));
    	String str = (String) holder.get();
	}
}
public class GenericHolder<T> {
    private T t ;
    public void set(T t){this.t = t ;}
    public T get(){return  t;}
public static void main(String[] args) {
    	GenericHolder<String> holder = new GenericHolder<>();
    	holder.set(new String("a"));
    	String str = holder.get();
	}
}
  • 上面两个类,一个是非泛型类,一个是泛型类 直接从反编译两个类给出结论:
    1.对于传递进来的值,如set方法,在编译期进行检查

    2.对传递出去的值,对于泛型代码来说,虽然没有显示的给出强制类型转换,但是编译器会在返回值的地方额外插入转换语句(从非泛型类的get方法与泛型类的get方法得到相同字节码能说明这一点)

擦除的补偿

  • 擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道类型确切信息的操作都无法工作。我们可以通过传递类型标签Class
    来补偿擦除造成的类型信息丢失

    无法使用instanceof检测一个对象是否为T类型
    在这里插入图片描述

    传递类型标签进行补偿
    在这里插入图片描述

创建类型的实例

  • 创建类型实例,T var = new T()无法实现,是由于类型擦除(new
    一个Object不是我所需要的)。解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象
    在这里插入图片描述
    不过使用这种方法有一种缺陷,比如这个类型(例如Integer)没有默认构造器,那么newInstance()在运行时会报异常,但是在编译期却检查不出来

  • 最好的方法是使用显示工厂去创建泛型,看代码: 定义一个工厂,
    在这里插入图片描述
    由工厂对象为我们创建对象
    在这里插入图片描述

    //生产Integer的工厂
    在这里插入图片描述

    实例化时要求调用者传入工厂对象帮助我们创建泛型对象
    在这里插入图片描述
    Supplier 就是一个很常用的工厂类
    在这里插入图片描述

    具体使用,要求调用者传递工厂对象
    在这里插入图片描述

    Lambda表达式实现get方法
    在这里插入图片描述

创建泛型数组

  • 由于擦除导致不能创建泛型数组,一般的解决方案是在任何想要使用泛型数组的地方使用ArrayList
    如果确实需要泛型数组,那么使用Array.newInstance的方式来创建在这里插入图片描述
    一种不是很恰当的处理方法(下面有注解)
    在这里插入图片描述

    请注意!数组在创建的时候会跟踪它们的实际类型!所以上面arr数组的实际类型是Object,运行时get和set都不会发生问题,但是在调用rep方法时,你实际是将Object[]转换为Integer[],这会抛出ClassCastException,但是这些在编译期编译器检查不出来,所以尽量不要酱紫操作,使用Array.newInstance(Class<?> componentType, int length)方法创建数组,传递类型标签让它指定所需要的类型

边界

  • 边界使得你可以在用于泛型的参数类型上设置限制条件,你可以规定泛型可以应用的类型,更重要的一个效果是你可以按照给出边界类型来调用方法。Java重用了extends关键字来给泛型添加边界(C#中的约束),多个约束以’&’分隔

    Interface HasColor{ Color getColor();}
     
     class Colored<T extends HasColor> {
     T item ;
     Color color(){return item.getColor();}
     }
    

通配符

泛型不支持子类继承,看下面例子
List listInt = new ArrayList<>();
List listObj ;
//listObj = listInt ;

Integer是Object的子类,那么ArrayList 对象可以赋值给ArrayList完成所谓的向上转型吗? 不可以

假设操作可以完成
listObj = listInt ;
现在listObj 运行时的实际类型是listInt
那么进行如下操作:
listObj.add(new String(“”));往一个Integer的List里存储String显然是不行的,但是编译器将不能检测出存在的错误(object引用的List当然可以添加任意类型的数据),只有在运行时才能发现错误

结论:Object的List持有Object以及Object的子类,Integer的List持有Integer以及Integer的子类,List 不等价于List

如果想要在两个类型之间建立某种类型的向上转型关系,可以使用通配符
例如:

List<? extends Number> list <? extends T> 表示:任何T或从T继承的类型我都接受 List<? extends Number> 表示:存储任何T或从T继承的类型数据的列表

List<? extends Number> list = new ArrayList() ;
List<? extends Number> list = new ArrayList() ;
List<? extends Number> list = new ArrayList() ;在这里插入图片描述

  • 要注意的一点是使用通配符 List<? extends Number> list
    不能进行add操作,因为你压根不知道list存储的具体类型是什么,它有可能是List也有可能是List(因为它们都可以协变为List<?
    extends
    Number>),如果要是可以进行add操作那么会存在类型安全的问题(向List里面添加float类型的数据??),这样很危险
  • 查看ArrayList文档可以发现add()接受一个泛型参数,contains和indexOf接受Object类型的参数。当你指定一个ArrayList<?
    extends Number>时,add()的参数列表也变成了<? extends
    Number>。从这个描述中,编译器并不能了解传递过来的是Number的哪个具体子类,因此add方法不会接受任何类型的Number。
  • 在使用contains和indexOf是,参数类型是Object的,因此不涉及任何通配符,编译器将允许这个调用。这意味着泛型类的设计者要考虑清楚哪些调用是“安全的”。
  • 调用get方法,它只会返回Number类型,这就是在给定“任何扩展自Number的对象”这一边界后,它知道这些类型都可以向上转型到Number,所以编译器允许这样的操作。

逆变

可以声明通配符是由某个特定类的任何父类来界定“<? super T>”限定为任何T或T 的父类对象这一边界
在这里插入图片描述
<? super Number>限定参数是Number或者Number的某种子类型,这样就可以知道向其中添加Number或Number的子类型是安全的
添加Object是不安全的,Object是Number的父类,如果可以添加Object的话,那么这个口子是敞开的,这个Object里面存的可以会是其他的一些类型。

无界通配符

无界通配符<?>意味着”任何事物”
编译器很少关心使用的是原生类型还是<?>

<?>可以看作是一中装饰,但是它任然是有价值的,因为,实际上它在声明“我是想用Java泛型来编写这段代码,而不是使用原生类型” <?>的泛型参数可以持有任何类型

无界通配符的一个重要应用。当你在处理多个泛型参数的时候,有时允许个别参数是任何类型的

List 和 List<?>
List表示 ”持有Object类型的原生List”
List<?>表示 ”持有某种特定类型的非原生List,只是不知道是哪种具体的类型”

public class Holder<T> {
    T item ;
    public Holder(){}
    public Holder(T item){this.item = item ;}
    public void set(T val){item = val ;}
    public T get(){return item ; }
}



public static void rawArgument(Holder holder,Object arg){
    /*
    * 编译器知道Holder是一个泛型类,尽管这里被表示成一个原生类型,
    * 编译器任知道向set方法传递一个对象是不安全的,因此抛出警告
    * Warning: unchecked call to set(T)
    * */
    holder.set(arg);
    //the type information has been lost
    Object obj = holder.get();
}

static void testMethod(){
    Holder<Long> holder = new Holder<>() ;
    long ln = 1l ;
    rawArgument(holder, new Object());
    /*
    * 这里会抛出异常 ClassCastException
    * 尝试把object对象赋值给long类型变量
    * rawArgument方法中的holder.set方法是危险的
    * */
    ln = holder.get() ;
    System.out.println(ln);
}

因此,只要使用了原生类型,都会放弃编译器检查

unboundedArg 和 rawArgument 给出了Holder 和 Holder 的区别!

public static void unboundedArg(Holder<?> holder,Object arg){

        /*
        * 这里调用holder的set方法会直接报错而不是警告
        * 请对照rawArgument 方法
        * */
//        holder.set(arg);
        //the type information has been lost
        Object obj = holder.get() ;
    }

因为,原生的Holder将可以持有任何类型!而Holder 只能持有某种特定类型

问题

使用Java泛型时会出现的各类问题
1.任何基本类型都不能作为类型参数
解决方法时使用它们的包装类
2.实现参数化接口(基类型劫持了接口)
一个类不能实现同一个泛型接口的两种不同变体,由于擦除的原因,这两个变体会称为相同的接口,看代码:
在这里插入图片描述
有趣的是,如果Payable都移除掉泛型参数(像编译器在擦除阶段所作的那样),这段代码就可以编译
在这里插入图片描述

这个问题在某些时候十分令人恼火,例如Comparable接口!

看下面的代码

假设我有一个pet类,它可以和其他pet对象进行比较
对与Pet的子类型进行比较的类型窄化是有意义的
假如,我希望Cat对象进行比较
在这里插入图片描述
这段代码将不能工作

自限定的类型

class SelfBounded<T extends SelfBounded>{}
SelfBounded类接受泛型参数T,这个T有一个边界,限制这个T必须是继承自SelfBounded本身

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值