引言
对泛型的简明定义
在Java中,泛型(generic)是一种强类型机制,允许程序员在类、接口或方法中的编译时指定和检查类型。通俗地讲,泛型就是参数化类型的应用,允许你创建可以处理各种数据类型的代码,同时保持类型检查的优势。使用泛型可以让你的代码更加灵活,同时又保持了类型安全。
Java语言中存在泛型的原因和目标
存在泛型的原因在于,Java希望确保在编译时刻就可以进行严格的类型检查,以便在开发时期找到和修正可能的错误。在没有泛型之前,我们可以将任何类型的对象添加到Java集合中,编译器也不能对此进行检查。然而在运行时,一旦取出的对象类型不准确,就会抛出ClassCastException,带来严重的后果。
引入泛型后,Java能提供编译时类型安全检查,减少运行时类型错误,提供程序的稳定性。同时,它还可以消除源代码中的许多类型强制转换,使代码看上去更加简洁。
总结一下,泛型的目标主要有三点:
- 提供编译时类型检查,防止插入错误类型的数据。
- 消除源代码中的类型转换,简化代码。
- 使得类和方法能适用于多种类型,提高代码复用性。
Java泛型基础
泛型类和泛型方法
泛型类是在类名后面跟一个尖括号,里面是类的类型参数。这些类型参数只存在于编译期,实例化对象时,需要指定具体的类型。例如,ArrayList<E>就是一个泛型类,E是ArrayList内部元素的类型。
public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
泛型方法和泛型类类似,是在返回类型之前添加类型参数列表< E >。泛型方法并不需要在一个泛型类中,而且类型参数<E>只在方法声明中定义。
public class Util { public static <T> T getLast(T[] array) { return array[array.length - 1]; } }
泛型接口
和泛型类一样,泛型接口允许接口的某个或部分或全部方法使用泛型。
public interface Pair<K, V> { public K getKey(); public V getValue(); }
泛型和继承
Java中的泛型继承规则是,如果类A是类B的子类,那么List<A>不是List<B>的子类。这是因为即使A和B有继承关系,但它们对应的泛型List<A>和List<B>没有继承关系。
通配符
Java泛型接口、泛型类和泛型方法对类型参数有严格的限制,而通配符就是为了解决这种严格限制的问题。Java提供了两种通配符,'?'代表未知类型;'? extends E' 代表类型上限,表示参数化类型是“E或者E的子类型”; '? super E' 代表类型下限,表示参数化类型是“E或者E的父类型”。
类型参数的边界
明确参数类型
在Java泛型中,你可以使用特定的参数类型来创建实例化的对象。例如,“Box<Integer> integerBox = new Box<>()"就是一个明确参数类型的例子,在创建Box实例时已经明确确定了储存的内容类型为Integer。
使用extends关键字设定上边界
在有些情况下,我们可能需要限制可用于特定类的类型参数,这就需要使用extends关键字来设定类型的上边界。例如,我们想定义一个处理Number类及其子类的泛型,则可以这么定义:"<T extends Number>",那么这个泛型就只能指定Number或者其子类作为类型参数。
public class Box<T extends Number> { private T t; public void set(T t) { this.t = t; } //... }
这样在使用Box类时,我们就不能再使用String或其他非Number的子类作为类型参数了。
使用super关键字设定下边界
super关键字在泛型中的主要用处是设定类型参数的下限。例如,“List<? super Integer>”就表示只能接受Integer以及其超类(super class)的类型。
这种方式非常有用,例如当我们需要从集合中获取元素,同时这些元素应该是某个特定的超类型时。
public void processElements(List<? super Integer> elements){ for(Object o : elements){ // can only process elements as Object } }
在上述代码示例中,我们可以将任何Integer对象或者其子类对象添加到集合中,而获取的元素只能按Object对象来处理。
泛型的类型擦除
什么是类型擦除
类型擦除(Type Erasure)是Java泛型的一个核心概念。它指的是在编译期间,Java编译器会把泛型的具体类型参数信息全部擦除,替换为它的限定类型(没有定义限定类型的用Object代替),生成运行时的字节码。换句话说,到了运行期间,泛型的类型信息已经被擦除了,不再存在。
类型擦除如何影响我们的代码
类型擦除主要有以下几种影响:
- 无法使用instanceof关键字判断对象是否是一个特定的泛型类型。
List<String> strList = new ArrayList<>(); if (strList instanceof ArrayList<String>) { } // 这将会触发编译错误:"Cannot perform instanceof check against parameterized type ArrayList<String>"
- 在一个类中,不能声明多个同名方法,参数类型仅有泛型参数区别。
public class Test<T> { public void setData(T data){} public void setData(String data){} // 这将会触发编译错误因为在运行时都为 setData(Object data) }
解决因类型擦除产生的问题
使用特定类型替代泛型类型。
例如,“List<Integer>”和“List<String>”在类型擦除后都是“List”,如果需要在运行期间判断原始的泛型类型,可以创建一个IntegerList和StringList扩展自List。对于方法的重载,可以利用方法的返回类型进行重载,或者改变方法名以避免使用擦除后类型相同的参数列表。
深入泛型
泛型方法
泛型方法与泛型类一样,都使用类型参数。泛型方法的类型参数出现在返回值前面,并且仅在该方法中有效。因而,泛型方法可以说是整个类中独立的部分,可以在非泛型类中使用。
例如:
public class Utility { // 泛型方法 printArray public static < E > void printArray( E[] inputArray ) { // 输出数组元素 for(E element : inputArray) { System.out.printf("%s ", element); } System.out.println(); } }
通用类型推断
在Java SE 8及更高版本中,类型推断已被加强以便程序员可以省略必要的类型参数。该操作称为钻石操作符 (<>),因为 <> 运算符看起来像是一个钻石。
例如:
List<String> list = new ArrayList<>();。
在上述代码中,我们并没有在右侧的 expression 显式声明类型,Java编译器能够推断出我们想要创建一个
ArrayList<String>
对象。泛型嵌套
泛型嵌套是指创建的类型或方法本身就是泛型,同时它的类型参数(对于类)或类型变量(对于方法)包含另一个泛型表达式。这可能包括多级嵌套,但无论如何,都要保持明确的可读性和简洁性为优。
例如:
Map<String, List<Integer>> map = new HashMap<>();
在这里,
List<Integer>
嵌套在Map<String, List<Integer>>
之中,是对泛型的嵌套使用。
泛型的限制
泛型不能用于静态成员
我们需要先知道静态成员对于类的所有实例来说是共享的。考虑到泛型类型是在实例化类或调用方法时才被确定,如果允许我们将泛型用于静态成员,那么这个静态成员就得需要同时满足所有可能的泛型类型,这显然是不切实际的。因此,泛型不能用于静态类型或者方法。
无法创建泛型数组
Java不支持直接创建泛型数组。以下代码是有误的:
List<String>[] array = new List<String>[10]; // 编译错误
这是因为会存在类型协变的问题。假设这是可行的,我们会面临下列情况:
Object[] objArray = new List<String>[10]; objArray[0] = new ArrayList<Integer>();
第二行使用Object[]引用List<String>[]是合法的,因为数组支持协变。第三行再往所谓的List<String>[]数组存放ArrayList<Integer>,也没有类型问题。然而,这明显是我们不期望的,因此,Java编译器就禁止直接创建泛型数组。
泛型异常的限制
在处理异常时,Java泛型有如下的限制:
- 不能捕捉泛型类型的异常,因为在运行时期异常类型需要明确。
public <T extends Exception, J> void execute(J array, Function<J, T> function) { try { function.apply(array); } catch (T e) { // 编译错误,无法捕捉泛型类型的异常 //... } }
- 泛型类不能直接或者间接继承自Throwable(也就是说,异常类型不能是泛型)。这也是因为在抛出与捕捉异常时,需要明确的异常类型信息。
泛型在实践当中的应用
在集合中使用泛型的例子
泛型在集合中的使用可以提升程序的灵活性以及类型安全。例如,在使用ArrayList存储数据时,如果不确定存储元素的类型,可以使用泛型来解决问题。
List<String> list = new ArrayList<String>(); list.add("Hello"); list.add("World"); for(String s: list) { System.out.println(s); }
在这个例子中,我们定义了一个只能存储String类型数据的ArrayList。当我们试图添加一个非String类型的数据时,编译器会报错。
自定义泛型类和方法的使用实例
下面是一个自定义的泛型类Pair以及一个泛型方法示例。
public class Pair<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 T getFirst() { return first; } public T getSecond() { return second; } // 泛型方法 public <U extends Number> void inspect(U u){ System.out.println("Number: " + u); } }
上述代码首先定义了一个泛型类Pair,该类有两个类型为T的属性。然后,我们实现了一个泛型方法inspect,该方法接受一个继承自Number类的参数U。
例如,下面的代码创建了一个Pair<String>对象,并使用了inspect方法:
Pair<Integer> pair = new Pair<>(1, 2); pair.inspect(3.14); // 输出: Number: 3.14
泛型与反射
如何通过反射访问泛型信息
Java的反射API提供了许多方法,允许我们在运行时访问类和方法的泛型信息。通过
Class
对象的getDeclaredMethod()
方法,可以获取到Method
对象。然后可以通过Method
对象的getGenericReturnType()
和getGenericParameterTypes()
方法,获取到泛型返回类型和参数类型。以下是一个例子:
public class Test { public Map<String, Integer> testMethod(List<String> keys) { return null; } public static void main(String[] args) throws NoSuchMethodException { Method method = Test.class.getDeclaredMethod("testMethod", List.class); Type returnType = method.getGenericReturnType(); Type[] paramTypes = method.getGenericParameterTypes(); if(returnType instanceof ParameterizedType) { System.out.println(((ParameterizedType) returnType).getActualTypeArguments()[0]); System.out.println(((ParameterizedType) returnType).getActualTypeArguments()[1]); } if(paramTypes[0] instanceof ParameterizedType) { System.out.println(((ParameterizedType) paramTypes[0]).getActualTypeArguments()[0]); } } }
这个例子将会打印出方法
testMethod
的参数和返回值的泛型类型。通过反射创建泛型实例
由于Java的类型擦除,我们不能直接通过反射来创建一个具有具体参数化类型的泛型实例。但我们可以创建泛型类的一个未参数化的实例,然后在使用时进行类型转换。
这是一个例子:
public class Test<T> { private T field; public Test(T field) { this.field = field; } public T getField() { return field; } public static void main(String[] args) throws Exception { Class<?> clazz = Class.forName("your.package.Test"); Test<String> instance = (Test<String>) clazz.getDeclaredConstructor(Object.class).newInstance("Hello"); System.out.println(instance.getField()); // Prints: Hello } }
在这个例子中,我们首先获取了
Test
类的Class对象,然后通过getDeclaredConstructor
方法获取了构造函数,并传入Object.class
表示需要一个接收Object的构造函数。然后我们调用newInstance
方法创建了一个新的Test
实例,并传入一个字符串"Hello"作为参数。最后,我们将这个新创建的无参数化实例统一转型为Test<String>
类型。
总结
泛型在Java中的重要性
提高程序的可读性:泛型能够让代码清晰地表达出其工作方式,因为泛型方法包含了显式的参数类型。例如,
List<String>
,这就明确表示我们在用一个专门存放String类型对象的列表。代码重用:通过泛型,你可以编写出可以对多种数据类型进行操作的通用算法。这避免了因类型不同而需要编写多个方法或者类的问题。
类型安全:在编译阶段,就可以检查类型的匹配性,一旦类型不匹配,编译器就不会通过,这样可以提早发现并解决问题,保证了程序的稳定性和可靠性。
泛型用法的最佳实践
使用边界通配符来提高灵活性:如果你需要从一个泛型实例中读取类型T的实例,你应该使用
? extends T
。如果你需要写入类型T的实例,你应使用? super T
。如果你既需要读取又需要写入,你应该直接使用T。避免类型警告:即使类型警告通常并不会导致运行时错误,也应尽可能地消除它们。如果无法消除类型警告,但你可以证明引发警告的代码是类型安全的,可以使用一个
@SuppressWarnings("unchecked")
注解来禁止警告。优先使用列表代替数组:泛型和数组是有冲突的,Java中的数组是协变的,而泛型却是可替换的。数组提供了运行时的类型安全,但泛型的类型安全是在编译时提供。所以,在你需要泛型和数组功能的地方,应当优先使用列表。
注意泛型方法的类型擦除:你要意识到类型参数在运行时可能并不存在,所有的真实对象都必须是未经检查的原始类型。如果因为类型擦除,你的方法声明产生了类型错误,可以通过引入一个
Class<T>
引用来解决。利用有限的通配符实例化泛型:在创建泛型对象时,
List<T>
可以接受任何类型T,此时如果实例化时没有明确指出具体类型,可能会增加类型推导的难度,所以可以利用有界通配符比如List<? extends Number>
来限定接受的类型范围,提高代码的易读性。