JAVA泛型学习笔记


前言

这几天重新回来复习泛型,顺便记录总结一下在学习过程中遇到的重要知识点。


一、为什么要使用泛型?

我们知道ArrayList是一个带有泛型参数的的类,但我们用不带泛型参数的原始类型去操作数据时,可以添加任何类型,编译跟运行都没有报错,但真正对外get值的时候,却出行类型转化异常:

static void test() {
        ArrayList list = new ArrayList();
        list.add("string");//添加字符串
        list.add(1);//添加int类型
        list.add(true);//添加booleanleixng
       for (int i = 0; i < list.size(); i++) {
           String item = (String) list.get(i);//强转成String
           System.out.println(item);//输出
       }
    }

报错:

string
Exception in thread "main" java.lang.ClassCastException: 
java.lang.Integer cannot be cast to java.lang.String
	at threadDemo.ThreadOperation.test(ThreadOperation.java:44)
	at threadDemo.ThreadOperation.main(ThreadOperation.java:35)

所以一般都会用限制类型的泛型,如下:

  ArrayList<String> list = new ArrayList<>();
        list.add(1);//编译器报错
        list.add(true);//编译器报错

另外下面的泛型方法可以传入任何非null的类型,具有极大的代码复用效果:

 //测试
public class Test {
    //可以用一个方法代替下面3个方法
    private static <T extends Number>void  setNum( T t){
    }
    
    private static void  set(int t){
    }
    private static void  set(double t){
    }
    private static void  set(long t){
    }
}

我们得出两点结论:

1、泛型具有提前在编译期进行代码检查的作用,防止运行期出现类型转化异常。

2、泛型具有被很多不同类型的对象所重用的优点

二、定义简单的泛型

1、泛型类

泛型类就是在类名后用< T >代表引入的类型,可以用多个字母,表示多个引用类型,如< T,U >等。 引入类型可以修饰成员变量、局部变量、参数、返回值。
public class Test<T,U> {
    //修饰成员变量
    private T t;
	private U u;
	
    //修饰返回值
    public T getT() {
        return t;
    }

    //修饰参数
    public void setU(U u) {
        //修饰局部变量
        U u1 = u;
        this.t = u;
    }
}

泛型类的使用:

        Test<String,String> test = new Test<>();
        test.setU("haha");
        System.out.println(test.getT());

2、泛型方法

该方法可以在普通类、泛型类中使用,< T >在修饰符后,返回值前如下:

//泛型方法
public class Test<T>{
	//注意这里的T与类
    public <T> T getTestStr(T t) {
        System.out.println("== do work ==");
        return t;
    }
}

泛型方法使用:

        Test test = new Test();
        //<String>在JDK 1.7后可以省略<String>,虚拟机会自动推断类型
        System.out.println(test.<String>getTestStr("String"));

要注意泛型方法跟普通方法的区别。
我的记法是泛型方法在定义的时候有尖括号< T >

	//泛型方法。 T 跟泛型类无关
    public <T> T getSomeThing(T t) {
        return t;
    }
    //普通方法。 T 为泛型类定义
    public T getSomeThing1(T t) {
        return t;
    }

2、泛型接口

泛型接口与泛型类相似,只是在实现时指定泛型类型,也可以不指定类型。
java中的List< T > 的源码也是泛型接口:

	public interface List<E> extends Collection<E> {
    int size();

    boolean isEmpty();
    ...//省略其他方法
    }

其中List< T >在ArrayList< T >中使用:

	public class ArrayList<E> extends AbstractList<E> implements List<E>, 
		RandomAccess, Cloneable, Serializable {
		...//省略其他方法
   		public ArrayList(int var1) {
	    }
	    ....//省略其他代码
    }

三、泛型的限定与类型擦除

1、泛型的限定

对范型变量的范围进行限制,格式< T extends XXX>,其中T表示绑定类型的子类型,XXX为绑定类型,绑定类型XXX可以是类,可以是接口。绑定类型可以有多个,且用&符号隔开。
由于Java的单继承,限定类型中只能有一个类,且必须放在限定列表中的第一个,多个接口放在类后面:

//泛型的限定 其中HashMap 属于类必须放到第一位,接口排在它后面
public class Test<T extends HashMap & Serializable & Cloneable & Runnable> {
}

如果多个参数类型,则用逗号(,)分隔开:

//泛型的限定
public class Test<K extends Object & Serializable & Cloneable, V extends Object & Runnable> {
    private HashMap<K,V> hashMap;
}

使用泛型的限定,注意传进去的类型必须为所限定类的本身或者子类,并且要满足全部限定的类,如下面的Test中传入的类型Date 必须同时满足为Serializable跟Cloneable的子类:

		//java.util.Date 为Serializable与Cloneable子类编译通过
        Test test = new Test<Date,Thread>();
        //Double 不是Serializable的子类,编译不通过
        Test test1 = new Test<Double,Thread>();

我们总结为:被限定的类型参数必须为限定类的子类或其本身,否则编译不会通过。

2、泛型的类型擦除

由于虚拟机不支持泛型类型对象,编译期会把所有泛型擦除成普通变量,规则为:
存在一个或多个限定类型,就擦除为第一受限类型。
无限定类型则擦除为Object。
我们可以分析下面的字节码文件来验证:

//定义一个类型参数为T,无限制参数类型
public class Test<T> {
    private T t;
    public void setT(T t) {
        this.t = t;
    }
}

让TestThread继承Test:

//继承Test,并且继承参数类型为自定义的Thread
//另外TestThread类又定义了K,V两个参数类型,其中K的限制参数类型为 ArrayList,V无限定类型。
public class TestThread<K extends ArrayList,V> extends Test<Thread> {

    private K t;
    private V v;

    public void setT(Thread thread) {
        super.setT(thread);
    }
}

对TestThread编译后的字节码分析如下:

// class version 51.0 (51)
// access flags 0x21
==签名信息==
// signature <K:Ljava/util/ArrayList;V:Ljava/lang/Object;>LthreadDemo/Test<Ljava/lang/Thread;>;
// declaration: threadDemo/TestThread<K extends java.util.ArrayList, V> extends threadDemo.Test<java.lang.Thread>
public class threadDemo/TestThread extends threadDemo/Test {

  // compiled from: TestThread.java

  private Ljava/util/ArrayList; t     ==t的第一受限参数类型为ArrayList==


  private Ljava/lang/Object; v        ==v无受限函数,所以擦除为Object==

  // access flags 0x1
  public <init>()V
  /....省略
  // access flags 0x1
  public setT(Ljava/lang/Thread;)V   
   /....省略
  // access flags 0x1041
  public synthetic bridge setT(Ljava/lang/Object;)V  ==由于java希望方法能够具有多态==
  											 		 ==则需要解决类型擦除与多态的冲突==
  								 					 ==这里就生成了桥方法==

}

另外对于程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

		ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("string");
        //编译器将下面的调用分为两条虚拟机指令。
        // 1.对原始方法ArrayList.get(int index)的调用。
        // 2.将返回的Object类型,请强转成String类型。
        String string = arrayList.get(0);

类型擦除的总结:

  1. 虚拟机中没有泛型,只有普通类和方法。
  2. 所有类型参数都用它们的限定类型替换。
  3. 桥方法被合成用来保持多态。
  4. 为了保持类型安全,必要时插入类型转化。

四、泛型的约束与限制

当然泛型也存在不足,表现为以下几点:

1、不能用基本类型实例化类型参数

泛型不能传入基本数据类型,因为类型擦除后,无限定参数类型只存在Object类,而Object不能存储基本数据类型的值。如:

		//int 为基本数据类型,编译错误
        List<int> lists1=new ArrayList<>();
        //boolean 为基本数据类型,编译错误
        List<boolean> lists2=new ArrayList<>();
        //正确为使用包装类:
        List<Integer> lists3= new ArrayList<>();
        List<Boolean> lists4= new ArrayList<>();

2、运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型,因此,所有运行时类型查询只存在原始类型。即不能使用 instanceof 来进行判断运行时对象类型。如:

if (a instanceof ArrayList< String >)//error
if (a instanceof ArrayList< T >)//error
if (a instanceof ArrayList)//正确
ArrayList< String > list=(ArrayList< String >)a//warning 

3、不能创建参数化类型的数组

由于数组是一个协变类型,比如说数组中的元素A继承于B,那么数组A也继承于数组B,但带有参数类型A与带有参数类型B的类之间没有任何关系的:

		//下面两个类没有继承关系
		new ArrayList<Son>();
        new ArrayList<Father>();
		//error 不允许实例化泛型数组
		List<String>[] arrayList = new ArrayList<>()[10];

但是定义成员变量又是允许的:

public class Test{
    //定义参数类型数组是允许的 我也不知道这样定义有啥用=。=
    private ArrayList<String>[] arrayList;
}

4、可变参数警告

由于java不能创建泛型数组,但是如果我们想在可变参数的方法中传入泛型呢?
如下:

	private <T> void setName(T... ts) {
        for (T t : ts) {
            //todo
        }
    }

答案是可以的,只是这里编译器会发出警告,需要用@SuppressWarnings或者java SE 7 中的@SafeVarargs来标注。

5、不能实例化类型变量

如:

			T t =new T();//不允许 编译错误

6、 不能构造泛型数组

由于泛型在运行时具有类型擦除机制,虚拟机只知道存储的对象为Object而已,而对于数组Array来说,必须知道持有对象的具体类型,而泛型擦除机制却违反了数组的安全检查原则。

	public static <T extends Serializable> T[] getT(){
        	return new T[2]; //不允许
    }

7、 泛型类的静态上下文中类型变量无效

public class Test<T>{
    //不允许
    private static T instance;
    //不允许
    public static T getInstance(){
        //...
        return instance;
    }
}

8、 不能抛出或捕获泛型类的异常实例

 	public static <T extends Throwable> void test(){
        try {
            //...
        }catch (T t){//不允许
            throw t;
        }
    }

9、 类型擦除后引发的方法冲突

public class Test<T> {
    /**编译器报错:'equals(T)' in 'com.enjoy.entity.bag.Test'
     * clashes with 'equals(Object)' in 'java.lang.Object';
     * both methods have same erasure, 
     * yet neither overrides the other
     */
    public boolean equals(T t){
    }
}

意思是此处的equals方法与object中的equals具有相同的类型擦除,彼此都不能重写对方,产生冲突。解决方法只能修改方法名。
其实泛型的大多数限制都输由于类型擦除引起的

五、通配符类型

1、通配符概念

泛型中,问号?符号被称为通配符,是用来解决固定泛型类型的约束性,它允许类型参数变化。
只能用在适用范围有:
1、参数类型
2、字段类型
3、局部变量类型
4、返回值类型


public class Test<T> {
	 //字段类型
    private List<? extends Serializable> list;
   	//参数类型 、返回值类型
    public List<? extends Serializable>  test1(List<? extends Serializable> list){
        //局部变量类型
        List<? extends Serializable> list2=null;
        return list2;
    }
}

2、上限界定符

< ? extend X> 被称为上限界定符,只能匹配X及其子类。只能从从泛型类中读取数据,并且不能写入,(是PECS原则中的生产者,Producer Extends,往外输出东西,所以你只能获取(get),不能够提供它)

class Grandfather{

}
class Father extends Grandfather{

}
class Son extends Father{

}
public class Test {

    private static void  set(List<? extends Father> list){
        //只提供安全的访问,不允许修改
        list.get(0);//允许
        list.set(new Son());//不允许
    }

    public static void main(String[] args) {
        List<Grandfather> grandfathers =new ArrayList<>();
        List<Father> fathers =new ArrayList<>();
        List<Son> sons =new ArrayList<>();
        //限制为Father及其子类Son ,Grandfather编译器报错
        set(sons);//允许
        set(fathers);//允许
        set(grandfathers);//不允许
    }
}

3、下限界定符

< ? superX> 被称为下限界定符,只能匹配X及其超类。只能从从泛型类中写入数据,并且不能读取,(是PECS原则中的消费者,Comsumer Super,它要消费东西,所以你只能提供(set),不能跟他抢)。

public class Test {

    private static void  set(List<? super Father> list){
        //只提供安全的修改,不允许安全的访问
        list.add(new Son());//允许
        Father object = (Father) list.get(0);//不允许,需要强转,但存在强转异常
        
    }

    public static void main(String[] args) {
        List<Grandfather> grandfathers =new ArrayList<>();
        List<Father> fathers =new ArrayList<>();
        List<Son> sons =new ArrayList<>();
        //限制为Father及其父类Grandfather ,子类Son编译器报错
        set(sons);//不允许
        set(fathers);//允许
        set(grandfathers);//不允许
    }
}

4、无限定通配符

< ? > 为无限定通配符,表示任意通配符,不能Get/Set。

public class Test {

    private static void  set(List<?> list){
        //既不提供安全的修改,也不允许安全的访问
        list.add(new Son());//不允许
        Father object = list.get(0);//不允许

    }

    public static void main(String[] args) {
        List<Grandfather> grandfathers =new ArrayList<>();
        List<Father> fathers =new ArrayList<>();
        List<Son> sons =new ArrayList<>();
        //匹配任意类型
        set(sons);//允许
        set(fathers);//允许
        set(grandfathers);//允许
        set(new ArrayList<>());//允许
    }
}


总结

梳理学习要点还是挺不容易的,最后的总结想用一个问题来结束:

Java中的泛型是什么 ? 使用泛型的好处是什么?
答:泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。
泛型还是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。

参考《java核心技术卷1》第八章泛型技术设计

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值