[java]泛型和通配符

一、泛型类的前世

在泛型还没有出现以前,我们如果想让一个类中的一个数组能够根据需要存储各种类型的变量,我们会将这个数组定义为一个Object数组,因为Object类是所有类型的父类,Object的引用能接受所有类型的实例化对象;

class ArrayList{

    Object[]arr=new Object[10];

    public Object get(int index){
        //返回特定下标的object对象
        return arr[index];
    }
    public void set(int index,Object data){
        arr[index]=data;
    }
   
}
public class Main{
    public static void main(String[] args){
        ArrayList a=new ArrayList();
        
        a.set(1,new String());//向下标为1的位置放入一个String类型对象
        
    }
}

上面的代码就向这个Object数组中放入了一个String类对象。

但是此时使用arr这个数组来访问数组元素也只能访问object类的属性,为了能访问到String类的特殊属性,我们需要在每次使用这个Object引用强制类型转换成String类型的变量

class ArrayList{

    Object[]arr=new Object[10];
    public Object get(int index){
        return arr[index];
    }

    public void set(int index,Object data){
        arr[index]=data;
    }
}
public class Main{
    public static void main(String[] args){
        ArrayList a=new ArrayList();
        
        a.set(1,new String());//向下标为1的位置放入一个String类型对象
        
        String str=(String)a.get(1);//需要手动的将object对象强制转换成String对象
    }
}

上面main方法中get返回的object对象被强制类型转换后就可以重新赋值给String类型的引用,并且使用String的成员方法和属性;我们只能将其他类型的对象赋值给object类型,且赋值后的object对象只能被转换回去原本的类型。

所以,如果Object的类型转换是正确的还好,要是将一个String类型的Object对象转换成一个Integer类型的对象,代码就会报错。因此,实际使用Object进行对象转换我们还需要都对每个Object对象的实际类型进行记忆,这增加了我们编写代码的难度,也使得编码容易出错

类型转换异常代码:

class ArrayList{

    Object[]arr=new Object[10];
    public Object get(int index){
        return arr[index];
    }
    
    public void set(int index,Object data){
        arr[index]=data;
    }
}
public class Main{
    public static void main(String[] args){
        ArrayList a=new ArrayList();
        
        a.set(1,new String());//向下标为1的位置放入一个String类型对象
        
        String str=(String)a.get(1);//需要手动的将object对象强制转换成String对象
        Integer i=(Integer)a.get(1);//报错,类型切换异常
    }
}

值得注意的是这是一个运行时异常,就是在运行时才会报错,写出这样的代码在idea内不是不会直接标红的,这和泛型类是不同

所以出现了泛型类,泛型类的最大功能就是编译器自动帮我们进行上述的类型转换和类型匹配,减轻了编写代码的负担

首先,我们需要了解什么是泛型,泛型你可以理解为是一个容器,这个容器可以装入且只能装入你指定的类型变量

一、泛型类的使用

泛型类的使用语法为:

类名<类型参数> 引用名=new 类名<类型参数>();

//泛型类实例化
ArrayList<Strig> arr=new ArrayList<String>();
//非泛型类实例化
ArrayList arr=new ArrayList();

可以看出,泛型类和非泛型类的区别仅仅是在泛型类名后面加上了尖括号和尖括号内部的类型参数

泛型类对象的创建还可以简化,根据自动类型推导

由于泛型类型在创建引用的时候就已经传入了参数,所以编译器可以根据自动类型推导将

ArrayList<String> arr=new ArratLIst<String>();中第二个尖括号内的String省略

如:

ArrayList<Strnig> arr=new ArrayList<>();

二、泛型类的编写,如何自己定义一个泛型类

相比于简单的泛型类型的使用,泛型类型的创建会有很多的注意事项

首先我们要了解泛型创建的语法

//只在类名后面加上尖括号,在尖括号内传入要使用的类型参数
class Test<T1,T2>{
    //这里可以使用传入的参数T1和T2
}
//泛型类可以继承泛型类
class Test<T> extends ArrayList<T>{
    
}
//泛型类可以实现泛型接口

class Test<T> implements CompareTo<T>{
    
}

 下面是一个泛型创建的示例代码,里面有几个错误,会在下面一一解答

class MyArray<T> {
  public T t=new T();//一号错误
  public T[] array2=new T[10];//二号错误
  public T[] array1 = (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();
}

首先我们需要介绍泛型类的背景知识,java为了实现泛型类,使用了类型擦除这种机制,具体的就是在编译的时候,泛型类中的所有T对编译器来说都会被擦除为Object,也就是说上述代码最终实际在编译器中被编译器看到的是

class MyArray<T> {
  public Object t=new Object();//一号错误
  public Object[] array2=new Object[10];//二号错误
  public Object[] array1 = (Object[])new Object[10];//三号错误
  
  
  public Object getPos(int pos) {
    return this.array[pos];
 }
  public void setVal(int pos,Object val) {
    this.array[pos] = val;
 }
  public Object[] getArray() {
    return array;
 }
}
public static void main(String[] args) {
  MyArray<Integer> myArray1 = new MyArray<>();
  Integer[] strings = myArray1.getArray();
}

看到这里你可能会疑问,为什么要将这些类型全部擦除成Object,都擦除成Object了,那还能实现泛型类的作用吗???

(1)为什么要擦除成Object类型

这是一个历史遗留问题,总的来说,java是一个年轻的语言,起初泛型并没有在java的语法里面,泛型其实是java5之后才有的概念,那再没有泛型之前,就像上面说的,java程序员就只能使用Object类型来创建一些和泛型类功能相似的代码,在java5泛型开始设计时,java设计师必须考虑到代码兼容的问题,也就是说,在java5版本更新之后,之前的老版本代码在新版本也要能运行,甚至引的库方法,使用库方法的方式都能保持兼容,这些老版本都是使用的Object类型,所以java擦除成Object,此外,object是所有类型的父类,擦除成Object也能方便处理各种类型

(2)擦除成Object了,那还能实现泛型类的作用吗???

这就要和泛型类的作用进行联系了,泛型类的作用就是进行静态类型检查和自动类型转换,总的来说,泛型提供了类型参数这个变量T,T指代了将会被传入泛型的类型,我们可以再泛型类代码内部使用T来假定真正被传入的类型,编译器根据假设T是一个类型进行静态的类型检查和检查类型转换是否合法,然后再自动的进行类型转换,这里的“自动类型转换”也不是那么的自动,在泛型代码内部还是需要手动的切换类型的,如:

MyClass<T>{
    Object o;
    public void set(T data){
        o=data;
    }
    public T get(){
        return  (T)o;
    }
}

上述代码中,它只是在将一个不是T类型的数据返回时,根据写的代码,将o“自动转换”成T类型,这是由于我们将o手动的转换为了T类型,但是在外部使用者看来,使用泛型类并没有让使用者自己进行类型的转换,是泛型类自动的进行了类型转换,这不仅方便了使用者,还不会出错。

综上所诉,T就是一个类型占位符,它在泛型类内部代替了实例化时会被传入的类型参数,使得我们在设计一个类的时候就能方便的进行类型转换和类型检查。

了解的擦除机制后,我们就可以再来说这三个错误

(1)一号错误和二号错误

public T t=new T();//一号错误

 根据java5的开发人员写的博客,在泛型中不能使用这样的语法,java的类型匹配是静态的,所以它不能静态的知道被传入T是否具有一个无参构造函数,这就像我们平时如果用一个实例调用方法时将方法名写错一样,java找不到这个方法,所以报错

据此我们可以退出,在泛型类中我们不能使用甚至不能正常的创建一个T类型的实例,因为java并不能静态的知道这个T到底有哪些构造函数

所以二号错误创建T类型数组的实例也错

public T[] ts=new T[10];

(2)三号错误

  public T[] array1 = (T[])new Object[10];//三号错误

三号错误在很多时候被认为是正确的代码,他创建了一个T类型的数组,我们可以往这个数组里面传入T类型的变量,也能正确的从这个数组中正确的拿到这个T类型变量,但是这个数组本质上是一个Object数组,他并没有被切换成T类型的数组,这样写也被java设计人员认为是不安全的,因为将一个Object类型数组强行转换成一个T[]类型是不安全的,除非这个T是Object,否则会报类型转换异常;但是上述代码在实际运行过程中并没有直接就抱异常,除非你将array1的数组类型返回出来,然后放入一个真正的T[]类型的数组引用才会报错

在创建泛型子类的时候,还能使用<T extend 类型名>,<T super 类型名>,来替换<T>

其中<T extend 类型名>代表T类型必须是后面这个类型的子类,此时擦除机制将不在把T擦除为Object,而是擦除为这个父类,在这样的一个类中,还是不能创建一个T类型的实例,

<T super 类型名>代表T必须是后面这个类的子类,此时就擦除机制会将T擦除为他的后面这个类,由于现在T相对于后面这个类是父类了,此时代码中可以创建T类型变量和数组了

通配符

通配符是在写方法为了传入一个泛型类对象而设计的形参写法,如:

public void func(MyClass<?> myClass){}

由于在写方法时,方法并不知道被传入的泛型类使用了什么类型参数,所以使用?来代替所有的类型参数

如果?不加extends或者super修饰,那这个形参能接受所有类型做参数的泛型实例

也可以向T一样加修饰

public void func1(MyClass<? extends String> myClass){}
//?是String的子类
public void func2(MyClass<? super String> myClass){}
//?是String的父类

加了修饰之后表示接收的类型参数只能是某个类本身和某个类的子类或者父类

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值