day3_1:理解“泛型”,你只需要读这一篇!

可以给我一个🆓的大拇哥吗?👍😚

读前扫盲

1.钻石运算符 <>:
自 Java 7 以后,Java 引入了钻石运算符,可以让编译器自动推断出泛型参数的类型。
在这里,new ArrayList<>() 等价于 new ArrayList(),因为编译器可以根据左边的 List 自动推断出 ArrayList 应该是 ArrayList

List<String> list = new ArrayList<>();

表示:
定义一个名为 list 的变量,它的类型是 List(即只能存储字符串的列表)。
同时,利用 new ArrayList<>() 创建一个 ArrayList 对象来实现这个列表,自动推断出这个 ArrayList 也是存储 String 类型的对象。

2.未使用泛型,调用 list.get(0) 返回的是 Object,你必须手动将其转换为 期望的类型(例如 String)。因为:
在不使用泛型的情况下,List 被称为“原始类型”,它并没有保存任何元素的具体类型信息,因此调用 list.get(0) 返回的默认类型是 Object。这是因为在 Java 泛型引入之前,集合类(例如 ArrayList)内部只能存储 Object 类型的引用。当你从集合中取出元素时,编译器只能知道它是一个 Object,而不知道具体是什么类型,所以你必须手动将其转换为期望的类型(例如 String)。

正文开始

泛型(Generics)详解

1. 什么是泛型?

泛型是 Java 提供的一种类型安全机制,允许在定义类、接口和方法时使用参数化类型。它在编译阶段对类型进行检查,从而提高代码的安全性和可读性。

  • 通俗理解:
    泛型就像在容器上贴一个“标签”,告诉编译器这个容器只能存放某种类型的元素。例如,一个装苹果的篮子只能放苹果,不能放橘子。

2. 泛型的主要作用

  1. 提高类型安全性
    • 编译阶段就能检查出类型不匹配的错误,避免运行时抛出 ClassCastException
  2. 减少强制类型转换
    • 泛型让代码更加简洁,避免频繁的类型转换。

3. 泛型的语法糖效果

3.1 泛型代码示例
import java.util.ArrayList;
import java.util.List;

public class GenericExample {
    public static void main(String[] args) {
        // 定义一个泛型列表,指定只能存储字符串类型的数据
        List<String> list = new ArrayList<>();
        list.add("Hello");  // 添加字符串
        list.add("World");  // 添加字符串
        
        // 从列表中取出元素,不需要强制类型转换
        String firstElement = list.get(0);
        System.out.println("第一个元素:" + firstElement);

        // 尝试添加非字符串类型会在编译期报错
        // list.add(123); // 编译失败
    }
}
  • 编译期保证类型安全
    • list.add(123); 编译器会直接报错,说明不允许插入非 String 类型。

为了体会泛型的好处,下面提供没有泛型的实现方式。

import java.util.ArrayList;
import java.util.List;

public class NonGenericExample {
    public static void main(String[] args) {
        // 定义一个非泛型列表,列表可以存储任何类型的数据
        List list = new ArrayList();
        list.add("Hello");  // 添加字符串
        list.add("World");  // 添加字符串
        
        // 由于没有泛型,编译器无法检查存入的数据类型,
        // 因此可以添加任意类型的数据,如下面这行不会报错
        list.add(123); // 添加一个整数

        // 从列表中取出元素时,返回的类型是 Object
        // 需要进行强制类型转换才能当作 String 使用
        String firstElement = (String) list.get(0);
        System.out.println("第一个元素:" + firstElement);
        
        // 如果取出一个非字符串的数据,却错误地进行强制类型转换,程序会在运行时报错
        // 例如,下面这行试图将整数转换为字符串,将导致 ClassCastException 异常
        try {
            String thirdElement = (String) list.get(2);
            System.out.println("第三个元素:" + thirdElement);
        } catch (ClassCastException e) {
            System.out.println("转换失败:尝试将非字符串类型转换为 String。");
        }
    }
}

显然可见,使用泛型:
编译期间就能保证类型安全,不允许添加错误类型的数据。
取出元素时不需要显式的类型转换,代码更简洁、安全。


3.2 编译后的效果

假设我们用 javac 编译上述代码,然后通过 javap 查看字节码,可以发现:

原始代码:

List<String> list = new ArrayList<>();
list.add("Hello");
String firstElement = list.get(0);

反编译后的字节码等价代码:

List list = new ArrayList();  // 泛型信息被擦除
list.add("Hello");            // 允许添加任何类型
String firstElement = (String) list.get(0);  // 需要强制类型转换
  • 泛型的本质:
    • 泛型只在编译阶段有效,编译器通过泛型保证类型安全。
    • 编译后,泛型信息会被类型擦除(Type Erasure),实际运行时只是普通的 List

4. 泛型类型擦除的底层机制

  1. 编译阶段:

    • 编译器会在生成字节码时移除所有的泛型信息。
    • 例如,List<String> 会变成 List
    • 所有泛型参数都会被替换为 Object 或其边界类型(如 T extends Number 会替换为 Number)。
  2. 运行阶段:

    • JVM 只知道这是一个 List,不知道具体存储了哪些类型的数据。
    • 这也是为什么泛型的具体类型在运行时是无法获取的。

示例:

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 编译后的字节码中:
List list1 = new ArrayList();  // 对应 stringList
List list2 = new ArrayList();  // 对应 integerList

5. 泛型的限制与注意事项

5.1 运行时无法获取泛型类型

由于泛型在运行时被擦除,导致无法获取泛型的实际类型信息。

示例:

import java.util.ArrayList;
import java.util.List;

public class TypeErasureDemo {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();

        System.out.println(stringList.getClass() == integerList.getClass()); // 输出 true
    }
}
  • 输出结果: true
  • 原因: 泛型在运行时被擦除,stringListintegerList 实际上都是 ArrayList

5.2 泛型不能用于基本数据类型

泛型不支持直接使用基本数据类型(如 intdouble),需要使用包装类(如 IntegerDouble)。

示例:

List<Integer> intList = new ArrayList<>();
intList.add(10);  // 自动装箱:将 int 转换为 Integer
int value = intList.get(0);  // 自动拆箱:将 Integer 转换为 int

6. 通用泛型类和泛型方法

6.1 泛型类
// 定义一个泛型类
class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        // 创建一个存储字符串的 Box
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello, 泛型!");
        System.out.println(stringBox.getValue());

        // 创建一个存储整数的 Box
        Box<Integer> intBox = new Box<>();
        intBox.setValue(123);
        System.out.println(intBox.getValue());
    }
}
  • 关键点:
    • 泛型类通过 <T> 定义一个类型参数 T,可以在类中通用。
    • 使用时需指定具体类型(如 StringInteger)。

6.2 泛型方法
// 定义一个泛型方法
class Util {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}

public class GenericMethodExample {
    public static void main(String[] args) {
        String[] stringArray = {"Java", "C++", "Python"};
        Integer[] intArray = {1, 2, 3};

        // 调用泛型方法
        Util.printArray(stringArray);
        Util.printArray(intArray);
    }
}
  • 关键点:
    • 泛型方法在返回类型前加 <T> 定义泛型。
    • 方法调用时可以自动推断类型。

7. 总结

  1. 编译期安全性: 泛型在编译阶段检查类型,避免类型转换异常。
  2. 类型擦除: 泛型信息在运行时被移除,所有泛型类型被替换为 Object 或其边界类型。
  3. 泛型类和泛型方法: 提供了代码复用能力,支持灵活定义和使用。

通过泛型,Java 提供了更安全、更通用的编程方式,但由于类型擦除的限制,在某些高级场景中会有局限性。

可以关注我,后续持续更新中……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值