我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。

参考书籍

《Java核心技术:卷1》

泛型, 先睹为快

先通过一个简单的例子说明下Java中泛型的用法:

泛型的基本形式类似于模板, 通过一个类型参数T, 你可以"私人定制"一个类,具体定制的范围包括实例变量的类型返回值的类型传入参数的类型

Foo.java

public class Foo <T> {
  // 约定实例变量的类型
  private T data;
  // 约定返回值的类型
  public T getData () {
    return this.data;
  }
  // 约定传入参数的类型
  public void setData (T data) {
    this.data = data;
  }
}

Test.java

public class Test {
  public static void main (String args[]) {
    Foo<String> s = new Foo<String> ();
  }
}

泛型的由来

泛型设计源于我们的编写类时的一个刚需:想让我们编写的处理类能够更加"通用", 而不是只能处理某些特定的对象或场景。或者说:我们希望我们的类能实现尽可能多的复用。举个栗子:一般来说,你并不想要编写多个分别处理不同数据类型,但内在逻辑代码却完全一样的类。因为这些处理类可能除了数据类型变换了一下外,所有代码都完全一致。“只要写一个模板类就OK了嘛~ 等要使用的时候再传入具体的类型,多省心”, 当你这么思考的时候:浮现在你脑海里的,就是泛型程序设计(Generic pogramming)的思想

在介绍Java的泛型机制之前, 先让我们来看看, 还没加入泛型机制的“泛型程序设计”是怎样子的

泛型程序设计1.0: 不用Java泛型机制

下面我们编写一个存储不同的对象的列表类,列表有设置(set)和取值(get)两种操作。
假设这个列表类为ObjArray,同时尝试存储的值为String类型,则:
1.在ObjArray类里我们维护一个数组arr, 为了将来能容纳不同的对象, 将对象设为Object类型(所有对象的父类)
2.在实例化ObjArray后, 通过调用set方法将String存入Object类型的数组中; 而在调用get方法时, 要对取得的值做强制类型转换—从Object类型转为String类型

ObjArray.java:

public class ObjArray  {
  private Object [] arr;
  public ObjArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, Object o) {
    this.arr[i] = o;
  }
  public Object get (int i) {
    return this.arr[i];
  }
}

Test.java:

/**
 * @description: 测试代码
 */
public class Test {
  public static void main (String args[]) {
    ObjArray arr = new ObjArray(3);
    arr.set(0, "彭湖湾");
    // get操作时要做强制类型转换
    String n =(String)arr.get(0);
    // 输出 "彭湖湾"
    System.out.print(n);
  }
}

如果不使用泛型机制,但又想要实现泛型程序设计,就会编写出类似这样的代码。

泛型程序设计2.0: 使用Java泛型机制

让我们来看看使用泛型机制改进后的结果。
看起来大约是这样:

GenericArray.java

public class GenericArray<T>  {
  public void set (int i, T o) {
    // ...
  }
  public T get (int i) {
    // ...
  }
}

【具体代码下面给出】

Test.java:

public class Test {
  public static void main (String args[]) {
    GenericArray<String> arr = new <String>GenericArray(3);
    arr.set(0, "彭湖湾");
    // 不用做强制类型转换啦~~
    String s =arr.get(0);
    // 输出: 彭湖湾
    System.out.print(s);
  }
}

我们发现,改进后的设计有以下几点好处:

1. 规范、简化了编码: 我们不用在每次get操作时候都要做强制类型转换了
2. 良好的可读性:GenericArray<String> arr这一声明能清晰地看出GenericArray中存储的数据类型
3. 安全性:使用了泛型机制后,编译器能在set操作中检测传入的参数是否为T类型, 同时检测get操作中返回值是否为T类型,如果不通过则编译报错

泛型并非无所不能

了解到了泛型的这些特性后, 也许你会迫不及待地想要在ObjArray类里大干一场。
例如像下面这样, 用类型参数T去直接实例化一个对象, 或者是实例化一个泛型数组

可惜的是 ......

public class GenericArray<T>  {
  private T obj = new T (); // 编译报错
  private T [] arr = new T[3]; // 编译报错
  // ...
}

没错, 泛型并不是无所不能的, 相反, 它的作用机制受到种种条框的限制。

这里先列举泛型机制的两个限制:

1.不能实例化类型变量, 如T obj = new T ();

2. 不能实例化泛型数组,如T [] arr = new T[3];

【注意】这里不合法仅仅指实例化操作(new), 声明是允许的, 例如T [] arr

我们现在来继续看看上面泛型设计中, GenericArray类的那部分代码:

public class GenericArray<T>  {
  private Object [] arr;
  public GenericArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

没错, 在ObjArray类内部我们仍然还是用到了强制转型。看到这里也许令人有那么一点点的小失望, 毕竟还是没有完全跳出
初始的泛型设计的边界。 但是, 泛型的优点仍然是显而易见的, 只不过要知道的是:它并没有无所不能的魔法, 并受到诸多限制。

泛型的编写规则

1.泛型类和泛型方法的定义

泛型类
如前面所说,可以像下面一样定义一个泛型类
类型变量T放在类名的后面

public class Foo <T> {
  // 约定实例变量的类型
  private T data;
  // 约定返回值的类型
  public T getData () {
    return this.data;
  }
  // 约定传入参数的类型
  public void setData (T data) {
    this.data = data;
  }
}

泛型方法
也可以定义一个泛型方法:

泛型变量T放在修饰符(这里是public static)的后面, 返回类型的前面

public class Foo {
  public static <T> T getSelf (T a) {
    return a;
  }
}

泛型方法可以定义在泛型类当中,也可以定义在一个普通类当中

2.可以使用多个类型变量

public class Foo<T, U> {
  private T a;
  private U b;
}

【注意】在Java库中,常使用E表示集合的元素类型, K和V分别表示关键字和值的类型, T(U,S)表示任意类型

3.JavaSE7以后,在实例化一个泛型类对象时,构造函数中可以省略泛型类型

ObjArray<Node> arr = new <Node>ObjArray();

可简写成:

ObjArray<Node> arr = new <>ObjArray();

类型变量的限定

当我们实例化泛型类的时候, 我们一般会传入一个可靠的类型值给类型变量T。 但有的时候,被定义的泛型类作为接收方,也需要对传入的类型变量T的值做一些限定和约束,例如要求它必须是某个超类的子类,或者必须实现了某个接口, 这个时候我们就要使用extends关键字了。如:

超类SuperClass:

public class SuperClass {
}

子类SubClass:

public class SubClass extends SuperClass {
}

对T使用超类类型限定:要求父类必须为SuperClass

public class Foo<T extends SuperClass> {
}

测试:

public class Test {
  public static void main (String args[]) {
    Foo<SubClass> f = new Foo<SubClass>(); // 通过
    Foo<String> n = new Foo<String>(); // 报错
  }
}

extends使用的具体规则

1. 对于要求实现接口, 或者继承自某个父类, 统一使用extends关键字 (没有使用implements关键字,为了追求简单)

2. 限定类型之间用 "&" 分隔

3. 如果限定类型既有超类也有接口,则:超类限定名必须放在前面,且至多只能有一个(接口可以有多个)

这个书写规范和类的继承和接口的实现所遵循的规则是一致的(<1>不允许类多继承,但允许接口多继承<2>书写类的时候类的继承是写在接口实现前面的)

// 传入的T必须是SuperClass的子类,且实现了Comparable接口
public class Foo<T extends SuperClass&Comparable> {
}

【注意】: 上面的SuperClass和Comparable不能颠倒顺序

泛型类的继承关系

泛型类型的引入引发了一些关于泛型对象继承关系的有趣(?)问题。

在Java中, 如果两个类是父类和子类的关系,那么子类的实例也都是父类的实例,这意味着:
一个子类的实例可以赋给一个超类的变量:

SubClass sub = new SubClass();
SuperClass sup = sub;

当引入了泛型以后, 有趣(?)的问题来了:

我们通过两对父子类List/ArrayList, Employee/Manager来说明这个问题

(我们已经知道List是ArrayList的父类(抽象类),这里假设Employee是Manager的父类)
1. ArrayList<Employee> 和 ArrayList<Manager>之间有继承关系吗?(ArrayList<Manager>的实例能否赋给ArrayList<Employee>变量?)
2. List<Employee> 和 ArrayList<Employee>之间有继承关系吗?(ArrayList<Employee>的实例能否赋给 List<Employee>变量?)
3. ArrayList和ArrayList<Employee>之间有继承关系吗?(ArrayList<Employee>的实例能否赋给ArrayList变量?)

答案如下:

对1: 没有继承关系; 不能

ArrayList<Manager> ae = new ArrayList<Manager>();
ArrayList<Employee> am = ae; // 报错

对2: 有继承关系; 可以

ArrayList<Employee> al = new ArrayList<>();
List<Employee> l = al; // 通过

对3: 有继承关系; 可以

ArrayList<Employee> ae = new ArrayList<>();
ArrayList a = ae;

下面用三幅图描述上述关系:

描述下1,2的关系

对上面三点做个总结:
1. 类名相同,但类型变量T不同的两个泛型类没有什么联系,当然也没有继承关系(ArrayList<Manager>和ArrayList<Employee>)
2. 类型变量T相同,同时本来就是父子关系的两个类, 作为泛型类依然保持继承关系 (ArrayList<Employee>和List<Employee>)
3. 某个类的原始类型,和其对应的泛型类可以看作有“继承关系”(ArrayList和ArrayList<Employee>)

引用一幅不太清晰的图

通配符?

泛型继承关系带来的问题 — 类型匹配的苦恼

问题出现在上面所述规范中的第二点:ArrayList<Manager> 和 ArrayList<Employee>之间没有继承关系。
这意味着,如果你像下面一样编写一个处理ArrayList<Employee>的方法

public class Foo {
  public static void handleArr (ArrayList<Employee> ae) {
    // ...
  }
}

你将无法用它来处理ArrayList<Manager>:

public static void main (String args[]) {
    ArrayList<Manager> am = new ArrayList<Manager>();
    Foo.handleArr(am); // 报错,类型不匹配
}

现在我们想要:“handleArr方法不仅仅能处理ArrayList<Employee>, 而且还能处理ArrayList<X> (这里X代表Employee和它子类的集合)”。于是这时候通配符?就出现了:ArrayList<? extends Employee>能够匹配ArrayList<Manager>, 因为ArrayList<? extends Employee>是 ArrayList的父类

现在我们的例子变成了:

public class Foo {
  public static void handleArr (ArrayList<? extends Employee> ae) {
    // ...
  }
}
public static void main (String args[]) {
    ArrayList<Manager> am = new ArrayList<Manager>();
    Foo.handleArr(am); // 可以运行啦!
  }

通配符和super关键字

?统配不仅可以用于匹配子类型, 还能用于匹配父类型:

<? super Manager>

泛型的其他约束

上面我们介绍了泛型的一些约束,例如不能直接实例化实例化类型变量和泛型数组,这里和其他约束一起做个总结:

在定义泛型类时不能做的事:

1. 不能实例化类型变量

T obj = new T (); // 报错, 提示: Type parameter 'T' cannot be instantiated directly

解决方案:
如果实在要创建一个泛型对象的话, 可以使用反射:

public class GenericObj<T> {
  private T obj;
  public GenericObj(Class<T> c){
      try {
        obj = c.newInstance(); // 利用反射创建实例
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}
/**
 * @description: 测试代码
 */
public class Test {
  public static void main (String args[]) {
    // 通过
    GenericObj<String> go = new GenericObj<> (String.class);
  }
}

因为Class类本身就是泛型, 而String.class是Class<T>的实例,

2. 不能实例化泛型数组,如T [] arr = new T[3];

private T [] arr = new T[3]; // 报错, 提示: Type parameter 'T' cannot be instantiated directly

解决方法一:

上文所提到的,创建Object类型的数组,然后获取时转型为T类型:

public class GenericArray<T>  {
  private Object [] arr;
  public GenericArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

解决方法二: 利用反射
这里使用反射机制中的Array.newInstance方法创建泛型数组

GenericArray.java

public class GenericArray<T> {
  private T [] arr;
  public GenericArray(Class<T> type, int n) {
    arr = (T[])Array.newInstance(type, n); // 利用反射创建泛型类型的数组
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

Test.java

/**
 * @description: 测试代码
 */
public class Test {
  public static void main (String args[]) {
  GenericArray<String> genericArr = new GenericArray<>(String.class, 5);
  genericArr.set(0, "penghuwan");
  System.out.println(genericArr.get(0)); // 输出 "penghuwan"
  }
}

3. 不能在泛型类的静态上下文中使用类型变量

public class Foo<T> {
  private static T t;
  public static T get () { // 报错, 提示: 'Foo.this' can not be referenced from a static context
    return T;
  }
}

注意这里说的是泛型类的情况下。如果是在一个静态泛型方法中是可以使用类型变量的

public class Foo {
  public static<T> T get (T t) { // 通过
    return t;
  }
}

(这里的泛型方法处在一个非泛型类中)

4. 不能抛出或者捕获泛型类的实例

不能抛出或者捕获泛型类的实例:

// 报错 提示:Generic class may not extend java.lang.throwable
public class Problem<T> extends Exception {
}

甚至扩展Throwable也是不合法的

public class Foo {
  public static  <T extends Throwable> void doWork () {
    try {
      // 报错 提示: Cannot catch type parameters
    }catch (T t) {
    }
  }
}

但在异常规范中使用泛型变量是允许的

// 能通过
public class Foo {
  public static  <T extends Throwable> void doWork  (T t) throws T {
    try {
      // ...
    }catch (Throwable realCause) {
      throw  t;
    }
  }
}

在使用泛型类时不能做的事

1. 不能使用基本类型的值作为类型变量的值

Foo<int> node = new Foo<int> (); // 非法

应该选用这些基本类型对应的包装类型

Foo<Integer> node = new Foo<Integer> ();

2. 不能创建泛型类的数组

public static void main (String args[]) {
    Foo<Node> [] f =new Foo<Node> [6]; // 报错
}

解决方法:
可以声明通配类型的数组, 然后做强制转换

Foo<Node> [] f =(Foo<Node> [])new Foo<?> [6]; // 通过

【Java】泛型学习笔记的更多相关文章

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

    一.基本概念:在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List< ...

  2. Java泛型学习笔记--Java泛型和C#泛型比较学习(一)

    总结Java的泛型前,先简单的介绍下C#的泛型,通过对比,比较学习Java泛型的目的和设计意图.C#泛型是C#语言2.0和通用语言运行时(CLR)同时支持的一个特性(这一点是导致C#泛型和Java泛型 ...

  3. Java泛型学习笔记 - (六)泛型的继承

    在学习继承的时候, 我们已经知道可以将一个子类的对象赋值给其父类的对象, 也就是父类引用指向子类对象, 如: Object obj = new Integer(10); 这其实就是面向对象编程中的is ...

  4. Java泛型学习笔记 - (三)泛型方法

    泛型方法其实和泛型类差不多, 就是把泛型定义在方法上, 格式大概就是: public <类型参数> 返回类型 方法名(泛型类型 变量名) {...}泛型方法又分为动态方法和静态方法,:1. ...

  5. Java泛型学习笔记 - (二)泛型类

    1. 我们先写一个没有泛型的类Box: public class Box { private Object obj; public Box() {} public Object getObj() { ...

  6. Java泛型学习笔记

    泛型是Java5引进的新特征,是类和接口的一种拓展机制,主要实现参数化类型机制.Java的泛型,跟C++的类模板有很多相似的地方,或者说,就是C++类模板的升级版. 泛型类 在开发过程中,我们或许要设 ...

  7. 【09-03】java泛型学习笔记

    静态方法的泛型 /** * @description 静态方法的泛型无法使用类定义的泛型,因为类在实例化时才确定具体的泛型类,因此静态方法要使用泛型需要使用泛型方法的方式 */ public clas ...

  8. Java泛型学习笔记 - (五)泛型接口

    所谓泛型接口, 类似于泛型类, 就是将泛型定义在接口上, 其格式如下: public interface 接口名<类型参数>如: interface Inter<T> { pu ...

  9. Java泛型学习笔记 - (四)有界类型参数

    1. 当我们希望对泛型的类型参数的类型进行限制的时候(好拗口), 我们就应该使用有界类型参数(Bounded Type Parameters). 有界类型参数使用extends关键字后面接上边界类型来 ...

  10. Java泛型学习笔记 - (一)泛型的介绍

    一.什么是泛型:泛型的作用是用来规定一个类, 接口或方法所能接受的数据的类型. 就像在声明方法时指定参数一样, 我们在声明一个类, 接口或方法时, 也可以指定其"类型参数", 也就 ...

随机推荐

  1. Android 服务_笔记

    Service服务 服务(Service)是Android中的四大组件之一,适用于开发无界面.长时间运行的应用功能,服务是在后台运行,服务的创建与Activity类似,只需要继承Service和在An ...

  2. 阿里云云虚拟主机安装Z-BlogPHP

    简介 在阿里云买了一个云虚拟主机,叫共享虚拟主机普惠版,6 块钱一年.本着练习上手的目的,尝试在阿里云云虚拟主机上安装Z-BlogPHP,一个个人建站的CMS 系统. 云虚拟主机网页空间为200M,M ...

  3. SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)

    在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...

  4. 如何让一个DIV水平,垂直方向都居中于浏览器?

    <style type="text/css"><!-- div {position:absolute;top:50%;left:50%;margin:-150px ...

  5. CTF---Web入门第二题 上传绕过

    上传绕过分值:10 来源: Justatest 难度:易 参与人数:5847人 Get Flag:2272人 答题人数:2345人 解题通过率:97% bypass the upload 格式:fla ...

  6. [bzoj2333] [SCOI2011]棘手的操作 (可并堆)

    //以后为了凑字数还是把题面搬上来吧2333 发布时间果然各种应景... Time Limit: 10 Sec  Memory Limit: 128 MB Description 有N个节点,标号从1 ...

  7. UVA10341-Solve It-二分查找

    二分查找 二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.首先,假设表中元素是按升序 ...

  8. hdu_1030(数学题+找规律)

    规律就是两个数字的level差+left差+right差 代码: #include<cstdio> #include<iostream> #include<cstring ...

  9. c++工程重复编译与重复定义

    #ifndef #define #endif防止的是"重复编译",而不是"重复定义"重复编译可能造成重复定义,但重复定义的来源不只有重复编译从代码变成可执行的程 ...

  10. Windows下MYSQL读取文件为NULL

    只记录解决问题的方法. mysql 版本: 5.7.18 问题: 在执行mysql 函数load_file时,该函数将加载指定文件的内容,存储至相应字段.如: SELECT LOAD_FILE(&qu ...