Java高级语法详解之泛型
- :one: 概念
- :two: 优势
- :three: 使用
- 3.1 泛型类
- 3.2 泛型接口
- 3.3 泛型方法
- :four: 通配符(Wildcards)
- 4.1 无界通配符(Unbounded Wildcard)
- 4.2 上限通配符(Upper Bounded Wildcard)
- 4.3 下限通配符(Lower Bounded Wildcard)
- :five: 类型擦除(Type Erasure)
- :six: 泛型对协变和逆变的支持
- :seven: 应用场景
- :ear_of_rice: 总结
- :bookmark_tabs: 本文源码下载地址
1️⃣ 概念
Java 编程语言在 JDK 5.0 版本中引入了泛型(Generics)的概念,以增加源代码的类型安全性和可读性。泛型允许类、接口和方法在定义时使用一个或多个类型参数,使得它们可以在编译时具有更强的类型检查,并且能够避免类型转换错误。
泛型的核心思想是参数化类型(Parameterized Type)。通过在类、接口或方法的定义中使用类型参数,我们可以创建一种“模板”,这个模板可以用于不同的数据类型,并在编译时进行类型检查。这个类型参数可以在实例化的时候指定具体的类型,从而实现类型安全。
2️⃣ 优势
使用泛型的主要优势是提供了类型安全和可读性方面的好处:
- 类型安全:使用泛型可以在编译时捕获类型错误。通过在编译时对传递给泛型容器或方法的元素类型进行检查,可以避免在运行时发生意外的
ClassCastException
; - 可读性:通过明确指定类型参数,可以使代码更易读和理解。方法或类命名更清晰,并且减少了需要对类型进行注释或文档说明的情况;
- 代码复用:泛型可以使用相同的类型安全体系来操作多种不同类型的数据,从而提高代码的可重用性;
- 简化开发:使用泛型可以减少冗余的类型转换代码,提供更简洁和优雅的编程方式;
- 错误检测:泛型使得编译器能够在编译时对代码进行更严格的类型检查,减少错误的产生,并会提供更精确的错误提示。
3️⃣ 使用
3.1 泛型类
在Java中,我们可以创建泛型类和泛型接口。泛型类和泛型接口的定义方式与普通类和接口相似,只是在名称后面加上一对尖括号(<>
)并在其中指定类型参数。
下面是一个简单的示例,展示了如何定义一个泛型类:
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
@Override
public String toString() {
return "Box{" +
"value=" + value +
'}';
}
}
在这个示例中,Box
是一个泛型类,类型参数 T
被放置在类名后的尖括号中。在类内部,我们可以使用类型参数 T
来声明成员变量和方法的类型,并在实例化时指定具体的类型。
以下是使用该泛型类的使用示例:
public class GenericClassDemo {
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.setValue(10);
System.out.println(integerBox);
Box<String> stringBox = new Box<>();
stringBox.setValue("hello");
System.out.println(stringBox);
}
}
通过在实例化 Box
类时指定了具体的类型参数,我们可以创建不同类型的 Box
实例,并将不同类型的值存储在其中。
输出结果:
Box{value=10}
Box{value=hello}
3.2 泛型接口
下面是一个简单的示例,定义了一个泛型接口Container<T>
,其中T
表示泛型类型参数。接口中有两个方法:addItem
用于添加一个元素到容器中,getItem
用于获取容器中的元素:
public interface Container<T> {
void addItem(T[] item); // 添加一组元素到容器中
T[] getItem(); // 获取容器中的元素
}
以下是一个实现了该泛型接口的具体类Box<T>
,该类通过实现接口的方式来定义接口中的方法:
import java.util.Arrays;
// 实现泛型接口
public class Box<T> implements Container<T> {
private T[] item;
public void addItem(T[] item) {
this.item = item;
}
public T[] getItem() {
return item;
}
@Override
public String toString() {
return "Box{" +
"item=" + Arrays.toString(item) +
'}';
}
}
以下是使用该泛型接口的使用示例:
public class GenericsInterfaceDemo {
public static void main(String[] args) {
// 使用泛型接口
Container<String> container1 = new Box<>();
container1.addItem(new String[]{"Hello","world"});
System.out.println(container1);
Container<Integer> container2 = new Box<>();
container2.addItem(new Integer[]{42,36});
System.out.println(container2);
}
}
在main
函数中,我创建了两个泛型容器对象container1
和container2
,分别使用了泛型实参String
和Integer
。然后,调用addItem
方法将一组元素添加到容器中,最后,将容器对象的元素进行打印输出。
运行结果:
Box{item=[Hello, world]}
Box{item=[42, 36]}
3.3 泛型方法
除了在类和接口级别上定义泛型之外,Java 还允许在单独的方法中使用泛型。这被称为泛型方法(Generic Method)。
下面是一个简单的示例,展示了如何定义泛型方法:
public class ArrayUtils {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
使用泛型方法:
public class GenericMethodDemo {
public static void main(String[] args) {
// 使用泛型方法
Integer[] intArray = {1, 2, 3, 4, 5};
ArrayUtils.printArray(intArray);
String[] strArray = {"Hello", "World"};
ArrayUtils.printArray(strArray);
}
}
在这个示例中,printArray
是一个泛型方法。类型参数 T
被放置在方法的返回类型之前的尖括号中,它表示任意类型。通过在调用泛型方法时传递相应的实际参数类型,编译器会对数组进行相应的类型检查,并执行相应的方法体。
运行结果:
1 2 3 4 5
Hello World
4️⃣ 通配符(Wildcards)
在泛型中,可以使用通配符来表示一组类型。通配符允许我们在泛型代码中处理不同类型的参数。
Java 提供了三种通配符:?
无界通配符(Unbounded Wildcard)、? extends T
上限通配符(Upper Bounded Wildcard)和 ? super T
下限通配符(Lower Bounded Wildcard)。
- 无界通配符(
?
):用于表示未知类型。例如,List<?>
表示一个具有未知元素类型的列表; - 上限通配符(
? extends T
):用于限制传入的类型必须是某个类或其子类。例如,List<? extends Number>
表示一个元素类型是Number
或其子类的列表; - 下限通配符(
? super T
):用于限制传入的类型必须是某个类或其父类。例如,List<? super Integer>
表示一个元素类型是Integer
或其父类的列表。
4.1 无界通配符(Unbounded Wildcard)
无界通配符(?
) 允许我们对未知类进行操作,并在类型安全的前提下进行编码。
以下是一个示例,展示了如何使用无界通配符:
import java.util.ArrayList;
import java.util.List;
//一个使用了泛型的无界通配符的演示程序
public class UnboundedWildcardDemo {
static class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
static class Dog extends Animal {
public void makeSound() {
System.out.println("Dog is barking");
}
}
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Dog());
// 定义一个使用无界通配符的List
List<?> wildcardList = animals;
// 使用无界通配符迭代列表并调用方法
for (Object animal : wildcardList) {
// 由于通配符类型未知,只能调用Object上定义的方法
System.out.println(animal.toString());
}
}
}
在这个例子中,我定义了一个Animal
类和一个继承自Animal
的Dog
类。然后创建了一个ArrayList
来存储Animal
对象和Dog
对象。
接下来,使用无界通配符(?
)定义了另一个列表wildcardList
,将其指向之前创建的animals
列表。这意味着可以将任何类型的列表赋值给wildcardList
,而不仅仅是List<Animal>
。
在循环中,通过迭代wildcardList
中的元素来调用它们的toString()
方法。由于无界通配符的类型未知,只能将迭代出的元素视为Object
。在本例中,输出结果将是每个对象的默认toString()实现。
运行结果:
com.xiaoshan.unboundedwildcard.UnboundedWildcardDemo$Animal@1b6d3586
com.xiaoshan.unboundedwildcard.UnboundedWildcardDemo$Dog@4554617c
4.2 上限通配符(Upper Bounded Wildcard)
上限通配符(? extends T
):用于限制传入的类型必须是某个类T 或其子类。
以下是一个示例,展示了如何使用上限通配符:
import java.util.Arrays;
import java.util.List;
public class UpperBoundedWildcardDemo {
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
double result = sum(integers);
System.out.println(result);
List<Double> doubles = Arrays.asList(1.5, 2.2, 3.82);
result = sum(doubles);
System.out.println(result);
}
}
在这个示例中,sum
方法使用上限通配符(? extends Number
),它允许方法接受继承自 Number
的子类的列表作为参数,例如 Integer
或 Double
,使得支持多种数字类型的运算。在方法内部,通过迭代遍历列表中的每个元素,将其转换为 double
类型并累加到变量 total
上。最后,返回 total
的值。
通过扩展泛型方法的灵活性,可以将不同类型的列表传递给 sum
方法,并对其进行求和操作。
运行结果:
6.0
7.52
4.3 下限通配符(Lower Bounded Wildcard)
下限通配符(? super T
):用于限制传入的类型必须是某个类或其父类。
以下是一个示例,展示了如何使用下限通配符。
首先定义两个类Fruit
和Apple
。Fruit
是一个父类,Apple
是Fruit
的子类。
public class Fruit {
private String info = "一个水果";
@Override
public String toString() {
return "Fruit{" +
"info='" + info + '\'' +
'}';
}
}
class Apple extends Fruit {
private String info = "一个苹果";
@Override
public String toString() {
return "Apple{" +
"info='" + info + '\'' +
'}';
}
}
然后以下程序展示了Java中下界通配符的使用:
import java.util.Arrays;
import java.util.List;
public class LowerBoundedWildcardDemo {
public static void printf(List<? super Apple> list) {
System.out.println(list);
}
public static void main(String[] args) {
List<Apple> appleList = Arrays.asList(new Apple(), new Apple());
printf(appleList);
List<Fruit> fruitList = Arrays.asList(new Fruit(), new Fruit());
printf(fruitList);
}
}
静态方法printf
的参数是一个类型为 List<? super Apple>
的列表,这个列表可以接受任何类型为Apple
及其父类的列表。 然后只简单地打印了传递给方法的列表。
在main
方法中,首先创建了一个List<Apple>
对象appleList
并将其初始化为包含两个Apple
对象的数组的列表。然后,调用printf
方法并将appleList
作为参数传递给它。由于参数类型为List<Apple>
,而printf
方法接受的参数类型为List<? super Apple>
,所以这个调用是合法的。
接下来,创建了一个List<Fruit>
对象fruitList
并将其初始化为包含两个Fruit
对象的数组的列表。然后,再次调用printf
方法并将fruitList
作为参数传递给它。由于Fruit
是Apple
的父类,所以这个调用仍然是合法的。
运行结果:
[Apple{info='一个苹果'}, Apple{info='一个苹果'}]
[Fruit{info='一个水果'}, Fruit{info='一个水果'}]
5️⃣ 类型擦除(Type Erasure)
Java 的泛型实现使用了类型擦除机制。这意味着泛型的类型信息只存在于代码编译阶段,在运行时会被擦除掉。类型擦除是为了实现与之前版本的 Java 兼容,并且可以在运行时提高性能。
由于类型擦除,泛型类型参数在运行时会被擦除为它们的原始类型或限定类型。例如,一个泛型类在运行时会变成它的原始形式。这就是为什么无法在运行时获得泛型类型参数的具体类型。
以下是一个示例,展示了类型擦除的效果:
public class TypeErasureDemo<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
// 使用类型擦除
TypeErasureDemo<String> example = new TypeErasureDemo<>();
example.setValue("Hello");
// 返回类型为 String
String value = example.getValue();
System.out.println(value);
// 返回类型为 TypeErasureDemo,而不是 TypeErasureDemo<String>
Class<? extends TypeErasureDemo> clazz = example.getClass();
System.out.println(clazz);
}
}
在这个示例中,GenericExample
是一个泛型类。虽然我在实例化 GenericExample
时指定了具体的类型参数(String
),但是在运行时通过调用 example.getClass()
来获取其类对象时,,返回的类型是 GenericExample
,而不是 GenericExample<String>
。这说明在运行时,泛型类型参数会被擦除为其原始类型。
运行结果:
Hello
class com.xiaoshan.typeerasure.TypeErasureDemo
尽管在运行时无法获得泛型类型参数的具体类型信息,但是可以通过反射来获取泛型类或方法的基本信息。通过反射,可以获取泛型类的属性、方法和接口等,并进一步操作它们。
6️⃣ 泛型对协变和逆变的支持
需要注意的是,泛型在存在子类型关系时有一些特殊的规则:
- 泛型不支持协变:即使
Sub
是Super
的子类,Container<Sub>
也不是Container<Super>
的子类型; - 泛型不支持逆变:即使
Super
是Sub
的子类,Container<Super>
也不是Container<Sub>
的父类型。 - 泛型可以使用上限通配符(
? extends T
)模拟协变,使用下限通配符(? super T
)模拟逆变。
🔍 协变(covariant)和逆变(contravariant)是什么?
协变和逆变是类型系统中的概念,用于描述类型之间的替换关系。
协变指的是在一种类型替换另一种类型时,被替换的类型(通常称为子类型或窄类型)可以是原始类型(通常称为父类型或宽类型)的子类型。换句话说,当你需要一个特定类型的对象时,你可以使用它的子类型作为替代,且不会引发任何错误。
逆变则是相反的概念-指的是当一个类型替换另一个类型时,被替换的类型可以是原始类型的父类型。换句话说,在需要特定类型的地方,可以使用其父类型的实例而不会导致错误。
总结起来,协变描述了窄类型(子类型)替换宽类型(父类型),而逆变则描述了宽类型(父类型)替换窄类型(子类型)。在类型系统中,这些概念有助于确保类型安全和灵活性,允许我们在使用类型的地方传递更具体或更一般化的类型对象。
以下是一个示例,展示了这些规则的应用:
import java.util.ArrayList;
import java.util.List;
public class InvarianceDemo{
static class Super {
@Override
public String toString() {
return "Super{}";
}
}
static class Sub extends Super {
@Override
public String toString() {
return "Sub{}";
}
}
public static void test1() {
// 1、不支持协变
// 编译不通过:Required type: List <Super>. Provided: ArrayList <Sub>
List<Super> superList1 = new ArrayList<Sub>();
// 编译不通过:Required type: List <Super>. Provided: List <Sub>
List<Sub> subList1 = new ArrayList<>();
List<Super> superList = subList1;
// 2、不支持逆变
// 编译不通过:Required type: List <Sub>. Provided: ArrayList <Super>
List<Sub> subList2 = new ArrayList<Super>();
// 编译不通过:Required type: List <Sub>. Provided: List <Super>
List<Super> superList2 = new ArrayList<>();
List<Sub> subList = superList2;
}
public static void test2() {
// 1、实现协变
// 创建一个泛型为 Super 的列表 list,其类型为 List<? extends Super>
List<? extends Super> list = new ArrayList<Super>();
// 创建一个子类为 Sub 的列表 subList 并向其中添加两个新的 Sub 对象
List<Sub> subList = new ArrayList<>();
subList.add(new Sub());
subList.add(new Sub());
// 将 subList 赋值给 list 变量,因为 subList 是一个子类列表,可以赋值给泛型为 Super 的列表
list = subList;
System.out.println(list);
// 2、实现逆变
// 创建一个泛型为 Sub 的超类列表 list2,其类型为 List<? super Sub>
List<? super Sub> list2 = new ArrayList<Sub>();
// 创建一个超类为 Super 的列表 superList 并向其中添加两个新的 Super 对象
List<Super> superList = new ArrayList<>();
superList.add(new Super());
superList.add(new Super());
// 将 superList 赋值给 list2 变量,因为 superList 是一个超类列表,可以赋值给泛型为 Sub 的列表
list2 = superList;
System.out.println(list2);
}
}
在这个示例中,test1
展示了泛型类型的不变性。虽然 Super
是 Sub
的父类,但是 List<Super>
并不是 List<Sub>
的父类,反之亦然。所以test1
里的代码全部无法通过编译,如下图:
而 test2
方法则展示了通过使用上限通配符(? extends T
)来模拟实现协变,使用下限通配符(? super T
)来模拟实现逆变。
运行结果:
[Sub{}, Sub{}]
[Super{}, Super{}]
7️⃣ 应用场景
泛型在实际开发中有很多应用场景,以下是一些常见的用法:
- 容器类:泛型使得容器类(如列表、集合、映射)可以存储不同类型的数据,并提供类型安全的访问和操作;
- 泛型算法:通过使用泛型方法,可以编写适用于多种类型的通用算法,减少了代码的重复,增加了代码的可重用性;
- 数据结构:泛型也被广泛用于定义数据结构,如栈、队列、二叉树等。通过使用泛型,可以定义通用的数据结构,以便处理不同类型的数据;
- 接口的泛型化:在设计接口时,可以使用泛型将其参数或返回类型与具体实现解耦,从而提高代码的灵活性和可扩展性;
- 异常处理:Java 标准库中的异常类也使用了泛型,这样可以更好地捕获和处理特定类型的异常。
🌾 总结
泛型是 Java 语言提供的一种强大的功能,它允许在编译时对类型进行检查,并提高代码的类型安全性和可读性。通过在类、接口或方法的定义中使用类型参数,可以创建通用的数据结构和算法,并简化代码的开发过程。
尽管泛型的实现采用了类型擦除机制,导致在运行时无法获得泛型类型参数的具体类型信息,但是仍然可以通过通配符和反射等机制来操作泛型对象和获取相关的元数据。
在实际开发中,泛型被广泛应用于容器类、算法、数据结构、接口的设计以及异常处理等领域,它大大提高了代码的可重用性、可扩展性和可维护性。
掌握泛型的使用方法以及了解其原理和限制,将使开发人员能够更好地利用 Java 编程语言的强大功能,并编写更优雅、健壮且类型安全的代码。
📑 本文源码下载地址
Java语言 泛型讲解案例代码(泛型类、泛型接口、泛型方法、无界及上下限通配符、泛型对协变和逆变的支持、类型擦除 …)