JavaSE-泛型

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


前言

本篇文章主要讲述JavaSE知识体系中的泛型部分的内容。


一、案例分析

为了引出泛型的使用,我们先给出一个简单的小案例,并使用传统的方式(无泛型)实现。案例如下:请使用ArrayList来存储三个Dog对象的信息,每个Dog对象包含两个属性:姓名name、年龄age,存储完毕进行遍历输出。

1.1 传统方式

先定义一个Dog类:

/**
 * Dog类定义
 */
public class Dog {
    private String name;
    private Integer age;

    public Dog() {
    }

    public Dog(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

现在我们使用ArrayList来存储Dog对象并输出(不使用泛型):

public class Client1 {
    public static void main(String[] args) {
        ArrayList dogList = new ArrayList<>();
        //添加
        dogList.add(new Dog("哮天犬", 500));
        dogList.add(new Dog("大壮", 500));
        dogList.add(new Dog("小黄", 500));
        //遍历输出
        for (Object obj : dogList) {
            //向下转型
            Dog d = (Dog) obj;
            System.out.println(d);
        }
    }
}

分析代码,可以看到上述代码最直观的问题:

  1. 频繁地进行向下转型处理:每次从容器中取出对象都需要进行一个向下转型的处理,一是比较麻烦,二是当容器中数据量比较大时会影响到程序执行的效率。
  2. 编译期间检查不出数据类型异常:因为Object类是所有Java类的父类,那么可以向容器中加入任意类的对象。现我们创建Person类如下:
public class Person {
    private String name;
    private String address;

    public Person() {
    }

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

Person类的属性为:姓名name、地址address。现在我们往容器中加入Person类的对象:

public class Client1 {
    public static void main(String[] args) {
        ArrayList dogList = new ArrayList<>();
        //添加Dog对象
        dogList.add(new Dog("哮天犬", 500));
        dogList.add(new Dog("大壮", 500));
        dogList.add(new Dog("小黄", 500));
        //添加Person对象
        dogList.add(new Person("李不苟", "地球村"));
        dogList.add(new Person("沃士人", "地球村"));
        //遍历输出
        for (Object obj : dogList) {
            //向下转型
            Dog d = (Dog) obj;
            System.out.println(d);
        }
    }
}

可以看到编译器没有出现任何错误提示:
在这里插入图片描述
现在运行程序,发现控制台输出异常信息:
在这里插入图片描述
使用传统方式无法对加入容器中的数据类型进行约束

1.2 使用泛型

话不多说,先来一波泛型的快速体验。使用泛型创建容器的代码为:

ArrayList<Dog> dogList = new ArrayList<>();

当我们想再往dogList 中添加Person类的对象时,编译器会出现错误提示且程序无法编译通过:
在这里插入图片描述
因为已经使用了泛型约束了容器dogList 只能存储Dog类的对象,所以遍历处理时也无需再进行向下转型处理:

public class Client2 {
    public static void main(String[] args) {
        ArrayList<Dog> dogList = new ArrayList<>();
        //添加Dog对象
        dogList.add(new Dog("哮天犬", 500));
        dogList.add(new Dog("大壮", 500));
        dogList.add(new Dog("小黄", 500));

        for (Dog dog : dogList) {
            System.out.println(dog);
        }
    }
}

在这里插入图片描述

二、泛型

2.1 概念

Java泛型又称为参数化类型,是JDK5.0出现的新特性,用于解决数据类型的安全性问题。Java泛型可以保证,如果程序在编译期间没有发出警告,那么运行时就不会出现类型转换异常(ClassCastException),同时,也可以使得代码更加简洁、健壮。
我们可以认为,Java中的泛型是一种数据类型,一种“表示某一种数据的数据类型”。

2.2 语法

2.2.1 声明

在接口/类中声明泛型的方式为:

  1. interface 接口名称<T,...>{}
  2. class 类名<K,V,...>{}

其中,字母T、K、V不代表值,而只是表示类型,字母的形式可以任意,一般常用T(Type)、K(Key)、V(value)等容易理解的形式。示例如下:

public interface MyInterface<T> {
}
public class Customer<K,V> {
}

2.2.2 实例化

泛型声明以后,使用时需要在接口/类名后面指定类型参数的值(类型)。示例如下:

public class Client3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        Customer<String, Integer> customer = new Customer<>();
    }
}

2.3 细节

1.public class Customer<K,V> 中的K、V只能是引用类型。如下所示:

在这里插入图片描述
2. 在指定具体的泛型类型之后,可传入该类型或者其子类类型。示例如下:

public class Animal {
}
public class Cat extends Animal{
}
public class Client3 {
    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<Animal>();
        //添加
        animals.add(new Animal());
        animals.add(new Cat());
    }
}

我们约束容器animals 的数据类型为Animal类型,但是当我们传入Cat类的实例对象时,编译器也不会出现编译错误。

3.当没有指定具体泛型类型时,默认就是Object类型(参见案例部分)。

2.4 自定义泛型类

自定义泛型类的语法形式如下:

class 类型<T,R,...> {
}

现自定义泛型类Person类如下:

public class Person<T> {
}

使用自定义泛型类的注意事项如下:

1.普通成员可以使用泛型(属性、方法等)。示例如下:

public class Person<T> {
    private T name;
    private Integer age;

    public Person(T name, Integer age) {
        this.name = name;
        this.age = age;
    }
    
    public T getName() {
        return name;
    }
}

2.使用泛型的数组,不能进行初始化(后文将会在泛型擦除章节中说明为什么不能初始化)。示例如下:
在这里插入图片描述
3.静态成员(属性、方法)不能使用泛型,示例如下:
在这里插入图片描述
4.泛型类的类型是在创建对象实例时确定的。
5.如果在创建对象时,没有指定泛型的类型,则默认为Object。

2.5自定义泛型接口

自定义泛型接口的基本语法格式为:

interface 接口名称<T, R, ...> {}

使用自定义泛型接口的注意事项如下:

1.接口中的静态成员(JDK1.8以后接口中可以有静态方法)也不能使用泛型(同自定义泛型类)。
2.泛型接口的类型在继承接口或实现接口时确定。示例如下:

public interface MyList<T> {
    void add(T item);
}
//如果继承接口时不指定类型则会出现错误
public interface MyArrayList extends MyList<String>{

}
//实现接口时同样需要指定类型
public class MyDataList implements MyList<Integer>{
    @Override
    public void add(Integer item) {
        
    }
}

3.同理,若没有指定类型则默认为Object。

2.6 自定义泛型方法

自定义泛型方法的语法格式为:

修饰符 <T,R,...> 返回类型 方法名(参数列表){}

其中<T,R,…>可称为泛型方法标识符,在方法被调用时根据所传类型确定泛型类型。
1.在普通类中可使用自定义泛型方法,示例如下:

public class Student {
    private String name;
    private Integer age;

    /*  自定义泛型方法    */
    public <T> void showInfo(T info) {
        System.out.println("info => " + info);
    }
}

在方法调用时可确定泛型类型:

public class Client {
    public static void main(String[] args) {
        Student student = new Student("cxk", 10000);
        //调用对象实例中的自定义泛型方法
        student.showInfo("我是练习时长...");
    }
}

在这里插入图片描述
2.在泛型类中同样可使用自定义泛型方法,自定义泛型方法的泛型符号可以是自定义的,也可以使用泛型类的泛型符号。示例如下:

public class Student<S> {
    private String name;
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    /*  自定义泛型方法    */
    public <T> void showInfo(T info) {
        System.out.println("info => " + info);
    }

    /*  自定义泛型方法中使用泛型类的泛型符号    */
    public <T> void showPrivateInfo(T info, S extraInfo) {
        System.out.println("info => " + info + ", extraInfo: " + extraInfo);
    }
}

调用如下:

public class Client {
    public static void main(String[] args) {
        Student<Integer> student = new Student<>("小黑子", 99);
        //Integer类型 => 9527666
        student.showPrivateInfo("我不是小黑子,我的编号是: ", 9527666);
    }
}

在这里插入图片描述

2.7 泛型通配符

Java中的泛型通配符提供了一种更为灵活的方式来处理泛型。泛型通配符主要有3种形式:

  • 无界通配符:使用符号?表示,表示可以接收任意类型。示例如下:
public class Client1 {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("cxkun");
        strList.add("xiaoheizi");
        printList(strList);
        System.out.println("---------------");
        List<Integer> integerList = new ArrayList<>();
        integerList.add(9);
        integerList.add(999);
        printList(integerList);
    }
    
    /* List<?> dataList => 支持传入任意类型的List集合 */
    private static void printList(List<?> dataList) {
    	//任意类型 => 元素取出时为Object类型
        for (Object item : dataList) {
            System.out.println("item => " + item);
        }
    }
}

在这里插入图片描述

  • 上界通配符:使用? extends Type表示,表示可以是Type类型或者其子类类型。示例如下:
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

	@Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
}
public class Client2 {
    public static void main(String[] args) {
        //Animal集合
        List<Animal> animalList = new ArrayList<>();
        animalList.add(new Animal("哺乳动物老祖宗"));
        printList(animalList);
        //Dog集合
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog("大壮"));
        dogList.add(new Dog("旺财"));
        printList(dogList);
        //Cat集合..省略
    }

    /*   上界通配符  => List<? extends Animal   */
    private static void printList(List<? extends Animal> dataList) {
        for (Object item : dataList) {
            System.out.println("item => " + item);
        }
        System.out.println("---------------------");
    }
}

在这里插入图片描述

  • 下界通配符:使用? super Type表示,表示可以是Type类型或者其父类类型。示例如下:
public class Client2 {
    public static void main(String[] args) {
        //Animal集合
        List<Animal> animalList = new ArrayList<>();
        animalList.add(new Animal("哺乳动物老祖宗"));
        printList(animalList);
        //Dog集合
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog("大壮"));
        dogList.add(new Dog("旺财"));
        printList(dogList);
    }

    /*   下界通配符  => List<? super Dog> dataList   */
    private static void printList(List<? super Dog> dataList) {
        for (Object item : dataList) {
            System.out.println("item => " + item);
        }
        System.out.println("---------------------");
    }
}

三、拓展 - 泛型擦除

Java中的泛型擦除是指Java编译器在编译阶段将泛型类型参数从代码中移除(即擦除),并将其替换为原始类型或它们的上界(若未指定上界则为Object类型),即泛型信息在编译为字节码之后就丢失了,实际的类型都当做Object类型来处理。编写示例代码如下:

public class Client3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //存入
        list.add(6);
        //取出
        Integer num = list.get(0);
    }
}

查看字节码如下所示(IDEA可安装ASM Bytecode Outline插件):
在这里插入图片描述

  • list.add(6)实际调用的是List.add(Object o):当做Object类型存储。
  • list.get(0)实际调用的是Object obj = List.get(int index):取出为Object类型,但是做了一个强制类型转换,把Object类型转为Integer类型。
  • JVM指令checkcast:用于执行类型转换的检查。该指令确保对象引用可以安全地从一种类型转换为另一种类型。如果在运行时无法执行该转换,则 checkcast 指令会抛出 ClassCastException。

现在就可以回答2.4章节中的问题:为什么定义泛型数组不能进行初始化?Java中的泛型是通过泛型擦除来实现的,在编译时泛型信息已经被擦除,并替换为它们的上界(一般为Object),即在运行时JVM并不知道泛型的具体类型信息。创建数组时,JVM会在运行时执行类型检查以确保数组元素类型的一致性,但是由于泛型擦除,泛型数组T[]在运行时实际上变成了Object[](擦除后的某种原始类型,也不一定是Object),JVM不允许将Object[]直接视为T[],因为这样做可能会破坏类型安全性,即T[] arr = new T[10],这样的代码在编译时会报错,因为编译器无法确定T的具体类型,从而无法生成正确的字节码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值