原创

Java基础篇Map集合类简介和使用原理剖析

Java中最长用的集合类是List和Map,List的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象元素列表。List 适用于按数值索引访问元素的情形。


Map提供了一个更通用的元素存储方法,Map集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。从概念上而言可以将List看作是具有数值键的 Map。而实际上除了List和Map都在定义 java.util中外,两者并没有直接的联系。


Java核心类中有很多预定义的Map类,在介绍具体实现之前先了解一下Map接口本身,以便于清楚所有实现的共同点。Map接口定义可以分为四种类型的方法,两个最常用的方法equals(Object o)是比较指定对象与此Map的等价性,而hashCode()返回此Map的哈希码,可以将这Object的这两个方法覆盖,以正确比较Map对象的等价性。

blob.png

blob.png


Map定义了几个用于插入和删除元素的变换方法,clear()是从Map中删除所有数据,remove(Object key)是从Map中删除键和关联的值,put(Object key, Object value)将指定值与指定键相关联,putAll(Map t)将指定Map中的所有数据复制到此map。注意:putAll通常不必使用大量的put()调用更有效率,而putAll()存在的意义是除了用户迭代put()所执行的每个键值对添加到Map的算法外,还需迭代所传递的Map元素。putAll()在添加元素之前可以调整Map的大小。如果未调整Map的大小,则putAll()可能比预期的更有效。


Java中自带各种Map类,这些Map类可以归为三种类型

1) 通用Map用于在应用程序中管理映射,通常在java.util程序包中实现

HashMap   是基于“拉链法”实现的散列表。底层采用“数组+链表”实现。
Hashtable    基于“拉链法”实现的散列表。
Properties    继承Map类
LinkedHashMap   是HashMap的一个子类,保存了记录的插入顺序
IdentityHashMap    哈希表实现Map接口,比较键(和值)时使用引用相等性代替对象相等性。
TreeMap    有序散列表,实现SortedMap接口,底层通过红黑树实现。
WeakHashMap    基于“拉链法”实现的散列表。
ConcurrentHashMap    采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。


2) 专用Map通常不必亲自创建此类Map,而是通过某些其他类对其进行访问

java.util.jar.Attributes
javax.print.attribute.standard.PrinterStateReasons
java.security.Provider
java.awt.RenderingHints
javax.swing.UIDefaults


3) 用于帮助实现自己的Map类的抽象类

AbstractMap

这个类注意用于减少实现Map接口任务而进行设计的,一些简单通用的方法就不需要Map本身实现。

transient和volatile关键词的使用,源码如下:

/**
 * Each of these fields are initialized to contain an instance of the
 * appropriate view the first time this view is requested.  The views are
 * stateless, so there's no reason to create more than one of each.
 */
transient volatile Set        keySet = null;
transient volatile Collection values = null;

关于关键词解释:

transient是变量修饰符,表明该字段不是对象持久状态的一部分,储存的时候不用储存,比如序列化这个对象时,该字段是不会储存的。


volatile也是变量修饰符,只能用来修饰变量。volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。


在此解释一下Java的内存机制:

Java使用一个主内存来保存变量当前值,而每个线程则有其独立的工作内存。线程访问变量的时候会将变量的值拷贝到自己的工作内存中,这样,当线程对自己工作内存中的变量进行操作之后,就造成了工作内存中的变量拷贝的值与主内存中的变量值不同。


Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。

这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。


使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。


如何获取最佳Map性能的简单方式,将所有Map变量声明为Map,而不是具体实现,参考实例:

Map cMap = new HashMap(); // 好
HashMap cMap = new HashMap(); // 差

这样可以只更改一行代码即可非常轻松的替换任何特定Map实例。


由于HashMap在java开发中占有比较重要的地位,本文重点分析一下hashmap的重要属性和优化。

HashMap重要属性

/**
* The default initial capacity - MUST be a power of two.(map的初始大小)
*/

DEFAULT_INITIAL_CAPACITY = 16; // 默认大小

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*(最大容量,如果指定的容易大于最大容量,将使用此值)
*/
MAXIMUM_CAPACITY = 1 << 30; // 最大容量

/**
* The load factor used when none specified in constructor.
*/
DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子

/**
* The next size value at which to resize (capacity * load factor).
*/
int threshold; // map是否扩容的决定性因素

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 * bucket 数组中最小存储单元
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
	if ((size >= threshold) && (null != table[bucketIndex])) {
		resize(2 * table.length);
		hash = (null != key) ? hash(key) : 0;
		bucketIndex = indexFor(hash, table.length);
	}

	createEntry(hash, key, value, bucketIndex);
}

HashMap优化

如果哈希映射的内部数组只包含一个元素,则所有项将映射到此数组位置,从而构成一个较长的链接列表。由于我们的更新和访问使用了对链接列表的线性搜索,而这要比Map中的每个数组索引只包含一个对象的情形要慢得多,因此这样做的效率很低。访问或更新链接列表的时间与列表的大小线性相关,而使用哈希函数问或更新数组中的单个元素则与数组大小无关 — 就渐进性质(Big-O 表示法)而言,前者为 O(n),而后者为 O(1)。因此使用一个较大的数组而不是让太多的项聚集在太少的数组位置中是有意义的。


调整Map实现的大小

在哈希术语中,内部数组中的每个位置称作“存储桶”(bucket),而可用的存储桶数(即内部数组的大小)称作容量(capacity)。为使Map对象有效地处理任意数目的项,Map实现可以调整自身的大小。但调整大小的开销很大。调整大小需要将所有元素重新插入到新数组中,这是因为不同的数组大小意味着对象现在映射到不同的索引值。先前冲突的键可能不再冲突,而先前不冲突的其他键现在可能冲突。这显然表明,如果将 Map 调整得足够大,则可以减少甚至不再需要重新调整大小,这很有可能显著提高速度。

关注下方微信公众号“Java精选”(w_z90110),回复关键字领取资料:如HadoopDubboCAS源码等等,免费领取资料视频和项目。 

涵盖:程序人生、搞笑视频、算法与数据结构、黑客技术与网络安全、前端开发、Java、Python、Redis缓存、Spring源码、各大主流框架、Web开发、大数据技术、Storm、Hadoop、MapReduce、Spark、elasticsearch、单点登录统一认证、分布式框架、集群、安卓开发、iOS开发、C/C++、.NET、Linux、Mysql、Oracle、NoSQL非关系型数据库、运维等。

评论

分享:

支付宝

微信