【转】Java学习---Java核心数据结构(List,Map,Set)使用技巧与优化

【原文】https://www.toutiao.com/i6594587397101453827/

Java核心数据结构(List,Map,Set)使用技巧与优化

JDK提供了一组主要的数据结构实现,如List、Map、Set等常用数据结构。这些数据都继承自 java.util.Collection 接口,并位于 java.util 包内。

1、List接口

最重要的三种List接口实现:ArrayList、Vector、LinkedList。它们的类图如下:

可以看到,3种List均来自 AbstratList 的实现。而 AbstratList 直接实现了List接口,并扩展自 AbstratCollection。

ArrayList 和 Vector 使用了数组实现,可以认为,ArrayList 封装了对内部数组的操作。比如向数组中添加、删除、插入新的元素或数组的扩展和重定义。对ArrayList或者Vector的操作,等价于对内部对象数组的操作。

ArrayList 和 Vector 几乎使用了相同的算法,它们的唯一区别可以认为是对多线程的支持。ArrayList 没有对一个方法做线程同步,因此不是线程安全的。Vector 中绝大多数方法都做了线程同步,是一种线程安全的实现。因此ArrayList 和 Vector 的性能特性相差无几。

LinkedList 使用了循环双向链表数据结构。LinkedList 由一系列表项连接而成。一个表项总是包含3个部分:元素内容、前驱表项和后驱表项。如图所示:

LinkedList的表项源码:

无论LinkedList是否为空,链表都有一个header表项,它既是链表的开始,也表示链表的结尾。它的后驱表项便是链表的第一个元素,前驱表项便是链表的最后一个元素。如图所示:

下面比较下ArrayList 和 LinkedList的不同。

1. 增加元素到列表尾端

对于ArrayList来说,只要当前容量足够大,add()操作的效率是非常高的。

只有当ArrayList对容量的需求超过当前数组的大小时,才需要进行扩容。扩容会进行大量的数组复制操作。而复制时最终调用的是System.arraycopy()方法,因此,add()效率还是相当高的。

LinkedList由于使用了链表的结构,因此不需要维护容量的大小。这点比ArrayList有优势,不过,由于每次元素增加都需要新建Node对象,并进行更多的赋值操作。在频繁的系统调用中,对性能会产生一定影响。

2. 插入元素到列表任意位置

ArrayList是基于数组实现的,而数组是一块连续的内存空间,每次插入操作,都会进行一次数组赋值。大量的数组复制会导致系统性能低下。

LinkedList是基于链表实现的,在任意位置插入和在尾端增加是一样的。所以,如果系统应用需要对List对象在任意位置进行频繁的插入操作,可以考虑用LinkedList替代ArrayList。

3. 删除任意位置元素

对ArrayList来说,每次remove()移除元素都需要进行数组重组。并且元素位置越靠前开销越大,要删除的元素越靠后,开销越小。

在LinkedList的实现中,首先需要通过循环找到要删除的元素。如果要删除的元素位置处于List的前半段,则从前往后找;若处于后半段,则从后往前找。如果要移除中间位置的元素,则需要遍历完半个List,效率很低。

4. 容量参数

容量参数是ArrayList 和 Vector等基于数组的List的特有性能参数,它表示初始数组的大小。

合理的设置容量参数,可以减少数组扩容,提升系统性能。

默认ArrayList的数组初始大小为10。

private static final int DEFAULT_CAPACITY = 10;

5. 遍历列表

常用的三种列表遍历方式:ForEach操作、迭代器 和 for循环。

对于ForEach操作,反编译可知实际上是将ForEach循环体作为迭代器处理。不过ForEach比自定义的迭代器多了一步赋值操作,性能不如直接使用迭代器的方式。

使用For循环通过随机访问遍历列表,ArrayList表现很好,速度最快;但是LinkedList的表现非常差,应避免使用,这是因为对LinkedList的随机访问时,总会进行一次列表的遍历操作。

2、Map接口

Map是一种非常常用的数据结构。围绕着Map接口,最主要的实现类有Hashtable, HashMap, LinkedHashMap 和 TreeMap,在Hashtable中,还有Properties 类的实现。

Hashtable和hashMap的区别在于Hashtable的大部分方法都做了线程同步,而HashMap没有,因此,Hashtable是线程安全的,HashMap不是。其次,Hashtable 不允许key 或 value使用null值,而HashMap可以。第三,它们在内部对key的hash算法和hash值到内存索引的映射算法不同。

由于HashMap使用广泛,本文以HashMap为例,阐述它的实现原理。

1. HashMap的实现原理

简单来说,HashMap就是将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应的数据。在HashMap中,底层数据结构使用的是数组。所谓的内存地址,就是数组的下标索引。

用代码简单表示如下:

object[key_hash] = value;

2. Hash冲突

当需要存放的两个元素1和2经hash计算后,发现对应在内存中的同一个地址。此时HashMap又会如何处理以保证数据的完整存放?

在HashMap的底层使用数组,但数组内的元素不是简单的值,而是一个Entity类的对象。每一个Entity表项包括key,value,next,hash几项。注意这里的next部分,它指向另外一个Entity。当put()操作有冲突时,新的Entity会替换原有的值,为了保证旧值不丢失,会将next指向旧值。这便实现了在一个数组空间内存放多个值项。因此,HashMap实际上是一个链表的数组。而在进行get()操作时,如果定位到的数组元素不含链表(当前entry的next指向null),则直接返回;如果定位到的数组元素包含链表,则需要遍历链表,通过key对象的equals方法逐一比对查找。

3. 容量参数

和ArrayList一样,基于数组的结构,不可避免的需要在数组空间不足时,进行扩展。而数组的重组比较耗时,因此对其做一定的优化很有必要了。

HashMap提供了两个可以指定初始化大小的构造函数:

HashMap(int initialCapacity)

构造一个带指定初始容量和默认负载因子 (0.75) 的空 HashMap。

HashMap(int initialCapacity, float loadFactor)

构造一个带指定初始容量和负载因子的空 HashMap。

其中,HashMap会使用大于等于initialCapacity并且是2的指数次幂的最小的整数作为内置数组的大小。

负载因子又叫做填充比,它是介于0和1之间的浮点数。

负载因子 = 实际元素个数 / 内部数组总大小

负载因子的作用就是决定HashMap的阈值(threshold)。

阈值 = 数组总容量 × 负载因子

当HashMap的实际容量超过阈值便会进行扩容,每次扩容将新的数组大小设置为原大小的1.5倍。

默认情况下,HashMap的初始大小是16,负载因子为0.75。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final float DEFAULT_LOAD_FACTOR = 0.75f;

4. LinkedHashMap

LinkedHashMap继承自HashMap,因此,它具备了HashMap的优良特性,并在此基础上,LinkedHashMap又在内部增加了一个链表,用以存放元素的顺序。因此,LinkedHashMap 可以简单理解为一个维护了元素次序表的HashMap.

LinkedHashMap 提供两种类型的顺序:一是元素插入时的顺序;二是最近访问的顺序。

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

构造一个带指定初始容量、负载因子和排序模式的空 LinkedHashMap 实例

其中 accessOrder 为 true 时,按照元素最后访问时间排序;当 accessOrder 为 false 时,按照插入顺序排序。默认为 false 。

在内部实现中,LinkedHashMap 通过继承 HashMap.Entity 类,实现 LinkedHashMap.Entity,为 HashMap.Entity 增加了 before 和 after属性用以记录某一表项的前驱和后继,并构成循环链表。

5. TreeMap

TreeMap可以简单理解为一种可以进行排序的Map实现。与 LinkedHashMap 不同,LinkedHashMap 是根据元素增加或者访问的先后顺序进行排序,而TreeMap则根据元素的Key进行排序。为了确定Key的排序算法,可以使用两种方式指定:

(1)在TreeMap的构造函数中注入一个Comparator:

TreeMap(Comparator<? super K> comparator)

(2)使用一个实现了 Comparable 接口的 Key。

TreeMap的内部实现是基于红黑树的。红黑树是一种平衡查找树,这里不做过多介绍。

TreeMap 其它排序接口如下:

subMap(K fromKey, K toKey)

返回此映射的部分视图,其键值的范围从 fromKey(包括)到 toKey(不包括)。

tailMap(K fromKey)

返回此映射的部分视图,其键大于等于 fromKey。

firstKey()

返回此映射中当前第一个(最低)键。

headMap(K toKey)

返回此映射的部分视图,其键值严格小于 toKey。

一个简单示例如下:

3、Set接口

Set并没有在Collection接口之上增加额外的操作,Set集合中的元素是不能重复的

其中最为重要的是HashSet、LinkedHashSet、TreeSet 的实现。这里不再一一赘述,因为所有的这些Set实现都只是对应的Map的一种封装而已。

4、优化集合访问代码

1. 分离循环中被重复调用的代码

举个例子,当我们要使用for循环遍历集合时

for (int i =0;i<collection.size();i++){

//.....

}

很明显,每次循环都会调用size()方法,并且每次都会返回相同的数值。分离所有类似的代码对提升循环性能有着积极地意义。因此,可以将上段代码改造成

int size= collection.size();

for (int i =0;i<size;i++){

//.....

}

当元素的数量越多时,这样的处理就越有意义。

2. 省略相同的操作

假设我们有一段类似的操作如下

int size= collection.size();

for (int i =0;i<size;i++){

if (list.get(i)==1||list.get(i)==2||list.get(i)==3){

//...

}

}

虽然每次循环调用get(i)的返回值不同,但在同一次调用中,结果是相同的,因此可以提取这些相同的操作。

int size= collection.size();

int k=0;

for (int i =0;i<size;i++){

if ((k = list.get(i))==1||k==2||k==3){

//...

}

}

3. 减少方法调用

方法调用是需要消耗系统堆栈的,如果可以,则尽量访问内部元素,而不要调用对应的接口,函数调用是需要消耗系统资源的,直接访问元素会更高效。

假设上面的代码是Vector.class的子类的部分代码,那么可以这么改写

int size = this.elementCount;

Object k=null;

for (int i =0;i<size;i++){

if ((k = elementData[i])=="1"||k=="2"||k=="3"){

//...

}

}

可以看到,原本的 size() 和 get() 方法被直接替代为访问原始变量,这对系统性能的提升是非常有用的。

5、RandomAccess接口

RandomAccess接口是一个标志接口,本身并没有提供任何方法,任何实现RandomAccess接口的对象都可以认为是支持快速随机访问的对象。此接口的主要目的是标识那些可以支持快速随机访问的List实现

在JDK中,任何一个基于数组的List实现都实现了 RandomAccess接口,而基于链表的实现则没有。这很好理解,只有数组能够快速随机访问,(比如:通过 object[5],object[6]可以直接查找并返回对象),而对链表的随机访问需要进行链表的遍历。

在实际操作中,可以根据list instanceof RandomAccess来判断对象是否实现 RandomAccess 接口,从而选择是使用随机访问还是iterator迭代器进行访问。

在应用程序中,如果需要通过索引下标对 List 做随机访问,尽量不要使用 LinkedList,ArrayList和Vector都是不错的选择。

参考

《Java程序性能优化》葛一鸣著

原文地址:https://www.cnblogs.com/ftl1012/p/9570768.html

时间: 2024-10-27 08:28:23

【转】Java学习---Java核心数据结构(List,Map,Set)使用技巧与优化的相关文章

redis 源码学习(核心数据结构剖析)

redis是个key, value数据库,是个内存数据库.目前是个互联网公司的架构标配. 支持的数据对象有string, list, set, zest和hash object. 数据结构: 数据库的核心结构是dict(实现是使用hashmap): key: string value: string或者list或者set或者zest或者hash object. dict数据结构定义: typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈

Java学习:集合双列Map

数据结构 数据结构: 数据结构_栈:先进后出 入口和出口在同一侧 数据结构_队列:先进先出 入口和出口在集合的两侧 数据结构_数组: 查询快:数组的地址是连续的,我们通过数组的首地址可以找到数组,通过数组的索引可以快速的查找某一个元素. 增删慢:数组的长度是固定的,我们想要增加/删除一个元素,必须创建一个新数组,把原数组的数据复制过来 例: int[] arr = new int[]{1,2,3,4}; 要把数组索引是3的元素删除 必须创建一个新的数组,长度是原数组的长度-1 把原数组的其它元素

java学习第18天(map集合)

Map集合是将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值. 存储的是键值对形式的元素,键唯一,值可以重复,有点类似于数据库中的主键加数据.主要功能有: A:添加功能 put方法 B:删除功能 remove方法 C:判断功能 boolean containsKey(Object key)//判断是否含有键 boolean containsValue(Object Value) boolean isEmpty() D:获取功能 get方法//举例说明 E:长度功能 Map

JAVA学习总结-常用数据结构

java中集合框架其实就是数据结构的实现的封装; 参考资料:任小龙教学视频 1,什么是数据结构? 数据结构是计算机存储,组织数据的方式; 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合; 通常情况下,精心选择的数据结构可带来更高的运行或者存储效率, 数据结构往往同高效的检索算法和索引技术有关; 2,数据结构的基本功能 增(Create)  删(Delete)   改(Update)  查(Read) 3,常见的数据结构 3.1,数组Array; 数组是最简单的数据结构;是用来存放同一

JAVA学习--java中的集合框架

与数组相比:1.数组的长度固定,而集合的长度可变2.数组只能通过下表访问元素,类型固定,而有的集合可以通过任意类型查找所映射的具体对象 java集合框架:collection(list序列,queue队列,set集)和map(映射存储数据),红色为常用

[java学习]java容器源码初探(1)

一.动态数组ArrayList 在我们开发者眼中,这就是一个"动态数组",可以"动态"地调整数组的大小,虽然说数组从定义了长度后,就不能改变大小. 实现"动态"调整的基本原理就是:按照某个调整策略,重新创建一个调整后一样大小的数组,然后将原来的数组赋值回去. 下面我们来解析一下几个与数组不一样的方法. 看看ArrayList中主要的几个字段(源码剖析): // 默认的初始数组大小 private static final int DEFAULT_

JAVA学习-Java新特性(泛型、枚举、Annotation)

所谓的Java新特性现在都是指从JDK 1.5之后开始的,例如,在前面已经学习过两个新特性:switch支持String判断(JDK 1.7提供的).自动装箱和拆箱.可变参数.foreach.静态导入.泛型.枚举.Annotation. 对于所有的新特性,我的个人建议:有些新特性你今天一定是不知道怎么用的,我们今天只是来看一下这些语法,至于使用方面,慢慢来观察. 1.可变参数(理解) 如果说现在有这样一个要求,要求实现整数的加法操作,并且方法可以接收任意多个整型数据一起实现加法操作. 如果按照传

[Java学习] Java字符串(String)

从表面上看,字符串就是双引号之间的数据,例如"微学苑"."http://www.weixueyuan.net"等.在Java中,可以使用下面的方法定义字符串: String stringName = "string content"; 例如: 1. String url = "http://www.weixueyuan.net"; 2. String webName = "微学苑"; 字符串可以通过&quo

[Java学习] Java方法重载

在Java中,同一个类中的多个方法可以有相同的名字,只要它们的参数列表不同就可以,这被称为方法重载(method overloading). 参数列表又叫参数签名,包括参数的类型.参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同. 重载是面向对象的一个基本特性. 下面看一个详细的实例. 1. public class Demo{ 2. // 一个普通的方法,不带参数 3. void test(){ 4. System.out.println("No parameters");