`

再探集合框架(二)——深入源码看数据结构

阅读更多
这篇我准备从源码的高度来看看集合中各个实现类的是如何组织我们存进去的数据的,主要包括Java类库中提供的几个具体的类:
LinkedList
ArrayList
HashMap
HashSet
TreeMap
TreeSet
PriorityQueue(顺序按下面的讲解顺序)


-----------------------------------------------------------------------------------------------------
1、java.util.LinkedList<E>
当我们创建一个LinkedList类的对象,并且试图增加一个新的元素的时候,到底是如何组织我们传进去的数据的呢?
//创建一个LinkedList类型的对象
java.util.LinkedList<String> l=new java.util.LinkedList<String>();
l.add(e);//e为E类的对象


打开add方法的源码看看:
public boolean add(E e) {
//调用LinkedList的私有方法
//header是LinkedList中的一个属性,这样定义的private transient Entry<E> //header = new Entry<E>(null, null, null);

	addBefore(e, header);        
return true;
	}
//被调用的私有方法
private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
	}
//Entry<E>是LinkedList的内部类,包装每一个E类型的对象e,形成一个链表
private static class Entry<E> {
	E element;
	Entry<E> next;
	Entry<E> previous;

	Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
    }


我们惊喜的发现,原来就是把我们传去的e对象包装成了Entry<E>,然后通过Entry<E>的next和previous两个属性形成了一个以包装后的e对象(即Entry<E>)为节点的双向链表。
于是我们彻底明白了LinkedList果然名副其实,就是一个链表嘛!



-----------------------------------------------------------------------------------------------------
2、java.util.ArrayList<E>


我们看看在ArrayList对象调用add();方法时,底层到底是如何执行的
public boolean add(E e) {
	ensureCapacity(size + 1);  // size是ArrayList中元素的个数
	elementData[size++] = e;   //在调整后的elementData末尾加入新的元素
	return true;
    }
 public void ensureCapacity(int minCapacity) {
	modCount++;
	//elementData就是ArrayList中一个数组类型的属性,用来放进去的元素:	//Object[] elementData
	int oldCapacity = elementData.length;
	if (minCapacity > oldCapacity) {//原来的elementData空间不够用了!
	    Object oldData[] = elementData;
	    int newCapacity = (oldCapacity * 3)/2 + 1;
       //如果通过oldCapacity 计算出的新空间依然不够用
	if (newCapacity < minCapacity)		
	newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
	//这一步最后会调用System.arraycopy(original, 0, copy, 0,
	                         Math.min(original.length, newLength));
	//来实现将所有的元素copy到长度更大的数组中,这一步将很费时间
         elementData = Arrays.copyOf(elementData, newCapacity);
	}
	}


于是我们发现:原来ArrayList也是如名字说的,用Array组织数据。不过它内部定义的那个调整elementData数组的方法copy太多,显然当数据量大的时候,性能不会很好。



-----------------------------------------------------------------------------------------------------
3、java.util.HashMap<K,V>


//向HashMap中插入键值对
	 public V put(K key, V value) {
	         if (key == null)   //如果没有输入的key是null值
	             return putForNullKey(value);//插在Entry[0]的第一个,返回null
	 	//获得哈希码
	 	//1、首先用key类定义的hashcode()方法计算得到一个int
	 	//2、进行一些>>>和^的操作
	         int hash = hash(key.hashCode());
	 	//通过&运算将hash按二进制位取反(1变为0,0变为1)
	 	//得到要插入的元素在table中的index
	         int i = indexFor(hash, table.length);
	 	
	 	//遍历table[i]数据元下拖带的一个链表的所有元素
	         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
	             Object k;
	 	    //如果有一个已经存在的元素的哈希码"=="为true,
	 	    //并且key值"=="或者"equals"为true
	 	    //也就是所谓的key经过hashcode()的一系列运算和
	 	   //equals()的一系列运算相同的元素,就替换原来的value
	             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
	                 V oldValue = e.value;
	                 e.value = value;
	                 e.recordAccess(this);
	                 return oldValue;
	             }
	         }

	         modCount++;
	 	//把原来在table[i]位置的元素挤到Entry<K,V>的next位置
	         addEntry(hash, key, value, i);
	         return null;
	     }
  }


想必大家看这段代码都看到晕了吧,为了让大家能够更加形象的人知道HashMap对数据的的组织形式,上了一个HaspMap数据结构图:



这里解释一下,这个图的最左边的一些就是上面源码中的table也就是HashMap的一个属性Entry[] table。将一个新的键值对插入需要经过这几步:
---给key值计算哈码(计算在这一步int hash = hash(key.hashCode());),
    ---得出在table数组的中index:int i = indexFor(hash, table.
length);
---将键值对插入index确定的上图所示的一个横向的链表中。如果在这个链表中有要插入的pair的key经过hashcode()的一系列运算和equals()的一系列运算相同的元素,就替换原来的value。(这也就是我们自己定义的类要用到HashMap存储的时候,必须重写hashcode()和equals()方法,并且要保证对同一对象两个方法计算结果要相同的原因。因为如果不相同,在一个同一对象为key插入值的时候就不会像你期望的那样后插入的value覆盖前面的value了,而是会重新开辟一个空间存储)

于是,到这里我们明白了,原来HashMap就是通过散列表这种数据结构组织数据的


-----------------------------------------------------------------------------------------------------
4、java.util.HashSet<E>


public boolean add(E e) {
	//map是该类的一个属性,这样定义的:HashMap<E,Object> map
	//这里e作为key了
	//value用本类的属性代替private static final Object PRESENT = new Object();每个键值对都相同
	return map.put(e, PRESENT)==null;
	}


小样直接自己不解决,抛给HashMap类的put()方法,也就是用一个散列表存数据。详解见第三条对HashMap的讲解


-----------------------------------------------------------------------------------------------------
5、java.util.TreeMap<E>


public V put(K key, V value) {
        Entry<K,V> t = root;//root是整棵树的根节点
        if (t == null) {
	    //插入的第一个元素会成为根节点
            root = new Entry<K,V>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // 调用Comparator的compare()方法确定新加的元素出现的位置。
	//我们可以再自己定义的类中实现Comparator接口,然后传给树集的构造器。从而按照自己定义的不同的比较规则来给整个树的数据进行排序。
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
	//这里我们将传进来的数据包装成Entry<K,V> ,通过Entry<K,V> 内部类的//属性 Entry<K,V> parent来组织一棵树
        Entry<K,V> e = new Entry<K,V>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
	}


我们又可以开心的大笑了,原来就是如此简单,就是按照一定的规律形成一棵二叉树来存数据。
大笑过后,我们再次静下心来观察,源码中出现了这样一句:k.compareTo(t.key);是说用key对应的类中实现的compareTo()方法来判断两个key的先后顺序。有若干标准的java平台类都实现了Compatable接口(Compatator可以自己定义不同的比较规则,不过这个接口的比较规则只有一个,是定义key的类的时候定义的,没有可变性),如String类:

//用字典式排序。不展开分析了。
 public int compareTo(String anotherString) {
	int len1 = count;
	int len2 = anotherString.count;
	int n = Math.min(len1, len2);
	char v1[] = value;
	char v2[] = anotherString.value;
	int i = offset;
	int j = anotherString.offset;

	if (i == j) {
	    int k = i;
	    int lim = n + i;
	    while (k < lim) {
		char c1 = v1[k];
		char c2 = v2[k];
		if (c1 != c2) {
		    return c1 - c2;
		}
		k++;
	    }
	} else {
	    while (n-- != 0) {
		char c1 = v1[i++];
		char c2 = v2[j++];
		if (c1 != c2) {
		    return c1 - c2;
		}
	    }
	}
	return len1 - len2;
	    }

所以,我们自己定义key的类的时候,要特别注意compareTo()方法中算法的选择,以便有一个最好的插入、查找、遍历的性能。一般而言将元素添加到树集的速度快于数组和链表,慢于散列表(素服比较:数组、链表<树集<散列表)。



-----------------------------------------------------------------------------------------------------
6、java.util.TreeSet<E>


public boolean add(E e) {
	return m.put(e, PRESENT)==null;
	}

相信大家看到源码立马就能明白了吧,向HashSet一样TreeSet也偷懒了(至于为什么要偷懒,感兴趣的朋友可以去研究,这里不展开了),也是用二叉树的结构存数据,不多说!



-----------------------------------------------------------------------------------------------------
7、java.util.PriorityQueue<E>
(这一条有错,详解见附)

public boolean add(E e) {
        return offer(e);
	}
public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)//属性:Object[] queue
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
	}


一看就明白,就是通过数组组织数据。不过喜欢刨根问底的朋友又会提出一个问题了:
既然和ArrayList一样都是数组组织数据,那干嘛还要存在这个类呢?
问的好!继续看:
PriorityQueue类在数组满了的时候(代码为i >= queue.length),就调用grow(i + 1)这个方法来调整queue的长度。具体调整的算法如下
private void grow(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
	int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        int newCapacity = ((oldCapacity < 64)?
                           ((oldCapacity + 1) * 2):
                           ((oldCapacity / 2) * 3));
        if (newCapacity < 0) // overflow
            newCapacity = Integer.MAX_VALUE;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        queue = Arrays.copyOf(queue, newCapacity);
	}


而ArrayList一上来就调用方法调整了:ensureCapacity(size + 1);里面的具体算法这里就不列出来了。两个类调整的算法不同。这就造成了两者性能上差别。

tip:好了,今天就分析道这里了。进一步的研究,等过段时间才能出来,到时候再贴出来。时间仓促,难免有漏洞,大家多提意见。
另外抱怨一下JE的编辑器,真不好用,害得我重新录入!


纠错:感谢大家能及时反馈给我一些有用的信息,就不一一回复了。就不在原来的文章里改错了,把错误的纠正全写在这后面了,再次感谢!

-----------------------------------------------------------------------------------------------------
PriorityQueue<E>重新写了一份:


我们看看调用add()方法在底层到底发生了什么事情!
public boolean add(E e) {
        return offer(e);
	}

public boolean offer(E e) {
//前面这的几行无非就是判断非空,判断本类的属性queue的长度是否够用然后做相应调整
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
//最后终于要将元素插进去了
//如果queue空就插在index为0的位置,很好理解
//否则调用siftUp()方法(第一个参数是the position to fill,第二个参数是the element to insert)
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
	}
//再来看看siftUp()方法是如何实现的
//api文档的注释的意思是:将x插入合适的位置保持heap的有序性不变
//排序标准有两种途径获取:
//1、在构造PriorityQueue的时候传入的Comparator ,这个优先选用
//2、 要插入的x自己实现的compareTo方法
private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
	}

//这里我只需分析comparator的情况就可以了
private void siftUpUsingComparator(int k, E x) {
//最坏的情况是:我找了一圈发现x才是整棵树种最小的。这时k为0,也就是到达整个堆的最小的元素(或者整棵树的根节点),停止循环。        
while (k > 0) {
	//第一句的意思是获得要插入的这个k位置在queue中对应的父元素的索引
	//我可以告诉大家这个式子的计算结果是:queue[n]节点的子节点是queue[2*n+1]和queue[2*(n+1)]
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
	//如果比较规则确定x"大于"父节点,就插在k位置了,跳出循环
            if (comparator.compare(x, (E) e) >= 0)
                break;
	//如果发现x较小,就将父节点的元素移到这个k位置
            queue[k] = e;
            k = parent;//现在要插入的位置变为原来父节点的位置
        }
        queue[k] = x;//
	}

嗯,这个类用了一种“堆”(逻辑上是二叉树,存储上用数组,树中的元素有大小关系,越小在数组中的index也越小)的数据结构。
典型应用是存储有优先级的任务,因为每次调用remove移除最小的元素(优先级最高的元素),都会自动排序,保证每次移除的都是优先级最高的任务。
同样,TreeMap逻辑上也是通过有序二叉树来组织数据的,不过,TreeMap通过节点的链接来组织存储结构,而PriorityQueue是通过数组的一些列计算确定逻辑上的树的节点的存放位置。

  • 大小: 5.4 KB
分享到:
评论
10 楼 逸情公子 2013-09-12  
不错不错,总结的很好,呵呵,以后面试之前就不用自己去看源码了
9 楼 贾懂凯 2011-04-12  
s929498110 写道
 

二叉树哪里,写的太简单了点吧

没有数据结构和算法上的解释啊, 我感觉这个TreeMap的源代码设计思路很复杂。在其他地方见过一篇专门介绍TreeMap的红黑二叉树,很详细,但是仍然不明白它的设计思路和算法效率。。。

是的,这篇却是简单了点,明天看能不能详细写一篇将TreeMap的。
8 楼 s929498110 2011-04-12  
 

二叉树哪里,写的太简单了点吧

没有数据结构和算法上的解释啊, 我感觉这个TreeMap的源代码设计思路很复杂。在其他地方见过一篇专门介绍TreeMap的红黑二叉树,很详细,但是仍然不明白它的设计思路和算法效率。。。
7 楼 无根V稻草 2011-03-28  
很详细呢,学习了
6 楼 xuehan_1010 2011-03-23  
TreeMap, TreeSet 用的是红黑树
5 楼 bryande 2010-11-28  
HashMap是数组+链表实现的...
4 楼 1927105 2010-11-28  
分析的很详细啊
3 楼 贾懂凯 2010-11-27  
纠正了PriorityQueue的错误,谢谢大家踊跃发言。
2 楼 J-catTeam 2010-11-27  
yiyidog125 写道
PriorityQueue 是个堆

优先级队列。
1 楼 yiyidog125 2010-11-27  
PriorityQueue 是个堆

相关推荐

    最新Java面试题视频网盘,Java面试题84集、java面试专属及面试必问课程

    │ 大型公司面试必答之数据结构与算法(二).mp4 │ ├─面试必问-JVM性能调优 │ JVM性能调优 2018-10-25.mp4 │ ├─面试必问-mybaits源码分析 │ │ 鲁班学院-上课笔记mybaits源码分析9-05.docx │ │ │ └─...

    java外部注册机源码-Data-Driven-Framework:数据驱动框架

    java外部注册机源码—— Selenium 自动化框架 自动化框架是您在开发自动化项目时引入的假设、概念和实践的集合,因此它有助于构建工作平台或支持自动化测试。如果框架独立于应用程序,那就太好了。 如果您查看任何...

    asp.net知识库

    与DotNet数据对象结合的自定义数据对象设计 (二) 数据集合与DataTable 与DotNet数据对象结合的自定义数据对象设计 (一) 数据对象与DataRow ASP.NET中大结果集的分页[翻译] .net 2.0 访问Oracle --与Sql Server的...

    Java SE实践教程 源代码 下载

    第3章 当一个变成多个——集合框架的基本概念 53 .3.1 讲解 54 3.1.1 集合概述 54 3.1.2 Collection接口 54 3.1.3 泛型(Generics) 56 3.1.4 Map接口 57 3.2 练习 59 3.2.1 创建课程管理系统 59 3.3 小结 ...

    OPhone平台2D游戏引擎实现——物理引擎

    OPhone平台2D游戏引擎实现——物理引擎(一) OPhone平台开发, 2010-10-19 17:27:20 标签 : Ophone平台 2D 游戏 引擎  上一篇文章我们介绍了常见的各种游戏特效的实现,你现在可以很轻松的实现各种游戏中所需要...

    李兴华 Java Web 开发实战经典_带源码_高清pdf 带书签 上

    5.2.2、第二种Scriptlet:!%&gt; 5.2.3、第三种Scriptlet: 5.3、Scriptlet标签 5.4、page指令 5.4.1、设置页面的MIME 5.4.2、设置文件编码 5.4.3、错误页的设置 5.4.4、数据库连接操作 5.5、包含指令 5.5.1、...

    疯狂的java讲义源码-gal-practice-lesson:gal实践课

    疯狂的java讲义源码Swift的 写一个教训: 课程目标 在 JavaScript 中使用Array.prototype.map执行基本数据转换。 假设 学生已经使用 Java、Ruby 或任何其他面向对象语言...语言的所有基本语法、类型和数据结构。 我们还

    java web 视频、电子书、源码(李兴华老师出版)

    7.2.1、WEB开发的标准目录结构 7.2.2、使用JSP的page指令导入所需要的JavaBean 7.2.3、使用指令 7.3、JavaBean与表单 7.4、设置属性: 7.4.1、设置指定的属性 7.4.2、指定设置属性的参数 7.4.3、为属性设置...

    李兴华 java_web开发实战经典 源码 完整版收集共享

    5.2.2、第二种Scriptlet:!%&gt; 5.2.3、第三种Scriptlet: 5.3、Scriptlet标签 5.4、page指令 5.4.1、设置页面的MIME 5.4.2、设置文件编码 5.4.3、错误页的设置 5.4.4、数据库连接操作 5.5、包含指令 5.5.1、...

    李兴华 Java Web 开发实战经典_带源码_高清pdf 带书签 下

    5.2.2、第二种Scriptlet:!%&gt; 5.2.3、第三种Scriptlet: 5.3、Scriptlet标签 5.4、page指令 5.4.1、设置页面的MIME 5.4.2、设置文件编码 5.4.3、错误页的设置 5.4.4、数据库连接操作 5.5、包含指令 5.5.1、...

    Java数据库编程宝典3

    14.1.1 使用Blob存储二进制数据 14.1.2 使用Clob存储文本数据 14.2 从浏览器上载图像或文档 14.2.1 用于从DBMS下载大对象的servlet 14.3 小结 第15章 使用JSP,XSL和可滚动的ResultSet显示数据 15.1 可滚动...

    Java语言程序设计

    不但详细介绍了Java语言本身,而且讨论了面向对象的设计思想和编程方法、UML建模语言、图形用户界面的编程方法、网络和数据库程序的编程方法、线程的使用、Java集合框架等实用开发技术。全书以面向对象的程序设计...

    Java Web编程宝典-十年典藏版.pdf.part2(共2个)

    是PDF电子书,不是源码。共分2个包。 《Java Web编程宝典(十年典藏版)》是一本集技能、范例、项目和应用为一体的学习手册,书中介绍了应用Java Web进行程序开发的各种技术、技巧。全书分4篇,共24章,其中,第1篇为...

    各大数据组件介绍.pdf

    特性: 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。 ⾼吞吐量 :即使是⾮常普通的硬件Kafka也可以⽀持每秒数百万的消息。 ⽀持通过Kafka服务器和消费...

    c#学习笔记.txt

    委托是一个数据结构,该数据结构引用一个静态方法,或引用一个对象实例和该对象的实例方法。在 C 或 C 中与委托最接近的是函数指针,但函数指针只能引用静态函数,而委托可以同时引用静态方法和实例方法。在后一种...

    外文翻译 stus MVC

    Struts——an open-source MVC implementation This article introduces Struts, a Model-View-Controller implementation that uses servlets and JavaServer Pages (JSP) technology. Struts can help you control...

    Java2游戏编程.pdf

    4.3.3 Java2集合框架 4.4 总结 4.5 练习 第2篇 Java 2-D图像开发和抽象Window工具包 第5章 Applet基础 5.1 什么是Java applet 5.2 Applet和Application的比较 5.3 Applet的组成和生命周期 5.4 一个Applet例子 5.5 ...

Global site tag (gtag.js) - Google Analytics