-
集合包
集合包中常用的是Collection和Map两个接口的实现类,Collection接口主要抽象了对单对象集合的操作,
Map接口抽象了对Key-Value形式键值对集合的操作。
Collection的子接口中常用的是List接口和Set接口,两者最明显的区别就是List的实现类可以放重复元素,Set的
实现类是没有办法放重复元素,跟离散数学概念的集合比较像。
List接口最常用的实现类有:ArrayList、LinkedList、Vecotr、及Stack;
Set接口常用的实现类有:HashSet、TreeSet
List接口
-
ArrayList
ArrayList在数据结构中被称作顺序表,基于数组实现,我们进入源码可以看到,如果构造器不指定数组大小initialCapacity的话
将在第一次使用时候默认创建一个大小为10的对象数组,并将此数组赋给了当前实例的elementData属性。
插入对象:add(E)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
大体调用ensureCapacityInternal确保插入后不会溢出,然后就是插入到对象数组中。
如果超过现有数组的容量的话,将调用一定的算法计算现有的容量值,这其中会调用调用Arrays.copyOf
复制旧的数组。
还提供了add(int,E)这样的方法将元素直接插入指定的int位置上,将目前index及其后的数据都往后挪一位,然后才能将指定的index位置的赋值为传入的对象,这种代价很高,此外还有addAll方法。
删除对象:remove(E)
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
一个是根据索引删除对象,一个是根据对象删除对象的,调用fastRemove可以快速移动对象填补空出来的位置,根据对象查找删除这个会慢一点,因为有个查找的过程。
获取单个对象:get(int )
遍历对象:iterator()
判断对象是否存在:contains(E)
总结:
1,ArrayList基于数组方式实现,无容量的限制;
2,ArrayList在执行插入元素时可能要扩容,在删除元素时并不会减小数组的容量(如希望相应的缩小数组容量,可以调用ArrayList的trimToSize()),在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找;
3,ArrayList是非线程安全的。
2. LinkedList
LinkedList基于双向链表机制,所谓双向链表机制,就是集合中的每个元素都知道其前一个元素以及其后一个元素的位置。
在LinkedList中,以一个内部的Node类来代表链表中的每个节点。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
跟数据结构中的一模一样,双向链表可以快速遍历,值得一提的是ArrayList封装了指向头结点和尾节点的指针。
1,LinkedList基于双向链表机制实现;
2,LinkedList在插入元素时,须创建一个新的Entry对象,并切换相应元素的前后元素的引用;在查找元素时,须遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可,此时原有的前后元素改变引用连在一起;
3,LinkedList是非线程安全的。
3.Vector
其add、remove、get(int)方法都加了synchronized关键字,默认创建一个大小为10的Object数组,并将
capacityIncrement设置为0。容量扩充策略:如果capacity大于0,则将object数组的大小扩大为现有size加上
capacityIncrement的值;如果capacity等于或则小于0,则将object数组的大小扩大为现有size的两倍,这种容量控制策略
比ArrayList更为可控。
Vector是基于Synchronized实现的线程安全的ArrayList,但在插入元素时容量扩充的机制和ArrayList
稍有不同、并可以通过传入capacityIncrement来控制容量的扩充。
1.4 Stack
Stack继承于Vector,所以是顺序表实现的栈,在其基础上实现了stack所要求的FIFO操作,其提供了
push、pop、peek三个主要的方法:
push操作通过调用Vector中的addElement来完成;
pop操作操作调用vector的peek来获取元素,并同时删除数组中的最后一个元素;
peek操作通过获取当前Object数组的大小,并获取数组上的最好一个元素。
Set接口
2.1 HashSet
默认构造创建一个HashMap对象
add(E):调用HashMap的put方法来完成此操作,将需要添加的元素作为Map中的key,value
则传入之前已创建的Object对象。
remove(E): 调用HashMap的remove(E)方法来完成此操作。
contains(E):调用HashMap的containsKey方法
iterator():调用HashMap的keyset的iterator方法。
HashSet不支持通过getInt(Index)获取指定位置的元素,只能通过iterator方法来获取。
总结:
1:HashSet是基于HashMap实现的,无容量限制;
2,HashSet是非线程安全的。
2.2 TreeSet
TreeSet和HashSet的主要不同在于TreeSet对于排序的支持,TreeSet基于TreeMap实现。
Map接口
1.HashMap
HashMap没有参数的构造方法,将loadFactor负载因子设置为默认的0.75,阈值threshold设置为
12,于是并创建一个大小为12/0.75=16的对象数组。
Java里面的哈希表是基于对象数组和拉链法实现的,
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
注意到next指针是指向相同key的节点就是拉链,而非相邻节点。
put的时候根据key hash后的hashcode和数组length-1按位与的结果值来判断放在数组哪个位置,
如果该数组位置上已经存放其它元素,则在这个位置上以链表的形式存放。如果该位置上没有元素则直接存放。
考虑到链表过大后,性能变为On,Java8的类库设计者们,决定当链表长度超过7的时候链表转化为红黑树。
一种全新的数据结构,查找很快。
get(key)取值的时候根据key的hashCode确定在数组的位置,再根据key的equals确定在链表中的位置。
如上的映射算法加上扩容是2倍扩的,可能最大限度上减少了链表出现的机会,从而保证hashMap的速度。
扩容resize()
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了
提高查找的效率,就需要对HashMap的数组进行扩容,扩容之后,各个节点需要重新计算位置,并放进去,
最消耗性能。这就是resize,那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*
Loafactor的时候,就是超过前面那个阈值的时候就会进行扩容。
负载因子=当前节点数/数组容量 衡量的是一个散列表空间的使用程度,负载因子越大表示散列表的装填程度越高,反之越小。
负载因子越大,查找效率越低,负载因子越小,对空间造成严重浪费。
HashMap的实现中,负载因子的点就是loadFactor属性设置的值,一旦超过阈值就会重新resize
以降低实际的负载因子。默认的0.75是时间和空间的权衡后进行的一个选择。
initialCapacity*2,成倍的扩大容量,HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。不设定参数,则初始容量值为16,默认的负载因子为0.75,不宜过大也不宜过小,过大影响效率,过小浪费空间。
扩容后需要重新计算每个元素在数组中的位置,是一个非常消耗性能的操作,所以我们如果已经预只HashMap
中元素的个数,那么预设元素的个数能够有效提高HashMap的性能。
2 HashTable
HashTable数据结构跟HashMap的原理差不多,区别在于put,get时加了同步关键字,而且HashTable
Key不可以放null值。
虽然HashTable是线程安全的,不过我们在高并发情况下使用的是ConcurrentHashMap数据结构,它的内部
实现使用了锁分段技术,维持着锁segement的数组,在数组中又存放着我们的对象数组,内部hash算法将数据
较均匀分布在不同锁中。
总结:
1、HashMap采用数组方式存储KV构成的node对象,而且没有容量限制
2、HashMap基于Key的hash来寻找node对象存放在数组中位置,对应hash冲突采用链表的方法解决
3、HashMap在插入元素的时候可能会扩打数组的容量,这时需要重新计算各个节点的位置,复制到新的数组中;
4、HashMap是非线程安全的
3TreeMap
TreeMap顾名思义,它是基于红黑树实现的,因此它要求一定有key比较的方法,要么传入
Comparator的实现,要不就让key实现Comparable接口。在put操作的时候,基于红黑树的方式遍历,
基于Comparator来比较key应放在树的左边还是右边,如找到找到相同的key,则直接替换掉value