Java 每日一刊(第19期):泛型

文章目录
- 前言
- 1. 泛型概述
- 1.1 不使用泛型 vs 使用泛型
- 1.2 泛型的作用
- 2. 泛型的基本语法
- 2.1 定义带类型参数的泛型类
- 2.2 使用泛型类
- 2.3 泛型方法
- 3. 泛型类型推断与钻石操作符
- 3.1 类型推断
- 3.2 钻石操作符
- 4. 通配符的使用
- 4.1 无界通配符 `<?>`
- 4.2 上界通配符 `<? extends T>`
- 4.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;}
}
你会发现,我们要写两个几乎完全一样的类,只是因为它们处理的数据类型不同。这是非常繁琐的。
使用泛型的情况:使用泛型后,我们只需要写一个 Box<T> 类,不管是 String 还是 Integer,都可以通过同一个类来处理。
class Box<T> {private T item;public void set(T item) {this.item = item;}public T get() {return this.item;}
}
这样,代码更简洁、通用,也更容易维护。
1.2 泛型的作用
泛型的主要好处有两个:
-
提高代码的安全性:泛型让我们能够提前检查代码中的类型错误。在编译时(也就是程序运行前),编译器会检查我们传入的类型是否正确。如果类型不对,代码甚至不会通过编译。这可以避免很多不必要的错误。比如,如果你想要把一个
Box<String>放到一个装数字的盒子里,编译器会立刻提醒你错误:Box<Integer> intBox = new Box<>(); intBox.set("错误的类型"); // 编译器会报错,因为它需要的是整数而不是字符串 -
提升代码的复用性:泛型让我们可以写出更加通用的代码,只用写一次就能适应多种类型。比如,
Box<T>可以用来存放不同的数据类型,不管是String还是Integer,都可以复用这段代码。这避免了为每种类型都单独写一份代码。
2. 泛型的基本语法
2.1 定义带类型参数的泛型类
假设我们想创建一个可以存放任何类型数据的盒子,这个盒子应该能存放字符串、数字,甚至其他类型的对象。我们可以通过泛型类来实现。
class Box<T> {private T item;public void set(T item) {this.item = item;}public T get() {return this.item;}
}
Box<T>:这里的 T 是泛型中的“占位符”,可以代表任何类型。当我们创建 Box 对象时,再告诉它 T 具体是什么类型,比如 String 或 Integer。
T item:item 是一个 T 类型的变量,而 T 是我们用泛型指定的类型。
2.2 使用泛型类
在使用 Box 这个类时,我们需要告诉它 T 具体是什么类型:
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
System.out.println(stringBox.get()); // 输出 "Hello World"Box<Integer> intBox = new Box<>();
intBox.set(123);
System.out.println(intBox.get()); // 输出 123
在上面的例子里:
Box<String>表示这是一个装String类型数据的盒子。Box<Integer>表示这是一个装Integer类型数据的盒子。
class Box<T> {private T item; // 这里的 T 是类型的占位符public void set(T item) {this.item = item;}public T get() {return this.item;}
}
在上面的例子里,T 就是占位符,它可以代表任何数据类型。等到我们真正使用这个盒子时,再告诉它 T 具体是什么类型:
Box<String> stringBox = new Box<>();
stringBox.set("一本书");Box<Integer> 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 自己就能搞定。
举个例子,假设我们有一个简单的泛型类 Box<T>:
class Box<T> {private T item;public void set(T item) {this.item = item;}public T get() {return this.item;}
}
在创建 Box 对象时,通常我们需要明确指定 T 的类型:
Box<String> stringBox = new Box<String>();
stringBox.set("Hello");
这里我们指定了两次 String,一次在 Box<String>,一次在 new Box<String>()。看起来有点啰嗦。其实,Java 能自动推断出第二次 String 是什么类型。
Java 能自动推断泛型类型,所以我们可以简化代码:
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
你会发现,new Box<>() 这里少了 String,而代码仍然是正确的。这就是 类型推断,它帮你省去了重复声明的麻烦。
3.2 钻石操作符
为了进一步简化泛型的使用,Java 7 引入了一个叫做 钻石操作符(<>)的符号。这个符号让我们在创建泛型对象时,不需要再重复写泛型类型,编译器会根据上下文推断出正确的类型。
钻石操作符(<>)看起来像一对尖括号,放在 new 后面,用来表示“这里的类型让我自动推断吧!”。它用起来特别简单,只需要像这样写:
Box<String> stringBox = new Box<>();
这里,Box<String> 表示我们声明了一个泛型类,其中的类型是 String,而 new Box<>() 使用了钻石操作符,表示 Box 类的实例化时,类型为 String(由前面的 Box<String> 决定)。
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) {List<String> stringList = List.of("apple", "banana", "cherry");List<Integer> intList = List.of(1, 2, 3);printElements(stringList); // 输出字符串列表printElements(intList); // 输出整数列表}
}
在这个例子中,printElements() 方法可以接受 List<String>、List<Integer> 等任意类型的列表作为参数,因为它使用了无界通配符 <?>。该方法可以遍历并打印列表的元素,但无法向列表中添加新元素。
4.2 上界通配符 <? extends T>
<? extends T> 表示 T 类型或 T 的子类。上界通配符适用于那些需要从泛型对象中读取数据的场景,因为它确保集合中的元素是某个类型的子类。在这种情况下,我们可以安全地读取元素并知道它们至少是某种类型的子类。
这种机制称为 协变(Covariant),允许使用父类引用子类对象。这在 Java 中非常常见,比如我们可以用 List<Number> 来操作 List<Integer> 或 List<Double>,因为 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) {List<Integer> intList = List.of(1, 2, 3);List<Double> doubleList = List.of(1.1, 2.2, 3.3);printNumbers(intList); // 输出整数列表printNumbers(doubleList); // 输出浮点数列表}
}
在这个例子中,printNumbers() 方法使用了 <? extends Number> 上界通配符,这表示 list 可以是 Number 类及其任意子类的集合(如 List<Integer> 或 List<Double>)。我们可以读取并打印这些数字,但不能向列表中添加新元素。
4.3 下界通配符 <? super T>
<? super T> 表示 T 类型或 T 的父类。下界通配符适用于那些需要向泛型对象中写入数据的场景。下界通配符确保我们可以将类型为 T 的对象安全地添加到泛型集合中,因为集合至少能够接受 T 类型或其父类的对象。
这种机制称为 逆变(Contravariant),允许子类对象安全地添加到父类集合中。例如,我们可以将 Integer 对象添加到 List<Number> 或 List<Object> 中。
逆变的用法及场景:
public class ContravariantDemo {public static void addIntegers(List<? super Integer> list) {list.add(10);list.add(20);}public static void main(String[] args) {List<Number> numberList = new ArrayList<>();addIntegers(numberList); // 添加整数到 Number 列表中System.out.println(numberList);}
}
在这个例子中,addIntegers() 方法使用了 <? super Integer> 下界通配符,这表示 list 可以是 Integer 类的父类集合(如 List<Number> 或 List<Object>)。我们可以安全地向其中添加 Integer 类型的元素。注意我们只能保证能向集合添加 Integer 或其子类,但不能保证读取时的具体类型。
5. 泛型的高级特性
在 Java 泛型中,除了基本的类型参数化功能,还有一些高级特性,可以进一步提升代码的灵活性和可扩展性。这些高级特性包括 多重边界、泛型嵌套、以及 泛型方法与构造函数。这些特性允许我们为泛型指定更多的约束条件、处理复杂的数据结构、以及在方法和构造函数中使用泛型,使代码更灵活。
5.1 多重边界
多重边界允许我们为泛型参数定义多个限制条件。通过多重边界,我们可以让泛型参数同时满足多个接口或类的约束,这使得泛型更加灵活和安全。要实现多重边界,使用 & 符号连接多个限制条件。
多重边界的语法是:T extends ClassA & InterfaceB & InterfaceC...
其中,T 必须是 ClassA 的子类,并且实现 InterfaceB、InterfaceC 等接口。
例如:T extends Comparable<T> & Serializable
public class MultiBoundExample<T extends Comparable<T> & 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 Comparable<T> & Serializable 表示 T 必须实现 Comparable<T> 接口并且是 Serializable(可序列化)类型。这种限制确保我们可以对 T 进行比较(例如排序),并且可以将它序列化(例如保存到文件)。
MultiBoundExample 类可以处理任何既可比较又可序列化的类型。
5.2 泛型嵌套
在 Java 泛型中,泛型类型可以相互嵌套。例如,集合类可以包含其他泛型类型,像 Map<String, List<Integer>> 这样的结构在实际开发中非常常见。处理泛型嵌套时,我们可以组合不同的泛型类型来表示更复杂的数据结构。
使用场景:当你需要一个复杂的数据结构,例如 Map 类型,其中键是 String 类型,值是包含 Integer 的 List。
例如:Map<String, List<Integer>> 的使用
public class NestedGenericsExample {public static void main(String[] args) {// 创建一个Map,其中键是String,值是List<Integer>Map<String, List<Integer>> 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));}}
}
Map<String, List<Integer>> 表示键为 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 等)广泛使用泛型。使用泛型可以指定集合存储的数据类型,例如 List<String> 表示一个只存储 String 类型的列表,Map<Integer, String> 则表示一个键为 Integer,值为 String 的映射。
6.2 泛型集合的常见类型
List:有序的集合,存储类型为 T 的元素。
Set:无序且不允许重复的集合,存储类型为 T 的元素。
Map<K, V>:键值对集合,键的类型为 K,值的类型为 V。
6.3 泛型集合的使用
public class GenericCollectionExample {public static void main(String[] args) {// 创建一个泛型List集合,存储String类型List<String> fruitList = new ArrayList<>();fruitList.add("苹果");fruitList.add("香蕉");fruitList.add("樱桃");// 创建一个泛型Map集合,键为Integer,值为StringMap<Integer, String> idToName = new HashMap<>();idToName.put(1, "张三");idToName.put(2, "李四");idToName.put(3, "王五");// 输出集合内容System.out.println("水果列表: " + fruitList);System.out.println("ID到姓名映射: " + idToName);}
}
6.4 泛型在集合中的优势
-
类型安全:泛型确保集合只存储指定类型的元素,防止类型错误。例如,
List<String>不允许添加Integer类型的数据。List<String> names = new ArrayList<>(); names.add("张三"); // names.add(123); // 编译时错误,防止将Integer插入到List<String> -
简洁性:泛型消除了手动类型转换的需要,不需要在读取集合元素时进行强制类型转换。
-
编译时检查:泛型在编译时检查类型错误,避免运行时抛出异常。
7. 泛型的运行时行为与限制
在 Java 中,泛型的使用使代码更加灵活和安全,但它也有一些运行时的限制,这主要是因为 Java 的 类型擦除 机制。理解这些限制可以帮助我们更好地处理泛型的使用场景,并避免常见的错误。
7.1 类型擦除
类型擦除 是 Java 编译器在编译时处理泛型的一种机制。在编译时,Java 会检查泛型类型的安全性,但是在运行时,泛型信息会被“擦除”,也就是说,程序在运行时不知道泛型的具体类型。例如,List<String> 和 List<Integer> 在运行时都被当作 List 处理。
简单解释:编译器会在编译时使用泛型检查类型,但在运行时,泛型的具体类型就不存在了。这个机制帮助 Java 保持向后兼容,但也带来了一些限制。
例如:类型擦除的效果
public class TypeErasureExample {public static void main(String[] args) {List<String> stringList = new ArrayList<>();List<Integer> integerList = new ArrayList<>();System.out.println(stringList.getClass() == integerList.getClass()); // 输出: true}
}
尽管 stringList 是 List<String>,integerList 是 List<Integer>,但在运行时,它们的类型都是 List。因此,getClass() 返回的结果是相同的。
由于类型擦除,Java 在运行时无法获得泛型的具体类型信息,这带来了一些限制:
无法在运行时检查泛型类型:你不能在运行时通过 instanceof 检查带泛型的类型。例如,不能直接检查 List<String>。
if (obj instanceof List<String>) { // 编译错误// 不允许这么写
}
7.2 泛型数组与实例化
Java 中不能创建泛型数组,因为数组在运行时必须知道它的具体类型,而泛型类型在运行时已经被擦除,无法保留具体的类型信息。数组和泛型的设计方式不同,数组在运行时保留其元素的类型,而泛型类型在运行时被擦除,因此二者不兼容。
List<String>[] arrayOfLists = new List<String>[10]; // 编译错误
由于类型擦除,List<String>[] 在运行时实际上是 List[],这可能导致类型不安全的问题。例如,你可以往 List[] 数组中插入一个 List<Integer>,这与泛型的类型安全性目标相冲突。
由于泛型数组无法直接创建,建议使用集合类(如 ArrayList)代替数组。集合类可以提供灵活的数据结构,并且泛型在编译时会进行类型检查,避免了数组的类型不匹配问题。
例如:使用集合代替数组
public class GenericArraySolution {public static void main(String[] args) {// 使用List<List<String>>代替数组List<List<String>> listOfLists = new ArrayList<>();List<String> sublist = new ArrayList<>();sublist.add("苹果");sublist.add("香蕉");listOfLists.add(sublist);System.out.println(listOfLists);}
}
通过使用 List<List<String>>,可以避免泛型数组的限制,并且集合类在编译时仍然提供类型安全性。
7.3 静态上下文中的泛型
泛型在静态上下文中是受限制的。原因是 静态成员 属于类本身,而不是某个特定的实例。由于泛型类型在类的实例化过程中才被具体化,而静态成员是在类加载时就存在,因此泛型无法应用于静态成员。
public class GenericClass<T> {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 GenericTypeCheck<T> {public boolean isString(Object obj) {// if (obj instanceof T) { // 编译错误,无法使用泛型类型进行类型检查// return true;// }return false;}
}
在编译时,T 可能是 String、Integer 等任何类型,但在运行时,这个类型信息会被擦除,导致无法使用 instanceof 检查泛型类型。
解决这个问题的一个常见方法是通过传递 Class<T> 类型的参数,让泛型方法在运行时能够获取到泛型的实际类型。
public class GenericTypeCheck<T> {private Class<T> type;public GenericTypeCheck(Class<T> type) {this.type = type;}public boolean isInstance(Object obj) {return type.isInstance(obj);}public static void main(String[] args) {GenericTypeCheck<String> 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。
// List<int> numbers = new ArrayList<>(); // 编译错误
List<Integer> numbers = new ArrayList<>(); // 正确
int 是基本类型,不能直接用作泛型参数。必须使用它的包装类 Integer,因为 Integer 是对象类型,可以与泛型兼容。
为了在泛型中处理基本类型,Java 提供了基本类型的包装类,例如:
int对应Integerchar对应Characterboolean对应Boolean
public class GenericPrimitiveExample {public static void main(String[] args) {// 使用Integer包装类代替intList<Integer> 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 GenericException<T 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 Box<T> {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(Box<U> box) {System.out.println("Box contains: " + box.getValue());}public static void main(String[] args) {Box<String> stringBox = new Box<>("苹果");Box<Integer> intBox = new Box<>(123);printBox(stringBox);printBox(intBox);}
}
这里的 Box 类和 printBox 方法都使用了简单明了的泛型设计,确保代码清晰且可复用。
当泛型设计过于复杂时,代码的可读性和维护性会大幅下降。特别是在处理多层泛型嵌套或过多边界限制时,可能让其他开发者(甚至是自己)感到困惑。因此,在设计泛型时,保持简单 是关键。
反例:过度复杂的泛型设计
public class ComplicatedClass<K 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) {List<Integer> intList = new ArrayList<>();addIntegers(intList); // 可以添加 IntegerList<Number> numberList = new ArrayList<>();addNumbers(numberList); // 可以读取 Number 及其子类的数据}
}
addNumbers 方法使用 <? extends Number>,表示该方法只读取 Number 或其子类的数据。
addIntegers 方法使用 <? super Integer>,表示该方法可以接收 Integer 或其父类的数据,并向列表中添加数据。

相关文章:
Java 每日一刊(第19期):泛型
文章目录 前言1. 泛型概述1.1 不使用泛型 vs 使用泛型1.2 泛型的作用 2. 泛型的基本语法2.1 定义带类型参数的泛型类2.2 使用泛型类2.3 泛型方法 3. 泛型类型推断与钻石操作符3.1 类型推断3.2 钻石操作符 4. 通配符的使用4.1 无界通配符 <?>4.2 上界通配符 <? exten…...
windows下安装rabbitMQ并开通管理界面和允许远程访问
如题,在windows下安装一个rabbitMQ server;然后用浏览器访问其管理界面;由于rabbitMQ的默认账号guest默认只能本机访问,因此需要设置允许其他机器远程访问。这跟mysql的思路很像,默认只能本地访问,要远程访…...
深度剖析音频剪辑免费工具的特色与优势
是热爱生活的伙伴或者想要记录美好声音的普通用户,都可能会需要对音频进行剪辑处理。而幸运的是,现在有许多优秀的音频剪辑软件提供了免费版本,让我们能够轻松地施展音频剪辑的魔法。接下来,就让我们一同深入了解这些音频剪辑免费…...
Oracle中TRUNC()函数详解
文章目录 前言一、TRUNC函数的语法二、主要用途三、测试用例总结 前言 在Oracle中,TRUNC函数用于截取或截断日期、时间或数值表达式的部分。它返回一个日期、时间或数值的截断版本,根据提供的格式进行截取。 一、TRUNC函数的语法 TRUNC(date) TRUNC(d…...
【Spring Boot 入门一】构建你的第一个Spring Boot应用
一、引言 在当今的软件开发领域,Java一直占据着重要的地位。而Spring Boot作为Spring框架的延伸,为Java开发者提供了一种更加便捷、高效的开发方式。它简化了Spring应用的搭建和配置过程,让开发者能够专注于业务逻辑的实现。无论是构建小型的…...
PPT 快捷键使用、技巧
前言: 本文操作是以office 2021为基础的,仅供参考;不同版本office 的 ppt 快捷键 以及对应功能会有差异,需要实践出真知。 shift 移动 水平/垂直 移动 ; shift 放大/缩小 等比例放大 缩小 ; 正圆 正…...
Web安全 - 文件上传漏洞(File Upload Vulnerability)
文章目录 OWASP 2023 TOP 10导图定义攻击场景1. 上传恶意脚本2. 目录遍历3. 覆盖现有文件4. 文件上传结合社会工程攻击 防御措施1. 文件类型验证2. 文件名限制3. 文件存储位置4. 文件权限设置5. 文件内容检测6. 访问控制7. 服务器配置 文件类型验证实现Hutool的FileTypeUtil使用…...
vue3中el-input在form表单按下回车刷新页面
摘要: 在input框中点击回车之后不是调用我写的回车事件,而是刷新页面! 如果表单中只有一个input 框则按下回车会直接关闭表单 所以导致刷新页面 再写一个input 表单 ,并设置style“display:none” <ElInput style"display…...
SQL Server中关于个性化需求批量删除表的做法
在实际开发中,我们常常会遇到需要批量删除表,且具有共同特征的情况,例如:找出表名中数字结尾的表之类的,本文我将以3中类似情况为例,来示范并解说此类需求如何完成: 第一种,批量删除…...
关于按键状态机解决Delay给程序带来的问题
问题产生 我在学习中断的过程中,使用EXTI15外部中断,在其中加入HAL_Delay();就会发生报错 错误地方 其它地方配置 问题原因 在中断服务例程(ISR)中使用 HAL_Delay() 会导致问题的原因是: 阻塞性: HAL_D…...
62.【C语言】浮点数的存储
目录 1.浮点数的类型 2.浮点数表示的范围 3.浮点数的特性 《计算机科学导论》的叙述 4.浮点数在内存中的存储 答案速查 分析 前置知识:浮点数的存储规则 推导单精度浮点数5.5在内存中的存储 验证 浮点数取出的分析 1.一般情况:E不全为0或不全为1 2.特殊情况:E全为0…...
GO网络编程(一):基础知识
1. 网络编程的基础概念 TCP/IP 协议栈 TCP/IP 是互联网通信的核心协议栈,分为以下四个层次: 应用层(Application Layer):为应用程序提供网络服务的协议,比如 HTTP、FTP、SMTP 等。传输层(Tra…...
【Linux】用虚拟机配置Ubuntu环境
目录 1.虚拟机安装Ubuntu系统 2.Ubuntu系统的网络配置 3.特别声明 首先我们先要下载VMware软件,大家自己去下啊! 1.虚拟机安装Ubuntu系统 我们进去之后点击创建新的虚拟机,然后选择自定义 接着点下一步 再点下一步 进入这个界面之后&…...
酒店智能门锁SDK接口pro[V10] 门锁校验C#-SAAS本地化-未来之窗行业应用跨平台架构
一、代码 int 酒店标识_int Convert.ToInt32(酒店标识);StringBuilder 锁号2024 new StringBuilder(8);//信息 "未知返回值:" bufCard_原始;GetGuestLockNoByCardDataStr_原始(酒店标识_int, bufCard_原始.ToString(), 锁号2024);StringBuilder 退…...
Gitのrebase用法
在 Git 中,rebase 是一种用于整合多个提交历史的操作,它可以将一个分支的变更“重放”到另一个分支上。与 merge 不同,rebase 会产生一个线性的提交历史,使得项目的历史记录更加整洁和易于理解。 1. 什么是 Rebase? …...
二分查找一>:在排序数组中查找元素的第一个和最后一个位置
1.题目: 2.解析:这里不能用传统二分,因为涉及范围,传统二分时间复杂度会降为O(N),要做些改动。 步骤一:查找区间左端点 细节图: 步骤二:查找区间右端点: 细节图: 代码…...
undeclared identifier ‘UNITY_PREV_MATRIX_M‘ - Unity Shader自己写URP,引用内部 hlsl
碰到这样的问题,居然非常淡定 这个链接里说了问题: 一个哥们A问,为什么include urp common.hlsl 提示莫名其妙 另一个哥们B说,这个issue 说了,可能是这个原因(也没正面答) 从issue我们知道&a…...
信息安全工程师(29)存储介质安全分析与防护
前言 存储介质安全分析与防护是确保数据安全与完整性的重要环节。存储介质,如硬盘、U盘、SD卡等,作为数据的载体,其安全性直接关系到数据的安全。 一、存储介质安全分析 1. 数据泄露风险 格式化不彻底:传统的格式化操作往往只能删…...
Html5知识点介绍
HTML5 是 HTML 的最新版本,它引入了许多新特性和元素来增强 Web 开发的能力和灵活性。以下是一些关键的 HTML5 知识点: 1. 语义化标签 HTML5 增加了许多新的语义化标签,用来更好地定义页面结构和内容,这些标签使代码更加清晰易读&…...
探索机器学习中的特征选择技术
在机器学习和数据科学领域,特征选择是一个关键步骤,它不仅有助于提高模型的性能,还能帮助我们更好地理解数据。本文将深入探讨特征选择的重要性、常见方法以及如何在实际项目中应用这些技术。 一、特征选择的重要性 降低维度:减…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
Python 高效图像帧提取与视频编码:实战指南
Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...
GraphQL 实战篇:Apollo Client 配置与缓存
GraphQL 实战篇:Apollo Client 配置与缓存 上一篇:GraphQL 入门篇:基础查询语法 依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在: https://github.com/GoldenaArcher/graphql…...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...
