初识Java泛型以及桥接方法

时间:2023-12-27 11:32:37

泛型的由来

在编写程序时,可能会有这样的需求:
容器类,比如java中常见的list等。为了使容器可以保存多种类型的数据,需要编写多种容器类,每一个容器类中规定好了可以操作的数据类型。
此时可能会有IntList、LongList、StringList、XXXList等多个List容器类,但是容器类内部的各个方法,除了add、remove、get方法操作的数据类型不同意外,几乎全部相同。为了方便起见,可以使用ObjectList,但是在get的数据是Object类型,需要进行强制类型转换,这就会造成一些程序运行时的隐患。

通过查询网络资料时,发现这个需求对于泛型的需要最迫切。于是在jdk1.5(2004年)中,java加入了泛型。

什么是泛型

泛型:参数化类型。将类类型,作为参数,进行传递
泛型参数,可以为多个,使用尖括号括起来。
比如,通过实例化List容器时,指定容器类类型String,实现了String容器。

List<String> list= new LinkedList<String>();
list.add("1");//没有编译错误
list.add(1);//编译错误

如何实现泛型

在java中,可实现泛型接口、泛型类、泛型方法。

1、泛型接口:常见的List接口,Map接口,都是泛型接口。
举例:

public interface Test<T>{
  public void add(T element);
  public T get(int index);
}

2、泛型类:LinkedList、ArrayList、HashMap、TreeMap等都是泛型类,并且他们实现了泛型接口;

举例:

public class Target<T, E> implements Test<T>{
private Object[] list=new Object[100];
private int index=0;
public void add(T element){
list[index++]=element;
}
public T get(int index){
return (T)list[index];
}
}

3、泛型方法:同泛型接口和泛型类的定义稍微有些不同。

举例:

public <T> T add( Class<T> clz){
T instance=clz.newInstance();
return instance
}

在定义泛型接口、类、方法时,可以添加多个泛型形参,一般使用大写字母表示,并用逗号分隔,泛型定义处,使用尖括号将所有泛型形参括起来。

泛型接口、类的泛型形参,放置在接口或类名的后面。
泛型方法的泛型形参,放置在方法返回类型的前面即可。

注意,java的泛型属于code share,只是在编译时使用,用于检查传递给泛型类、方法的数据类型是否符合泛型定义,泛型类和方法中的泛型会在编译成功后擦出,成为Object类型,编译器并会在泛型数据操作时,自动添加类型转换。由于编译期间保证了泛型类型的正确使用,因此自动添加的类型转换是安全的。
举例:
使用List容器,调用其方法

public class Main{
public static void main(String[] args){
List<Long> ttt=new LinkedList<Long>();
tt.add(1L);
Long value=tt.get(0);
}
}

在编译后的class文件,可翻译为:

public class Main{
public static void main(String[] args){
List ttt=new LinkedList();
tt.add(1L);
Long value=(Long)tt.get(0);
}
}

编译时,编译器会检查凡是使用泛型类实例的地方,是不是按照泛型的规定,进行传参的。

比如ttt.add(1L),编译器会检查传入的参数是不是Long型;还有tt.get(0),编译器会自动加上类型转换。最终看到编译后,泛型类的泛型被擦出了。
类型擦出导致,以下输出会为true,因为泛型类不会因为不同的泛型类型,导致生成不同的类。

List<String> list1=new LinkedList<String>();
List<Integer> list2=new LinkedList<Integer>();
System.out.println(list2.getClass()==list1.getClass());//输出为true

泛型定义时的约束

定义泛型形参时,可以使用extends限制泛型的范围。
比如使用<T extends K>,表示T必须是类K的子类,或者接口K的实现类。
比如使用<T super K>,表示T必须是类K的父类,或类K实现的接口。
注意:extends并不表示类的继承含义,只是表示泛型的范围关系。
注意:extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。

java泛型实现的原理

在C++中也有泛型的概念,但java的泛型实现原理同C++是不一样的。
泛型实现原理有两种:
1、为使用的泛型类,单独生成一份非泛型的具体类。
比如List<String> list=new ArrayList<String>,在程序编译时,会单独生成一份StringArrayList类,并且类里面所有的操作元素都是String类。
2、采用擦出机制,泛型接口或类中,所有使用泛型形参的地方,全部擦除,替换为Object类型(java中所有类的父类)。所有相同泛型类的实例共享使用泛型类的代码。在泛型类的实例进行数据操作的地方(泛型类外部,自定义的程序部分),由编译器检查操作的参数是否为泛型类实例定义时的类类型,必要时自动添加强制类型转换。

在擦除时,如果<T extends XXClass>,T擦出后会变为XXClass。
如果<T extends AClass,BClass>,T擦出后,类型为AClass,以最左边的类型为最终类型,这点要注意。

桥接方法

首先举一个例子。

public class A<T>{
public T get(T a){
//a进行一些操作
return a;
}
}
public class B extends A<String>{
@override
public String get(String a){
//a进行一些操作
return a;
}
}

由于类型擦出机制的存在,按理说编译后的文件在翻译为java应如下所示

public class A{
public Object get(Object a){
//a进行一些操作
return a;
}
}
public class B extends A{
@override
public String get(String a){
//a进行一些操作
return a;
}
}

@override意味着B对父类A中的get方法进行了重写,但是依上面的程序来看,只是重载,依然可以执行父类的方法,这和期望是不附的,也不符合java继承、多态的特性。

注:子类重写父类方法,只要此实例的实际类型是子类类型了(不是引用类型),便不能调用父类的方法,这是语言的多态特性。

为了解决这个问题,java在编译期间加入了桥接方法。编译后再翻译为java原文件,应如下所示:

public class A{
public Object get(Object a){
//a进行一些操作
return a;
}
}
public class B extends A{
@override
public String get(String a){
//a进行一些操作
return a;
}
public Object get(Object a){
return get((String)a)
}
}

类B中的第二个方法,即为桥接方法。桥接方法重写了父类相同的方法,并且桥接方法中,最终调用了期望的重写方法,并且桥接方法在调用目的方法时,参数被强制转换为指定的泛型类型。

桥接方法测试:

A a=new B();
a.get(new Object());

由于A未声明泛型类型,编译期间会警告存在类型转换错误。

运行期间,还真抛出了类型转换错误,Object to String。

实例a实际类型为B,在调用a.get方法时最终调用了B类中的桥接方法,保证了传入参数为子类继承父类使用的泛型类型。