泛型的目的:Java 泛型就是把一种语法糖,通过泛型使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现 ClassCastException,即类型转换异常。
泛型的好处
- 类型安全。类型错误现在在编译期间就被捕获到了,而不是在运行时当作java.lang.ClassCastException展示出来,将类型检查从运行时挪到编译时有助于开发者更容易找到错误,并提高程序的可靠性。
- 消除了代码中许多的强制类型转换,增强了代码的可读性。
- 为较大的优化带来了可能。
为什么需要泛型
通过下面的代码就可知道原因
public int addInt(int x, int y) {
return x + y;
}
public float addFloat(float x, float y) {
return x + y;
}
开发中经常有数值类型求和的需求,例如实现 int 类型的加法,有时还需要 long 类型的求和,如果还需要 double 类型的求和,需要重新在重载一个输入是 double 类型的 add 方法。
public static void main(String[] args) {
List list = new ArrayList();
list.add("corn");
list.add("qq");
list.add(22);
for (int i = 0; i < list.size(); i++) {
String name = (String) list.get(i);
System.out.println("args = " + name);
}
}
图一
定义一个 List 类型的集合,先向其中加入了两个字符串类型的值,随后加入一个 Integer 类型的值。这是完全允许的,因为此时 lis t默认的类型为 Object 类型。在之后的循环中,由于忘记了之前在 list 中也加入了 Integer 类型的值或其他编码原因,很容易出现类似于图一中的错误。因为编译阶段正常,而运行时会出现java.lang.ClassCastException
异常。因此,导致此类错误编码过程中不易发现。
在如上的编码过程中,我们发现主要存在两个问题:
- 当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了 Object 类型,但其运行时类型任然为其本身类型。
- 因此,
String name = (String) list.get(i);
处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException
异常。
泛型类、泛型接口、泛型方法
泛型,即“参数化类型”,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用或调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用 <> 括起来,并放在类名的后面。泛型类是允许有多个类型变量的。
public class NormalGeneric<T> {
private T data;
public NormalGeneric() {
}
public NormalGeneric(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
public class NormalGeneric2<T,K> {
private T data;
private K result;
public NormalGeneric2() {
}
public NormalGeneric2(T data, K result) {
this.data = data;
this.result = result;
}
}
泛型接口
泛型接口与泛型类的定义基本相同。
public interface Generator<T> {
void next(T t);
}
而实现泛型接口的类,有两种实现方法:
1、未传入泛型实参时:
public class ImplGenerator<T> implements Generator<T> {
@Override
public void next(T t) {
System.out.println("show:" + t);
}
}
在 new 出类的实例时,需要指定具体类型:
public static void main(String[] args) {
ImplGenerator<String> implGenerator = new ImplGenerator<>();
implGenerator.next("9");
}
2、传入泛型实参
public class ImplGenerator2 implements Generator<String> {
@Override
public void next(String t) {
System.out.println("show:" + t);
}
}
在 new 出类的实例时,和普通的类没区别。
泛型方法
public class GenericMethod {
/**
* public:修饰符
* <T>:返回值
*/
public <T> T genericMethod(T... a) {
return a[a.length / 2];
}
public void test(int x, int y) {
System.out.println(x + y);
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
genericMethod.test(5, 6);
System.out.println(genericMethod.genericMethod("a", "b", "c"));
System.out.println(genericMethod.genericMethod("sdf", "fdg ", 3));
}
}
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。
普通方法:
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey() {
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
*/
public E setKey(E key){
this.key=key;
}
}
泛型方法
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
*/
public <T> T showKeyName(Generic<T> container) {
System.out.println("container key:" + container.getKey());
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,
// 只是使用了Generic<Number>这个泛型类做形参而已。
public void show(Generic<Number> obj){
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
*/
// public <T,E> T show(E ab){
// //
// }
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
}
*/
// public void show(T obj){
//
// }
限定类型变量
我们需要对类型变量加以约束,比如计算两个变量的最小,最大值。
public static <T> T min(T a, T b) {
if (a.compareTo(b) > 0) return a; else return b;
}
如何确保传入的两个变量一定有 compareTo 方法?
解决方案就是将 T 限制为实现了接口 Comparable 的类。
public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) > 0) return b; else return a;
}
T extends Comparable 中,T 表示应该绑定类型的子类型,Comparable 表示绑定类型,子类型和绑定类型可以是类也可以是接口。
同时 extends 左右都允许有多个,如 T,V extends Comparable & Serializable。
注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。
这种类的限定既可以用在泛型方法上也可以用在泛型类上
public static <T extends ArrayList&Comparable> T min(T a, T b){
if(a.compareTo(b)>0) return a; else return b;
}
泛型中的约束和局限性
泛型类
public class Restrict<T> {}
不能用基本类型实例化类型参数
// Restrict<double> 这种不允许
Restrict<Double> restrict = new Restrict<>();
运行时类型查询只适用于原始类型
// if (restrict instanceof Restrict<Double>){} 这种不允许
// if (restrict instanceof Restrict<T>){} 这种不允许
System.out.println(restrict.getClass() == restrictString.getClass());
System.out.println(restrict.getClass().getName());
泛型类的静态上下文中类型变量失效
// 静态域或者方法里不能引用类型变量
// private static T instance;
// 静态方法 本身是泛型方法就行
private static <T> T getInstance(){};
不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是 static 的部分,然后才是构造函数等等。所以在对象初始化之前 static 的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。
不能创建参数化类型的数组
Restrict<Double>[] restrictArray;// 可以
// Restrict<Double>[] restricts = new Restrict<Double>[10]; // 不允许
不能实例化类型变量
// 不能实例化类型变量
// public Restrict(T data) {
// this.data = new T();
// }
不能捕获泛型类的实例
// 泛型类不能 extends Exception/Throwable
private class Problem<T> extends Exception;
// 不能捕获泛型类对象
public <T extends Throwable> void doWork(T x) {
try {
} catch (T x) {
// do sth
}
}
但是这样可以:
public <T extends Throwable> void doWorkSuccess(T x) throws T{
try {
}catch (Throwable e){
throw x;
}
}
泛型类型的继承规则
有一个类和子类
public class Employee {
}
public class Worker extends Employee {
}
有一个泛型类
public class Pair<T> {
}
那么 Pair<Employee> 和 Pair<Worker> 是继承关系吗?
答案:不是,他们之间没有什么关系。
36DAEA90-C871-48C0-81C7-E63569FDC864.png
但是泛型类可以继承或者扩展其他泛型类,比如 List 和 ArrayList。
Pair<Employee> pair = new ExtendPair<>();
// 泛型类可以继承或者扩展其他泛型类,比如 List 和 ArrayList
private static class ExtendPair<T> extends Pair<T> {
}
通配符类型
上述代码中 Pair<Employee> 和 Pair<Worker> 没有任何关系,如果我们有一个泛型类和一个方法
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
private static void print(GenericType<Fruit> p) {
System.out.println(p.getData().getColor());
}
继承关系的类
public class Fruit {
}
public class Orange extends Fruit {
}
public class Apple extends Fruit{
}
public class HongFuShi extends Apple{
}
则会出现这种使用情况:
通配符类型 ? 可以解决上述问题。
有两种使用方式:
- ? extends X 表示类型的上界,类型参数是X的子类
- ? super X 表示类型的下界,类型参数是X的超类
无限定的通配符 ?
类型通配符是一个问号(?),将一个问号作为类型实参传给 List 集合,写作:List<?>(意思是元素类型未知的 List )。这个问号(?)被成为通配符,它的元素类型可以匹配任何类型。
public void test(List<?> c){
for(int i =0;i<c.size();i++){
System.out.println(c.get(i));
}
}
现在可以传入任何类型的List来调用test()方法,程序依然可以访问集合c中的元素,其类型是Object。
List<?> c = new ArrayList<String>();
//编译器报错
c.add(new Object());
但是并不能把元素加入到其中。因为程序无法确定c集合中元素的类型,所以不能向其添加对象。
下面就该引入带限通配符,来确定集合元素中的类型。
上限通配符 ?extends X
如果想限制使用泛型类别时,只能用某个特定类型或者是其子类型才能实例化该类型时,可以在定义类型时,使用extends关键字指定这个类型必须是继承某个类,或者实现某个接口,也可以是这个类或接口本身。
它表示集合中的所有元素都是Shape类型或者其子类
List<? extends Shape>
上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
? extends X
表示传递给方法的参数,必须是 X 的子类(包括X本身)。如下代码所示:
private static void print2(GenericType<? extends Fruit> p) {
System.out.println(p.getData().getColor());
}
public static void main(String[] args) {
GenericType<Fruit> genericType = new GenericType<>();
print2(genericType);
GenericType<Orange> genericType2 = new GenericType<>();
print2(genericType2);
GenericType<? extends Fruit> p = genericType;
}
但是对泛型类 GenericType 来说,如果其中提供了 get 和 set 类型参数变量的方法的话,set 方法是不允许被调用的,会出现编译错误。
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
get 方法则没问题,会返回一个 Fruit 类型的值。
GenericType<? extends Fruit> c = genericType;
Fruit data = c.getData();
原因: ? extends X
表示类型的上界,类型参数是 X 的子类,那么可以肯定的说,get 方法返回的一定是个 X(不管是X或者X的子类)编译器是可以确定知道的。但是 set 方法只知道传入的是个 X,至于具体是 X 的那个子类,不知道。
总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。
下限通配符 ?super X
如果想限制使用泛型类别时,只能用某个特定类型或者是其父类型才能实例化该类型时,可以在定义类型时,使用super关键字指定这个类型必须是是某个类的父类,或者是某个接口的父接口,也可以是这个类或接口本身。
它表示集合中的所有元素都是Circle类型或者其父类
List <? super Circle>
这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
例如:
//Shape是其父类
List<? super Circle> list = new ArrayList<Shape>();
?super X
表示传递给方法的参数,必须是 X 的超类(包括X本身)
对泛型类 GenericType 来说,如果其中提供了 get 和 set 类型参数变量的方法的话,set 方法可以被调用的,且能传入的参数只能是 X 或者 X 的子类
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
get 方法只会返回一个 Object 类型的值。
原因:? super X
表示类型的下界,类型参数是 X 的超类(包括 X 本身),那么可以肯定的说,get 方法返回的一定是个 X 的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以 get 方法返回Object。
编译器是可以确定知道的。对于 set 方法来说,编译器不知道它需要的确切类型,但是 X 和 X 的子类可以安全的转型为 X。
总结:主要用于安全地写入数据,可以写入 X 及其子类型。
类型擦除
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
如在代码中定义 List<Object> 和 List<String> 等类型,在编译后都会变成 List, JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况。
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
程序输出:
true
在这个例子中,我们定义了两个 ArrayList 数组,不过一个是 ArrayList<String> 泛型类型的,只能存储字符串;一个是 ArrayList<Integer> 泛型类型的,只能存储整数,最后,我们通过 list1 对象和 list2 对象的 getClass() 方法获取他们的类的信息,最后发现结果为 true。说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型。
在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。
网友评论