Java-泛型

1. 什么是泛型?

泛型(Generics)是 Java 引入的一种特性,允许你在定义类、接口或方法 时使用类型参数(Type Parameters) 。这些类型参数在编译时会被替换为具体的实际类型(Actual Type Arguments)。通俗地说,泛型让你可以编写能够处理多种数据类型的代码,而无需为每一种数据类型都重写一遍相似的逻辑,同时还能在编译时提供更强的类型检查。

泛型的本质:把具体的数据类型作为参考传给类型变量

2. 为什么需要泛型?

在泛型出现之前,Java 主要使用 Object 类型来实现代码的通用性(例如 ArrayList 可以存储任何对象)。但这带来了两个主要问题:

  • 类型不安全: 从集合中取出元素时,需要进行强制类型转换((String) list.get(0))。如果转换错误(例如集合里存的是 Integer,你却转成了 String),会在运行时抛出 ClassCastException。
  • 繁琐的强制转换: 代码中需要大量显式的类型转换,降低了代码的可读性和易用性。

泛型的主要目的就是解决这些问题:

  • 类型安全: 编译器可以在编译时检查你放入集合的元素类型是否符合预期。例如,一个 ArrayList<String> 只能存放 String 对象。尝试放入其他类型会在编译时报错。
  • 消除强制转换: 编译器知道集合中元素的类型,取出元素时无需手动强制转换。
  • 代码复用: 编写一次泛型类或方法,就可以用于多种不同的数据类型。

3. 泛型的使用

3.1 泛型类

在类名后面使用尖括号 < > 来声明类型参数。类型参数通常用单个大写字母表示,常见的约定有:

  • T - 表示类型(Type)

  • E - 表示元素(Element),常用于集合类

  • K - 表示键(Key)

  • V - 表示值(Value)

  • N - 表示数字(Number)

  • S, U, V 等 - 第二、第三、第四种类型

    修饰符 class 类名<类型变量,类型变量,...>{

    }

    public class ArrayList{

    }

    类型变量常用的E、T、K、V建议大写!

java 复制代码
// 定义一个简单的泛型类 Box
public class Box<T> {
    private T content; // 使用 T 作为成员变量类型

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

// 使用
Box<String> stringBox = new Box<>(); // 创建时指定 T 为 String
stringBox.setContent("Hello World"); // OK
String str = stringBox.getContent(); // 无需强制转换

Box<Integer> integerBox = new Box<>(); // 创建时指定 T 为 Integer
integerBox.setContent(42); // OK, 自动装箱
int num = integerBox.getContent(); // 自动拆箱
// integerBox.setContent("abc"); // 编译错误!不能放 String

3.2 泛型接口

接口也可以定义类型参数。

复制代码
修饰符 interface 接口名<类型变量,类型变量,...>{

}

public interface A<E>{

}

类型变量常用的E、T、K、V建议大写!
java 复制代码
public interface Pair<K, V> {
    K getKey();
    V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;

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

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

// 使用
Pair<String, Integer> pair = new OrderedPair<>("Age", 30);
String key = pair.getKey();
Integer value = pair.getValue();

3.3 泛型方法

方法也可以有自己的类型参数,写在返回值类型之前。泛型方法可以在普通类、泛型类或接口中定义。

复制代码
修饰符<类型变量,类型变量,...>返回值类型 方法名(形参列表){

}

public static<T> void test(T,t){

}
java 复制代码
public class Util {
    // 泛型方法:交换数组中两个元素的位置
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 泛型方法:返回两个参数中较大的一个(要求参数实现 Comparable)
    public static <T extends Comparable<T>> T max(T a, T b) {
        return (a.compareTo(b) > 0) ? a : b;
    }
}

// 使用
Integer[] intArray = {1, 2, 3, 4, 5};
Util.swap(intArray, 0, 4); // 交换第一个和最后一个元素
Integer maxVal = Util.max(10, 20); // 返回 20
String maxStr = Util.max("apple", "orange"); // 返回 "orange"
复制代码
package com.yzdan.genericity.demo04;

import com.yzdan.genericity.demo03.Student;


public class GenericDome04 {
    static void main(String[] args) {
        String[] names = {"张三","李四","王五"};
        printArray(names);

        Student[] students = new Student[3];
        printArray( students);
        
        Student max = getMax(students);
        String maxName = getMax(names);
    }

    private static <T> T getMax(T[] students) {
    }

    private static <T> void printArray(T[] names) {
    }
}

4. 类型边界(Bounded Type Parameters)

有时你可能希望对类型参数施加限制。例如,你可能希望类型必须是某个类的子类,或者实现了某个接口。这可以通过 extends 关键字来实现。

  • 上限(Upper Bound): <T extends SomeClass> 表示 T 必须是 SomeClass 或其子类。
  • 多个边界(Multiple Bounds): <T extends ClassA & InterfaceB & InterfaceC> 表示 T 必须继承 ClassA 并实现 InterfaceB 和 InterfaceC (类只能有一个,接口可以有多个,类写在接口前面)。
java 复制代码
// 要求 T 必须是 Number 或其子类(如 Integer, Double)
public class NumericBox<T extends Number> {
    private T number;

    public NumericBox(T number) {
        this.number = number;
    }

    public double square() {
        return number.doubleValue() * number.doubleValue();
    }
}

// 使用
NumericBox<Integer> intBox = new NumericBox<>(5); // OK
// NumericBox<String> strBox = new NumericBox<>("abc"); // 编译错误!String 不是 Number 子类
Double squared = intBox.square(); // 25.0

5. 通配符(Wildcards)

通配符 ? 用于表示未知的类型,通常用于提高 API 的灵活性,特别是在处理参数化类型时。

  • 上界通配符(Upper Bounded Wildcard): <? extends T> 表示类型是 T 或其子类。适合用于读取数据(生产者 Producer)。
  • 下界通配符(Lower Bounded Wildcard): <? super T> 表示类型是 T 或其父类。适合用于写入数据(消费者 Consumer)。
  • 无界通配符(Unbounded Wildcard): <?> 表示任何类型。通常用于不关心具体类型,或只使用 Object 类中方法的情况。
java 复制代码
import java.util.List;

public class WildcardExample {

    // 方法接收一个 List,其元素类型是 Number 或其子类 (如 Integer, Double)
    //方法可以接收接口作为参数。这是因为Java支持多态(polymorphism):
    //接口(如 List)定义了行为规范,方法参数可以声明为接口类型。
    //在调用方法时,你可以传递任何实现了该接口的类的对象。
    //List 是一个泛型接口,代表一个有序的集合(例如,可以存储多个元素)。
    //<? extends Number> 是泛型约束,表示 List 中的元素必须是 Number 类或其子类(如 Integer 或 Double)。
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    // 方法接收一个 List,其元素类型是 Integer 或其父类 (如 Number, Object)
    // 可以向这个列表添加 Integer 对象
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
}

// 使用
List<Integer> intList = List.of(1, 2, 3);
double intSum = WildcardExample.sumOfList(intList); // 6.0

List<Double> doubleList = List.of(1.5, 2.5, 3.5);
double doubleSum = WildcardExample.sumOfList(doubleList); // 7.5

List<Number> numberList = new ArrayList<>();
WildcardExample.addIntegers(numberList); // OK, 添加了 1, 2, 3
// WildcardExample.addIntegers(intList); // 也可以,但通常下界用于更通用的容器

6. 类型擦除(Type Erasure)

Java 的泛型是在编译时实现的特性,称为类型擦除。这意味着:

  • 编译器在编译时会检查泛型类型的使用是否正确(类型安全)。
  • 编译完成后,所有的泛型类型信息都会被擦除。泛型类中的类型参数会被替换为它们的边界 (如果指定了边界,例如 <T extends Number> 则替换为 Number)或 Object(如果没有指定边界)。
  • 在运行时,ArrayList<String> 和 ArrayList<Integer> 实际上是同一个类 ArrayList。
java 复制代码
// 编译前
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");

// 编译后(类型擦除后的近似表示)
Box stringBox = new Box(); // Box 类中的 T 被替换为 Object
stringBox.setContent("Hello"); // 参数类型变为 Object

String content = (String) stringBox.getContent(); // 编译器插入的强制转换

类型擦除是 Java 为了保持向后兼容性(兼容没有泛型的旧 JVM 和代码)而采用的设计。它带来了运行时无法获取泛型类型参数信息等限制。

7. 泛型的注意事项

  • 不能使用基本类型: 泛型类型参数必须是引用类型。 不能使用 int, double, char 等基本类型。需要使用它们的包装类(Integer, Double, Character)。

泛型擦除,即编译后泛型就没用了,所有泛型在编译后都会被擦除,所有类型恢复成Object类型。即对象类型。而对象类型/引用类型不能指向基本数据,只能指向对象!

所以将基本数据包装成对象,即包装类。

1. 包装类

包装类(Wrapper Class)位于 java.lang 包中,用于将基本数据类型封装成对象。每个基本数据类型都有对应的包装类:

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

2. 基本数据类型 vs 包装类

特性 基本数据类型 包装类
存储方式 栈内存(直接值) 堆内存(对象引用)
默认值 有(如 0) null
性能 更高(无对象开销) 略低(对象创建/GC)
用途 局部变量、计算 集合类(如 List<Integer>)、泛型
允许 null 否 是
方法支持 无 提供实用方法(如 Integer.parseInt())

3. 自动装箱与拆箱

Java 5+ 引入了自动装箱(Autoboxing)和拆箱(Unboxing)机制,简化基本类型与包装类的转换:

java 复制代码
// 自动装箱:int -> Integer
Integer num = 10; 

// 自动拆箱:Integer -> int
int value = num; 

// 集合中使用
List<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱
int first = list.get(0); // 自动拆箱
java 复制代码
package com.yzdan.genericity.demo05;

import com.yzdan.genericity.demo02.MyArrayList;

public class GenericDome05 {
    static void main(String[] args) {
        //泛型只支持对象类型/引用类型,不支持基本数据类型
        MyArrayList<Integer> list = new MyArrayList<>();
        //泛型擦除:泛型工作在运行期间,JVM将泛型擦除,只保留类型信息
        //手动装箱,将基本数据类型转换为对象类型
        Integer it1 = Integer.valueOf(123);
        Integer it2 = Integer.valueOf(123);
        System.out.println(it1 == it2);// true, 因为Integer.valueOf()方法会缓存-128~127的数值,范围内只缓存一个对象
        Integer it3 = Integer.valueOf(130);
        Integer it4 = Integer.valueOf(130);
        System.out.println(it3 == it4);// false, 因为Integer.valueOf()方法不会缓存数值>127的数值,会创建新的对象
        //自动装箱,将对象类型转换为基本数据类型
        Integer int11 = 123;
        Integer int12 = 123;
        System.out.println(int11 == int12);// true, 因为Integer.valueOf()方法会缓存-128~127的数值,范围内只缓存一个对象
    }
}

4.包装类的其他功能

可以把基本类型的数据转换成字符串类型

可以把字符串类型的数值转换成数值本身对应的真实数据类型

java 复制代码
//1.将基本数据类型转换为字符串
int i = 123;
String s = i + "";
System.out.println(s);
System.out.println(Integer.toString(i));
//2.将字符串转换为基本数据类型
String s1 = "123";
//int i1 = Integer.parseInt(s1);
int i1 = Integer.valueOf(s1);//和Integer.parseInt()方法效果一样
// 区别在于parseInt()只能转换字符串为int,parseDouble()方法可以转换字符串为double,..
// 而valueOf()方法可以转换字符串为int、long、double、float、boolean、char等类型
System.out.println(i1+2);//125
  • 不能创建参数化类型的数组: new List<String>[10]; 这样的代码在 Java 中是不允许的(编译错误)。可以使用 List<String>[] listArray = (List<String>[]) new List[10]; 来绕过,但这会失去部分类型安全,编译器会警告。通常推荐使用 ArrayList 的 ArrayList (例如 ArrayList<ArrayList<String>>) 或者使用通配符 List<?>[]。
  • 不能实例化类型参数: new T() 是不允许的,因为类型擦除后 T 可能是 Object 或没有无参构造函数的类。可以通过反射结合 Class<T> 参数等方式实现。
  • 不能声明静态字段为泛型类型: 类的静态字段是类级别的,所有实例共享。而泛型类型参数是实例级别的(在创建对象时确定)。所以 private static T instance; 是错误的。
  • 注意类型擦除的影响: 在运行时无法直接获取泛型类的具体类型参数信息(例如 List<String> 在运行时只是 List)。

总结: Java 泛型是一个强大的工具,它极大地提高了代码的类型安全性、可读性和复用性。理解泛型类、泛型方法、类型边界、通配符以及背后的类型擦除机制,对于编写健壮、灵活的 Java 代码至关重要。

相关推荐
Cc.Y10 小时前
Java零基础入门:类与对象深度掌握 —— 从“零散变量“到“打包管理“
java·开发语言·python
\光辉岁月/10 小时前
2.java变量
java·开发语言·变量
Wang's Blog10 小时前
Java 项目实战: 外卖平台优化-前端dist部署与Nginx反向代理rewrite
java·前端·nginx
专业程序开发源10 小时前
springboot生命故事书制作小程序65290-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·小程序·课程设计
代码山河11 小时前
Java的前世今生:从Oak语言到Java 23的发展史
java·学习·架构·教程·面向对象·项目
vx_Biye_Design11 小时前
springboot角色扮演服务平台65161-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·spring·课程设计
李游Leo11 小时前
HarmonyOS 7 QuickDock 闪控窗开发实录 02:floatView × floatingBall:形态切换、位置恢复与单一状态源【鸿蒙心迹】
java·华为·harmonyos
企业数字化笔记11 小时前
视频局部合成怎么做得自然?掩膜修复、边缘羽化、时序稳定与背景替换
java·javascript·音视频
仍然.11 小时前
Redis---主从复制
java·数据库·redis
vx_Biye_Design11 小时前
springboot咖啡厅顾客点单管理系统12080-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express