8.2 深入泛型


所谓泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态指定(即传入实际的类型参数,也可叫做类型实参)。

一、定义泛型接口、类

下面是Java 5改写的List接口、Iterator接口和Map接口:

//定义接口时,创建一个泛型形参,该形参名为E
public interface List<E>
{
    //在接口里E可以作为类型使用
    //下面方法可以使用E作为参数类型
    void add(E x);
    Iterator<E> iterator();
}

//定义接口时指定一个泛型形参,该形参名为E
public interface Iterator<E>
{
//在接口里完全可以作为类型使用
    E next();
    boolean hasNext();
    ...
}

//定义接口时指定两个泛型参数,其形参名为K,V
public interface Map<K,V>
{
//在接口里定义的K,V完全可以作为类型使用
Set<k> keySet()
V put(K key,V value)
...
}

泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可以当作类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型。
例如使用List类型时,如果为E形参传入String类型实参,则产生一个新的类型:List类型,可以把List想象成E被全部替换成String的特殊List子接口。

//List<String>等同于如下接口
public interface List<String>
{
    void add(String x);
    Iterator<String> iterator();
    ...
}

注:包含泛型声明的类型可以在定义变量、创建对象(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的主要使用场景)。下面定义一个Apple类,这个Apple类里就可以包含一个泛型声明。

//定义Apple类时使用泛型声明
public class Apple<T> 
{
	//使用T类型定义实例变量
	private T info;
	public Apple(){};
	//下面使用T类型来定义构造器
	public Apple(T info)
	{
		this.info=info;
	}
	public void setInfo(T info)
	{
		this.info=info;
	}
	public T getInfo()
	{
		return this.info;
	}
	public static void main(String[] args) 
	{
		//传给T形参的是String,所以构造器参数只能是String
		Apple<String> a1=new Apple<>("苹果");
		System.out.println(a1.getInfo());//苹果
		////传给T形参的是Double,所以构造器参数只能是Double或double
		Apple<Double> a2=new Apple<>(5.67);
		System.out.println(a2.getInfo());//5.67
	}
}

上面程序定义了一个带泛型声明的Apple类,使用Apple类就可以为T形参传入实际类型,这样就可以生成Apple、Apple...形式的多个逻辑子类(物理上不存在)。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。

二、从泛型类派生子类

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

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

方法中的形参代表变量、常量、表达式等数据,本书把它们直接称为形参,或者称为数据形参。定义方法时可以声明数据形参,调用方法时必须为这些数据形参传入实际的数据;依次类时的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。
从Apple类派生一个子类:

//使用Aplle类为T形参传入String类型
public class A extends Apple<String>

调用方法时必须为所有数据形参传入参数值,与调用方法不同的是,使用类接口、接口时可以不为泛型形参传入实际的类型参数,即下面的代码是正确的:

//使用Aplle类时,没有为T形参传入类型参数
public class A extends Apple//省略泛型的形式称为原始类型

如果Apple类派生子类,则在Apple类中所有使用T类型的地方都被替换成String类型,即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类需要重写父类的父类的放啊必须注意到这一点。例如:

public class A1 extends Apple<String>
{
	// 正确重写了父类的方法,返回值
	// 与父类Apple<String>的返回值完全相同
	public String getInfo()
	{
		return "子类" + super.getInfo();
	}
	/*
	// 下面方法是错误的,重写父类方法时返回值类型不一致
	public Object getInfo()
	{
		return "子类";
	}
	*/
	public static void main(String[] args)
	{
		var a=new A1();
		System.out.println(a.getInfo());
	}
}

如果使用Aplle类时没有传入实际的类型(即使用原始类型),Java编译器可能发出警告:使用了未经检查或不安的操作————这就是泛型警告。如果需要看到该警告提示的更详细的信息,则可以通过为javac命令增加-Xlint:unchecked选项来实现。此时系统会把Apple类里的T形参当成Object类型处理:

public class A2 extends Apple 
{
	//重写父类的方法
	public String getInfo()
	{
		//下面重写将报错,原始类型是Object.错误: 不兼容的类型: Object无法转换为String
		//return super.getInfo();//方法返回的值是Object
		return super.getInfo().toString();
	}
}

三、并不存在泛型类

可以把ArrayList类当成ArrayList的子类,事实上,ArrayList类是一种特殊的ArrayList类:该ArrayList对象只能添加String对象作为集合元素。但实际上系统并没有为ArrayList生成新的class文件,而且也不会把ArrayList当成新类使用。

import java.util.*;
public class ArrayListTest
{
	public static void main(String[] args)
	{
		//分别创建List<String>对象和List<Integer>对象
		List<String> l1=new ArrayList<>();
		List<Integer> l2=new ArrayList<>();
		System.out.println(l1.getClass()==l2.getClass());//true
	}
}

不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,再内存中也只有一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化块不允许使用泛型。下面程序演示这种错误:

import java.util.*;
public class R<T>
{
	public T age;
	//不能在静态变量声明中使用泛型形参
	//public static T info;//错误: 无法从静态上下文中引用非静态 类型变量 T

	public void foo(T msg){System.out.println(msg);}
	
	//R.java:12: 错误: 不兼容的类型: String无法转换为T
	/*{
		age="二十";
	}*/

	//不能在静态方法中使用泛型形参
	//public static void bar(T msg){}//错误: 无法从静态上下文中引用非静态 类型变量 T
	public static void main(String[] args)
	{
		R<String> r=new R<>();
		r.foo("测试泛型");
	}
}
输出:测试泛型
原文地址:https://www.cnblogs.com/weststar/p/12591344.html