Java泛型的使用和深度解析

Java泛型的使用和解析

什么是泛型

在Java中,泛型是一种强类型约束的机制,可以在编译期间检查数据的类型安全性,并且可以提高代码的复用性和可读性。Java在1.5时增加了泛型,据说专家们为此花费了差不多五年时间(相当不容易了),有了泛型之后我们对集合的使用就变得很规范了。

看下下面这段简单的代码,其限定了集合插入元素的类型为String

ArrayList<String> list = new ArrayList<>();
list.add("string");
list.add("qg");
System.out.println(list.get(0));

那如果没有泛型我们该如何限定集合中元素的类型呢?

我们可以使用Object数组设计应该Arraylist类模拟一下

public class Arraylist {
    //数组默认大小
    private final int DEFAULT_SIZE = 10;
    //存储元素的数组
    private Object[] elementData = new Object[DEFAULT_SIZE];
	//当前元素个数
    private int size = 0;
	
    //增加元素
    public void add(Object o) {
        elementData[size++] = o;
    }
    
    //根据索引(index)获取元素
    public Object get(int index) {
        return elementData[index];
    }

}

然后我们像Arrraylist类对象中存储数据

Arraylist list = new Arraylist();
list.add("string");
list.add(new Date());
System.out.println((String)list.get(0));

大家有没有发现两个问题?

  • Arraylist可以存放任何类型的数据(String、Date等),因为所有类都继承于Object类。
  • 从Arraylist取出数据的时候需要强制转换类型,因为编译器并不能确定你取的是字符串函数日期。

对比之下,大家应该就可以感受到泛型的优势,规范了数据的类型,不需要进行类型的强制转换,对于使用者清晰的知道集合中存储的元素类型。

泛型的使用

下面使用泛型去定义一个类和泛型方法

public class Arraylist2<T> {
    private Object[] elementData;
    private int size = 0;
    private int initialCapacity = 10;

    public Arraylist2() {
        elementData = new Object[initialCapacity];
    }

    //增加集合元素,元素的类型为T,即创建类时指定的类型
    public void add(T o) {
        elementData[size++] = 0;
    }

    //根据索引index获取元素,并转换为T类型
    public T get(int index) {
        return (T)elementData[size];
    }

    //将集合元素转换为数组,数组类型为E[],E为传入数组的元素类型
    public <E> E[] toArray(E[] a) {
        return (E[])Arrays.copyOf(elementData,size,a.getClass());
    }
}

测试代码如下

public static void main(String[] args) {
        Arraylist2<String> list = new Arraylist2<>();
        list.add("1");
        list.add("2");
        list.add("3");
        String[] strs = new String[3];
        String[] strArr = list.toArray(strs);
        for (int i = 0; i < strArr.length; i++) {
            System.out.println(strArr[i]);

        }
    }

上述代码展示了定义泛型方法和泛型类的基本使用方法,可以看出泛型可以让方法接受任意类型的参数,并作出相同的动作,这就可以让方法的普适性更高,更加灵活。

泛型限定符 extends

首先说一下 extends 的作用

​ 例如 <T extends Number>,表示T只能接受 Number 或 Number 的子类。使用上限通配符可以提高程序的类型安全性。

在解释这个限定符前,我们假设有三个类

class A {
    public String toString() {
        return "father";
    }
}

class B extends A {
    public String toString() {
        return "my";
    }
}

class C extends B {
    public String toString() {
        return "son";
    }
}

我们重新定义一下Arraylist类的泛型

class Arraylist<T extends B> {
	//同上.....
}

此时我们像Arraylist的实例对象中添加A对象就会报错,添加B对象和C对象则不会报错,因为extends限定了 T 这个泛型只能继承于B或者是B,即B是T的父类或者T,即定义了T的上限为B类。

泛型通配符 ?

通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 extends 限定子类,也可以使用关键字 super 限定父类。

通配符用于表示某种未知的类型,例如 List<?> 表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作**(因为你不知道List中存储的元素类型,如果让你添加元素,元素类型不一致,则不安全)**。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。

使用通配符可以使方法更加通用,同时保证类型安全。

例如,定义一个泛型方法:

public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.print(obj + " ");
    }
    System.out.println();
}

这个方法可以接受任意类型的 List,例如 List<Integer>List<String> 等等。

上限通配符

泛型还提供了上限通配符 <? extends T>,表示通配符只能接受 T 或 T 的子类。使用上限通配符可以提高程序的类型安全性。

例如,定义一个方法,只接受 Number 及其子类的 List:

public static void printNumberList(List<? extends Number> list) {
    for (Number num : list) {
        System.out.print(num + " ");
    }
    System.out.println();
}

这个方法可以接受 List<Integer>List<Double> 等等。

但上限通配符接受的集合一样不可以添加元素,原因和普通通配符使用一样,因为限制的是上限,集合的类型为Number的子类或Number类,不能确定类型,添加了会有安全问题。

下限通配符

下限通配符(Lower Bounded Wildcards)用 super 关键字来声明,其语法形式为 <? super T>,其中 T 表示类型参数。它表示的是该类型参数必须是某个指定类的超类(包括该类本身)。

当我们需要往一个泛型集合中添加元素时,如果使用的是上限通配符,集合中的元素类型可能会被限制,从而无法添加某些类型的元素。但是,如果我们使用下限通配符,可以将指定类型的子类型添加到集合中,保证了元素的完整性。

因为下限通配符限定了类型为 T 或他的父类,T和T的父类都可以接受T的子类,即向上转型。

举个例子,假设有一个类 Animal,以及两个子类 Dog 和 Cat。现在我们有一个 List<? super Dog> 集合,它的类型参数必须是 Dog 或其父类类型。我们可以向该集合中添加 Dog 类型的元素,也可以添加它的子类。但是,不能向其中添加 Cat 类型的元素,因为 Cat 不是 Dog 的子类。

下面是一个使用下限通配符的示例:

List<? super Dog> animals = new ArrayList<>();

// 可以添加 Dog 类型的元素和其子类型元素
animals.add(new Dog());
animals.add(new Bulldog());

// 不能添加 Cat 类型的元素
animals.add(new Cat()); // 编译报错

需要注意的是,虽然使用下限通配符可以添加某些子类型元素,但是在读取元素时,我们只能确保其是 Object 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换,如下所示:

List<? super Dog> animals = new ArrayList<>();
animals.add(new Dog());

// 读取元素时需要进行类型转换
Object animal = animals.get(0);
Dog dog = (Dog) animal;

总的来说,Java 的泛型机制是一种非常强大的类型约束机制,可以在编译时检查类型安全性,并提高代码的复用性和可读性。但是,在使用泛型时也需要注意类型擦除和通配符等问题,以确保代码的正确性。

类型擦除

什么是类型擦除?即虚拟机不存在泛型,在类文件被编译成字节码时,会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object

我们怎么确定虚拟机没有泛型呢?将类文件编译后的class文件进行反编译就可以了。

编译前类文件:

class Arraylist<E> {
    private Object[] elementData;
    private int size = 0;

    public Arraylist(int initialCapacity) {
        this.elementData = new Object[initialCapacity];
    }
    
    public boolean add(E e) {
        elementData[size++] = e;
        return true;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
}

反编译后的文件:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   Arraylist.java

package com.qg

import java.util.Arrays;

class Arraylist
{

    public Arraylist(int initialCapacity)
    {
        size = 0;
        elementData = new Object[initialCapacity];
    }

    public boolean add(Object e)
    {
        elementData[size++] = e;
        return true;
    }

    Object elementData(int index)
    {
        return elementData[index];
    }

    private Object elementData[];
    private int size;
}

大家可以看到 泛型 E消失了,取而代之的是Object

既然如此,那如果泛型类使用了限定符 extends,结果会怎么样呢?

结果是 类中的E都会被替换为Number。

那泛型擦除会给我们带来什么问题吗?

当然也会有

public class Cmower {
    
    public static void method(Arraylist<String> list) {
        System.out.println("Arraylist<String> list");
    }

    public static void method(Arraylist<Date> list) {
        System.out.println("Arraylist<Date> list");
    }

}

在以上这种情况下,我们会认为两个方法的参数是不同类型,方法可以编译通过,但因为类型擦除的存在,两个方法的参数被编译后类型都是object,编译器会出现编译异常,即无法通过编译。

欢迎大家指正!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值