Java泛型知识学习

一、泛型出现的原因

首先,我们先看一个例子,如下:
public class ListErr {

	public static void main(String[] args) {
		//创建一个只想保存字符串的List集合
		List strList = new ArrayList();
		strList.add("Hello World");
		strList.add("Good Morning");
		strList.add("你好");
		//"不小心"把一个Integer对象"丢进"了集合
		strList.add(5);
		for(int i = 0; i < strList.size(); i++){
			//因为List里取出的全部是Object,所以必须强制类型转换
			//最后一个元素将出现ClassCastException异常
			// java.lang.ClassCastException:java.lang.Integer cannot be cast to java.lang.String
			String str = (String) strList.get(i);
		}
	}
}
 
 

上面的程序创建了一个List集合,并且只是希望该List对象保存字符串对象,但是我们没有办法添加任何的限制,所以程序中“不小心”丢进了一个Integer对象,这将导致程序发java.lang.ClassCastException异常。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常,因此导致此类错误编码过程中不易发现。通过上面程序,可以使我们了解到Java集合的两个问题:

a.集合对元素类型没有任何限制,这样可能引发一些问题:例如想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常;
b.由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它们存储的是Object类型,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException;
那么有没有什么方法解决上面的问题呢?这就是Java SE5的重大变化之一:泛型的概念。

二、泛型的含义

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会
大。泛型可以 很好的解决此问题。
泛型(Generic),即“参数化类型”。一提到参数,最能让我们联想到的就是定义方法时的形参,调用此方法时传递实参。那么参数化类型怎么理解呢?其实就
将类型由原来 的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用或调用时传入具体的类型。
对于前面的ListErr类,可以使用泛型改进这个程序:
public class GenericList {

	public static void main(String[] args) {
		//创建一个只想保存字符串的List集合
		List<String> strList = new ArrayList<String>();
		strList.add("Hello World");
		strList.add("Good Morning");
		strList.add("你好");
		//下面代码将引起编译错误
		strList.add(5);//01
		for(int i = 0; i < strList.size(); i++){
			//下面代码无须强制类型转换
			String str = strList.get(i);//02
		}
	}
}
上面程序将在01处引起编译错误,因为strList集合只能添加String对象,所以不能将Integer对象"丢进"该集合。而且程序在02处不需要进行强制类型转换,因为strList对象可 “记住”它的所有集合元素都是String类型。
JDK1.5的泛型有一个很重要的设计原则:如果一段代码在编译时系统没有产生:"[unchecked] 未经检查的转换"警告,则程序在运行时不会引发
"ClassCastException"异常。

三、深入泛型

所谓泛型:就是允许在定义类、接口时指定类型形参,这个类型形参将在声明变量、创建对象时确定(即传入实际的参数类型、也可称为类型形参)。JDK1.5
之后集合 框架中的全部类和接口都增加了泛型支持,因此可以在声明变量创建对象时传入类型形参。
1、定义泛型接口、类
接下类我们首先看一下接口List、ArrayList的部分代码:
public interface List<E> extends Collection<E> {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();
  
    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);

    boolean removeAll(Collection<?> c);
}
可以看到,在List接口中采用泛型化定义之后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型 实参
自然的ArrayList作为List的实现类,其定义形式为:
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

   
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }
	//省略
}
通过上面的介绍可以发现,我们可以为任何类增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然泛型是集合类的重要使用场所),
自定义泛型类和泛型接口与上述Java 源码中的List和ArrayList类似,如下,我们看一个简单的泛型类和方法:
public class Apple<T> {

	private T data;
	public Apple(T data){
		this.data = data;
	}
	
	public void setData(T data){
		this.data = data;
	}
	
	public T getData(){
		return this.data;
	}
	
	public static void main(String[] args) {
		//因为传给T形参的是String类型,所以构造函数只接受String类型
		Apple<String> a1 = new Apple<String>("苹果");
		System.out.println(a1.getData());
		//因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或者double
		Apple<Double> a2 = new Apple<Double>(1.23);
		System.out.println(a2.getData());
	}
}
上面程序定义了一个带泛型声明的Apple<T>类,实际使用 Apple<T> 类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>等等形式的多个逻辑子类(物理上并不存在)。
注意:当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,但调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。

2、从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类来派生子类,但值得指出的是,当使用这些接口、父类时不能再包含类型形参。例如下面代码就是错误的:
//定义类继承Apple类,Apple类不能跟类型形参
	public class A extends Apple<T>{}
正确的写法应该是:
public class A extends Apple<String>{}
如果从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String  getData()和void  setData(String data)两个方法,如果子类需要重写父类的方法,必须注意这一点。此处不再赘述。
注意:方法中的形参(这种形参代表变量、常量、表达式等数据),只有当定义方法时才可以使用形参,当调用方法(使用方法)时必须为这些数据形参传入实际的数据; 与此类似的是:类、接口中的类型形参,只有在定义类、接口时才可以使用类型形参,当使用类、接口时应为类型形参传入实际的类型。

四、类型通配符

通过上面的结论,我们可以知道List<String>和List<Object>实际上都是List类型,那现在就存在一个疑问了,因为Object是String的父类,那么List<Object>也是List<String>类型的父类吗?现在我们来验证一下:
//定义一个如此的方法
	public void test(List<Object> ol){
		for(int i = 0; i < ol.size(); i++){
			System.out.println(ol.get(i));
		}
	}
表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入实际参数值时可能不是我们所期望的,如下调用此方法:
//创建一个List<String>对象
	List<String> strList = new ArrayList<String>();
	//将strList作为参数来调用前面的test方法
	test(strList);//01
编译上面程序,将在01处发生如下编译错误:
The method test(List<Object>) in the type Apple<T> is not applicable for the arguments (List<String>)
显然得到List<Object>不是List<String>的父类,那么它们的父类是谁呢?为了表示各种泛型的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将问号作为类型实参传给泛型类,例如List<?>。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。因此可以把List<?>在逻辑上认为是List<Object>、List<String>等所有List<具体类型实参>的父类(类型通配符一般是使用?代替具体的类型实参,注意是类型实参而不是类型形参)。
虽然使用类型通配符可以适应于任何支持泛型声明的接口和类,但这种带通配符的泛型仅表示它是各种泛型的父类,并不能把元素加入到其中,例如如下代码将会引起编译错误:
List<?> c = new ArrayList<String>();
//下面程序引起编译时错误
c.add(new Object());
因为我们不知道上面程序中c的集合里元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以我们传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。如何解决这个问题呢,下面介绍的类型通配符的上限和下限将解决这个问题。
1、类型通配符上限现在有一个方法是这样的,如下:
public void test(List<?> c){
		for(int i = 0; i < c.size(); i++){
			System.out.println(c.get(i));
		}
	}
现在我们对类型实参进一步限制:只能是Number类及其子类。此时,需要用到类型通配符上限,将上面的代码修改为如下:
public void test(List<? extends Number> c){
		for(int i = 0; i < c.size(); i++){
			System.out.println(c.get(i));
		}
	}
同理,如果只能是Integer类本身及其父类呢?此时,需要用到类型通配符下限,示例代码为如下:
public void test(List<? super Integer> c){
		for(int i = 0; i < c.size(); i++){
			System.out.println(c.get(i));
		}
	}
同样可以使用类型通配符上下限设置类型形参。

五、类型的擦除

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但是为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定类型参数。如果没有为这个泛型类指定类型参数,则该类型参数被陈祚一个raw type(原始类型),默认是该声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,则所有在尖括号之间的类型信息都被扔掉了。比如说一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型变量的上限(即Object)。下面程序示范了这种擦除:
public class Apple<T extends Number> {

	private T data;
	public Apple(T data){
		this.data = data;
	}
	
	public void setData(T data){
		this.data = data;
	}
	
	public T getData(){
		return this.data;
	}
}
public class TestErasure{
	public static void main(String[] args) {
		Apple<Integer> a = new Apple<Integer>(6);  //01
		//a的getSize方法返回Integer对象
		Integer as = a.getData();
		//把a对象赋给Apple变量,会丢失尖括号里的类型信息
		Apple b = a;   //02
		//b只知道sizede类型是Number
		Number size1 = b.getData();
		//下面代码引起编译错误
		Integer size2 = b.getData();  //03
	}
}
从上面程序可知,当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即使所有尖括号里的信息都被丢失。但因为Apple的类型形参的上限是Number类,所有编译器依然知道b的getSize方法返回Number类型,但具体是Number的哪个子类就不清楚了。这就是类型的擦除。

六、话外篇

实际上,泛型对其所有可能的类型参数,都具有同样的行为,从而可以把相同的类型当成许多不同的类来处理。与此完全一致的是,类的静态变量和方法也在所有的实例间共享,所以在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参。同样还要注意的是, Java中没有所谓的泛型数组一说
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
针对泛型,最主要的还是需要理解它的思想、目的及其原理。








































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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值