《重学Java系列》之 泛型(上)

300 阅读9分钟

不诗意的女程序媛不是好厨师~ 【转载请注明出处,From李诗雨---blog.csdn.net/cjm24848365…

[篇外话] 最近给自己安排了不少的学习计划,每天都有很多的书要看,课要听,字要写,感觉很充实~

越来越觉得,大脑是很容易欺骗我们的。很多东西瞟一眼,听一句,就感觉我会我懂,但是要让我说,让我写,就GG了。

在输入知识之后,要及时的输出,只有经过一进一出的反馈之后,才会真正的学到点东西。

今天,我把自己抛空,重新学习。

所以,泛型你好,我们重新认识一下吧~ ❤

关于泛型,我要说的有以下8点:

泛型的东西细细品起来还是不少的,所以我会分为上下两篇, 今天我们先来看前5点~

下面就让我们开始吧~

1.什么是泛型?

泛型,即“参数化类型”。 你可能又会问,参数化类型又是什么东东啊?

这个我们可以通过类比、扩展的方式来理解。 大家应该都知道参数吧,定义方法的时候我们有形参,当要调用该方法时,我们要传进去实参。 其实,参数化类型我们可以这样理解,就是类型由原来的“ 具体的类型参数化。我们定义的时候用 参数形式(可以类比为形参),使用的时候再传入具体的类型(可类比于形参)。

如果这样说你还不太懂,没关系的!

最近,诗雨正好看到了一个大佬的文章,用了一个更加生动形象的比喻来解释泛型。我真的是佩服佩服,下面就请允许我来和大家分享吧:

一只玻璃杯我们可以用来干什么呢? 我可以用它来盛一杯可乐,你可以用它来盛一杯江小白,然后我们还可以干一杯。 泛型的概念就在于此,烧制完成这只杯子的时候没有必要在说明书上定义死,指明它只能盛可乐,却不能盛江小白。也没有必要在说明书上指明它用来盛液体,或许一个熊孩子会用它来装彩虹糖呢。 这么一说,你有没有觉得形象很多? 泛型其实就是在定义类、接口、方法的时候不局限地指定某一种特定类型,而是让类、接口、方法的调用者自己来决定具体使用哪一种类型的参数。

现在有一只玻璃杯,你可以让它盛一杯可乐、雪碧,也可以盛一杯江小白——泛型的概念就在于此,制造这只杯子的时候没必要在说明书上定义死,指明它只能盛碳酸饮料而不能盛白酒!

就好比,玻璃杯的烧制者说,我不知道使用者用这只玻璃杯来干嘛,所以我只负责造这么一只杯子;玻璃杯的使用者说,这就对了,我来决定这只玻璃杯是盛可乐还是白酒,或者彩虹糖。

真的是有才,这个比喻太形象了,我现已经可以泛型的意思了,你呢?

如果你也理解了,那就让我们继续往下看吧~

2.为什么要有泛型?

这个,我们可以从反面来论证。 如果没有泛型,我们会遇到哪些问题呢? 我们先来看两段小代码: (1). 我们在实际开发中经常遇到不同数据类型的求和问题: 如int类型的求和,float类型的求和,double类型的求和,如果我们通过重载的方式来实现,就会出现大量的重复代码,如下所示:

    //int型求和
    public int add(int a, int b) {
        return a + b;
    }

    //float型求和
    public float add(float a, float b) {
        return a + b;
    }

    //double型求和
    public double add(double a, double b) {
        return a + b;
    }

那我们就想,此处要是有泛型的话,代码的冗余问题就会在一定程度上得到解决呢。

(2). 我们不使用泛型很可能会出现下面这种情况:

在这里插入图片描述
在编译阶段没有问题,但是一旦运行起来就会报ClassCastException的错:
在这里插入图片描述
你说,我本来是想取个帅哥出来,结果一不小心差点把他转为了数字,这当然是不行的。

如果我们使用泛型来做约束,那上面的这种情况就不会出现了:

在这里插入图片描述
一旦我们规定了加入List中的数据类型必须是String,那么只要你添加的不符合,它在编译期便会及时报错,而不是要等到运行期间才迟迟告诉你错了。而且取出的时候你也不用考虑类型转换问题了,真是一举两得。

通过以上的实例我们不难看出 使用泛型的两个优点

  • 适用于多种数据类型执行相同的代码
  • 泛型中的类型在使用时指定,不需要强制类型转换,而且可以尽早的避免错误。

好了,现在我们已经知道了什么是泛型,为什么要有泛型,下面就让我们来具体看看泛型在 类、接口 和 方法 中的使用吧~

3.泛型类、泛型接口、泛型方法

通过上面的学习,我们知道 泛型的本质就是为了参数化类型,即在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

我们先来看看泛型类。 (1). 泛型类

  • 引入一个类型变量T,并且用<>括起来,并放在类名的后面。这样就是一个泛型类了。
    在这里插入图片描述
    如下列代码:
public class MyGeneric<T> {
    private T data;

    public MyGeneric() {
    }

    public MyGeneric(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

  • 泛型类是允许有多个类型变量的。 如下列代码:
public class MyGeneric2<K,V> {
    private K key;
    private V value;

    public MyGeneric2() {
    }

    public MyGeneric2(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public void setValue(V value) {
        this.value = value;
    }
}

(PS:除了T,其他大写字母都可以哦,不过常用的就是T,E,K,V等)

(2). 泛型接口 泛型接口与泛型类的定义基本相同。

public interface Generic<T> {
    public T doSth();
}

需要注意的是 实现泛型接口的类,有两种实现方法:

①. 实现泛型接口的类,未传入泛型实参

public class ImlGeneric<T> implements Generic<T> {
    private T data;

    @Override
    public T doSth() {
        return null;
    }
}

在new出类的实例时,需要指定具体类型:

在这里插入图片描述

②. 实现泛型接口的类,传入泛型实参

public class ImpGeneric2 implements Generic<String> {
    @Override
    public String doSth() {
        return "突然好想你";
    }
}

在new出类的实例时,和普通的类没区别。

(3). 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。

有没有什么技巧来辨认一个泛型方法呢? 答案是有的:在 方法的 修饰符 和 返回值 之间有一个 < T> 的 才是泛型方法欧~

在这里插入图片描述
来一个泛型方法给大家看看:
在这里插入图片描述
再次牢记一下秘方 : 方法的 修饰符 < T> 返回值 三者只有这样的排列才是泛型方法偶~

好的,下面我们就赶快来测试一下,加深记忆: 测试1:下面这个可不是泛型方法哈!

在这里插入图片描述
测试2:下面这个才是正真的泛型方法:
在这里插入图片描述
好的,现在我们泛型类、泛型接口、泛型方法也基本掌握了,又进步了一点点,是不是有点开心呢?下面让我们继续前行~

4.给类型变量增加约束

有时候,我们需要对类型变量加以约束。

比如计算两个变量的最大值。 要找出最大,就免不了要进行比较。那么问题来了,不是所有的类型都能进行比较啊?

在这里插入图片描述
所以我们要求传入的类型要能够进行比较,最好有比较方法。是的,或许我们想到一块去了,我也想到了要让其实现Comparable接口。
在这里插入图片描述

T extends Comparable中 T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以是类也可以是接口。 如果这个时候,我们试图传入一个没有实现接口Comparable的类的实例,将会发生编译错误。

在这里插入图片描述
注意:

  • extends左右都允许有多个 如 T,V extends Serializable & Comparable
  • 注意限定类型中,只允许有一个类,因为类是单继承的。 如果限定类型中既有类又有接口,那这个类必须要放在第一个位置上,接口接着往后放就可以了。
  • 这种类的限定既可以用在泛型方法上也可以用在泛型类、泛型接口上。

另外需要注意理解区分一下: 因为在Java中,<T, U>表示指定了2个泛型,T、U。 所以,在对泛型做限制限制时,

  • <T, U extends Comparable > 表示泛型U限定了必须为 Comparable 的子类,而T没有。

  • <T extends Comparable, U extends Comparable > 表示泛型T和U都限定为 Comparable 的子类 由于 Comparable 是个接口,因此“ Comparable 的子类 ” 正确的描述应该为实现了 Comparable 接口的类

  • <T, U extends Comparable & Serializable>表示: U 有限制,为现实了Comparable 和 Serializable 接口的类, T 为任意类型,没有限制

5. 泛型类型的继承规则

泛型是怎样的继承规则呢? 让我们来自己探讨一下吧~ 现在有两个类,他们是“父与子的关系”:

在这里插入图片描述
在这里插入图片描述
还有一个泛型类:
在这里插入图片描述
那么问题来了:Generic< Pet> 和 Generic< Dog> 是什么关系呢?
在这里插入图片描述
是的,结果如图所示,Generic< Pet> 和 Generic< Dog>竟然没有任何关系! 这关系可不是闹着玩的,Generic< Pet>不信,非得要做个DNA去验证,我们也理解他的心情就带着他去做了:
在这里插入图片描述
现实总是骨干,Pet 和 Dog 有 父与子 的关系,但是 Generic< Pet>和Generic< Dog>真的没有任何关系!

好吧,我们就让 Generic< Pet> 在厕所里哭一会吧。

但是,注意啦,泛型类是可以继承或者扩展其他泛型类,比如List和ArrayList。又比如下面的代码: public class PetGeneric<T> { } public class DogPeneric<T> extends PetGeneric<T> { } 用的时候" PetGeneric dog=new DogPeneric<>();"这样是没有问题的。

好的,今天我们就先到这。 剩下的3点内容,我们下篇见吧~

积累点滴,做好自己~