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中没有所谓的泛型数组一说。

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。

针对泛型,最主要的还是需要理解它的思想、目的及其原理。

时间: 2024-10-26 15:04:11

Java泛型知识学习的相关文章

Java泛型再学习

泛型是对于数据的一种规范,他限定了类.容器.方法可以接受的参数类型,避免参数类型混乱. 一.基本泛型 泛型最常见的地方就是集合,如: -- ArrayList<String>  表示这个集合中只能存放String类型的元素 -- HashMap<String, Object>  表示这个图中只能存放键为String类型,值为Object类型的元素 特别要注意的时,泛型只存在于编译阶段,在程序运行阶段,我们定义的泛型是并不存在的,这种方案叫“擦除”,示例: 1 public clas

Java泛型通配符学习 —— Java Generic&#39;s Wildcards

Java Generic's wildcards is a mechanism in Java Generics aimed at making it possible to cast a collection of a certain class, e.g A, to a collection of a subclass or superclass of A. This text explains how. 理解:Java的泛型通配符机制旨在实现集合的类型转换.例如集合A,转换为A的子类集合或

Java基础知识学习(九)

GUI开发 先前用Java编写GUI程序,是使用抽象窗口工具包AWT(Abstract Window Toolkit).现在多用Swing.Swing可以看作是AWT的改良版,而不是代替AWT,是对AWT的提高和扩展.所以,在写GUI程序时,Swing和AWT都要作用.它们共存于Java基础类(Java Foundation Class,JFC)中. AWT依赖于主平台绘制用户界面组件:而Swing有自己的机制,在主平台提供的窗口中绘制和管理界面组件.Swing与AWT之间的最明显的区别是界面组

java泛型简单学习

一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: //import java.util.List; public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("语文"); list.add("数学"); list.add(100); //编译错误 for (int i = 0; i &l

Java基础知识学习笔记(一)

理解面向对象: Java纯粹的面向对象的程序设计语言,主要表现为Java完全支持面向对象的三个基本特征:继承.封装.多态. Java程序的最小单位是类,类代表客观世界中具有某种特征的一类事物,这些类可以生成系统中的多个对象,而这些对象直接映射成客观世界的各种事物,整个Java程序由一个一个的类组成. 结构化(主张按功能把软件逐步细分,面向功能)/面向对象程序设计:(分析>设计>编程)SA/OOA > SD/OOD > SP/OOP 结构化程序设计最小的程序单元是函数,每个函数都负责

JAVA 基础知识学习笔记 名称解释

Java ee:? IDE: ? itegrity   development environment 集成开发环境 JMS:? java Message Service java   信息服务 JMX? Java Management Extensions,即Java管理扩展 JNDI:(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司 提供的一种标准的Java命名系统接口,JNDI提供统一的客户端 API,通过不同的访问提供者接口J

Java基础知识学习(八)

IO操作 5个重要的类分别是:InputStream.OutStream.Reader.Writer和File类 面向字符的输入输出流 输入流都是Reader的子类, CharArrayReader 从字符数组读取的输入流 BufferedReader 缓冲输入字符流 PipedReader 输入管道 InputStreamReader 将字节转换到字符的输入流 FilterReader 过滤输入流 StringReader 从字符串读取的输入流 LineNumberReader 为输入数据附加

java基础知识学习笔记(四)

类与对象 java是一种面向对象的开发语言.java程序是由类与对象组成的.类与对象之间有什么关系呢? 类是构造对象的蓝图或模板.由类构造对象的过程,称之为创建类的实例.可知对象就是类的一种实例或具体实现.为什么为选用java语言做开发,这种面向对象的语言对开发有什么好处? 首先,从设计上,对一个问题,你可以暂且不管它的具体实现是什么,先把它抽象成一个对象,问题中涉及到的数据,变成对象中的实例域,求解问题的方法,变成对象中的方法,这样做可以分清要对哪些数据进行操作,逻辑上比较清晰. 其次,当问题

Java基础知识学习12-常用的API-03

基本数据类型的包装类 基本数据类型如int.float.double.boolean.char等是不具备对象的特征,比如:不能调用方法,功能比较简单.为了让基本数据类型具有对象的特征,Java为每个基本数据类型都提供了一个包装类,这样就具备了对象的特征. 将字符串转为基本类型的方法  通用格式:包装类.parseXxxx(String str) int a=Integer.parseInt("123456"); System.out.println(a+1); 将基本类型转换为字符串