[自用篇]Java——泛型基础

了解泛型

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

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常

泛型的基本用法

了解"菱形"语法 < >

使用泛型时,大多情况需要把两个尖括号并排放一起,看起来像是一个菱形,故被称为"菱形"语法

Java7之前,如果使用带泛型的接口、类定义变量,那么调用构造器的后面也必须带泛型,例:

List <String> strList = new ArrayList<String>();
Map <String , Integr> scores = new HashMap<String , Integer>();

Java7开始,开始允许构造器后不需要带完整的泛型信息,只要给出尖括号<>即可
跟上面的代码片比较,两者的效果相同

List <String> strList = new ArrayList<>();
Map <String , Integr> scores = new HashMap<>();

Java9中增强的"菱形"语法

Java9开始允许创建匿名内部类时使用"菱形"语法,Java可以跟据上下文来推断匿名内部类中泛型的类型。
例:

interface A <T>
{
	void test(T t);
}
public static Test
{
	//指定A类中的泛型为String
	A <String> a0=new A <>()
	{
		//test()方法的参数类型为String
		public void test(String t)
		{
			System.out.println("test的形参为"+t);
		}
	};
	
	//使用泛型通配符
	A <?> a1=new A <>()
	{
		//test()方法的参数类型为Object
		public void test(Object t)
		{
			System.out.println("test的Object参数为"+t);
		}
	};
	
	//使用泛型通配符,设置上限为Number
	A <? extends Number> a2=new A <>()
	{
		//test()方法的参数类型为Number
				public void test(Number t)
		{
			System.out.println("test的Number参数为"+t);
		}
	}
}

定义泛型接口、类

定义泛型接口

//定义一个接口时指定一个泛型形参,该形参名为E
public interface A <E>
{
	//E可作为类型使用
	void add(E e);
	E next();
}

参考Java文档的List接口、Map接口的代码片段

public interface List<E>
{
		void  add(E e);
		Iterator <E> iterator();
}

//定义接口时指定了两个泛型形参
public interface Map<K , V>
{
		Set<K>  keySet();
		K put( K key  , V value)
}

定义泛型类

//定义A类时使用泛型声明
public class A<T>
{
	//使用T类型定义实例变量
	private T a;
	public A(T a)
	{
		this.a=a;
	}
	public T getA()
	{
		return a;
	}
	
	public static void main(String[] args)
	{
		//传递给T的形参为String,则构造器参数只能为String
		A <String> a1=new A<>("Test");
		System.out.println(a1.getA());
		//传递给T的形参为Integer,则构造器参数只能为Integer
		A <Integer> a2=new A<>(2);
		System.out.println(a2.getA());
	}
}

当创建带泛型声明的自定义类,为该类定义构造器时,构造器还是原来的类名,不需要增加泛型声明
例如,为A < T>类定义构造器时,其构造名为A,而不是A< T>

从泛型类派生子类

基本用法

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

public class B extends A< T>{ }//这是错误的用法

正确的用法

//使用A类时为T形参传入String类型
public class B exntends A<String>
//使用A类时,不传入实际的类型参数
public class C exntends A

注意
调用方法时必须为所有的数据形参传入参数值,但使用类、接口时也可以不为泛型形参传入实际的参数*,即上面的C类
使用省略泛型的形式被称为原始类型

重写父类的方法

传入了实际的类型参数

例如:

public class B extends A <String>
{
	//正确的重写
	//需要保证与父类的返回值相同
	public String getA()
	{
		return "子类"+super.getA();
	}
	/*
	//错误的重写
	//重写父类方法时返回值类型不一致
		public Object getA()
	{
		return "子类";
	}
	*/
}

不传入实际的类型参数(省略泛型的形式)

使用原始类型时(即没有传入实际的类型),编译器通常会发出警告,大致内容为:使用了未经过检查或不安全的操作(即泛型警告)。
例如:

public class C extends A 
{
	/**
	//此时系统会把A<T>类的T形参作为Object类型处理
	*/
	//重写父类方法
	public Object getA()
	{
		//super.getA()方法返回的是Object类型
		return super.getA();
	}
	/*
	//重写父类方法
	//可以根据需要进行转换
	public String getA()
	{
		return "子类"+super.getA().toString();
	}*/
}

注意,并不存在泛型类

举个例子,我们可以把ArrayList< String>当成ArrayList,事实上,ArrayList< String>类也确实像一种特殊的ArrayList类:该ArrayList< String>对象只能添加String对象作为元素集合。但实际上,系统并没有为ArrayList< String>生成新的class文件,而且也不会把ArrayList< String>当成新类来处理。
看下列代码

List<String>  L1=new ArrayList<>();
List<Integer> L2=new ArrayList<>();
System.out.println(L1.getClass()==L2.getClass());//结果为true

由此可以看出:不管泛型的实际类型参数是什么,它们在运行时总有同样的类。
/
不管泛型形参传入哪一种类型实参,对于Java而言,它们依然被当成同一个类处理,在内存中也只占用一块内存,因此,在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类

类型通配符

通常用于这种情况:
此时我们定义了一个方法,该方法包含了一个集合形参,集合的元素类型不确定,需要根据实际情况动态实现

深入前需要了解的一些基本概念

此时我们定义一个遍历集合的方法

public void Read(List<Object> L)
{
	for(int i=0;i<L.size();i++)
	{
		System.out.println(L.get(i))
	}
}

当下面代码试图调用该方法时会出现编译错误的问题

List<String> strL=new ArrayList<>();
//Read(strL)//此句将发生编译错误

上面代码出现的编译错误表明:List< String>对象不能被当成List< Object>对象使用,即List< String>并不是List< Object>类的子类

假如B是A的一个子类型(子类或者子接口),而C是具有泛型声明的类或接口,C< B>并不是C< A>的子类型
但需要注意的是假如B是A的一个子类型(子类或者子接口),那么B[ ]依然是A[ ]的子类型
B[ ]自动向上转型为A[ ]的方式称为型变
Java的数组支持型变,但Java的集合不支持型变

使用类型通配符

为了表示各种泛型List的父类,可以使用类型通配符
类型通配符是一个问号(?)
例如:List< ?>。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型
将前面的Read()方法改写,改为使用类型通配符

public void Read(List<?> L)
{
	for(int i=0;i<L.size();i++)
	{
		System.out.println(L.get(i))
	}
}

现在使用任何类型的List都可以调用,程序可以访问L中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object
但是注意,这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中
例如,下面的代码

List<?> L=new ArrayList<>();
L.add(new Object);//此句将引起编译错误

因为程序无法确定L集合中的元素类型,所以不能向其中添加对象。(null是个例外,它是所有引用类型的实例)
如果程序调用get()方法来返回List< ?>集合索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object

设定类型通配符的上限

在有些情况下,我们不希望使用List< ?>是任何泛型List的父类,只希望它代表某一类泛型List的父类
下面通过代码来说明

public abstract class A {
	public abstract void Output();
}

public class B extends A{
	public void Output()
	{
		System.out.println("A的子类1");
	}

}

public class Show {
		//错误的
		//	public void ShowOutput(List<A> a)
		//	{
		//		for (A temp : a)
		//		{
		//			temp.ShowOutput();
		//		}
		//	}
	//使用被限制的泛型通配符
	public void ShowOutput(List<? extends A> a)
	{
		for(A temp : a)
		{
			temp.Output();
		}
	}
	public static void main(String[] args)
	{
		List<B> b = new ArrayList<B>();
		Show s = new Show();
		s.ShowOutput(b);
	}

}

由于List< B>并不是List< A>的子类型,所以不能把List< B>当作List< A>的子类使用
若只是为了表示List< B>的父类,可以考虑使用List< ?>,但这样做的局限性在于提取出来的元素只能被编译器当作Object处理
为了表示List集合的元素是A的子类,可以使用Java提供的被限制的泛型通配符,例:
List<? extends A>.此处的问号(?)也代表一个未知的类型,但是可以肯定的是这个未知类型一定是A的子类型。类似地,由于程序无法确定具体这个受限的具体类型,所以不能把A对象或者其子类的对象添加进这个泛型集合,下列代码错误

public void add(List<? extends A> a)
{
	//下列代码引发编译错误
	a.add(0,new B());
}

综合上面所述,指定通配符上限的集合,只能从集合中取元素(且取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。
可以总结为:只出不进

深入补充:
对于更广泛的泛型而言,指定通配符上限是为了支持类型型变(与前面提到的相像)。比如B是A的子类,这样C< A>就相当于C<? extends B>的子类(C为一个泛型类派生的子类),可以将C< A>赋值给C<? extends B>的变量,这种型变方式又被称为协变
对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法,而不能调用泛型类型作为参数的方法

设定泛型形参的上限

Java泛型不仅允许在使用通配符形参时设定上线,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么就是该上限类型,要么就是该上限类型的子类。
参考下面代码:

public class A <T extends Number>
{
	//泛型形参的上限为Number类
	//使用A类时为T形参传入的实际类型只能为Number类或者其子类
	T Num;
	public static void main(String[] args)
	{
		A <Integer> a = new Apple<>();
		A <Double> b = new Apple<>();
		// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
		// 但String不是Number的子类型,所以引发编译错误
		// Apple<String> as = new Apple<>();		
	}
}

在有些情况下,可以为泛型形参设定多个上限。
但至多一个父类上限,可以有多个接口上限
这种情况下表明该泛型形参必须是其父类本身或者其子类,且需实现多个上限接口

与类同时继承父类和实现接口类似,泛型形参实现多个上限的时候,所有接口上限必须位于类上限之后
参考下列代码片段:

	//T类型必须是Number类或者其子类,且实现相关I/O接口
	//接口上限必须位于类上限后面
	public class A< T extends Number & java.io.Serializable>

设定类型通配符的下限

通配符的下限用<? super 类型>的方式来指定,作用与通配符上限的作用相反。
指定通配符的下限也是为了支持类型型变。比如B是A的子类,当程序需要一个C<? super A>变量时,程序可以将C< B>、C< Object>赋给C<? super A>类型的变量,这种型变方式又被称为逆变
对于逆变,逆变的泛型集合能向其中添加元素(编译器只知道集合元素的下限的父类型,但具体的父类型则不去欸的那个,另一个原因则是实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)

参考下面代码

import java.util.*;
/**
 *自定义一个工具方法
 *实现将一个集合的数据放入另一个集合中
 *将B集合的元素复制到A集合中
 */
public class TestUtils
{
	// 下面A集合元素类型必须与B集合元素类型相同,或是其父类
	public static <T> T copy(List<? super T> A
		, List<T> B)
	{
		T last = null;
		for (T temp  : B)
		{
			last = temp;
			// 逆变的泛型集合可以添加元素且是安全的
			A.add(temp);
		}
		return last;
	}
	public static void main(String[] args)
	{
		List<Number> ln = new ArrayList<>();
		List<Integer> li = new ArrayList<>();
		li.add(5);
		// 此处可准确的知道最后一个被复制的元素是Integer类型
		// 与B集合元素的类型相同
		Integer last = copy(ln , li);    
		System.out.println(ln);//观测结果
	}
}

上面代码的关键点在于:对于上面的copy()方法,因为使用了类型通配符下限,不管B集合元素的类型是什么,只要A集合元素的类型与前者相同或者是前者的父类即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值