Java:泛型

一、序言

变化一:

在引入范型之前,Java中的类型分为原始类型、复杂类型,其中复杂类型分为数组和类;引入范型后,一个复杂类型可以细分成更多的类型。

例如,原先的List类型,现在细分成List<Object>, List<String>等更多的类型。

注:List<Object>和List<String>是两种不同的类型, 它们之间没有继承关系,即使String继承了Object。下述代码是非法的:

List<String> ls = new ArrayList<String>();
List<Object> lo = ls;

这样设计的原因:根据lo的声明,编译器允许你向lo中添加任意对象(例如Integer),但是为其赋值List<String>类型的值,破坏了数据类型的完整性。

变化二:

在引入范型之前,要让类中的方法支持多个数据类型,就需要对方法进行重载;在引入范型之后,可以更进一步定义多个参数以及返回值之间的关系。

例如,public void write(Integer i, Integer[] ia);及public void write(Double  d, Double[] da);的范型版本为:public <T> void write(T t, T[] ta);

二、详述Java泛型

在细说Java泛型之前,大家可以先看一下如下这样一段代码,相信大家肯定不会感觉陌生:

package com.soft;

public class WW {
    public <T> void write(T t, T[] ta){

    }
}

在编码时,可经常见到类似于上面这样的代码,接下来将详述Java泛型。

那么我们首先要知道什么是泛型,泛型即“参数化类型”,顾名思义,就是将类型由原来具体的类型变为参数化的形式。在定义泛型接口、泛型类和泛型方法的过程中,我们常见的T、E、S等形式的参数用于表示泛型形参,用于接收来自外部使用时候传入的类型实参。

A、自定义泛型类和泛型方法

(1)定义带类型参数的类

package com.soft;

public class MM<T, S extends T> {
    public T say(){
        T t=null;
        return t;
    }
}

规则:在定义带类型参数的类时,在紧跟类命之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用","进行分隔。

说明:定义完类型参数后,可以在类中定义位置之后的几乎任意地方使用类型参数(静态块,静态属性,静态方法除外),就像使用普通的类型一样。

注意:父类定义的类型参数不能被子类继承。

(2)定义带类型参数方法

package com.soft;

public class WW {
    public <T> void write(T t, T[] ta){

    }
    public <S> void read(S s, S[] sa){

    }
    public <T, S extends T> T test(T t, S s){
        S ss=null;     return ss;
    }
}

规则:在定义带类型参数的方法时,在紧跟可见范围修饰(如public)之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用","进行分隔。

说明: 定义完带类型参数的方法后,可以在方法中定义位置之后的任意地方使用类型参数,就像使用普通的类型一样。

注意:定义带类型参数的方法,其主要目的是为了表达多个参数以及返回值之间的关系。例如本例中T和S为继承关系,返回值的类型(T)和第一个类型参数(t)的类型(T)相同。

补充:

当我们需要一个在逻辑上可以用来表示同时是List<Integer>和List<Number>的类型时,类型通配符应运而生。类型通配符一般是使用"?"代替具体的类型实参。

如果仅仅是想实现多态,请优先使用通配符解决。

初始代码如下:

public <T> void test1(List<T> u){
     ...
}
public <S> void test2(List<S> u){
     ...
}

使用通配符的代码如下:

public void test(List<?> s){
     ...
 }

B、泛型类和泛型方法的使用

(1)对带类型参数的类进行赋值

对带类型参数的类进行赋值有两种方式:

A、声明类变量或者实例化时,如下:

List<String> list;
list = new ArrayList<String>();

如下赋值方式会报错:

List<Integer> a = new ArrayList<Integer>(712);
List<Number> b = a;

List<Number>不能视为List<Integer>的父类,请参照前面“序言->变化一”中的描述。

B、继承类或者实现接口时,如下:

public class MyList<E> extends ArrayList<E> implements List<E> {...}

(2)对带类型参数的方法进行赋值

当调用泛型方法时,编译器自动对类型参数进行赋值,当不能成功赋值时报编译错误,如下:

package com.soft;

import java.util.List;

public class WW {public <T, S extends T> T test(T t, S s){
        return s;
     }
    public <T> T test1(T t, List<T> list){
        return t;
     }
    public <T> T test2(List<T> list1, List<T> list2){
       return null;
    }

    public void common(){

         Object o = null;
         Integer i = null;
         Number n = null;
         test(n, i);//此时T为Number, S为Integer
         test(o, i);//T为Object, S为Integer

         List<Integer> list1 = null;
         List<Number> list2 = null;
         test1(i, list1);//此时T为Integer,i为Integer类型,list1为List<Integer>类型
         test1(i, list2);//此时T为Number,i为Integer类型,list2为List<Number>类型
test2(list1, list2);//编译报错
    }

}

请读者自行分析上述代码中着色语句为什么会编译报错,很简单的哦O(∩_∩)O哈哈~

(3)对通配符类型参数进行赋值

在上面两小节中,对是类型参数赋予具体的值,除此,还可以对类型参数赋予不确定值

假设有如下定义:

List<?> unknownList;
List<? extends Number> unknownNumberList;
List<? super Integer> unknownBaseLineIntgerList; 

现对上述定义的变量赋值,如下:

List<String> listString = null;
unknownList = listString;

三、类型通配符上限与下限

有时候,我们会听到类型通配符上限和类型通配符下限这样的说法,其实在前面的例子中有下面这样一小段代码,在该代码中已经体现出一些上下限的思想

public <T, S extends T> T test(T t, S s){
      S ss=null;
    return ss;
}

此处我们进一步说明

package com.soft;

public class GenericTest {

    public static void main(String[] args) {

        Student<String> name = new Student<String>("xiaoming");
        Student<Integer> age = new Student<Integer>(24);
        Student<Number> number = new Student<Number>(1001);

        getData(name);
        getData(age);
        getData(number);

        getUpperNumberData(name);//编译报错
        getUpperNumberData(age);
        getUpperNumberData(number);
    }

    public static void getData(Student<?> data) {
        System.out.println("data :" + data.getData());
    }

    public static void getUpperNumberData(Student<? extends Number> data){//此处限定类型实参只能是Number类及其子类
        System.out.println("data :" + data.getData());
    }

}

class Student<T> {

    private T data;

    public Student() {
    }

    public Student(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}

我想大家通过上例肯定能体会到什么是类型通配符上限,类型通配符上限通过形如Student<? extends Number>形式定义,相对应的,类型通配符下限为Student<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

四、注意事项

A、当对类或方法的类型参数进行赋值时,要求对所有的类型参数进行赋值,否则,将得到一个编译错误。

B、在Java集合框架中,对于参数值是未知类型的容器类,只能读取其中的元素,不能向其中添加元素,因为其类型是未知,编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是NULL

C、可以使用带泛型参数的类声明数组,却不可以用于创建数组

List<Integer>[] iListArray;
iListArray=new ArrayList[10];
iListArray=new ArrayList<Integer>[10];//编译时错误

五、实现原理

(1)一个泛型类被其所有的实例共享

现有如下这样一段代码,其打印的结果是什么?

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

或许你会说是false,but,you are wrong!它打印出true。我们可以发现虽然传入的类型实参不同,但其生成的所有对象实例的类型是一样的,即一个泛型类的所有实例在运行时具有相同的运行时类(class), 而不管他们的实际类型参数。

事实上,泛型之所以叫泛型,就是因为它对其所有可能的类型参数有同样的行为。类的静态变量和静态方法在所有的实例间共享,这就是为什么在静态变量的声明和初始化时或者在静态方法或静态初始化代码中使用类型参数是不合法的原因,类型参数是属于具体实例的。

(2)擦除(erasure)

  在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,可以接收不同泛型实参的泛型类在内存中只有一个,是原来的最基本的类型(上例中为List),当然,在逻辑上我们可以理解成多个不同的泛型类型。

  究其原因,Java泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的,Java中的泛型只作用于代码编译阶段,在编译过程中正确检验泛型结果后,会将泛型的相关信息擦除(erasure),也就是说,成功编译之后的.class文件中是不包含任何泛型信息的,泛型信息不会进入运行时阶段。你可以把它认为是(基本上就是)一个从源码到源码的转换,它把泛型版本转换成非泛型版本。

  基本上,擦除(erasure)去掉了所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉了,比如说,一个 List<String>类型被转换为List,所有对类型变量的引用被替换成类型变量的上限(通常是Object)。对此总结成一句话:泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同的基本类型。

(3)类型转换

类型参数在运行时并不存在,这意味着它们不会添加任何时间或者空间上的负担,这很好,不幸的是,这也意味着你不能依靠它们进行类型转换,下面这段代码有如下图所示的报错信息:

public <T> T badCast(T t, Object o) {
      return (T) o; // unchecked warning
}

类似的,如下的类型转换将得到一个unchecked warning,因为运行时环境不会为你做这样的检查。

Collection cs = new ArrayList<String>();
Collection<String> cstr = (Collection<String>) cs;

(4)instanceof

泛型类被其所有实例(instances)共享的另一个暗示就是检查一个实例是不是某一个特定类型的泛型类是没有意义的,如下图所示:

六、Class泛型的使用

  Java5中的一个变化是类java.lang.Class是泛型化的(Class<T>),这是把泛型扩展到容器类之外的一个很有意思的例子。 你很可能会问,Class的类型参数T代表什么?它代表Class对象代表的类型,比如说,Class对象String.class的类型为Class<String>,Class对象Serializable.class的类型为Class<Serializable>,此处T代表什么请自行体会。

  Java5为什么会有这样的变化,这可以被用来提高你反射代码的类型安全。特别的,因Class的newInstance()方法返回一个T,你可以在使用反射创建对象时得到更精确的类型。比如说,假定你要写一个工具方法来进行数据库查询,给定一个SQL语句,请返回一个数据库中符合查询条件的对象集合(collection),你该怎么做?

解决方案:

(1)定义接口:

interface Factory<T> {
      public T[] make();
}

(2)定义查询方法:

public <T> Collection<T> select(Factory<T> factory, String statement) {
       Collection<T> result = new ArrayList<T>();
       /* run sql query using jdbc */
       for ( int i=0; i<10; i++ ) { /* iterate over jdbc results */
            T item = factory.make();
            /* use reflection and set all of item’s fields from sql results */
            result.add( item );
       }
       return result;
}

(3)调用(匿名类方案 ):

select(new Factory<EmpInfo>(){
          public EmpInfo make() {
            return new EmpInfo();
          }
       } , ”selection string”);

除了上述方式外,也可以声明一个类来支持Factory接口:

class EmpInfoFactory implements Factory<EmpInfo> { ...
    public EmpInfo make() { return new EmpInfo();}
}

select(getMyEmpInfoFactory(), "selection string");//调用语句,其中getMyEmpInfoFactory()方法返回自定义的支持Factory接口的类

解说:上述两种解决方案虽然可行,却很不自然,其缺点是它们需要下面的二者之一:

  a.为每个要使用的类型定义冗长的匿名工厂类;

  b.为每个要使用的类型声明一个工厂类并传递其对象给调用处

此时可以选择使用class类型参数,它可以被反射使用,没有泛型的代码可能如下:

Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”);
public static Collection select(Class c, String sqlStatement) {
    Collection result = new ArrayList();
    /* run sql query using jdbc */
    for ( /* iterate over jdbc results */ ) {
        Object item = c.newInstance();
        /* use reflection and set all of item’s fields from sql results */
        result.add(item);
    }
    return result;
}

上述方案虽然能达到目的,却不能给我们返回一个精确类型的集合,因Java5及以后版本中Class是泛型的,我们可以按下面这种方式写:

Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”);
public static <T> Collection<T> select(Class<T>c, String sqlStatement) {
    Collection<T> result = new ArrayList<T>();
    /* run sql query using jdbc */
    for ( /* iterate over jdbc results */ ) {
        T item = c.newInstance();
        /* use reflection and set all of item’s fields from sql results */
        result.add(item);
    }
    return result;
}

上述方案通过一种类型安全的方式得到了我们想要的集合,这项技术是一个非常有用的技巧。

七、参考资料

本文只阐述了Java泛型的部分内容,此处贴出一些优质文章以供参考

(1)http://www.cnblogs.com/lwbqqyumidi/p/3837629.html

时间: 2024-10-09 23:28:23

Java:泛型的相关文章

Java泛型边界问题,super关键字

背景 Java给定一个具体的类型参数A之后的泛型List,与给定另一个具体的类型参数X的泛型List super或者extends可以定义一大类的泛型,作为给出具体类型参数的泛型的父类. super或者extends定义的有边界泛型,根据参数类型的层次覆盖判定和具体参数类型泛型之间的层次关系. 不要对super望文生义 super关键字,用于类的方法中表示指向父类对象的引用. 在泛型边界语法中指出泛型下界. 假设有继承关系,A <- B <- C <- D <- E void f(

Java 泛型 &lt;? super T&gt; 中 super 怎么 理解?与 extends 有何不同?

https://www.zhihu.com/question/20400700/answer/117464182 Java是单继承,所有继承的类构成一棵树. class Apple(A)class Banana(B) extend Apple(A) class Orange ()) extend Apple(A) 泛型里面A super B 表示A是B的父类或者祖先A extend B 表示A是B的子类或者子孙 由于树这个结构上下是不对称的 1) 参数写成:T<? super B>,对于这个泛

Java泛型学习笔记 - (七)浅析泛型中通配符的使用

一.基本概念:在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>. 无边界的通配符的主要作用就是让泛型能够接受未知类型的数据. 2. 固定上边界的通配符(Upper Bounded Wildcards): 使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是

java泛型中的上、下界通配符

java泛型中,通配符?表示未知类型,等同于<? extends Object>,<? extends T>是上边界限定通配符,<? super T>是下边界限定通配符. 一.区别 在一个list中,上下界通配符能够存放和读取的对象类型如下图所示: 二.原则 上下界通配符的使用应当遵循PECS原则:Producer Extends,Consumer Super. 限定通配符总是包括自己 上界类型通配符:add方法受限 下界类型通配符:get方法受限 如果你想从一个数据类

Java泛型中的PECS原则

今天在写代码的时候使用到了这样一个方法签名: public void foo(Map<String, String> map); 在写这个参数的时候正好在想一些关于泛型的东西,于是: public void foo(Map<? extends String, ? extends String> map); 这两种写法有什么区别呢?记得以前和同学讨论过这个问题,但后来没有记下来,渐渐又淡忘了.今天又去翻了好多资料,总算找到一些可以参考的,赶紧记在这里方便以后温故知新啦.好了,言归正传

java 泛型上下限的例子

public static void doGenericExtends(List<? extends Goods> list) { Goods goods = new Goods(); GoodsChild goodsChild = new GoodsChild(); GoodsParent goodsParent = new GoodsParent(); // list.add(goods); compile error // list.add(goodsChild); compile er

2.java泛型基础

Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JDK 5中的新集合类框架中.对于泛型概念的引入,开发社区的观点是褒贬不一.从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误.而从不好的地方来说,为了保证与旧有版本的兼容性,Java泛型的实现上存在着一些不够优雅的地

面试题_关于Java泛型的面试题

1. Java中的泛型是什么 ? 使用泛型的好处是什么? 这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中.那些拥有Java1.4或更早版本的开发背景的人 都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便.泛型防止了那种情况的发生.它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException. 2. Java的泛型是如何工作的 ? 什么是类型擦除 ? 这是一道更好的泛型面试题.泛型是

java泛型常见面试题

背景:泛型这个知识点平时用的不多,但是在面试的时候很容就被问到,所以还是要准备一些基础的知识储备. 面试旧敌之 Java 泛型 :主要概念及特点 “泛型” 意味着编写的代码可以被不同类型的对象所重用. 泛型是在JDK1.5之后出现的. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 可以看到,使用 Object 来实现通用.不同类型的处理,有这么两个缺点: 每次使用时都需要强制转换成想要的类型 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全 根据<Java 编程

赢在面试之Java泛型篇(十二)

139. Java中的泛型是什么 ? 使用泛型的好处是什么? 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 好处: 1.类型安全,提供编译期间的类型检测 2.前后兼容 3.泛化代码,代码可以更多的重复利用 4.性能较高,用GJ(泛型JAVA)编写的代码可以为java编译器和虚拟机带来更多的类型信息,这些信息对java程序做进一步优化提供条件. 140,Java的泛型是如何工作的 ? 什么是类型擦除 ?如何工作? 1.类型检查:在生成字节