哪个网站做图书广告好,东莞网站推广设计,网站建设续费的回访话术,网站备案审核流程图文章目录 前言1. 泛型概述1.1 不使用泛型 vs 使用泛型1.2 泛型的作用 2. 泛型的基本语法2.1 定义带类型参数的泛型类2.2 使用泛型类2.3 泛型方法 3. 泛型类型推断与钻石操作符3.1 类型推断3.2 钻石操作符 4. 通配符的使用4.1 无界通配符 ?4.2 上界通配符 ? exten… 文章目录 前言1. 泛型概述1.1 不使用泛型 vs 使用泛型1.2 泛型的作用 2. 泛型的基本语法2.1 定义带类型参数的泛型类2.2 使用泛型类2.3 泛型方法 3. 泛型类型推断与钻石操作符3.1 类型推断3.2 钻石操作符 4. 通配符的使用4.1 无界通配符 ?4.2 上界通配符 ? extends T4.3 下界通配符 ? super T 5. 泛型的高级特性5.1 多重边界5.2 泛型嵌套5.3 泛型方法与构造函数 6. 泛型在 Java 集合中的应用6.1 Java 集合框架中的泛型6.2 泛型集合的常见类型6.3 泛型集合的使用6.4 泛型在集合中的优势 7. 泛型的运行时行为与限制7.1 类型擦除7.2 泛型数组与实例化7.3 静态上下文中的泛型 8. 常见泛型问题与解决方案8.1 泛型类型检查8.2 泛型不支持基本类型8.3 泛型方法不能直接抛出或捕获泛型异常 9. 泛型的设计与最佳实践9.1 灵活的泛型 API 设计9.2 PECS 原则 前言
这里是分享 Java 相关内容的专刊每日一更。
本期将为大家带来以下内容
泛型概述泛型的基本语法泛型类型推断与钻石操作符通配符的使用泛型的高级特性泛型在 Java 集合中的应用泛型的运行时行为与限制常见泛型问题与解决方案泛型的设计与最佳实践
1. 泛型概述
泛型听起来很复杂但其实它的工作原理就像一个“模具”或“占位符”。在编写代码时我们可能希望编写一个可以处理多种不同数据类型的功能比如数字、字符串或者其他类型而不需要为每种类型重复编写相同的代码。泛型就能帮我们做到这一点
泛型的核心思想 是让一个类、方法或者接口可以处理不确定的数据类型直到你真正使用它的时候再决定具体用什么类型。
1.1 不使用泛型 vs 使用泛型
不使用泛型的情况假设我们没有泛型那么要写两个盒子一个存 String一个存 Integer代码可能会是这样的
class StringBox {private String item;public void set(String item) {this.item item;}public String get() {return this.item;}
}class IntegerBox {private Integer item;public void set(Integer item) {this.item item;}public Integer get() {return this.item;}
}你会发现我们要写两个几乎完全一样的类只是因为它们处理的数据类型不同。这是非常繁琐的。
使用泛型的情况使用泛型后我们只需要写一个 BoxT 类不管是 String 还是 Integer都可以通过同一个类来处理。
class BoxT {private T item;public void set(T item) {this.item item;}public T get() {return this.item;}
}这样代码更简洁、通用也更容易维护。
1.2 泛型的作用
泛型的主要好处有两个 提高代码的安全性泛型让我们能够提前检查代码中的类型错误。在编译时也就是程序运行前编译器会检查我们传入的类型是否正确。如果类型不对代码甚至不会通过编译。这可以避免很多不必要的错误。比如如果你想要把一个 BoxString 放到一个装数字的盒子里编译器会立刻提醒你错误 BoxInteger intBox new Box();
intBox.set(错误的类型); // 编译器会报错因为它需要的是整数而不是字符串提升代码的复用性泛型让我们可以写出更加通用的代码只用写一次就能适应多种类型。比如BoxT 可以用来存放不同的数据类型不管是 String 还是 Integer都可以复用这段代码。这避免了为每种类型都单独写一份代码。
2. 泛型的基本语法
2.1 定义带类型参数的泛型类
假设我们想创建一个可以存放任何类型数据的盒子这个盒子应该能存放字符串、数字甚至其他类型的对象。我们可以通过泛型类来实现。
class BoxT {private T item;public void set(T item) {this.item item;}public T get() {return this.item;}
}BoxT这里的 T 是泛型中的“占位符”可以代表任何类型。当我们创建 Box 对象时再告诉它 T 具体是什么类型比如 String 或 Integer。
T itemitem 是一个 T 类型的变量而 T 是我们用泛型指定的类型。
2.2 使用泛型类
在使用 Box 这个类时我们需要告诉它 T 具体是什么类型
BoxString stringBox new Box();
stringBox.set(Hello World);
System.out.println(stringBox.get()); // 输出 Hello WorldBoxInteger intBox new Box();
intBox.set(123);
System.out.println(intBox.get()); // 输出 123在上面的例子里
BoxString 表示这是一个装 String 类型数据的盒子。BoxInteger 表示这是一个装 Integer 类型数据的盒子。
class BoxT {private T item; // 这里的 T 是类型的占位符public void set(T item) {this.item item;}public T get() {return this.item;}
}在上面的例子里T 就是占位符它可以代表任何数据类型。等到我们真正使用这个盒子时再告诉它 T 具体是什么类型
BoxString stringBox new Box();
stringBox.set(一本书);BoxInteger integerBox new Box();
integerBox.set(123);在第一个例子里T 是 String所以盒子存的东西是一本书。
在第二个例子里T 是 Integer所以盒子存的是数字 123。
这样我们只写了一次盒子的代码却可以存放不同类型的数据。
2.3 泛型方法
不仅是类方法也可以使用泛型。比如写一个打印任何类型的东西的方法
public T void print(T item) {System.out.println(item);
}这里的 T 告诉我们print 方法是泛型方法T 可以是任何类型。这意味着 print 方法能处理 String、Integer 等各种类型的数据。
3. 泛型类型推断与钻石操作符
3.1 类型推断
类型推断顾名思义就是 Java 可以自动“猜出”我们需要使用的泛型类型而不需要我们手动明确指定。这样可以让代码变得更简洁、更易读。
类型推断就像是 Java 帮你填空。在一些情况下Java 编译器能够根据上下文自动判断出你正在使用的泛型类型。这样一来很多时候我们不需要手动写出复杂的类型声明Java 自己就能搞定。
举个例子假设我们有一个简单的泛型类 BoxT
class BoxT {private T item;public void set(T item) {this.item item;}public T get() {return this.item;}
}在创建 Box 对象时通常我们需要明确指定 T 的类型
BoxString stringBox new BoxString();
stringBox.set(Hello);这里我们指定了两次 String一次在 BoxString一次在 new BoxString()。看起来有点啰嗦。其实Java 能自动推断出第二次 String 是什么类型。
Java 能自动推断泛型类型所以我们可以简化代码
BoxString stringBox new Box();
stringBox.set(Hello);你会发现new Box() 这里少了 String而代码仍然是正确的。这就是 类型推断它帮你省去了重复声明的麻烦。
3.2 钻石操作符
为了进一步简化泛型的使用Java 7 引入了一个叫做 钻石操作符的符号。这个符号让我们在创建泛型对象时不需要再重复写泛型类型编译器会根据上下文推断出正确的类型。
钻石操作符看起来像一对尖括号放在 new 后面用来表示“这里的类型让我自动推断吧”。它用起来特别简单只需要像这样写
BoxString stringBox new Box();这里BoxString 表示我们声明了一个泛型类其中的类型是 String而 new Box() 使用了钻石操作符表示 Box 类的实例化时类型为 String由前面的 BoxString 决定。
4. 通配符的使用
在 Java 的泛型中通配符用于表示泛型类型中的未知类型帮助我们编写更加通用和灵活的代码。通过使用通配符方法或类可以适应多种类型而不局限于某一具体类型。通配符主要有以下三种形式
无界通配符 (?)表示任意类型的泛型参数。上界通配符 (? extends T)适合读取操作支持协变。下界通配符 (? super T)适合写入操作支持逆变。
4.1 无界通配符 ?
? 通配符用于表示可以接收 任意类型 的参数但不能确定其具体类型。无界通配符通常用于处理泛型类型不重要或者无需关心集合内容类型的情况。它适用于那些只需要读取、遍历等操作而不涉及修改集合内容的场景。
例如使用通配符处理不同类型的集合
public class WildcardDemo {public static void printElements(List? list) {for (Object element : list) {System.out.println(element);}}public static void main(String[] args) {ListString stringList List.of(apple, banana, cherry);ListInteger intList List.of(1, 2, 3);printElements(stringList); // 输出字符串列表printElements(intList); // 输出整数列表}
}在这个例子中printElements() 方法可以接受 ListString、ListInteger 等任意类型的列表作为参数因为它使用了无界通配符 ?。该方法可以遍历并打印列表的元素但无法向列表中添加新元素。
4.2 上界通配符 ? extends T
? extends T 表示 T 类型或 T 的子类。上界通配符适用于那些需要从泛型对象中读取数据的场景因为它确保集合中的元素是某个类型的子类。在这种情况下我们可以安全地读取元素并知道它们至少是某种类型的子类。
这种机制称为 协变Covariant允许使用父类引用子类对象。这在 Java 中非常常见比如我们可以用 ListNumber 来操作 ListInteger 或 ListDouble因为 Integer 和 Double 都是 Number 的子类。
协变的实际应用
public class CovariantDemo {public static void printNumbers(List? extends Number list) {for (Number number : list) {System.out.println(number);}}public static void main(String[] args) {ListInteger intList List.of(1, 2, 3);ListDouble doubleList List.of(1.1, 2.2, 3.3);printNumbers(intList); // 输出整数列表printNumbers(doubleList); // 输出浮点数列表}
}在这个例子中printNumbers() 方法使用了 ? extends Number 上界通配符这表示 list 可以是 Number 类及其任意子类的集合如 ListInteger 或 ListDouble。我们可以读取并打印这些数字但不能向列表中添加新元素。
4.3 下界通配符 ? super T
? super T 表示 T 类型或 T 的父类。下界通配符适用于那些需要向泛型对象中写入数据的场景。下界通配符确保我们可以将类型为 T 的对象安全地添加到泛型集合中因为集合至少能够接受 T 类型或其父类的对象。
这种机制称为 逆变Contravariant允许子类对象安全地添加到父类集合中。例如我们可以将 Integer 对象添加到 ListNumber 或 ListObject 中。
逆变的用法及场景
public class ContravariantDemo {public static void addIntegers(List? super Integer list) {list.add(10);list.add(20);}public static void main(String[] args) {ListNumber numberList new ArrayList();addIntegers(numberList); // 添加整数到 Number 列表中System.out.println(numberList);}
}在这个例子中addIntegers() 方法使用了 ? super Integer 下界通配符这表示 list 可以是 Integer 类的父类集合如 ListNumber 或 ListObject。我们可以安全地向其中添加 Integer 类型的元素。注意我们只能保证能向集合添加 Integer 或其子类但不能保证读取时的具体类型。
5. 泛型的高级特性
在 Java 泛型中除了基本的类型参数化功能还有一些高级特性可以进一步提升代码的灵活性和可扩展性。这些高级特性包括 多重边界、泛型嵌套、以及 泛型方法与构造函数。这些特性允许我们为泛型指定更多的约束条件、处理复杂的数据结构、以及在方法和构造函数中使用泛型使代码更灵活。
5.1 多重边界
多重边界允许我们为泛型参数定义多个限制条件。通过多重边界我们可以让泛型参数同时满足多个接口或类的约束这使得泛型更加灵活和安全。要实现多重边界使用 符号连接多个限制条件。
多重边界的语法是T extends ClassA InterfaceB InterfaceC... 其中T 必须是 ClassA 的子类并且实现 InterfaceB、InterfaceC 等接口。
例如T extends ComparableT Serializable
public class MultiBoundExampleT extends ComparableT Serializable {private T data;public MultiBoundExample(T data) {this.data data;}public void display() {System.out.println(data);}public int compare(T other) {return data.compareTo(other);}
}T extends ComparableT Serializable 表示 T 必须实现 ComparableT 接口并且是 Serializable可序列化类型。这种限制确保我们可以对 T 进行比较例如排序并且可以将它序列化例如保存到文件。
MultiBoundExample 类可以处理任何既可比较又可序列化的类型。
5.2 泛型嵌套
在 Java 泛型中泛型类型可以相互嵌套。例如集合类可以包含其他泛型类型像 MapString, ListInteger 这样的结构在实际开发中非常常见。处理泛型嵌套时我们可以组合不同的泛型类型来表示更复杂的数据结构。
使用场景当你需要一个复杂的数据结构例如 Map 类型其中键是 String 类型值是包含 Integer 的 List。
例如MapString, ListInteger 的使用
public class NestedGenericsExample {public static void main(String[] args) {// 创建一个Map其中键是String值是ListIntegerMapString, ListInteger studentGrades new HashMap();// 添加学生及其成绩studentGrades.put(Alice, Arrays.asList(90, 85, 88));studentGrades.put(Bob, Arrays.asList(78, 82, 80));// 读取数据for (String student : studentGrades.keySet()) {System.out.println(student s grades: studentGrades.get(student));}}
}MapString, ListInteger 表示键为 String例如学生的名字值为包含多个 Integer 的 List例如学生的成绩。
这种结构常用于表示复杂的数据关系能够存储不同类别的信息。
5.3 泛型方法与构造函数
除了类可以使用泛型外方法 和 构造函数 也可以使用泛型参数。这让方法或构造函数能够独立于类本身的泛型参数变得更加灵活。泛型方法的定义通常在返回类型之前加上 T 这样的泛型声明。
使用场景
当你需要在类的某个方法中使用泛型类型但该类型与类的泛型参数无关时。当你希望构造函数可以处理多个类型但不希望为整个类定义泛型时。
例如泛型方法设计
public class GenericMethodExample {// 泛型方法T 可以是任何类型public static T void printArray(T[] array) {for (T element : array) {System.out.println(element);}}public static void main(String[] args) {// 打印整数数组Integer[] intArray {1, 2, 3, 4};printArray(intArray); // 输出 1 2 3 4// 打印字符串数组String[] strArray {apple, banana, cherry};printArray(strArray); // 输出 apple banana cherry}
}public static T void printArray(T[] array) 是一个泛型方法它可以接受任意类型的数组并打印数组的内容。方法中的 T 类型是独立的与类无关。
该方法在调用时会根据传入的参数类型自动推断泛型类型。
例如泛型构造函数
public class GenericConstructorExample {private Object data;// 泛型构造函数public T GenericConstructorExample(T data) {this.data data;System.out.println(Stored: data);}public static void main(String[] args) {// 创建泛型构造函数的实例new GenericConstructorExample(123); // 存储整数new GenericConstructorExample(Hello); // 存储字符串}
}泛型构造函数 T GenericConstructorExample(T data) 可以接受任意类型的数据存储到 data 属性中。每次创建实例时该构造函数可以根据传入的数据类型自动推断类型。
这个特性允许构造函数灵活处理不同的数据类型而不需要为类整体定义泛型。
6. 泛型在 Java 集合中的应用
Java 集合框架与泛型结合使用可以有效提升类型安全和代码简洁性。泛型让开发者能够指定集合中存储的数据类型避免类型不匹配的错误。此外Java 8 引入的 Stream API 与泛型结合实现了对数据更简洁和灵活的处理。
6.1 Java 集合框架中的泛型
Java 集合框架如 List、Set、Map 等广泛使用泛型。使用泛型可以指定集合存储的数据类型例如 ListString 表示一个只存储 String 类型的列表MapInteger, String 则表示一个键为 Integer值为 String 的映射。
6.2 泛型集合的常见类型
List有序的集合存储类型为 T 的元素。
Set无序且不允许重复的集合存储类型为 T 的元素。
MapK, V键值对集合键的类型为 K值的类型为 V。
6.3 泛型集合的使用
public class GenericCollectionExample {public static void main(String[] args) {// 创建一个泛型List集合存储String类型ListString fruitList new ArrayList();fruitList.add(苹果);fruitList.add(香蕉);fruitList.add(樱桃);// 创建一个泛型Map集合键为Integer值为StringMapInteger, String idToName new HashMap();idToName.put(1, 张三);idToName.put(2, 李四);idToName.put(3, 王五);// 输出集合内容System.out.println(水果列表: fruitList);System.out.println(ID到姓名映射: idToName);}
}6.4 泛型在集合中的优势 类型安全泛型确保集合只存储指定类型的元素防止类型错误。例如ListString 不允许添加 Integer 类型的数据。 ListString names new ArrayList();
names.add(张三);
// names.add(123); // 编译时错误防止将Integer插入到ListString简洁性泛型消除了手动类型转换的需要不需要在读取集合元素时进行强制类型转换。 编译时检查泛型在编译时检查类型错误避免运行时抛出异常。
7. 泛型的运行时行为与限制
在 Java 中泛型的使用使代码更加灵活和安全但它也有一些运行时的限制这主要是因为 Java 的 类型擦除 机制。理解这些限制可以帮助我们更好地处理泛型的使用场景并避免常见的错误。
7.1 类型擦除
类型擦除 是 Java 编译器在编译时处理泛型的一种机制。在编译时Java 会检查泛型类型的安全性但是在运行时泛型信息会被“擦除”也就是说程序在运行时不知道泛型的具体类型。例如ListString 和 ListInteger 在运行时都被当作 List 处理。
简单解释编译器会在编译时使用泛型检查类型但在运行时泛型的具体类型就不存在了。这个机制帮助 Java 保持向后兼容但也带来了一些限制。
例如类型擦除的效果
public class TypeErasureExample {public static void main(String[] args) {ListString stringList new ArrayList();ListInteger integerList new ArrayList();System.out.println(stringList.getClass() integerList.getClass()); // 输出: true}
}尽管 stringList 是 ListStringintegerList 是 ListInteger但在运行时它们的类型都是 List。因此getClass() 返回的结果是相同的。
由于类型擦除Java 在运行时无法获得泛型的具体类型信息这带来了一些限制
无法在运行时检查泛型类型你不能在运行时通过 instanceof 检查带泛型的类型。例如不能直接检查 ListString。
if (obj instanceof ListString) { // 编译错误// 不允许这么写
}7.2 泛型数组与实例化
Java 中不能创建泛型数组因为数组在运行时必须知道它的具体类型而泛型类型在运行时已经被擦除无法保留具体的类型信息。数组和泛型的设计方式不同数组在运行时保留其元素的类型而泛型类型在运行时被擦除因此二者不兼容。
ListString[] arrayOfLists new ListString[10]; // 编译错误由于类型擦除ListString[] 在运行时实际上是 List[]这可能导致类型不安全的问题。例如你可以往 List[] 数组中插入一个 ListInteger这与泛型的类型安全性目标相冲突。
由于泛型数组无法直接创建建议使用集合类如 ArrayList代替数组。集合类可以提供灵活的数据结构并且泛型在编译时会进行类型检查避免了数组的类型不匹配问题。
例如使用集合代替数组
public class GenericArraySolution {public static void main(String[] args) {// 使用ListListString代替数组ListListString listOfLists new ArrayList();ListString sublist new ArrayList();sublist.add(苹果);sublist.add(香蕉);listOfLists.add(sublist);System.out.println(listOfLists);}
}通过使用 ListListString可以避免泛型数组的限制并且集合类在编译时仍然提供类型安全性。
7.3 静态上下文中的泛型
泛型在静态上下文中是受限制的。原因是 静态成员 属于类本身而不是某个特定的实例。由于泛型类型在类的实例化过程中才被具体化而静态成员是在类加载时就存在因此泛型无法应用于静态成员。
public class GenericClassT {private static T staticField; // 编译错误静态字段不能使用泛型
}在上面的例子中T 是一个泛型参数但是由于 staticField 是静态的T 在类加载时还没有具体类型所以编译器无法确定 T 的类型导致编译错误。
尽管不能在静态字段或方法中直接使用类的泛型参数但可以通过在 静态方法 中定义自己的泛型参数来解决问题。
例如静态方法中的泛型参数
public class GenericMethodExample {public static T void printArray(T[] array) {for (T element : array) {System.out.println(element);}}public static void main(String[] args) {String[] stringArray {苹果, 香蕉, 樱桃};Integer[] intArray {1, 2, 3};// 调用泛型静态方法printArray(stringArray);printArray(intArray);}
}在 printArray 静态方法中T 定义了一个方法级别的泛型参数因此你可以使用它来处理任何类型的数组而不依赖于类的泛型参数。
8. 常见泛型问题与解决方案
在使用 Java 泛型时开发者常会遇到一些限制和问题。这些问题通常与泛型的类型擦除、基本类型的支持和异常处理等机制有关。下面我们将介绍常见的泛型问题并提供相应的解决方案。
8.1 泛型类型检查
在 Java 中不能直接使用 instanceof 来检查泛型的类型。因为 Java 泛型在运行时经过了类型擦除具体的泛型类型信息在运行时已经不存在。
public class GenericTypeCheckT {public boolean isString(Object obj) {// if (obj instanceof T) { // 编译错误无法使用泛型类型进行类型检查// return true;// }return false;}
}在编译时T 可能是 String、Integer 等任何类型但在运行时这个类型信息会被擦除导致无法使用 instanceof 检查泛型类型。
解决这个问题的一个常见方法是通过传递 ClassT 类型的参数让泛型方法在运行时能够获取到泛型的实际类型。
public class GenericTypeCheckT {private ClassT type;public GenericTypeCheck(ClassT type) {this.type type;}public boolean isInstance(Object obj) {return type.isInstance(obj);}public static void main(String[] args) {GenericTypeCheckString checker new GenericTypeCheck(String.class);System.out.println(checker.isInstance(Hello)); // 输出: trueSystem.out.println(checker.isInstance(123)); // 输出: false}
}8.2 泛型不支持基本类型
Java 泛型不支持基本类型int、char、boolean 等只能使用对象类型例如 Integer、Character。这是因为泛型类型的擦除机制要求泛型类的实例参数必须是 Object 类型而基本类型不是 Object。
// Listint numbers new ArrayList(); // 编译错误
ListInteger numbers new ArrayList(); // 正确int 是基本类型不能直接用作泛型参数。必须使用它的包装类 Integer因为 Integer 是对象类型可以与泛型兼容。
为了在泛型中处理基本类型Java 提供了基本类型的包装类例如
int 对应 Integerchar 对应 Characterboolean 对应 Boolean
public class GenericPrimitiveExample {public static void main(String[] args) {// 使用Integer包装类代替intListInteger numbers new ArrayList();numbers.add(1); // 自动装箱将int转换为Integernumbers.add(2);numbers.add(3);for (Integer number : numbers) {System.out.println(number); // 自动拆箱将Integer转换为int}}
}Java 会自动进行 装箱将 int 转换为 Integer和 拆箱将 Integer 转换为 int这使得基本类型可以轻松与泛型一起使用。
8.3 泛型方法不能直接抛出或捕获泛型异常
Java 不允许使用泛型类型作为异常类。这是因为异常在运行时需要保留其具体类型而泛型的类型信息在运行时被擦除无法获得泛型的具体类型。
public class GenericExceptionT extends Exception {public void throwException(T ex) throws T { // 编译错误不能抛出泛型异常throw ex;}
}泛型类型 T 在运行时会被擦除因此不能用于抛出或捕获具体的异常类型。
虽然泛型方法不能直接抛出泛型异常但我们可以通过参数传递或捕获具体的异常类型来处理。例如
public class GenericExceptionHandler {public T extends Exception void handleException(T exception) {try {throw exception; // 抛出异常} catch (Exception e) { // 捕获所有的异常类型System.out.println(捕获到异常: e.getMessage());}}public static void main(String[] args) {GenericExceptionHandler handler new GenericExceptionHandler();handler.handleException(new IllegalArgumentException(非法参数异常));handler.handleException(new NullPointerException(空指针异常));}
}这里我们通过泛型方法 handleException 来处理不同类型的异常。虽然无法直接抛出泛型异常但可以通过 catch 块捕获 Exception从而间接处理不同的异常类型。
9. 泛型的设计与最佳实践
在 Java 中泛型的设计非常灵活可以帮助我们编写类型安全、可复用的代码。但过度复杂的泛型设计可能会让代码变得难以维护。因此在设计泛型类和方法时有一些最佳实践和原则可以帮助我们写出更优雅的代码。
9.1 灵活的泛型 API 设计
为了使代码更加灵活和易于维护设计泛型类和方法时需要注重简单性和清晰性。过于复杂的泛型层次结构可能会使代码难以理解甚至带来维护上的困难。
简单规则
明确类型边界在泛型定义中使用边界限制如 extends 或 super确保类型的合理使用。单一职责一个泛型类或方法应只解决一个问题避免让它承担过多功能。代码可读性保持泛型代码的可读性比过度抽象更重要。
public class BoxT {private T value;public Box(T value) {this.value value;}public T getValue() {return value;}public void setValue(T value) {this.value value;}// 泛型方法: 可以处理任何类型的Boxpublic static U void printBox(BoxU box) {System.out.println(Box contains: box.getValue());}public static void main(String[] args) {BoxString stringBox new Box(苹果);BoxInteger intBox new Box(123);printBox(stringBox);printBox(intBox);}
}这里的 Box 类和 printBox 方法都使用了简单明了的泛型设计确保代码清晰且可复用。
当泛型设计过于复杂时代码的可读性和维护性会大幅下降。特别是在处理多层泛型嵌套或过多边界限制时可能让其他开发者甚至是自己感到困惑。因此在设计泛型时保持简单 是关键。
反例过度复杂的泛型设计
public class ComplicatedClassK extends Comparable? super K, V extends List? extends K {private K key;private V value;public ComplicatedClass(K key, V value) {this.key key;this.value value;}public K getKey() {return key;}public V getValue() {return value;}
}上面的类设计虽然是合法的但泛型的复杂性会让代码很难理解并且难以实际应用。尽量避免这种过度复杂的设计。
9.2 PECS 原则
PECS 原则是泛型设计中的一条重要规则全称是“Producer Extends, Consumer Super”。它帮助我们在使用泛型通配符时明确如何设置类型边界。
Producer Extends生产者用 extends如果一个泛型类或方法是生产数据的即向外提供数据我们应该使用上界通配符 ? extends T。
Consumer Super消费者用 super如果一个泛型类或方法是消费数据的即接收数据我们应该使用下界通配符 ? super T。
public class PecsExample {// 使用 extends作为生产者提供数据public static void addNumbers(List? extends Number numbers) {for (Number num : numbers) {System.out.println(数字: num);}}// 使用 super作为消费者接收数据public static void addIntegers(List? super Integer integers) {integers.add(10);integers.add(20);}public static void main(String[] args) {ListInteger intList new ArrayList();addIntegers(intList); // 可以添加 IntegerListNumber numberList new ArrayList();addNumbers(numberList); // 可以读取 Number 及其子类的数据}
}addNumbers 方法使用 ? extends Number表示该方法只读取 Number 或其子类的数据。
addIntegers 方法使用 ? super Integer表示该方法可以接收 Integer 或其父类的数据并向列表中添加数据。