Java高级系列——如何使用、何时使用泛型(Generics)?

一、介绍

泛型的概念代表了对类型的抽象(C++开发人员熟知的模板)。它是一个非常强大的概念,它允许开发抽象算法和数据结构,并提供实体类型以供后续操作。在早期的Java版本中并未出现泛型,泛型是在Java 5发布之后被添加到Java中的。从那以后,可以说泛型彻底改变了Java程序的编写方式,提供了更强大的类型保证,使代码更安全。

本文我们将会从接口、类、和方法的开始去讨论有关泛型的用法。泛型提供了很多好处,但是也引入了一些限制和副作用,我们也将讨论这些限制和副作用。

二、泛型和接口(Generics and interfaces)

和普通的接口相比,定义泛型接口,只需提供参数化类型就足够了,比如:

public interface GenericInterfaceOneType< T > {
    void performAction( final T action );
}

GenericInterfaceOneType用单个类型T进行参数化,类型T通过接口声明之后即可立即使用。接口可以被多个类型参数化,比如:

public interface GenericInterfaceSeveralTypes< T, R > {
    R performAction( final T action );
}

每当有任何类想要实现这个接口时,它都可以选择提供准确的类型替换泛型类型,比如说ClassImplementingGenericInterface类提供了String类型替换泛型接口的参数T。

public class ClassImplementingGenericInterface implements GenericInterfaceOneType< String > {
    @Override
    public void performAction( final String action ) {
        // Implementation here
    }
}

Java标准库有大量的泛型接口的例子,主要在集合库中。定义和使用泛型接口是非常容易的,然而,当我们讨论有界类型(通配符和有界类型)和泛型限制(泛型的局限性)时,我们将会再次回到它们。

三、泛型和类(Generics and classes)

和接口类似,普通类和泛型类之间的不同点仅仅只是在定义类中的类型参数。比如:

public class GenericClassOneType< T > {
    public void performAction( final T action ) {
        // Implementation here
    }
}

请注意,任何类(实体、抽象或final)都可以使用泛型参数化。一个有趣的细节是类可能会传递(或可能不会)它的泛型类型(或类型)到接口和父类,而不提供确切的类型实例,例如:

public class GenericClassImplementingGenericInterface< T > implements GenericInterfaceOneType< T > {
    @Override
    public void performAction( final T action ) {
        // Implementation here
    }
}

这种非常方便的技术允许类利用泛型类型附加的界限但仍然符合接口(或父类)的约定,这一块的内容我们将会在“通配符和有界类型”那一节介绍。

四、泛型和方法(Generics and methods)

在前面的章节中我们在讨论类和接口的时候已经看到过一些泛型方法。然而,这里我们将继续详细探讨泛型方法。方法可以使用泛型类型作为参数声明或返回类型声明的一部分。比如:

public< T, R > R performAction( final T action ) {
    final R result = ...;
        // Implementation here
    return result;
}

对于哪些方法可以使用泛型类型没有限制,他们可以是常规的方法、abstract, static或final方法。我们来看个例子:

static< T, R > R performActionOn( final Collection< T > action ) {
    final R result = ...;
    // Implementation here
    return result;
}

如果方法被声明(或者被定义)作为泛型类或者泛型接口的一部分,他们可以使用他们自己的泛型类型。他们可以定义自己的泛型类型或者混合使用他们的泛型类型和从他们的类或者接口中声明的泛型类型。比如:

public class GenericMethods< T > {
    // 混合使用方法本身定义的泛型类型R和类中定义的T
    public< R > R performAction( final T action ) {
        final R result = ...;
        // Implementation here
        return result;
    }


    //使用方法本身的泛型类型U,R
    public< U, R > R performAnotherAction( final U action ) {
        final R result = ...;
        // Implementation here
        return result;
    }
}

类的构造函数被认为是一种初始化方法,因此也可以使用类声明的泛型类型,他们自身声明的泛型类型或者混合使用(但是它们不能作为返回值,所以返回类型参数化不适用于构造函数),比如:

public class GenericMethods< T > {
    // 使用类声明的泛型类型
    public GenericMethods( final T initialAction ) {
        // Implementation here
    }

    // 混合使用
    public< J > GenericMethods( final T initialAction, final J nextAction ) {
        // Implementation here
    }
}

这看起来非常容易和简单并且确实也是如此。然而,在Java语言中实现泛型的方式有一些限制和副作用,下一节将解决这个问题。

五、泛型的限制(Limitation of generics)

作为语言最闪亮的功能之一,泛型不幸的拥有一些限制,实际上导致这些限制的原因主要是泛型被引入这个成熟语言的时间太晚。很可能更彻底的解决这些问题需要更多的时间和资源,但是为了能够及时发布泛型,所以在这些限制方面做了权衡。

首先,基本类型(像int,long,byte,…)不允许在泛型中使用。那就意味着无论何时你想使用基本类型参数化你的泛型类型,你就必须得使用基本类型各自的包装类来代替。

final List longs = new ArrayList<>();
final Set integers = new HashSet<>();

不仅如此,由于必须在泛型中使用包装类,他将会导致基本类型的隐式装箱和拆箱,比如:

final List< Long > longs = new ArrayList<>();
longs.add(0L); // ’long’被装箱为’Long’

long value = longs.get(0); // ’Long’被拆箱为’long’

基本数据类型仅仅只是泛型的缺陷之一。另外一个非常的隐蔽,它就是类型擦除。知道泛型仅在编译时存在是非常重要的:Java编译器使用非常复杂的规则设定来强制泛型类型安全和泛型类型参数的使用,然而生成的JVM字节码已经擦除了所有具体的类型(被Object类替代)。比如下面的这段代码就不能编译:

void sort(Collection<String> strings) {
    // Some implementation over strings heres
}

void sort(Collection<Number> numbers) {
    // Some implementation over numbers here
}

站在开发者的角度,这是一个完全有效的代码,然而因为类型擦除,这两个方法被窄化为相同的签名并且它将会导致编译错误(他会爆出一个异常“Erasure of method sort(Collection) is the same as another method …”):

void sort(Collection strings)
void sort(Collection numbers)

另外一个类型擦除导致的缺陷来自这样一个事实,即你不能够以任何有实际意义的方式去使用泛型的类型,比如创建一个新的类型实例,或者获取泛型类型参数的具体类,或者用instanceof操作符使用它。下面的这个例子就不能通过编译:

public< T > void action( final T action ) {
    if( action instanceof T ) {
        // Do something here
    }
}

public< T > void action( final T action ) {
    if( T.class.isAssignableFrom( Number.class ) ) {
        // Do something here
    }
}

最后,你也不能使用泛型类型参数创建数组实例。比如,下面的这段代码就不能通过编译(此时可能就会报“Cannot create a generic array of T”异常):

public< T > void performAction( final T action ) {
    T[] actions = new T[0];
}

尽管泛型有这么多的限制,但是泛型任然非常有用并且带来了很大的价值。在访问泛型类型参数(Accessing generic type parameters)这一节我们将会阐述几种方法来克服Java语言中泛型的实现所导致的一些约束。

六、泛型、通配符与有界类型(Generics, wildcards and bounded types)

到目前为止,我们已经看到了一些使用无界类型参数泛型实例。泛型的强大能力是可以使用extends和super关键字在他们的参数化类型上强加一些约束(或者界限)。

extends关键字约束类型参数必须作为一个类的子类或者或者实现一个或者多个接口,比如:

public< T extends InputStream > void read( final T stream ) {
    // Some implementation here
}

以上代码段要求方法read中的类型参数T必须是InputStream类的子类。相同的关键字可以用来约束接口的实现,比如:

public< T extends Serializable > void store( final T object ) {
    // Some implementation here
}

方法store的类型参数要求实现接口Serializable,从而让该方法实现一些期望的操作。也可以利用extends关键字为其他的类型参数设定界限,比如。

public< T, J extends T > void action( final T initial, final J next ) {
    // Some implementation here
}

有界并不限定于单个约束条件,他也可以通过&关键字整个多个约束条件,可以整合多个特定的接口但仅仅只允许指定一个类。类和接口的组合也是可以的,请看如下的例子:

public< T extends InputStream & Serializable > void storeToRead( final T stream ) {
    // Some implementation here
}

public< T extends Serializable & Externalizable & Cloneable > void persist(final T object ) {
    // Some implementation here
}

在讨论super关键字之前,我们需要先熟悉一下通配符的概念。如果泛型类,接口或方法中不确定类型参数的具体类型时,那么可以使用?通配符替换。比如:

public void store( final Collection< ? extends Serializable > objects ) {
    // Some implementation here
}

方法store并不关心它被调用的是什么类型参数,它所关心的仅仅只是确定每个类型参数都已经实现了Serializable接口。或者说如果类型参数是否实现Serializable接口等并不重要的话,那么可以使用无边界的通配符:

public void store(final Collection objects) {
    // Some implementation here
}

和extends相比,super关键字约束类型参数为某些其他类的超类。比如:

public void interate(final Collectionsuper Integer> objects) {
    // Some implementation here
}

通过使用上下类型边界(使用extends和super关键字)以及类型通配符,泛型提供了一种方式去微调类型参数的要求,或者在某些情况下,我们可以完全忽略上下类型边界以及类型通配符,这样的话泛型仍然保留面向类型的语义。

七、泛型与类型推断(Generics and type inference)

在泛型刚被引入到Java语言时,为了满足语言语法规则,开发者就不得不写更多的代码,比如;

final Map< String, Collection<String>> map = new HashMap< String, Collection<String>>();

for( final Map.Entry<String, Collection<String>> entry: map.entrySet()) {
    // Some implementation here
}

Java 7版本通过在编译器中进行更改并引入新的尖括号运算符<>来解决这个问题。比如:

final Map< String, Collection< String > > map = new HashMap<>();

编译器能够根据左边的类型参数推断右边的类型参数,所以允许在右边的表达式中忽略类型参数。在减少泛型语法的长度方面这是非常有意义的一个进展,然而编译器推断泛型类型参数的能力也是有限的。比如说,下面的这段代码在Java 7中就不能通过编译:

public static  void performAction(final Collection actions,final Collection defaults ) {
    // Some implementation here
}

final Collection strings = new ArrayList<>();

performAction(strings, Collections.emptyList());

Java 7编译器无法推断Collections.emptyList()调用的类型参数,因此需要显式传递:

performAction( strings, Collections.emptyList());

幸运的是,Java 8版本为编译器带来了更多的增强,特别是泛型的类型推断,所以上面显示的代码片段能够成功编译,节省了开发人员很多不必要的输入。

八、泛型与注解(Generics and annotations)

虽然我们在后续的文章中我们会讨论注解,但是值得一提的是,在Java 8以前的版本中,泛型不允许有注解与类型参数相关联。但是Java 8改变的这个限制,并且现在允许在泛型声明和使用的地方注解泛型类型参数。比如,下面这段代码就展示了如何声明泛型方法以及他的类型参数如何被注解所修饰:

public<@Actionable T> void performAction(final T action) {
    // Some implementation here
}

另外一个应用注解的实例就是我们在使用泛型的时候:

final Collection<@NotEmpty String> strings = new ArrayList<>();
// Some implementation here

后续的文章中我们会讨论注解,这里只是简单的提及,让读者能够知道注解可以让泛型的使用更加强大。

九、访问泛型类型参数(Accessing generic type parameters)

通过我们本文前述内容的描述,我们已经知道我们不能够从泛型类型参数中获取到类。解决这个问题的一个简单的技巧就是需要传递额外的一个参数Class,在这个地方有必要了解类型参数T这个类。比如:

public void performAction(final T action, final Class clazz) {
    // Some implementation here
}

在Java中使用泛型时经常出现的另一个有趣的用例是确定泛型实例已经参数化的类型的具体类。这不是那么简单,这需要涉及到Java反射API,我们将会在本系列的后续的文章中讨论java反射及动态语言支持,在这里我们仅仅只是提及ParameterizedType实例是对泛型进行反射的核心点。

十、何时使用泛型(When to use generics)

尽管泛型有很多的限制,但是泛型加入Java语言所带来的价值是非常庞大的。如今很难去想象如果Java没有泛型支持将会是什么样的一幅景象。应该用泛型而不是原始类型(Collection代替Collection, Callable

然而,请一定要注意当前Java中泛型实现的约束,类型擦除和我们所熟知的基本类型的隐式拆箱和装箱。泛型并不是解决所有可能遇到的问题的银弹并且任何东西都无法取代精心的设计和深思熟虑。

最好的方式就是看一些泛型使用的真实的实例,领略泛型的合理使用给开发人员带来的工作上的便利。

实例1:让我们来考虑一个典型的实例,一个方法执行一个实现某些接口(比如Serializable)的类的操作之后返回此类修改过后的实例。

class SomeClass implements Serializable {
}

不使用泛型,解决方案看起来如下:

public Serializable performAction( final Serializable instance ) {
    // Do something here
    return instance;
}

final SomeClass instance = new SomeClass();

// 请注意这里必须进行强制类型转换
final SomeClass modifiedInstance = (SomeClass)performAction(instance);

我们再来看一下泛型提供的解决方案:

public< T extends Serializable > T performAction( final T instance ) {
    // Do something here
    return instance;
}

final SomeClass instance = new SomeClass();
final SomeClass modifiedInstance = performAction( instance );

这里我们就可以看到类型转换已经消失,因为编译器能够推断出正确的类型并证明这些类型是正确使用的。

实例2:让我们在看一个稍微复杂一点的实例,一个方法要求类的实例实现两个接口(如Serializable和Runnable)。

class SomeClass implements Serializable, Runnable {
    @Override
    public void run() {
        // Some implementation
    }
}

不使用泛型,简单的解决方案就是引入中间接口,比如:

public interface SerializableAndRunnable extends Serializable,Runnable {
}

// 类本省需要修改使用中间接口代替直接实现
class SomeClass implements SerializableAndRunnable {
    @Override
    public void run() {
        // Some implementation
    }
}

public void performAction(final SerializableAndRunnable instance) {
    // Do something here
}

虽然这是合法的解决方案,但是看起来这并不是最好的选择,随着接口数量的递增这就可能变得非常讨厌和难以管理。让我们再看看泛型如何帮助解决这个问题:

public void performAction( final T instance) {
    // Do something here
}

非常简洁明了的代码,不需要中间接口或其他技巧。

泛型让代码可读性和简洁性提升的范例实际上是无止境的。 在本系列文章的下一部分中,泛型将经常用于演示Java语言的其他功能。

在本系列的下一文中,我们将会阐述枚举和注解相关的内容,敬请期待。

你可能感兴趣的:(Java,Java高级系列,Java高级系列文章)