泛型
==浅浅了解一下泛型:==适用与许多许多类型,从代码上讲,就是一种类不止一种类型使用,就是对类型实现了参数化。
==引出泛型:==实现一个类,类中有数组成员,使得可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
思路:
1、我们知道数组只能存放指定类型的元素,例如:int[] array=new int[10],String array=new String[10];
2、所有类的父类,默认为Object类,数组是否可以创建为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,10);
//编译错误
String ret= myArray.getPos(0);
//ClassCastException类型转换异常,父类转子类存在风险
String ret2= (String) myArray.getPos(0);
System.out.println(ret);
}
}
问题:以上代码实现后发现
1、任何类型数据都可以存放
2、0号下标本身是int类型,然后利用成员方法getPos获取时,成员方法以Object类型返回,在主函数中用String类型接收,之前学习向下转型中知道,父类传给子类是存在风险的(原本是猫,但是向上转型成动物,又向下转型成狗,这时就会发生编译异常),会发生ClassCastException类型转换异常。
总结:上述代码情况下,数组中可以存放任何数据,但是,更多情况下,还是希望它只能够持有一种数据类型,而不是同时持有多种类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。此时,就是把类型,作为参数传递,需要什么类型,就传入什么类型。
上述代码进行改写:(先大致了解泛型类的语法,再看代码)
class MyArray2<T>{
public T[] array=(T[])new Object[10];//one
public T getPos(int pos){
return this.array[pos];
}
public void setVal(int pos,T val){
this.array[pos]=val;
}
}
public class TestDemo1 {
public static void main(String[] args) {
MyArray2<Integer> myArray=new MyArray2<>();//two
myArray.setVal(0,10);
myArray.setVal(1,12);
int ret=myArray.getPos(0);//three
System.out.println(ret);
//代码编译报错,此时因为在注释2处指定类当前的类型,此时的注释4处,编译器会在存放元素的时候帮助我们进行类型检查
// myArray.setVal(2,"bit");//four
}
}
有几点需要解释一下:
one:类名后的代表占位符,表示当前类是一个泛型类
了解:【规范】类型形参一般使用一个大写字母表示,常用的名称有:
- E表示Element
- K表示Key
- V表示Value
- N表示Number
- T表示Type
- S、U、V等等,第二、第三、第四个类型
one中不能new泛型类型的数组(就相当于抽象类一样,不能确切是哪种类型,因此不能new新的对象)
T[] ts = new T[5] //false
T[] ts = (T[]) new Object[5]//这个表达式是否足够好,答案是未必的,这个问题一会介绍
two:类型后加入指定当前类型,<里面只能是类类型,不能是基本数据类型>
three:不需要强制类型转换–相当于拆包(后面会讲)
four:此时因为在注释2处指定类当前的类型,此时的注释4处,编译器会在存放元素的时候帮助我们进行类型检查。
使用
泛型类
语法
class 泛型类名称<类型形参列表>{
//这里可以使用类型参数
}
//还可以同时持有多种类型
class ClassName<T1,T2,T3……Tn>{
}
class 泛型类名称<类型形参列表> extends 继承类{
//可以是一种范围—这里只能使用继承类的超类或基类
}
class ClassName<T1,T2,……,Tn>extends ParentClass{
//这里只能使用以上列举的类
}
类型推导(Type Inference)
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
MyArray myArray=new MyArray<(类型实参)>();//可以推导出实例化需要实参为String
裸类型(Raw Type):
实例化对象时,不定义是什么类型的泛型
MyArray myArray=new MyArray();
默认无参,任何数据都可以,使用时可能需要强制转换
注意:我们不要自己去使用裸类型,裸类型是为了兼容老版本的API保留的机制
下面的类型擦除部分,我们会讲到编译器是如何使用裸类型的
泛型目前为止的优点:
1、就是存放元素的时候,会进行类型的检查
2、取出元素的时候,会自动帮你进行类型转换,因此不需要再进行类型的强转
为什么不能实例化泛型类型数组
根据上面所学习的,在这里我想问几个问题:
1、泛型是如何进行编译的?(面试题)
通过命令:javap -c 查看字节码文件,所有的T都是Object
在编译过程中,将所有的T替换成Object这种机制,我们称为擦除机制。
Java的泛型机制是在编译级别实现的,编译器生成的字节码在运行期间并不会包含泛型的信息,意思就是由于编译的擦除机制,使得生成的字节码文件中全部T替换成Object,自然在运行时,没有看到关于T被类型替换的痕迹。
2、那为什么.T[] ts=new T[5];是不对的
解答:编译的时候,替换为Object,不是相当于:Object[] ts=new Object了吗,这不就回到一开始出现的问题了吗,一旦访问,就会引起向下转型(不安全)
为什么不能实例化泛型类数组?
原因:根本原因还是类型的继承关系问题,Integer[]并不是Object[]的子类。虽然,Integer继承自Object,但Integer[]的直接父类是Object。即所有数组类型的直接父类都是Object,可以通过反射来验证。数组类型是写在jvm里得,就像8种基本类型,我们无法在java的标准库中找到这个类。
数组和泛型之间重要区别是它们如何强制执行类型检查,具体来说,数组在运行时存储和检查类型信息,泛型是在编译时检查类型信息。
因此,运行时T已经被Object替换,直接转给Interfer类型的数组,编译器认为是不安全。
正确方式:
正规初始化
import java.lang.reflect.Array;
/**
* Created with IntelliJ IDEA
* Description:
* User:恋恋
* Date:2022-10-18
* Time:13:36
*/
class MyArray3<T>{
public T[] array;
/**
* 通过反射创建,指定类型的数组
* clazz:指定类型
* capacity:指定容量
*/
public MyArray3(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 class TestDemo3 {
public static void main(String[] args) {
MyArray3<Integer> myArray=new MyArray3<>(Integer.class,10);
Integer[] integers=myArray.getArray();
}
}
泛型方法
泛型能够使一个方法同时兼容多种不同类型的参数,那有没有一种方法不随着参数的变换而去创建,这就是泛型方法,适用于自定义的方法,不用重载太多的方法。
静态泛型方法不用依赖对象,那如何定义她的类型呢
泛型静态方法的语法
方法限定符<类型形参列表> 返回值类型 方法名称(形参列表){
}
public static <T> void print(T str){
System.out.println(str);
}
泛型非静态方法的语法
方法限定符 返回值类型 方法名称(T 形参列表)
通配符
?用于在泛型的使用,即为通配符
通配符解决什么问题:
这个是泛型也无法解决协变的问题,协变指的就是如果Student是Person的子类,但是泛型是不支持这样的父子类关系的。
泛型T是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更加是是用于补充参数的范围。
总结:为了解决泛型方法中接送参数的不同,有可能接送泛型类类数组,这时泛型就有可能是Integer或者是String等,利用通配符就会解决泛型无法协变的问题和不同类型的问题。
例子:使用通配符
public class TestDemo1 {
//此时使用通配符“?”描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(MyArray2<?> temp){
System.out.println(temp.getPos(0));
}
public static void main(String[] args) {
MyArray2<Integer> myArray=new MyArray2<>();//two
myArray.setVal(0,10);
myArray.setVal(1,12);
fun(myArray);
}
}
通配符上界
上界只能读不能写
语法:
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
class Food{
}
class Fruit extends Food{
}
class Apple extends Food{
}
class Banana extends Food{
}
class Massage<T>{
private T massage;//定义message
public T getMassage(){//获得message
return massage;
}
public void setMassage(T massage){//初始化message
this.massage=massage;
}
}
public class TestDemo5 {
public static void fun(Massage<? extends Food> temp){//只能接收Food的本身或者子类
System.out.println("====写入时===");
// temp.setMassage(new Food());
// temp.setMassage(new Fruit());
//temp.setMassage(new Apple());
//temp.setMassage(new Banana());
//写入时,全部编译异常,原因:此时的temp只能接收Food的本身或者它的子类,当temp为Food的子类时,setMassage方法(接送了Food的实例化对象),就会产生向下转型-不安全
System.out.println("====读取时====");
Food food=temp.getMassage();
//Apple apple=temp.getMassage();--编译异常
//读取时,只能以最高的父类来接收,否则会出现向下转型
}
public static void main(String[] args) {
Massage<Apple> massage1=new Massage<>();
massage1.setMassage(new Apple());
fun(massage1);
Massage<Banana> massage2=new Massage<>();
massage2.setMassage(new Banana());
fun(massage2);
//Fruit fruit=(Fruit)massage1.getMassage();
Massage<Food> massage3=new Massage<>();
massage3.setMassage(new Food());
fun(massage3);
}
}
总结:
只能读取get,并且用最高的父类接送,否则会发生向下转型
不能写入set,有可能T是Food、或者Banana、Apple,设置的时候会不安全
通配符下界
下界只能写不能读
语法:
<? super 下界> <? super Integer> //代表可以传入的实参的类型是Integer或者Integer的父类类型
class Food0{
}
class Fruit0 extends Food0{
}
class Apple0 extends Fruit0{
}
class Message0<T>{
private T message;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo6 {
public static void fun(Message0<? super Fruit0> temp){ //?只能是Fruit0本身或者是Fruit0父类
System.out.println("========写入==============");
//temp.setMessage(new Food0());
//编译错误,原因是此时temp只可能是Fruit0本身或者是Fruit0的父类,当temp为Fruit0本身时,传递过去会产生向下转型-不安全
temp.setMessage(new Fruit0());
temp.setMessage(new Apple0());
//只可以写入Fruit0的子类或者它本身
System.out.println("========读取==============");
System.out.println(temp.getMessage());//可以直接读取
//Fruit0 fruit0=temp.getMessage();
//编译错误,原因:此时temp只可能是Fruit0本身或者是Fruit0的父类,当temp为Fruit0的父类时,传递给变量时,会发生向上转型-不安全
}
public static void main(String[] args) {
Message0<Apple> message0=new Message0<>();
message0.setMessage(new Apple());
//fun(message0);--编译错误
// message0类型是Apple,不符合fun的方法
Message0<Fruit0> message2=new Message0<>();
message2.setMessage(new Fruit0());
fun(message2);//message2类型是Fruit本身,符合fun参数的要求
Message0<Food0> message3=new Message0<>();
message3.setMessage(new Food0());
fun(message3);//message3类型是Fruit的父类,符合fun参数的要求
}
}
总结:当限制参数条件了通配符的下界时,成员方法只能接收本身或者父类;写入时,只能写入本身或者子类;读取时,只能直接读取(sout)。
只能写入,不能读取
总而言之,无论是通配符上界或者下界,只需知道它的判定标准是是否为向下转型,若为向下转型,则编译异常。总之,知道通配符是如何利用即可。
泛型的上界
在定义泛型类时,对传入的类型变量做一定的约束,可以通过类型边界来约束
语法
class 泛型类名称<类型形参 extends 类型边界> {
……
}
实例
public class ClassName < E extends Number >{
}
只接送Number的子类型作为E的类型实参
ClassName < Integer >
若没有指定类型边界E,可以默认为E extends Object
还有复杂实例:
public class MyArray < E extends Comparable< E >>{
}
E必须是实现了Comparable接口的
包装类
在java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,java给每个基本类型都对应了一个包装类型。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Flat |
double | Double |
char | Character |
boolean | Boolean |
装包和拆包
装包
基本数据类型变成包装类
//手动装包
int i=10;
Integer i1=Integer.valueOf(i);
Integer i2=new Integer(i);
//自动装包
Integer i3=i;
Integer i4=(Integer)i;
拆包
包装类变成基本数据类型
//手动拆包
Integer j=20;
int j1=j.intValue();
//自动拆包
int j2=j;
int j3=(int)j;
【面试题:】
public class TestDemo7 {
public static void main(String[] args) {
Integer a=127;//涉及到了装包
Integer b=127;
Integer c=128;
Integer d=128;
System.out.println(a==b);
System.out.println(c==d);
}
}
这个Integer a=127,涉及了自动装包,也相当于手动装包Integer a=Integer.valueOf(127)。
我们来分析以下源代码:
从这个可以知道cache是个静态final修饰的数组,静态说明数组的大小在运行的时候不变,final说明数组类型不变且指向的对象不能改变,是Integer类型。
cache=new Integer[high- -128 +1]说明cache是长度为127+128+1=256的数组。
在谈谈valueOf函数:
如果装包的数字是-128~127以内,则直接存入到数组中,若不在以内的化,就重新new一个对象。
因此,上面127是ture,128是false
这样有个优点是,频繁使用到的小数据,不用每次都new新对象,只需从字符串中取出即可