在看集合类之前, 我们要先明白一下概念:
1.数据结构
(1):线性表
[1]:顺序存储结构(也叫顺序表)
一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。
[2]:链表
链表里面节点的地址不是连续的,是通过指针连起来的。
(2):哈希表
解释一:
哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
解释二:
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
Hash 表的查询速度非常的快,几乎是O(1)的时间复杂度。
hash就是找到一种数据内容和数据存放地址之间的映射关系。
散列法:元素特征转变为数组下标的方法。
我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用“链表”。我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。
散列表的查找步骤
当存储记录时,通过散列函数计算出记录的散列地址
当查找记录时,我们通过同样的是散列函数计算记录的散列地址,并按此散列地址访问该记录
优缺点
优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。
哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。
如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
哈希表的原理:
1,对对象元素中的关键字(对象中的特有数据),进行哈希算法的运算,并得出一个具体的算法值,这个值 称为哈希值。
2,哈希值就是这个元素的位置。
3,如果哈希值出现冲突,再次判断这个关键字对应的对象是否相同。如果对象相同,就不存储,因为元素重复。如果对象不同,就存储,在原来对象的哈希值基础 +1顺延。
4,存储哈希值的结构,我们称为哈希表。
5,既然哈希表是根据哈希值存储的,为了提高效率,最好保证对象的关键字是唯一的。
这样可以尽量少的判断关键字对应的对象是否相同,提高了哈希表的操作效率。
扩展:
相同的字符串如果存进去,哈希值相同并且equals方法为true,不会存入相同的
只要哈希值相同或者equals方法为true都成立才不会存入,只要其中一条不满足,都会储存
哈希表存储过程:
1.调用对象的哈希值(通过一个函数f()得到哈希值):存储位置 = f(关键字)
2.集合在容器内搜索有没有重复的哈希值,如果没有,存入新元素,记录哈希值
3.再次存储,重复上边的过程
4.如果有重复的哈希值,调用后来者的equals方法,参数为前来者,结果得到true,集合判断为重复元素,不存入
哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?
哈希冲突的解决方案有多种:
开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)
再散列函数法
链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式
关于hashcode和equals的一些问题,在面试中会问道:
1.两个对象哈希值相同,那么equals方法一定返回true吗?
不一定:取决于如何重写equals,如果重写固定了它返回false,结果就一定是false
2.equals方法返回true,那么哈希值一定相同吗?
一定:如果类中定义一个静态变量(static int a = 1),然后重写hashcode返回a+1,那么每一个对象的哈希值都不一样,不过java中规定:对象相等,必须具有相同的哈希码值,所以这里是一定的
(3)数组
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
(4)区别
1.数组
优点:(1)随机访问效率高(根据下标查询),(2)搜索效率较高(可使用折半方法)。
缺点:(1)内存连续且固定,存储效率低。(2)插入和删除效率低(可能会进行数组拷贝或扩容)。
2.链表
优点:(1)不要求连续内存,内存利用率高,(2)插入和删除效率高(只需要改变指针指向)。
缺点:(1)不支持随机访问,(2)搜索效率低(需要遍历)。
3.Hash表
优点:(1)搜索效率高,(2)插入和删除效率较高,
缺点:(1)内存利用率低(基于数组),(2)存在散列冲突。
2.集合类种重要概念词解释
不弄清楚这些词的概念, 总感觉集合类学的很模糊!!!
(1).泛型
java中很重要的概念, 集合里面应用很多.
集合的元素,可以是任意类型对象的引用,如果把某个对象放入集合,则会忽略它的类型,就会把它当做Object类型处理.
泛型则是规定了某个集合只可以存放特定类型的对象的引用,会在编译期间进行类型检查,可以直接指定类型来获取集合元素
在泛型集合中有能够存入泛型类型的对象实例还可以存入泛型的子类型的对象实例
注意:
1 泛型集合中的限定类型,不能使用基本数据类型
2 可以通过使用包装类限定允许存放基本数据类型
泛型的好处
1 提高了安全性(将运行期的错误转换到编译期)
2 省去强转的麻烦
(2).哈希值
1 就是一个十进制的整数,有操作系统随机给出
2 可以使用Object类中的方法hashCode获取哈希值
3 Object中源码: int hashCode()返回该对象的哈希码值;
源码:
public native int hashCode();
native:指调用了本地操作系统的方法实现
(3).平衡二叉树(称AVL树)
其特点是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。
注意:
关键点是左子树和右子树的深度的绝对值不超过1
那什么是左子树深度和右子树深度呢?
如上图中:
如果插入6元素, 则8的左子树深度就为2, 右子树深度就为0,绝对值就为2, 就不是一个平很二叉树
[1].二叉排序树
1若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3左、右子树也分别为二叉排序树
解释一:
现在有a[10] = {3, 2, 1, 4, 5, 6, 7, 10, 9, 8}需要构建二叉排序树。在没有学习平衡二叉树之前,根据二叉排序树的特性,通常会将它构建成如下左图。虽然完全符合二叉排序树的定义,但是对这样高度达到8的二叉树来说,查找是非常不利的。因此,更加期望构建出如下右图的样子,高度为4的二叉排序树,这样才可以提供高效的查找效率。
平衡二叉树是一种二叉排序树,是一种高度平衡的二叉树,其中每个结点的左子树和右子树的高度至多等于1.意味着:要么是一棵空树,要么左右都是平衡二叉树,且左子树和右子树深度之绝对值不超过1. 将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
平衡二叉树的前提是它是一棵二叉排序树。
[2].旋转
假设一颗 AVL 树的某个节点为 r,有四种操作会使 r 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。使用旋转达到平衡性
1.对 r 的左儿子的左子树进行一次插入(左旋转 LL)
2.对 r 的左儿子的右子树进行一次插入(LR)
3.对 r 的右儿子的左子树进行一次插入(RL)
4.对 r 的右儿子的右子树进行一次插入(RR)
(4).红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树
(1) 检索效率O(log n)
(2) 红黑树的五点规定:
1.每个结点要么是红的要么是黑的
2.根结点是黑的
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的
4.如果一个结点是红的,那么它的两个儿子都是黑的(反之不一定)
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点
它的每个结点都额外有一个颜色的属性,颜色只有两种:红色和黑色。
示例:(这块难度比较大, 建议自行百度,查阅相关文档)
红黑树插入操作
如果是第一次插入,由于原树为空,所以只会违反红黑树的规则2,所以只要把根节点涂黑即可;
如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;
但是遇到如下三种情况时,我们就要开始变色和旋转了:
1. 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;
2. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
3. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。
下面我们先挨个分析这三种情况都需要如何操作:
对于情况1:插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。
这里考虑父节点是祖父节点的左子节点的情况(即插入一个4节点,插入的节点一般为红色,不然可能违反规则5.),如下左图所示:
对于这种情况,我们要做的操作有:将当前节点(4)的父节点(5)和叔叔节点(8)涂黑,将祖父节点(7)涂红,变成上右图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法。这样上右图就变成了情况2了。
对于情况2:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。
对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!
我们可以看出,如果是从情况1开始发生的,必然会走完情况2和3,也就是说这是一整个流程,当然咯,实际中可能不一定会从情况1发生,如果从情况2开始发生,那再走个情况3即可完成调整,如果直接只要调整情况3,那么前两种情况均不需要调整了。故变色和旋转之间的先后关系可以表示为:变色->左旋->右旋。
红黑树删除操作
我们现在约定:后继节点的子节点称为“当前节点”.
删除节点有三种情况分析:
a. 叶子节点;(直接删除即可)
b. 仅有左或右子树的节点;(上移子树即可)
c. 左右子树都有的节点。( 用删除节点的直接前驱或者直接后继来替换当前节点,调整直接前驱或者直接后继的位置)
删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则,这很好理解。如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,我们只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复。但是如果遇到以下四种情况,我们就需要通过变色或旋转来恢复红-黑树的平衡了。
1. 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的);
2. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;
3. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的;
4. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。
以上四种情况中,我们可以看出2,3,4其实是“当前节点是黑色的,且兄弟节点是黑色的”的三种子集,等会在程序中可以体现出来。现在我们假设当前节点是左子节点(当然也可能是右子节点,跟左子节点相反即可,我们讨论一边就可以了),分别解决上面四种情况:
对于情况1:当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)。如左下图所示:A节点表示当前节点。针对这种情况,我们要做的操作有:将父节点(B)涂红,将兄弟节点(D)涂黑,然后将当前节点(A)的父节点(B)作为支点左旋,然后当前节点的兄弟节点就变成黑色的情况了(自然就转换成情况2,3,4的公有特征了),如右下图所示:
对于情况2:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。如左下图所示,A表示当前节点。针对这种情况,我们要做的操作有:将兄弟节点(D)涂红,将当前节点指向其父节点(B),将其父节点指向当前节点的祖父节点,继续新的算法(具体见下面的程序),不需要旋转。这样变成了右下图所示的情况:
对于情况3:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的。如左下图所示,A是当前节点。针对这种情况,我们要做的操作有:把当前节点的兄弟节点(D)涂红,把兄弟节点的左子节点(C)涂黑,然后以兄弟节点作为支点做右旋操作。然后兄弟节点就变成黑色的,且兄弟节点的右子节点变成红色的情况(情况4)了。如右下图:
对于情况4:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。如左下图所示:A为当前节点,针对这种情况,我们要做的操作有:把兄弟节点(D)涂成父节点的颜色,再把父节点(B)涂黑,把兄弟节点的右子节点(E)涂黑,然后以当前节点的父节点为支点做左旋操作。至此,删除修复算法就结束了,最后将根节点涂黑即可。
我们可以看出,如果是从情况1开始发生的,可能情况2,3,4中的一种:如果是情况2,就不可能再出现3和4;如果是情况3,必然会导致情况4的出现;如果2和3都不是,那必然是4。当然咯,实际中可能不一定会从情况1发生,这要看具体情况了。不懂的欢迎加群讨论: QQ群:123300273 (相亲相爱的一家人)
(5).迭代器
[1].迭代器模式
把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。在java中它是一个对象,其目的是遍历并选中其中的每个元素,而使用者(客户端)无需知道里面的具体细节。
[2].Iterator
Collection集合元素的通用获取方式:在取出元素之前先判断集合中有没有元素。如果有,就把这个元素取出来,继续再判断,如果还有就再取出来,一直把集合中的所有元素全部取出来,这种取出元素的方式专业术语称为迭代。
java.util.Iterator:在Java中Iterator为一个接口,它只提供了迭代的基本规则。在JDK中它是这样定义的:对Collection进行迭代的迭代器。迭代器取代了Java Collection Framework中的Enumeration。
Collection中有一个抽象方法iterator方法,所有的Collection子类都实现了这个方法;返回一个Iterator对象
定义:
package java.util;
public interface Iterator {
boolean hasNext();//判断是否存在下一个对象元素
E next();//获取下一个元素
void remove();//移除元素
}
在使用Iterator的时候禁止对所遍历的容器进行改变其大小结构的操作。例如: 在使用Iterator进行迭代时,如果对集合进行了add、remove操作就会出现ConcurrentModificationException异常。
在进行集合元素取出的时候,如果集合中没有元素了,还继续使用next()方法的话,将发生NoSuchElementException没有集合元素的错误
修改并发异常:在迭代集合中元素的过程中,集合的长度发生改变(进行了元素增加或者元素删除的操作), 增强for的底层原理也是迭代器,所以也需要避免这种操作;
解决以上异常的方法:使用ListIterator
任何集合都有迭代器。
任何集合类,都必须能以某种方式存取元素,否则这个集合容器就没有任何意义。
迭代器,也是一种模式(也叫迭代器模式)。迭代器要足够的“轻量”——创建迭代器的代价小。
[3].Iterable(1.5)
Java中还提供了一个Iterable接口,Iterable接口实现后的功能是‘返回’一个迭代器,我们常用的实现了该接口的子接口有:Collection、List、Set等。该接口的iterator()方法返回一个标准的Iterator实现。实现Iterable接口允许对象成为Foreach语句的目标。就可以通过foreach语句来遍历你的底层序列。
Iterable接口包含一个能产生Iterator对象的方法,并且Iterable被foreach用来在序列中移动。因此如果创建了实现Iterable接口的类,都可以将它用于foreach中。
定义:
Package java.lang; import java.util.Iterator; public interface Iterable { Iterator iterator(); }
Iterable是Java 1.5的新特性, 主要是为了支持forEach语法, 使用容器的时候, 如果不关心容器的类型, 那么就需要使用迭代器来编写代码. 使代码能够重用.
使用方法很简单:
List strs = Arrays.asList(“a”, “b”, “c”); for (String str: strs) { out.println(str); }
好处:代码减少,方便遍历
弊端:没有索引,不能操作容器里的元素
增强for循环底层也是使用了迭代器获取的,只不过获取迭代器由jvm完成,不需要我们获取迭代器而已,所以在使用增强for循环变量元素的过程中不准使用集合对象对集合的元素个数进行修改;
[4].forEach()(1.8)
使用接收lambda表达式的forEach方法进行快速遍历.
List strs = Arrays.asList(“a”, “b”, “c”); // 使用Java 1.8的lambda表达式 strs.forEach(out::println);
[5].Spliterator迭代器
Spliterator是1.8新增的迭代器,属于并行迭代器,可以将迭代任务分割交由多个线程来进行。
Spliterator可以理解为Iterator的Split版本(但用途要丰富很多)。使用Iterator的时候,我们可以顺序地遍历容器中的元素,使用Spliterator的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。使用 Spliterator 每次可以处理某个元素集合中的一个元素 — 不是从 Spliterator 中获取元素,而是使用 tryAdvance() 或 forEachRemaining() 方法对元素应用操作。但Spliterator 还可以用于估计其中保存的元素数量,而且还可以像细胞分裂一样变为一分为二。这些新增加的能力让流并行处理代码可以很方便地将工作分布到多个可用线程上完成
[6].ListIterator
ListIterator是一个更强大的Iterator子类型,能用于各种List类访问,前面说过Iterator支持单向取数据,ListIterator可以双向移动,所以能指出迭代器当前位置的前一个和后一个索引,可以用set方法替换它访问过的最后一个元素。我们可以通过调用listIterator方法产生一个指向List开始处的ListIterator,并且还可以用过重载方法listIterator(n)来创建一个指定列表索引为n的元素的ListIterator。
ListIterator可以往前遍历,添加元素,设置元素
Iterator和ListIterator的区别:
两者都有next()和hasNext(),可以实现向后遍历,但是ListIterator有previous()和hasPrevious()方法,即可以实现向前遍历
ListIterator可以定位当前位置,nextIndex()和previous()可以实现
ListIterator有add()方法,可以向list集合中添加数据
都可以实现删除操作,但是ListIterator可以实现对对象的修改,set()可以实现,Iterator仅能遍历,不能修改
[7]Fail-Fast
类中的iterator()方法和listIterator()方法返回的iterators迭代器是fail-fast的:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
迭代器与枚举有两点不同:
1. 迭代器在迭代期间可以从集合中移除元素。
2. 方法名得到了改进,Enumeration的方法名称都比较长。
迭代器的好处:屏蔽了集合之间的不同,可以使用相同的方式取出
3.集合类概念
(1).集合类的作用
集合类也叫做容器类,和数组一样,用于存储数据,但数组类型单一,并且长度固定,限制性很大,而集合类可以动态增加长度。
集合存储的元素都是对象(引用地址),所以集合可以存储不同的数据类型,但如果是需要比较元素来排序的集合,则需要类型一致。
集合中提供了统一的增删改查方法,使用方便。
支持泛型,避免数据不一致和转换异常,还对常用的数据结构进行了封装。
所有的集合类的都在java.util包下。
(2)集合框架体系的组成
集合框架体系是由Collection、Map(映射关系)和Iterator(迭代器)组成,各部分的作用如下所示。
[1]Collection体系中有三种集合:Set、List、Queue
Set(集): 元素是无序的且不可重复。
List(列表):元素是有序的且可重复。
Queue(队列):封装了数据结构中的队列。
[2]Map体系
Map用于保存具有映射关系的数据,即key-value(键值对)。Map集合的key是唯一的,不可重复,而value可以重复。所以一个value可以对应多个key。
Map体系除了常用类之外,还有Properties(属性类)也属于Map体系。
[3]Iterator(迭代器)
请查看上面!
(3)Collection的由来
由于数组中存放对象,对对象操作起来不方便。java中有一类容器,专门用来存储对象。
集合可以存储多个元素,但我们对多个元素也有不同的需求
多个元素,不能有相同的
多个元素,能够按照某个规则排序
针对不同的需求:java就提供了很多集合类,多个集合类的数据结构不同。但是,结构不重要,重要 的是能够存储东西,能够判断,获取.
把集合共性的内容不断往上提取,最终形成集合的继承体系—->Collection
并且所有的Collection实现类都重写了toString()方法.
(4)集合和数组
集合与数组的区别:
1.数组的长度固定的,而集合长度时可变的
2.数组只能储存同一类型的元素,而且能存基本数据类型和引用数据类型。集合可以存储不同类型的元素,只能存储引用数据类型
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合只能保存对象。
数组和集合的主要区别包括以下几个方面:
一:数组声明了它容纳的元素的类型,而集合不声明。这是由于集合以object形式来存储它们的元素。
二:一个数组实例具有固定的大小,不能伸缩。集合则可根据需要动态改变大小。
三:数组是一种可读/可写数据结构没有办法创建一个只读数组。然而可以使用集合提供的ReadOnly方 只读方式来使用集合。该方法将返回一个集合的只读版本。
集合的作用:
如果一个类的内部有很多相同类型的属性,并且他们的作用与意义是一样的,比如说学生能选课学生类就有很多课程类型的属性,或者工厂类有很多机器类型的属性,我们用一个类似于容器的集合去盛装他们,这样在类的内部就变的井然有序———这就是:
在类的内部,对数据进行组织的作用。
简单而快速的搜索查找其中的某一条元素
有的集合接口,提供了一系列排列有序的元素,并且可以在序列中间快速的插入或者删除有关元素。
有的集合接口在其内部提供了映射关系的结构,可以通过关键字(key)去快速查找对应的唯一对象,而这个关键可以是任意类型的。
(5)泛型与集合的区别
泛型听起来很高深的一个词,但实际上它的作用很简单,就是提高java程序的性能。
比如在计算机中经常用到一些数据结构,如队列,链表等,而其中的元素以前一般这么定义:object a=new object();
这样就带来一个严重的问题,用object来表示元素没有逻辑问题,但每次拆箱、封箱就占用了大量的计算机资源,导致程序性能低下,而这部分内容恰恰一般都是程序的核心部分,如果使用object,那么程序的表现就比较糟糕。
而使用泛型则很好的解决这个问题,本质就是在编译阶段就告诉编译器,数据结构中元素的种类,既然编译器知道了元素的种类,自然就避免了拆箱、封箱的操作,从而显著提高java程序的性能。
比如List就直接使用string对象作为List的元素,而避免使用object对象带来的封箱、拆箱操作,从而提高程序性能。
4.集合接口与类
(1)数组和集合一般就用到下面接口和集合
Array 数组
Arrays 数组工具
Collection 最基本的集合接口
Collections 集合工具类
List 接口
ArrayList 一种可以动态增长和缩减的索引序列
LinkedList 一种可以在任何位置进行高效地插入和删除操作的有序序列
Vector
Set
HashSet 一种没有重复元素的无序集合
TreeSet 一种有序集
LinkHashSet 一种可以记住元素插入次序的集合
map
HashMap 一种存储key:value关联的映射
HashTable
TreeMap 一种key有序的映射
LinkedHashMap 一种可以记住插入次序的映射
Deque
Stack
ArrayDeque 一种用循环数组实现的双端队列
Queue
PriorityQueue 一种可以高效删除最小元素的集合
(2)Array
数组:是以一段连续内存保存数据的;随机访问是最快的,但不支持插入,删除,迭代等操作。
Array可以包含基本类型和对象类型
Array大小是固定的
指定数组引用为 null,则此类中的方法都会抛出 NullPointerException。
所创建的对象都放在堆中。
够对自身进行枚举(因为都实现了IEnumerable接口)。
具有索引(index),即可以通过index来直接获取和修改任意项。
Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。
Array只能存储同构的对象,而ArrayList可以存储异构的对象。
在CLR托管对中的存放方式
Array是始终是连续存放的,而ArrayList的存放不一定连续。
Array不能够随意添加和删除其中的项,而ArrayList可以在任意位置插入和删除项。
采用数组存在的一些缺陷:
1.数组长度固定不变,不能很好地适应元素数量动态变化的情况。
2.可通过数组名.length获取数组的长度,却无法直接获取数组中真实存储的个数。
3.在进行频繁插入、删除操作时同样效率低下。
(3)Arrays
数组的工具类,里面都是操作数组的工具.
常用方法:
1、数组的排序:Arrays.sort(a);//实现了对数组从小到大的排序//注:此类中只有升序排序,而无降序排序。
2、数组元素的定位查找:Arrays.binarySearch(a,8);//二分查找法
3、数组的打印:Arrays.toString(a);//String 前的a和括号中的a均表示数组名称
4、 查看数组中是否有特定的值:Arrays.asList(a).contains(1);
(4)Collection
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些 Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类, Java SDK提供的类都是继承自Collection的“子接口”如List和Set。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的 Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:
Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) {
Object obj = it.next(); // 得到下一个元素
由Collection接口派生的两个接口是List和Set。
Collection返回的是Iterator迭代器接口,而List中又有它自己对应的实现–>ListIterator接口 Collection。标识所含元素的序列,这里面又包含多种集合类,比如List,Set,Queue;它们都有各自的特点,比如List是按顺序插入元素,Set是不重复元素集合,Queue则是典型的FIFO结构
Collection接口描述:
Collection接口常用的子接口有List 接口和Set接口
List接口中常用的子类有:ArrayList类(数组列表)和LinkedList(链表)
Set接口中常用的子类有:HashSet (哈希表)和LinkedHashSet(基于链表的哈希表)
Collection 层次结构 中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。JDK 不提供此接口的任何直接 实现:它提供更具体的子接口(如 Set和 List)实现。此接口通常用来传递 collection,并在需要最大普遍性的地方操作这些 collection。(面向接口的编程思想)
(5)Collections
[1]排序操作
Collections提供以下方法对List进行排序操作
void reverse(List list):反转
void shuffle(List list),随机排序
void sort(List list),按自然排序的升序排序
void sort(List list, Comparator c);定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j),交换两个索引位置的元素
void rotate(List list, int distance),旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
[2]查找,替换操作
int binarySearch(List list, Object key), 对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll),根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c),根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj),用元素obj填充list中所有元素
int frequency(Collection c, Object o),统计元素出现次数
int indexOfSubList(List list, List target), 统计targe在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素。
[3]同步控制
Collections中几乎对每个集合都定义了同步控制方法, 这些方法,来将集合包装成线程安全的集合
SynchronizedList(List);
SynchronizedSet(Set;
SynchronizedMap(Map);
SynchronizedMap和ConcurrentHashMap 区别
Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步,而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。这样,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,所以,即使在遍历map时,其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
ConcurrentHashMap从类的命名就能看出,它必然是个HashMap。而Collections.synchronizedMap()可以接收任意Map实例,实现Map的同步
线程安全,并且锁分离。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
(6)List
List:有序(元素存入集合的顺序和取出的顺序一致),元素都有索引。元素可以重复。
List本身是Collection接口的子接口,具备了Collection的所有方法。
List的特有方法都有索引,这是该集合最大的特点。
List集合支持对元素的增、删、改、查。
List中存储的元素实现类排序,而且可以重复的存储相关元素。
次序是List最重要的特点:它保证维护元素特定的顺序。
List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
和下面要提到的Set不同,List允许有相同的元素。
除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
优点:操作读取操作效率高,基于数组实现的,可以为null值,可以允许重复元素,有序,异步。
缺点:由于它是由动态数组实现的,不适合频繁的对元素的插入和删除操作,因为每次插入和删除都需要移动数组中的元素。
(7)ArrayList
ArrayList 是基于数组实现,内存中分配连续的空间,需要维护容量大小。随机访问.
ArrayList就是动态数组,也是一个对象。
ArrayList不自定义位置添加元素和LinkedList性能没啥区别,ArrayList默认元素追加到数组后面,而LinkedList只需要移动指针,所以两者性能相差无几。
如果ArrayList自定义位置插入元素,越靠前,需要重写排序的元素越多,性能消耗越大,LinkedList无论插入任何位置都一样,只需要创建一个新的表项节点和移动一下指针,性能消耗很低。
ArrayList是基于数组,所以查看任意位置元素只需要获取当前位置的下标的数组就可以,效率很高,然而LinkedList获取元素需要从最前面或者最后面遍历到当前位置元素获取,如果集合中元素很多,就会效率很低,性能消耗大。
频繁遍历查看元素,使用 ArrayList 集合,ArrayList 查询快,增删慢
ArrayList线程不安全的
1、ArrayList是用数组实现的,该对象存放在堆内存中,这个数组的内存是连续的,不存在相邻元素之间还隔着其他内存。底层是一个可动态扩容的数组
2、索引ArrayList时,速度比原生数组慢是因为你要用get方法,这是一个函数调用,而数组直接用[ ]访问,相当于直接操作内存地址,速度当然比函数调用快。
3、新建ArrayList的时候,JVM为其分配一个默认或指定大小的连续内存区域(封装为数组)。
4、每次增加元素会检查容量,不足则创建新的连续内存区域(大小等于初始大小+步长),也用数组形式封装,并将原来的内存区域数据复制到新的内存区域,然后再用ArrayList中引用原来封装的数组对象的引用变量引用到新的数组对象:
elementData = Arrays.copyOf(elementData, newCapacity);
ArrayList里面的removeIf方法就接受一个Predicate参数,采用如下Lambda表达式就能把,所有null元素删除:
list.removeIf(e -> e == null);
ArrayList:每次添加元素之前会检查是否需要扩容,是按照原数组的1.5倍延长。构造一个初始容量为 10 的空列表。
使用for适合循环ArrayLIst以及数组,当大批量的循环LinkedList时程序将会卡死,for适合循环数组结构,通过下标去遍历。
get访问List内部任意元素时,ArrayList的性能要比LinkedList性能好。LinkedList中的get方法是要按照顺序从列表的一端开始检查,直到另一端。
在ArrayList的 中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;
ArrayList的空 间浪费主要体现在在list列表的结尾预留一定的容量空间
ArrayList只能包含对象类型。
ArrayList的大小是动态变化的。
对于基本类型数据,集合使用自动装箱来减少编码工作量
够对自身进行枚举(因为实现了IEnumerable接口)。
具有索引(index),即可以通过index来直接获取和修改任意项。
ArrayList允许存放(不止一个)null元素
允许存放重复数据,存储时按照元素的添加顺序存储
ArrayList可以存放任何不同类型的数据(因为它里面存放的都是被装箱了的Object型对象,实际上ArrayList内部就是使用”object[] _items;”这样一个私有字段来封装对象的)
ArrayList不是一个线程安全的集合,如果集合的增删操作需要保证线程的安全性,可以考虑使用CopyOWriteArrayList或者使用collections.synchronizedList(Lise l)函数返回一个线程安全的ArrayList类。
实现了RandomAccess接口,底层又是数组,get读取元素性能很好
顺序添加很方便
删除和插入需要复制数组 性能很差(可以使用LinkindList)
为什么ArrayList的elementData是用transient修饰的?
transient修饰的属性意味着不会被序列化,也就是说在序列化ArrayList的时候,不序列化elementData。
为什么要这么做呢?
elementData不总是满的,每次都序列化,会浪费时间和空间
重写了writeObject 保证序列化的时候虽然不序列化全部 但是有的元素都序列化
所以说不是不序列化 而是不全部序列化。
elementData属性采用了transient来修饰,不使用Java默认的序列化机制来实例化,自己实现了序列化writeObject()和反序列化readObject()的方法。
每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常。
如果提前知道数组元素较多,可以在添加元素前通过调用ensureCapacity()方法提前增加容量以减小后期容量自动增长的开销。
当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
ArrayList基于数组方式实现,容量限制不大于Integer.MAX_VALUE的小大,每次扩容1.5倍。有序,可以为null,允许重复,非线程安全。
增加和删除会修改modCount,在迭代的时候需要保持单线程的唯一操作,如果期间进行了插入或者删除操作,就会被迭代器检查获知,从而出现运行时异常。
一般建议在单线程中使用ArrayList。
当在index处放置一个元素的时候,会将数组index处右边的元素全部右移
当在index处删除一个元素的时候,会将数组index处右边的元素全部左移
ArrayList底层是数组结构,因为数组有维护索引,所以查询效率高;而做插入、删除操作时,因为要判断扩容(复制一份新数组)且数组中的元素可能要大规模的后移或前移一个索引位置,所以效率差。
Arrays.asList()方法返回的List集合是一个固定长度的List集合,不是ArrayList实例,也不是Vector的实例
ArrayList也采用了快速失败(Fail-Fast机制)的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考HashMap的实现原理中的Fail-Fast机制。
(8)linkedList
LinkedList 是基于循环双向链表数据结构,不需要维护容量大小。顺序访问。
频繁插入删除元素 使用 LinkedList 集合
LinkedList 线程不安全的
LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList 更大。
LinkedList提供了大量的首尾操作
LinkedList:底层的数据结构是链表,线程不同步,增删元素的速度非常快。
LinkedList:底层基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢
LinkedList由双链表实现,增删由于不需要移动底层数组数据,其底层是链表实现的,只需要修改链表节点指针,对元素的插入和删除效率较高。
LinkedList缺点是遍历效率较低。HashMap和双链表也有关系。
LinkedList是一个继承于AbstractSequentialList的双向链表,它可以被当做堆栈、队列或双端队列进行操作
LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
使用foreach适合循环LinkedList,使用双链表结构实现的应当使用foreach循环。
LinkedList实现了List接口,允许null元素。
LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(…));
在LinkedList的中间插入或删除一个元素的开销是固定的。
LinkedList不支持高效的随机元素访问。
LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
LinkedList是List和Deque接口的双向链表的实现。实现了所有可选列表操作,并允许包括null值。
Fail-Fast机制:LinkedList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
LinkedList因为底层为链表结构,查询时需要从头节点(或尾节点)开始遍历所以查询效率差;但同时也因为是链表结构,做插入、删除操作时只要断开当前删除节点前驱、后驱引用,并将原来的前、后节点的引用链接起来,所以效率高。
千万不要使用普通for循环遍历LinkedList,这么做会让你崩溃!可以选择使用foreach或迭代器来进行遍历操作
LinedList适合用迭代遍历;
基于链表结构的集合 LinkedList。LinkedList 属于 java.util 包下面,也实现Iterable接口,说明可以使用迭代器遍历;LinkedList 还实现 Deque,Queue 操作。Deque 和 Queue 是 LinkedList 的父接口,那么 LinkedList 也可以看成一种 Deque 或者 Queue;Queue表示一种队列,也是一种数据结构,它的特点是先进先出,因此在队列这个接口里面提供了一些操作队列的方法,同时LinkedList也具有这些方法;Deque(Double ended queues双端队列),支持在两端插入或者移除元素; 那也应该具有操作双端队列的一些方法;LinkedList 是他们的子类,说明都具有他们两者的方法;LinkedList也可以充当队列,双端队列,堆栈多个角色。
(9)vector
Vector:底层的数据结构就是数组,线程同步的,Vector无论查询和增删都巨慢。
Vector:是按照原数组的2倍延长。
Vector是基于线程安全的,效率低 元素有放入顺序,元素可重复
Vector可以由我们自己来设置增长的大小,ArrayList没有提供相关的方法。
Vector相对ArrayList查询慢(线程安全的)
Vector相对LinkedList增删慢(数组结构)
以前还能见到Vector和Stack,但Vector太过古老,被ArrayList取代,所以这里不讲;而Stack已经被ArrayDeque取代。
对于想在迭代器迭代过程中针对集合进行增删改的,可以通过返回ListIterator来操作。
Vector:底层结构是数组,线程是安全的,添加删除慢,查找快,(同ArrayList)
ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快。
Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
由Vector创建的Iterator,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
(10)Set
无序(存入和取出顺序有可能不一致),不可以存储重复元素。必须保证元素唯一性。
元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)
Set具有与Collection完全一样的接口,因此没有任何额外的功能,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。
Set不保存重复的元素(至于如何判断元素相同则较为负责)
存入Set的每个元素都必须是唯一的,因为Set不保存重复元素,加入Set的元素必须定义equals()方法以确保对象的唯一性。
Set 是基于对象的值来确定归属性的。
Set本身有去重功能是因为String内部重写了hashCode()和equals()方法,在add里实现了去重, Set集合是不允许重复元素的,但是集合是不知道我们对象的重复的判断依据的,默认情况下判断依据是判断两者是否为同一元素(euqals方法,依据是元素==元素),如果要依据我们自己的判断来判断元素是否重复,需要重写元素的equals方法(元素比较相等时调用)hashCode()的返回值是元素的哈希码,如果两个元素的哈希码相同,那么需要进行equals判断。【所以可以自定义返回值作为哈希码】 equals()返回true代表两元素相同,返回false代表不同。
set集合没有索引,只能用迭代器或增强for循环遍历
set的底层是map集合
Set最多有一个null元素
必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
Set具有与Collection完全一样的接口,没有额外的任何功能。所以把Set就是Collection,只是行为不同(这就是多态);Set是基于对象的值来判断归属的,由于查询速度非常快速,HashSet使用了散列,HashSet维护的顺序与TreeSet或LinkedHashSet都不同,因为它们的数据结构都不同,元素存储方式自然也不同。TreeSet的数据结构是“红-黑树”,HashSet是散列函数,LinkedHashSet也用了散列函数;如果想要对结果进行排序,那么选择TreeSet代替HashSet是个不错的选择
(11)Hashset
HashSet : 为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
Hashset实现set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set的迭代顺序,别是它不保证该顺序恒久不变。此类允许使用Null元素
对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都说调用HashMap的相关方法来实现的
对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。
HashSet: 哈希表结构的集合 利用哈希表结果构成的集合查找速度会很快。
HashSet : 底层数据结构是哈希表,线程 是不同步的 。 无序,高效;HashSet 集合保证元素唯一性 :通过元素的 hashCode 方法,和 equals 方法完成的。当元素的 hashCode 值相同时,才继续判断元素的 equals 是否为 true。如果为 true,那么视为相同元素,不存。如果为 false,那么存储。如果 hashCode 值不同,那么不判断 equals,从而提高对象比较的速度。
HashSet类直接实现了Set接口, 其底层其实是包装了一个HashMap去实现的。HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。
元素值可以为NULL,但只能放入一个null
HashSet集合保证元素唯一性:通过元素的hashCode方法,和equals方法完成的。
当元素的hashCode值相同时,才继续判断元素的equals是否为true。
如果hashCode值不同,那么不判断equals,从而提高对象比较的速度。
对于HashSet集合,判断元素是否存在,或者删除元素,底层依据的是hashCode方法和equals方法。
特点:存储取出都比较快
1、不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
2、HashSet不是同步的,必须通过代码来保证其同步。
3、集合元素可以是null.
原理:简单说就是链表数组结合体
对象的哈希值:普通的一个整数,可以理解为身份证号,是hashset存储的依据
HashSet按Hash算法来存储集合中的元素。在存取和查找上有很好的性能。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该hashCode值决定该对象在HashSet中存储的位置。
如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,hashSet将会把它们存储在不同位置,依然可以添加成功。如果两个对象的hashCode()方法返回的hashCode值相同,当它们的equals()方法返回false时,会在hashCode所在位置采用链式结构保存多个对象。这样会降低hashSet的查询性能。
在使用HashSet中重写hashCode()方法的基本原则
1、在程序运行过过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
2、当两个对象的equals()方法比较返回true时,这个两个对象的hashCode()方法返回相同的值。
3、对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。
把对象内的每个意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值。用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回
return f1.hashCode()+(int)f2;
为了避免直接相加产生的偶然相等(两个对象的f1、f2实例变量并不相等,但他们的hashCode的和恰好相等),可以通过为各个实例变量的hashCode值乘以一个质数后再相加
return f1.hashCode()*19+f2.hashCode()*37;
如果向HashSet中添加一个可变的对象后,后面的程序修改了该可变对想的实例变量,则可能导致它与集合中的其他元素的相同(即两个对象的equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。
(12)Linkedhashset
LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序
LinkedHashSet 综合了链表+哈希表,根据元素的hashCode值来决定元素的存储位置,它同时使用链表维护元素的次序。
当遍历该集合时候,LinkedHashSet 将会以元素的添加顺序访问集合的元素。
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。
这个相对于HashSet来说有一个很大的不一样是LinkedHashSet是有序的。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
与HashSet相比,特点:
对集合迭代时,按增加顺序返回元素。
性能略低于HashSet,因为需要维护元素的插入顺序。但迭代访问元素时会有好性能,因为它采用链表维护内部顺序。
LinkedHashSet不允许元素的重复
存储的顺序是元素插入的顺序。
(13)TreeSet
TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
TreeSet 继承AbstractSet类,实现NavigableSet、Cloneable、Serializable接口。与HashSet是基于HashMap实现一样,TreeSet 同样是基于TreeMap 实现的。由于得到Tree 的支持,TreeSet 最大特点在于排序,它的作用是提供有序的Set集合。
用于对 Set 集合进行元素的指定顺序排序,排序需要依据元素自身具备的比较性。
如果元素不具备比较性,在运行时会抛出ClassCastException 异常。 所以元素需要实现Comparable 接口 ,让元素具备可比较性, 重写 compareTo 方法 。依据 compareTo 方法的返回值,确定元素在 TreeSet 数据结构中的位置。 或者用比较器方式,将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法
TreeSet底层的数据结构就是二叉树。
不能写入空数据
写入的数据是有序的。
不写入重复数据
TreeSet方法保证元素唯一性的方式:就是参考比较方法的结果是否为0,如果return 0,视为两个对象重复,不存。
TreeSet集合排序有两种方式,Comparable和Comparator区别:
1:让元素自身具备比较性,需要元素对象实现Comparable接口,覆盖compareTo方法。
2:让集合自身具备比较性,需要定义一个实现了Comparator接口的比较器,并覆盖compare方法,并将该类对象作为实际参数传递给TreeSet集合的构造函数。
TreeSet类是SortedSet接口的实现类。因为需要排序,所以性能肯定差于HashSet。与HashSet相比,额外增加的方法有:
first():返回第一个元素
last():返回最后一个元素
lower(Object o):返回指定元素之前的元素
higher(Obect o):返回指定元素之后的元素
subSet(fromElement, toElement):返回子集合
可以定义比较器(Comparator)来实现自定义的排序。默认自然升序排序。
TreeSet两种排序方式:自然排序和定制排序,默认情况下,TreeSet采用自然排序
TreeSet会调用集合元素的compareTo(Object object)方法来比较元素之间的大小关系,然后将元素按升序排列
如果试图把一个元素添加到TreeSet中,则该对象必须实现Comparable接口实现Comparable接口必须实现compareTo(Object object),两个对象即通过这个方法进行比较Comparable的典型实现
BigDecimal、BigInteger以及所有的数值类型对应的包装类型,按对应的数值大小进行比较
Character:按字符的Unicode值进行比较
Boolean:true对应的包装类实例大于false包装类对应的实例
String:按字符对应的Unicode值进行比较
Date、Time:后面的时间、日期比前面的时间、日期大
向TreeSet中添加一个元素,只有第一个不需要使用compareTo()方法,后面的都要调用该方法
因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象
TreeSet采用红黑树的数据结构来存储集合元素
对于TreeSet集合而言,它判断两个对象的是否相等的唯一标准是:两个对象的通过compareTo(Object obj)方法比较是否返回0–如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等,否则认为它们不相等。对于语句,obj1.compareTo(obj2),如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2.
在默认的compareTo方法中,需要将的两个的类型的对象的转换同一个类型,因此需要将的保证的加入到TreeSet中的数据类型是同一个类型,但是如果自己覆盖compareTo方法时,没有要求两个对象强制转换成同一个对象,是可以成功的添加treeSet中
如果两个对象通过CompareTo(Object obj)方法比较返回0时,但它们通过equals()方法比较返回false时,TreeSet不会让第二个元素添加进去
(14)Map
Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。
Map接口概述:Java.util.Map