泛型
意义 (why?)
-
类型参数化,使得同一个类、方法、接口可以根据传入的参数类型,进行各自表达。代码复用。‘
例如下面代码:
private static int add(int a, int b) { System.out.println(a + "+" + b + "=" + (a + b)); return a + b; } private static float add(float a, float b) { System.out.println(a + "+" + b + "=" + (a + b)); return a + b; } private static double add(double a, double b) { System.out.println(a + "+" + b + "=" + (a + b)); return a + b; }
以上三个方法,代表不同类型的加法,如果没有泛型,需要给3个结构类似的方法,违背了代码复用原则。
现在,有泛型后的代码
private static <T extends Number> double add(T a, T b) {
System.out.println(a + “+” + b + “=” + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
现在数值的加法通过上面方法就可以实现
-
类型约束
在编译期间,验证对象存储类型是否在规定泛型范围内,如果错误,直接抛出类型转换异常:java.lang.ClassCastException。程序更加安全
种类(what?)
- 类
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
}
public class GenericsDemo06{
public static void main(String args[]){
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
p.setVar("it") ; // 设置字符串
System.out.println(p.getVar().length()) ; // 取得字符串的长度
}
}
- 多元泛型
class Notepad<K,V>{ // 此处指定了两个泛型类型
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
public K getKey(){
return this.key ;
}
public V getValue(){
return this.value ;
}
public void setKey(K key){
this.key = key ;
}
public void setValue(V value){
this.value = value ;
}
}
public class GenericsDemo09{
public static void main(String args[]){
Notepad<String,Integer> t = null ; // 定义两个泛型类型的对象
t = new Notepad<String,Integer>() ; // 里面的key为String,value为Integer
t.setKey("汤姆") ; // 设置第一个内容
t.setValue(20) ; // 设置第二个内容
System.out.print("姓名;" + t.getKey()) ; // 取得信息
System.out.print(",年龄;" + t.getValue()) ; // 取得信息
}
}
- 方法
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
//返回值必须有 <T>
-
接口
interface Info<T>{ // 在接口上定义泛型 public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型 } class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类 private T var ; // 定义属性 public InfoImpl(T var){ // 通过构造方法设置属性内容 this.setVar(var) ; } public void setVar(T var){ this.var = var ; } public T getVar(){ return this.var ; } } public class GenericsDemo24{ public static void main(String arsg[]){ Info<String> i = null; // 声明接口对象 i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象 System.out.println("内容:" + i.getVar()) ; } }
-
上下限
上限 <? extengs class>
class Info<T extends Number>{ // 此处泛型只能是数字类型
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class demo1{
public static void main(String args[]){
Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
}
}
下限<? super class>
class Info<T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class GenericsDemo21{
public static void main(String args[]){
Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
i1.setVar("hello") ;
i2.setVar(new Object()) ;
fun(i1) ;
fun(i2) ;
}
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
System.out.print(temp + ", ") ;
}
}
结论:上限用于生产,下限用于消费。
用途(how?)
类型擦除
-
消除类型参数声明,即删除
<>
及其包围的部分。 -
根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
-
为了保证类型安全,必要时插入强制类型转换代码。
-
自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
ArrayList<String> list=new ArrayList();//这行在编译期间 会转化===>ArrayList<Object> list=new ArrayList();
//即String会往它的最大级父类转化
ArrayList<? extends Number> list=new ArrayList();会转化===>ArrayList<Number> list=new ArrayList()
我们可以通过Class对象来验证以上观点:
public class Test {
public static void main(String[] args) {
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 原本一个存储String一个存储Integer
//,但是经过类型擦除后,都为Object,所以它们的Class相同。
}
}
还可以通过反射的方式验证:
ublic class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");//因为反射作用于运行期间,已经完成了
//类型擦除,Integet==》Object,故可以存储字符串类型,下面输出语句。输出两个元素:1和"asd"
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
那么,类型擦除后的原始类型是什么?
如果没有上下限界定,就是Object
如果有界定,例子如下
public class Pair<T extends Comparable> {}
//那么原始类型就是 Comparable了
在调用泛型方法时,要注意是否使用了泛型。
-
在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object
-
在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类
public class Test { public static void main(String[] args) { /**不指定泛型的时候*/ int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型 Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object /**指定泛型的时候*/ int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类 int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float } //这是一个简单的泛型方法 public static <T> T add(T x,T y){ return y; } }
编译期间泛型检查顺序
编译前先检查代码中泛型类型,检验通过后再进行泛型擦除,再进行编译
检查针对的类型是引用,即左边
ArrayList<String> list1 = new ArrayList(); //第一种 情况,引用中有泛型限定,编译前检查元素类型为String。
ArrayList list2 = new ArrayList<String>(); //第二种 情况 左边没有泛型限定,默认Object,无需检查。
//new ArrayList()只是在内存中开辟了空间,存储任何对象。
泛型的继承?不考虑,不支持
如下例子:
ArrayList<String> list1 = new ArrayList<Object>(); //编译错误
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误
//泛型设计的初衷就是避免 ClassCastException和向下转型的问题。
泛型下继承的桥方法
场景:父子继承体系统,子类继承父类某个类型的方法,但是由于泛型擦除的步骤,父类的方法参数升级为Object,但是子类重写方法还是父类指定的类型参数,导致从 宏观上看来是重载并非重写。如何解决?
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//父类指定参数类型为Date,子类顺利继承,并且重写父类两个方法
//由于泛型擦书,导致父类参数类型变为Object,而子类还是Date,由重写变为重载。
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
解决方式:JVM采用了桥反方。编译器除了给子类生成两个重写的方法外,还进行了2个和父类参数类型一致的方法,也就是说子类真是实现父类的方法是编译器生产的方法,成为桥方法,而桥方法内部的调用就是我们自己重写的两个方法
泛型无法实例化
因为在编译期间通过类型擦除无法决定参数化类型,找不到对应字节码文件。擦除后类型为Object,失去了本意。可以通过反射实现。