【JavaSE】泛型

系列文章目录


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

        学习泛型之前请大家先详细地了解一下,关于Java当中的包装类和Object类的概念以及包装类当中的自动装箱和自动拆箱是什么,如果这部分内容不理解的话,泛型当中的很多细节可能无法理解,这篇文章集中讲解泛型当中的重难点,对于简单的细节不做介绍。


一、什么是泛型

1、基础概念

        泛型是一种在编程语言中实现参数化类型的机制,它允许在定义类、接口、方法时使用类型参数,以便在使用时指定具体的类型。Java中引入了泛型的概念,它提供了编译时的类型检查,并允许在编写通用代码时指定具体的数据类型。

2、主要特点

        1)参数化类型:泛型允许类、接口、方法在定义时使用参数,这些参数可以在使用时被具体的类型替代。

        2)类型安全:泛型提供了编译时的类型检查机制,避免了在运行时出现类型转换异常。这样,开发者在编写代码时就能发现并修复类型相关的错误。

        3)代码复用:泛型增加了代码的灵活性和复用性。通用的算法或数据结构可以用于处理各种类型的数据,而不需要为每种类型都编写相似的代码。

3、泛型语法

        在Java中,泛型使用尖括号(<>)来声明类型参数。例如:

public class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

        在上面的例子中,Box 是一个泛型类,其中 表示类型参数。T 是一个占位符,它会在实例化时被具体的类型替代。

        使用泛型的例子如下:

// 创建一个存储整数的Box对象
Box<Integer> integerBox = new Box<>(42);
int intValue = integerBox.getValue(); // 不需要进行类型转换,编译器已经检查过了

// 创建一个存储字符串的Box对象
Box<String> stringBox = new Box<>("Hello, Generics!");
String stringValue = stringBox.getValue(); // 同样不需要进行类型转换

        在这个例子中,Box 类被实例化两次,分别用于存储整数和字符串,而不需要为每一种类型编写不同的类。这样就实现了通用性和代码复用。泛型的强大之处在于它可以适用于各种数据类型,提高了代码的灵活性和可维护性。


 二、为什么需要泛型

1、引出泛型:

        我们可以通过引入泛型来实现一个通用的类,使其能够存放任何类型的数据,并提供方法获取数组中指定下标的值。以下是一个简单的示例:

public class GenericArray<T> {
    private T[] array;

    // 构造方法,初始化泛型数组
    public GenericArray(int size) {
        // 创建泛型数组的通用方式是使用 Object 类型的数组,然后进行强制类型转换
        this.array = (T[]) new Object[size];
    }

    // 设置数组指定下标的值
    public void set(int index, T value) {
        array[index] = value;
    }

    // 获取数组指定下标的值
    public T get(int index) {
        return array[index];
    }

    public static void main(String[] args) {
        // 创建一个存放整数的数组
        GenericArray<Integer> intArray = new GenericArray<>(5);
        intArray.set(0, 42);
        intArray.set(1, 15);

        // 获取数组中指定下标的值
        int valueAtIndex1 = intArray.get(1);
        System.out.println("Value at index 1: " + valueAtIndex1);

        // 创建一个存放字符串的数组
        GenericArray<String> stringArray = new GenericArray<>(3);
        stringArray.set(0, "Hello");
        stringArray.set(1, "Generics");

        // 获取数组中指定下标的值
        String valueAtIndex0 = stringArray.get(0);
        System.out.println("Value at index 0: " + valueAtIndex0);
    }
}

        在这个示例中,GenericArray 类是一个泛型类,它使用类型参数 T 表示数组中元素的类型。构造方法中创建了一个 Object 类型的数组,然后进行强制类型转换。通过泛型,我们可以在使用该类时指定存放的数据类型,从而实现了通用性。

 2、泛型擦除

        Java中的擦除机制(Type Erasure)是指在编译时期,泛型类型信息会被擦除,T这个占位符会被全部替换成为Object或者是它的上界,而在运行时,Java虚拟机将不再保留泛型类型的具体信息。这是为了向后兼容性,因为在引入泛型之前的Java版本中,是没有泛型概念的。

        1)类型参数擦除:在编译时,泛型类型的类型参数将被擦除,替换为Object,例如对于List,编译后的代码中的类型信息将被替换为List<Object>。

        2)泛型数组限制:由于擦除机制,不能直接创建泛型数组。例如,以下代码是非法的:

List<String>[] arrayOfLists = new ArrayList<String>[10]; 

        编译器会发出警告,因为在运行时,无法获得泛型数组的确切类型信息。通常,可以使用原始类型的数组,然后进行强制类型转换,但这可能导致运行时异常。

擦除机制的主要目标是确保与没有泛型的旧代码的兼容性,并允许在运行时处理泛型代码。然而,擦除也带来了一些挑战,尤其是在编写需要在运行时访问泛型类型信息的代码时。在这种情况下,通常需要使用反射或其他技术来处理擦除后的类型。

在Java中,T[] ts = new T[5]; 是不允许的,因为直接创建泛型数组是违反Java语言规范的。泛型在Java中是通过擦除实现的,即在运行时泛型信息会被擦除,编译器会将泛型类型替换为原始类型或其边界。

T[] ts = new T[5];

        那么在运行时,它实际上会被替换为:

Object[] ts = new Object[5];

        这似乎是合理的,但问题在于泛型的目的是提供类型安全性,而直接使用 Object[] 会失去泛型的好处。你可以轻松地将任意对象放入 Object[] 中,而不会得到编译器的警告。这就违背了使用泛型的初衷,因为泛型的目标是在编译时提供更严格的类型检查。

因此,为了维护类型安全性,Java禁止直接创建泛型数组。相反,你应该使用集合类或其他类型安全的数据结构,或者在需要时使用非泛型数组,并在运行时进行必要的类型转换。

3、为什么不能实例化泛型数组

        因为一个T类型的数组也就是T[] array = new T[];或者:

class MyArray<T> {
public T[] array = (T[])new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
Integer[] strings = myArray1.getArray();
}

        泛型擦除之后,这个泛型又没有设置上界(泛型的上界我们一会介绍),T占位符全部被替换成Object,也就是可以是任意类型,这个数组里面可以存放任意类型,编译器认为这是不安全的,而且数组存放什么内容是运行时决定的,编译器无法对其进行限制,所以Java的编译器不允许程序员做这样危险的操作。

        尤其是这行代码:

Integer[] strings = myArray1.getArray();

        这个数组里面可能存放了任意类型的数据,而这个时候又要把这个数组赋值给一个Integer类型的数组,编译器一定报错。


三、泛型上界

        在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
1、泛型语法
        
class 泛型类名称<类型形参 extends 类型边界> {
...
}

 2、实例

public class MyArray<E extends Number> {
...
}
        只接受 Number 的子类型作为 E 的类型实参
MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型

        如果没有指定类型边界 E,可以视为 E extends Object。


四、泛型方法

        泛型方法是一种在方法级别使用泛型的方式,允许在方法中使用具有独立类型参数的泛型。通过泛型方法,我们可以编写更灵活、通用的代码,而不受限于整个类的泛型类型参数。下面是一个简单的泛型方法的例子:

public class GenericMethodExample {
    // 泛型方法,接收一个数组并返回数组中的最大值
    //这里是泛型的上界代表着这个方法必须实现Comparable接口
    public static <T extends Comparable<T>> T findMax(T[] array) {
        if (array == null || array.length == 0) {
            return null;  // 返回 null 表示数组为空
        }

        T max = array[0];
        for (T element : array) {
            if (element.compareTo(max) > 0) {
                max = element;
            }
        }

        return max;
    }

    public static void main(String[] args) {
        // 使用泛型方法找到整数数组的最大值
        Integer[] intArray = {3, 7, 1, 9, 4};
        Integer maxInt = findMax(intArray);
        System.out.println("Max Integer: " + maxInt);

        // 使用泛型方法找到字符串数组的最大值
        String[] stringArray = {"apple", "orange", "banana", "grape"};
        String maxString = findMax(stringArray);
        System.out.println("Max String: " + maxString);
    }
}

        findMax()方法是一个泛型方法,使用 > 来声明泛型类型参数。这表示传入的类型 T 必须实现 Comparable 接口(泛型的上界),以便进行比较。

方法使用了泛型类型参数

        T 来定义数组和返回值的类型。这样,findMax 方法可以接受不同类型的数组,包括整数、浮点数、字符串等。

        在 main 方法中,我们分别使用 findMax 方法找到整数数组和字符串数组的最大值。

通过使用泛型方法,我们可以避免为每个类型都编写一个专门的方法,使代码更加灵活和通用。泛型方法通常在集合类、算法类等地方得到广泛应用。


 总结

        泛型的学习主要是为了我们更好地学习数据结构,所以这部分的内容大家会用就可以,主要通过上手实践来解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清灵白羽 漾情天殇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值