java泛型详解

来源:互联网 发布:智能后视镜安装软件 编辑:程序博客网 时间:2024/06/10 23:43

一、概念

百度百科:

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java语言引入泛型的好处是安全简单。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。

根据《Java编程思想 (第4版)》中的描述,泛型出现的动机在于:

有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。

二、泛型类

容器类应该算得上最具重用性的类库之一。先来看一个没有泛型的情况下的容器类如何定义:

public class Container {    private String key;    private String value;    public Container(String k, String v) {        key = k;        value = v;    }    //省略Setter Getter}

Container类保存了一对key-value键值对,但是类型是定死的,也就说如果我想要创建一个键值对是String-Integer类型的,当前这个Container是做不到的,必须再自定义。那么这明显重用性就非常低。

当然,我可以用Object来代替String,并且在Java SE5之前,我们也只能这么做,由于Object是所有类型的基类,所以可以直接转型。但是这样灵活性还是不够,因为还是指定类型了,只不过这次指定的类型层级更高而已,有没有可能不指定类型?有没有可能在运行时才知道具体的类型是什么?

所以,就出现了泛型。

public class Container<K, V> {    private K key;    private V value;    public Container(K k, V v) {        key = k;        value = v;    }    public K getKey() {        return key;    }    public void setKey(K key) {        this.key = key;    }    public V getValue() {        return value;    }    public void setValue(V value) {        this.value = value;    }}

在编译期,是无法知道K和V具体是什么类型,只有在运行时才会真正根据类型来构造和分配内存。所以容器可以支持不同的类型

Container<String, String> c1 = new Container<String, String>("name", "findingsea");Container<String, Integer> c2 = new Container<String, Integer>("age", 24);Container<Double, Double> c3 = new Container<Double, Double>(1.1, 2.2);

三、泛型接口

在泛型接口中,生成器是一个很好的理解,看如下的生成器接口定义:

public interface Generator<T> {    public T next();}

然后定义一个生成器类来实现这个接口:

public class FruitGenerator implements Generator<String> {    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};    @Override    public String next() {        Random rand = new Random();        return fruits[rand.nextInt(3)];    }}

四、泛型方法

一个基本的原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。下面来看一个简单的泛型方法的定义:

public class Main {    public static <T> void out(T t) {        System.out.println(t);    }    public static void main(String[] args) {        out("findingsea");        out(123);        out(11.11);        out(true);    }}

可以看到方法的参数彻底泛化了,这个过程涉及到编译器的类型推导和自动打包,也就说原来需要我们自己对类型进行的判断和处理,现在编译器帮我们做了。这样在定义方法的时候不必考虑以后到底需要处理哪些类型的参数,大大增加了编程的灵活性。

再看一个泛型方法和可变参数的例子:

public class Main {    public static <T> void out(T... args) {        for (T t : args) {            System.out.println(t);        }    }    public static void main(String[] args) {        out("findingsea", 123, 11.11, true);    }}

输出和前一段代码相同,可以看到泛型可以和可变参数非常完美的结合。

五、泛型通配符

使用大写字母A,B,C,D……X,Y,Z定义的,就都是泛型,把T换成A也一样,这里T只是名字上的意义而已

  • ? 表示不确定的java类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值对中的Key Value
  • E (element) 代表Element

将T换成A,在执行效果上是没有任何区别的,只不过我们约定好了T代表type,按照约定规范来,增加了代码的可读性。

  • ArrayList al=new ArrayList(); 指定集合元素只能是T类型
  • ArrayList
List<Object> objects = new ArrayList<String>(); //合法吗?

乍看起来,似乎是正确的,因为String是Object的子类,因而集合中任何一个String类型的元素都是有效的Object对象。但是

List<Object> objects = new ArrayList<String>(); //合法吗?objects.add(new Object()); //如果合法,那么这句呢?

既然objects的类型声明为List< Object >,那么就能将Object实例存入其中;但是,这个实例保存的元素都是字符串,尝试存入的Object对象与其并不兼容。因而这种写法是不合理的,也就是说,“ List< String > 并不是 List< Object > 的子类” 如果想让容器的类型具有父子关系,需要使用未知类型:

List<?> objects = new ArrayList<String>();

使用Class和Class

People people =(People)Class.forName("com.lyang.demo.fanxing.People").newInstance();

需要强转,如果反射的类型不是People类,就会报java.lang.ClassCastException错误。使用Class泛型后,不用强转了。

public class Test {    public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {        return clazz.newInstance();    }    public static void main(String[] args)  throws IllegalAccessException, InstantiationException  {            Fruit fruit= createInstance(Fruit .class);            People people= createInstance(People.class);    }}

那Class< T >和Class< ? >有什么区别呢?
Class< T >在实例化的时候,T要替换成具体类。
Class< ? >它是个通配泛型,?可以代表任何类型,主要用于声明时的限制情况。

public Class<?> clazz; //合理public Class<T> clazz; //不合理

六、类型擦除

Java 泛型中的类型参数只在编译时可见,javac 会去掉类型参数,这个被称为类型擦除(type erasure)。在生成的Java字节代码中是不包含泛型中的类型信息的,但会保留泛型的一些踪迹,在运行时通过反射可以看到。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

Java 5中添加的泛型是一个新增加的语言特性,而类型擦除正是解决后向兼容型的保证。旧的非泛型集合类和新的泛型集合类是可以兼容的:

List someList = getSomeThings();List<String> stringList = (List<String>)someList;

这是因为在经过了编译器的类型擦除后,对 JVM 来说看到的都是List。非泛型的List一般成为原始类型。类型擦除机制会导致一些看上去比较奇怪的现象:

//无法编译interface OrderCounter {    int totalOrders(Map<String, List<String>> orders);    int totalOrders(Map<String, Integer> orders);}

上述代码乍看上去是合法的,但实际上并不能进行编译。因为在类型擦除以后,两个方法的签名都是:

int totalOrders(Map);

运行时无法通过签名区分这两个方法,因而Java规范把这种句法列为非法的。

关于类型擦除需要记住的几点是:

  • 泛型类并没有自己独有的Class类对象。比如并不存List< String >.class或是List< Integer >.class,而只有List.class。在经过类型擦除后剩下的只有原始类型,无论是List< String >或是List< Integer >,对JVM来说都看作List。
  • 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass< String > 还是newMyClass< Integer >创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException< String >和MyException< Integer >的。对于JVM来说,它们都是MyException类型的。也就无法执行与异常对应的catch语句。

七、泛型的问题

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于List< String >和List< Object >这样的情况,类型参数String是继承自Object的。而第二种指的是List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

  1. 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List< String >是Collection< String >的子类型,List< String >可以替换Collection< String >。这种情况也适用于带有上下界的类型声明。
  2. 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对Collection< ? extends Number>来说,其子类型可以在Collection这个维度上展开,即List< ? extends Number>和Set< ? extends Number>等;也可以在Number这个层次上展开,即Collection< Double >和 Collection< Integer >等。如此循环下去,ArrayList< Long >和HashSet< Double >等也都算是Collection< ? extends Number>的子类型。
  3. 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

创建泛型数组在Java中是不被允许的。需要适用类型转换来创建泛型数组。

T[] a = (T[])new Object[10];
原创粉丝点击