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

Java 泛型

一、简介

Java的泛型(Generics)是Java 5引入的一项重要特性,允许在定义类、接口和方法时使用类型参数,从而增强代码的类型安全性和重用性。泛型的主要目的是在编译时提供类型检查,减少运行时的类型转换错误。

1.1 基本概念

泛型允许你编写可以操作多种类型的代码,而不需要为每种类型编写单独的类或方法。通过使用类型参数,可以在编译时指定具体的类型。

1.2 泛型类

泛型类是指具有一个或多个类型参数的类。类型参数在类定义时指定,并在实例化时确定具体类型。

public class Box<T> {
    private T item;

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

    public T getItem() {
        return item;
    }
}

在这个例子中,Box 类有一个类型参数 T,可以在实例化时指定具体的类型:

Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
String item = stringBox.getItem();  // 不需要类型转换
1.3 泛型方法

泛型方法是指具有一个或多个类型参数的方法。类型参数在方法定义时指定,并在调用时确定具体类型。

public <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

在这个例子中,printArray 方法有一个类型参数 T,可以在调用时指定具体的类型:

Integer[] intArray = {1, 2, 3};
String[] strArray = {"A", "B", "C"};

printArray(intArray);  // 输出: 1 2 3
printArray(strArray);  // 输出: A B C
1.4 泛型的类型参数

类型参数可以是任何有效的Java标识符,但通常使用单个大写字母,如 T、E、K、V 等。常见的类型参数约定如下:

  • T:表示类型(Type)
  • E:表示元素(Element),常用于集合类
  • K:表示键(Key),常用于映射
  • V:表示值(Value),常用于映射
1.5 泛型的通配符

Java泛型支持通配符,用于表示未知类型。通配符有两种形式:?? extends T? super T

  1. 无界通配符
    • 无界通配符 ? 表示任意类型。
      public void printList(List<?> list) {
      	for (Object element : list) {
      		System.out.println(element);
      	}
      }
      
  2. 上界通配符
    • 上界通配符 ? extends T 表示类型参数是 T 或其子类。
      public void printNumbers(List<? extends Number> list) {
      	for (Number number : list) {
      		System.out.println(number);
      	}
      }
      
  3. 下界通配符
    • 下界通配符 ? super T 表示类型参数是 T 或其父类。
      public void addNumbers(List<? super Integer> list) {
      	list.add(1);
      	list.add(2);
      }
      
1.6 泛型的应用场景
  1. 集合框架:Java集合框架(如 ArrayList、HashMap 等)广泛使用泛型来保证类型安全。
  2. 工具类:泛型可以用于编写通用的工具类,如 Comparator、Function 等。
  3. 设计模式:泛型可以用于实现一些设计模式,如工厂模式、策略模式等。

二、泛型的类型擦除

泛型的类型擦除(Type Erasure) 是Java泛型实现的核心机制之一。它的主要目的是在编译时确保类型安全,同时在运行时保持与Java早期版本(Java 5之前)的兼容性。类型擦除的具体表现是:泛型类型参数在编译后被替换为它们的上限(通常是 Object),并在必要时插入类型转换。

2.1 类型擦除的工作原理
  1. 编译时:
    • 编译器会检查泛型代码的类型安全性,确保类型参数的使用是正确的。
    • 在编译后的字节码中,所有的泛型类型参数都会被擦除,替换为它们的上限(如果没有指定上限,则替换为 Object)。
    • 编译器会在需要的地方插入类型转换代码,以确保运行时的类型安全。
  2. 运行时:
    • 在运行时,JVM(Java虚拟机)并不知道泛型类型参数的具体信息。所有的泛型类型都被视为它们的上限类型(如 Object)。
    • 由于类型擦除,泛型类型的具体信息在运行时是不可用的。
2.2 类型擦除的具体表现
  1. 泛型类中的类型擦除
    以下是一个泛型类的例子:

    public class Box<T> {
    	private T item;
    
    	public void setItem(T item) {
        	this.item = item;
    	}
    
    	public T getItem() {
        	return item;
    	}
    }
    

    在编译后,类型参数 T 会被擦除,替换为 Object:

    public class Box {
    	private Object item;
    
    	public void setItem(Object item) {
        	this.item = item;
    	}
    
    	public Object getItem() {
        	return item;
    	}
    }
    

    当使用泛型类时,编译器会自动插入类型转换代码:

    Box<String> stringBox = new Box<>();
    stringBox.setItem("Hello");
    String item = stringBox.getItem();  // 编译后:String item = (String) stringBox.getItem();
    
  2. 泛型方法中的类型擦除
    以下是一个泛型方法的例子:

    public <T> T getFirst(List<T> list) {
    	return list.get(0);
    }
    

    在编译后,类型参数 T 会被擦除,替换为 Object:

    public Object getFirst(List list) {
    	return list.get(0);
    }
    

    当调用泛型方法时,编译器会自动插入类型转换代码:

    List<String> stringList = Arrays.asList("A", "B", "C");
    String first = getFirst(stringList);  // 编译后:String first = (String) getFirst(stringList);
    
  3. 有界类型参数的类型擦除
    如果泛型类型参数有上限(如 T extends Number),类型擦除时会替换为上限类型:

    public class Box<T extends Number> {
    	private T item;
    
    	public void setItem(T item) {
        	this.item = item;
    	}
    
        public T getItem() {
        	return item;
    	}
    }
    

    在编译后,类型参数 T 会被擦除,替换为 Number:

    public class Box {
    	private Number item;
    
    	public void setItem(Number item) {
            this.item = item;
    	}
    
    	public Number getItem() {
        	return item;
    	}
    }
    
2.3 类型擦除的优点
  1. 兼容性:
    • 类型擦除确保了Java泛型与早期版本(Java 5之前)的兼容性,使得现有的非泛型代码可以继续运行。
  2. 性能:
    • 由于类型擦除,泛型代码在运行时不需要额外的类型信息,减少了运行时的开销。
  3. 简单性:
    • 类型擦除简化了JVM的实现,因为JVM不需要为泛型引入新的机制。

三、泛型的限制

Java的泛型虽然极大地增强了代码的类型安全性和重用性,但也带来了一些问题和限制。

3.1 常见限制
  1. 不能创建泛型类型的实例:

    • 由于类型擦除,运行时无法知道类型参数的具体类型,因此不能直接创建泛型类型的实例。
    • 例如:
      public class Box<T> {
      	private T item;
      
      	// 错误:不能直接实例化泛型类型
      	public Box() {
      		this.item = new T();  // 编译错误
      	}
      }
      
  2. 不能创建泛型数组:

    • 由于类型擦除,运行时无法确保数组的类型安全,因此不能直接创建泛型数组。
    • 例如:
      // 错误:不能创建泛型数组
      T[] array = new T[10];  // 编译错误
      
      // 替代方案:使用 ArrayList
      List<T> list = new ArrayList<>();
      
  3. 不能使用基本类型作为类型参数:

    • 泛型类型参数必须是引用类型,不能是基本类型(如 int、char 等)。
    • 例如:
      // 错误:不能使用基本类型
      List<int> intList = new ArrayList<>();  // 编译错误
      
      // 必须使用包装类
      List<Integer> integerList = new ArrayList<>();
      
  4. 类型擦除导致的运行时类型信息丢失:

    • 由于类型擦除,运行时无法获取泛型类型参数的具体信息。
    • 例如:
      List<String> stringList = new ArrayList<>();
      List<Integer> integerList = new ArrayList<>();
      
      // 运行时无法区分
      System.out.println(stringList.getClass() == integerList.getClass());  // 输出: true
      
  5. 泛型类型不能用于静态上下文:

    • 泛型类型参数不能用于静态字段、静态方法或静态初始化块,因为静态成员属于类级别,而泛型类型参数属于实例级别。
    • 例如:
      public class Box<T> {
      	// 错误:不能使用泛型类型参数
      	private static T staticField;  // 编译错误
      
      	// 错误:不能使用泛型类型参数
      	public static T staticMethod() {  // 编译错误
      		return null;
      	}
      }
      
  6. 泛型与重载的冲突:

    • 两个泛型方法如果只有类型参数不同,会导致编译错误,因为它们在编译后会被擦除为相同的方法签名。
    • 例如:
      public class Example {
      	// 错误:方法签名冲突
      	public void print(List<String> list) {}
      	public void print(List<Integer> list) {}  // 编译错误
      }
      
  7. 泛型与异常处理的冲突:

    • 泛型类型参数不能用于 catch 块中的异常类型。
    • 例如:
      public class Example {
      	public <T extends Exception> void handle(T exception) {
      		try {
          		throw exception;
      		} catch (T e) {  // 编译错误
                  // 处理异常
      		}
      	}
      }
      
  8. 泛型与多态性的冲突:

    • 泛型类型参数在继承和多态性中可能会导致一些意外行为。
    • 子类无法重写父类的泛型方法时,可能会导致类型不匹配的问题。
    • 泛型类型参数在继承链中可能会导致类型安全问题。
      class Parent<T> {
      	public void set(T item) {}
      }
      
      class Child extends Parent<String> {
      	// 错误:不能重写父类的泛型方法
      	public void set(Object item) {}  // 编译错误
      }
      
  9. 泛型与类型转换的冲突:

    • 由于类型擦除,泛型类型在运行时无法进行类型检查,可能导致 ClassCastException。
    • 如果泛型类型使用不当,可能会导致运行时的类型转换异常。
      List<String> stringList = new ArrayList<>();
      List rawList = stringList;  // 原始类型
      rawList.add(10);  // 编译通过,但运行时会抛出 ClassCastException
      String item = stringList.get(0);  // 抛出 ClassCastException
      
  10. 泛型与通配符的复杂性:

    • 泛型通配符(如 ?、? extends T、? super T)虽然增强了灵活性,但也增加了代码的复杂性。
    • 通配符的使用可能会导致代码难以理解和维护。
    • 通配符的上界和下界可能会导致类型安全问题。
      public void process(List<? extends Number> list) {
      	// 不能添加元素,因为类型未知
      	list.add(10);  // 编译错误
      }
      
3.2 常见的绕过泛型限制的方法

Java泛型的限制主要源于类型擦除和语言设计,虽然无法完全绕过,但可以通过一些常用方法缓解这些限制。

  1. 使用Class传递类型信息
    • 解决问题:类型擦除导致运行时无法获取泛型的具体类型。
    public class Box<T> {
    	private Class<T> type;
    	public Box(Class<T> type) {
        	this.type = type;
    	}
    	public T createInstance() throws Exception {
        	return type.getDeclaredConstructor().newInstance();
    	}
    }
    
  2. 使用反射创建泛型实例
    • 解决问题:无法直接实例化泛型类型(如new T())。
    public <T> T createInstance(Class<T> clazz) throws Exception {
    	return clazz.getDeclaredConstructor().newInstance();
    }
    
  3. 使用Object[]并进行类型转换
    • 解决问题:无法直接创建泛型数组(如new T[10])。
    @SuppressWarnings("unchecked")
    public <T> T[] createArray(Class<T> clazz, int size) {
    	return (T[]) java.lang.reflect.Array.newInstance(clazz, size);
    }
    
  4. 使用带界限的通配符(如<? super T>)来允许添加元素
    • 解决问题:通配符集合(如List<?>)不能添加元素。
    public void addNumber(List<? super Integer> list) {
    	list.add(42); // 允许添加
    }
    

四、桥接方法

4.1 桥接方法的作用

Java编译器会自动生成一个桥接方法,用于在类型擦除后保持方法重写的正确性。桥接方法的作用是将泛型类型参数的具体类型转换为擦除后的类型,并调用子类的具体实现。

4.2 桥接方法的实现
  1. 桥接方法的生成
    • 编译器会为子类生成一个桥接方法,并调用子类的具体实现。
      class Child extends Parent<String> {
      	// 子类的具体实现
      	@Override
          public void set(String item) {
      		System.out.println("Child set: " + item);
      	}
      
      	 // 编译器生成的桥接方法
      	public void set(Object item) {
      		set((String) item);  // 调用子类的具体实现
       	}
      }
      
    • 这个桥接方法的作用是:
      • 将 Object 类型的参数强制转换为 String 类型。
      • 调用子类的 set(String item) 方法。
    • 通过这种方式,桥接方法确保了 Child 类的 set 方法能够正确重写 Parent 类的 set 方法。
  2. 桥接方法的字节码
    • 查看 Child 类的字节码,可以看到桥接方法的存在:
      // 子类的具体实现
      public void set(java.lang.String);
      
      // 编译器生成的桥接方法
      public void set(java.lang.Object);
      
      桥接方法的字节码如下:
      public void set(java.lang.Object);
      	descriptor: (Ljava/lang/Object;)V
      	flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
      	Code:
      		aload_0
      		aload_1
      		checkcast     #2  // 强制转换为 String
      		invokevirtual #3  // 调用子类的 set(String) 方法
      		return
      
      • ACC_BRIDGE 标志表示这是一个桥接方法。
      • ACC_SYNTHETIC 标志表示这个方法是由编译器生成的,不会出现在源代码中。
4.3 桥接方法的典型场景
  1. 泛型方法的重写
    • 桥接方法主要用于泛型方法的重写场景。例如:
      class Parent<T> {
      	public T get() {
      		return null;
      	}
      }
      
      class Child extends Parent<String> {
      	@Override
      	public String get() {
      		return "Hello";
      	}
      }
      
      在编译后,Parent 类的 get 方法会被擦除为:
      public Object get() {
      	return null;
      }
      
      而 Child 类的 get 方法仍然是:
      public String get() {
      	return "Hello";
      }
      
      为了确保 Child 类的 get 方法能够正确重写 Parent 类的 get 方法,编译器会生成一个桥接方法:
      class Child extends Parent<String> {
      	@Override
      	public String get() {
      		return "Hello";
      	}
      
      	// 编译器生成的桥接方法
      	public Object get() {
      		return get();  // 调用子类的具体实现
      	}
      }
      
  2. 泛型接口的实现
    • 桥接方法也用于泛型接口的实现。例如:
      interface MyInterface<T> {
      	void set(T item);
      }
      
      class MyClass implements MyInterface<String> {
      	@Override
      	public void set(String item) {
      		System.out.println("MyClass set: " + item);
      	}
      }
      
      在编译后,MyInterface 的 set 方法会被擦除为:
      void set(Object item);
      
      而 MyClass 的 set 方法仍然是:
      public void set(String item) {
      	System.out.println("MyClass set: " + item);
      }
      
      为了确保 MyClass 的 set 方法能够正确实现 MyInterface 的 set 方法,编译器会生成一个桥接方法:
      class MyClass implements MyInterface<String> {
      	@Override
      	public void set(String item) {
      		System.out.println("MyClass set: " + item);
      	}
      
      	// 编译器生成的桥接方法
      	public void set(Object item) {
      		set((String) item);  // 调用子类的具体实现
      	}
      }
      

五、在泛型为String的List中存放Integer对象

在Java中,泛型提供了编译时的类型安全检查,确保在泛型集合中只能存储指定类型的对象。例如,一个声明为 List 的集合只能存储 String 类型的对象。如果尝试将 Integer 对象存入 List 中,编译器会直接报错。

List<String> stringList = new ArrayList<>();
stringList.add("Hello");  // 正常
stringList.add(100);      // 编译错误:无法将 Integer 添加到 List<String>

然而,在某些情况下,可以通过一些特殊手段绕过编译器的类型检查,将不兼容类型的对象存入泛型集合中。绕过方法有如下:

  1. 通过原始类型绕过类型检查
    • 通过将泛型集合转换为原始类型,可以绕过编译器的类型检查,从而存入不兼容类型的对象。
      List<String> stringList = new ArrayList<>();
      List rawList = stringList;  // 转换为原始类型
      rawList.add(100);           // 绕过类型检查,存入 Integer 对象
      
      System.out.println(stringList);  // 输出: [100]
      
  2. 通过反射绕过类型检查
    • Java的反射机制可以在运行时动态操作对象,包括绕过泛型的类型检查。通过反射,可以直接向泛型集合中存入不兼容类型的对象。
      import java.lang.reflect.Method;
      import java.util.ArrayList;
      import java.util.List;
      
      public class Main {
      	public static void main(String[] args) throws Exception {
      		List<String> stringList = new ArrayList<>();
      
      		// 获取 List 的 add 方法
      		Method addMethod = stringList.getClass().getMethod("add", Object.class);
      
      		// 使用反射调用 add 方法,存入 Integer 对象
      		addMethod.invoke(stringList, 100);
      
      		System.out.println(stringList);  // 输出: [100]
      	}
      }
      

虽然可以绕过了编译器的类型检查,但如果尝试从 List 中获取元素并强制转换为 String,会抛出异常。

String item = stringList.get(0);  // 抛出 ClassCastException

正确的做法可以使用下面方法:

  • 使用 List:
    List<Object> list = new ArrayList<>();
    list.add("Hello");
    list.add(100);
    
  • 使用泛型通配符:
    List<?> list = new ArrayList<>();
    list.add("Hello");  // 编译错误,但可以通过其他方式实现
    

六、泛型的PECS原则

PECS 是 Java 泛型中的一个重要原则,全称为 Producer Extends, Consumer Super。它是用来指导在使用泛型通配符(? extends T 和 ? super T)时的最佳实践。PECS 原则的核心思想是:

  • Producer Extends:如果泛型集合是数据的生产者(即从中读取数据),使用 ? extends T。
  • Consumer Super:如果泛型集合是数据的消费者(即向其中写入数据),使用 ? super T。

PECS 原则的目的是在保证类型安全的同时,最大化代码的灵活性和通用性。

6.1 Producer Extends
  1. 含义
    • Producer 是指数据的生产者,即从集合中读取数据。
    • Extends 表示使用 ? extends T,即集合中的元素类型是 T 或其子类。
  2. 使用场景
    • 当我们需要从一个泛型集合中读取数据时,应该使用 ? extends T。这样可以确保从集合中读取的元素是 T 类型或其子类型。
      public void printNumbers(List<? extends Number> numbers) {
      	for (Number number : numbers) {
      		System.out.println(number);
      	}
      }
      
    • List<? extends Number> 表示 numbers 是一个可以存储 Number 或其子类(如 Integer、Double)的集合。
    • 我们可以安全地从 numbers 中读取元素,因为所有元素都是 Number 类型或其子类型。
  3. 限制
    • 由于 ? extends T 表示类型参数是 T 或其子类,因此无法向集合中添加元素(除了 null),因为编译器无法确定具体的类型。
      List<? extends Number> numbers = new ArrayList<Integer>();
      numbers.add(10);  // 编译错误:无法添加元素
      numbers.add(null); // 允许,因为 null 是所有类型的有效值
      
6.2 Consumer Super
  1. 含义
    • Consumer 是指数据的消费者,即向集合中写入数据。
    • Super 表示使用 ? super T,即集合中的元素类型是 T 或其父类。
  2. 使用场景
    • 当我们需要向一个泛型集合中写入数据时,应该使用 ? super T。这样可以确保向集合中添加的元素是 T 类型或其父类型。
      public void addNumbers(List<? super Integer> numbers) {
      	numbers.add(1);
      	numbers.add(2);
      }
      
    • List<? super Integer> 表示 numbers 是一个可以存储 Integer 或其父类(如 Number、Object)的集合。
    • 我们可以安全地向 numbers 中添加 Integer 类型的元素,因为 Integer 是 ? super Integer 的子类型。
  3. 限制
    • 由于 ? super T 表示类型参数是 T 或其父类,因此无法从集合中读取元素(除了 Object),因为编译器无法确定具体的类型。
      List<? super Integer> numbers = new ArrayList<Number>();
      numbers.add(10);  // 允许
      Integer number = numbers.get(0);  // 编译错误:无法读取元素
      Object obj = numbers.get(0);  // 允许,因为所有类型都是 Object 的子类
      
6.3 PECS 原则的综合应用
  1. 集合的复制

    • 将一个集合中的元素复制到另一个集合中:
      public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      	for (T item : src) {
      		dest.add(item);
      	}
      }
      
    • src 是数据的生产者,因此使用 ? extends T。
    • dest 是数据的消费者,因此使用 ? super T。
  2. 集合的最大值

    • 查找集合中的最大值:
      public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
      	if (list.isEmpty()) {
      		throw new IllegalArgumentException("List is empty");
      	}
      	T max = list.get(0);
      	for (T item : list) {
      		if (item.compareTo(max) > 0) {
          		max = item;
      		}
      	}
      	return max;
      }
      
    • List<? extends T> 用于读取数据。
    • Comparable<? super T> 确保 T 或其父类实现了 Comparable 接口。

相关文章:

  • 生活之味:苦与甜的交织-中小企实战运营和营销工作室博客
  • 大模型叙事下的百度智能云:比创新更重要的,是创新的扩散
  • 第九课:WebSocket与实时通信技术解析
  • TCP三次握手与四次挥手详解:建立与断开连接的底层逻辑
  • mysql主从复制
  • python pip及常用国内镜像源
  • Java爬虫测试淘宝快递费接口的完整指南
  • Visual Studio 安装及使用教程(Windows)【安装】
  • QT系列教程(15) 鼠标事件
  • LuaJIT 学习(1)—— LuaJIT介绍
  • RabbitMQ重复消费如何解决
  • flutter 如何与原生框架通讯安卓 和 ios
  • 虚拟展览馆小程序:数字艺术与文化展示的新形式探索
  • Java EE 进阶:SpringBoot 配置⽂件
  • Day07 -实例 非http/s数据包抓取工具的使用:科来 wrieshark 封包监听工具
  • mingw32编译ffmpeg
  • 【Python】Selenium根据网页页面长度,模拟向下滚动鼠标,直到网页底部的操作
  • es6 尚硅谷 学习
  • 面试之《实现Event Bus》
  • 基于Spring Boot的牙科诊所管理系统的设计与实现(LW+源码+讲解)
  • 平安资管总经理罗水权因个人工作原因辞职
  • 浪尖计划再出发:万亿之城2030课题组赴九城调研万亿产业
  • 习近平主持召开部分省区市“十五五”时期经济社会发展座谈会
  • 2025年“投资新余•上海行”钢铁产业“双招双引”推介会成功举行
  • 文天祥与“不直人间一唾轻”的元将唆都
  • 修订占比近30%收录25万条目,第三版《英汉大词典》来了