太惊悚了!上班摸鱼一万字,就是为了让你理解Java泛型

什么是泛型(先理解是个啥)

在学习Java基础的过程中,泛型绝对算得上是一个比较难理解的知识点,尤其对于初学者而言,而且就算是已经有基础的Java程序员,可能对泛型的理解也不是那么透彻,属于那种看了明白,时间长了就忘的那种,究其根本,还是对泛型不够理解。 大部分人对泛型的认识:“基础知识,但是比较模糊

为啥要有泛型

泛型这个概念是在Java1.5提出来的,之前是没有的,那为什么之前没有,现在要提出来这个概念呢?那你就得想啊: 1、要么是之前的技术太垃圾了,得升级换代下 2、要么是技术发展,搞出来的新玩意,让Java更好用 3、要么就是填坑,之前的有部分,搞个出来填坑 4、…… 那你看,这个泛型,有可能是啥,实际上它就是填坑的,说这个之前,咱们先来看一段代码: 这段代码没有看不懂的吧,就是简单的一个List集合,这里可能需要你额外注意的就是我这里定义的List不是这样的哦:

List stringList = new ArrayList();

不知道你看出来区别了吗?这个应该是大部分我们用的时候会定义的那种,熟悉泛型的可能知道这是怎么回事,但是不熟悉泛型的就记住这么用就ok了。 继续看上面我举的那个例子,这里我想说明的就是这里定义的List集合是什么都能装的,集合一般啥用,不就是往里面存放数据的嘛,把集合看做一个房子,里面可以进去老人和小孩,当然各种猫啊狗啊都可以进去,来,上图: 咋样,我的绘画功底还是可以吧,List就好比一个房子,里面是可以进人,当然也可以进动物,比如各种各样的小猫小狗啊,当然了,List这里可进入的就是数据了,比如字符串啊,整型啊等等。 这个时候就会产生一个问题,啥问题嘞,你看啊,这座房子里面可以进入各种各样的物种,就是各种类型的啊,如果这个房子会说话的话,它就说了“啥玩意啊,你们啥都往我这来,啥品种都行,也太乱了吧,都记不清楚你们谁是谁了,算了我也不记住你们谁是啥类型了,进来的统一当做‘活物’吧” 就是说啊,对于房子来说,不管你进来的是个啥,我统一把你们当做“活物”,在我这里你们就是一个类型的,同样的,放到List集合这里来说的话,也是这样的,就是你字符串啊,整型啊都可以放入List集合中,那么List也说了,我不记住你们到底哪个是字符串哪个是整型,在我这里统一把你们看做是Object类,至于为啥是Object类,这是因为Object是一切类的父类啊,所以没有比它更合适啦,不管你是字符串还是整型,你们一定都是Object类型的,这点容易理解吧。 这样看似挺完美的,但是也有潜在的问题啊,什么问题嘞,你看啊,我们还拿房子来说,对于房子来说,在房子里面的都被看成是“活物”类型了,那么如果我要从房子里找一个小狗出来,那么我找出来的类型都是“活物”,可是我要的是小狗啊,你给我来个“活物”,不符合我的要求啊,怎么办?大家都知道有个强制类型转换吧,好吧,我就把“活物”给你强制转换成小狗,这不就符合要求了,但是这样问题就来了,假如你找出来的就是个小狗,你把它强制转换成小狗,那倒也没啥,但是你也有可能会找出来个人啊,这时候你要把一个人强制转换成小狗,搁谁谁也不乐意啊,你说是不是。 同样的,对于List集合也是这样,你什么都可以往里面存储,然后统一被看成是Object类型,这个时候如果我们从List集合中取值的话,那就要用到强制转换了,需要把Object类型转换成我们要的类型,比如我们想要字符串,如果取出的本来就是字符串,那转成字符串没啥关系,但是如果取出来的是整型嘞,强制转换成字符串,那整型还不乐意嘞,于是乎,程序就要给你报错了,来来,看看代码: 咋一看,貌似没啥问题,编译也没报错啥的,我们运行一下看看: 吆喝,运行报错了,这个错也简单,就是不能将整型Integer转换成String字符串类型,就是类型不匹配啊,这里要记住这个错误类型叫做:ClassCastException 看到这里你也许就明白了,我定义一个List集合,本来只想往里面存入字符串嘞,但是不知怎么滴,里面混入了一个整型,因为List并不知道进来的都是什么类型,反正都看作Object类型,都可以进来,那么我们取数据的时候就会发生ClassCastException错误。 那这不行啊,我给你List里面存一个数据,我是希望你记住我传入的是什么类型的数据的,其他的就不允许再传入了,这样我取值的时候也不用强制类型转换,也就不会发生ClassCastException了。 这个你能想到,那么JDK官方更加能想到,于是乎,在Java1.5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是为了解决我们上述的问题,说白了就是我希望集合可以记得住我存进去的数据是什么类型的,以此做一个筛选,不是同类型的就不允许在一块存放,这样也避免了ClassCastException错误的出现,因为都是同一类型,也就没必要做强制类型转换了。 所以,你现在知道了为啥要有泛型了吧,当然,泛型的引入大部分原因是为了弥补集合的一个缺点,但是泛型的应用是很广的,不仅仅局限于Java的集合。

泛型的定义和理解

以上还算详细的和大家介绍了为啥要有泛型的出现,那么泛型是如何定义的呢?

泛型就是参数化类型,也就是说把我们要操作的类型作为了一个参数,比如我们创建集合的时候,允许我们可以指定集合中元素的数据类型。

在JDK1.5中引入了“参数化类型”的概念,这个就是泛型,也就是说泛型和参数化类型是等价的,一回事,那么我们来理解理解啥是参数化类型。

参数化类型

我们看看字面意思,参数化参数化,你想啊,我们有时候看一些魔幻电视剧,比如说某个人兽化了,大致知道啥意思吧,就是他变成了一个怪兽,变形了,哈哈,那么这里的参数化意思大概是不是就是变成了一个参数的意思呢?那么后面还有一个类型,组合起来是不是就是“把类型变成了参数”,那类型是啥啊,不就是String类型,Integer类型这些嘛,现在把这些类型都作为了参数,这就是参数化类型了。 不知道我表达的是否清楚,你明白吗? 好了,来看看泛型到底长啥样吧:

List stringList = new ArrayList();

就是这个,我们经常这样操作的,它就是泛型的应用,你看看它和如下没有使用泛型的有啥区别:

List stringList = new ArrayList();

很容易看出,就是多了一个这个,那么该如何进一步去理解它嘞?

理解泛型

还记得之前我们说的吧,泛型的引入很大一部分原因是为了让集合能够记住他里面的元素的数据类型,怎么让它记住嘞,实际上实现也很简单,就是只传入特定的类型,比如要传入字符串类型,那么久只能传入字符串类型,像整型类型及其他类型就是不允许进入的,这样的话就能保证集合中的元素都是统一的类型,集合自然就能记住了。 我们再来看之前举的那个房子的例子: 通俗易懂吧,之前这个房子是开放的,谁都能进,现在嘞,我这个房子是用作狗屋的,自然是只提供小狗来住,那么其他的就不允许进入,你一个人让你进你也不进啊,怎么搞嘞,那就是在门口搞个检查装置,就好比安检,首先告诉安检,只能让小狗进,其他的不让进,于是乎,每过来一个,安检都要检查下是不是指定的小狗类型,不是的话不让进,是的话就进去,如此一来这个屋子里就都是小狗了,名副其实的狗屋啊。 那放到集合也是一样的,现在要理解的就是如何给这集合加上安检啊,再来看没使用泛型的时候:

List stringList = new ArrayList();

这个时候是开放的,各个数据类型的都可以存放到这个List集合中,现在看下使用了泛型的:

List stringList = new ArrayList();

大家也看到了,就是多了个,我们在实际代码中看下: 显而易见啊,报错了,说是我需要一个String,你给传入一个int,不符合要求,那就不能存入,所以啊,你看明白了吗?List是一个集合,可以往里面存入数据,现在要进行限制,不是什么类型的都能存入,那就对存入的数据进行检查,怎么搞,那就在List后面加个,就成了List,看到没,这个是不是就和我们上面举例子中的那个安检很像,负责检查进来的数据,首先给它指定一个数据类型,这里就是String,然后就是检查,不是String的都不让存进来,所以啊,这个就是一个安检的作用啊,其中的String就是指定的数据类型,这就是泛型啊,也就是参数化类型,String是字符串类型,这里就作为一个参数放在这里,保存进List集合中的元素都要是String类型的。 咋样,明白没?这就是泛型啊,把String作为一个参数,类型参数化了,你看,是不是这么回事。 这样一看,是不是觉得泛型也挺好理解的,其实这只是对泛型的基本理解,泛型还是有不少内容的,在理解了泛型的基本概念之后,我们还需要看看泛型的其他内容,当然,我必须告诉你,即使上面我讲的你明白了,接下来的内容同样有可能让你费解,一起来看下!

泛型进阶

菱形语法

咋一看,这个很高级啊,啥意思嘞,其实看代码就知道怎么回事,我们上面举了这样的集合泛型代码:

List stringList = new ArrayList();

你平常是这样写的吗?我猜有这样写的:

List stringList = new ArrayList<>();

有啥区别,很简单,就是后面的ArrayList后面有尖括号内有没有这个String,也就是类型参数,这个理解吧,按照我们上面说的,这里加上是为了指明集合中元素的数据类型,那么后面的不写也行吗?在Java7之前是必须写的,也就是必须是这样的形式:

List stringList = new ArrayList();

但是在Java7开始就可以这样写了:

List stringList = new ArrayList<>();

因为在Java7中是可以通过前面的类型参数去推导出ArrayList中的数据类型的,也就是类型参数不需要了,但是这个<>尖括号是必须的,至于尖括号中的类型,是可以自动被推导出来的,这个就叫做菱形语法,为啥叫菱形语法嘞,因为这个<>尖括号像菱形啊……

理解类型参数(重点)

还记得什么是类型参数吗?看这行代码:

List stringList = new ArrayList<>();

泛型的本质是参数化类型,就是把类型当做参数了,而这个类型参数就是尖括号内的东西了,在上面的这行代码中,所谓的类型参数就是这个String了,这点明白吧,另外啊,我们都是到Java中的方法中有形参和实参,这个都知道怎么回事吧,那么这里类型参数其实也是有区别的,它也是分为类型形参和类型实参。 那啥是类型形参,啥又是类型实参啊?是不是觉得理解不了?别着急,经过我下面的解释,你就会觉得这是如此的简单。 我们上面写的这行代码:

List stringList = new ArrayList<>();

这里的类型参数String其实就是类型实参了,也就是实际的类型参数,这样的类型参数其实就是各个数据类型,泛型不就是参数化类型嘛,类型都被当做参数使用,所以这里的String其实就是实际的类型实参,是个字符串类型,咋样,理解吧? 那啥是类型形参呢?我们来看看List接口是如何定义的? 源码中List接口是这样定义的,当然,看到尖括号,这就是泛型,只不过好像跟我们之前看的不太一一样,之前这里的尖括号都是具体的类型,比如String,这里弄了一个E,这是啥玩意啊。 首先啊,你看,还记得Lsit集合人家是什么都可以存储的嘛,也就是整型啊,字符串都可以,现在使用泛型之后,相当于你可以这样写代码:

List stringList = new ArrayList<>();

那么你这里的List集合就只能存储字符串类型的,当然,如果我想让List集合专门存储整型数据呢?那是不是要这样写:

List list = new ArrayList<>();

可是这样的话,你发现问题没?那我源码中的List该怎样?现在要求就是我实际写代码中List后面的泛型可以写成各种数据类型,这就要求源码中的List定义必须是具备通用特性的,那就用个啥来做个抽象的,泛型中就是使用一些大写的英文字母来作为类型形参,比如这里的E就是一个类型形参,实际中你可以写成String啊或者Interger,以便达到List集合只存储特定类型数据的目的,而String这些就是类型实参了。 咋样,明白吧,我之前学习这里的时候也比较疑惑这个E是啥啊,想必大家也见过T,这都是啥,这些其实就是泛型中的类型形参,我们在创建具体的类,接口或者方法的时候可以把这些类型形参转换成具体的类型形参,大家可以看看Map的定义,是这样的: 这里是一个K和一个V,所以啊这些都是有个基本命名的,大致有如下这些:

E 元素 集合框架使用 K 键 映射关系键的类型 V 值 映射关系中值得类型 N 数字 主要用于表示数字 T 通用类型1 S 通用类型1 U 通用类型1 V通用类型1

咋样,看到这里是不是有种豁然开朗的感觉呢?这些就是实际的类型形参,比如上面的Map,类型形参是这样的:

Map

实际的类型实参样式是这样的:

Map

ok了吧!理解力类型参数的形参和实参,我们再看接下来的内容。

泛型使用

我们在上面介绍泛型的时候,基本上都是使用集合类来说明,这很大一部分原因是因为泛型的提出有相当大的原因是为了弥补集合的缺陷,当然我也说了,泛型绝不仅仅是局限于集合,我们可以自定义泛型,比如自定义泛型接口和泛型类以及方法。

1、定义泛型接口

我们还是再来看下JDK中的List的定义,就是它: 这是定义了一个泛型的List接口,接下来我们来自己定义一个泛型接口,看看是怎么定义的,来,上代码: 我们这里自定义了一个泛型接口,这个泛型的类型形参是E,包含一个showTypeName方法,目的是打印出泛型实际类型实参的类型名称,接下来写一个类去实现这个泛型接口: 这里要注意了,我们写一个类,然后去实现上面我们自定义的泛型接口,这时候我们的类名后面也是要写上泛型的,就是不能这样,否则报错: 然后就是实现泛型接口中的方法了,获取传入的类型实参的类型名称,接下来我们使用这个类看下: 到这里是不是就比较熟悉啦,跟我们之前一直举例的List集合有点相似吧,我们看输出结果: 得出我们输入的类型形参类型是String类型,以上就是泛型接口定义的一个非常简单的例子了,咋样,不知道你看明白了吗?

2、自定义泛型类

上面我们简单说了下泛型接口的自定义,大致上就是你要知道泛型接口如何定义,类如何去实现一个泛型接口,接下来我门来看如何自定义一个泛型类,直接看代码: 这里简单自定义了一个泛型类,这里要注意,我门上面说过了什么是类型形参,并且介绍了几个约定俗成的,实际上这里的类型形参,你用任何一个大写的英文字母都是ok的,比如这里我就用了一个大写的G,然后看看如何使用这个泛型类: 我们再来看看输出结果: 咋样,泛型类的自定义是不是也比较简单,接下来我们看看,一个类如何去继承一个泛型类。

3、继承泛型类

直接看代码吧: 这里就是直接继承了上述我们实现的泛型类,这里要注意了,我们继承的泛型类,不能再是类型形参的形式了,也就是不能这个样子: 也就是当你继承了一个泛型类的时候,就要指定真实的类型实参,这个时候就要确定类型了。

4、原始类型

我们上面说了继承泛型类的时候,后面不能再跟泛型形参的形式了,但是你是可以完全去除泛型的,也就是这样的形式: 这样也可以的,这被称作是原始类型,那么这样的话,实际使用输出是什么呢?我们来使用这个类: 这时候其实就是把原先的类型形参G当做Object类型的了,而之前我们这样的形式: 就相当于把原先的类型形参G全部当做是String类型了,所以你这里就会报错了: 因为这时候就只能传入String了。

并不存在泛型类

我们拿JDK中的List来举例说明,首先看下面的代码:

List stringList = new ArrayList<>();

这行代码想必你已经非常熟悉了吧,再看个熟悉的代码:

List stringList = new ArrayList();

经过上面那么多的讲解,你肯定知道了,上面那种我们使用了泛型的形式,下面则没有,也就是说本身List是什么都可以存储的,但是现在加上泛型之后,就只能存储特定数据类型了,比如这里的List,也就是从List变成了List了,但是实际上啊,这里并没有生成新的List,其实本质上还是List,只不过List做了规定,只能装载String类型的数据,但是实际上List还是原来的那个List,所以啊,实现类ArrayList并不是一个新的类,不过可以看做是ArrayList的一个子类。 所以啊,泛型类,实际上是不存在的。

通配符

泛型中还有个通配符的概念,这个该怎么理解呢?还是看例子比较好理解,来看代码: 假如这里有这么一个方法,也没啥特别,要非说特别那就是方法中的参数是一个List,为啥特别,因为List是一个泛型接口啊,你看: 但是上面我们的List并没有指定实际的类型实参,这样就会产生一些问题,它会产生泛型警告的问题,那么最好我们还是指定实际的类型形参,但是这里也有问题啊,就是这里我并不确定以后传入过来的List的实际类型实参是啥,也就是说可能传入这样的一个List:

List stringList = new ArrayList<>();

也有可能传入这样的一个List:

List integerList = new ArrayList<>();

那该怎么指定嘞?这样吗: 看着好像很对,我们试试: 很不幸,这里报错了,其实这里一句话可以概括:

String是Object的子类,但是List却不是List的子类

记住这个,就知道这里肯定是不行的,那该怎么办嘞?这里就要使用到通配符了,就是这样: 就一个问号就搞定啦,这个时候我们使用这个Test方法的时候就是既可以传入这样的List:

List stringList = new ArrayList<>();

也可以传入这样的List:

所以啊,使用通配符之后可以传入的类型就多了,但是有的时候可能不需要传入所有的,希望还是有一定的限制的,这个时候就需要通配符的上下限设置了,关于这个,限于篇幅问题,就不展开来讲了。

泛型方法

这里把泛型方法单独拿出来讲是觉得这个泛型方法理解起来还是需要费点劲儿的!实际情况也确实如此,泛型方法有点不好理解。 那啥是泛型方法嘞,简单来说啊,泛型方法就是:

声明方法的时候,可以定义一个或多个泛型形参

这里我们拿泛型类来对比一下,对于泛型类,比如这个: 我们在实例化这个类的时候需要指明具体的类型实参,比如是String还是Intenger之类的,那么对于泛型方法而言,它就是在定义的时候是泛型形参,而实际调用的时候需要指定泛型实参(也就是泛型的具体类型)

定义及使用泛型方法

接下来我们来看如何定义泛型,首先我们来看一个正常的方法定义: 这是一个很正常的方法定义,但是里面的逻辑似乎不正常,为啥?觉得这样没必要啊,你看我们这里需要给参数传入的就是String类型,你这里还输出类型名称,好像只有给你输出来你才相信似的,好像是这儿回事,那我们接着来看,如何定义一个泛型方法。 你想啊,我们怎么定义类,是不是有个关键字class,如何定义接口呢?是不是用关键字interface,所以,定义这些比较特殊的东西,必定有个特殊的东西,那么如何定义泛型方法呢?你看: 这里就简单定义了一个泛型方法,这里需要注意的就是,你凭啥说你定义的是一个泛型方法呢?这里的一个象征就是红框中的泛型,也就是说啊,你要定义一个泛型方法的话,那么你就必须在权限修饰符(这里是public)和返回值之间写上,这个是泛型标志,代表你这个是一个泛型方法,对了,这里的T就是个泛型形参,也可以是其他的大写字母,这个之前讲过的,所以啊,你在看这个泛型方法,比如我们调用这个泛型方法: 是不是觉得逻辑没啥问题啊,因为传入的是个泛型,所以这里通过一个泛型方法,可以传入不同的类型,以便查看其类型,这里要注意了,我们写的平常的方法,方法传入的是确定的类型,但是使用泛型后,可以传入各种类型,那么就可以测试一些不知道是什么类型的类型名称了,可能有点绕,理解下! 所以啊,泛型方法的定义的语法如下:

修饰符 返回值类型 方法名称(形参列表){方法体……}

我们来对照一下我们定义的泛型方法: 这里的public就是修饰符,就是泛型列表,为啥说列表,其实是它可以包含多个泛型形参名称,也就是那些个大写的英文字母,后面的void就是返回值类型了,然后就是方法名称和形参列表了。

重点总结

我们还是来看这个泛型方法的定义: 这里有几个知识点需要强调一下: 1、权限修饰符和返回值中间的这个泛型列表,也即是很重要,就相当于是泛型方法的标志,有了这个你才能叫做泛型方法 2、代表将使用泛型类型T,只有这个时候,你在方法中才可以使用泛型类型T 3、另外你需要知道的就是这个T啊就是泛型形参的命名,可以是T,E,K等等这些,反正就是大写的英文字母就ok了

泛型方法的的学习建议

对于Java基础中的泛型这块,我一直觉得是个比较重要的知识点,当然,事实也是如此,所以这个知识点需要理解透彻了,但是另外一个现实情况就是,泛型的理解还是有一定门槛的,尤其对于初学者而言,那么泛型中的泛型方法可能又稍微难理解一点,那么对于这块的学习,刚开始不建议死扣,有个基本认识即可,比如我上面介绍的那些关于泛型方法的基本知识点,刚开始了解这些即可,当然,泛型方法不仅仅是这些,只不过我们暂且不深入学习。 学习还是要一步步来,后续会单独出文章详细聊聊泛型方法的。

泛型擦除与转换

这个泛型擦除是啥嘞?一般的话面试的时候要是问泛型的话,那就大概率会被问到这个泛型擦除了,先来看一段代码: 猜想一下,这两个ArrayList的类型一样吗?看一下结果: 可以看到,这两个是完全一样的,也就是说,这里的泛型实参String和Integer并没有对ArrayList造成什么本质上的影响,其实这里蛮好理解的:

这里的泛型就相当于一个安检,指定了一个具体的数据类型,想要往这个集合中存入数据就得经过泛型这个安检检查,和指定数据类型不一样的都不允许存储,这就保证了存储的类型都是指定的类型,也就是说啊,我这个集合只想存储统一的数据类型,怎么做嘞,那就搞个检查装置(就是泛型),起到一个过滤数据的作用,但是你这检查装置只是起到一个检查过滤的作用,并不影响我本身,举个例子就是好比我要开个针对程序员的大会,选了一个酒店,那么这个酒店目前只允许程序员进入,所以找个保安(泛型)站在门口,只允许程序员进入,不是程序员的不让进,那么在大会开始之前(编译阶段),保安会在门口把门,只让程序员进,等到人员都到场了,那么可以确定的是酒店里都是程序员了,那么保安此时的任务就结束了,然后保安就可以撤了(泛型擦除),然后我们就开始开会了(运行阶段)

这就是泛型擦除了,咋样,我说的够明白吗?也就是说啊,泛型就是在编译阶段做了一个检查,在编译阶段,不符合你指定的类型的数据都会检查报错,比如下面的代码:

ArrayList stringArrayList = new ArrayList<>();

因为指定了具体的泛型实参String,那么你这里就只能添加字符串,编译阶段是会检查的,一旦你添加的不是字符串,它就会报错,你看: 等你添加完毕,这个ArrayList里面就都是字符串类型了,到了运行阶段,这里的泛型信息就被擦除了,没泛型啥事了,也就是本身ArrayList啥都可以存储,现在是加了个泛型,可以确保我存储的都是同种类型的。 再举个例子就是,有一座茅草屋,本来这个茅草屋谁都可以进去,但是现在规定(相当于加了泛型),身价一个亿的才能进去,但是即使进去的都是身价一个亿的,这个茅草屋还是原来那个茅草屋,它也不可能变成豪华别墅啊(只是举例子,拒绝杠精) 除了擦除,还有个转换,举个例子也就很容易明白,看代码: 这是我们之前自定义的一个简单的泛型类,现在我们使用这个类: 我们这里指定具体的泛型实参是String字符串类型,那么这个时候,代码的实际编译阶段,这个G就全部被替换成了String,也就是变成了这个样子: 这就是一个类型的转换了,说的简单点就是你定义的泛型形参在编译的阶段会被全部替换成你实际指定的泛型实参。

总结

好了,限于文章的篇幅,关于泛型有些点并没有详细展开来讲,或者有的点也没讲,对于知识的学习,是需要不断的深入和反复的迭代的,想要一次性把一个知识全部学完也是不可能的,只能说,需要不断的学习,所以关于泛型的讲解就到这了,下面我简单说下我个人对泛型的一些理解吧,希望对你有锦上添花的作用。 其实我一直觉得泛型这个东西有点像保安,就是看大门的,按照你的规定,允许谁可以进,谁不可以进,当然,这里的进不进就是可不可以存储的问题了,比如集合,是否可以向其添加数据,这是在你给规定之后,而这个规定你可以制定的多种多样,这么一来,就灵活多变了,其实关于泛型吧,刚开始理解是一道坎,理解了就觉得通透了,但是理解了也不一定算是掌握了,后面看一些源码,使用泛型的很多,可是你还是不一定懂,但是在理解了的前提下,你才有可能读懂那些源码,学习就是这样,需要不断的迭代,反复的打磨,不能停啊! 好了,泛型就说到这里,以后会根据情况适当增加和修改内容,一篇好的文章一定是反复修改出来的,也欢迎大家提出你的看法和建议,一起交流学习,文中若有不当之处,还望提出!

作者 | 庆哥

来源 | 编码之外(ID:ithuangqing)

如果你喜欢本文

请关注编码之外