今天学习集合框架的剩余知识。
重复的判断:以equals为依据。
① 元素是无序(存入和取出的顺序不一定一致),元素不可以重复;最多只有一个null。
② 查看API会发现,Set集合的功能与Collection集合的功能是一致的(就方法调用而言);
③ 底层数据结构是哈希表
Hash:哈希,可以理解为散列,计算任意的字符串为一个值。
哈希表:在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
这个函数可以简单描述为:存储位置 = f(关键字) ,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。
查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。
哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
hashCode():散列码是由对象导出的一个整型值。散列码是没有规律的。类的hashCode()方法继承自Object类,因此每个对象都有一个默认的散列码,他的值为对象的存储地址(由对象的物理存储地址通过散列转换来的)。
重复的判断:HashSet使用equals和hashCode在内部判断是否重复。
size():大小
isEmpty:是否为空
containsKey(Object key):判断是否包含键
containsValue(Object value):判断是否包含值
V get(Object key); 根据键获取值
V put(K key, V value):存储数据(键值对)
putAll(Map extends K, ? extends V> m);合并两个MAP,只不过如果有相同的key那么用后面的覆盖前面的
V remove(Object key);根据键删除数据
clear();清空
Set keySet();获取键的集合
Collection values();获取值的集合
entrySet();键-值 对的集合,Set里面的类型是Map.Entry.
/*由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。
Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value
Map.Entry里面包含getKey()和getValue()方法*/
Set<Entry<T,V>> entrySet()
//该方法返回值就是这个map中各个键值对映射关系的集合。
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)
//HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap中的一个静态内部类。存在链表结构。
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
特点:键是不重复的,最多只有一个null,取决于hashCode与equals方法。
List和Set是用来存放集合的接口,并且二者都继承自接口Collection。
ArrayList、LinkedList、Vector是List的三个实现类。
ArrayList:
底层的实现就是一个可变数组非同步实现,当数组长度不够用的时候就会重新开辟一个新的数组,然后将原来的数据拷贝到新的数组内。由于这一底层实现,所以ArrayList集合中元素存储的位置是连续的,查询起来效率比较高,插入删除效率较低。
LinkedList:
底层实现是双向循环链表数据结构非同步实现,数据结构如下代码
LinkList中元素存储位置是不连续的,插入删除的执行效率高,查询效率低。
Vector:
Vector作为List的另外一个典型实现类,完全支持List的全部功能,Vector类也封装了一个动态的,允许在分配的Object[]数组,Vector是一个比较古老的集合,JDK1.0就已经存在,建议尽量不要使用这个集合,Vector与ArrayList的主要是区别是,Vector是线程安全的,但是性能比ArrayList要低。
没有存放顺序,不能存放重复元素检索效率较低,插入删除效率较高,由于set集合储存位置是由他的HashCode码决定的,所以他的存储对象必须有equals()方法,而且set遍历只能用迭代,没有下标。
HashSet:
底层由哈希表(实际上是一个HashMap实例)支持,不能保证元素的顺序,元素是无序的,可以有null,但是null只能有一个,不能有重复的元素。HashSet不是同步的,需要外部保持线程之间的同步问题。
TreeSet:
TreeSet实现了SortedSet接口,它是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。TreeSet也支持两种排序方式:自然排序和自定义排序。不能放入重复元素和null。
主要区别:线程安全性,同步(synchronization),以及速度。
HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null。Hashtable是线程安全的,多个线程可以共享一个Hashtable。
HashMap的同步问题可通过Collections的一个静态方法得到解决,Map Collections.synchronizedMap(Map m) 返回一个同步的Map。
HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。fail-fast结构上更改时(删除或者插入一个元素),将会抛出ConcurrentModificationException异常。
HashMap不能保证随着时间的推移Map中的元素次序是不变的。
HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。
HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对。
HashTable中的方法加了同步锁(synchronized),所以对象是线程安全。
而HashMap是异步的,所以存放的对象并不是线程安全的。
而HashSet的底层是用HashMap实现的,所以它也不是线程安全的。