一.泛型的概念
1.1什么是泛型
现在你有一个多功能的保温杯,可以用来装各种类型的饮料,例如咖啡、茶和果汁。这个保温杯就可以被看作是一个泛型容器,不同的饮料代表不同类型的参数。
其中保温杯就是一个泛型类,它可以适用于不同类型的饮料。泛型参数就是饮料的类型,例如咖啡、茶或果汁。通过使用泛型,你不需要为每种类型的饮料分别设计一个特定的容器,而是可以使用同一个保温杯来装不同类型的饮料。
1.2引出泛型
泛型的语法:
前提:类名后的 <T> 代表占位符,表示当前类是一个泛型类
- 我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10];
- 所有类的父类,默认为Object类。数组是否可以创建为Object?
通过以上发现:
- 任何类型数据都可以存放
- 1号下标本身就是字符串,但是确编译报错。必须进行强制类型转换
class 泛型类名称<类型形参列表> {// 这里可以使用类型参数}
代码案例:
class Pair<T1, T2> {
private T1 first;
private T2 second;
public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}
public T1 getFirst() {
return first;
}
public T2 getSecond() {
return second;
}
}
public class Main {
public static void main(String[] args) {
// 创建一个存储整数和字符串的Pair对象
Pair<Integer, String> pair = new Pair<>(1, "One");
int first = pair.getFirst();
String second = pair.getSecond();
System.out.println("First: " + first);
System.out.println("Second: " + second);
// 创建一个存储布尔值和字符的Pair对象
Pair<Boolean, Character> anotherPair = new Pair<>(true, 'A');
boolean bool = anotherPair.getFirst();
char ch = anotherPair.getSecond();
System.out.println("Boolean: " + bool);
System.out.println("Character: " + ch);
}
}
代码解析:
我们定义了一个名为Pair
的泛型类,并使用了类型形参T1
和T2
。该类有两个成员变量first
和second
,它们分别使用了类型形参T1
和T2
。我们在构造函数中接受一个first
和一个second
,并将它们分配给相应的成员变量。
在Main
类的main
方法中,我们创建了两个Pair
对象。第一个对象存储了一个整数和一个字符串,通过指定类型实参Integer
和String
,我们告诉编译器在实例化Pair
类时使用这些类型。第二个对象存储了一个布尔值和一个字符,通过指定类型实参Boolean
和Character
,我们指定了相应的类型。
运行截图:
语法2:
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {// 这里可以使用类型参数}
代码案例:
class GenericClass<T> extends BaseClass {
private T genericField;
public GenericClass(T genericField) {
this.genericField = genericField;
}
public T getGenericField() {
return genericField;
}
public void setGenericField(T genericField) {
this.genericField = genericField;
}
// 可以使用类型参数和继承类的方法
public void someMethod() {
// 使用类型参数
System.out.println("Generic Field: " + genericField);
// 调用继承类的方法
super.baseMethod();
}
}
class BaseClass {
public void baseMethod() {
System.out.println("Base Method");
}
}
public class Main {
public static void main(String[] args) {
// 创建一个存储整数的GenericClass对象
GenericClass<Integer> genericObj = new GenericClass<>(10);
int value = genericObj.getGenericField();
System.out.println("Value: " + value);
// 调用泛型类的方法
genericObj.someMethod();
}
}
代码解析:
我们定义了一个名为GenericClass
的泛型类,它继承自BaseClass
。该泛型类有一个类型形参T
,并具有一个泛型字段genericField
,以及相关的getter和setter方法。在someMethod
方法中,我们演示了如何使用类型参数和调用继承类的方法。
在Main
类的main
方法中,我们创建了一个GenericClass
对象,其中类型参数为Integer
。通过指定类型实参Integer
,我们告诉编译器在实例化GenericClass
类时使用该类型。然后,我们获取泛型字段的值并输出。
运行截图:
- E 表示 Element
- K 表示 Key
- V 表示 Value
- N 表示 Number
- T 表示 Type
- S, U, V 等等 - 第二、第三、第四个类型
二.泛型的使用
语法:
泛型类<类型实参> 变量名; // 定义一个泛型类引用new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
代码案例:
MyArray<Integer> list = new MyArray<Integer>();
比如:
class GenericClass<T> {
private T genericField;
public GenericClass(T genericField) {
this.genericField = genericField;
}
public T getGenericField() {
return genericField;
}
public void setGenericField(T genericField) {
this.genericField = genericField;
}
}
public class Main {
public static void main(String[] args) {
// 使用包装类作为类型参数
GenericClass<Integer> obj1 = new GenericClass<>(10);
int intValue = obj1.getGenericField();
System.out.println("Integer Value: " + intValue);
GenericClass<Double> obj2 = new GenericClass<>(3.14);
double doubleValue = obj2.getGenericField();
System.out.println("Double Value: " + doubleValue);
}
}
代码解析:
我们使用包装类作为泛型类的类型参数。使用 Integer
包装类实例化了一个 GenericClass
对象 obj1
,并提供整数值 10
。我们获取泛型字段的值并将其转换为基本数据类型 int
。
同样地,我们使用 Double
包装类实例化了另一个 GenericClass
对象 obj2
,并提供浮点数值 3.14
。我们获取泛型字段的值并将其转换为基本数据类型 double
。
运行截图:
2.1类型推导
1.泛型类实例化时的类型推断:
// 完整写法
List<String> list = new ArrayList<String>();
// 类型推断,可以省略类型实参
List<String> list = new ArrayList<>();
2.方法调用时的类型推断:
// 完整写法
String firstElement = Collections.<String>first(list);
// 类型推断,可以省略类型实参
String firstElement = Collections.first(list);
2.2裸类型
指在泛型类或泛型方法中省略了类型参数的使用,或者使用原始类型作为类型参数的情况。
1.泛型类的裸类型:
List list = new ArrayList(); // 裸类型
list.add("Hello");
String value = (String) list.get(0); // 需要进行强制类型转换
2.泛型方法的裸类型
public static <T> T identity(T element) {
return element;
}
// 使用泛型方法的裸类型
Object result = Main.<Object>identity("Hello"); // 显式提供类型实参
注意:如果使用裸类型,则编译器将进行类型擦除,将方法的类型参数视为其原始类型(在这种情况下是 Object
),裸类型的使用通常是不推荐的,因为它会绕过编译器对类型安全性的检查。裸类型可能导致运行时错误或类型不匹配的问题。
三.泛型是如何编译的
3.1泛型的擦除机制
泛型的编译是通过泛型的擦除机制。在编译时,Java 编译器会擦除泛型的类型信息,将泛型类型转换为它们的原始类型或限定类型。
在这里提出两个问题:
T
的具体类型,从而无法创建一个泛型数组。如果允许创建泛型数组,例如
Object[] ts = new Object[5]
,然后将其转换为
T[]
,就会存在类型安全性问题。这是因为在运行时,数组的实际类型是
Object[]
,而不是
T[]
,从而可能导致出现类型不匹配的情况。如果需要表示泛型数组的概念,可以使用通配符
?
或无限定通配符
<?>
,
List<?>[] lists = new List<?>[5],
T
,在类型擦除时会被擦除为
Object
。这是因为没有指定上界,所以
Object
是最广泛的类型。然而,对于有限定条件的类型参数
T
,在类型擦除时并不一定会被擦除为
Object
。具体的擦除行为取决于泛型类型的上下文和限定条件。
List<?>[] lists = new List<?>[5],
在这个示例中,类型参数 T
被限定为必须是 Number
或其子类。在类型擦除时,T
不会被擦除为 Object
,而是被擦除为 Number
。这样可以保持类型安全性,并限制泛型参数的类型范围。
3.2为何不能实例化泛型类型数组
代码案例:
package demo3;
class MyArray<T> {
public T[] array = (T[])new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public class Main {
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
Integer[] strings = myArray1.getArray();
}
}
报错截图:
原因:替换后的方法为:将Object[]分配给Integer[]引用,程序报错
public Object[] getArray() {
return array;
}
所以正确的方式: 通过反射创建,指定类型的数
public MyArray(Class<T> clazz, int capacity) {
array = (T[])Array.newInstance(clazz, capacity);
}
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);
Integer[] integers = myArray1.getArray();
}
四.泛型的类型边界
4.1泛型的上界
泛型的上界是指在泛型类型参数中指定的类型的限定条件。通过使用上界,可以限制泛型类型参数必须是指定类型或其子类型。
通过使用关键字 extends
来指定泛型类型参数的上界
语法:
class 泛型类名称<类型形参 extends 类型边界> {...}
import java.util.ArrayList;
import java.util.List;
public class MyArray<E extends Number> {
private List<E> list;
public MyArray() {
list = new ArrayList<>();
}
public void add(E element) {
list.add(element);
}
public E get(int index) {
return list.get(index);
}
}
public class Main {
public static void main(String[] args) {
MyArray<Integer> l1 = new MyArray<>();
l1.add(5);
l1.add(10);
Integer num1 = l1.get(0);
Integer num2 = l1.get(1);
System.out.println(num1 + ", " + num2);
MyArray<String> l2 = new MyArray<>(); // 编译通过
l2.add("Hello");
// 编译错误,String 不是 Number 的子类型
String str = l2.get(0);
System.out.println(str);
}
}
4.2泛型的下界
泛型的下界是指在泛型类型参数中指定一个限制,该限制要求泛型类型参数必须是指定的类型或其超类型。
泛型下界的语法使用下界通配符 super
,其语法为 ? super T
,其中 T
是指定的类型。
下界通配符 super
的作用是限制泛型类型参数必须是指定类型的超类型。
举一个通俗的例子:
现在我有一个购物袋,我可以放入不同类型的物品,例如水果和蔬菜。
现在,假设你有以下几种类别的物品:
- 类型A:水果
- 类型B:蔬菜
- 类型C:食品
其中我可以放入类型A的物品或者类型A的超类型,这样购物袋就可以容纳各种水果。
在这个例子中,购物袋就是一个泛型类,而类型A代表泛型类型参数的下界。泛型下界的目的是为了确保购物袋可以接受指定类型及其超类型的物品。
代码案例:
package demo3;
class ShoppingCart<T> {
private T item;
public void addItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart<? super Fruit> shoppingCart = new ShoppingCart<>();
shoppingCart.addItem(new Fruit("Apple")); // 添加一个水果
// 编译错误,无法获取物品
// Fruit fruit = shoppingCart.getItem();
Object item = shoppingCart.getItem();
System.out.println(item); // 输出: Fruit{name='Apple'}
}
}
class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
@Override
public String toString() {
return "Fruit{name='" + name + "'}";
}
}
代码解析:
以上代码已经使用了泛型下界的语法。在 ShoppingCart
类的声明中,类型参数 T
没有指定上界,因此它可以是任何类型。
在 main
方法中,ShoppingCart<? super Fruit>
表示创建了一个 ShoppingCart
对象,该对象的泛型类型参数是 Fruit
或 Fruit
的超类型。这意味着我们可以将其赋值为 ShoppingCart<Fruit>
、ShoppingCart<Object>
或其他 Fruit
的超类型的实例。
通过 shoppingCart.addItem(new Fruit("Apple"))
方法,我们添加了一个水果到购物车。由于类型参数的下界是 Fruit
的超类型,因此可以接受 Fruit
类型的值。
然后,我们尝试使用 shoppingCart.getItem()
方法获取物品,并将其赋值给 item
变量。由于类型参数的下界是 Fruit
的超类型,返回的值的类型为 Object
。我们可以打印输出 item
的值,得到 Fruit{name='Apple'}
。
运行截图:
4.3泛型的方法
泛型方法是指在方法声明中使用泛型类型参数的方法。
定义语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
...
}
package demo3;
import java.util.Arrays;
public class Main {
public static <T> void swap(T[] array, int index1, int index2) {
T temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
public static void main(String[] args) {
Integer[] numbers = {1, 2, 3, 4, 5};
System.out.println("Before swap: " + Arrays.toString(numbers));
swap(numbers, 1, 3);
System.out.println("After swap: " + Arrays.toString(numbers));
}
}
代码解析:
swap
方法是一个泛型方法,它接受一个泛型数组 array
,以及要交换的两个索引 index1
和 index2
。
在 main
方法中,我们创建了一个整数数组 numbers
,并调用 swap
方法来交换索引为 1 和 3 的元素。最后,我们打印输出交换后的数组。
通过使用泛型方法,我们可以在不同类型的数组上重用 swap
方法,而不需要为每种类型都编写一个单独的交换方法。
运行截图:
4.3.1可以类型推导
泛型方法允许根据方法参数的类型来推导泛型类型参数。这种类型推导可以减少代码中显式指定泛型类型参数的冗余。
class GenericMethods {
public <T> void printArray(T[] array) { // 定义泛型方法printArray,接受类型为T的数组作为参数
for (T element : array) { // 使用增强for循环遍历数组中的元素
System.out.println(element); // 打印数组中的元素
}
}
}
public class Main {
public static void main(String[] args) {
GenericMethods genericMethods = new GenericMethods(); // 创建GenericMethods对象
Integer[] intArray = {1, 2, 3, 4, 5}; // 创建Integer类型的数组
genericMethods.printArray(intArray); // 调用printArray方法,传递intArray数组作为参数
String[] stringArray = {"Hello", "World"}; // 创建String类型的数组
genericMethods.printArray(stringArray); // 调用printArray方法,传递stringArray数组作为参数
}
}
代码解析:
在上述例子中,GenericMethods
类包含一个泛型方法 printArray
。该方法接受一个类型为 T
的数组,并遍历打印数组中的元素。
在 main
方法中,我们创建了一个 GenericMethods
对象,并使用不同类型的数组调用了 printArray
方法。注意,我们没有显式地指定泛型类型参数,而是根据传递的实参类型进行了类型推导。
对于 intArray
,编译器会推导出 T
的类型为 Integer
,并相应地调用 printArray(Integer[] array)
方法。
对于 stringArray
,编译器会推导出 T
的类型为 String
,并相应地调用 printArray(String[] array)
方法。
4.3.2不使用类型推导
如果不使用类型推导,我们需要显式指定泛型类型参数,以确保编译器能够正确地进行类型检查和方法调用。
代码案例:
class GenericMethods {
public <T> void printArray(T[] array) { // 定义泛型方法printArray,接受类型为T的数组作为参数
for (T element : array) { // 使用增强for循环遍历数组中的元素
System.out.println(element); // 打印数组中的元素
}
}
}
public class Main {
public static void main(String[] args) {
GenericMethods genericMethods = new GenericMethods(); // 创建GenericMethods对象
Integer[] intArray = {1, 2, 3, 4, 5}; // 创建Integer类型的数组
genericMethods.<Integer>printArray(intArray); // 显式指定泛型类型参数为Integer,并调用printArray方法打印数组元素
String[] stringArray = {"Hello", "World"}; // 创建String类型的数组
genericMethods.<String>printArray(stringArray); // 显式指定泛型类型参数为String,并调用printArray方法打印数组元素
}
}
代码解析:
在上述代码中,我们在调用泛型方法 printArray
时使用了显式的类型参数。
对于 intArray
,我们使用 <Integer>
显式指定泛型类型参数,告诉编译器 T
的类型为 Integer
,从而调用 printArray(Integer[] array)
方法。
对于 stringArray
,我们使用 <String>
显式指定泛型类型参数,告诉编译器 T
的类型为 String
,从而调用 printArray(String[] array)
方法。
五.通配符
通过使用通配符,可以允许泛型类型参数接受不同类型的实参
代码案例:
package demo3;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main {
public static void main(String[] args) {
Message<String> message = new Message<>() ;
message.setMessage("北京欢迎您!");
fun(message);
}
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(99);
fun(message); // 出现错误,只能接收String
}
public static void fun(Message<String> temp){//fun()接收的是String类型的参数
System.out.println(temp.getMessage());
}
}
package demo3;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(55);
fun(message);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message<?> temp){
//temp.setMessage(100); 无法修改!
System.out.println(temp.getMessage());
}
}
5.1通配符上界
通配符上界,表示传入的实参类型必须是指定上界的子类(或者本身就是上界类型)。
比如现在我们有一个水果篮子(FruitBasket
)类,它可以容纳各种类型的水果。我们可以使用通配符的上界来限制篮子中存放的水果类型。
语法:
<? extends 上界><? extends Number>//可以传入的实参类型是Number或者Number的子类
class Fruit {
// 水果类
}
class Apple extends Fruit {
// 苹果类
}
class Orange extends Fruit {
// 橙子类
}
class FruitBasket<T extends Fruit> {
private List<T> fruits;
public FruitBasket() {
fruits = new ArrayList<>();
}
public void addFruit(T fruit) {
fruits.add(fruit);
}
public List<T> getFruits() {
return fruits;
}
}
public class Main {
public static void main(String[] args) {
FruitBasket<? extends Fruit> basket1 = new FruitBasket<>();
// 可以添加任何水果类型
basket1.addFruit(new Apple());
basket1.addFruit(new Orange());
List<? extends Fruit> fruits1 = basket1.getFruits();
// 可以获取水果列表,但只能以Fruit或其子类的方式访问
for (Fruit fruit : fruits1) {
System.out.println(fruit);
}
FruitBasket<? extends Apple> basket2 = new FruitBasket<>();
// 只能添加Apple或Apple的子类
basket2.addFruit(new Apple());
// 编译错误,无法添加Orange
// basket2.addFruit(new Orange());
List<? extends Apple> apples = basket2.getFruits();
// 可以获取Apple及其子类的列表,只能以Apple或其子类的方式访问
for (Apple apple : apples) {
System.out.println(apple);
}
}
}
代码解析:
在上述例子中,Fruit
是所有水果类的父类,Apple
和 Orange
分别是 Fruit
的子类。FruitBasket
类使用了通配符的上界 <? extends Fruit>
,这表示篮子中可以放置任何 Fruit
类型或其子类的水果。
通过创建 FruitBasket<? extends Fruit>
对象,我们可以添加不同类型的水果,例如苹果和橙子。然后,我们可以使用 List<? extends Fruit>
来获取水果列表,但只能以 Fruit
或其子类的方式进行访问。
同时,我们还创建了一个 FruitBasket<? extends Apple>
对象,并观察到只能添加苹果或其子类的水果。获取水果列表时,我们可以使用 List<? extends Apple>
,并以 Apple
或其子类的方式进行访问。
注意:通配符的上界,不能进行写入数据,只能进行读取数据。
5.2通配符下界
用于限制传入的实参类型必须是指定下界的超类(或者本身就是下界类型)。
语法:
<? super 下界><? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
同样:
我们也可以使用食物(Food
)和水果(Fruit
)之间的关系来解释。
假设我们有一个食物篮子(FoodBasket
)类,它可以容纳各种类型的食物。在这个例子中,我们将食物作为一个更一般的概念,而水果是食物的一个子类。
class Food {
// 食物类
}
class Fruit extends Food {
// 水果类
}
class Apple extends Fruit {
// 苹果类
}
class Pizza extends Food {
// 比萨类
}
class FoodBasket<T> {
private List<T> foods;
public FoodBasket() {
foods = new ArrayList<>();
}
public void addFood(T food) {
foods.add(food);
}
public List<T> getFoods() {
return foods;
}
}
public class Main {
public static void main(String[] args) {
FoodBasket<? super Fruit> basket1 = new FoodBasket<>();
// 可以添加水果或水果的父类(例如食物)
basket1.addFood(new Fruit());
basket1.addFood(new Apple());
// 编译错误,无法添加比萨
// basket1.addFood(new Pizza());
List<? super Fruit> foods1 = basket1.getFoods();
// 可以获取食物列表,但只能以Object的方式访问
for (Object food : foods1) {
System.out.println(food);
}
FoodBasket<? extends Fruit> basket2 = new FoodBasket<>();
// 只能添加水果或水果的子类(例如苹果)
basket2.addFood(new Fruit());
basket2.addFood(new Apple());
// 编译错误,无法添加食物
// basket2.addFood(new Food());
List<? extends Fruit> foods2 = basket2.getFoods();
// 可以获取食物列表,但只能以Fruit或其子类的方式访问
for (Fruit food : foods2) {
System.out.println(food);
}
}
}
代码解析:
我们创建了一个 FoodBasket<? super Fruit>
对象,这表示篮子中可以放置水果或水果的父类(例如食物)。因此,我们可以添加水果和食物,但无法添加比萨。获取食物列表时,我们使用了 List<? super Fruit>
,这意味着我们只能以 Object
的方式来访问食物。
另外,我们还创建了一个 FoodBasket<? extends Fruit>
对象,这表示篮子中只能放置水果或水果的子类(例如苹果)。因此,我们可以添加水果和苹果,但无法添加食物。获取食物列表时,我们使用了 List<? extends Fruit>
,这意味着我们可以以 Fruit
或其子类的方式来访问食物。
注意: 通配符的下界,不能进行读取数据,只能写入数据。
总结:
泛型进阶的小结如下:
1. 泛型类:可以创建具有泛型类型参数的类,类名后面使用尖括号 `<T>` 来表示泛型类型。泛型类可以在实例化时指定具体的类型参数,以实现类型安全和重用性。
2. 泛型方法:可以在普通类或泛型类中定义泛型方法,方法的返回类型或参数可以是泛型类型。泛型方法可以根据实参的类型推导出泛型类型参数,或者通过显式指定类型参数来调用。
3. 通配符:使用 `?` 通配符表示未知类型,可以用于泛型方法的参数类型、泛型类的类型参数以及通配符限定。通配符可以限制类型的上界或下界,以增加灵活性和泛化能力。
4. 通配符上界和下界:使用 `extends` 关键字指定通配符的上界,表示通配符必须是指定类型或其子类型;使用 `super` 关键字指定通配符的下界,表示通配符必须是指定类型或其父类型。通配符上界和下界的使用可以在泛型方法中灵活处理不同类型的参数。
5. 类型擦除:Java 的泛型是通过类型擦除来实现的,编译器会在编译时擦除泛型类型信息,并进行类型安全检查。在运行时,泛型类型参数被擦除为其上界或 Object 类型。类型擦除可以使泛型代码与旧的非泛型代码兼容,并提供性能优化。
6. 泛型和数组:不能直接创建泛型数组,因为类型擦除后无法确定泛型类型的具体信息。可以使用通配符配合边界限定或转型来处理泛型数组的需求。
7. 泛型和继承:泛型类和泛型接口支持继承关系,可以使用泛型类型参数作为父类或接口的类型参数,实现更灵活的设计和代码重用。