[Java进阶] 泛型

Java基础系列

[Java基础] 基本数据类型

[Java基础] 运算符

[Java基础] 流程控制

[Java基础] 面向对象编程

[Java基础] 集合框架

[Java基础] 输入输出流

[Java基础] 异常处理机制

[Java基础] Lambda 表达式

目录

为什么需要泛型?

泛型的用法

泛型类

泛型接口

泛型方法

通配符

类型限定

总结

类型擦除

什么是类型擦除

为何需要类型擦除

如何类型擦除

类型擦除的验证

类型擦除的影响与限制

类型擦除的补偿机制

示例讲解

总结

为什么需要泛型?

Java引入泛型(Java SE 5)的主要原因是为了提高类型安全性代码复用性。在泛型出现之前,Java程序员通常会使用 Object 类型或者一组相关的具体类型来编写集合类和其他可重用组件。这种方式的问题在于缺乏类型安全性和额外的类型转换需求。以下是引入泛型的一些主要原因:

  • 类型安全性 :在使用非泛型集合时,编译器不会检查你放入集合中的元素类型是否与取出时一致。这可能导致运行时的 ClassCastException。而使用泛型后,编译器会在编译期就确保类型的安全性。
  • 消除强制类型转换:在非泛型时代,从集合中获取元素时往往需要显式地进行类型转换,这不仅增加了代码量,还容易出错。泛型则允许编译器自动处理类型转换。
  • 更好的可读性和可维护性:使用泛型可以清楚地表明集合中存储的对象类型,使得代码更容易阅读和理解。此外,当类型信息在编译时就被确定下来,也有助于减少错误并简化调试过程。
  • 代码复用:泛型允许编写通用的类和方法,能够处理多种数据类型,这样可以减少重复代码的编写,并且让程序更加灵活。
  • 静态类型检查:泛型提供了一种静态类型检查机制,使得错误能够在编译阶段被发现,而不是等到运行时才暴露出来。

泛型的用法

Java 泛型的用法非常广泛,主要集中在以下几个方面:泛型类、泛型方法、通配符、泛型接口和类型限定。下面我将详细解释每个方面的用法,并给出相应的代码示例。

泛型类

泛型类允许你在定义类时使用类型参数,从而使得这个类可以处理多种数据类型。

示例:泛型类

复制代码
//一套代码可以处理多种数据类型,实现代码的复用
public class Box<T> {
    private T item;

    public Box(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

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


public class Main {
    public static void main(String[] args) {
        // 创建一个 Integer 类型的 Box
        Box<Integer> intBox = new Box<>(10);
        System.out.println("Integer Box: " + intBox.getItem());

        // 创建一个 String 类型的 Box
        Box<String> stringBox = new Box<>("Hello");
        System.out.println("String Box: " + stringBox.getItem());
    }
}

泛型接口

泛型接口允许你在定义接口时使用类型参数,从而使得实现该接口的类可以处理多种数据类型。

示例:泛型接口

复制代码
public interface Container<T> {
    void add(T item);
    T get(int index);
}

public class ArrayListContainer<T> implements Container<T> {
    private List<T> items = new ArrayList<>();

    @Override
    public void add(T item) {
        items.add(item);
    }

    @Override
    public T get(int index) {
        return items.get(index);
    }
}

public class Main {
    public static void main(String[] args) {
        Container<String> stringContainer = new ArrayListContainer<>();
        stringContainer.add("Hello");
        stringContainer.add("World");
        System.out.println(stringContainer.get(0));  // 输出: Hello
        System.out.println(stringContainer.get(1));  // 输出: World

        Container<Integer> intContainer = new ArrayListContainer<>();
        intContainer.add(1);
        intContainer.add(2);
        System.out.println(intContainer.get(0));  // 输出: 1
        System.out.println(intContainer.get(1));  // 输出: 2
    }
}

泛型方法

泛型方法允许你在方法签名中使用类型参数,从而使得同一个方法可以处理多种数据类型。

示例:泛型方法

复制代码
public class Util {
    // 泛型方法
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    // 带返回值的泛型方法
    public static <T> T getFirstElement(T[] array) {
        if (array.length > 0) {
            return array[0];
        }
        return null;
    }
}

public class Main {
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        Util.printArray(intArray);  // 输出: 1 2 3 4 5 
        System.out.println("First element: " + Util.getFirstElement(intArray));  // 输出: First element: 1

        String[] stringArray = {"Hello", "World"};
        Util.printArray(stringArray);  // 输出: Hello World 
        System.out.println("First element: " + Util.getFirstElement(stringArray));  // 输出: First element: Hello
    }
}

通配符

通配符 ? 表示未知类型,可以用于泛型方法的参数类型。通配符可以分为无界通配符、上界通配符和下界通配符。

示例:通配符

复制代码
public class Util {
    // 无界通配符,可以是任何类型
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    // 上界通配符,必须是Number的子类
    public static void printNumbers(List<? extends Number> list) {
        for (Number num : list) {
            System.out.print(num + " ");
        }
        System.out.println();
    }

    // 下界通配符,必须是Integer的父类
    public static void addNumbers(List<? super Integer> list) {
        list.add(10);
    }
}

public class Main {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("Hello", "World");
        Util.printList(stringList);  // 输出: Hello World 

        List<Integer> intList = Arrays.asList(1, 2, 3);
        Util.printNumbers(intList);  // 输出: 1 2 3 

        List<Number> numberList = new ArrayList<>();
        Util.addNumbers(numberList);
        System.out.println(numberList);  // 输出: [10]

        List<Object> objectList = new ArrayList<>();
        Util.addNumbers(objectList);
        System.out.println(objectList);  // 输出: [10]
    }
}

类型限定

类型限定允许你在定义泛型类或泛型方法时对类型参数进行约束,确保传入的类型满足一定的条件。

示例:类型限定

复制代码
public class Pair<K, V> {
    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 void setKey(K key) {
        this.key = key;
    }

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

    // 带类型限定的方法,上界通配符
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("Age", 25);
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());

        String maxString = Pair.max("Apple", "Banana");
        System.out.println("Max String: " + maxString);  // 输出: Max String: Banana

        Integer maxInt = Pair.max(10, 20);
        System.out.println("Max Integer: " + maxInt);  // 输出: Max Integer: 20
    }
}

总结

通过这些示例,你可以看到 Java 泛型在不同类型的应用场景中的具体用法。泛型的使用不仅提高了代码的类型安全性,还增强了代码的复用性和可读性。

类型擦除

什么是类型擦除

类型擦除(Type Erasure)是指在编译时,JVM编译器会将所有的泛型信息都擦除掉,变成原始类型。具体来说,就是将泛型的类型参数替换成具体类型的上限或下限(如果没有指定上界,则默认为Object)。这样,虽然我们在代码中使用了泛型,但在编译后,所有的泛型类型都会被擦除掉,转而使用其对应的原始类型。

为何需要类型擦除

  1. **兼容性:**Java在引入泛型之前(JDK1.5及之前版本)是没有泛型概念的。为了与之前的JDK版本保持兼容,Java引入了类型擦除的概念。
  2. **效率:**如果为每个泛型类型都生成不同的目标代码,会导致代码膨胀和内存占用增加。类型擦除可以避免这个问题,因为它使得所有使用泛型的类在编译后都使用相同的字节码。

如何类型擦除

  1. **无泛型上界:**如果定义泛型的地方没有指定泛型上界,则所有该泛型类型的变量的数据类型在编译之后都替换为Object。
  2. **有泛型上界:**如果定义泛型的地方指定了泛型上界,则所有该泛型类型的变量的数据类型在编译之后都替换为泛型上界。

类型擦除的验证

  1. **通过Class对象验证:**使用Class对象的getTypeParameters方法获取的泛型信息是占位符(如T、K、V等),而不是实际的泛型类型。
  2. **通过反射机制验证:**泛型约束只在编译阶段有效,在编译之后泛型就被擦除了。因此,如果可以绕过编译阶段对泛型的约束检测,就可以传入任何类型的变量(因为都可以向上转型为Object类型)。

类型擦除的影响与限制

  1. 无法获取泛型类型信息: 由于类型擦除,运行时无法获取泛型类型参数的信息。例如,无法通过反射获取Box<Integer>中的Integer类型信息。再例如,instanceof 操作符不能用于泛型类型参数,if (box instanceof Box<Integer>) 会编译失败,因为 Box<Integer> 在运行时被擦除为 Box
  2. 不能创建泛型类型的数组 :由于类型擦除,你不能创建具有泛型元素的数组。例如,new Box<Integer>[10] 会编译失败。
  3. 不能实例化类型参数 :你不能在泛型类或方法中直接实例化类型参数。例如,new T() 会编译失败。
  4. **类型转换:**编译器在编译时会进行类型检查和转换,以确保类型安全。然而,运行时类型转换错误仍然可能发生,这通常是由于不正确的类型假设导致的。
  5. **桥方法的生成:**为了确保多态性,编译器可能会生成桥方法。这些方法在子类中提供与父类中被类型擦除的方法相同的方法签名。

类型擦除的补偿机制

尽管类型擦除有一些限制,但Java提供了反射机制来操作泛型类型,使得泛型类型在某些情况下还是可以被获取到的。此外,Java还通过编译时的类型检查和转换来确保类型安全。

示例讲解

泛型类类型擦除

以下是一个简单的泛型类示例,以及编译后类型擦除的结果:

复制代码
public class Box<T> {  
    private T value;  
    public void set(T value) {  
        this.value = value;  
    }  
    public T get() {  
        return value;  
    }  
}

在编译后,这个泛型类的类型参数T会被擦除,变成其对应的原始类型Object:

复制代码
public class Box {  
    private Object value;  
    public void set(Object value) {  
        this.value = value;  
    }  
    public Object get() {  
        return value;  
    }  
}

使用泛型类的代码在编译后也会进行相应的类型转换:

复制代码
Box<Integer> intBox = new Box<>();  
intBox.set(10);  
Integer value = intBox.get();

编译后变成:

复制代码
Box intBox = new Box();  
intBox.set(10);  
Integer value = (Integer) intBox.get(); // 插入类型转换

泛型方法的类型擦除

复制代码
public class Util {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

编译后的字节码(伪代码):

复制代码
public class Util {
    public static void printArray(Object[] array) {
        for (Object element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

桥接方法

桥接方法是编译器为了确保多态性而自动生成的方法。这些方法在字节码中存在,但在源代码中不可见。

复制代码
public class Box<T> {
    public void setItem(T item) {
        // 方法体
    }
}

public class IntegerBox extends Box<Integer> {
    @Override
    public void setItem(Integer item) {
        // 重写的方法体
    }
}

编译后的字节码(伪代码):

复制代码
public class Box {
    public void setItem(Object item) {
        // 方法体
    }
}

public class IntegerBox extends Box {
    // 桥接方法
    public void setItem(Object item) {
        setItem((Integer) item);
    }

    // 重写的方法
    public void setItem(Integer item) {
        // 重写的方法体
    }
}

总结

本文主要讲解了为什么需要泛型,以及泛型的一些常用用法,比如泛型类、泛型接口、泛型方法、通配符等,并且给出了这些用法的代码案例。同时也介绍了泛型的类型擦除机制,讲述了为什么要类型擦除以及编译的时候是怎么做类型擦除的,同时因为类型擦除,也给出了一些泛型的使用限制。

相关推荐
武昌库里写JAVA14 分钟前
iview组件库:关于分页组件的使用与注意点
java·vue.js·spring boot·学习·课程设计
小伍_Five19 分钟前
spark数据处理练习题番外篇【上】
java·大数据·spark·scala
海尔源码26 分钟前
支持多语言的开源 Web 应用
java
dragon09071 小时前
Python打卡day49!!!
开发语言·python
摩天崖FuJunWANG1 小时前
c语言中的hashmap
java·c语言·哈希算法
LUCIAZZZ1 小时前
Java设计模式基础问答
java·开发语言·jvm·spring boot·spring·设计模式
IsPrisoner1 小时前
Go 语言实现高性能 EventBus 事件总线系统(含网络通信、微服务、并发异步实战)
开发语言·微服务·golang
hu_nil1 小时前
Python第七周作业
java·前端·python
秋水丶秋水1 小时前
电脑桌面太单调,用Python写一个桌面小宠物应用。
开发语言·python·宠物