【原创】java中各种集合类的实现浅析
【LinkedList】
LinkedList使用了链表来实现List功能,而且是双向循环链表,它的Entry定义如下:
private static class Entry<E> {
//保存放入list中的对象
E element;
//当前节点的下一节点
Entry<E> next;
//当前节点的上一节点
Entry<E> previous; //新构造的entry,next节点为头节点,previous为尾节点,
//新插入的节点在尾节点之后
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
插入一个节点时:
private Entry<E> addBefore(E o, Entry<E> e) {
Entry<E> newEntry = new Entry<E>(o, e, e.previous);
//定义节点在链表中的链接方式,插入一个新节点
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
可见,只需要移动指针即可,当按照节点索引删除时,需要将节点先查询到:
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
//定位头节点
Entry<E> e = header;
//如果要寻找的索引小于链表长度的一半,则从头开始找,否则从尾部开始找
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
可见,此时变成了顺序查找,不过此处小小的优化了一下,如果按照对象来删除,那就成了全表扫描了,呵呵:
public boolean remove(Object o) {
if (o==null) {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
}
【ArrayList】
使用数组来实现,相对来说,通过数组下标访问效率很高,但是通过对象访问效率仍然不高,大量的使用了System.arrayCopy方法来拷贝数组。
【Vector】
和arraylis比较相似,都是使用数组来实现,不过二者空间扩展的方式不同,arraylist默认为50%+1,vector为一倍,而且操作方法都是线程安全的,不过效率要慢。
【HashMap】
HashMap的原理比以上都要复杂,可以说HashMap是集成了链表的快速增删和数组的快速随机读取功能于一身,仔细看了下HashMap的源码,其结构如下:
其内部的Entry定义:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
final int hash;
//定义了一个指针,指向该节点的下一个节点
Entry<K,V> next; /**
* Create new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
} public K getKey() {
return HashMap.<K>unmaskNull(key);
} public V getValue() {
return value;
} public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
可见,entry内部定义了一个指针(引用),该引用就是指向其他节点,这不就是一个单链表吗?下面看一下往map中增加对象时的方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//首先获取该key的hash值
int hash = hash(key.hashCode());
//通过该hash值结合hash表的长度,计算出该key应该落在数组的哪个位置
int i = indexFor(hash, table.length);
//找到数组位置后,遍历该数组的entry节点,通过头节点的指针遍历所有的entry,找到目标节点
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
}
可见,hashMap的结构有点像查字典,如果想要查一个字,首先根据字的拼音(hash值)找到字所在的大体位置(数组),找到大体位置,然后一个个的去对比找到确定的位置。如这张图,左边就是数组,数组中都存放着一个entry头节点,而每个头节点后面都串联着很多entry,当要查找时,首先计算查找对象的hash值,确定该对象在哪个数组中,找到数组位置(随机访问)后,拿到entry头节点,然后线性遍历该链表,找到对应的具体节点,可见,hashmap解决冲突的办法就是链接地址法,将所有hash值相同的对象链接起来。
【linkedHashMap】
继承自HashMap,大部分功能都由HashMap来实现,不同的是其中的Entry,使用一个双向链表来维护节点插入的顺序,而且使用LRU算法来维护链表中节点的顺序,对于经常使用的节点,将其拿到链表的最前面。
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
} /**
* Remove this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
LinkedHashMap.Entry继承自HashMap.Entry,可见,除了自身的before和after指针之外,还继承了一个next指针,其中,这三个指针的用处分别为:
before和after维护着节点的插入顺序,这就像一根线,将节点按照插入顺序穿起来,而存在一个header引用,表示头节点,而next指针仍然起着链接hash冲突的节点。当执行hash查找时,仍然使用先table再链表的形式,而当迭代时,会按照before和after指针串联的方式进行迭代,
其中有before和after指针,指针的连接标示着节点的插入顺序,当迭代的时候按照这个顺序来迭代。
我画了个图简单描述如下:
图中虚线部分就是before和after指针,实线部分就是next指针,而黑体箭头就是header节点,代表最先插入的节点或者最近用到的节点。
【TreeMap】
treemap是排序的map,背后使用了平衡二叉树来实现,具体的是红黑树,看它的entry定义:
static class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left = null;
Entry<K,V> right = null;
Entry<K,V> parent;
boolean color = BLACK; /**
* Make a new cell with given key, value, and parent, and with
* <tt>null</tt> child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
可见,entry就是树的一个节点,其中定义了一个color属性,代表红黑。由于使用红黑树来实现,当插入新节点或者删除节点时,需要重新调整树,使其满足红黑规则:
public V put(K key, V value) {
//首先找到root节点
Entry<K,V> t = root; //如果root为空,则新建一个节点
if (t == null) {
incrementSize();
root = new Entry<K,V>(key, value, null);
return null;
} //依次根据root往下遍历
while (true) {
//此处相当于二分查找,将当前节点和目标节点对比,如果小就往右走,否则往左走
int cmp = compare(key, t.key);
if (cmp == 0) {
//如果想等,直接覆盖替换
return t.setValue(value);
} else if (cmp < 0) {
//如果小则往左走
//如果左节点不为空,那么直接再次左移
if (t.left != null) {
t = t.left;
} else {
//否则就增加空间,然后新建节点,将该节点插入到空的节点处
incrementSize();
t.left = new Entry<K,V>(key, value, t);
//插入了新节点,打破了平衡,所以需要重新调整树使其满足红黑规则
fixAfterInsertion(t.left);
return null;
}
} else { // cmp > 0
if (t.right != null) {
t = t.right;
} else {
incrementSize();
t.right = new Entry<K,V>(key, value, t);
fixAfterInsertion(t.right);
return null;
}
}
}
}
由于使用了平衡查找树,所以查找节点时效率是很高的,就是一个二分查找树:
private Entry<K,V> getEntry(Object key) {
Entry<K,V> p = root;
K k = (K) key;
while (p != null) {
int cmp = compare(k, p.key);
if (cmp == 0)
return p;
else if (cmp < 0)
p = p.left;
else
p = p.right;
}
return null;
}
【HashTable】
和HashMap差不多,不过其方法是同步的,ConcurrentHashMap和它比较类似,只是后者的get方法不需要同步,所以效率想对更高效。
【HashSet】
本质上由hashMap来实现,只是对象不能重复:
public boolean add(E o) {
return map.put(o, PRESENT)==null;
}
【TreeSet】
本质上默认由TreeMap来实现,默认构造函数将使用TreeMap来实现。
public TreeSet() {
this(new TreeMap<E,Object>());
}
TreeSet也是排序的,可以指定排序器,如果不指定则使用默认的自然序列排序器来排序。
【原创】java中各种集合类的实现浅析的更多相关文章
- java中的集合类总结
在使用Java的时候,我们都会遇到使用集合(Collection)的时候,但是Java API提供了多种集合的实现,我在使用和面试的时候频 频遇到这样的“抉择” . :)(主要还是面试的时候) 久而久 ...
- java中的集合类详情解析以及集合和数组的区别
数组和链表 数组:所谓数组就是相同数据类型的元素按照一定顺序排列的集合. 它的存储区间是连续的,占用内存严重,所以空间复杂度很大,为o(n),但是数组的二分查找时间复杂度很小为o(1). 特点是大小固 ...
- 面试题: Java中各个集合类的扩容机制
个人博客网:https://wushaopei.github.io/ (你想要这里多有) Java 中提供了很多的集合类,包括,collection的子接口list.set,以及map等.由于它 ...
- [原创]Java中的字符串比较,按照使用习惯进行比较
java中的字符串比较一般可以采用compareTo函数,如果a.compareTo(b)返回的是小于0的数,那么说明a的unicode编码值小于b的unicode编码值. 但是很多情况下,我们开发一 ...
- Java中的集合类
实线边框的是实现类,比如ArrayList,LinkedList,HashMap等 折线边框的是抽象类,比如AbstractCollection,AbstractList,AbstractMap等, ...
- Java中的集合类、Lambda、鲁棒性简述
集合类 在java.util包中提供了一些集合类,常用的有List.Set和Map类,其中List类和Set类继承了Collection接口.这些集合类又称为容器,长度是可变的,数组用来存放基本数据类 ...
- java中的集合类学习(三)
JAVA中有许多的集合,常用的有List,Set,Queue,Map. 1.其中List,Set,Queue都是Collection(集合),其每个元素都是单独的一个对象,如List<Strin ...
- [原创]Java中使用File类的list方法获取一定数量的文件:FilenameFilter接口的特殊用法
前言:有时候我们可能会遇到这样一个问题:需要遍历一个包含极多文件的文件夹,首先想到的肯定是使用File.list()方法,该方法返回一个String[],但是如果文件达到几亿呢?这个时候我们就需要分批 ...
- Java中的集合类(List,Set.Map)
1.List 1.1 Arraylist 与 LinkedList 区别 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全: 底层数据结构: Arr ...
随机推荐
- SVN——Jenkins自动发布
最近公司项目处于开发阶段,很多功能开发完后就需要发布到测试环境等待测试去验收,这个时候如果手动更新网站的话,是很费时费力的. 于是乎,我们做成了自动发布,这样我们只管提交代码到SVN就行了,发布由软件 ...
- SQL学习——基础语句(4)
前面感觉真的好乱,想哪,写哪.这里慢慢整理…… SQL Having 语句 还是前面的那两个表: grade表: student表: 我们需要查找这里的s_id下的gradeValue的和,这就要分组 ...
- 洛谷P2915 [USACO08NOV]奶牛混合起来Mixed Up Cows 状压动归
考场上空间开大了一倍就爆0了QAQ- Code: #include<cstdio> #include<algorithm> #include<cmath> usin ...
- 路飞学城Python-Day34
01-MySQL-开篇 数据库:数据库就是数据存储的仓库,数据想要永久存储只能放在文件中,如果忽略文件的存储的效率问题,文件的组件全部都存放在一台机器上,那么文件数据就可以存储在一台机器上,但是这样做 ...
- 路飞学城Python-Day30
11-僵尸进程与孤儿进程 现象:运行程序会产生父进程,在父进程中开子进程,这两个进程公用一个打印终端,运行的时候就只运行父进程,父进程虽然自己结束了,但是要等子进程结束完才会结束. 父进程可以开多个子 ...
- MySQL Reading table information for completion of table and column names
打开数据库是发现提示: mysql> show databases; +--------------------+ | Database | +--------------------+ | b ...
- CommonJS 与 ES6 的依赖操作方法(require、import)
CommonJS:http://www.commonjs.org/specs/modules/1.0/ ES2015的 export:https://developer.mozilla.org/en- ...
- 浅谈optparse 解析命令行参数库
使用的背景 在工作中我们经常要制定运行脚本的一些参数,因为有些东西是随着我么需求要改变的,所以在为们写程序的时候就一定不能把写死,这样我们就要设置参数 在python中我们可以通过sys 模板的arg ...
- HTML 编码规范
语法 使用 4 个空格做为一个缩进层级,不允许使用 2 个空格或 tab 字符 在属性上,使用双引号 "",不要使用单引号 '' 属性名 / 属性值全小写,用中划线 - 做分隔符 ...
- linux上重启jboss服务器
ps -ef|grep jboss :查看当前jboss进程 kill -9 进程id :杀掉进程,kill -9发送的信号是SIGKILL,即exit.exit信号不会被系统阻塞 ...