欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

JAVA泛型浅析(深入讲解)

java water 3735℃ 0评论

本文主要列举了在使用Java泛型时应该注意的问题。Java泛型是Java5的一个重要特性,它和自动装箱、变长参数等新特性一起,提升了Java代码的健壮性和易用性,但SUN本身过分强调向前的兼容性,也引入了不少问题和麻烦。[@more@]

JAVA泛型和C++泛型的区别:

Java的泛型被定义成擦除,而C++的泛型则是扩展;

对于C++模板,当参数类型为不同的类型时,生成的模板实例也是不同的类型,如:定义类模板

Template <typename T> class A : public B<T>;

当实例化模板时

A<int> a;

A<std::string> b;

这里的ab是两种不同的类型的实例;

Java则不是这样的,如泛化List<E>,分别用IntegerString来实例化,

List<Integer> l

List<String> s

通过反射机制看lsclass,他们都是List!所有的参数类型都被编译器擦除了!

这样造成的结果是以往用C++模板可以实现某种契约(contract )的功能在Java中变得很另类了,举一个例子:

C++代码:

Template <typename T > class A{

Private:

T x;

Public:

Void func(){

int ret = x.foo(12);

}

}

上面这段代码在C++中经常能够看到,它暗中就设定了一个契约:凡是能否实例化这个模板的类型T,其必须具有一个公共的函数foo,这个函数返回整数,并且接受一个整数做为参数。

Java的代码:

public class A<E>{

private E e;

public void func(){

int ret = e.foo(12); //编译错误啊

}

}

编译器给出的错误原因是foo函数对于类型中E是未定义的!!造成这样的问题的原因有两个:

1、 C++的编译器直到模板被使用(实例化)的时候才去编译模板,如果你不去使用模板C++编译器不会编译模板;而实例化的时候编译器已经能够确定具体的参数类型,所以能够检测契约是否符合;Java的编译器不是这样工作的,所以它在编译模板类型的时候不能够确定E到底有没有这个foo函数;

2、 类型擦除的结果;修改一下上面的程序,我们看看在func中到底能够调用什么函数,一看只能调用Object对象中的函数。所有的类型E都被擦除成Object了!

如果真的要想实现类似C++的契约,就必须确保参数类型E不被擦除成Object!需要如下修改代码:

public class A<E extends B>{

private E e;

public void func(){

int ret = e.foo(12);

}

}

Class B{

Public int foo(int param){…};

}

这样虽然可以实现我们期望的形式,但是约束的程度要比C++的强很多,C++中,只要任意类型,其具有一个符合契约的函数,就可以实例化模板,而Java中,则要求所有的类型必须是给定类型的子类才可以实例化模板;

擦除的原则:

1、 所有参数化容器类都被擦除成非参数化的(raw type);如List<E>List<List<E>>都被擦除成List

2、 所有参数化数组都被擦除成非参数化的数组;如List<E>[],被擦除成List[]

3、 Raw type的容器类,被擦除成其自身,如List 被擦除成List

4、 原生类型(int,String还有wrapper类)都擦除成他们的自身;

5、 参数类型E,被擦除成Object

6、 所有约束参数如<? Extends E><X extends E>都被擦除成E

7、 如果有多个约束,擦除成第一个,如<T extends Object & E>,则擦除成Object

例如:

泛化代码:

                 List<String> words = new ArrayList<String>();
                               words.add("Hello ");
                               words.add("world!");
                               String s = words.get(0)+words.get(1);
         擦除后就变成了:
                 List words = new ArrayList();
                               words.add("Hello ");
                               words.add("world!");
                               String s = ((String)words.get(0))+((String)words.get(1))
         擦除后的代码和以前没有泛型时候写的代码没有任何区别!
     再例如:
         泛化代码:

public class textReader<T>{

private T a;

public textReader(T b){

this.a = b;

}

public T getA(){

return a;

}

public static void main(String[] agrvs){

String in = “1234567890”;

textReader<String> test = new textReader<String>(in);

String out = test.getA();

System.out.println(out);

}

               }
               擦除后(所有类型参数都被去掉,T被擦除成Object)就变成(注意红色部分)

public class textReader{

private Object a;

public textReader(Object b){

this.a = b;

}

public Object getA(){

return a;

}

public static void main(String[] agrvs){

String in = “1234567890”;

textReader test = new textReader (in);

String out = (String)test.getA();

System.out.println(out);

}

               }

擦除所带来的问题:

1、 静态成员共享问题

        List<Integer> ints = Arrays.asList(1,2,3);
               List<String> strings = Arrays.asList("one","two");
               assert ints.getClass() == strings.getClass();

intsstrings两个对象最终被擦除成具有相同类型的(List)的对象,于是这两个对象共享List的静态成员,于是就可以得出这样的结论,所有泛化类型的静态成员被其所有的实例化对象共享,因此也就要求所有静态成员不能够是泛化的!

class Foo<T> {
  private final T value;
  private static List<T> values = new ArrayList<T>(); //非法
  public T getValue() { return value; }
                   public static List<Object> values =  new ArrayList<Object>() // ok
}

2、 过载(overload)冲突问题

函数过载的定义这样的:在一个类的范围内,如果两个函数具有相同的函数名称,不同的参数(返回值不考虑)就互相称为过载函数。看一个例子:

Class A{

Public int foo(int a){};

Public int foo(float f){}; // 是过载,编译没有问题

Public int foo(int a){};

Public float foo(int f){}; // 报错

Public static int foo1(List<Integer> a){}

Public static int foo1(List<String> s){} //编译有错误,因为所有的List<T>都被擦除成List,这样两个函数重复定义,报错;

Public static int foo1(List<Integer> a){}

Public static String foo1(List<String> s){} //没有问题,编译器不会报错!

}

3、 接口实现

一个类不能同时实现具有相同擦除效果的接口,例如:

class Foo implements Comparable<Integer>, Comparable<Long>

Comparable<Integer>, Comparable<Long>都被擦除成Comparable

继承关系

1、 原始继承:就是我们经常提到的继承关系,如ArrayListList的子类;

2、 泛化继承:

a) 泛化后的ArrayList<T>依旧是List<T>的子类;其中T是参数化类型

b) 如果类型T是类型B的子类,那么List<T>不是List<B>的子类

c) List<T>List<? extends T>的子类

d) List<T>List<? extends B>的子类

e) List<T>List<? super T>的子类

f) List<B>List<? super T>的子类

g) 如果类型T是类型B的子类,那么T[]B[]的子类

3、 关于协变式(covariant)、不变式(invariant)和反变式(contravariant)

a) 数组和扩展类(extends)的泛化是协变式,即如果类型T是类型B的子类,那么T[]B[]的子类;List<T>List<? extends B>的子类

b) 非扩展类泛型是不变式,即如果类型T是类型B的子类,那么List<T>不是List<B>的子类

c) Super类泛型是反变式,即如果BT的超类,则List<B> List<? super T>的子类

GetPut原则:

当从一个泛化的结构中取数据的时候请使用extends通配,当往一个泛化的结构中放数据的时候请使用super通配;

当需要同时从一个泛化结构中读取和写入数据是,请不使用通配符号;

为什么会这样,我们简单的分析一下:假定类型T继承自AB,类型CD又从T类型继承,那么List<? extends T>中存放的只能是T类型或是C或是D类型,里面存放的类型都可以向上castT,所以从List<? extends T>中取东西编译器能够正确处理,只要映射到T就可以了(T是他们的父类)!往List<? extends T>放东西就不一样的,原来里面放的是T/C还是D都被擦除成T,所以编译器不知道原来到底存放的是什么类型,无法保证类型安全,所以这个操作被禁止!

List<? super T>中存放的是A/B或是T,往List<? super T>T是允许的,因为T总是可以向上转换成A或是B,但是从里面取东西就有问题了,编译器还是不能够确定List里面放的是什么类型,可能是A也可能是B

具体化:

当一个类型能够在运行时态被完整的表现,我们就称为其是可以具体化的,举一个例子:

List<Integer>就不是一个可以具体化的类型,因为它在运行时态被擦除成List!所以当处理一些需要运行时检测类型的操作的时候(如instanceof)就要特别注意。

究竟哪些类型是可具体化的呢;

1)、原始类型,如int

2)、非参数化的类和接口;

3)、非限定的参数化类型,如List<?>, Map<?,?>

4)、Raw类型,如 List,ArrayList

5)、所有由可具体化类型组成的数组,如Number[]List<?>[]

哪些是不可具体化的类型呢

1)、类型变量,如T;

2),参数化类型,如List<Number>

3),有限定的参数化类型,如List<? extends Number>;

具体哪些操作需要注意区分是否是具体化类型:

转载请注明:学时网 » JAVA泛型浅析(深入讲解)

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!