Java体系中的泛型


1. 泛型

一般的类和方法,只能够使用基本类型,要么是自定义的类,如果要编写可以应用于多种数据类型的代码,这种刻板的限制对代码的约束就会很大,那么如何实现可应用于多种数据类型的代码,而不局限于单一一种呢?

答:参数化数据类型,将我们需要的数据类型指定为一个参数。在Java中实现类型参数化的方法是利用泛型。

1.1 什么是泛型

#泛型(generics) 是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时检测到非法的类型数据结构。

1.2 泛型的语法

java 复制代码
//泛型类的定义语法
class 泛型类<泛型标识,泛型标识,...> {
	//修饰符 泛型标识 变量名;
}
//常用的泛型标识:T、E、K、V
  1. 类名后的<泛型标识>代表占位符,例如:< T>,代表当前类是一个泛型类

泛型标识一般使用一个大写字母表示,常用的名称有:

  1. E 表示Element(一般在集合中使用,因为集合中存放的是元素)
  2. K 表示 Key
  3. V 表示 Value
  4. T 表示 Type
  1. 泛型只能接受类,所有的基本数据类型必须使用包装类

1.3 为什么使用泛型

当我们实现一个类,类中包含一个可以存放任何类型信息的数组成员,那么这个数组类型势必是所有类的父类Object,代码如下:

java 复制代码
//MyArray.java
public class MyArray {
	public Object[] array = new Object[10];

	public Object getPos(int pos) {
		return this.array[pos]; 
	}
	public void setVal(int pos,Object val) {
		this.array[pos] = val;
	}
}

//Test.java
public class Test {
	public static void main(String[] args) {
		MyArray myArray = new MyArray();
		myArray.setVal(0,10);//存放整形
		myArray.setVal(1,"Hello World!");//存放字符串
		myArray.setVal(2,true);//存放布尔类型

		String str = myArray.getPos(1);//1),Error
		System.out.println(str);
	}
}

//1)处代码应修改为
//String str = (String)myArray.getPos(1);

我们发现自定义类中可以存放任何类型数据,但是当取出数据的时候1)处出现了编译报错,这是为什么呢?

我们可以发现,getPos()方法的返回值是Object,也就是当我们接受数据的时候需要将返回值强转成相应类型,这样才能通过编译。

虽然在这种情况下,当前数组可以存放任何类型的数据;但是当我们取某下标对应值的数据时,我们必须知道当前下标下存的是什么类型,这对于我们来说是不现实的,那么如何解决呢?

所以有了泛型,泛型的目的是:指定当前的容器持有什么类型的对象,交由编译器去检查

上述代码修改成泛型类后:

java 复制代码
//MyArray.java
public class MyArray<T> {
	public Object[] array = new Object[10];

	public T getPos(int pos) {
		return (T)this.array[pos]; 
	}
	public void setVal(int pos,T val) {
		this.array[pos] = val;
	}
}

//Test.java
public class Test {
	public static void main(String[] args) {
		MyArray<String> myArray = new MyArray<>();
		myArray.setVal(0,"Hello ");//存放字符串
		myArray.setVal(1,"World!");//存放字符串
		String str = myArray.getPos(1);//自动类型转换
		System.out.println(str);
	}
}
  1. 在<>中加入String指定了当前数组中存放String类型的数据,此时编译器会帮你实现自动类型检查,也就是说当你执行myArray.setVal(0,true);时编译器会报错,这就确保了数据存放的确切性。
  2. 在上面代码中,我们发现取数据类型的时候不用进行数据的强转了,也就是我们在泛型类中已经进行了数据的转换,换句话说编译器帮我们进行了自动类型转换

1.4 泛型类和泛型方法

1.4.1 定义

java 复制代码
//泛型类的定义语法:
class 泛型类名称 <泛型标识,...> {
	...//返回值和存放值可以由泛型标识代替
}

//泛型方法的定义语法:
修饰符 <泛型标识,...> 返回值类型 方法名(形参列表) {
	方法体...
}

1.4.2 实例

java 复制代码
//MyArray.java
//定义一个泛型类MyArray
public class MyArray<T> {
	public Object[] array = new Object[10];

	public T getPos(int pos) {
		return (T)this.array[pos]; 
	}
	public void setVal(int pos,T val) {
		this.array[pos] = val;
	}
	
	//定义一个静态泛型方法
	public static<T> void swap(T[] array,int i,int j) {
		T t = array[i];
		array[i] = array[j];
		array[j] = t;
	}
}

当泛型类和泛型方法定义出来后,我们该如何使用呢?

java 复制代码
//Test.java
public class Test {
	public static void main(String[] args) {
		//实例一个泛型类,指明类型参数
		MyArray<String> myArray = new MyArray<>();
		int[] array1 = {1,2};
		String[] array2 = {"HELLO","WORLD"};
		//调用泛型方法
		MyArray.swap(array1,0,1);
		MyArray.swap(array2,0,1);
		System.out.println("array1:" + Arrays.toString(array1));
		System.out.println("array2:" + Arrays.toString(array2));
		
	}
}
//输出结果:
array1: [2 ,1 ]
array2: [WORLD ,HELLO ]

从上述运行结果,我们发现了

对于泛型方法:

  1. 泛型方法的调用,类型可不依赖于泛型类本身,类型是通过调用方法的时候来指定的
  2. 泛型类中的使用了泛型的成员方法并不是泛型方法,只有声明了< T>的方法才是泛型方法
  3. public与返回值中间< T>非常重要,可以理解为声明此方法为泛型方法
  4. < T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
  5. 泛型方法可以不存在泛型类中,泛型方法是独立的

1.4.3 类型推导(Type Inference)

类型推导,就是编译器根据上下文推导出类型实参,从而省略类型实参的编写

也就是上述泛型方法和泛型类的编写可分为使用类型推导和不使用类型推导两种

当我们实例泛型类和使用泛型方法时

不使用类型推导:

MyArray<String> myArray = new MyArray<String>();
MyArray.<Integer>swap(array1,0,1);
MyArray.<String>swap(array2,0,1);

使用类型推导:

//泛型类使用类型推导会根据前面传入的类型参数进行推导
MyArray<String> myArray = new MyArray();

//泛型方法会根据后面传入数据类型进行推导
MyArray.swap(array1,0,1);
MyArray.swap(array2,0,1);

1.5 裸类型(Raw Type)

裸类型就是一个泛型类但没有带着类型参数的,例如:

MyArray myArray = new MyArray();

泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为--类型擦除。

1.5.1 擦除机制

#擦除机制 : 在编译的过程当中,将所有的泛型标识(T等)替换为Object这种机制

Java的泛型机制是在编译期间 实现的,编译器生成的字节码文件.class在运行期间并不包含泛型的类型信息,换句话说就是在进入JVM前,与泛型相关的信息会被擦除掉。

java 复制代码
//Test.java
public class Test {
	public static void main(String[] args) {
		MyArray<String> arrString = new MyArray<>();
		MyArray<Integer> arrInteger = new MyArray<>();
		System.out.println(arrString.getClass() == arrInteger.getClass());//true
		
	}
}
  • 我们通过getClass()方法获取两个存放不同信息的MyArray类信息进行比较,输出结果为==true。我们在<>中传入不同的数据类型,他们的类型参数不同,那为什么比较类信息的时候得到的true==呢?
    这就不能提到我们的擦除机制了,在编译期间我们借助泛型来实现自动类型检测和自动类型转换,在编译后擦除机制会将所有的泛型信息擦除,也就是在编译后上述代码类型变为了MyArray
    我们定义一个内容简单的泛型类进行讲解:
java 复制代码
//无限制类型擦除
public class Counter<T> {
	private T number;

	public T getNumber() {
		return number;
	}
	public void setNumber(T number) {
		this.number = number;
	}
}

//有限制类型擦除
//<T extends Number>是什么意思呢?我们在下面讲解泛型边界的时候进行解释
public class Counter<T extends Number> {
	private T number;
	
	public T getNumber() {
		return number;
	}
	public void setNumber(T number) {
		this.number = number;
	}
}

大多数情况下我们面对的泛型类是没有边界约束的,这时候经过编译器==擦除,我们会将所有的泛型标识T的数据类型Object;而面对有边界限制的泛型类,我们通常会将数据类型擦除==为它的上届类型。

java 复制代码
//无限制类型擦除
public class Counter {
	private Object number;

	public Object getNumber() {
		return number;
	}
	public void setNumber(Object number) {
		this.number = number;
	}
}

//有限制类型擦除
public class Counter {
	private Number number;
	
	public Number getNumber() {
		return number;
	}
	public void setNumber(Number number) {
		this.number = number;
	}
}
#类型擦除是如何执行的呢?

在我们定义了一个 MyArray< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里就让我们对上述学习的擦除机制产生了一些疑问:

  1. 不是说泛型信息在编译后就会被擦除掉吗?为什么泛型信息在擦除后能够保证我们添加数据类型对象的准确性,即如何保证我们只添加指定类型的数据类型呢?
  2. 泛型信息被擦除后我们又如何能够实现自动类型转换?

这里我们就要了解一下Java内部是如何解决这个问题的了:

  • 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并保存下来,然后再对代码进行编译,编译的同时将会进行类型擦除 ;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换
java 复制代码
public class Test {
	public static void main(String[] args) {
		MyArray<String> myArray = new MyArray<>();
		myArray.setVal(0,"Hello ");//存放字符串
		String str = myArray.getPos(0);//自动类型转换
		System.out.println(str);
	}
}
//也就是在编译后,当MyArray<String>的泛型信息被擦除后,getPos()方法会返回Object类型,但是编译器会自动插入String的强制类型转换
  • 泛型类型被擦除后,当需要使用相关的泛型信息时,编译器底层会自动实现类型转换

1.6 泛型的上界

java 复制代码
//语法
class 泛型类名称<泛型标识 extends 类型边界> {
	...
}
//这对传入的类型参数有了一定要求,要求传入类型参数必须时类型边界的子类或类型边界

例如:

java 复制代码
//MyArray.java
public class MyArray<T extends Number> {
	...
}

//Test.java
public class Test {
	public static void main(String[] args) {
		MyArray<Integer> i1;//Integer是Number子类,正确
		MyArray<String> i2;//编译错误,String不是Number子类型
	}
}
//Tip:没有指定类型边界时,可以视为T extends Object

1.7 通配符

将<>中的泛型标识替换为?,这就是通配符。

以下述代码引入:

java 复制代码
class Print<T> {
	private T message ;
	public T getMessage() {
		return message;
	}
	public void setMessage(T message) {
		this.message = message;
	}

}

public class TestDemo {
	public static void main(String[] args) {
		Print<String> message1 = new Print<>() ;
		message.setMessage("我是字符串...");
		fun(message1);
		Print<Integer> message2 = new Print<>() ;
		message.setMessage(100);
		fun(message2);//Error
	}
	public static void fun(Print<String> temp){
		System.out.println(temp.getMessage());
	}
}
//观察发现,fun()方法指定了Print<String>类型,如果我们实例一个对象泛型参数设置的是Integer,这时候当我们调用fun()方法的时候,编译器就报错了

这时候我们需要一个可以接收所有泛型类型的标识符<?>

java 复制代码
public static void fun(Print<?> temp) {
	System.out.println(temp.getMessage());
}
//这时候调用fun()方法就可以传递,任何指定的泛型数据类型了

1.7.1 通配符的上界

java 复制代码
//语法
<? extends 类>//设置通配符上限

传入的参数类型必须是该类或该类的子类

我们定义以下几个类:

java 复制代码
//Animal.java
public class Animal {
	...
}

//Cat.java
public class Cat extends Animal {
	...
}

//Dog.java
public class Dog extends Animal {
	...
}

//PetDog.java
public class PetDog extends Dog {
	...
}

我们在测试类中设计一个新的方法:

java 复制代码
public Test {
	public static void funUpper(Print<? extends Animal> temp) {
		System.out.println(temp.getMessage()):
	}
}

需要注意的是,此时通配符描述的是他可以接受任意属于或继承于Animal的类,但是我们无法在funUpper方法内部去设置任何类型的元素,因为我们无法确认类型

如:

java 复制代码
public static void main(String[] args) {
	Print<Cat> message1 = new Print<>();
	message1.setMessage(new Cat());
	funUpper(message1);
	
	Print<Dog> message2 = new Print<>();
	message2.setMessage(new Dog());
	funUpper(message2);
	//temp可以接收任意属于或继承于`Animal`的类
}

public static void funUpper(Print<? extends Animal> temp){
	//能够接受数据,明确数据上界
	Animal animal = temp.getMessage()
	System.out.println(animal);
	//不能写入数据
	//temp.setMessage(new Animal()); //Error
	//temp.setMessage(new Dog()); //Error
	//temp.setMessage(new PetDog()); //Error
	//temp.setMessage(new Cat()); //Error
	
}

解释:

此时无法在funUpper函数中对temp进行添加元素,因为temp接收的是Animal和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素。

  • <? extends T>小结:通配符的上界,不能进行数据的写入,只能进行数据的读取

1.7.2 通配符的下界

java 复制代码
//语法
<? super 类>//设置通配符上限

传入的参数类型必须是该类或该类的父类

我们在测试类中设计一个新的方法:

java 复制代码
public static void funDown(Print<? super Dog> temp) {
	//可以修改数据
	temp.setMessage(new Dog());//Dog类本身
	temp.setMessage(new PetDog());//Dog类的子类
	
}

此时通配符描述的是他可以接受任意属于DogDog的父类,在funDown中不可以接收,我们无法确认是哪个父类

错误用法:

java 复制代码
public static void funDown(Print<? super Dog> temp) {
	//可以修改数据
	temp.setMessage(new Dog());//Dog类本身
	temp.setMessage(new PetDog());//Dog类的子类
	//Dog dog = temp.getMessage();//Error,不能接收,无法确定是那个父类
	System.out.println(temp.getMessage());//可以直接输出
}
  • <? super T>小结:通配符的下界,不能进行读取数据,只能写入数据。
相关推荐
李少兄2 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝2 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖2 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s9123601012 小时前
rust 同时处理多个异步任务
java·数据库·rust
9号达人2 小时前
java9新特性详解与实践
java·后端·面试
cg50172 小时前
Spring Boot 的配置文件
java·linux·spring boot
啊喜拔牙2 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
anlogic3 小时前
Java基础 4.3
java·开发语言
非ban必选3 小时前
spring-ai-alibaba第七章阿里dashscope集成RedisChatMemory实现对话记忆
java·后端·spring
A旧城以西3 小时前
数据结构(JAVA)单向,双向链表
java·开发语言·数据结构·学习·链表·intellij-idea·idea