• Java——泛型与通配符的详解


    定义

    对于一般的类和方法,我们只能将其用在特定的数据类型。因此JDK1.5引入了泛型,其可将代码适用于多种数据类型

    为何不能用Object

    class MyArray {
    	public Object[] array = new Object[10];
    	public Object getPos(int pos) {
    	return this.array[pos];
    	}
    
    	public void setVal(int pos,int val) {
    		this.array[pos] = val;
    	}
    }
    
    public class TestDemo {
    	public static void main(String[] args) {
    		MyArray myArray = new MyArray();
    		myArray.setVal(0,1);
    		myArray.setVal(1,"hello");//可以存放
    		String ret = myArray.getPos(1);//编译报错
    		System.out.println(ret);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们发现想要用Object来实现存储不同的数据类型时,虽然可以存储进去,但是当我们想要拿出来的时候,我们并不知道该数组下标所存放的元素类型,并且拿出来的时候即使是一个字符串类型,也需要我们进行强制类型转换,否则就会报错。
    因此,泛型的目的就是让容器持有的对象类型让编译器进行检查

    语法

    class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
    // 可以只使用部分类型参数
    }
    
    • 1
    • 2
    • 3

    根据上面的语法规则,我们可以对之前的代码进行改进

    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 class TestDemo {
    	public static void main(String[] args) {
    		MyArray<Integer> myArray = new MyArray<>();
    		myArray.setVal(0,10);
    		myArray.setVal(1,12);
    		int ret = myArray.getPos(1);
    		System.out.println(ret);
    		myArray.setVal(2,"hello");//报错
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    规则:一般用以下大写字母来表示形参
    E 表示 Element
    K 表示 Key
    V 表示 Value
    N 表示 Number
    T 表示 Type
    S, U, V 等等 - 第二、第三、第四个类型

    需要注意的是,在Java中不能new泛型类型的数组

    T[] t = new T[5];//error
    
    • 1

    我们可以用以下代码进行替换

    T[] array = (T[])new Object[10];
    
    • 1

    但是上述代码也不是十全十美的,在接下来的内容中将进行讲解

    使用

    我们用以下语法使用泛型类

    泛型类<类型实参> 变量名; // 定义一个泛型类引用
    new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

    例如:

    MyArray<Integer> list = new MyArray<Integer>();
    
    • 1

    需要注意的是:
    泛型类的尖括号中需要使用包装类,不能用基本类型
    第二个尖括号中的内容可以进行省略

    裸类型

    也就是一个泛型类但是没有带着类型,是为了兼容老版本而遗留下来的语法规则,我们自己尽量不要使用

    MyArray list = new MyArray();
    
    • 1

    泛型的编译

    擦除机制

    在字节码文件中,并没有T这类的符号出现,而都是Object,这种编译方式称之为擦除机制
    具体内容可以参考下面这篇文章
    https://zhuanlan.zhihu.com/p/51452375

    不能实例化泛型数组的原因

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当我们运行上述代码时,会报以下的错误:
    Exception in thread “main” java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; at test.main(test.java:18)

    报错的意思是:我们将Object的数组给Integer数组引用
    数组是在运行的时候检测类型的匹配问题,而泛型是在编译时检测

    也就是说,返回的Object数组中可能存放任意类型的数据,而把这些数据直接赋值给Integer类型的数组是不安全的,因此编译器报错。
    所以,我们之前的定义泛型数组的方法并不是完美的,我们应该用反射的方法来定义

    class MyArray<T> {
    	public T[] array;
    	
    	public MyArray() {
    	}
    	public MyArray(Class<T> clazz, int capacity) {
    		array = (T[])Array.newInstance(clazz, capacity);
    	}
    	
    	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.class,10);
    	Integer[] integers = myArray1.getArray();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    泛型的上界

    语法

    class 泛型类名称<类型形参 extends 类型边界> {

    }
    我们用extends关键字来定义泛型的上界,这样的话用户在传泛型参数的时候只能传上界及其子类

    public class MyArray<E extends Number> {
    ...
    }
    
    • 1
    • 2
    • 3

    而当我们的类型边界写的是一个接口时,那么用户传的参数必须是实现了该接口的

    public class MyArray<E extends Comparable<E>> {
    ...
    }
    
    • 1
    • 2
    • 3

    E必须实现Comparable接口

    泛型方法

    语法

    方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { … }

    public class Util {
    	public static <E> void swap(E[] array, int i, int j) {
    		E t = array[i];
    		array[i] = array[j];
    		array[j] = t;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在使用这个方法时,我们可以直接传类型参数,也可以不传,让编译器进行类型推导

    Integer[] a = { ... };
    swap(a, 1, 2);
    
    Integer[] a = { ... };
    Util.<Integer>swap(a, 1, 2);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通配符

    由于泛型无法解决协变问题,即泛型之间没有父子类关系,泛型就是传什么参数就是什么参数。因此我们用通配符可以实现父子类关系,使参数范围更广

    class Message<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) {
    		Message<String> message = new Message() ;
    		message.setMessage("hello world");
    		fun(message);
    	}
    	public static void fun(Message<String> temp){
    		System.out.println(temp.getMessage());
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    如果我们使用上述代码时,当我们把T改为Integer时,那么fun函数没法进行相对应的调整
    因此,这时我们可以使用通配符?

    public class TestDemo {
    	public static void main(String[] args) {
    		Message<Integer> message = new Message() ;
    		message.setMessage(55);
    		fun(message);
    	}
    
    	public static void fun(Message<?> temp){
    		//temp.setMessage(100); 无法修改
    		System.out.println(temp.getMessage());
    	}
    }
    由于不确定传入通配符的类型,因此无法修改值,只能获取值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通配符的上界

    和泛型的上界类似

    <? extends 上界>
    <? extends Number>//可以传入的实参类型是Number或者Number的子类
    
    • 1
    • 2

    实例:

    class Food {
    }
    class Fruit extends Food {
    }
    class Apple extends Fruit {
    }
    class Banana extends Fruit {
    }
    
    class Message<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) {
    		Message<Apple> message = new Message<>() ;
    		message.setMessage(new Apple());
    		fun(message);
    		Message<Banana> message2 = new Message<>() ;
    		message2.setMessage(new Banana());
    		fun(message2);
    	}
    	public static void fun(Message<? extends Fruit> temp){
    		//temp.setMessage(new Banana()); //无法修改
    		//temp.setMessage(new Apple()); //无法修改
    		System.out.println(temp.getMessage());
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    由于不确定传入类型是fruit的哪个子类,因此无法set
    因此,通配符确定上界一般用来读取数据,而不是写入数据

    通配符的下界

    我们用super关键字来描述通配符的下界

    <? super 下界>
    <? super Integer>//可以传入的实参的类型是Integer或者Integer的父类类型
    
    • 1
    • 2

    那么,在刚才的例子中,我们的fun方法有如下特点

    public static void fun(Message<? super Fruit> temp){
    	// 此时可以修改添加的是Fruit 或者Fruit的子类
    	temp.setMessage(new Apple());//这个是Fruit的子类
    	temp.setMessage(new Fruit());//这个是Fruit的本身
    	//Fruit fruit = temp.getMessage(); 不能接收,无法确定是哪个父类
    	System.out.println(temp.getMessage());//只能直接输出
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因此,通配符确定下界一般用来写入数据,而不是读取数据

    包装类

    为了支持泛型,Java中的基本类型都有自己对应的包装类

    基本数据类型包装类
    byteByte
    shortShort
    intInteger
    longLong
    floatFloat
    doubleDouble
    charCharacter
    booleanBoolean

    装箱

    手动装箱

    int i = 1;
    Integer ii = Integer.valueOf(i);
    Integer ij = new Integer(i);
    
    • 1
    • 2
    • 3

    自动装箱

    int i = 1;
    Integer ii = i; 
    Integer ij = (Integer)i; 
    
    • 1
    • 2
    • 3

    拆箱

    手动拆箱

    int j = ii.intValue();
    
    • 1

    自动拆箱

    int j = ii; 
    int k = (int)ii; 
    
    • 1
    • 2
  • 相关阅读:
    PLC梯形图实操——风扇正反转
    04-JS函数
    Linux下如何操作寄存器
    java并发编程学习一——Thread
    github.com/yuin/gopher-lua 踩坑日记
    开发过程中常见数据库。
    15、JAVA入门——封装
    C#上位机与PLC
    降价背后,函数计算规格自主选配功能揭秘
    Java基础实现加油站圈存机系统
  • 原文地址:https://blog.csdn.net/m0_60867520/article/details/125159422