Java泛型是Java编程语言中的一个特性,引入泛型的目的是为了增强代码的类型安全性和重用性。在没有泛型之前,Java中的集合类(如ArrayList、HashMap等)只能存储Object类型的对象,这使得在使用集合时需要进行强制类型转换,容易出现类型错误。
泛型的背景:在Java 5版本之前,Java的类型是静态的,在编译时确定,并且在运行时擦除类型信息。这种情况下,编译器无法对集合的元素类型进行验证,因此可能会导致运行时类型错误。为了解决这个问题,Java引入了泛型机制。
泛型的作用:
在使用泛型时,可以定义类、接口、方法和变量等具有泛型参数,并通过使用具体的类型实参来指定泛型的具体类型。例如,可以定义一个泛型类ArrayList,其中的T表示类型参数,可以在创建ArrayList对象时指定具体的类型,如ArrayList表示存储整数的ArrayList。
在许多编程语言中,如Java和C#,泛型类是一种特殊类型的类,它可以接受不同类型的参数进行实例化。泛型类提供了代码重用和类型安全性的好处,因为它们可以与各种数据类型一起使用,而无需为每种类型编写单独的类。
下面是定义泛型类的语法:
java复制代码public class GenericClass<T> { // 类成员和方法定义}
在上面的示例中,GenericClass 是一个泛型类的名称,<T> 表示类型参数,T 可以替换为任何合法的标识符,用于表示实际类型。
要使用泛型类,可以通过指定实际类型来实例化它。例如,假设我们有一个名为 MyClass 的泛型类,我们可以按以下方式使用它:
java复制代码GenericClass<Integer> myInstance = new GenericClass<Integer>();
在上面的示例中,我们使用整数类型实例化了 GenericClass 泛型类。这样,myInstance 将是一个只能存储整数类型的对象。
在实例化泛型类后,可以使用该类中定义的成员和方法,就像普通的类一样。不同之处在于,泛型类中的成员或方法可以使用类型参数 T,并且会根据实际类型进行类型检查和处理。
如果需要在泛型类中使用多个类型参数,可以通过逗号分隔它们:
java复制代码public class MultiGenericClass<T, U> { // 类成员和方法定义}
上面的示例定义了一个具有两个类型参数的泛型类 MultiGenericClass。
总结起来,定义泛型类的语法是在类名后面使用 <T> 或其他类型参数,并在类中使用这些类型参数。然后,可以通过指定实际类型来实例化泛型类,并可以使用泛型类中定义的成员和方法。
类型参数的限定:
类型参数的限定允许我们对泛型类或方法的类型参数进行约束,以确保只能使用特定类型或满足特定条件的类型。
在 Java 中,可以使用关键字 extends 来限定类型参数。有两种类型参数的限定方式:
通过类型参数的限定,可以在泛型类或方法中对类型进行更精确的控制和约束,以提高代码的类型安全性和灵活性。
通配符的使用:
通配符是一种特殊的类型参数,用于在泛型类或方法中表示未知类型或不确定的类型。有两种通配符可以使用:
通过使用通配符,可以编写更通用的泛型代码,允许处理各种类型的参数。它提供了更大的灵活性,尤其是当你不关心具体类型时或需要对多个类型进行操作时。
需要注意的是,在使用通配符时,不能对带有通配符的泛型对象进行添加元素的操作,因为无法确定通配符表示的具体类型。但是可以进行读取元素的操作。如果需要同时支持添加和读取操作,可以使用有限定通配符来解决这个问题。
在Java中,泛型类是能够对类型进行参数化的类。通过使用泛型,我们可以编写更加通用和可复用的代码,同时提高类型安全性。在实例化泛型类时,我们需要指定具体的类型参数。
以下是实例化泛型类的一般语法:
java复制代码ClassName<DataType> objectName = new ClassName<>();
在上面的语法中,ClassName 是泛型类的名称,DataType 是实际类型参数的占位符。通过将适当的类型替换为 DataType,我们可以创建一个特定类型的对象。例如,如果有一个泛型类 Box<T>,其中 T 是泛型类型参数,我们可以实例化它如下:
java复制代码Box<Integer> integerBox = new Box<>();
在这个例子中,我们将泛型类型参数 T 替换为 Integer,然后创建了一个 Box 类型的整数对象。
另一方面,类型推断是指编译器根据上下文信息自动推断出泛型类型参数的过程。在某些情况下,我们可以省略泛型类型参数,并让编译器自动推断它们。这样可以简化代码,使其更具可读性。
以下是一个示例,展示了类型推断的用法:
java复制代码Box<Integer> integerBox = new Box<>(); // 类型推断List<String> stringList = new ArrayList<>(); // 类型推断
在这些示例中,我们没有显式地指定泛型类型参数,而是使用了 <> 运算符。编译器会根据变量的声明和初始化值来推断出正确的类型参数。
需要注意的是,类型推断只在Java 7及更高版本中才可用。在旧版本的Java中,必须显式指定泛型类型参数。
泛型方法是指具有泛型类型参数的方法。通过使用泛型方法,我们可以在方法级别上使用类型参数,使方法能够处理不同类型的数据,并提高代码的灵活性和复用性。
以下是定义泛型方法的一般语法:
java复制代码public <T> ReturnType methodName(T parameter) { // 方法体}
在上面的语法中,<T> 表示类型参数的占位符,可以是任意标识符(通常使用单个大写字母)。T 可以在方法参数、返回类型和方法体内部使用。ReturnType 是方法的返回类型,可以是具体类型或者也可以是泛型类型。
下面是一个简单的示例,展示了如何定义和使用泛型方法:
java复制代码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 的泛型方法。它接受一个泛型数组作为参数,并打印出数组中的每个元素。我们可以使用这个方法打印不同类型的数组,例如整数数组和字符串数组。
需要注意的是,泛型方法可以独立于泛型类存在,并且可以在任何类中定义和使用。它们提供了更大的灵活性,使我们能够对特定的方法进行泛型化,而不仅仅是整个类。
在调用泛型方法时,我们需要注意几个关键点:
需要注意的是,调用泛型方法时,编译器会根据传递的参数类型和上下文进行类型检查。如果类型不匹配,将产生编译错误。
泛型接口是具有泛型类型参数的接口。通过使用泛型接口,我们可以在接口级别上使用类型参数,使得实现类能够处理不同类型的数据,并提高代码的灵活性和复用性。
以下是定义泛型接口的一般语法:
java复制代码public interface InterfaceName<T> { // 接口方法和常量声明}
在上面的语法中,<T> 表示类型参数的占位符,可以是任意标识符(通常使用单个大写字母)。T 可以在接口方法、常量和内部类中使用。
下面是一个简单的示例,展示了如何定义和使用泛型接口:
java复制代码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 方法获取整数值。
需要注意的是,实现泛型接口时可以选择具体地指定类型参数,也可以继续使用泛型。
使用以上两种方式中的一种,您可以根据需要选择实现泛型接口的方式。具体取决于实现类在处理数据时需要限定特定类型还是保持灵活性。
另外,无论使用哪种方式来实现泛型接口,都需要确保实现类中的方法签名与泛型接口中定义的方法完全匹配。这包括方法名称、参数列表和返回类型。
上界通配符(Upper Bounded Wildcard)
上界通配符用于限制泛型类型参数必须是指定类型或指定类型的子类。使用 extends 关键字指定上界。
语法:
java复制代码<? extends Type>
例如,假设我们有一个泛型方法 printList,它接受一个列表,并打印列表中的元素。但我们希望该方法只能接受 Number 类型或其子类的列表,可以使用上界通配符来实现:
java复制代码public static void printList(List<? extends Number> list) { for (Number element : list) { System.out.println(element); }}// 调用示例List<Integer> integerList = Arrays.asList(1, 2, 3);printList(integerList); // 可以正常调用List<String> stringList = Arrays.asList("Hello", "World");printList(stringList); // 编译错误,String 不是 Number 的子类
在上面的示例中,printList 方法使用 <? extends Number> 定义了一个上界通配符,表示方法接受一个 Number 类型或其子类的列表。因此,我们可以传递一个 Integer 类型的列表作为参数,但不能传递一个 String 类型的列表。
下界通配符(Lower Bounded Wildcard)
下界通配符用于限制泛型类型参数必须是指定类型或指定类型的父类。使用 super 关键字指定下界。
语法:
java复制代码<? super Type>
例如,假设我们有一个泛型方法 addToList,它接受一个列表和一个要添加到列表中的元素。但我们希望该方法只能接受 Object 类型或其父类的元素,可以使用下界通配符来实现:
java复制代码public static void addToList(List<? super Object> list, Object element) { list.add(element);}// 调用示例List<Object> objectList = new ArrayList<>();addToList(objectList, "Hello");addToList(objectList, 42);List<String> stringList = new ArrayList<>();addToList(stringList, "World"); // 编译错误,String 不是 Object 的父类
在上面的示例中,addToList 方法使用 <? super Object> 定义了一个下界通配符,表示方法接受一个 Object 类型或其父类的列表,并且可以向列表中添加任意类型的元素。因此,我们可以将字符串和整数添加到 objectList 中,但不能将字符串添加到 stringList 中。
需要注意的是,上界通配符和下界通配符主要用于灵活地处理泛型类型参数,以便在泛型代码中处理不同类型的数据。它们提供了更大的灵活性和复用性。
泛型方法中使用通配符的场景:
泛型接口中使用通配符的场景:
上述场景中,使用通配符的目的是提供更大的灵活性和复用性。通配符允许我们在泛型方法和泛型接口中处理多种类型的数据,而不需要与具体类型绑定。这样可以使代码更通用、可扩展,并且适用于更广泛的场景。
泛型集合框架是Java中提供的一组用于存储和操作数据的容器类,它们支持泛型类型参数。泛型集合框架在 JDK 中的 java.util 包下提供了丰富的实现,包括列表(List)、集合(Set)、映射(Map)等。
核心接口:
泛型的优势:
泛型集合框架的主要优势是提供了类型安全和编译时类型检查的功能。通过指定泛型类型参数,我们可以在编译时捕获许多类型错误,并避免在运行时出现类型转换异常。泛型还提供了更好的代码可读性和可维护性,因为它们明确地指定了容器中存储的元素类型。
示例用法:
以下是一些常见的泛型集合框架的示例用法:
java复制代码// 创建一个泛型列表,并添加元素List<String> stringList = new ArrayList<>();stringList.add("Hello");stringList.add("World");// 使用迭代器遍历列表for (String element : stringList) { System.out.println(element);}// 创建一个泛型集合,并添加元素Set<Integer> integerSet = new HashSet<>();integerSet.add(1);integerSet.add(2);integerSet.add(3);// 判断集合是否包含特定元素boolean containsTwo = integerSet.contains(2);System.out.println(containsTwo); // 输出: true// 创建一个键值对映射表,并添加元素Map<String, Integer> stringToIntegerMap = new HashMap<>();stringToIntegerMap.put("One", 1);stringToIntegerMap.put("Two", 2);stringToIntegerMap.put("Three", 3);// 根据键获取值int value = stringToIntegerMap.get("Two");System.out.println(value); // 输出: 2
通过使用泛型集合框架,我们可以轻松地创建和操作不同类型的集合,并且在编译时获得类型安全和检查的好处。
泛型类型擦除(Type Erasure)是Java中泛型的实现方式之一。它是在编译期间将泛型类型转换为非泛型类型的一种机制。在泛型类型擦除中,泛型类型参数被擦除为它们的上界或 Object 类型,并且类型检查主要发生在编译时而不是运行时。
泛型类型擦除的原理:
泛型类型擦除的影响:
示例影响:
以下示例说明了泛型类型擦除的影响:
java复制代码// 定义一个泛型类public class GenericClass<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; }}// 使用泛型类GenericClass<String> stringGeneric = new GenericClass<>();stringGeneric.setValue("Hello");String value = stringGeneric.getValue();// 编译后的泛型类型擦除GenericClass stringGeneric = new GenericClass();stringGeneric.setValue("Hello");String value = (String) stringGeneric.getValue(); // 需要进行类型转换// 运行时类型异常示例GenericClass<String> stringGeneric = new GenericClass<>();GenericClass<Integer> integerGeneric = new GenericClass<>();System.out.println(stringGeneric.getClass() == integerGeneric.getClass()); // 输出: truestringGeneric.setValue("Hello");try { Integer value = integerGeneric.getValue(); // 运行时抛出 ClassCastException 异常} catch (ClassCastException e) { System.out.println("ClassCastException: " + e.getMessage());}
泛型桥方法(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编译器可以在继承和实现泛型类型时保持类型安全性和兼容性。这些桥方法在内部转换和维护泛型类型擦除的同时,提供了更好的类型检查和运行时类型安全性。
在泛型中,类型安全性是指编译器对类型进行检查以确保程序在运行时不会出现类型错误。通过使用泛型,可以在编译时捕获许多类型错误,并避免在运行时出现类型转换异常。
类型安全性的优势:
类型安全性的实现:
运行时异常:
尽管泛型增强了类型安全性,但在某些情况下仍可能发生运行时异常。这些异常通常发生在以下情况:
示例:
java复制代码List<String> stringList = new ArrayList<>();stringList.add("Hello");stringList.add("World");// 编译时类型检查,不允许添加非 String 类型的元素stringList.add(123); // 编译错误// 获取元素时不需要进行类型转换String firstElement = stringList.get(0);// 迭代器遍历时可以确保元素类型的安全性for (String element : stringList) { System.out.println(element);}// 类型擦除引起的运行时异常示例List<Integer> integerList = new ArrayList<>();integerList.add(10);List rawList = integerList; // 原始类型与泛型类型交互List<String> stringList = rawList; // 编译通过,但在运行时会导致类型错误String firstElement = stringList.get(0); // 运行时抛出 ClassCastException 异常
在这个示例中,原始类型 rawList 在编译时可以与泛型类型 List<String> 相互赋值。但在运行时,当我们尝试从 stringList 中获取元素时,由于类型擦除并且实际存储的是整数类型,会导致 ClassCastException 异常。
因此,尽管泛型提供了类型安全性和编译时类型检查的优势,但仍需小心处理类型擦除和与原始类型的交互,以避免可能的运行时异常。
泛型数组是指使用泛型类型参数创建的数组。然而,Java中存在一些限制,不允许直接创建具有泛型类型参数的数组。这是由于Java泛型的类型擦除机制导致的。
限制:
问题原因:
泛型的类型擦除机制是导致不能直接创建泛型数组的主要原因。泛型在编译时被擦除为原始类型,因此无法在运行时获取泛型类型的具体信息。这就导致了无法确定数组的确切类型。
解决方案:
虽然直接创建具有泛型类型参数的数组是受限制的,但可以通过以下两种解决方案来处理泛型数组的问题:
1. 使用通配符或原始类型数组:
可以使用通配符(?)或原始类型数组来代替具体的泛型类型参数。例如,可以创建 List<?>[] 或者 Object[] 类型的数组。这种方式虽然不会得到类型安全性,但可以绕过编译时的限制。
java复制代码List<?>[] arrayOfLists = new List<?>[5];Object[] objects = new Object[5];
需要注意的是,由于无法确定数组的确切类型,因此在访问数组元素时可能需要进行显式的类型转换。
2. 使用集合或其他数据结构:
可以使用集合(如 ArrayList、LinkedList 等)或其他数据结构代替数组来存储泛型类型参数。这样可以避免直接使用泛型数组带来的限制和问题。
java复制代码List<List<String>> listOfLists = new ArrayList<>();
使用集合的好处是它们提供了更灵活的操作和类型安全性,并且不受泛型数组的限制。
泛型和反射之间存在一些兼容性问题,这是由于Java泛型的类型擦除机制和反射的特性所导致的。
1. 类型擦除导致的信息丢失: 泛型在Java中是通过类型擦除实现的,即在运行时,泛型类型参数会被擦除为原始类型(如 Object)。这意味着在使用反射时,无法获取泛型类型参数的具体信息,只能得到原始类型。
解决方案: 可以使用反射操作获取泛型类、泛型方法或泛型字段的元数据(例如名称、修饰符、泛型参数等),但无法准确获得泛型类型参数的具体类型。在某些情况下,可以结合使用泛型标记接口来传递类型信息,从而在反射操作中获取更多的类型信息。
2. 泛型数组的限制: 无法直接创建具有泛型类型参数的数组。这是由于类型擦除机制导致的,无法在运行时确定泛型类型参数的具体类型。
解决方案: 可以通过使用通配符(?)或原始类型数组来代替具体泛型类型参数的数组。然而,在访问数组元素时可能需要进行显式的类型转换。
3. 泛型方法的反射调用: 反射调用泛型方法时需要注意类型安全性。由于反射操作是在运行时动态执行的,编译器无法进行静态类型检查,因此可能会导致类型错误。
解决方案: 在使用反射调用泛型方法时,可以通过传递正确的参数类型来确保类型安全性,并对返回值进行合适的类型转换。
4. Class 对象的泛型信息限制: 对于具体的泛型类型,无法通过 Class 对象获取其泛型类型参数的具体信息。例如,对于 List<String> 类型,无法直接从 List.class 中获取到泛型类型参数为 String 的信息。
解决方案: 可以使用 TypeToken 类库等第三方库来绕过该限制。TypeToken 可以通过子类化和匿名内部类的方式捕获泛型类型参数的具体信息。
1. 泛型类和接口: 定义带有类型参数的泛型类或接口,可以使代码适用于不同类型的数据。通过在类或接口中使用类型参数,可以在实例化时指定具体的类型。
java复制代码public class GenericClass<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; }}
2. 泛型方法: 定义带有类型参数的泛型方法,可以使方法在调用时根据传入的参数类型进行类型推断,并返回相应的类型。
java复制代码public <T> T genericMethod(T value) { // 方法逻辑 return value;}
3. 通配符: 使用通配符(?)可以表示未知类型或限定类型范围,增加代码的灵活性。
4. 类型限定和约束: 使用类型限定和约束可以限制泛型类型参数的范围,提供更精确的类型信息。
java复制代码public <T extends Number> void processNumber(T number) { // 方法逻辑}
5. 泛型与继承关系: 泛型类和接口可以继承、实现其他泛型类和接口,通过继承关系可以构建更丰富的泛型层次结构。
java复制代码public interface MyInterface<T> { // 接口定义}public class MyClass<T> implements MyInterface<T> { // 类定义}
6. 泛型数组和集合: 使用泛型数组和集合可以处理不同类型的数据集合,提供更安全和灵活的数据存储和操作方式。
java复制代码List<String> stringList = new ArrayList<>();stringList.add("Hello");stringList.add("World");String value = stringList.get(0); // 获取元素,无需转换类型
7. 类型推断: Java 7 引入的钻石操作符(<>)可以根据上下文自动推断类型参数,使代码更简洁。
java复制代码Map<String, List<Integer>> map = new HashMap<>(); // 类型推断
1. 混淆原始类型和泛型类型: 在使用泛型时,应确保正确区分原始类型和泛型类型。原始类型不具有类型参数,并丧失了泛型的好处。
避免方法: 使用泛型类型参数声明类、接口和方法,并在代码中明确指定类型参数。
2. 忽略类型检查警告: 在使用泛型时,编译器可能会生成类型检查警告,如果忽略这些警告,可能导致类型安全问题。
避免方法: 尽量避免直接忽略类型检查警告,可以通过合理的类型限定、类型转换或使用 @SuppressWarnings 注解来解决或抑制警告。
3. 创建泛型数组: 无法直接创建泛型数组,因为Java中的数组具有固定的类型(协变性)。如果尝试创建泛型数组,可能会导致编译时错误或运行时异常。
避免方法: 可以使用通配符或原始类型数组代替具体的泛型数组。例如,使用 List<?> 或 List<Object> 代替 List<T>。
4. 泛型类型擦除: 在运行时,泛型类型参数会被擦除为原始类型(如 Object),导致无法获取泛型类型参数的具体信息。
避免方法: 可以通过传递类型标记或使用第三方库(如 TypeToken)来绕过泛型类型擦除问题,从而获取更多的类型信息。
5. 静态上下文中的泛型: 静态字段、静态方法和静态初始化块不能引用泛型类型参数,因为它们在类加载时就存在,并且与实例化无关。
避免方法: 如果需要在静态上下文中使用泛型类型,可以将泛型参数声明为静态方法内部的局部变量。
6. 范型和可变参数方法: 当调用可变参数方法时,在泛型方法中使用 <T...> 语法可能会导致编译错误。
避免方法: 可以使用边界类型通配符(T[] 或 List<T>)作为参数类型,或者使用非泛型类型参数。
7. 泛型类型参数的边界限定: 当泛型类型参数受到边界限定时,要注意在代码中合理使用这些限制,并防止类型转换错误。
避免方法: 在合适的情况下,使用边界限定来约束泛型类型参数,并在代码中根据边界类型进行相应的操作和转换。
作者:蜀山剑客李沐白
链接:
https://juejin.cn/post/7249913673215836218