一,引言
泛型的背景
Java泛型是Java编程语言中的一个特性,引入泛型的目的是为了增强代码的类型安全和重用性。在没有泛型之前,Java中的集合类(如ArrayList、HashMap等)只能存储Object类型的对象,这使得在使用集合时需要进行强制类型转换,容易出现类型错误。
List list = Arrays.asList("smile","admin","root");
String s = (String) list.get(0);
Integer i = (Integer) list.get(0);//Exception in thread "main" java.lang.ClassCastException
在Java 5版本之前,Java的类型是静态的,在编译时确定,并且在运行时擦除类型信息。这种情况下,编译器无法对集合的元素类型进行验证,因此可能会导致运行时类型错误。为了解决这个问题,Java引入了泛型机制。
List<Object> list = Arrays.asList(1,"smile");
for (Object o : list) {
Integer i = (Integer) o; //Exception in thread "main" java.lang.ClassCastException
System.out.println(i);
}
泛型的作用
-
类型安全性:泛型提供了更严格的类型检查,在编译时就能够发现类型错误。通过指定具体的类型参数,可以在编译期间捕获不兼容的类型操作,避免了在运行时出现类型转换异常。
-
代码重用性:泛型使得我们可以编写通用的代码逻辑,可以在多种类型上进行操作,而无需为每种类型都编写相应的代码。这样可以减少重复的代码,提高代码的可维护性和可读性。
-
高效性:泛型在编译时进行类型擦除,将泛型类型转换为它们的边界类型(通常是Object类型),这意味着在运行时并不需要保留泛型的类型信息,从而避免了额外的开销,提高了程序的性能
由于JVM里没有泛型,所以的泛型对于JVM来说都是普通类,当我们在使用泛型的时候,编译器就会将泛型代码中的类型完全擦出,使其变成原始类型,也就是编译后生成的字节码文件是不带泛型的,无界的泛型类型 如<T>擦除后的原始类型就是Object类型,有界泛型类型擦除之后的原始类型就是父类型,如 <T extends Person>,类型擦除后的原始类型就是Person类型。多重有界泛型类型擦除之后的原始类型就是第一个父类,如<T extends Animal&Person>,那么类型擦除后的原始类型就是Animal类
在使用泛型时,可以定义:类、接口、方法、变量等具有泛型参数,并通过使用具体的类型实参来指定泛型的具体类型。例如,可以定义一个泛型类ArrayList,其中的T表示类型参数,可以在创建ArrayList对象时指定具体的类型,如ArrayList表示存储整型的ArrayList集合。
public static void main(String[] args) {
ArrayList<Integer> integerArrayList = new ArrayList<>();
}
public static class ArrayList<T> {
}
泛型参数的基本概念
- 类型参数:在泛型中,使用类型参数来表示一个未知的类型。类型参数可以用任意标识符来表示,通常使用单个大写字母作为惯例,如
T、E、K等。 - 实际类型参数:在使用泛型时,需要指定具体的类型给类型参数,这些具体的类型被称为实际类型参数。例如,在创建一个泛型类的实例时,可以将
Integer作为实际类型参数传递给类型参数T,从而创建一个存储整数的对象,例如:ArrayList。
二,类型参数的限定和通配符的使用
类型参数的限定
类型参数的限定允许我们对泛型类或方法的类型参数进行约束,以确保只能使用特定类型或满足特定条件的类型
在 Java 中,可以使用关键字 extends 来限定类型参数
-
单一限定(Single Bound) :指定类型参数必须是某个类或接口的子类。
public static class Person { // 类成员和方法定义 } public static class Student<T extends Person> { // 类成员和方法定义 }在上面的示例中,类型参数
T必须是Person类的子类或实现了Person接口的类型 -
多重限定(Multiple Bounds) :指定类型参数必须是多个类或接口的子类,并且只能有一个类
public static class Person { // 类成员和方法定义 } public static class Student<T extends Person & Learn & Sport> { // 类成员和方法定义 } public interface Learn{}; public interface Sport{};在上面的示例中,类型参数
T必须是Person类的子类,并且还要实现Learn和Sport接口,其中&后面,必须是接口,因为在Java中不存在多继承
通过类型参数的限定,可以在泛型类或方法中对类型进行更精确的控制和约束,以提高代码的类型安全性和灵活性。
通配符的使用
通配符是一种特殊的类型参数,用于在泛型类或方法中表示未知类型或不确定的类型。有两种通配符可以使用:
-
无限定通配符(Unbounded Wildcard) :使用问号
?表示,表示可以匹配任何类型。public void myMethod(List<?> myList) { // 方法实现 }在上面的示例中,
myMethod方法接受一个类型为List的参数,但是该列表的元素类型是未知的,可以是任何类型。 -
有限定通配符(Bounded Wildcard) :使用
extends和具体类或接口来限定通配符所能匹配的类型范围。public void myMethod(List<? extends SomeClass> myList) { // 方法实现 }在上面的示例中,
myMethod方法接受一个类型为List的参数,但是该列表的元素类型必须是SomeClass类或其子类。-
上界限定:通配符可以通过上界限定来限制泛型类型的范围。使用 "? extends 类型" 表示通配符必须是指定类型或其子类型。这样可以确保在使用通配符时,只能访问被限定类型或其子类型的成员
public void myMethod(List<? extends SomeClass> myList) { // 方法实现 } -
下界限定:通配符还可以通过下界限定来限制泛型类型的范围。使用 "? super 类型" 表示通配符必须是指定类型或其父类型。这样可以确保在使用通配符时,只能写入被限定类型或其父类型的对象。
public void myMethod(List<? super SomeClass> myList) { // 方法实现 }
-
通过使用通配符,可以编写更通用的泛型代码,允许处理各种类型的参数。它提供了更大的灵活性,尤其是当你不关心具体类型时或需要对多个类型进行操作时。
使用场景
- 方法参数:通配符可以用作方法的参数类型,用于接收不同类型的泛型对象作为参数传递给方法。通过使用通配符,方法可以处理各种类型的泛型对象,从而提高方法的灵活性和适用性。
- 方法返回类型:通配符还可以用作方法的返回类型,用于表示方法返回一个未知类型的泛型对象。通过使用通配符作为返回类型,可以在方法中处理不确定类型的泛型对象,并将其返回给调用者。
- 类型声明和实例化:通配符可以用于定义泛型类、接口或方法的类型参数,在声明时使用通配符表示一个未知的类型。在实例化时,可以使用具体的类型来替代通配符,以根据实际需求来指定泛型的类型。
- 集合类定义:通配符可以用于声明集合类的泛型类型,使集合能够接收多种类型的元素。例如,List<?> 表示一个未知类型的 List,可以接受任意类型的元素。
- 上界限定:通配符可以通过上界限定来限制泛型类型的范围。使用 "? extends 类型" 表示通配符必须是指定类型或其子类型。这样可以确保在使用通配符时,只能访问被限定类型或其子类型的成员。
- 下界限定:通配符还可以通过下界限定来限制泛型类型的范围。使用 "? super 类型" 表示通配符必须是指定类型或其父类型。这样可以确保在使用通配符时,只能写入被限定类型或其父类型的对象。
class Benz{
}
class Car <T> {
public double getPrice() {
return 58.8;
}
}
class Man <T> {
private Car<? extends Benz> car;
//方法参数
public void add(Car<? extends Benz> car) {
this.car = car;
}
//方法返回类型
public Car<? extends Benz> get() {
return car;
}
}
需要注意的是,在使用通配符时,不能对带有通配符的泛型对象进行添加元素的操作,因为无法确定通配符表示的具体类型。但是可以进行读取元素的操作。如果需要同时支持添加和读取操作,可以使用有限定通配符来解决这个问题。
三,泛型使用
泛型类
定义泛型类的语法和使用方法
在许多编程语言中,如Java和C#,泛型类是一种特殊类型的类,它可以接受不同类型的参数进行实例化。泛型类提供了代码重用和类型安全性的好处,因为它们可以与各种数据类型一起使用,而无需为每种类型编写单独的类。
下面是定义泛型类的语法:
public class GenericClass<T> {
// 类成员和方法定义
}
在上面的示例中,GenericClass 是一个泛型类的名称,<T> 表示类型参数,T 可以替换为任何合法的标识符,用于表示实际类型。
要使用泛型类,可以通过指定实际类型来实例化它。例如,假设我们有一个名为 MyClass 的泛型类,我们可以按以下方式使用它:
GenericClass<Integer> myInstance = new GenericClass<Integer>();
在上面的示例中,我们使用整数类型实例化了 GenericClass 泛型类。这样,myInstance 将是一个只能存储整数类型的对象。
在实例化泛型类后,可以使用该类中定义的成员和方法,就像普通的类一样。不同之处在于,泛型类中的成员或方法也可以使用类型参数 T,并且会根据实际类型进行类型检查和处理。
如果需要在泛型类中使用多个类型参数,可以通过逗号分隔它们:
public class MultiGenericClass<T,U> {
// 类成员和方法定义
}
上面的示例定义了一个具有两个类型参数的泛型类 MultiGenericClass。
需要注意的是,继承泛型类时可以选择具体地指定类型参数,也可以继续使用泛型
继承泛型类的方式
-
具体类型参数继承:在继承类中显式指定具体的类型参数。这将使实现类只能处理特定类型的数据。
public static class Man extends Person<String> { // 类成员和方法定义 }
在上面的示例中,Man 类继承了泛型类 Person<String>,并明确指定了类型参数为 String。因此,Man 类只能处理字符串类型的数据
-
保留泛型类型参数:在继承类中继续使用泛型类型参数。这将使继承类具有与泛型类相同的类型参数,从而保持灵活性。
public static class Man <T> extends Person<T> { // 类成员和方法定义 }
在上面的示例中,Man<T> 类继承了泛型类 Person<T>,并保留了类型参数 T。这意味着 Man 类可以处理任意类型的数据,具有更大的灵活性。
总结起来,定义泛型类的语法是在类名后面使用 <T> 或其他类型参数,并在类中使用这些类型参数。然后,可以通过指定实际类型来实例化泛型类,并可以使用到泛型类中定义的成员和方法中。
泛型接口
定义泛型接口的语法和使用方法
泛型接口是具有泛型类型参数的接口。通过使用泛型接口,我们可以在接口级上使用类型参数,使得实现类能够处理不同类型的数据,并提高代码的灵活性和复用性。
以下是定义泛型接口的一般语法:
public interface InterfaceName<T> {
// 接口方法和常量声明
}
在上面的语法中,<T> 表示类型参数的占位符,可以是任意标识符(通常使用单个大写字母)。T 可以在接口方法、常量和内部类中使用。
下面是一个简单的示例,展示了如何定义和使用泛型接口:
public interface Box<T> {
void add(T item);
T get();
}
// 实现泛型接口
public class IntegerBox implements Box<Integer> {
private Integer item;
public void add(Integer item) {
this.item = item;
}
public Integer get() {
return item;
}
}
// 使用泛型接口
Box<Integer> box = new IntegerBox();
box.add(10);
Integer value = box.get();
在上面的示例中,我们定义了一个名为 Box 的泛型接口。它包含了一个 add 方法和一个 get 方法,分别用于添加和获取泛型类型的数据。然后,我们实现了这个泛型接口的一个具体类 IntegerBox,并在其中指定了具体的类型参数为 Integer。
最后,我们使用泛型接口创建了一个 Box<Integer> 类型的对象,通过 add 方法添加整数值,并通过 get 方法获取整数值。
需要注意的是,实现泛型接口时可以选择具体地指定类型参数,也可以继续使用泛型。
实现泛型接口的方式
-
具体类型参数实现:在实现类中显式指定具体的类型参数。这将使实现类只能处理特定类型的数据。
public class IntegerBox implements Box<Integer> { private Integer item; public void add(Integer item) { this.item = item; } public Integer get() { return item; } }在上面的示例中,
IntegerBox类实现了泛型接口Box<Integer>,并明确指定了类型参数为Integer。因此,IntegerBox类只能处理整数类型的数据。 -
保留泛型类型参数:在实现类中继续使用泛型类型参数。这将使实现类具有与泛型接口相同的类型参数,从而保持灵活性。
public class GenericBox<T> implements Box<T> { private T item; public void add(T item) { this.item = item; } public T get() { return item; } }在上面的示例中,
GenericBox<T>类实现了泛型接口Box<T>,并保留了类型参数T。这意味着GenericBox类可以处理任意类型的数据,具有更大的灵活性。
使用以上两种方式中的一种,您可以根据需要选择实现泛型接口的方式。具体取决于实现类在处理数据时需要限定特定类型还是保持灵活性。
另外,无论使用哪种方式来实现泛型接口,都需要确保实现类中的方法签名与泛型接口中定义的方法完全匹配。这包括方法名称、参数列表和返回类型
泛型方法
定义泛型方法的语法和使用方法
泛型方法是指具有泛型类型参数的方法。通过使用泛型方法,我们可以在方法级别上使用类型参数,使方法能够处理不同类型的数据,并提高代码的灵活性和复用性。
以下是定义泛型方法的一般语法:
public <T> ReturnType methodName(T parameter) {
// 方法体
}
在上面的语法中,<T> 表示类型参数的占位符,可以是任意标识符(通常使用单个大写字母)。T 可以在方法参数、返回类型和方法体内部使用。ReturnType 是方法的返回类型,可以是具体类型或者也可以是泛型类型。
下面是一个简单的示例,展示了如何定义和使用泛型方法:
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
// 调用泛型方法
Integer[] intArray = { 1, 2, 3, 4, 5 };
printArray(intArray);
String[] stringArray = { "Hello", "World" };
printArray(stringArray);
在上面的示例中,我们定义了一个名为 printArray 的泛型方法。它接受一个泛型数组作为参数,并打印出数组中的每个元素。我们可以使用这个方法打印不同类型的数组,例如整数数组和字符串数组。
需要注意的是,泛型方法可以独立于泛型类存在,并且可以在任何类中定义和使用。它们提供了更大的灵活性,使我们能够对特定的方法进行泛型化,而不仅仅是整个类。
在调用泛型方法时,我们需要注意几个关键点:
-
显式指定类型参数:如果泛型方法的类型参数没有被编译器自动推断出来,我们需要显式地指定类型参数。可以在方法名前使用尖括号(<>)并提供具体的类型参数。
// 显式指定类型参数为String String result = myGenericMethod.<String>genericMethod(argument); -
自动类型推断:Java编译器在某些情况下能够自动推断泛型方法的类型参数,使代码更简洁易读。可以省略显式指定类型参数。
// 自动类型推断,根据参数类型推断类型参数为Integer Integer result = myGenericMethod.genericMethod(argument);编译器通过方法参数的类型和上下文信息来推断类型参数。这种类型推断对于简化代码和提高可读性非常有用。
-
通配符类型参数:在某些情况下,我们可能希望泛型方法能够接受不特定类型的参数。这时可以使用通配符作为类型参数。
-
无限制通配符(Unbounded wildcard):使用问号(?)表示,可以接受任意类型的参数。
// 泛型方法接受任意类型的参数 void myGenericMethod(List<?> list) { // 方法体 } -
有限制通配符(Bounded wildcard):使用 extends 关键字指定上界或者使用 super 关键字指定下界,限制了泛型方法接受的参数类型范围。
// 泛型方法接受 Number 及其子类的参数 void myGenericMethod(List<? extends Number> list) { // 方法体 } // 泛型方法接受 Integer 及其父类的参数 void myGenericMethod(List<? super Integer> list) { // 方法体 }
-
-
对于一个静态方法,它是无法访问泛型类的类型参数,如果静态方法需要使用泛型,那么就应该将此方法声明泛型方法
class Smile <T> { public static <E> E get(E data) { return data; } }
需要注意的是,调用泛型方法时,编译器会根据传递的参数类型和上下文进行类型检查。如果类型不匹配,将产生编译错误
类型推断
类型推断是指编译器根据上下文信息自动推断出泛型类型参数的过程。在某些情况下,我们可以省略泛型类型参数,并让编译器自动推断它们。这样可以简化代码,使其更具可读性
以下是一个示例,展示了类型推断的用法:
Box<Integer> integerBox = new Box<>(); // 类型推断
List<String> stringList = new ArrayList<>(); // 类型推断
在这些示例中,我们没有显式地指定泛型类型参数,而是使用了 <> 运算符。编译器会根据变量的声明和初始化值来推断出正确的类型参数。
需要注意的是,类型推断只在Java 7及更高版本中才可用。在旧版本的Java中,必须显式指定泛型类型参数。
四,类型擦除
类型擦除的原理和影响
泛型类型擦除(Type Erasure)是Java中泛型的实现方式之一。它是在编译期间将泛型类型转换为非泛型类型的一种机制。在泛型类型擦除中,泛型类型参数被擦除为它们的上界或 Object 类型,并且类型检查主要发生在编译时而不是运行时。
泛型类型擦除的原理
-
类型擦除:在编译过程中,所有泛型类型参数都被替换为它们的上界或 Object 类型。例如,
List<String>在编译后会变成List<Object>。 -
类型擦除后的转换:由于类型擦除,原始的泛型类型信息在运行时不可用。因此,在使用泛型类型时,会进行必要的转换来确保类型安全性。
- 向上转型:如果泛型类型参数是一个子类,那么它会被转换为其上界类型。例如,
List<String>被转换为List<Object>。 - 向下转型:如果我们需要从泛型类型中获取具体的类型参数,我们需要进行类型转换。但这可能导致运行时类型异常(ClassCastException)。
- 向上转型:如果泛型类型参数是一个子类,那么它会被转换为其上界类型。例如,
泛型类型擦除的影响
- 可兼容性:泛型类型擦除确保了与原始非泛型代码的兼容性。这意味着可以将使用泛型类型的代码与不使用泛型的旧代码进行交互。
- 无法获得具体类型参数:由于类型擦除,无法在运行时获取泛型类型参数的详细信息。例如,无法在运行时判断一个 List 对象是
List<String>还是List<Integer>。 - 类型安全性:类型擦除导致泛型在运行时失去了类型检查。编译器只能在编译时进行类型检查,如果存在类型不匹配的情况,可能在运行时出现 ClassCastException 异常。
- 限制反射操作:通过反射机制,可以绕过泛型类型擦除的限制,在运行时获取泛型类型的信息。但是,反射的使用复杂且性能较低,不推荐频繁使用。
示例影响:
//编译后的泛型类型擦除
GenericClass<String> stringGeneric = new GenericClass<>();
GenericClass<Integer> integerGeneric = new GenericClass<>();
System.out.println(stringGeneric.getClass() == integerGeneric.getClass());
//运行时类型异常示例
GenericClass<String> stringGeneric1 = new GenericClass<>();
stringGeneric1.setValue("admin");
GenericClass integerGeneric1 = new GenericClass();
integerGeneric1 = stringGeneric1;
GenericClass<Integer> integerGeneric2 = integerGeneric1;
Integer value = integerGeneric2.getValue();
桥方法的概念和作用
泛型桥方法(Generic Bridge Method)是Java编译器为了保持泛型类型的安全性而自动生成的方法。它的作用是在继承或实现带有泛型类型参数的类或接口时,确保类型安全性和兼容性。
概念
当一个类或接口定义了带有泛型类型参数的方法,并且该类或接口被子类或实现类继承或实现时,由于泛型类型擦除的原因,编译器需要生成额外的桥方法来确保类型安全性。这些桥方法具有相同的方法签名,但使用原始类型作为参数和返回值类型,以保持与继承层次结构中的其他非泛型方法的兼容性。
作用
- 类型安全:泛型桥方法的主要作用是保持类型安全性。通过添加桥方法,可以在运行时防止对不兼容的类型进行访问。这样可以避免在编译期间无法检测到的类型错误。
- 维护继承关系:泛型桥方法还用于维护泛型类或接口之间的继承关系。它们确保子类或实现类能够正确地覆盖父类或接口的泛型方法,并使用正确的类型参数。
示例
考虑以下示例:
java
复制代码public class MyList<T> {
public void add(T element) {
// 添加元素的逻辑
}
}
// 子类继承泛型类,并覆盖泛型方法
public class StringList extends MyList<String> {
@Override
public void add(String element) {
// 添加元素的逻辑
}
}
在这个示例中,由于Java的泛型类型擦除机制,编译器会生成一个桥方法来确保类型安全性和兼容性。上述代码实际上被编译器转换为以下内容:
java
复制代码public class MyList {
public void add(Object element) {
// 添加元素的逻辑
}
}
public class StringList extends MyList {
@Override
public void add(Object element) {
add((String) element);
}
public void add(String element) {
// 添加元素的逻辑
}
}
在这个转换后的代码中,StringList 类包含了一个桥方法 add(Object element),它调用了真正的泛型方法 add(String element)。这样就保持了类型安全性,并且与父类的非泛型方法兼容。
通过生成泛型桥方法,Java编译器可以在继承和实现泛型类型时保持类型安全性和兼容性。这些桥方法在内部转换和维护泛型类型擦除的同时,提供了更好的类型检查和运行时类型安全性。
五,注意事项
- 当我们使用泛型需要用到基本类型时候,<>里只能填基本类的包装类型,例如 、。这是由于在类型擦除的时候。会将泛型代码还原成原始类型(父类或者Object),基本类型不是任何一种类,所以必须使用他们的包装类。
- 我们定义了一个泛型类或者泛型方法,不一定要非要在<>里传入类型参数,如果不传入参数,使用泛型的方法和成员变量可以为任意类型。
- 使用泛型的时候,类型参数可以有多个,如常见的Map<String,Integer> 。
- 泛型只在编译期有效,泛型信息不会进入到运行时期。
- 不能对确切的泛型类型使用instanceof操作,编译时会报错。如下面操作是非法的。
- 编译器对泛型的类型推断只对赋值操作有效,其他时候并不起作用,如果将一个泛型方法调用的结果作为一个参数,传递给另一个方法,这时候编译器并不会执行类型推断,它会认定,调用泛型方法之后,其返回值被赋给一个Object类型变量。