当前位置: 首页 > news >正文

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 相关内容的专刊,每日一更。

本期将为大家带来以下内容:

  1. 泛型概述
  2. 泛型的基本语法
  3. 泛型类型推断与钻石操作符
  4. 通配符的使用
  5. 泛型的高级特性
  6. 泛型在 Java 集合中的应用
  7. 泛型的运行时行为与限制
  8. 常见泛型问题与解决方案
  9. 泛型的设计与最佳实践

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 泛型的作用

泛型的主要好处有两个:

  1. 提高代码的安全性:泛型让我们能够提前检查代码中的类型错误。在编译时(也就是程序运行前),编译器会检查我们传入的类型是否正确。如果类型不对,代码甚至不会通过编译。这可以避免很多不必要的错误。比如,如果你想要把一个 Box<String> 放到一个装数字的盒子里,编译器会立刻提醒你错误:

    Box<Integer> intBox = new Box<>();
    intBox.set("错误的类型");  // 编译器会报错,因为它需要的是整数而不是字符串
    
  2. 提升代码的复用性:泛型让我们可以写出更加通用的代码,只用写一次就能适应多种类型。比如,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 具体是什么类型,比如 StringInteger

T itemitem 是一个 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);

在第一个例子里,TString,所以盒子存的东西是一本书。

在第二个例子里,TInteger,所以盒子存的是数字 123

这样,我们只写了一次盒子的代码,却可以存放不同类型的数据。

2.3 泛型方法

不仅是类,方法也可以使用泛型。比如,写一个打印任何类型的东西的方法:

public <T> void print(T item) {System.out.println(item);
}

这里的 <T> 告诉我们,print 方法是泛型方法,T 可以是任何类型。这意味着 print 方法能处理 StringInteger 等各种类型的数据。

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 的泛型中,通配符用于表示泛型类型中的未知类型,帮助我们编写更加通用和灵活的代码。通过使用通配符,方法或类可以适应多种类型,而不局限于某一具体类型。通配符主要有以下三种形式:

  1. 无界通配符 (<?>):表示任意类型的泛型参数。
  2. 上界通配符 (<? extends T>):适合读取操作,支持协变。
  3. 下界通配符 (<? 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>,因为 IntegerDouble 都是 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 的子类,并且实现 InterfaceBInterfaceC 等接口。

例如: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 类型,值是包含 IntegerList

例如: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(例如学生的名字),值为包含多个 IntegerList(例如学生的成绩)。

这种结构常用于表示复杂的数据关系,能够存储不同类别的信息。

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 集合框架(如 ListSetMap 等)广泛使用泛型。使用泛型可以指定集合存储的数据类型,例如 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 泛型在集合中的优势

  1. 类型安全:泛型确保集合只存储指定类型的元素,防止类型错误。例如,List<String> 不允许添加 Integer 类型的数据。

    List<String> names = new ArrayList<>();
    names.add("张三");
    // names.add(123); // 编译时错误,防止将Integer插入到List<String>
    
  2. 简洁性:泛型消除了手动类型转换的需要,不需要在读取集合元素时进行强制类型转换。

  3. 编译时检查:泛型在编译时检查类型错误,避免运行时抛出异常。

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}
}

尽管 stringListList<String>integerListList<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 可能是 StringInteger 等任何类型,但在运行时,这个类型信息会被擦除,导致无法使用 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 泛型不支持基本类型(intcharboolean 等),只能使用对象类型(例如 IntegerCharacter)。这是因为泛型类型的擦除机制要求泛型类的实例参数必须是 Object 类型,而基本类型不是 Object

// List<int> numbers = new ArrayList<>(); // 编译错误
List<Integer> numbers = new ArrayList<>();  // 正确

int 是基本类型,不能直接用作泛型参数。必须使用它的包装类 Integer,因为 Integer 是对象类型,可以与泛型兼容。

为了在泛型中处理基本类型,Java 提供了基本类型的包装类,例如:

  • int 对应 Integer
  • char 对应 Character
  • boolean 对应 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 设计

为了使代码更加灵活和易于维护,设计泛型类和方法时需要注重简单性和清晰性。过于复杂的泛型层次结构可能会使代码难以理解,甚至带来维护上的困难。

简单规则:

  • 明确类型边界:在泛型定义中使用边界限制(如 extendssuper),确保类型的合理使用。
  • 单一职责:一个泛型类或方法应只解决一个问题,避免让它承担过多功能。
  • 代码可读性:保持泛型代码的可读性比过度抽象更重要。
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并开通管理界面和允许远程访问

如题&#xff0c;在windows下安装一个rabbitMQ server&#xff1b;然后用浏览器访问其管理界面&#xff1b;由于rabbitMQ的默认账号guest默认只能本机访问&#xff0c;因此需要设置允许其他机器远程访问。这跟mysql的思路很像&#xff0c;默认只能本地访问&#xff0c;要远程访…...

深度剖析音频剪辑免费工具的特色与优势

是热爱生活的伙伴或者想要记录美好声音的普通用户&#xff0c;都可能会需要对音频进行剪辑处理。而幸运的是&#xff0c;现在有许多优秀的音频剪辑软件提供了免费版本&#xff0c;让我们能够轻松地施展音频剪辑的魔法。接下来&#xff0c;就让我们一同深入了解这些音频剪辑免费…...

Oracle中TRUNC()函数详解

文章目录 前言一、TRUNC函数的语法二、主要用途三、测试用例总结 前言 在Oracle中&#xff0c;TRUNC函数用于截取或截断日期、时间或数值表达式的部分。它返回一个日期、时间或数值的截断版本&#xff0c;根据提供的格式进行截取。 一、TRUNC函数的语法 TRUNC(date) TRUNC(d…...

【Spring Boot 入门一】构建你的第一个Spring Boot应用

一、引言 在当今的软件开发领域&#xff0c;Java一直占据着重要的地位。而Spring Boot作为Spring框架的延伸&#xff0c;为Java开发者提供了一种更加便捷、高效的开发方式。它简化了Spring应用的搭建和配置过程&#xff0c;让开发者能够专注于业务逻辑的实现。无论是构建小型的…...

PPT 快捷键使用、技巧

前言&#xff1a; 本文操作是以office 2021为基础的&#xff0c;仅供参考&#xff1b;不同版本office 的 ppt 快捷键 以及对应功能会有差异&#xff0c;需要实践出真知。 shift 移动 水平/垂直 移动 &#xff1b; shift 放大/缩小 等比例放大 缩小 &#xff1b; 正圆 正…...

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表单按下回车刷新页面

摘要&#xff1a; 在input框中点击回车之后不是调用我写的回车事件&#xff0c;而是刷新页面&#xff01; 如果表单中只有一个input 框则按下回车会直接关闭表单 所以导致刷新页面 再写一个input 表单 &#xff0c;并设置style“display:none” <ElInput style"display…...

SQL Server中关于个性化需求批量删除表的做法

在实际开发中&#xff0c;我们常常会遇到需要批量删除表&#xff0c;且具有共同特征的情况&#xff0c;例如&#xff1a;找出表名中数字结尾的表之类的&#xff0c;本文我将以3中类似情况为例&#xff0c;来示范并解说此类需求如何完成&#xff1a; 第一种&#xff0c;批量删除…...

关于按键状态机解决Delay给程序带来的问题

问题产生 我在学习中断的过程中&#xff0c;使用EXTI15外部中断&#xff0c;在其中加入HAL_Delay();就会发生报错 错误地方 其它地方配置 问题原因 在中断服务例程&#xff08;ISR&#xff09;中使用 HAL_Delay() 会导致问题的原因是&#xff1a; 阻塞性&#xff1a; HAL_D…...

62.【C语言】浮点数的存储

目录 1.浮点数的类型 2.浮点数表示的范围 3.浮点数的特性 《计算机科学导论》的叙述 4.浮点数在内存中的存储 答案速查 分析 前置知识:浮点数的存储规则 推导单精度浮点数5.5在内存中的存储 验证 浮点数取出的分析 1.一般情况:E不全为0或不全为1 2.特殊情况:E全为0…...

GO网络编程(一):基础知识

1. 网络编程的基础概念 TCP/IP 协议栈 TCP/IP 是互联网通信的核心协议栈&#xff0c;分为以下四个层次&#xff1a; 应用层&#xff08;Application Layer&#xff09;&#xff1a;为应用程序提供网络服务的协议&#xff0c;比如 HTTP、FTP、SMTP 等。传输层&#xff08;Tra…...

【Linux】用虚拟机配置Ubuntu环境

目录 1.虚拟机安装Ubuntu系统 2.Ubuntu系统的网络配置 3.特别声明 首先我们先要下载VMware软件&#xff0c;大家自己去下啊&#xff01; 1.虚拟机安装Ubuntu系统 我们进去之后点击创建新的虚拟机&#xff0c;然后选择自定义 接着点下一步 再点下一步 进入这个界面之后&…...

酒店智能门锁SDK接口pro[V10] 门锁校验C#-SAAS本地化-未来之窗行业应用跨平台架构

一、代码 int 酒店标识_int Convert.ToInt32(酒店标识);StringBuilder 锁号2024 new StringBuilder(8);//信息 "未知返回值&#xff1a;" bufCard_原始;GetGuestLockNoByCardDataStr_原始(酒店标识_int, bufCard_原始.ToString(), 锁号2024);StringBuilder 退…...

Gitのrebase用法

在 Git 中&#xff0c;rebase 是一种用于整合多个提交历史的操作&#xff0c;它可以将一个分支的变更“重放”到另一个分支上。与 merge 不同&#xff0c;rebase 会产生一个线性的提交历史&#xff0c;使得项目的历史记录更加整洁和易于理解。 1. 什么是 Rebase&#xff1f; …...

二分查找一>:在排序数组中查找元素的第一个和最后一个位置

1.题目&#xff1a; 2.解析:这里不能用传统二分&#xff0c;因为涉及范围&#xff0c;传统二分时间复杂度会降为O(N)&#xff0c;要做些改动。 步骤一&#xff1a;查找区间左端点 细节图&#xff1a; 步骤二&#xff1a;查找区间右端点&#xff1a; 细节图&#xff1a; 代码…...

undeclared identifier ‘UNITY_PREV_MATRIX_M‘ - Unity Shader自己写URP,引用内部 hlsl

碰到这样的问题&#xff0c;居然非常淡定 这个链接里说了问题&#xff1a; 一个哥们A问&#xff0c;为什么include urp common.hlsl 提示莫名其妙 另一个哥们B说&#xff0c;这个issue 说了&#xff0c;可能是这个原因&#xff08;也没正面答&#xff09; 从issue我们知道&a…...

信息安全工程师(29)存储介质安全分析与防护

前言 存储介质安全分析与防护是确保数据安全与完整性的重要环节。存储介质&#xff0c;如硬盘、U盘、SD卡等&#xff0c;作为数据的载体&#xff0c;其安全性直接关系到数据的安全。 一、存储介质安全分析 1. 数据泄露风险 格式化不彻底&#xff1a;传统的格式化操作往往只能删…...

Html5知识点介绍

HTML5 是 HTML 的最新版本&#xff0c;它引入了许多新特性和元素来增强 Web 开发的能力和灵活性。以下是一些关键的 HTML5 知识点&#xff1a; 1. 语义化标签 HTML5 增加了许多新的语义化标签&#xff0c;用来更好地定义页面结构和内容&#xff0c;这些标签使代码更加清晰易读&…...

探索机器学习中的特征选择技术

在机器学习和数据科学领域&#xff0c;特征选择是一个关键步骤&#xff0c;它不仅有助于提高模型的性能&#xff0c;还能帮助我们更好地理解数据。本文将深入探讨特征选择的重要性、常见方法以及如何在实际项目中应用这些技术。 一、特征选择的重要性 降低维度&#xff1a;减…...

数造科技入选中国信通院《高质量数字化转型产品及服务全景图》三大板块

9月24日&#xff0c;2024大模型数字生态发展大会暨“铸基计划”年中会议在北京召开。会上&#xff0c;中国信通院发布了2024年《高质量数字化转型产品及服务全景图&#xff08;上半年度&#xff09;》和《高质量数字化转型技术解决方案&#xff08;上半年度&#xff09;》等多项…...

什么是分布式数据库

分布式数据库&#xff08;Distributed Database&#xff09;是一种数据库系统&#xff0c;它的数据被存储在不同的物理位置&#xff0c;但对用户来说表现得就像一个单一的、统一的数据库。这种系统由多个自治的数据库站点组成&#xff0c;这些站点通过网络相互连接&#xff0c;…...

从u盘直接删除的文件能找回吗 U盘文件误删除如何恢复

U盘上的文件被删除并不意味着它们立即消失。事实上&#xff0c;删除操作只是将文件从文件系统的目录中移除&#xff0c;并标记可用空间。这意味着在文件被覆盖之前&#xff0c;它们仍然存在于存储介质上。因此&#xff0c;只要文件没有被新的数据覆盖&#xff0c;我们就有机会恢…...

如何使用ssm实现基于HTML的中国传统面食介绍网站的搭建+vue

TOC ssm758基于HTML的中国传统面食介绍网站的搭建vue 第1章 绪论 1.1选题动因 当前的网络技术&#xff0c;软件技术等都具备成熟的理论基础&#xff0c;市场上也出现各种技术开发的软件&#xff0c;这些软件都被用于各个领域&#xff0c;包括生活和工作的领域。随着电脑和笔…...

【生成模型】学习笔记

生成模型 生成模型概述&#xff08;通俗解释&#xff09; 生成的核心是生成抽象化的内容&#xff0c;利用已有的内容生成没有的/现实未发生的内容。这个过程类似于人类发挥想象力的过程。 生成模型的应用场景非常广泛&#xff0c;可以应用于艺术表达&#xff0c;如画的生成、…...

大语言模型知识点分享

1 目前主流的开源模型体系有哪些&#xff1f; Prefix Decoder 系列模型 核心点&#xff1a; 输入采用双向注意力机制&#xff0c;输出为单向注意力。双向注意力意味着输入的每个部分都可以关注到输入的所有其他部分&#xff0c;这在理解上下文时具有很强的优势。 代表模型&a…...

openpnp - 底部相机高级校正的参数设置

文章目录 openpnp - 底部相机高级校正的参数设置概述笔记修改 “Radial Lines Per Calibration Z” 的方法不同 “Radial Lines Per Calibration Z”的校验结果不同 “Radial Lines Per Calibration Z”的设备校验动作的比较总结备注END openpnp - 底部相机高级校正的参数设置 …...

劳动与科技、艺术结合更好提高劳动教育意义

在中小学教育中&#xff0c;劳动教育是培养学生基本生活技能和劳动习惯的重要环节。但当代的劳动教育不在单纯的劳动&#xff0c;而是劳动技能的提升与学习&#xff0c;通过学习劳动技能与实践活动&#xff0c;强化劳动教育与其他课程的融合&#xff0c;学生深刻理解劳动的意义…...

基于Hive和Hadoop的招聘分析系统

本项目是一个基于大数据技术的招聘分析系统&#xff0c;旨在为用户提供全面的招聘信息和深入的职位市场分析。系统采用 Hadoop 平台进行大规模数据存储和处理&#xff0c;利用 MapReduce 进行数据分析和处理&#xff0c;通过 Sqoop 实现数据的导入导出&#xff0c;以 Spark 为核…...

目标检测评价指标

混淆矩阵&#xff08;Confusion Matrix&#xff09; 准确率&#xff08;accuracy&#xff09; 准确率&#xff1a;预测正确的样本数 / 样本数总数 &#xff08;正对角线 / 所有&#xff09; 精度&#xff08;precision&#xff09; 精度&#xff1a;预测正确里面有多少确实是…...

禁用wordpress默认编辑器/百度广告收费

Windows下安装Redis服务听语音Redis是有名的NoSql数据库&#xff0c;一般Linux都会默认支持。但在Windows环境中&#xff0c;可能需要手动安装设置才能有效使用。这里就简单介绍一下Windows下Redis服务的安装方法&#xff0c;希望能够帮到你。工具/原料Windows7 64位操作系统Re…...

餐饮公司的网站建设/今日国际新闻热点

琢磨这个概念很久了&#xff0c;有空就细细写下来&#xff01;&#xff01;先写点 关于傻逼 傻逼是一个概念&#xff0c;所以欲谈“傻逼”&#xff0c;必须先谈概念。 对于事物的认知&#xff0c;一步一步的了解&#xff0c;然后通过一个概念将认识的事物用一个词语标识&#x…...

wordpress 哪些网站/制作网页教程

企业中安全优化配置原则 推荐&#xff1a;尽可能不给内部服务器配置外网ip ,可以通过代理转发或者通过防火墙映射.并发不是特别大情况有外网ip,可以开启防火墙服务. 使用场景&#xff1a; 大并发的情况&#xff0c;不能开iptables,影响性能&#xff0c;利用硬件防火墙提升架构安…...

宁波手机网站制作/市场营销师报名官网

arch-vendor-kernel-system arch&#xff0c;即系统架构&#xff0c;表示交叉编译器&#xff0c;是用于哪个目标系统架构中&#xff0c;用于那个平台中的。即&#xff0c;用此交叉编译器编译出来的程序&#xff0c;是运行在哪种CPU上面的arch的值&#xff0c;常见的有很多种&a…...

做支付网站/广告投放公司

参考资料: ReferenceQueue食用手册 java引用食用手册 ReferenceQueue源代码里面很好的展示了java队列的实现思路&#xff0c; 以及多线程观察者的实现思路 多线程观察者实现思路&#xff1a; 入队的时候&#xff0c; 调用notify()方法 remove方法调用的时候&#xff0c;如果对列…...

深圳做网站建设月薪多少/百度站长资源

造价师考试最新推荐&#xff1a;2019年一级造价师考试《造价管理》考试真题(网友版)2019年一级造价师考试《土建计量》考试真题(网友版)2019年一级造价师考试试卷评分标准2020年一级造价师考试时间预测各省2019年一级造价师考试报考人数汇总&#xff0c;人气持续走高2019年一级…...