前言:
Java泛型还是比较难的,而且很重要;但是由于小白平时没怎么用/(ㄒoㄒ)/~~,所以一直都是一知半解;因此下定决心,咬牙切齿(有点夸张了,嘿嘿~~)专门抽了个时间根据Java核心卷和疯狂讲义,做了个笔记;不对或不合理的地方希望大家踊跃指出😝;
1.泛型入门
- 起因:集合的缺点:某个对象保存在集合后,全都按照Object类型处理。泛型程序设计 ( Generic programming ) 意味着编写的代码可以被很多不同类型的对象所重用。
- 没有泛型之前Java,Java中泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组。但是存在两个问题:①获取一个值时必须进行强制类型转换;②由于没有进行错误检查,可以添加任何类的对象;
1.1编译时不检查类型的异常
- 无泛型时引发的问题:
//例子①:
import java.util.ArrayList;
import java.util.List;
public class ListErr {
public static void main(String[] args){
//创建一个只想保存字符串的List集合
List strList = new ArrayList();
// 1 234567 8
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
//"不小心"往strList中add了一个Integer对象
strList.add(5);
strList.forEach(str -> System.out.println(((String)str).length())); //抛出ClassCastException,由于Integer转String
}
}
1.2使用泛型
import java.util.ArrayList;
import java.util.List;
public class GenericList {
//泛型:“参数化类型” parameterized type
public static void main(String[] args){
//创建一个只想保存字符串的List集合,此处声明的List是带有类型参数的泛型接口,所以只能add,List声明的类型,add其他类型会发生编译异常
List<String> strList = new ArrayList<>();
// 0 123456 7
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
//下面代码将引起编译错误
strList.add(5);
strList.forEach(str -> System.out.println(str.length()));
}
}
1.3Java9增强的“菱形”语法
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Java 7的"菱形"语法: 省略new对象时<>中的类型参数
*/
public class DiamondTest {
public static void main(String[] args){
//Java 自动推断出ArrayList的<>中应该是String
List<String> strList = new ArrayList<>();
strList.add("hello");
strList.add("LazyGoal");
//遍历strList集合,集合元素就是String类型
strList.forEach(temp -> System.out.println(temp));
//Java自动推断出HashMap的<>里应该是String, List<String>
Map<String, List<String>> schoolInfo = new HashMap<>();
//Java自动推断出ArrayList的<>里应该是String
List<String> schools = new ArrayList<>();
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolInfo.put("孙悟空", schools);
//遍历Map时,Map的key是String类型,value是List<String>类型
schoolInfo.forEach((key, value) -> System.out.println(key + "->" + value));
}
}
/**
* Java 9的“菱形”语法:允许创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型
*/
public class AnnoymousTest {
public static void main(String[] args){
//指定Foo类中泛型为String
Foo<String> f = new Foo<>(){
//test()方法的参数类型为String
public void test(String t){
System.out.println("test方法的t参数为:" + t);
}
};
//使用泛型通配符,此时相当于通配符的上限为Object
Foo<?> fo = new Foo<>(){
//test()方法的参数类型为Object
public void test(Object t){
System.out.println("test方法的Object参数为:" + t);
}
};
//使用泛型通配符,通配符的上限为Number
Foo<? extends Number> fn = new Foo<>(){
//此时test()方法的参数类型为Number
public void test(Number t){
System.out.println("test方法的Number参数为:" + t);
}
};
}
}
2.深入泛型
- 泛型:定义类、接口、方法时使用类型参数,而这个类型参数(泛型)会在声明变量、创建对象、调用方法时动态地指定(也就是传入实际的类型参数,也可称为类型实参)。泛型的形参在整个接口、类体内可当成类型使用,几乎所有可以使用普通类型的地方都可以使用这种泛型形参;简而言之,一个泛型类( generic class ) 就是具有一个或多个类型变量的类;换句话,泛型类可看作普通类的工厂;也就是编写的代码可以被很多不同类型的对象所重用;
- 注释:类型变量使用大写形式,且比较短,这是很常见的;在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”;
- **其中类型参数的使得程序有更好的可读性和安全性;**因为出现编译错误比类在运行时出现强制转换类型异常要好得多;
2.1定义泛型接口、类
/**
* 可为任何类、接口增加泛型声明;
* 定义Apple类时使用了泛型声明
* @param <T>
*/
public class Apple<T> {
//使用T类型定义实例变量
private T info;
public Apple(){}
/**
* 创建带泛型的自定义类时,写构造方法时方法名只写类名,不写类名后边的<>和其他的,而调用该构造方法时可以使用类名<>
* @param info
*/
//下面方法中使用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){
/**
* 使用Apple<T>类时可为T形参传入实际类型,从而可以生成多个逻辑子类;如:Apple<String>,Apple<Double>……
*/
//由于传给T形参的是String,因此构造器参数只能是String
Apple<String> a1 = new Apple<>("Apple");
System.out.println(a1.getInfo());
//由于传给T形参的是Double,因此构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
a2.setInfo(1.112);
System.out.println(a2.getInfo());
//其实好像不传类型参数也行,其中的泛型类型都按照Object算
Apple a3 = new Apple("1");
a3.setInfo(1.0);
System.out.println(a3.getInfo());
}
}
2.2从泛型类派生子类
- 带有泛型声明的接口、父类后,有子类想实现或继承时,可以有两种情况:①接口或父类的类型参数必须被传入;②接口或父类的泛型<>不被写在实现或继承语句中(也就是省略泛型的形式,被称为原始类型raw type)。如以下代码:
情况一:
public class A extends Apple<String> implements Foo<String> {
@Override
public void test(String o) {
}
}
此种情况下,子类重写父类方法时必须保持和父类的泛型类型的一直(此泛型有具体的类型)
public class A1 extends Apple<String> {
//正确重写了父类的方法,返回值
//与父类Apple<String>的返回值完全相同
public String getInfo(){
return "sonOfClass" + super.getInfo();
}
//下面方法是错误的,重写父类的方法时返回值类型不一致
// public Object getInfo(){
// return "sonOfClass";
// }
}
情况二:
public class A extends Apple implements Foo {
@Override
public void test(Object o) {
}
}
public class A2 extends Apple {
//重写父类的方法
public String getInfo(){
//super.getInfo()返回值是Object,所以需要用toString
return super.getInfo().toString();
}
}
2.3 并不存在泛型类
import java.util.ArrayList;
import java.util.List;
public class R<T> {
//错误实例,不能在静态变量声明中使用泛型形参,原因请参见下面说明
static T info;
T age;
public void foo(T msg){
}
//错误实例,不能在静态方法声明中使用泛型形参,原因请参见下面说明
public static void bar(T msg){
}
/**
* 不管泛型形参传入的是哪一种类型实参,对Java,它们依然被当成同一个类处理,内存中也只占用一块内存空间;
* 因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参
* 原因:①泛型在对象创建时才知道类型,而静态方法属于类,类在编译阶段就存在,所以虚拟机根本不知道静态***中引用的泛型的类型
* ②初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西
* @param args
*/
public static void main(String[] args){
//创建List<String>对象和List<Integer>对象
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
//比较list1对象和list2对象
System.out.println(list1.getClass() == list2.getClass()); //true
// java.util.Collection<String> cs = new java.util.ArrayList<>();
//系统中并不会真正的生成泛型类,因此instanceof运算符后不能使用泛型类
// if(cs instanceof java.util.ArrayList<String>){ }
}
}
3.类型通配符
- 首先得解释一下什么是通配符,在维基百科中是这么定义的:在计算机(软件)技术中,通配符可用于代替单个或多个字符;也就是通配符类型中,允许类型参数变化;通常地,星号“*”匹配0个或以上的字符,问号“?”匹配1个字符。其中T,E,K,V,有什么区别么?本质上没区别,只是有某些使用情况和约定而已。如:
?表示不确定的Java类型 T (type) 表示具体的一个java类型 K V (key value) 分别代表java键值中的Key Value E (element) 代表Element
①Java泛型中,如果Foo是Bar的某个子类(或子接口),而G是具有泛型声明的类或接口,但G<Foo>不是G<Bar>的子类型~
②但是在数组中可以直接把一个Integer[]数组赋值给一个Number[]变量,若Double变量赋值给刚刚的Number[]数组,虽然编译可通过,但是运行时会抛出ArrayStoreException异常。
③Java泛型的设计原则:代码编译时没有警告,就不会遇到运行时ClassCastException异常;
总结:若Foo是Bar的一个子类(或子接口),Foo[]依然是Bar[]的子类型(这种情况称之为型变);而G<Foo>不是G<Bar>的子类型;Java数组支持型变,而集合不支持型变;
参考笔记:聊一聊-JAVA 泛型中的通配符 T,E,K,V,? - 掘金
3.1使用类型通配符
- 类型通配符:是一个问号(?);
- 格式:接口和类的名称(它们都支持泛型声明)<?>;
- 用途:表示各种泛型类型***的父类;
- List<?>:将一个问号作为类型实参传递给List集合,也就是元素类型未知的List;此问号(?)是通配符,它的元素类型可以匹配任何类型。
//现在使用任何类型的List调用此方法都可以,因为此方法的形参的类型是Object
public void test(List<?> c){
for(int i = 0; i < c.size(); ++i)
System.out.println(c.get(i));
}
//带通配符的List仅表示它是各种泛型List的父类,不能把元素加入到其中
List<?> c = new ArrayList<String>();
//下一行代码引起编译错误,原因:程序无法确定c集合中元素的类型,但是null可以add,因为null是所有引用类型的实例
c.add(null);
- 注意:Pair<?>和Pair是有很大的区别的!!!
Pair<?>的setFirst()和getFirst()方法分别可以看成:? getFirst()和void setFirst(?);getFirst的返回值只能赋值给Object,而setFirst方法不能被调用(或仅仅可用null调用);
3.2设定类型通配符的上限
如果希望List<?>不是所有泛型List的父类,只是某一类泛型List的父类。如以下例子;List<? extends Shape>:是受限制的通配符类型,此处?号代表未知的类型,但是这个未知类型必须是Shape的子类型(也可以是Shape本身),因此可以把Shape视为这个通配符的上限(upper bound);由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中;
上界通配符<? extends E>: 上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。 在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:①如果传入的类型不是 E 或者 E 的子类,编译不成功;②泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用; 类型参数列表中如果有多个类型参数上限,用逗号分开;
Pair<Manager> managerPair = new Pair<>();
Pair<? extends Employee> employeePair = managerPair; //OK
//getFirstNumber函数可以使用,但是setFirstNumber函数不能使用,保证了安全性;
System.out.println("firstNumber:" + employeePair.getFirstNumber()); //null
// employeePair.setFirstNumber(1); //compile-time error,因为setFirstNumber函数中编译器只知道需要这个类型(? extends Employee)的变量,但不知道具体类型,所以会拒绝任何特定的类型
/**
* 定义一个抽象类Shape
*/
public abstract class Shape {
public abstract void draw(Canvas canvas);
}
/**
* 定义一个抽象类Shape
*/
public class Circle extends Shape {
//实现画图方案,以打印字符串来模拟画图方法实现
public void draw(Canvas canvas){
System.out.println("在画布" + canvas + "上画个圆");
}
}
/**
* 定义Shape的子类Rectangle
*/
public class Rectangle extends Shape {
//实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas canvas){
System.out.println("把一个矩形画在画布" + canvas + "上");
}
}
public class Canvas {
//同时在画布上绘画多个形状
public void drawAll(List<? extends Shape> shapeList){
for(Shape shape: shapeList)
shape.draw(this);
}
public void addRectangle(List<? extends Shape> shapes){
//下边代码引起编译错误
// shapes.add(0, new Rectangle());
}
public static void main(String[] args){
List<Circle> circles = new ArrayList<>();
Canvas c = new Canvas();
c.drawAll(circles);
}
}
3.3设定类型通配符的下限(通配符超类型限定)
- 通配符下限(通配符的超类型限定):<? super 类型>;
- 用途:支持类型型变,逆变;
下界通配符 < ? super E> 下届:用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object;简而言之,此通配符的限制为**类型的所有超类型; 在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类;
public class MyUtils {
//下面dest集合元素的类型必须与src集合元素的类型相同,或者是其父类
public static <T> T copy(Collection<? super T> dest, Collection<T> src){
T last = null;
for(T ele: src){
last = ele;
//逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}
public static void main(String[] args){
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
//此处可准确地知道最后一个被复制的元素是Integer类型
//与src集合元素的类型相同
Integer last = copy(ln, li);
System.out.println(ln);
}
}
public class TreeSetTest {
public static void main(String[] args){
//Comparator的实际类型是TreeSet的元素类型的父类,满足要求
TreeSet<String> ts1 = new TreeSet<>(
new Comparator<Object>(){
public int compare(Object fst, Object snd){
return hashCode() > snd.hashCode()? 1: hashCode() < snd.hashCode()? -1: 0;
}
}
);
ts1.add("hello");
ts1.add("wa");
//Comparator的实际类型是TreeSet元素的类型,满足要求
TreeSet<String> ts2 = new TreeSet<>(
new Comparator<String>(){
public int compare(String first, String second){
return first.length() > second.length()? -1: first.length() < second.length()? 1: 0;
}
}
);
ts2.add("hello");
ts2.add("wa");
System.out.println(ts1);
System.out.println(ts2);
}
}
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行; 请看下边经典例子:
/*
通配符的超类限定:**此时可以为方法提供参数,但不能使用方法返回值**;请看以下例子:
*/
Pair<Employee> employeePair = new Pair<>();
Pair<? super Manager> managerPair = employeePair;
//get方法的返回值不能保证返回对象的类型,只能把它赋给一个Object
Object manager = managerPair.getFirstNumber();
// Manager manager2 = managerPair.getFirstNumber(); //error
// Employee manager3 = managerPair.getFirstNumber();
System.out.println(managerPair.getFirstNumber());
//编译器无法知道setFirst方法的具体类型,
//因此调用这个方法时不能接受类型为Employee或Object的参数,
//只能传递Manager类型对象,或它的某个子类型对象;
managerPair.setFirstNumber(new Manager());
// managerPair.setSecondNumber(new Employee());
- 总结:带有超类限定的通配符可以向泛型对象写入,带有子类限定的通配符可以从泛型对象读取;
3.4设定泛型形参的上限
/**
* java中使用通配符形参时可以设定上限,同时也可以在定义泛型形参时设定上限,泛型形参的上限设定好之后
* 表示该泛型形参的实际类型或是该上限类型,或是该上限类型的子类
* @param <T>
*/
public class Apple2<T extends Number> {
T col;
public static void main(String[] args){
Apple2<Integer> ai = new Apple2<>();
Apple2<Double> ad = new Apple2<>();
//下面代码将引发编译异常,下面代码试图把String类型传给T形参
//但String不是Number的子类型,所以引起编译错误
// Apple2<String> as = new Apple<>();
}
}
//泛型形参指定多个上限时,所有的接口上限必须位于类上限之后
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Apple3<T extends Number & java.io.Serializable> {
}
3.5通配符捕获
- 请看通配符操作函数例子:
/**
* 交换Pair对象的两个属性的方法
* @param pair
*/
public static void swap(Pair<?> pair){
//很明显这方式不行,因为没有?类型,但可以用以下函数解决
// ? t = pair.getFirstNumber();
// pair.setFirstNumber(pair.getSecondNumber());
// pair.secondNumber(t);
swapHelper(pair);
}
/**
* 交换Pair对象的两个方法,此方法的参数T捕获通配符;
* @param pair
* @param <T>
*/
public static <T> void swapHelper(Pair<T> pair){
T t = pair.getFirstNumber();
pair.setFirstNumber(pair.getSecondNumber());
pair.setSecondNumber(t);
}
- 通配符捕获只有在有许多限制的情况下才是合法的;编译器必须能确定通配符表达式的是单个、确定的类型;如:ArrayList<Pair>中的T永远不能捕获ArrayList<Pair<?>>中的通配符;
4.泛型方法
4.1泛型方法
我们先定义一个带类型参数的简单方法:
public class ArrayAlg {
public static <T extends Comparable> Pair<T> minMax(T[] a){
if(a == null || a.length == 0)
return null;
T min = a[0];
T max = a[0];
for(int i = 1; i < a.length; ++i){
if(min.compareTo(a[i]) > 0)
min = a[i];
if(max.compareTo(a[i]) < 0)
max = a[i];
}
return new Pair<T>(min, max);
}
/**
* 此方法中使用<T extends Comparable>限制了类型变量T只能被实现了Comparable接口的类(String、LocalDate等);
* 当然一个类型变量或通配符可以有多个限定:<T extends Comparable & Serializable>,其中限定类型用"&"分隔;
* 而逗号用来分隔类型变量;
* @param a
* @param <T>
* @return
*/
public static <T extends Comparable> T min(T[] a){
if(a == null || a.length == 0)
return null;
T smallest = a[0];
for(int i = 1; i < a.length; ++i)
if(smallest.compareTo(a[i]) > 0)
smallest = a[i];
return smallest;
}
/**
* 泛型方法,可在普通类中,也可在泛型类中;
* 其中类型变量放在修饰符后,返回类型前;
* @param a
* @param <T>
* @return
*/
public static <T> T getMiddle(T[] a){
return a[a.length / 2];
}
public static void main(String[] args){
//当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型;在当前情况下,可省略<String>类型参数;
//因为编译器可以自行判断出调用的方法;
String middle = ArrayAlg.<String>getMiddle(new String[]{"a", "b", "c", "d", "e", "f"});
String middle2 = ArrayAlg.getMiddle(new String[]{"a", "b", "c", "d", "e", "f"});
System.out.println("middle:" + middle);
Double middle3 = ArrayAlg.<Double> getMiddle(new Double[]{3.14, 1729.0, 1.0});
System.out.println("middle3:" + middle3);
String[] temp = new String[]{"a", "b", "d", "o", "p", "q"};
String min = ArrayAlg.<String>minMax(temp).getFirstNumber();
String max = ArrayAlg.<String>minMax(temp).getSecondNumber();
System.out.println("min:" + min);
System.out.println("max:" + max);
}
}
5.泛型代码和虚拟机
虚拟机没有泛型类对象:所有对象都属于普通类;
5.1类型擦除
- 原始类型(raw type):原始类型名是删除类型参数后的泛型类型名;擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object);若类型变量有多个限定,原始类型用第一个限定的类型变量替换(若没有给限定的类型用Object替换);它是最后在字节码中类型变量的真正类型,无论何时定义泛型,相应的原始类型都会被自动提供;
Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉;Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除;
//public class Pair<T> {
// private T firstNumber;
// private T secondNumber;
//
// public Pair(T firstNumber, T secondNumber){
// this.firstNumber = firstNumber;
// this.secondNumber = secondNumber;
// }
//
// public Pair(){
// firstNumber = null;
// secondNumber = null;
// }
//
// public T getFirstNumber(){
// return firstNumber;
// }
//
// public void setFirstNumber(T firstNumber){
// this.firstNumber = firstNumber;
// }
//
// public T getSecondNumber(){
// return secondNumber;
// }
//
// public void setSecondNumber(T secondNumber){
// this.secondNumber = secondNumber;
// }
//
// public static void main(String[] args){
// Pair<String> pair = new Pair<>("wwf", "fsy");
// System.out.println("husband:" + pair.getFirstNumber());
// System.out.println("wife:" + pair.getSecondNumber());
// }
//}
//---对应的原始类型为---
public class Pair{
private Object firstNumber;
private Object secondNumber;
public Pair(){}
public Pair(Object firstNumber, Object secondNumber){
this.firstNumber = firstNumber;
this.secondNumber = secondNumber;
}
public Object getFirstNumber(){
return firstNumber;
}
public void setFirstNumber(Object firstNumber){
this.firstNumber = firstNumber;
}
public Object getSecondNumber(){
return secondNumber;
}
public void setSecondNumber(){
this.secondNumber = secondNumber;
}
}
//public class Interval <T extends Comparable & Serializable> implements Serializable{
// private T lower;
// private T upper;
//
// public Interval(){}
// public Interval(T lower, T upper){
// if(lower.compareTo(upper) <= 0){
// this.lower = lower;
// this.upper = upper;
// }else {
// this.lower = upper;
// this.upper = lower;
// }
// }
//}
//对应的原始类型
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
public Interval(){}
//...
}
5.2翻译泛型表达式
程序调用泛型方法时,若擦除返回类型,编译器插入强制类型转换;
//Pair<Employee> buddies = ...;
//Employee buddy = buddies.getFirst();
//其中编译器可以把这个方法调用翻译为两条虚拟机指令:①对原始方法Pair.getFirst的调用;②把返回的Object类型强制转换为Employee类型;
//存取泛型域时也要插入强制类型转换;
//Employee buddy = buddies.first;(若first域是公有的)
5.3翻译泛型方法
- 当然类型擦除也会出现在泛型方法中;
// public static <T extends Comparable> T min(T[] a){
// //...
// }
//擦除类型之后
public static Comparable min(Comparable[] a){
//...
}
- 类型擦除引起的问题及解决方法可参考笔记:Java泛型类型擦除以及类型擦除带来的问题 - 蜗牛大师 - 博客园
- 总结:
- 虚拟机中没有泛型,只有普通的类和方法;
- 所有的类型参数都用它们的限定类替换;
- 桥方法被合成来保持多态;
- 为保持类型安全性,必要时插入类型转换;
5.4调用遗留代码
- 设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作;……
6.约束与局限
6.1不能用基本类型实例化类型参数
- 由于类型擦除的原因,Java泛型中不能用基本类型实例化类型参数;
List<Double> list = new ArrayList<>();
//编译不通过
List<double> list2 = new ArrayList<>();
//主要原因是类型擦除之后List类有Object类型的域,Object可以存储Double引用,而不能存储double的值;
6.2运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型;因此,所有的类型查询只产生原始类型;
//检查a是否是任意类型的一个Pair
// if(a instanceof Pair<String>) //Error
// if(a instanceof Pair<T>)
//同样也可用强制类型转换检查
// Pair<String> p = (Pair<String>)a;
Pair<String> stringPair = new Pair<>();
Pair<Integer> integerPair = new Pair<>();
//getClass方法返回原始类型
if(stringPair.getClass() == integerPair.getClass())
System.out.println("res: true");
6.3不能创建参数化类型的数组
- 若想收集参数化类型的对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair>;
Pair<String>[] nums = new Pair<String>[10]; //Error
Pair<String>[] table =(Pair<String>[]) new Pair<?>[10]; //可以创建但是不安全
6.4Varargs警告
6.5不能实例化类型变量
6.6不能构造泛型数组
类似于不能实例化一个泛型实例,也不能实例化数组;
//ArrayList的最基本实现方式:Object
public class ArrayListW<E> {
private Object[] elements;
public ArrayListW(){
elements = (E[])new Object[10];
}
public static void main(String[] args){
List<String> list = new ArrayList<>();
}
}
6.7泛型类的静态上下文中类型变量无效
- 不能在静态域或方法中引用类型变量;下面的代码是错误的:
public class Singleton<T> {
private static T singleInstance;
public static T getSingleInstance(){
if(singleInstance == null)
return singleInstance;
}
}
6.8不能抛出或捕获泛型类的实例
- 即不能抛出也不能捕获泛型类对象;泛型类扩展Throwable都是不合法的;但是异常规范中使用类型变量是允许的;如以下代码:
public class Problem<T> extends Exception { //Error---can't extend Throwable
public static <T extends Throwable> void doWork(Class<T> tClass){
try{
//do work
}catch (T e){ //Error--can't catch type variable
Logger.global.info("...");
}
}
public static <T extends Throwable> void doWorkTwo(T t) throws T{ //OK
try{
//do work
}catch (Throwable realCause){
t.initCause(realCause);
throw t;
}
}
}
6.9可以消除对受查异常的检查
- 其实可以利用泛型消除,受查异常需要处理器的限制;也就是可把受查异常”包装“成非受查异常;其实是”哄骗“编译器,让它把受查异常认为是非受查异常;
/**
* 此类的设计目的是:
* 用户可覆盖body方法提供一个具体的动作;调用toThread时,会得到Thread类的一个对象,而它的run方法不会介意受查异常;
*/
public abstract class Block {
public abstract void body() throws Exception;
public Thread toThread(){
return new Thread(){
public void run(){
try{
body();
}catch (Throwable t){
Block.<RuntimeException>throwAs(t);
}
}
};
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e)throws T{
throw (T) e;
}
}
6.10注意擦除后的冲突
- 泛型类型被擦除后,有可能会发生冲突;如下代码中的equals方法:
public class Pair<T> {
private T firstNumber;
private T secondNumber;
public Pair(T firstNumber, T secondNumber){
this.firstNumber = firstNumber;
this.secondNumber = secondNumber;
}
public Pair(){
firstNumber = null;
secondNumber = null;
}
public T getFirstNumber(){
return firstNumber;
}
public void setFirstNumber(T firstNumber){
this.firstNumber = firstNumber;
}
public T getSecondNumber(){
return secondNumber;
}
public void setSecondNumber(T secondNumber){
this.secondNumber = secondNumber;
}
//Error:
//'equals(T)' in 'genericity.Pair' clashes with 'equals(Object)' in 'java.lang.Object';
// both methods have same erasure, yet neither overrides the other
//当然,补救方式肯定是重新命名此方法~~~
public boolean equals(T value){
return firstNumber.equals(value) && secondNumber.equals(value);
}
public static void main(String[] args){
Pair<String> pair = new Pair<>("wwf", "fsy");
System.out.println("husband:" + pair.getFirstNumber());
System.out.println("wife:" + pair.getSecondNumber());
}
}
泛型规范提到的另一个原则:想支持擦除的转换,需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化;
7.泛型类型的继承规则
- 无论S和T有什么联系,通常Pair<S>和Pair<T>没有什么联系!!!
虽然这限制过于严苛,但对于类型安全非常有必要;因为子类对象赋值给父类引用后,对其可修改的话可能会出现安全问题;具体看如下代码:
Pair<Manager> managerPair = new Pair<>(ceo, cfo);
Pair<Employee> employeePair = managerPair; //illegal, but suppose it wasn't
employeePair.setFirstNumber(lowlyEmployee);
- 这里必须要把泛型和Java数组间的区别理解清楚;因为Java数组中支持子类型的数组赋值给父类型的数组;原因是数组有特殊保护,存储不符合的值会抛出ArrayStoreException;
- 永远都可以把参数化类型转换成一个原始类型(如下代码);在与遗留代码衔接时这个很重要;
Pair<Integer> pair = new Pair<>();
Pair rawPair = pair;
rawPair.setFirstNumber(1);
System.out.println(rawPair.getFirstNumber());
- 泛型类可以扩展或实现其他的泛型类;