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

    }

    类型变量常用的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 并实现 InterfaceBInterfaceC (类只能有一个,接口可以有多个,类写在接口前面)。
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]; 来绕过,但这会失去部分类型安全,编译器会警告。通常推荐使用 ArrayListArrayList (例如 ArrayList<ArrayList<String>>) 或者使用通配符 List<?>[]
  • 不能实例化类型参数: new T() 是不允许的,因为类型擦除后 T 可能是 Object 或没有无参构造函数的类。可以通过反射结合 Class<T> 参数等方式实现。
  • 不能声明静态字段为泛型类型: 类的静态字段是类级别的,所有实例共享。而泛型类型参数是实例级别的(在创建对象时确定)。所以 private static T instance; 是错误的。
  • 注意类型擦除的影响: 在运行时无法直接获取泛型类的具体类型参数信息(例如 List<String> 在运行时只是 List)。

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

相关推荐
张np1 小时前
java基础-集合接口(Collection)
java·开发语言
jakeswang1 小时前
ServletLess架构简介
java·后端·servletless
搬山境KL攻城狮2 小时前
maven 私服上传jar
java·maven·jar
蓁蓁啊2 小时前
Ubuntu 虚拟机文件传输到 Windows的一种好玩的办法
linux·运维·windows·单片机·ubuntu
q***56382 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
FREE技术3 小时前
学生成绩管理系统 基于java+springboot+vue实现前后端分离项目并附带万字文档(源码+数据库+万字详设文档+软件包+安装教程)
java·vue.js·spring boot·mysql
q***57503 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
serve the people3 小时前
Comma-Separated List Output Parser in LangChain
windows·langchain·list
南方的狮子先生4 小时前
【C++】C++文件读写
java·开发语言·数据结构·c++·算法·1024程序员节