[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) {
        // 重写的方法体
    }
}

总结

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

相关推荐
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸3 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象4 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了4 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
----云烟----4 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it5 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
懒洋洋大魔王5 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq