05《数据结构入门教程》Java 中基于数组和链表的常用类

1. 前言

通过前面的学习,我们其实对 ArrayList 和 LinkedList 已经很熟悉了,他们虽然都是继承自 List,但是前者是基于数组实现的,后者是基于链表实现的,结合各自的特点我们会在前半节对比总结他们的特点和用法。 HashMap 和 HashSet 分别实现了 Map 接口和 Set 接口,HashSet 是一个值不重复的集合,HashMap 对键值对进行映射,是一个键不重复的集合。我们会在后半部分对比介绍各自的特点和用法。

2. Vector和ArrayList

结合 JAVA 源码,我们可以看到 Vector 和 ArrayList 都是基于数组实现的,都继承自 List 接口。因此他们随机查询的效率是非常高的,但是他们在数据插入或删除,以及需要扩容的时候效率都比较低下,需要在原有数组之外进行复制、移动。还有一点细节的区别在于扩容的默认值,ArrayList 在内存不足时默认扩容至 1.5 倍再加 1 个,Vector 默认扩容为原来的 2 倍。 他们最重要的区别在于 Vector 中大量使用了 synchronized 来修饰方法,所以它是线程安全的,相应的,效率也是比 ArrayList 更低的。所以我们生成今天的第一条结论:

  • 需要线程安全的场景使用Vector

3. ArrayList和LinkeList

经过前面的学习我们已经知道了 ArrayList 是基于数组的,查询快插入慢,LinkedList 是基于链表的,插入快遍历慢。我们来做个实验看一下是不是真的如此:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n13" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Integer index = 10000000;

List<Integer> arrayList = new ArrayList<Integer>();
List<Integer> linkedList = new LinkedList<Integer>();

Long arrayStart = new Date().getTime();
for (int i = 0; i < index; i++){
arrayList.add(i);
}
Long arrayEnd = new Date().getTime();

Long linkedStart = new Date().getTime();
for (int j = 0; j < index; j++) {
linkedList.add(j);
}
Long linkedEnd = new Date().getTime();

System.out.println("插入"+index+"条记录:");
System.out.println("ArrayList耗时:"+(arrayEnd-arrayStart)+"毫秒");
System.out.println("LinkedList耗时:"+(linkedEnd-linkedStart)+"毫秒");
代码块1234567891011121314151617181920</pre>

结果: 插入 10000000 条记录: ArrayList 耗时:359 毫秒 LinkedList 耗时:2891 毫秒

我们会发现两者相差并不大,而且当我们把数据量上升到 10000000 的时候还会发现 ArrayList 的效率反而更快,这是什么原因呢。 原来我们使用的 add (E e) 方法有这样的注释:

Appends the specified element to the end of this list (optional operation).

所以当把元素追加在最后的时候,数组中的其他元素不需要移动,此时 ArrayList 的处理效率也很不错。那么我们换个思路再试:

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n19" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">//元素个数减少到10万即可看出效果
Integer index = 100000;

List<Integer> arrayList = new ArrayList<Integer>();
List<Integer> linkedList = new LinkedList<Integer>();

Long arrayStart = new Date().getTime();
for (int i = 0; i < index; i++){
//其他代码不变,我们将每次遍历时添加新元素的位置固定为第一个
arrayList.add(0,i);
}
Long arrayEnd = new Date().getTime();

Long linkedStart = new Date().getTime();
for (int j = 0; j < index; j++) {
//其他代码不变,我们将每次遍历时添加新元素的位置固定为第一个
linkedList.add(0,j);
}
Long linkedEnd = new Date().getTime();

System.out.println("插入"+index+"条记录:");
System.out.println("ArrayList耗时:"+(arrayEnd-arrayStart)+"毫秒");
System.out.println("LinkedList耗时:"+(linkedEnd-linkedStart)+"毫秒");
代码块1234567891011121314151617181920212223</pre>

结果: 插入 100000 条记录 ArrayList 耗时:828 毫秒 LinkedList 耗时:16 毫秒

这个测试结果跟理论就匹配上了,ArrayList 把大量的时间都用在了移动元素上,而 LinkedList 不需要做这样的操作,所以此时它的效率优势体现的非常明显。所以我们在日常使用中一定要区分,在使用 add 方法的时候是否需要指定新添加元素的位置,另外,由于 ArrayList 是基于无序数组的,因此遍历的时候可能输出顺序和插入顺序是不一致的,而 LinkedList 由于有严格的指针相连,顺序一定是固定的。 所以我们生成第二组结论:

  • 需要频繁在指定位置添加、删除元素,而查询指定位置数据较少的场景优先使用 LinkedList

  • 随机查询较多、在指定位置添加、删除元素较少,或者干脆不关心插入或删除元素的位置及遍历顺序时,优先使用 ArrayList

  • 需要元素按顺序排列时,使用 LinkedList

4. Map家族

java.util.Map 接口常用的实现类有 HashMap、HashTable、ConcurrentHashMap、LinkedHashMap 和 TreeMap。他们都是用来储存 key-value 键值对的,可以通过键快速的读取值,也因此,他们的键都是不可以重复的。那么他们各自又有什么特点,彼此又有什么区别呢?

4.1 HashMap

HashMap 是我们最常用的 Map 类,它的底层是由数组和链表来实现的,允许键或值为 null,并且顺序是不固定的,默认容量是 16,当元素超过 Entry 数组的加载因子(默认值是 75%)时触发扩容,扩容时原来数组中的元素要依次重新插入到新的数组中。它是非线程安全的。

4.2 HashTable

HashTable 初始容量为 11,并且键和值都不允许为 null,其他特性与 HashMap 没有太大区别。虽然它是线程安全的,但是我个人更推荐大家使用 ConcurrentHashMap。

4.3 ConcurrentHashMap

我们可以在 java.util.concurrent.ConcurrentHashMap 下通过注释了解到,这个类在 HashMap 和 HashTable 的基础上做了优化。它实现了线程安全,通过把 Map 分段成若干个段,在需要锁定的时候只锁定其中的一段,而在读取的时候不做任何锁定操作,另外用 volatile 修饰 HashEntry 的 value 来实现数据的实时更新,在 HashTable 的基础上大大提升了效率。

5f026b1a09ada93609880863.jpg

4.4 LinkedHashMap

LinkedHashMap 记录了元素保存的顺序,因此在遍历的时候可以按照当时的顺序读取,一般用于我们需要保留元素顺序的场景。

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Map<String, String> hashMap = new HashMap<String, String>();
hashMap.put("水果", "苹果");
hashMap.put("蔬菜", "白菜");
hashMap.put("坚果", "核桃");
Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
while(iterator.hasNext()) {
Map.Entry entry = iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
代码块1234567891011</pre>

对于无序的 HashMap,依次输出 key: 蔬菜,value: 白菜 key: 水果,value: 苹果 key: 坚果,value: 核桃

<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="java" cid="n47" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Map<String, String> linkedHashMap = new LinkedHashMap<String,String>();
linkedHashMap.put("水果", "苹果");
linkedHashMap.put("蔬菜", "白菜");
linkedHashMap.put("坚果", "核桃");
Iterator<Map.Entry<String, String>> iterator = linkedHashMap.entrySet().iterator();
while(iterator.hasNext()) {
Map.Entry entry = iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
代码块1234567891011</pre>

对于有序的 LinkedHashMap,依次输出 key: 水果,value: 苹果 key: 蔬菜,value: 白菜 key: 坚果,value: 核桃

4.5 TreeMap

TreeMap 的底层实现了红黑树的结构,是一个可以比较元素大小的 Map 集合,默认会根据元素的 key 值进行自然排序(前提是 key 值实现了 Comparable 接口),我们也可以自己定义比较器来进行排序。 总结一下:

  • 日常使用 HashMap;

  • 需要线程安全时用 ConcurrentHashMap;

  • 需要记录元素保存顺序时使用 LinkedHashMap;

  • 需要排序时使用 TreeMap。

5. 小结

本节我们学习了数据结构在 Java 中的应用,对于不同数据结构的封装类的使用场景做了一些总结:List 接口下需要线程安全的场景使用 Vector,增、删较多或需要元素有序时使用 LinkedList,查询较多时使用 ArrayList;Map 接口下需要线程安全的场景使用 ConcurrentHashMap,需要记录元素顺序时使用 LinkedHashMap,需要排序时使用 TreeMap,日常使用 HashMap

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容