提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
本篇文章主要讲述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);
}
}
}
分析代码,可以看到上述代码最直观的问题:
频繁地进行向下转型处理
:每次从容器中取出对象都需要进行一个向下转型的处理,一是比较麻烦,二是当容器中数据量比较大时会影响到程序执行的效率。编译期间检查不出数据类型异常
:因为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 声明
在接口/类中声明泛型的方式为:
interface 接口名称<T,...>{}
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的具体类型,从而无法生成正确的字节码。