泛型的基本用法与一些细节问题
了解泛型
泛型,即就是允许在定义类、接口、方法时使用类型参数,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
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集合元素的类型与前者相同或者是前者的父类即可