欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java的泛型

程序员文章站 2024-03-15 12:32:59
...

一、泛型入门

  • Java5以后引入了“参数化类型(parameterized type)”的概念,允许在程序创建的时候指定集合元素的类型。Java的参数化类型被称为泛型(Generic)。
import java.util.ArrayList;
import java.util.List;

/**
 * @author QuLei
 *测试简单的泛型使用
 */
public class GenericList {
	public static void main(String[] args) {
		List<String> strList = new ArrayList<>();   //    (1)
		strList.add("测试简单的泛型使用");
		strList.add("这个List只能存放String");
		//这句代码会引起编译错误
		//strList.add(5);    (2)
		strList.forEach(str -> System.out.println(str.length()));   //(3)
	}
}

 

  • 使用泛型

         创建这种特殊的集合的方法是:在集合接口、类后增加尖括号,尖括号里面放一个数据类型,即表示这个接口、集合类只能保存特定类型的对象。注意(1)处是泛型的声明,它指示strList不是一个任意的List,而是一个String类型的List,写作:List<String>。在(2)处只能添加String类型的对象。在(3)处使用对象时不需要做强转。因为strList对象就可以“记住”它的所有元素的都是String类型。

  • Java7泛型的“菱形”语法 

        从Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即可以写成   List<String> strList = new ArrayList<>;把两个尖括号并排放在一起就非常像一个菱形,这种语法就被称为”菱形”语法。

二、深入泛型

       所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型参数,这就在前面的程序中看到的List<String>和ArrayList<String>两种类型。

  • 定义泛型接口、类

下面是Java5改写后的List接口、Iterator接口、Map接口的代码片段

public interface List<E>{
	//该接口里,E可以作为类型使用
	//下面的方法可以使用E作为参数类型使用
	void add(E x) ;
	Iterator<E> iterator();    //(1)
}

/**
 * @author QuLei
 *
 * @param <E>  定义接口时指定了一个类型形参,该参数名为E
 */
public interface Iterator<E>{
	//该接口内E完全可以作为类型使用
	E next();
	boolean hasNext();
}

/**
 * @author QuLei
 *
 * @param <K>  定义接口时定义两个类型参数名为 :K、V 
 * @param <V>
 */
public interface Map<K,V>{
	Set<K> keySet() //(2)
	V put(K key,V value)
}

       上面三个接口声明是比较简单的,除了尖括号中的内容--这就是泛型的实质:允许在定义接口、类时声明类型参数,类型形参在整个接口、类内可以当成类型使用,几乎所有有可能使用普通类型的地方都可以使用这种类型形参。 除此之外,(1)(2)处方法声明返回值类型是Iterator<E>、Set<K>,这表明Set<K>形式是一种特殊的数据类型,是一种与Set不同的数据类型--可以认为是Set类型的子类。

       也可以为任何的类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要的使用场所)。下面自定义一个类,这个类就包含了一个泛型声明。

package com.gen;

public class Person<T> {
	private T info;

	public Person(T info) {
		super();
		this.info = info;
	}

	public Person() {
		super();
		// TODO Auto-generated constructor stub
	}

	public T getInfo() {
		return info;
	}

	public void setInfo(T info) {
		this.info = info;
	}
	
	
	public static void main(String[] args) {
		//这里传入T形参是String,所以构造器参数只能是String
		Person<String> person = new Person<>("测试普通Java类的泛型参数");
		System.out.println(person.getInfo());
		//这里传给T的是Double,所以构造器参数只能是double或Double
		Person<Double> person2 = new Person<>(3.14);
		System.out.println(person2.getInfo());
	}
}

     当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如Person<T>类定义构造器,其构造器名依然是Person,而不是Person<T>!调用该构造器时却可以使用Person<T>的形式,当然应该为T形参传入实际的类型参数。

  • 从泛型类派生子类

       当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类不能再包含类型形参。例如,下面的代码就是错的。

//定义类A继承Person类,Person类不能跟类型形参
public class A extends Person<T>{}

如果想从Person类派生一个子类,则可以改为如下代码:

//使用Person类时为T形参传入String类型
public class A extends Person<String>{}
  • 并不存在泛型类

      前面提到可以把ArrayList<String>类当成ArrayList的子类,事实上,ArrayList<String>类也确实像一种特殊的ArrayList类:该ArrayList<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayLIst<String>生成class文件,而且也不会把ArrayList<String>当成新类来处理。

                List<String> l1 = new ArrayList<String>();
		List<Integer> l2 = new ArrayList<>();
		System.out.println(l1.getClass() == l2.getClass());

运行上面的代码结果为true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。下面岩石错误的操作。

public class R <T>{
	//错误,不能再静态变量声明中使用类型参数
	static T info;
	T age ;
	public void eat(T msg) {}
	//不能在静态方法声明中使用类型参数
	public static void bar(T msg) {}

}

三、泛型通配符

        如果Foo是Bar的一个子类型(子类或子接口),而G是具有泛型声明的类或接口,G<Foo> 并不是G<Bar>的子类!与数组进行比较,先看在数组中,程序可以直接把一个Integer[]数组赋给一个Number[]变量。如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时会抛ArrayStoreException异常。如下程序:

public class ArrayErr{
	public static void main(String[] args) {
        //定义一个Integer数组
		Integer [] ia = new Integer[5];
        //可以把一个Integer[]数组赋给Number[]变量
		Number [] na = ia;
        //这句代码在编译时正常,在运行时会抛ArrayStoreException异常  因为0.5不是Integer
		na[0] = 0.5;
	}
}

在Java泛型设计时,它不允许把List<Integer>对象赋给List<Number>变量。数组与泛型有所不同,假设Foo是Bar的一个子类型(子接口或子类)、那么Foo[]依然是Bar[]的子类型;但G<Foo>不是G<Bar>的子类型。

四、泛型方法

  • 定义泛型方法

首先考虑这样一个问题,定义一个方法负责将一个Object数组的所有元素添加到一个Collection集合中。用下面的代码来实现:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class AddErr{
	public static void main(String[] args) {
		String [] strArr = {"aa","bbb","ccc"};
		List<String> strList = new ArrayList<>();
		//因为List<String>不是Collection<Object>的子类因此会出现类型不匹配
		//formArrayToCollection(strArr, strList);
	}
	
	static void formArrayToCollection(Object [] a,Collection<Object> c) {
		for (Object o : a) {
			c.add(o);
		}
	}
}

       上面的定义一个关键的问题在于参数类型不可以使用Collection<String>作为Collection<Object>的子类,为了解决这个问题Java5提供了泛型方法(Generic Method)。所谓的泛型方法,就是在声明时定义一个或多个类型形参。泛型方法的用法格式如下:

修饰符  <T,S>返回值类型  方法名 (形参列表){

            //方法体...
    }

把上面的方法格式与普通的方法格式进行比较,不难发现泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参以尖括号括起来,多个类型之间以(,)隔开,所有的类型形参声明放在修饰符合返回值类型之间。因此以上的方法重新定义为:

import java.util.ArrayList;
import java.util.Collection;

/**
 * @author QuLei
 *测试泛型方法
 */
public class GenericMethod{
	public static void main(String[] args) {
		Object [] oa = new Object[100];
		Collection <Object> co = new ArrayList<>();
		//下面代码中T代表Object类型
		fromArrayToCollection(oa, co);
		//下面的代码中T代表String类型
		String [] sa = new String[100];
		Collection<String> cs = new ArrayList<>();
		fromArrayToCollection(sa, cs);
		//下面的代码T代表Object
		fromArrayToCollection(sa, co);
		Integer [] ia = new Integer[100];
		Float [] fa = new Float[100];
		Number [] na = new Number[100];
		Collection<Number> cn = new ArrayList<>();
		//下面的代码T代表Number类型
		fromArrayToCollection(ia, cn);
		fromArrayToCollection(fa, cn);
		fromArrayToCollection(na, cn);
		
	}
	
	/**定义泛型方法 ,该方法将给定数组的所有元素全部添加到给定的容器中去
	 * @param a 被添加的数组
	 * @param c
	 */
	static <T> void fromArrayToCollection(T [] a,Collection<T> c) {
		for (T o : a) {
			c.add(o);
		}
	}
}

与接口和类中使用泛型参数不同的是,方法中的泛型参数无须显示的传入实际类型参数,如上代码,当程序调用方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。