Java面向对象系列[v1.0.0][泛型进阶]

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或者叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数也可称为类型实参)
Java5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
Java5改写后List接口、Iterator接口、Map的代码片段:

// 定义接口时指定了一个泛型形参,该形参名为E
public interface List<E>
{
	// 在接口里,E可作为类型使用
	// 下面方法可以使用E作为参数类型
	void add(E x);
	Iterator<E> iterator();
	...
}
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Iterator<E>
{
	// 在该接口里E完全可以作为类型使用
	E next();
	boolean hasNext();
	...
}
// 定义接口时指定了两个泛型形参, 其形参名为K、V
public interface Map<K, V>
{
	// 在该接口里K、V完全可以作为类型使用
	Set<K> keySet();
	V put(K key, V value);
	...
}

三个接口声明是比较简单的,除了尖括号中的内容,而这就是泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可以当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参
除此之外:Iterator<E> iterator(); 和 Set<K> keySet();方法声明返回值类型是Iterator、Set,这表明他们是一种特殊的数据类型,是一种与Interator、Set不同的数据类型,可以当成他们的子类看待。
例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型:List类型,可以把List想象成E被全部替换成String的特殊List自接口

// List<String>等同于如下接口
public interface ListString extends List
{
	// 原来的E形参全部变成String类型实参
	void add(String x);
	Iterator<String> iterator();
	...
}

虽然程序只定义了一个List接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多处一个新的List子接口
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类、但这种子类在物理上并不存在

创建泛型类、接口

可以为任何类、接口增加泛型声明

import static java.lang.System.*;

// 定义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<>("苹果");
		out.println(a1.getInfo());
		// 由于传给T形参的是Double,所以构造器参数只能是Double或double
		Apple<Double> a2 = new Apple<>(5.67);
		out.println(a2.getInfo());
	}
}

代码中定义了一个带泛型声明的Apple类,不用管这个泛型形参是否具有实际意义,使用Apple类时就可以为T形参传入实际类型,这样就可以生成如Apple、Apple…形式的多个逻辑子类,物理上并不存在。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明,例如为Apple类定义构造器,其构造器名依然是Apple,而不是Apple,调用该构造器时却可以使用Apple的形式,当然应该为T形参传入实际的类型参数

泛型类派生子类

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

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

定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型
如果想从Apple类派生一个子类,则可以:

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

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

// 使用Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple

这样的用法省略了泛型的形式被称为原始类型(raw type),而如果从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 "子类";
	}
	*/
}

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

public class A2 extends Apple
{
	// 重写父类的方法
	public String getInfo()
	{
		// super.getInfo()方法返回值是Object类型,
		// 所以加toString()才返回String类型
		return super.getInfo().toString();
	}
}

创建带泛型声明的接口的实现类与此几乎完全一样

剖析泛型类

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

// 创建List<String>对象和List<Integer>对象
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 调用getClass()方法比较两个对象的类是否相等
System.out.println(strList.getClass() == intList.getClass());

结果返回true,不管泛型的世纪类型参数是什么,他们在运行时总是有同样的类(class),在内存中也只占一块内存空间,因此静态方法、静态初始化块、静态变量的声明和初始化中不允许使用泛型形参

public class R<T>
{
	// 下面代码错误,不能在静态变量声明中使用泛型形参
	//	static T info;
	T age;
	public void foo(T msg){}
	// 下面代码错误,不能在静态方法声明中使用泛型形参
	//	public static void bar(T msg){}

}

因为系统不会真正的生成泛型类,所有instanceof运算符后不能使用泛型类,如下是错误的示范:

java.util.Collection<String> cs - new java.util.ArrayList<>();
// 下面代码编译时会报错:instanceof运算符后不能使用泛型
if (cs instanceof java.util.ArrayList<String>{...})
发布了207 篇原创文章 · 获赞 124 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105281802
今日推荐