最新资讯

  • java教程笔记(十一)-泛型

java教程笔记(十一)-泛型

2025-06-11 14:38:03 4 阅读

Java 泛型(Generics)是 Java 5 引入的重要特性之一,它允许在定义类、接口和方法时使用类型参数。泛型的核心思想是将类型由具体的数据类型推迟到使用时再确定,从而提升代码的复用性和类型安全性。

1.泛型的基本概念

1. 什么是泛型?

泛型的本质是参数化类型。参数化类型是指带有类型参数的类或接口在定义类或方法时不指定具体的类型,而是在实例化时传入具体的类型。

  • 泛型中不能写基本数据类型
  • 指定具体的泛型类型后,传递数据时,可以传递该类类型及其子类类型
  • 如果不写泛型,类型默认为Object
/*
List 是 java.util 包下的接口。
E(Element)是类型参数,表示集合中元素的类型。
在代码中你看到的 List 实际上是程序员习惯使用的泛型变量名,与 List 等价。
*/

public interface List extends Collection { ... }


/*
List 是一个泛型接口(类型参数为 E)
List 是一个参数化类型(String 是类型实参)
*/

List list = new ArrayList<>(); 
list.add("hello"); 
String str = list.get(0); // 无需强制转换

2.为什么需要泛型 

在没有泛型的情况下,集合类默认存储的是 Object 类型,这意味着你可以往集合中添加任何类型的对象,但取出来时需要手动强制转换,容易引发 ClassCastException

示例:非泛型带来的问题

List list = new ArrayList();
list.add("hello");
list.add(100); // 编译通过

String str = (String) list.get(1); // 运行时报错:ClassCastException

 使用泛型后

泛型确保了编译期的类型检查,避免运行时类型转换错误。

List list = new ArrayList<>();
list.add("hello");
// list.add(100); // 编译错误,不能添加 Integer 类型
String str = list.get(0); // 不需要强制转换

泛型允许我们编写通用的类、接口和方法,而无需为每种数据类型重复实现相同逻辑。

示例:一个通用的容器类

public class Box {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

你可以这样使用:

 一份代码支持多种类型,提高复用性和可维护性。

Box stringBox = new Box<>();
stringBox.setItem("Hello");
String s = stringBox.getItem(); // 直接获取String类型

Box intBox = new Box<>();
intBox.setItem(123);
Integer i = intBox.getItem();

避免强制类型转换(Avoid Casting)

在没有泛型时,从集合中取出元素必须进行强制类型转换,这不仅繁琐,还可能出错。

非泛型写法

List list = new ArrayList(); 
list.add("hello"); 
String str = (String) list.get(0); // 强制转换

泛型写法

 泛型让代码更简洁、清晰,减少出错机会。

List list = new ArrayList<>(); 
list.add("hello");
 String str = list.get(0); // 自动类型匹配

3. 泛型的优点

特性描述
类型安全避免运行时 ClassCastException
自动类型转换不需要手动强转
代码复用使用泛型编写通用逻辑

2.泛型的使用方式

1. 泛型类

通过在类名后加上  来声明一个泛型类,T 是类型参数(Type Parameter)。可以表示属性类型、方法的返回值类型、参数类型

创建该对象时,该标识确定类型

静态方法不能使用类级别的泛型参数(如class MyClass中的T),但可以定义自己的泛型参数。

/*
<>括号中的标识是任意设置的,用来表示类型参数,指代任何数据类型
  T :代表一般的任何类。
  E :代表 Element 元素的意思,或者 Exception 异常的意思。
  K :代表 Key 的意思。
  V :代表 Value 的意思,通常与 K 一起配合使用。
  S :代表 Subtype 的意思,文章后面部分会讲解示意。
*/
public class Box {
 private T item;
 public void setItem(T item) { 
this.item = item; 
} 
public T getItem() { 
return item; 
} 
} 
// 使用 
Box stringBox = new Box<>(); 
stringBox.setItem("Hello"); 
Box integerBox = new Box<>(); 
integerBox.setItem(123);
//多个泛型参数
public class Pair {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}

泛型类的继承

子类也可以保留父类的泛型特性 

public class NumberBox extends Box {
    public double getDoubleValue() {
        return getValue().doubleValue();
    }
}

2. 泛型接口

  • T 表示实体类型(如 User、Product)。
  • ID 表示主键类型(如 Long、String)。
public interface Repository {
    T findById(ID id);
    void save(T entity);
    void deleteById(ID id);
}
public interface Repository { 
T findById(ID id); 
void save(T t); 
} 

public class UserRepository implements Repository { 
@Override 
public User findById(Long id) { 
return null; 
}
 @Override 
public void save(User user) {}
 }

实现泛型接口并保留泛型

如果希望实现类也保持泛型特性,可以这样做:

public class GenericRepository implements Repository { 
@Override public T findById(ID id) { 
// 泛型实现逻辑 
return null;
 } 
@Override public void save(T entity) { 
// 泛型保存逻辑 
} 
@Override public void deleteById(ID id) { 
// 泛型删除逻辑
 }
 }

调用示例:

GenericRepository userRepository = new GenericRepository<>(); 
User user = userRepository.findById(1L); 
userRepository.save(user);

3. 泛型方法

泛型方法的定义需要在返回类型前使用  来声明一个类型参数,其中 T 是一个占位符,表示任意类型。

方法中参数类型不确定时,泛型方案选择:

1.使用类名后面定义的泛型,所有方法都能用

2.在方法申明上定义自己的泛型,只有本方法能用

/*
:声明一个类型参数。
T value:接收任何类型的参数。
*/
public  void printValue(T value) {
    System.out.println(value);
}
printValue(10);       // 整数
printValue("Hello");  // 字符串
printValue(3.14);     // 浮点数
public class Utils { 
public static  void printArray(T[] array) { 
for (T item : array) { 
System.out.println(item);
 } 
} 
} 
// 调用 
Utils.printArray(new String[]{"A", "B"}); 
Utils.printArray(new Integer[]{1, 2, 3}); // 类型推断
//泛型方法也可以返回泛型类型的数据
public  T getValue(T defaultValue) {
    return defaultValue;
}

String result = getValue("Default");
Integer number = getValue(100);

 4.泛型类与泛型接口的区别

特性泛型类泛型接口
定义方式class ClassNameinterface InterfaceName
主要用途封装通用的数据结构或行为定义通用的行为规范
实现方式直接实例化使用需要被类实现后再使用
类型约束可以通过 extends 限制类型同样支持 extends 约束
多类型参数支持多个泛型参数同样支持多个泛型参数

3.泛型通配符

在 Java 泛型中,通配符(Wildcard) 是一种特殊的类型参数,用于表示未知的类型。它增强了泛型的灵活性,特别是在集合类的操作中非常有用。

1.为什么需要泛型通配符?

1. 泛型不具备多态性

在 Java 中,即使 Dog 是 Animal 的子类,List 并不是 List 的子类型。也就是说,泛型是不变的)即泛型类型之间不继承其参数类型的多态关系。

List dogs = new ArrayList<>();
// List animals = dogs; // 编译错误!不能这样赋值

2.为什么泛型不具备多态性?

1. 为了保证类型安全

如果允许 List 赋值给 List,就会带来潜在的类型不安全风险。

假设允许这种赋值:
List dogs = new ArrayList<>(); 
List animals = dogs; 
// 如果允许 animals.add(new Cat()); // 合法吗?理论上可以,因为 Cat 是 Animal 子类
 Dog dog = dogs.get(0); // 错误 ClassCastException!

这会导致运行时异常,破坏了类型安全性。

因此,Java 在编译期就禁止了这种行为。

3.对比数组的协变性

Java 中的数组是协变的(covariant),即:

Dog[] dogs = new Dog[3]; Animal[] animals = dogs; //  合法

但这其实也存在安全隐患,例如:

animals[0] = new Cat(); // 运行时报错:ArrayStoreException

所以,Java 数组的协变性是在运行时进行类型检查的,而泛型为了避免这种风险,在编译期就禁止了这种操作。

如果你写一个方法用于打印列表中的元素,你可能希望它能接受任何类型的 List,比如 ListList 等。 那么就需要泛型具有多态性,由此就出现了通配符

4.通配符的类型 

1.上界通配符(只能进行只读操作

在 Java 泛型中,上界通配符  是一种特殊的泛型表达方式,用于表示某个类型是 T 或其子类型。它提供了一种灵活的方式来处理具有继承关系的泛型集合。

List list;
  • ?:表示未知类型。
  • extends T:表示该未知类型是 T 或其子类。
List numbers = new ArrayList();

 合法赋值包括:

  • List
  • List
  • List
  • 等等 Number 的子类列表
1.为什么使用上界通配符?
1. 允许读取为父类型

你可以安全地将集合中的元素当作 T 类型来读取。

public void printNumbers(List numbers) {
    for (Number number : numbers) {
        System.out.println(number);
    }
}

安全地读取为 Number,无论实际是 Integer 还是 Double。 

List ints = List.of(1, 2);
List doubles = List.of(3.5, 4.5);

printNumbers(ints);   // ✅ 合法
printNumbers(doubles); // ✅ 合法
2. 避免类型不安全的写入

虽然可以读取,但不能向  集合中添加除 null 外的任何对象。

List list = new ArrayList(); // 
list.add(10); //  编译错误! 
list.add(null); // 合法(但几乎无意义)
 2.为什么上界通配符只能进行只读操作
List integers = new ArrayList<>();
List list = integers;

// list.add(10);       //  编译错误!
// list.add(new Integer(5)); //  同样不允许

虽然我们知道 list 实际上是一个 List,并且可以添加 Integer 类型的值,但编译器无法确定 ? extends Number 到底是 IntegerDouble 还是其他子类,所以为了保证类型安全,直接禁止写入操作

假设允许写入会发生什么?

List intList = new ArrayList<>();
 List list = intList; 
list.add(3.14); // 如果允许,会怎样?
 Integer i = intList.get(0); //  ClassCastException!
  • 3.14 是 Double 类型。
  • 虽然它是 Number 的子类,但 intList 只能存储 Integer
  • 此时如果允许写入,就会破坏 intList 的类型一致性。

因此,Java 在编译期就阻止了这种风险

为什么可以添加 null?

list.add(null); //  合法

  • null 是所有引用类型的合法值。
  • 它不违反任何类型约束,因为 null 可以被当作任何类型来处理。

2.下界通配符(只写不可读)

在 Java 泛型中,下界通配符  表示一个未知类型,它是 T 或其任意父类。这种通配符用于增强泛型的灵活性,特别是在需要向集合中写入数据时。

List list;