java使用泛型后消除泛型
本文是我们名为“ 高级Java ”的学院课程的一部分。
本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的旅程! 在这里查看 !
目录
-
1.简介 2.泛型和接口 3.泛型和类 4.泛型和方法 5.仿制药的局限性 6.泛型,通配符和有界类型 7.泛型和类型推断 8.泛型和注释 9.访问泛型类型参数 10.何时使用泛型 11.下一步 12.下载源代码
1.简介
泛型的概念表示对类型的抽象(C ++开发人员将其称为模板)。 这是一个非常强大的概念(很久以前就出现了),它允许开发抽象算法和数据结构,并提供具体类型以供以后使用。 有趣的是,泛型在Java的早期版本中不存在,并且仅在Java 5版本中才添加。 从那以后,可以说,泛型彻底改变了Java程序的编写方式,提供了更强的类型保证,并使代码更加安全。
在本节中,我们将从接口,类和方法开始介绍泛型的用法。 提供了很多好处,但是泛型确实引入了一些局限性和副作用,我们也将介绍这些局限性和副作用。
2.泛型和接口
与常规接口相反,要定义通用接口,只需提供应使用其进行参数化的类型即可。 例如:
package com.javacodegeeks.advanced.generics;
public interface GenericInterfaceOneType< T > {
void performAction( final T action );
}
GenericInterfaceOneType
用单一类型T
参数化,可以由接口声明立即使用。 该接口可以使用多种类型进行参数化,例如:
package com.javacodegeeks.advanced.generics;
public interface GenericInterfaceSeveralTypes< T, R > {
R performAction( final T action );
}
每当任何类想要实现该接口时,它都可以选择提供确切的类型替换,例如ClassImplementingGenericInterface
类提供String
作为通用接口的类型参数T
:
package com.javacodegeeks.advanced.generics;
public class ClassImplementingGenericInterface
implements GenericInterfaceOneType< String > {
@Override
public void performAction( final String action ) {
// Implementation here
}
}
Java标准库有很多通用接口的示例,主要是在集合库中。 这是很容易声明和使用通用接口,但是我们将讨论界类型(如果要回他们再次泛型,通配符和有界类型 )和通用限制( 仿制药的限制 )。
3.泛型和类
与接口相似,常规类和泛型类之间的区别仅在于类定义中的类型参数。 例如:
package com.javacodegeeks.advanced.generics;
public class GenericClassOneType< T > {
public void performAction( final T action ) {
// Implementation here
}
}
请注意,可以使用泛型对任何类( 具体 , 抽象或最终 )进行参数化。 一个有趣的细节是,该类可以将(或不可以)将其泛型类型传递给接口和父类,而无需提供确切的类型实例,例如:
package com.javacodegeeks.advanced.generics;
public class GenericClassImplementingGenericInterface< T >
implements GenericInterfaceOneType< T > {
@Override
public void performAction( final T action ) {
// Implementation here
}
}
这是一种非常方便的技术,它允许类在仍然符合接口(或父类)协定的泛型类型上施加附加界限,这将在“ 泛型,通配符和有界类型”部分中看到。
4.泛型和方法
在讨论类和接口时,我们已经在上一节中看到了几个通用方法。 但是,关于它们还有更多的话要说。 方法可以将泛型用作参数声明或返回类型声明的一部分。 例如:
public< T, R > R performAction( final T action ) {
final R result = ...;
// Implementation here
return result;
}
对于可以使用泛型类型的方法没有任何限制,它们可以是具体的, 抽象的 , 静态的或final的 。 这是几个示例:
protected abstract< T, R > R performAction( final T action );
static< T, R > R performActionOn( final Collection< T > action ) {
final R result = ...;
// Implementation here
return result;
}
如果方法被声明(或定义)为通用接口或类的一部分,则它们可以(也可以不)使用其所有者的通用类型。 他们可以定义自己的通用类型,也可以将其与类或接口声明中的类型混合使用。 例如:
package com.javacodegeeks.advanced.generics;
public class GenericMethods< T > {
public< R > R performAction( final T action ) {
final R result = ...;
// Implementation here
return result;
}
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语言实现泛型的方式存在一些限制和副作用,下一部分将解决该问题。
5.仿制药的局限性
不幸的是,泛型是语言的最鲜明的特征之一,它有一些局限性,这主要是由于泛型很晚才引入已经很成熟的语言。 最可能的是,更彻底的实施需要大量的时间和资源,因此需要进行权衡,以便及时提供仿制药。
首先,在泛型中不允许使用原始类型(例如int
, long
, byte
等等)。 这意味着无论何时需要使用原始类型参数化泛型类型时,都必须使用相应的类包装器( Integer
, Long
, Byte
…)代替。
final List< Long > longs = new ArrayList<>();
final Set< Integer > integers = new HashSet<>();
不仅如此,由于必须在泛型中使用类包装器,因此会导致对原始值进行隐式装箱和拆箱(本教程的第7部分“ 常规编程指南”中将详细介绍此主题),例如:
final List< Long > longs = new ArrayList<>();
longs.add( 0L ); // 'long' is boxed to 'Long'
long value = longs.get( 0 ); // 'Long' is unboxed to 'long'
// Do something with value
但是原始类型只是泛型陷阱之一。 另一个更晦涩的是类型擦除。 重要的是要知道泛型仅在编译时存在:Java编译器使用一组复杂的规则来强制有关泛型及其类型参数使用的类型安全,但是所产生的JVM字节码已擦除了所有具体类型(并替换为Object
类)。 首先,以下代码无法编译可能令人惊讶:
void sort( Collection< String > strings ) {
// Some implementation over strings heres
}
void sort( Collection< Number > numbers ) {
// Some implementation over numbers here
}
从开发人员的角度来看,这是一个完全有效的代码,但是由于类型擦除,这两种方法的范围缩小到了相同的签名,并导致编译错误(带有奇怪的消息,如“方法的擦除sort(Collection
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
}
}
最后,使用泛型的类型参数创建数组实例也是不可能的。 例如,以下代码无法编译(这时出现一条清晰的错误消息“无法创建T的通用数组” ):
public< T > void performAction( final T action ) {
T[] actions = new T[ 0 ];
}
尽管有所有这些限制,但泛型仍然非常有用,并带来了很多价值。 在“ 访问泛型类型参数 ”一节中,我们将介绍几种克服Java语言中的泛型实现所施加的一些约束的方法。
6.泛型,通配符和有界类型
到目前为止,我们已经看到了使用具有无限制类型参数的泛型的示例。 泛型的强大功能是将约束(或界限)强加在使用extends
和super
关键字对其进行参数化的类型上。
extends
关键字将type参数限制为其他某个类的子类或实现一个或多个接口。 例如:
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
}
方法存储区需要其类型参数T
来实现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
接口的方法。 或者,如果这不重要,则可以使用无界通配符:
public void store( final Collection< ? > objects ) {
// Some implementation here
}
与extends
相反, super
关键字将type参数限制为某个其他类的超类。 例如:
public void interate( final Collection< ? super Integer > objects ) {
// Some implementation here
}
通过使用类型上限和下限(具有extends
和super
)以及类型通配符,泛型提供了一种微调类型参数要求的方法,或者在某些情况下完全忽略了它们,仍然保留了面向类型的语义。
7.泛型和类型推断
当泛型进入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 < T > void performAction( final Collection< T > actions,
final Collection< T > defaults ) {
// Some implementation here
}
final Collection< String > strings = new ArrayList<>();
performAction( strings, Collections.emptyList() );
Java 7编译器无法推断Collections. emptyList ()
的type参数Collections. emptyList ()
Collections. emptyList ()
调用,因此需要将其显式传递:
performAction( strings, Collections.< String >emptyList() );
幸运的是,Java 8版本为编译器,尤其是对泛型的类型推断带来了更多增强,因此上面显示的代码片段成功编译,从而使开发人员不必进行不必要的键入。
8.泛型和注释
尽管我们将在本教程的下一部分中讨论注释,但是值得一提的是,在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
在本教程的第4部分“ 如何以及何时使用Enums和Annotations”中 ,我们将看几个示例如何使用注释以将某些元数据与泛型类型参数相关联。 本节仅使您感到可以通过注释丰富泛型。
9.访问泛型类型参数
正如您从“ 泛型的限制 ”一节中已经知道的那样,不可能获得泛型类型参数的类。 解决此问题的一个简单技巧是,在需要知道类型参数T
的类的地方,需要传递其他参数Class< T >
。 例如:
public< T > void performAction( final T action, final Class< T > clazz ) {
// Some implementation here
}
它可能会浪费方法所需的参数量,但经过精心设计,它并不像乍看上去那样糟糕。
在Java中使用泛型时经常会出现的另一个有趣的用例是,确定泛型实例已被参数化的类型的具体类。 它不是那么简单,并且需要包含Java反射API。 我们将在本教程的第11部分中查看完整示例,即反射和动态语言支持,但现在仅提及ParameterizedType
实例是对泛型进行反射的中心点。
10.何时使用泛型
尽管有所有限制,但泛型为Java语言增加的价值却是巨大的。 如今,很难想象曾经有一段时间Java没有泛型支持。 应该使用泛型而不是原始类型(用Collection< T >
代替Collection
,用Callable< T >
代替Callable
……)或Object
来保证类型安全,在合同和算法上定义明确的类型约束,并显着简化代码维护和重构。
但是,请注意Java当前泛型实现的局限性,类型擦除以及著名的原始类型隐式装箱和拆箱。 泛型不是解决您可能遇到的所有问题的灵丹妙药,没有什么可以代替精心设计和周到的思考。
查看一些真实的示例并了解泛型如何使Java开发人员的生活更轻松是一个好主意。
示例1 :让我们考虑该方法的典型示例,该方法针对实现某个接口(例如Serializable
)的类的实例执行操作并返回该类的修改后的实例。
class SomeClass implements Serializable {
}
如果不使用泛型,则解决方案可能如下所示:
public Serializable performAction( final Serializable instance ) {
// Do something here
return instance;
}
final SomeClass instance = new SomeClass();
// Please notice a necessary type cast required
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
}
}
不使用泛型,直接的解决方案是引入中间接口(或将纯Object
作为最后的手段),例如:
// The class itself should be modified to use the intermediate interface
// instead of direct implementations
class SomeClass implements SerializableAndRunnable {
@Override
public void run() {
// Some implementation
}
}
public void performAction( final SerializableAndRunnable instance ) {
// Do something here
}
尽管这是一个有效的解决方案,但它并不是最佳选择,并且随着接口数量的增加,它可能会变得非常讨厌和难以管理。 让我们看看泛型如何在这里提供帮助:
public< T extends Serializable & Runnable > void performAction( final T instance ) {
// Do something here
}
代码非常简洁明了,不需要任何中间接口或其他技巧。
泛型使代码易于阅读和直接的示例世界真是无穷无尽。 在本教程的下一部分中,通常将使用泛型来演示Java语言的其他功能。
11.下一步
在本节中,我们介绍了Java语言的一个非常与众不同的特性,称为泛型。 我们已经检查了泛型如何通过检查正确的类型(带有边界)是否在各处使用来使代码安全且简洁。 我们还研究了一些泛型限制以及克服这些限制的方法。 在下一节中,我们将讨论枚举和注释。
12.下载源代码
- 这是关于如何设计类和接口的课程。 您可以在此处下载源代码: advanced-java-part-4
翻译自: https://www.javacodegeeks.com/2015/09/how-and-when-to-use-generics.html
java使用泛型后消除泛型