Java泛型详解(上)

一. 什么是泛型

泛型是一种程序设计手段(机制),使用泛型可以让你的代码被很多不同类型的对象所重用,提高代码的重用性,还可以提高代码的可读性安全性

比如,我们经常使用的ArrayList类,就是一个泛型类,也正因如此,它可以接受很多不同类型的对象

/*
可以根据需要存储不同类型的对象
*/
ArrayList<Integer> arraylist = new ArrayList<Integer>();
ArrayList<String> arraylist = new ArrayList<String>();
ArrayList<File> arraylist = new ArrayList<File>();
......
/*
在Java SE 7及以后的版本中,可以省略构造函数中的泛型类型,即可写为:

ArrayList<Integer> arraylist = new ArrayList<>();

编译器会根据变量的类型推断出泛型类型
*/

二. 为什么要用泛型

如上所说,使用泛型可以提高代码对不同类型的重用性,但是实际上,在泛型出现之前,Java也有对不同类型重用代码的运行机制,那就是使用Object类,例如,ArrayList类就只维护一个Object引用的数组

//泛型机制之前
public class ArrayList {
    private Object[] elementData;
    ...
    public Object get(int i){...}
    public void add(Object o){...}
}

这样的做法会带来两个问题:

①. 当获取一个值时,必须进行强制类型转换

Array files = new ArrayList();
...
String filename = (String)files.get(0);

②. 没有错误检查

files.add(new file("..."));
 //不会报错,但是在获取filename转换成String是就会出错

然而在泛型机制中,提供了类型参数来解决这一问题,并且提高了代码的可读性和安全性

ArrayList<String> files = new ArrayList<String>();

三. 定义泛型类

public class Pair<T> {  //在类名后加类型变量T, 用尖括号<>括起来
    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public getFirst() { return this.first; }
    public getSecond() { return this.second; }

    public setFirst(T first) { this.first = first; }
    public setSecond(T second) { this.second = second; }
}

类型变量使用大写形式,且比较短。在Java库中:

E 表示集合元素类型
K 和 V 表示表的关键字与值的类型
T 表示“任意类型”(在需要多个类型变量时,还可以使用U和S

四. 定义泛型方法

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

当我们调用泛型类时,在尖括号中可以指明放入参数的具体类型,但是也可以不直接写出,因为编译器会推断出参数的类型

public static <T> T getMiddle(T... a) {
    return a[a.length / 2];
}

String middle = <String>getMiddle("hello", ",", "world"); //OK
String middle = getMiddle("hello", ",", "world"); //also OK

在大多数情况下,这样调用泛型方法没有任何问题,但是这里面还是有坑的,比如:

double middle = getMiddle(3.14, 1592, 6);
//这样,编译器就会自动将参数打包为一个Double,两个Integer

解决方法就是将所有的参数都写成double

注:当你要用的数据是浮点数类型时,即便它是个整数,也最好带上小数点,这样既可以防止出错,也可以让别人阅读代码时明白这是浮点数

五. 类型变量的限定

在一些情况下,我们必须要对我们的泛型类或方法的类型变量做一些限定才能保证程序的正确执行,比如:

class ArrayAlg {
    public static <T> T min(T... a) {
        if (a == null || a.length == 0)
            return null;
        T smallest = a[0];
        for (int i = 1; i < a.length; i++)
            if(smallest.compareTo(a[i]) > 0) //不是所有类都有compareTo方法
                smallest = a[i]
        return smallest;
    }
}

在上面的例子中,不是所有的类都有compareTo方法,为了保证程序的正确执行,我们就必须限制所有调用此方法的类型变量 T 都实现了Comparable接口(只含一个方法compareTo的标准接口)

我们可以这样写:

public static <T extends Comparable> T min(T... a){...}

这样一来,如果调用这个方法的类没有实现Comparable接口,就会出现编译错误

注意:这里的extends并不是继承的意思,而是绑定的意思。我们也可以对类型变量做多个限定,在多个限定中用“&”作间隔

<T extends Comparable & Serializable>

六. 类型擦除

Java代码都是跑在虚拟机里的,这个大家都知道,但是,在虚拟机里并没有泛型类的对象,一切的对象都是普通类,泛型机制只不过是一种方便我们重用代码的技术手段而已,那么泛型代码在虚拟机中使如何解释执行的呢?

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为所限定的类型(如果没有限定类型就用Object类),比如:

public class Pair {
    private Object first; //对 T 没有做限定所以用Object类替换
    private Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() { return this.first; }
    public Object getSecond() { return this.second; }

    public void setFirst(Object first) { this.first = first; }
    public void setSecond(Object second) { this.second = second; }
}

如果有限定类型,就用第一个限定类型替换:

public class Interval <T extends Comparable & Serializable>
            implements Serializable {
    private T first;
    private T second;
    ...
    public Interval(T first, T second){...}
}

//替换后
public class Interval implements Serializable {
    private Comparable first;
    private Comparable second;
    ...
    public Interval(Comparable first, Comparable second){...}
}

七. 桥方法

类型擦除也会出现在泛型方法中
然而,类型擦除与Java的多态之间会有一些小矛盾,比如:

class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst()) >= 0)
            super.setSecond(second);
        }
    ...
}

在上面的例子中Pair类也有一个setSecond方法,而它的setSecond方法和DateInterval类中的不一样

Pair类中的setSecond方法类型擦除后:

public void setSecond(Object second) {
    this.second = second;
}

那么这时,在我们调用setSecond方法时,就会出现冲突(因为Object类是一切类的父类,所以无法简单地根据参数类型选择调用的函数)

为了解决这一问题,编译器会在DateInterval类中生成一个桥方法:

public void setSecond(Object second) {
    setSecond((Date) second);
}

也就是说,桥方法会根据所引用的对象进行强制类型转换,来调用最合适的那个方法

假设DateInterval方法也覆盖了getSecond方法:

class DataInterval extends Pair<LocalDate> {
    public LocalDate getSecond() {
        return (Date) super.getSecond().clone();
        //这里调用clone方法是为了防止原数据被修改
    }
}

总结一下,关于Java泛型转换,我们要记住:

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都用它们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换

八. 泛型的约束与局限性

1. 不能用基本类型实例化类型参数

不能将基本类型写入类型参数

//错误!!!
Pair<int> , Pair<float>, Pair<double>, Pair<boolean>, ...

//正确
Pair<Integer>, Pair<Float>, Pair<Double>, Pair<Boolean>, ...

2. 运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型

无论使用instanceof, getClass()或是强制类型转换都会导致错误

if (a instanceof Pair<String>) //Error
if (a instanceof Pair<T>) //Error

Pair<String> p = (Pair<String>)A;
//Warning--can only test that A is a Pair

Pair<String> stringPair = ...;
Pair<Integer> integerPair = ...;
if (stringPair.getClass() == integerPair.getClass())
//always equal, 因为两次调用getClass都返回的是Pair.class

3. 不能创建参数化类型的数组

Pair<String>[] table = new Pair<String>[10]; //Error

以上代码的错误在于,在类型擦除后,table的类型是Pair[],可以把它转换为Object[]

Object[] objarray = table;

数组会记住它的元素类型,如果试图存储其他类型的元素时,就会抛出一个ArrayStoreException异常

objarray[0] = "Hello"; //Error--component type is Pair

当然,如果你很机智的这样写

objarray[0] = new Pair<String>();

这样的确会通过数组存储检查,但是还是会导致一个类型错误
那么,如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList

ArrayList<Pair<String>>

4. Varargs警告

我们来看这样一个方法

public static <T> void addAll(Collection<T> coll, T... ts) {
    for (t : ts) coll.add(t);
}

如果我们想调用这样一个方法来对一些泛型类操作,遵循之前的原则,我们要这样写

Collection<Pair<String>> table = ...;
Pair<String> pair_1 = ...;
Pair<Stirng> pair_2 = ...;
...
addAll(table, pair_1, pair_2, ...);

但是,为了调用这个方法,Java虚拟机必须建立一个Pair <String>数组,虽然这违反了之前的规则,但是这并不会导致错误,你只会得到一个警告,有两种方法抑制这个警告:
① 为包含addAll调用的方法增加注解 @SuppressWarnings(“unchecked”)
② 在Java SE 7之后,还可以用@SafeVarargs直接标注addAll方法

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)

5. 不能实例化类型变量

不能使用像new T(…),new T[…] 或 T.class这样的表达式中的类型变量。例如,下面的Pair<T>构造器就是非法的

public Pair() { first = new T(); second = new T(); } //Error

6. 不能构造泛型数组

同样由于类型擦除,你无法保证你所构造的泛型数组在虚拟机内是你需要的类型。如果你只将数组作为一个类的私有实例域,就可以将这个数组声明为Object[],并在获取元素时进行类型转换,但是这其中还是会有不少的安全隐患

7. 泛型类的静态上下文中的类型变量无效

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

泛型类扩展Throwable是不合法的

public class Problem<T> extends Exception {...} //Error

catch子句中不能使用类型变量。例如,以下方法不能通过编译:

public static <T extends Throwable> void doWork(Class<T> t) {
    try
    {
        ...
    }
    catch (T e) //Error--can't catch type variable
    {
        ...
    }
}

但是,在异常规范中使用类型变量是允许的。以下方法是合法的:

public static <T extends Throwable> void doWork(T t) throws T //OK{
    try
    {
        ...
    }
    catch (Throwable realCause)
    {
        ...
    }
}   

9. 可以消除对受查异常的检查

10. 注意擦除后的冲突

参考文献 [美]Cay S.Horstmann著 《Java核心技术 卷I》(第十版)

时间: 2024-09-06 07:45:38

Java泛型详解(上)的相关文章

Java泛型详解_java

1. Why --引入泛型机制的原因     假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象.然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现.     在Java 5之前,ArrayList的实现大致如下: public class ArrayList { public Object get(int i) { ... }

Java正则表达式详解(上)

详解|正则 如果你曾经用过Perl或任何其他内建正则表达式支持的语言,你一定知道用正则表达式处理文本和匹配模式是多么简单.如果你不熟悉这个术语,那么"正则表达式"(Regular Expression)就是一个字符构成的串,它定义了一个用来搜索匹配字符串的模式. 许多语言,包括Perl.PHP.Python.JavaScript和JScript,都支持用正则表达式处理文本,一些文本编辑器用正则表达式实现高级"搜索-替换"功能.那么Java又怎样呢?本文写作时,一个包

Java泛型详解

一 概念 1.1 为什么需要泛型?           当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型.因此,取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现"java.lang.ClassCastException"异常.使用泛型就可以解决此类问题. 1.2 什么是泛型?         泛型(Generic type 或者 generics)是对 Java 语

Java 泛型详解

在日常的开发中,我们会看到别人的框架很多地方会使用到泛型,泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法.泛型的类型参数只能是类类型(包括自定义类),不能是简单类型.本篇博客我们就来详细解析一下泛型的知识. 泛型类定义及使用 使用泛型有什么好处呢?首先我们先看一个例子,假设我们有两个类,代码如下: #StringClass  public class Strin

Java泛型详解(下)

九. 泛型类型的继承规则 假设现在有一个类Employee和它的子类Manager 现在问题来了: Pair<Manager>是Pair<Employee>的子类吗? 答案是:不是 例如,下面的代码将不会编译成功: Manager[] topHonchos = ...; Pair<Employee> result = ArrayAlg.minmax(topHonchos); //Error //minmax方法返回Pair<Manager>, 而不是Pair

Java虚拟机详解----JVM常见问题总结

[正文] 声明:本文只是做一个总结,有关jvm的详细知识可以参考本人之前的系列文章,尤其是那篇:Java虚拟机详解04----GC算法和种类.那篇文章和本文是面试时的重点. 面试必问关键词:JVM垃圾回收.类加载机制.   先把本文的目录画一个思维导图:(图的源文件在本文末尾)   一.Java引用的四种状态: 强引用: 用的最广.我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用. 如果一个对象具有强引用,那垃圾回收器绝不会回收它.当内存空间不足,Java

java关键字(详解)

基本类型 1 boolean 布尔型 2 byte 字节型 3 char 字符型 4 double 双精度 5 float 浮点 6 int 整型 7 long 长整型 8 short 短整型 9 null 空 10 true 真 11 false 假 程序控制语句 1 break 跳出中断 2 continue 继续 3 return 返回 4 do 运行 5 while 循环 6 if 如果 7 else 否则 8 for 循环 9 instanceof 实例 10 switch 观察 11

Java NIO 详解(一)

NIO即新的输入输出,这个库是在JDK1.4中才引入的.它在标准java代码中提供了高速的面向块的IO操作. 一.基本概念描述 1.1 I/O简介 I/O即输入输出,是计算机与外界世界的一个借口.IO操作的实际主题是操作系统.在java编程中,一般使用流的方式来处理IO,所有的IO都被视作是单个字节的移动,通过stream对象一次移动一个字节.流IO负责把对象转换为字节,然后再转换为对象. 关于Java IO相关知识请参考我的另一篇文章:Java IO 详解 1.2 什么是NIO NIO即New

HBase Java API详解

[本文转自HBase Java API详解] HBase是Hadoop的数据库,能够对大数据提供随机.实时读写访问.他是开源的,分布式的,多版本的,面向列的,存储模型. 在讲解的时候我首先给大家讲解一下HBase的整体结构,如下图: HBase Master是服务器负责管理所有的HRegion服务器,HBase Master并不存储HBase服务器的任何数据,HBase逻辑上的表可能会划分为多个HRegion,然后存储在HRegion Server群中,HBase Master Server中存