1 泛型机制(熟悉)
1.1 基本概念
(1)通常情况下集合中可以存放不同类型的元素,是因为将所有对象都看作Object类型放入,因此从集合中取出元素时,也是Object类型,为了表达该元素真实的数据类型,则需要强制类型转换,而强制类型转换可能会引发类型转换异常。
分析:
那么,在介绍集合相关的类库之前呢?我们首先先介绍一个概念,叫泛型机制。什么叫泛型呢?泛型从字面意思来理解,所谓的泛就是广泛,所谓的型其实就是类型。那合在一起就是说广泛的类型。对吧,那么在JAVA中,那到底什么是泛型呢?我们先回顾一个代码。我们回顾一下我们之前写过的代码。
1.1.1 问题1(为什么集合中可以存放各种不同类型的对象呢):
但现在我有一个问题,就是为什么集合中可以存放各种不同类型的对象呢?
答案就在这里。看明白了吗?注意看第二个add方法就行,或者说第一个也可以,对吧?你看第二个add方法怎么说的?也就是说我在调add方法的时候,它提示我的时候,这个参数的类型是什么类型?是object类型。那什么是object类型?object类型是JAVA官方,写的一个类,它是我们啊JAVA语言中所有类的一个根类。而add这个方法的形参类型,它提示我们的是object类型,那意味着什么?大家注意,那意味着我们只要让object类型往这一站,意味着实参,只要是个对象,是不是都可以传过来给它赋值,大不了形成多态呗,这一点我们在讲equals方法的重写的时候,是不是当时就强调过这一点?那这样一来,我觉得大家应该大悟了。为什么说大悟了呢?因为你现在就应该能明白,集合中之所以能够存放不同类型的对象,其根本原因是因为什么?是因为他把这些对象全部当做object类型放进去的,那说到底,集合中元素的类型其实还是什么的一样的,只是看似不一样,实质上还是一样的,因为全部都当做是object类型放入的。对不对?好啦,那既然全是当做object类型放入的,对不对?就是无论你是String类型还是int类型还是person类型,我是不是全部都是按照object类型放进去的?那么取的时候,当然也就是什么类型object类型取的。
1.1.2 问题2:具体问题的描述看图片
所以大家应该能明白为什么在我们这个代码中?我想要用string类型接的时候,为什么要做强转?
因为我们放入的时候按object类型放的,那取出来当然也是object类型,为了表达这个元素最真实的数据类型,我是不是就得需要强转,因为父类到子类是大到小,所以强转。但是强转的时候是不是一定要慎重一些?为什么?因为你如果不够慎重,你如果不小心啊,眼花了,看错了,是不是容易发生类型转换异常。所以总结一下,也就是说集合中虽然支持可以存放不同类型的对象,但实际上这种方式它是一种优点,但实际上是不是也带来一些缺陷?那就是我们取元素的时候很费劲儿,而且一旦类型转换的不太对或者我不知道集合中的这个元素到底是什么类型的时候,我是不是只能尝试?这会带来一些类型转换异常等等,一些不必要的错误。所以这是我们啊,就是集合中在我们之前使用集合时,它的一个特点。对吧,有优点当然也有缺点。
(2)为了避免上述错误的发生,从java5开始增加泛型机制,也就是在集合名称的右侧使用<数据类型>的方式来明确要求该集合中可以存放的元素类型,若放入其他类型的元素则编译报错。
分析:
然后这里面放一个类型比如说叫integer。相当于这样一个写法,那这样一个写法什么意思啊?哎,表示的是明确要求该集合中可以存放的元素类型,就是我这儿只要加了一个尖括号就是相当于。告诉编译器,哥们儿,我这个List的集合是用来放integer类型的。对不对?好,那大家想想,如果我要求放的是intger类型,那么我如果在下面儿再放像string类型,你看光标放上面儿。这儿是不是就报错了?为啥因为你上面儿已经明确说明了,说你要的是integer类型,你这个集合里面,结果你现在放的这不是integer类型,是String类型,你看。你要的是integer,我给你的string类型不匹配,所以报错呗。明白了吗?哎,这就是我们泛型机制的一个概念,总结下来啊,就是在集合类型的右侧是不是指定了集合的数据类型,当然,有的小伙伴可能在想,那在集合名称的右侧指定了数据类型之后,那这个集合不就不能再存放多种不同类型的对象了吗?相对之前的方式不就没那么灵活了吗?没错啊,这是它的有舍有得嘛,这是舍弃的东西,但是它也得到了好处啊。得到什么好处?(使用泛型的好处)因为我已经规定了这个集合中能放的类型是什么类型?那么后续取的时候是不是取到的还是这个类型?对不对?好那么实际上是不是也就是避免了一些放入其他类型元素时的一些错误?以及获取元素时类型转换的一些错误,是不是可以避免这些类型转换异常的发生啊?
(3)泛型只在编译期有效,在运行时期不区分是什么类型(对应下面图片中蓝框部分)。
然后另外这儿有一个小小的细节,给大家强调一下什么细节呢?就是大家会发现我们前面的类型跟后面儿的类型是不是写的一模一样。所以java官方会觉得有点重复,所以后面数值的<>中的数据类型可以省略。
这块儿的话呢,就我们刚刚画这个图,到了底层实际上是一样的,都是List集合。但是的话呢,不好意思,在编译阶段的话呢,它实际上还是错误的,为啥因为你编译过不去嘛,编译阶段我只检查类型,我只检查是否满足JAVA的语法规范,所以很多小伙伴呢,看到这句话之后,他就容易理解错了,理解成什么呢?就是这样是赋值可以。实际上是不可以的。明白了吧,就是虽然到了运行时期不区分是什么类型的,但是编译阶段过不去,是不是也走不到运行的阶段啊?所以这一块是一个笔试的考点。
我可能在栈区中申请一块内存,我new的时候在堆区中申请一块内存。
然后我在下面儿的时候,相当于我又在栈区区中申请一块内存,我又在堆区中申请一块儿内存,对不对好,然后各自是不是指向自己的内存空间?那你想想,如果这一行代码能够通过的话,那是不是就相当于是?我下面的这个代码或者说我是我下面这个这个变量是不是也指向上一面这一块内存空间了?那你想想,如果这一行代码允许通过的话,
按照我的语法规范,我是不是可以放一个double类型的数据,比如说3.14放进去了。但不好意思,这里面它支持吗?
这块堆区空间是不是只能放string类型,不是string类型不允许?所以为了避免这种错误的发生,实际上是不是就不允许这种赋值啊?明白这意思了吧?
1.2 底层原理(下面的红字一定要理解很重要)
(1)泛型的本质就是参数化类型,也就是让数据类型作为参数传递,其中E相当于形式参数负责占位,而使用集合时<>中的数据类型相当于实际参数,用于给形式参数E进行初始化,从而使得集合中所有E被实际参数替换,由于实际参数可以各种各样广泛的数据类型,因此得名为泛型。
如:
//其中i叫做形式参数,负责占位
//int i=10;
//int i=20;
public static void show(int i){
........
}
//其中10叫做实际参数,负责给形式参数初始化
show(10);
show(20);
其中E叫做形式参数,负责占位
E=String;
E=Integer;
public interface List<E>{
.......
}
//其中String叫做实际参数
List<String>lt1=.....;
List<Integer>lt2=......;
分析:大家要注意的一点,就是以前我们传的都是数值内容(上面代码中的show(10))。但是这里面我们传的都是什么数据类型而已(上面代码中的List<String>lt1)。
1.2.1 问题3:判断一个类或者一个接口是否支持泛型?
你就看它有没有尖括号
1.3 自定义泛型接口
(1)泛型接口和普通接口的区别就是后面添加了类型参数列表,可以有多个类型参数,如:<E,T.......>等。
在代码中的展示:
1.4自定义泛型类(泛型类被继承时的处理方式)
(1)泛型类和普通类的区别是类名后面添加了参数列表,可以有多个类型参数,如:<E,T......>等。
(2)实例化泛型时应该指定具体的数据类型,并且是引用数据类型而不是基本数据类型。
(3)父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型。
(4)子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型。
分析:
1.4.1 问题4:就是当一个泛型类被继承时,子类如何处理?
处理方式一:不保留泛型并且没有指定数据类型,此时Person类中的T为Object类型
意思就是说你person类中不是有一个泛型叫T吗?我现在SubPerson我继承自你之后,我不仅仅没有把你那个T保留下来,而且的话呢,我也没有给你这个T指定具体的类型。
注意:在上面代码中的SubPersonTest类中如果在创建对象时指定数据类型,比如说我们指定的数据类型是String,是会报错的。(报错的前提:用的是处理方式一)
处理方式二:不保留泛型,但指定了数据类型,此时Person类中的T为String类型
就是我继承person类的同时,我就指定了你person类里面的这个T。由什么来着?由String类取代
注意:我上面写的两种处理方式的共性和区别
共性: 都不保留泛型(意思就是在我们用测试类来声明对象的时候不能出现<>)
通过我们在测试类中声明对象时,就可以看出,我们是没有写<>的,总之就记住有<>,那就代表不保留泛型,如果没有<>,那就代表保留泛型
区别:一个没有指定Person类中T的数据类型,一个指定了(可以为String类型,Boolean类型........我们这里用String类型为例),具体体现,我们在用对象点的方式在调用setGender方法时,返回的数据类型不同,前者没有指定T的数据类型时,setGender返回的数据类型是Object,而指定了的返回的是String类型。
处理方式三:保留父类的泛型,在构造对象时可以指定T的类型
注意:如果用这种处理方式,我们也可以在构造对象时不指定T的类型,此时T就会被默认指成Object类型(和我们处理方式一的处理结果一样) 。
处理方式四: 保留父类的泛型,同时在子类中增加新的泛型
注意:如果用这种处理方式,我们也可以在构造对象时不指定T的类型,此时T就会被默认指成Object类型(和我们处理方式一的处理结果一样) 。
1.5 自定义泛型方法(泛型方法的定义和使用)
(1)泛型方法就是我们输入参数的时候,输入的是泛型参数,而不是具体的参数,我们在调用这个泛型方法的时需对泛型参数进行实例化。
(2)泛型方法的格式:
[访问权限] <泛型> 返回值类型 方法名([泛型标识 参数名称]){方法体;}
(3)在静态方法中使用泛型参数的时候,需要我们把静态方法定义为泛型方法。
分析:
1.6 泛型在继承上的体现
(1)如果B是A的一个子类或子接口,而G是具有泛型声明的类或接口,则G<B>并不是G<A>的子类型!
比如:String是Object的子类,但是List<String>并不是List<Object>的子类。
1.7 通配符的使用
(1)有时候我们希望传入的类型在一个指定的范围内,此时就可以使用泛型通配符了。
(2)如:之前传入的类型要求是Integer类型,但是后来业务需要Integer的父类Number类也可以传入。
(3)泛型中有三种通配符形式:
<?>无限统配符:表示我们可以传入任意类型的参数(表示的是可以指定任意的类型,并不是任意类型的对象)。
<? extends E>表示类型的上界是E,只能是E或是E的子类。
<? super E>表示类型的下界是E,只能是E或者是E的父类。
分析:
所以到了以后的开发里面,如果我们有两种不同的这种泛型类型时,我们想要描述它们的公共父类是不是就可以用这个?类型时,用这个的好处在于什么地方?用这个的好处在于我们找着了公共的父类,我们是不是就可以使用我们前面讲的多态的理念?
那既然我们发现这个List<?>的话呢?它是这两个类的公共父类,结果我们拿着这两个类的对象都往里面放的时候,结果它却怎么办?它却报错了,这是为啥呀?
给大家已经标的很明白了,<?>表示的是任意类型。任意类型意味着,类型的种类是不很多呀?对不对?那就意味着我的这个问号,我可以代表animal类型,我也可以代表dog类型。我甚至还可以代表dog类的子类类型。对吧,更小的类型,那既然我可以代表更小的类型,那你想想,如果我让你放animal,放dog,那不就有问题了吗?万一我这个<?>代表的是比dog还小的类,那你现在把animal跟dog类型的对象往里面放,那当然类型不匹配,当然应该报错了呗。
所以这个<?>的这种通配符啊,这种这种通配符啊是不支持什么来着呢?是不支持这个元素的添加操作。就是说,虽然它是公共父类,但是不支持把元素往里面添加,为啥?因为我不知道你到底放什么类型。而且我<?>又是任意的。
2 Set集合(熟悉)
2.1 基本概念
(1)java.util.Set集合是Collection集合的子集合,与List集合平级。
(2)该集合中元素没有先后放入次序(没有先后放入次序不代表随机),且不允许重复。
我们用Set集合的场合呢,实际上就是用它实现去重。
(3)该集合的主要实现类是:HashSet类和TreeSet类以及LinkedHashSet类。
(4)其中HashSet类的底层是采用哈希表进行数据管理。
(5)其中TreeSet类的底层是采用红黑树进行数据管理的。
(6)其中LinkedHashSet类与HashSet类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也是元素插入集合中的先后循序,因此便于迭代。
分析:
因为我们说Set的集合不是没有先后次序吗?对不对?没有先后放入次序,那意味着就是说我们打印出来的结果是不是不一定?跟你放入的先后次序是不是一样的?就是打印出来的次序啊,不一定一样是不是?那这个时候怎么办?我用LinkedHashSet,我是不是就可以把放入次序记录起来?再打印打出来的结果是不是就是放入的次序?
补充:哈希表和红黑树
大家现在看到这个哈希表是一个什么结构清楚了吗?是一个每个元素都是一个单项链表的一维数组,那这样的数组我们把它叫链表数组
2.2 常用的方法
(1)参考Collection集合中的方法即可!
(2)案例题目:
准备一个set集合指向HashSet对象,向该集合中添加元素“two”并打印,再向集合中添加元素“one”并打印,再向集合中添加”three“并打印,再向集合中添加”one“并打印。
补充上面的图片:第一个就set这个集合中元素没有先后放入次序,而且同时也验证了元素不能重复。
2.3 元素放入HashSet集合中的原理
(1)使用元素调用hashCode方法获取对应的哈希码值,再由某种哈希算法计算出该元素在数组中的索引位置。
(2)若该位置没有元素,则将该元素直接放入即可。
(3)若该位置有元素,则使用新元素与已有元素依次比较哈希码值,若哈希码值不相同,则将该元素直接放入。
(4)若新元素与已有元素的哈希码值相同,则使用新元素调用equals方法与已有元素依次比较。
(5)若相等则添加元素失败,否则将元素直接放入即可。
思考:为什么要求重写equals方法后要重写hashCode方法呢?
解析:当两个元素调用equals方法相等时证明这两个元素相同,重写hashCode后保证这两个元素得到的哈希码值相同,由同一个哈希算法生成的索引位置相同,此时只需要与该索引位置已有元素比较即可,从而提高效率并避免重复元素的出现。
2.4 TreeSet集合的概念
(1)二叉树主要指每个节点最多只有两个子节点的树型结构。
(2)满足一下3个特征的二叉树叫做有序二叉树。
a.左子树中的任意节点元素都小于根节点元素值。
b.右子树中的任意节点元素都大于根节点的元素值。
c.左子树和右子树的内部也遵守上述规则。
(3)由于TreeSet集合的底层采用有序二叉树进行数据管理,当有新元素插入到TreeSet集合时,需要使用新元素与集合中已有元素依次比较来确定新元素的合理位置。
(4)比较元素大小的规则有两种方式:
使用元素的自然排序规则进行比较并排序,让元素类型实现java.lang.Comparable接口;
使用比较器规则进行比较并排序,构造TreeSet集合时传入java.util.Comparator接口;
(5)自然排序的规则比较单一,而比较器的规则比较多元化,而且比较器优先于自然排序;
总结:
那总结下来,TreeSet的集合底层就是一棵特殊的有序二叉树,叫红黑树呗。为了让元素放进去之后,依然保证有序,我得需要给元素的之间指定比较大小的规则,而指定的比较规则的方式有两种,一种叫自然排序,一种叫比较器。
2.4.1 TreeSet集合中实现自然排序
补充下面图片的内容:前提是我们已经完成了一个Student类的一个封装
总结我们上面三张图片的写法:
我们现在的这种写法是不是有点太耍流氓了?就是不管他们的值是多少,是不是上来就要么都相等,要么小于,要么大于。所以开发中,肯定不是这么写的。因为你想想哪有这样的,就是一棒子全打死的呀。对吧,我们应该要拿着它真正的值去比一下对不对,那怎么比呀?那这个时候实际上我们是不是可以拿着他真正的值比,那按照哪个比啊?其实你喜欢用姓名就用姓名比,你喜欢用年龄就用年龄比,如果你喜欢两个都比都可以。
我们先按照名字比:
按年龄比:
还有的小伙伴有想法说能不能在姓名相同的情况下再比年龄呢?可以啊if判断
上面用if语句过于繁琐我们可以用三目运算符
2.4.2 TreeSet集合中实现比较器排序
实际上所谓的比较器的方式啊,就是我们人为指定的规则。
然后在构造的时候,我们实际上是不是就可以把这个比较器实际上是不是可以作为参数传给构造方法呀?所以在这里面传谁呀?c1。补充上面图片中的内容
此时大家会发现student类里面相当于我自然排序的代码也在我这儿,是不是又传了一个比较器的规则,那自然排序的规则是按什么比?先按姓名比姓名相同,按年龄比。而比较器,是直接按年龄比,那到底谁优先啊?应该是比较器,因为毕竟是我人为指定的嘛。
3 Map集合(重点)
3.1 基本概念
(1)java.util.Map<K,V>集合中存取元素的基本单位是:单对元素,其中类型参数如下:
K-此映射所维护的键(key)的类型,相当于目录。
V-映射值(Value)的类型,相当于内容。
(2)该集合中K是不允许重复的,而且一个key只能对应一个value.
(3) 该集合的主要实现类有:HashMap类,TreeMap类,LinkedHashMap类,Hashtable类
(4)其中HashMap类的底层是采用哈希表进行数据管理的。
(5)其中TreeMap类的底层是采用红黑树进行数据管理的。
(6)其中LinkedHashMap类与HashMap类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也就是元素插入集合的先后顺序,因此便于迭代。
(7)其中Hashtable类是古老的Map实现类,与HashMap类相比属于线程安全类,且不允许null作为key或者value的数值。
(8)其中Properities类是Hashtable类的子类,该对象用于处理属性文件,key和value都是String类型的。
(9)Map集合是面向查询优化的数据结构,在大数据量情况下有着优良的查询性能,
(10)经常用于根据key检索value的业务场景。
小总结:
你会发现真正我们去关注的还是前两类HashMap类和TreeMap类。HashMap类的底层是采用哈希表;TreeMap就是底层是红黑树,实际上是不是跟set的集合的特点是一样的,只不过这里面实际上是不是就相当于是是单对元素;而set的集合,里面是不是单个元素,是单对元素。
3.1.1 问题五:那set集合跟map集合之间又有什么样的关系呢?
在代码中的理解:我们进入的是hashSet类的内部
3.2 常用的方法
3.2.1Map集合实现元素的增加和修改
对方法V put(K key,V value)的使用:
3.2.2 Map集合实现元素的查找和删除操作
boolean containsKey(Object key),boolean containsValue(Object value),V get(Object key),V remove(Object key)方法的使用
3.2.3 Map集合的三种遍历方式
Set<K> keySet()【翻译过来意思就是说把这个map集合中所有的key拿出来组成一个set集合,当然这里面提到俩字儿叫视图啊,视图的意思就是说相当于只是开了一个窗口,并没有真正的把数据拿出来啊。明白这意思吧,】,Collection<V> values(),Set<Map.Entry<K,V>>entrySet()方法在代码中的使用:
遍历Map集合的方式已经有上面一张图片中的3种+toString方法
3.3 元素放入HashMap集合的过程
(1)使用元素的key调用hashCode方法获取对应的哈希码值,再由某种哈希算法计算在数组中的索引位置。
(2)若该位置没有元素,则将该键值对直接放入即可。
(3)若该位置有元素,则使用key与已有元素依次比较哈希值,若哈希值不相同,则将该元素直接放入。
(4)若key与已有元素的哈希值相同,则使用key调用equals方法与已有元素依次比较。
(5)若相等则将对应的value修改,否则将键值对直接放入即可。
3.4 相关的常量
(1)DEFAULT_INITIAL_CAPACITY:HashMap的默认容量是16。
分析:这个容量指的是什么呢?数组的长度。就是我们的哈希表啊,它底层首先是一个数组嘛。对不对?那这个数字多大呢?哎,就是16这么大。
(2)DEFAULT_LOAD_FACTOR:HashMap的默认加载因子是0.75。
(3)threshold:扩容的临界值,该数值为:容量*填充因子,也就是12。
(4)TREEIFY_THRESHOLD:若Bucket中链表长度大于该默认值则转化为红黑树存储,该数值是8。
(5)MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,该数值是64。
分析:
那加载因子什么意思呢?实际上是一个比例。我这儿写了一下,16是初始容量,然后呢?加载因子是0.75,然后它俩乘起来是12,12叫什么呢?叫扩容的临界值。啥意思啊?就是当我们这个哎,容量中有12个位置已经有元素了,我们是不是就相当于达到临界值了,当我们达到临界值,我们就会对数组进行扩容。
实际上,数组扩容的目的是为了让,我们这些元素尽可能分散在不同的下标,或者说每一个下标上所链接的元素应该尽可能的少,这样一来,我遍历的时候,我是不是就可以更快的找到我们这个元素了?减少比较的次数,提高效率。这就是为什么要扩容。
然后呢?当然为了再一步提高性能啊,我们还有一个特点,就是让它达到一个临界的一个值,之后我们会由链表转换成红黑树啊,这就是更先进了
案例题目:
准备一个HashMap集合,统计字符串”123,456,789,123,456“中每个数字字符串出现的次数并打印出来。如:
123出现了2次
456出现了2次
789出现了1次
4 Collection类(工具类)
4.1 基本概念
(1)java.util.Collections类主要提供了对集合操作或者返回集合的静态方法。
4.2 常用的方法
对上面方法的应用
补充:要想知道这个错误的根本原因啊,那无非就是去查手册或看原文呗 (我们代码报错通常要 想的内容)
总结: