2019/7/1 二刷结束留念
mark:重写equals和hashCode方法的情景~
01-集合框架(体系概述)
为什么会出现集合类呢?
面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式。
那存到哪里去捏?
有两种存储的地方可以用,第一种是数组,第二种是集合。
对象多了用集合存,数据多了用对象存,像姓名年龄这些都是数据~
可是已经有数组啦,为什么还要有集合呢?
数组是固定长度的,集合是可变长度的。
而且,数组当中存储对象的时候,它只能存储同一种类型的对象,比如说全存Demo或者全存Person,因为数组定义的时候已经指定了数据类型啦。
而集合不是,对它来说只要是对象就行。先存一个new Person()进去,再存一个new Demo()进去,都没问题哒。
所以,集合的特点是:集合的长度是可变的,集合可以存储不同类型的对象。
集合作为一种容器,也划分成了很多种。对它们不断的进行共性抽取,就形成了一个体系,我们叫做集合框架。
嘿嘿,这个图看着理解一下:
这个体系里面包含很多内容。
框架产生之后,我们一般会先看什么呢?
是不是会先看顶层呀~因为顶层当中定义的是这个体系当中最共性、最基本的行为,把这个顶层看明白之后,这个体系的基本功能就了解了。
看完了顶层之后,找底层来用~
为什么呢?
有两点原因:
1,不断向上抽取出来的东西很有可能是不能创建对象的,它可能是抽象的。
2,创建子类对象,方法更多一些。
所以说,参阅顶层,创建底层。
而这个顶层,就叫Collection,收集、集合的意思。
集合框架是工具包中的一个成员,它在java.util这个包中。
Collection是一个接口。在不断向上抽取的过程中,全变成抽象辣~它有很多实现类,也有很多子接口。
因为子接口太多,就不挨个讲啦。重点讲两个:List和Set。
把刚刚那个示意图简单填充了一下:
这些就是我们要重点讲的内容。
那么问题来啦,为什么要出现这么多种容器呢?
因为每一个容器对数据的存储方式都有不同。
这个存储方式称之为:数据结构。
02-集合框架(共性方法)
我们先从Collection开始学起~
Colletion是Colletion层次结构中的跟接口。
方法摘要:
在上述方法中,我们发现了一些不清楚的数据类型:
我们看到的这些不懂的字母,就先把它们理解成Object类~
顶层方法看完该建立子类对象啦,我们先建立ArrayList对象吧~
add方法的参数类型是Object,以便于接收任意类型对象。
集合中存储的都是对象的引用(地址)。就像这样,简单示意图:
打印集合对象:
删除元素:
运行结果:
写这句就全删啦(这个叫清空集合):
判断元素:
取交集:
取交集,al1中只会保留和al2中相同的元素~
再试试removeAll,是将和al2中相同的元素删掉~
03-集合框架(迭代器)
在Collection中,有一个方法叫iterator:
它是一个接口诶:
这个接口中有三个方法:
试着用一下:
Iterator it=al.iterator();//获取迭代器,用于取出集合中的元素。
我们发现它将集合中的前两个元素拿出来啦。
但是如果这个集合中元素很多,这样一个一个写就太麻烦了。
我们有更方便的方法呢:
那么什么是迭代器呢?
其实就是集合的取出元素的方式。
对于取出这个动作,一个函数不能完全描述,需要用多个功能来体现,这种情况下,就需要将功能们封装到一个对象中去。
画个图解释一下迭代器的特点,这里有三种容器,每个容器中都有一个取出的对象:
而且,因为数据结构的不同,每个取出对象中的实现方式也不一样。
那么,这个取出就需要被描述一下,怎么描述呢?
通过一个类来完成,而这个类,就定义在了集合的内部。
为什么定义在集合内部呢?
因为元素就在集合内部,如果想要操作这些元素,是不是集合的内部类最方便啦。
一般取出分为两步:1,判断元素是否存在2,若存在则取出。
根据容器数据结构的不同,每个容器判断和取出的具体实现细节不一样。但是它们都是有共性内容:判断和取出,那么可以将共性抽取,形成一个接口,这个接口就是Iterator:
这些内部类都符合一个规则,该规则就是Iterator。
如何获取集合的取出对象呢?
通过一个对外提供的方法:iterator();
这个对外提供的方法就相当于娃娃机外面的操纵杆,而夹子就相当于迭代器,它是封装在娃娃机里面的,可以取出娃娃,只对外暴露了操纵杆这个方法,可以让人使用。不同的娃娃机,它们的夹子也不一样,有两个钩的,有五个钩的,实现方法都不一样,但它们都是夹子~
后面我们再定义新的容器,只要将它的取出方法实现Iterator接口就可以啦:
取出方式就这样被统一啦,再新加集合也不怕~
除了用while循环,它也可以用for循环写:
这两种循环有什么不同呢?
推荐用for循环,因为for循环结束后,it就释放了,不再占用内存空间,而while循环的话则依然占用内存空间。
04-集合框架(List集合共性方法)
Collection
|——List:元素是有序的,元素可以重复,因为该集合体系有索引。
|——Set:元素是无序,元素不可以重复,该集合当中没有索引。
我们先讲List。
我们去List类中看一看它的方法~共性方法就不说啦,它的特有方法有:
在指定位置插入指定元素:
通过索引获取元素:
获取元素的索引:
List集合特有的迭代器:
移除指定元素:
改变指定位置的元素:
获取指定区间元素(包含头不包含尾):
综上,凡是可以操作角标的方法都是List体系特有的方法。
总结一下:
接下来挨个演示一遍。
在指定位置添加元素:
删除指定位置的元素:
修改元素:
通过角标获取元素:
获取所有元素:
如上,List集合有自己特殊的取值方式,就是遍历。
我们用迭代器也可以实现这个功能:
05-集合框架(ListIterator)
通过indexOf获取对象的位置、获取指定区间的元素:
接下来看一下列表迭代器listIterator。
它和普通迭代器到底有什么不同呢?
看示例:
程序运行过程中挂掉了:
我们来看一下这个异常:
我们来分析一下:
所以就存在安全隐患啦。不能同时用多种方式操作同一种元素,否则可能会发生并发修改异常。
那该怎么解决呢?
要么全用集合的方法,要么全用迭代器的方法。
全用迭代器的方法:
java02不是被删除了吗?为什么篮框部分还被打印了呢?
因为虽然这个元素的引用被移除了,可是在移除前它还被obj使用了,后面打印的是obj~
注意,刚刚用的都是迭代器的操作,做了判断、取出和移除,迭代器只能做这三个动作,添加和修改动作它做不了,有局限性。
不要怕~
它也知道自己有局限性,所以安排了一个小弟:
列表迭代器ListIterator有指针,有角标,所以它的方法比它的爸爸Iterator多多啦:
增删查改样样都可以呢。
List集合特有的迭代器:ListIterator是Iterator的子接口。
我们用一下~
添加:
修改:
除了hasNext,它还有hasPrevious:
试一下~
我们反向取一次:
实际开发中逆向遍历用的少一些,正向遍历用的多一些。
ListIterator出现后,可以对集合进行在遍历过程中的增删改查。
06-集合框架(List集合具体对象的特点)
Colletion中List集合的共性方法说完以后,介绍一下List集合中常见的三个子类对象: ArrayList、LinkedList、Vetor。
这三个子类对象的出现,是因为底层的数据结构不一样而出现的,那么它们的底层到底是什么样的数据结构呢?换句话说,它们的底层到底是怎样对数据进行存储的呢?
ArrayList:底层的数据结构使用的是数组结构。特点:查询速度快。但是增删稍慢(元素不多还好,多了之后慢得就比较明显)。线程不同步。
LinkedList:底层使用的链表数据结构。特点:增删速度很快,查询稍慢。
Vector:底层是数组数据结构。线程同步。(它1.0就出现了,那个时候还木有集合框架呢,而ArrayList是1.2出现的)被ArrayList替代了。
ArrayList和Vector中,建议用ArrayList,因为效率高,无论增删还是查询,Vector都稍慢。
那多线程怎么办呐?
可以自己加锁~
那我自己加锁不安全怎么办呢?
就怕你不会加所以Java的集合框架中提供了另外一个功能,专门帮助小笨笨来加锁的~
怎么加呢?“把你的不安全的给我,我给你个安全的。”好霸道总裁。。。
还有一个小问题~ArrayList和Vector都是数组数据结构,而数组数据结构的特点是它的长度是固定的,集合却是可变长度的,所以ArrayList和Vector是可变长度的数组。什么叫可变长度的数组呢?
ArrayList默认的长度是10:
当长度超过之后,它会new一个新的数组,长度是多少呢?50%延长,变15。然后,把原来数组中的元素copy到新数组中来,再把新元素拼接到它们后面。
而Vector长度也是10,超过之后new的新数组是100%延长。
两者相比,ArrayList好一些,因为它既可以延长又可以节省空间。Vector就比较浪费空间啦。
总的来说Vector现在都不用了,像这张图中都没有Vector:
最常用的就是加粗的这四块。
07-集合框架(Vector中的枚举)
接下来讲一下Vector的特点。(不能理解既然它都不用了还要讲它的特点是为什么。。。)
Collection通用的方法:
Vector特有的方法(一般带Element的都是它的特有方法):
添加元素:
查找元素:
插入:
删除:
还有其他的:
我们来看一下这个方法:
这是什么东西鸭,木有见过呢:
点进去这个接口看一下(这个接口叫枚举):
那这个接口都有什么方法呢:
话不多说,先玩一下~
枚举就是Vector特有的取出方式。
发现枚举和迭代器很像。
其实,枚举和迭代是一样的。
因为枚举的名称以及方法的名称都过长,所以被迭代器取代了,枚举郁郁而终了。
但是IO当中有一个对象用到了枚举,因为那个对象也是1.0粗现的,那个时候没有迭代,只有枚举。
Vector和ArrayList的区别是什么?
Vector支持枚举,ArrayList没有枚举。
08-集合框架(LinkedList)
我们来看一看LinkedList里面有什么特有方法~
试一下,addFirst:
再一次,addLast和getFirst、getLast:
removeFirst:
get...()和remove...()方法的区别:get可以获取元素但不删除元素,remove获取元素,但是元素被删除。
想把里面元素全取出来~
正着取:
倒着取:
removeFirst方法会抛出异常:
到了后期,LinkedList方法升级了,它里面加了一个新的方法,pollFirst/Last:
如果列表为空它不再抛出异常,而是返回null,这是1.6版本出现的,以后推荐用这个方法喔。
获取但不移除元素的新方法:
插入元素的新方法:
它们替代了之前旧的方法。
09-集合框架(LinkedList练习)
练习:使用LinkedList模拟一个堆栈或者队列数据结构。
描述队列:
封装好之后,我们就可以直接使用啦:
注意哦,像这样的封装很常见。
为什么要封装呢?我们直接用LinkedList不就可以完成了吗?
但是LinkedList只有自身的含义,叫做链表,我们想把它做成跟我们项目相关的一些容器,那么我们需要起一些特定的名称来用更方便一些,这个时候我们就会将原有的集合封装进我们的描述当中,并且对外提供一个自己更容易识别的方法名称。
好啦,存完之后该取啦:
10-集合框架(ArrayList练习)
练习:去除ArrayList中的重复元素。
思路:创建一个新容器,将旧容器的东西一个个放进去,放的时候判断它是否已存在在新容器~
代码:
接下来删除重复的:
11-集合框架(ArrayList练习2)
练习:将自定义对象作为元素存储到ArrayList集合总,并去除重复元素。
比如:存人对象。将同姓名同年龄,视为同一个人,为重复元素。
思路:
Person类:
主函数中:
可是为什么这样写会出错呢?
因为在往容器里面存Person的时候,add的其实是Object,al.add(Object obj),因为只有它能接收任意对象。把Person传进去,其实相当于Object obj=new Person("lisi01",30);,这时Person就被提升为Object了。而后面的it.next()往回返的时候,返回的是Object,Object中并没有getName方法,多态编译失败。
应该改成这样:
两句合一句:
下面一句话也改一下:
运行:
搞定。
接下来去重。
将之前写好的singleElement方法直接拿过来用:
我们可以看到,contains方法中其实调用了equals方法:
而默认的equals方法显然不适用于判断我们新建立的对象,所以我们要在Person类中重写equals方法:
调用singleElement方法:
删除成功:
List集合判断元素是否相同,依据的是元素的equals方法。 (其他的集合肯定不一样)
虽然主函数中没有直接调用equals方法,但是因为contains方法调用了equals方法,而singleElement方法又调用了contains方法。
这个问题搞明白之后,我们还会明白一系列其他问题。
先将重写的equals方法注释掉,我们现在删除一下其中一个元素:
但运行结果是false而且删除失败了:
为什么呢?
因为要删除这个元素,就得现在容器中寻找这个元素。remove底层也调用了equals。而默认的equals方法是对比对象是否是同一个,new的Person是新的对象,地址值不相同,所以肯定会删除失败。
现在将重写后的equals方法去掉注释,再试一下:
成功啦。
12-集合框架(HashSet)
讲完了List,那么ArrayList和LinkedList该用哪一个呢?
如果元素中涉及了频繁的增删操作,用LinkedList。
如果增删操作不频繁,可以选择用LinkList,也可以选择用ArrayList。
如果涉及了增删,同时又涉及了查询,建议使用ArrayList。因为其实频繁的增删操作并不多见,一般的情况下都是查询比较多。所以ArrayList作为一个最常见、最大频率使用的容器存在,当实在不知道该用谁,就用ArrayList。
List派系讲完啦,接下来讲Set派系。
|——Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。(想重复找List,不重复找Set)
|——HashSet:底层数据结构是哈希表。
|——TreeSet:
看一下Set接口的方法,发现Set集合的功能和Colletion是一致的。Colletion的方法我们之前都讲过啦,所以就不重复讲啦。
每个对象都有哈希值,那什么是哈希表呢?
示例:
发现它们都有哈希值:
接下来讲讲什么是哈希表:
很简单,存放哈希值的表就叫哈希表。哈希表的顺序和元素的存放顺序无关,和哈希值的大小有关。而取元素的时候也是按哈希值来取。
那会不会有哈希值相同的情况呢?
若哈希值相同,会判断哈希值所指的对象是否相同,若不同,会在这个哈希值上再向下顺延一个,表中有两个这个哈希值(不知道我理解的对不对,如果相同,那该怎么找对应的对象呢):
验证无序:
验证唯一性:
13-集合框架(HashSet存储自定义对象)
还和11中的例子一样,存储Person到HashSet中。
将那个例子中的代码都复制了过来,主函数中这样写:
我们再试试存入重复的Person对象,发现成功了:
为什么即使重写了equals方法,还是会出现重复呢?
因为现在往里面存的四个对象都是独立的,都有独立的哈希值,都存入了哈希表。
而我们现在要做的,就是覆盖Person的哈希值方法,建立Person对象自己的哈希值。
怎么建立哈希值呢?
你的判断条件是什么,我就依据你的条件来建立哈希值。比如这里是按照姓名和年龄来判断对象是否重复的,那我们就按照姓名和年龄来建立哈希值。
代码:
注意,只有当哈希值相同时,才会调用equals方法对比内容是否相同。
|——Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。(想重复找List,不重复找Set)
|——HashSet:底层数据结构是哈希表。
HashSet是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode和equals来完成。
如果元素的HashCode值相同,才会判断equals是否为true。
如果元素的hashcode值不同,不会调用equals。
|——TreeSet:
所以注意,在开发中,只要描述的对象要往哈希集合中存时,一般都会复写hashCode和equals。
还有注意,为了保证哈希值的唯一性,一般会给这里乘个数字(两个对象万一两部分加起来正好是同一个值就重复了):
14-集合框架(HashSet判断和删除的依据)
注意,HashSet对于判断元素是否存在,以及删除等操作,依赖的方法是元素的hashCode和equals方法,而且先依赖hashCode,再依赖equals。
ArrayList判断元素是否存在,以及删除等操作,只依赖equals。
这个原因全都在数据结构上,数据结构不同,它依赖的方法也不一样。