从本质上分析Java泛型

现在网上讲泛型的一大堆,但是很多人要么讲一下语法,要么讲几个注意点,读者无法深入了解泛型的本质!所以这篇文章从泛型的起源,本质上,以通俗易懂的方式讲解Java的泛型!

Java的泛型的起源

泛型是在java1.5时从c++中借鉴的!在1.5之前是没有泛型这个概念的!为什么要引入泛型呢?因为当时有以下几个问题:

问题一:

那个时候的集合类是这样的:

ArrayList list = new ArrayList();
list.add(new String("我是字符串!"));
list.add(new Integer(10));

我们可以往一个ArrayList中添加任何类型对象,为什么是这样设计的呢?

因为,我们这个世界有无数的对象,如果为每个对象都单独设置一个集合类,那是根本不可能的!所以集合类中,巧妙的运用了java多态,设置了一个Object类,用来接收所有的对象!我们来阅读一下ArrayList类的源码:

原来是设置了一个Object类空数组,当我们调用这个无参构造器的时候,会创建一个名为elementData的变量,这个变量会引用之前的静态Object

所以,我们是可以往ArrayList中添加任何我们想要添加的对象的!

读者自己想一下这样做有什么问题?(以上面创建的list对象为例)

首先,list中取出来的所有对象,他本质上是String类型,但是其表现出来的是Object类型,这样我们是无法直接使用String去接收的!见下图:(若对于这一点不懂,建议去看一下java多态的知识)

所以,我们在取出list中元素去使用的时候,必须要进行强转!见下图:

 

那如果list中不止String呢?那我们每使用一个,就必须对其进行强转!见下图:

如果我们对每个元素都进行强转,那是不是太麻烦了呢?

所以,为了解决这个问题,java引入了泛型!

问题二:

集合中的元素,一般都要求能够进行比较和排序,那么既然涉及到了比较,你想想,不同类型的对象能够比较、排序吗?人对象和猪对象能比较吗?(如果你觉得能,当我没说。)那就很难比较了!所以,为了使集合中的元素都能够比较和排序,规定一个集合对象中必须存入同一种类型!这是引入泛型的第二个原因!

问题三:

假如你现在要设计一个 " 点 " 类,即有点的横坐标、纵坐标字段,且要求支持Integer类型、String类型、Double类型。难道我们要为这三种类型每个都单独设计一个点类吗?有人说用Object做接受,那不就回到了上面说的第一个原因吗?

这个时候。为了解决这个问题,java的泛型就来了!

请读者记住这三个问题。

何为java泛型?

泛型有两种,一种是泛型类,一种是泛型方法!

这里就不再说那让人看了也不懂的百度百科式官方定义了,我们直接上代码吧!

首先是泛型类:

class TestClass<T>{
    //这就是泛型类最基本的定义方式了
}

我们在类名的后面,加上一个<>,然后在里面加上一个标识符就行了!不一定非要是T、Y或者K,只要符合标识符命名规则就OK了。

那加上<T>是什么意思呢?有什么用呢?

意思就是,在我这个TestClas类中,T代表了一种类型,但是这个类型是什么,是不确定的,然后我们在这个类中,就可以把这个T当成一个类名去使用,例如:

谁需要用这个类,就在调用的时候,把这个<>中的标识符换成你实际想要用的类型!例如:

这个时候,test对象中所有的T都变成了String类型了!

需要注意的是:在JAVA7之后,等号右边的<>中的类型就不用写了,因为等号左边已经有了,因此我们可以这样:

这种写法叫做  “ 菱形语法 ”。

当然了,T除了可以定义变量外,还可以当成方法的返回值,或者形式参数,例如:

当然了,T是不能new的!例如:

下面我们回到问题一。

有了泛型类这个设计之后,我们就可以在集合类中使用泛型了,于是在java1.5中,ArrayList中是这样的:

于是,我们就可以这样了:

这样,list里面就只能添加(add)String类型了!

如果加别的类型,就会提示  String类型不适用于Interger类型

也就是说,现在ArrayList中所有的E,都变成了String类型了!所以只能用String类型了!

看一下往ArrayList中添加元素的方法  add(E e);

也就是说这里的E现在变成String了,只能接受String了!

这意味着什么?是不是就是说现在list中的元素全部都是String了,那又意味着什么,是不是就是说我们在取出list中元素的时候,不用强转了!因为他已经知道,这个里面不可能存除了String外的其他任何类型了!如下图所示:

到这里,问题一就被解决了!

当然,问题二也被解决了!因为现在存入集合中的都是同一种类型,所以现在集合中的元素是不是就可以比较了!

问题三也解决了,因为我们可以设计一个类:class 点<T>{  },然后通过这个T,设置为Integer、Double或String来解决这个问题!

以上3个问题都解决了,下面我们来讲泛型方法!

我们都知道,静态方法是在类加载进jvm后,就加载进方法区,所以,静态方法是不能使用类名后的T的!

所以为了解决这个问题,又引入了静态方法,静态方法的定义如下图所示:

在方法返回值类型前面,加上<K>就行了,他就代表了这个K在这个方法中是一个不确定的类型!

那么亲爱的读者,你发现没有,以上这种写法是没有丝毫意义的!为什么呢?

因为这里定义的K ,无法从外界给他设置类型!那如何才能让外界调用者设置K的类型呢,只能在方法参数中设置了,所以,一个标准的泛型方法是这样的:

泛型方法只有从参数中设置类型,才是有意义的!

同时,我们要注意的是,如果一个泛型类定义为class A<T>{  },其中有一个静态泛型方法也使用了T,就像以下这样:

那么方法中的T会将类中的T隐藏,也就是说,在这个方法中是以这个方法的T为准,引用这个类时定义的T,不会影响到方法中的T!

值得注意的是,使用类名后的T作为方法返回值,或者参数的,不是泛型方法!如下图:

那么到此为止,泛型类与泛型方法都讲完了,文章开始提出的3个问题也得到了解决。

解决了老问题,就会出现新的问题,现在假设我们只想往泛型中添加一个制定了范围类型,或者要求其必须实现某个接口!例如,我规定,这个类的泛型只能是Number的子类,那就可以将泛型限制一个范围,具体做法如下:

使用extends,将这个T限制成只能是Number或者Number的子类!注意是包含Number的!

当然T也是可以划分多范围的,只需要在中间使用  &  将类名或者接口名隔开就行,如下所示:

在这里,A和B都是接口,注意,限制 T 的范围,只能有一个类,接口的数量不限制,因为java中类是 单继承多实现 的!

还有就是,必须把这个类放在第一位!

将类放在后面是不行的!

到此为止,java泛型类与泛型方法的基本使用就讲完了,但是你以为这样就完了吗,接下来我们深入分析java泛型原理!

-----------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------

泛型如此的好用,但是他到底是怎么实现的呢?其实泛型是一个编译时期的语法,是一个语法糖,意思就是他在编译后就会被删除掉,真正运行的时候,是不存在泛型的。

什么意思呢?废话不多说,直接上代码!

//首先我们创建一个泛型类
class Gp<T>{
    T t1;
    T t2;

    public void test_one(T t){
        
    }
    
    public T test_two(){
        T t = null;
        return t;
    }
    
    public static <K> void test_three(K k){
            //随便做点啥
    }
}


public class TestClass{
    public static void main(String[] args){
    //创建一个Gp对象
    Gp<String> gp = new Gp<>(); 
    //xianzai
    String str = gp.t1;
    System.out.println(str);
    }
}

创建的gp对象,我们将 K 变为 String类型,其实,这个K也好,String也好,都是语法糖,也就是说只是给我们程序员看的,是一种编译时期的语法,编译之后就不存在了,编译后使用的依然是强转!啥意思呢,我们将这个代码生成的class文件反编译之后看一下,你就会豁然开朗!

以上代码编译后的class文件反编译后的代码如下(每一个类都会生成一个class文件,所以有两个):

 

怎么样,是不是有一种豁然开朗的感觉!没错,我前面说了那么多的泛型,现在通通不存在!底层使用的依然是java1.5之前的方式,也就是说依然是使用Object接收,然后使用的时候再强转!这个现象叫做泛型擦除.

到了这里,大部分问题现在都解决了,但是现在又有了新的问题了!

假设现在有一个水果类  Fruit ,还有一个苹果类Apple,橘子类Orange,Apple和Orange继承Fruit!

现在有如下代码:

ArrayList<Fruit> list = new ArrayList<>();
list.add(new Fruit());   //绝对没问题
list.add(new Apple());   //绝对没问题
list.add(new Orange());  //绝对没问题

由于多态,所以这个地方是不会报错的!!!

但是我们再看下面的一段代码:

//首先有一个方法,就叫他s吧
pulic static void s(ArrayList<Fruit> list){
    list.add(new Fruit());
    list.add(new Orange());
}

//以下是main中的片段

ArrayList<Apple> list = new ArrayList<>();

//把这个list放进s方法中
s(list);    //报错

这是为什么呢?方法参数类型不是ArrayList<Fruit>吗?为什么不能传入ArrayList<Apple>呢?

要明确一个问题,Fruit是Apple的父类不假,但是不代表ArrayList<Fruit>是ArrayList<Apple>的父类,这两个没什么关系!

为什么要这样设计呢?这是因为,如果这样不报错,就破坏了原来的ArrayList中只能放Apple的约定了!  因为一旦ArrayList<Apple>转型为ArrayList<Fruit>,那么在s方法中,就可以放Orange了,但是List本来是只允许放Apple的!

再来一张图:

于是现在又有了一个问题,如何修改上面的s方法,使他能够接收ArrayList<Apple> 呢?

于是,又引入了一个东西,叫做通配符,即 “  ?” ,于是上面的代码就变成了以下这样,现在传参的时候就不会报错了:


pulic static void s(ArrayList<? extends Fruit> list){
    list.add(new Fruit());       //报错
    list.add(new Orange());      //报错
}

//以下是main中的片段

ArrayList<Apple> list = new ArrayList<>();

//把这个list放进s方法中
s(list);    //  不会报错

? extends Fruit  代表的是  Fruit类  及其子类,这个叫做通配符的上界。

现在这个传参的问题就解决了!但是突然上面的add方法又报错了,这是为什么呢?

由于s方法中的参数是未知的,当我们传入的是 Fruit 时,可以add   Fruit  或者  Apple  或者 Orange,但是如果传入的是Apple,那么只能add  Apple及其子类,但是现在编译器不知道日后你要传入什么类型,所以为了保持类型的一致性,是不允许add的,我们只能去操作,不能添加!

有上界就有下界,? super Apple,这个就是上界!只允许传入  Apple类  或者 其父类。


pulic static void s(ArrayList<? super Fruit> list){
    list.add(new Fruit());        //不会报错
    list.add(new Orange());       //不会报错
    list.add(new Apple());        //不会报错
} 

现在我们使用add又不会报错了,这个又是为什么呢?

因为我们现在知道传入的参数中的泛型,肯定最起码是Fruit,所以只要是Fruit或者其子类,我们都是可以添加的!

在这里,他们的父类处于一种无法确认状态!那么我们该如何去遍历他们呢?

只需要使用Object类接收就行了!


pulic static void s(ArrayList<? super Fruit> list){
    list.add(new Fruit());        //不会报错
    list.add(new Orange());       //不会报错
    list.add(new Apple());        //不会报错
    
    //由于不知道list的父类,所以我们只能使用Object类去做接收
    for (Object object : list) {
     //。。。doWork。。。
    }

} 

以上说的  ? extends Fruit   和  ? super Apple  都是属于有节通配符,如果是单独的  ? ,那么就叫做无界通配符!

如 List<?> ,代表未知的,这个东西,我们通常用在两个地方:

① 当一个方法是使用了Object类型作为参数时,例如:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + "");
    System.out.println();
}

这个时候,我们可以把以上方法改成使用 ?的,如下所示:

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + "");
    System.out.println();
}

需要注意的是,此时依然是不能使用add的!除了null;

但是这有什么用呢?用处就是 :可以兼容更多的输出,而不单纯是List<Object>,如下所示:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

 

② 在定义的方法体的业务逻辑与泛型类型无关的时候

什么意思?就是说,我现在要有一个方法要接受一个ArrayList参数,但是呢,我这个方法里面实现的功能是和你ArrayList里面装什么东西是没有关系的,所以可以使用  “  ?”。

其实通配符 ?和泛型一样,也是一个处于编译期的语法,编译后就不存在什么 ?,底层使用的依然是强转;可以通过反编译得出结论!

反编译后为:

 

 

 

 

 

 

 

 

 

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值