造轮子的时候不敢用不会用泛型?那你看这篇就够了!

4,757 阅读7分钟

阅读本文解决什么问题?

解决许多java开发 或者android开发 在平时写一些基础架构,或者是造一些轮子的时候不敢用泛型,用不好泛型的问题。 甚至有些人使用泛型的时候报错都只会用idea提示的方法来修改代码,却不知这样改的原因,也不知道强转泛型会有什么恶果。

泛型用来解决什么问题

先定义一个模仿List 的泛型list。 我们来看看这个乞丐版的list能帮我们做什么事

public class CustomList<T> {
    Object[] array = new Object[0];

    public T get(int index) {
        return (T) array[index];
    }

    public void add(T instance) {
        array[array.length - 1] = instance;
    }
}

看看怎么使用他

 CustomList<String> customList = new CustomList<>();
        customList.add("hahahaha");
        String c = customList.get(0);

到这,我们来看看 到底有啥好处。 首先看这个add方法,有了泛型以后,我们就不需要担心类型转换错误了。 因为我们在定义的时候 指定了泛型的类型,所以如果我们在调用add方法的时候传了一个 非string类型的 那么ide就会报错了,即使你不用ide 用记事本写,你编译起来也会报错的。 这就是静态语言的好处了, 很多bug 在编译的时候告诉你 不用像js 那么蛋疼。

然后再看看get 这个函数,想一下 如果没有泛型的话, 我们get出来的值 是一定要强转成string才能赋值给c的, 但是现在有了泛型, 所以你可以直接get出来,这个类型转换的东西 早就帮你做好了。

总结一下泛型的好处:

  1. 避免运行时出错,编译时就告诉你
  2. 方便你使用,省略强制类型转换的代码

泛型为什么不可以是静态的?

这边可以想一下,为什么泛型不能用静态的去定义?你怎么改都是无法突破这个规则,是无法编译成功的。

前面的例子我们可以知道,泛型主要用来可以初始化每个实例的。 注意是每个实例,他是动态确定的,

取决于你当时使用的时候 传的是什么参数,比如List<Object> List<String> List<Teacher>

对于一个静态变量来说 你如果用泛型 那就会乱套了。 例如我们上面截图的例子你用泛型会发生什么?

static Object ins? static String ins? static Teacher ins? 大家都叫ins,那我怎么知道 这个ins

到底应该是哪个类型的? 静态变量 全局唯一啊。 所以泛型是绝对不能用static来修饰的

这个地方一定要想明白了,想明白了,对你理解泛型是有好处的。

泛型的一种错误写法

这种就是一种典型的错误写法,明明接口中有泛型的,结果实体类中 把这个泛型给抹掉了。

虽然可以编译通过,但是这种写法就毫无意义了。

这种才是正确的写法,和前面那种错误的写法相比 我们明显可以省略一次强制类型转换。

大家可以比对一下这2种写法 和 文章开头泛型的2个优点。仔细体会一下。

如何正确extends泛型

泛型限制

interface IShop<T> {
    T buy(float price);
}


interface IPhoneShop2<T> extends IShop<T> {
    void repair(T phone);
}

前面我们说道 ,泛型最大的好处就是方便使用,比如上面的代码 我们使用起来就很轻松如意,但是因为这样的写法 太随意 所以要加一层限制。 上面的代码中,我们明明是一个手机商店,但实际使用的时候 却可以随便传, 传String 传Apple 传啥都行。 这和设计者的本意是不一致的。

所以泛型还可以加限制

interface Phone {

}

interface IPhoneShop2<T extends Phone> extends IShop<T> {
    void repair(T phone);
}

这样一来就可以限制我们使用时的类型,限制他一定得是Phone的类型才行。 考虑到java 是支持多接口,但是不支持多继承的,泛型的限制也遵循这个规定。

泛型限制在list中的迷惑性

来讲讲泛型中一个令很多人想不通的地方

定义一个水果 然后有苹果和香蕉

interface TFruit {

}

class Apple2 implements TFruit {
}

class Banana2 implements TFruit {
}

然后我们看看他们的使用

报红的地方为什么报错 是很多人想不明白的地方吗,我们的apple 命名是fruit的子类啊,为啥报错?

换个角度来思考一下这个问题:

所以对于 list 的 泛型来说, 他的类型 是动态确定的, 是没办法在编译期 确定的, 所以你需要保证他 在= 两边 泛型都是绝对一致的 才能声明成功,否则必定失败

继续看:

这个例子其实不难理解 为什么add 方法不能编译通过。

看到这,我相信很多人 都想告辞了。。。这tmd 泛型限制真多,咋用?不用了,以后自己造轮子自动屏蔽泛型了。

没关系 耐心一下,我们再缕一遍。

// =左边: 代表 我想要一个水果  =右边 :我给你个苹果  逻辑没问题 编译通过
        TFruit fruit = new Apple2();
        // =左边: 代表 我想要一个水果的list 任意的水果 =右边 :我给你个任意水果的list 逻辑没问题 编译通过
        List<TFruit> tf1 = new ArrayList<TFruit>();
        //既然是个水果的list 那我 add 苹果香蕉 肯定没问题
        tf1.add(new Apple2());
        tf1.add(new Banana2());


        // =左边: 代表 我想要一个水果的list 任意的水果  =右边 :我给你一个苹果的list
        //这样编译肯定不通过,因为我想要的是水果的list 你却给我一个苹果的list 这样你让我就没办法玩了
        // 我想要水果 你只给我苹果 那香蕉 葡萄 西瓜 我就没办法要了,所以你肯定不行 编译不过
        List<TFruit> tf2 = new ArrayList<Apple2>();



        //=左边: 代表 我想要一个list,这个list 必须是一个水果的类型,且只能是一种水果的类型   =右边 :我给你一个苹果的list
        //符合要求 编译通过
        List<? extends TFruit> tf3 = new ArrayList<Apple2>();
        //我这个tf3 要求的是必须是一种水果的类型,但是我并不知道是那种类型,可能是水果 可能是葡萄 可能是香蕉
        // 所以你直接往我这塞一个确定好的水果  我肯定是不接受的,编译肯定失败
        tf3.add(new Apple2());
        tf3.add(new Banana2());

?extends 好像有点蠢?

前面的文章看完,是不是觉得 这个?extends 有点蠢了,实际上 他在某种场景下 是 十分有用的(废话 不然java为啥要这么设计)

还是上面的水果,我们增加一个方法 返回对应水果的价格

interface TFruit {
    int getPrice();
}

class Apple2 implements TFruit {
    @Override
    public int getPrice() {
        return 1;
    }
}

class Banana2 implements TFruit {
    @Override
    public int getPrice() {
        return 2;
    }
}

看看会有什么问题

看这个函数的参数, 这个函数的参数 意思是 我想要一个list ,这个list里面 可以放任何水果, 只要是水果就行,

但是在调用的时候 我们给他的 却是苹果的list 和 香蕉的list ,这就不是他想要的了,我想要任意类型的水果 你却给我 苹果或者是香蕉的,你帮我指定了具体类型 那肯定是不可以的。

所以这个时候 ? extends 就出场了

改完以后 就直接编译成功了:

回想一下上一小节的内容,这个? extends 不就是代表 想要任意一种水果吗

你既然想要的是任意 一种水果,那我给你苹果或者香蕉 肯定是ok的。

你们使用的泛型埋坑了吗?

而且是运行期间报错了。后果比较严重,不易察觉。

一个香蕉当然不能转成苹果。

平时写代码的时候一定不要这么写。

?super 又是啥,怎么用。

改成

就可以, 这里怎么理解?

加上? super就代表 等号右边的东西 你只要可以接受一个苹果的list就可以了。