三.常见Java集合的实现细节

3.1Set和Map

Set代表一种集合元素无序,集合元素不可重复的集合,Map则代表一种由多个key-value对组成的集合,Map集合类似于传统的关联数组。表面上看他们之间相似性很少,但实际上Map和Set之间有莫大的关联,可以说Map集合是Set集合的扩展

3.1.1Set和Map的关系

Set<->Map
EnumSet<->EnumMap
SortedSet<->SortedMap
TreeSet<->TreeMap
NaivigableSet<->NaivigableMap
HashSet<->HashMap
LinkedHashSet<->LinkedHashMap
如果只考察Map集合的key,不难发现,这些Map集合的key都具有Set集合的特征,所有key不能重复,key之间没有顺序,也就是如果将Map集合所有的key集中起来,那这些key就组成了一个set集合而key与value相互关联,所以对于Map而言,相当于每个元素都是key-value的Set集合

import java.util.*;

class SimpleEntry<K , V>
	implements Map.Entry<K , V>, java.io.Serializable
{
	private final K key;
	private V value;
	//定义如下两个构造器
	public SimpleEntry(K key, V value)
	{
		this.key   = key;
		this.value = value;
	}
	public SimpleEntry(Map.Entry<? extends K
		, ? extends V> entry)
	{
		this.key   = entry.getKey();
		this.value = entry.getValue();
	}
	//获取key	
	public K getKey()
	{
		return key;
	}
	//获取value
	public V getValue()
	{
		return value;
	}
	//改变该key-value对的value值
	public V setValue(V value)
	{
		V oldValue = this.value;
		this.value = value;
		return oldValue;
	}
	//根据key比较两个SimpleEntry是否相等。
	public boolean equals(Object o) 
	{
		if (o == this)
		{
			return true;
		}
		if (o.getClass() == SimpleEntry.class)
		{
			SimpleEntry se = (SimpleEntry)o;
			return se.getKey().equals(getKey());
		}
		return false;
	}
	//根据key计算hashCode
	public int hashCode() 
	{
		return key   == null ? 0 :   key.hashCode();
	}

	public String toString() 
	{
		return key + "=" + value;
	}
}
//继承HashSet实现一个Map
public class Set2Map<K , V> 
	extends HashSet<SimpleEntry<K , V>>
{
	//实现清空所有key-value对的方法
	public void clear() 
	{
		super.clear();
	}
	//判断是否包含某个key
	public boolean containsKey(K key) 
	{
		return super.contains(
			new SimpleEntry<K , V>(key ,null));
	}
	//判断是否包含某个value
	boolean containsValue(Object value)
	{
		for (SimpleEntry<K , V> se : this)
		{
			if (se.getValue().equals(value))
			{
				return true;
			}
		}
		return false;
	}
	//根据指定key取出对应的value
	public V get(Object key)
	{
		for (SimpleEntry<K , V> se : this)
		{
			if (se.getKey().equals(key))
			{
				return se.getValue();
			}
		}
		return null;
	}
	//将指定key-value对放入集合中
	public V put(K key, V value)
	{
		add(new SimpleEntry<K , V>(key ,value));
		return value;
	}
	//将另一个Map的key-value对放入该Map中
	public void putAll(Map<? extends K,? extends V> m) 
	{
		for (K key : m.keySet())
		{
			add(new SimpleEntry<K , V>(key , m.get(key)));
		}
	}
	//根据指定key删除指定key-value对
	public V removeEntry(Object key) 
	{
		for (Iterator<SimpleEntry<K , V>> it = this.iterator()
			; it.hasNext() ; )
		{
			SimpleEntry<K , V> en = (SimpleEntry<K , V>)it.next();
			if (en.getKey().equals(key))
			{
				V v = en.getValue();
				it.remove();
				return v;
			}
		}
		return null;
	}
	//获取该Map中包含多少个key-value对
	public int size()
	{
		return super.size();
	}
}

以上代码表示了将Set2Map<k,v>改造为Map并实现了Map的大部分方法(因为采用HashSet作为实现类因此扩展出来的Map本质上是一个HashMap)

3.1.2HashMap和HashSet

HashSet和HashMap之间有许多相似之处,对于HashSet而言,系统采用Hash算法决定集合元素存储的位置,这样可以保证快速存取集合元素;对于HashMap而言,系统将value当成key的附属,系统根据Hash算法来决定key的存储位置,这样可以保证快速存取集合key,而value总是紧随key存储的

注:虽然集合号称存储的是Java对象,但实际上并不会真正将Java对象放入集合,而只是在Set集合中保留这些对象的引用。也就是说,Java集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的Java对象

HashSet和HashMap的存储方式
1).HashMap
采用一种所谓的Hash算法来决定每个元素的存储位置
当程序执行map.put()方法时,系统将调用key的hashCode()方法得到其hashCode值——每个Java对象都有hashCode()方法,都可以通过该方法获得它的hashCode值。得到这个对象的hashCode值之后,系统会根据hashCode值来决定该元素的存储位置。

在HashMap的put方法源码可以看出,当程序试图将一个key-value对放入HashMap中时,首先根据该key的hashCode()返回值决定该Entry的存储位置:如果两个Entry的key的HashCode()反回值相同,那他们的存储位置相同;如果这两个Entry的key通过equals比较返回true,新添加Entry的value将集合中原有Entry的value但key不会覆盖;如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有的Entry形成Entry链

Entry链;
Entry链是为了解决Hash冲突而存在的,不同的key可能hash值一样,这个时候就会散列在同一个position上面,解决Hash冲突的办法有很多种,
Java使用的就是拉链法,即hash值一样的就形成一条链,HashMap的key是不能重复的,因为需要进行查找,如果key不唯一,就不用说查找了,
HashMap的jdk1.8版本以上用了新的办法,当拉链个数大于8个时候采用红黑树,这样可以提高查找效率,重新hash之后,如果链数小于6,又会
重新转为链表结构。因此出现Hash冲突,性能是必然要打折扣的
产生链的原因是hash冲突,hashmap在解决hash冲突的时候采用的是拉链发,hashmap在进行put的时候,是先计算key的hash值,然后再把在根据
数组的长度取模,然后找到该位置上时候存在数据(hash冲突),不存在就直接插入,如果存在就继续沿着链表向下搜索,并使用hashcode和equal
方法进行比较,相同就直接进行替换,不同找到最后一个位置进行链表尾插(jdk1.8之前是头插,jdk1.8是尾插)

对于HashMap及其子类而言,它们采用Hash算法来决定集合中元素的存储位置。当系统开始初始化HashMap时,系统就会创建一个长度为capacity的Entry数组,这个数组里可以存储元素的位置被称为“桶(bucket)”每个bucket都有其指定索引,系统可以根据其索引快速访问该bucket里存储的元素
无论何时,HashMap的每“桶”只存储一个元素。由于Entry对象可以包含一个引用变量(就是Entry构造器的最后一个参数)用于指向下一个Entry,因此可能出现:HashMap的bucket中只有一个Entry但这个Entry指向另一个Entry——这就形成了Entry链
当HashMap的每个bucket里存储的Entry都是单个的Entry,即没有通过指针产生Entry链时,此时的HashMap有最好的性能,系统只要先计算出key的HashCode()返回值再根据该hashCode返回值找出该key在table数组中的索引,然后取出该索引处的Entry,最后返回该key对应的value
当产生Hash冲突即产生Entry链的情况下,系统只能按顺序遍历,直到找到想搜索的Entry为止

当创建HashMap时有一个默认的负载因子(load factor)其默认值为0.75.这是时间和空间成本的一种折衷;增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作,减小负载因子会提高数据查询的性能,但会降低Hash表所占用的内存空间

2)HashSet
HashSet的实现其实非常简单,它只是封装了一个HashMap对象来存储所有集合元素。所有放入HashSet中的集合元素实际上都由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象

3.1.3TreeMap和TreeSet

TreeSet的底层依然是依赖TreeMap实现
对于TreeMap而言,它采用一种被称为“红黑树”的排序二叉树来存储Entry——每一个Entry都被当做二叉树的节点来对待
以后每向TreeMap中放入一个key-value对,系统都需要将该Entry当成一个新节点,添加到已有红黑树中,通过这种方式就可保证TreeMap总是由小到大排列的有序状态

TreeMap比HashMap在查取效率上要低但优势在于总是保持指定排序的有序状态

3.2Map和List

3.2.1Map的values()方法

Map集合是一个关联数组,它包含两组值:一组是所有key组成的集合,因为Map集合的key是不允许重复的,而且Map不会保存key加入的顺序,因此这些key可以组成一个Set集合;另外一组是value组成的集合,因为Map集合的value完全可以重复,而且Map可以根据Key来获取对应的Value,所以这些value可以组成一个List集合
不管是HashMap,TreeMap,它们的values()方法都可返回其所有Value组成的Collection集合——按照通常理解,这个Collection集合应该是一个List集合因为Map的多个Value允许重复
但实际上,HashMap,TreeMap的value()方法实现要更巧妙。这两个Map对象values()方法返回的是一个不存储元素的Collection集合,当程序遍历Collection集合时,实际上就是遍历Map对象的value
HashMap和TreeMap的values()方法并未把Map中的value重新组合成一个包含元素的集合对象,这样就可以降低系统的开销

3.2.2Map和List的关系

Map和List底层实现并没有太大相似之处,只是在用法上存在一些相似之处:既可以说List相当于所有key都是int类型的Map,也可以说Map相当于索引是任意类型的List

3.3ArrayList和LinkedList

在List集合实现类中,主要有3个实现类:ArrayList,Vector,LinkedList

3.3.1Vector和ArrayList的区别

由于Vector包含的方法比ArrayList更多,因此Vector类的源代码比ArrayList的源代码要多,而且ArrayList的序列化实现比Vector的序列化实现更安全,因此Vector基本上已经被ArrayList所代替了。Vector唯一的好处是它是线程安全的。
注:即使需要在多线程的环境下使用List集合,而且需要保证List集合的线程安全,依然可以避免使用Vector,而是考虑将ArrayList包装成线程安全的集合类,Java提供了一个Collections工具类,通过该工具类synchronizedList方法即可以将一个普通ArrayList包装成线程安全的ArrayList

3.3.2ArrayList和LinkedList的性能分析和适用场景

ArrayList和LinkedList
我们先说性能方面;
1.插入:
ArrayList是单向链表,底层是数组存储形式,在添加的时候,如果添加在ArrayList尾部,则性能更快于LinkedList,但是在List中添加完元素之后,导致超过底层数组的长度,就会垃圾回收原来的数组,并且用System.copyArray赋值到新的数组当中,这开销就会变大,而LikedList在插入时候,明显高于ArrayList,因为LinkedList是双向链表,但是在如果index(要插入的位置==size)则就不会进行entry(index)查找,如果不等于则就会进行查找操作,这个也是LinkedList开销比较大得地方,即使这样性能也明显高于ArrayList;
2.删除:
ArrayList 整体的会向前移动一格,然后再要删除的index位置置空操作,ArrayList的remove要比add的时候更快,因为不用再复制到新的数组当中了
LikedList 的remove操作相对于ArrayList remove更快。
3. 使用与场景
但是在大部分情况下ArrayList性能不行优于LinkedList,但是如果是经常进行插入,删除操作则就会使用LinkedList会好很多。

3.4Iterator迭代器

迭代器(Iterator)是一个对象,它的工作是遍历并选择序列中的对象,它提供了一种访问一个容器(container)对象中的各个元素,而又不必暴露该对象内部细节的方法。通过迭代器,开发人员不需要了解容器底层的结构,就可以实现对容器的遍历。由于创建迭代器的代价小,因此迭代器通常被称为轻量级的容器。
迭代器的使用主要有以下三个方面的注意事项:
1)使用容器的iterator()方法返回一个Iterator,然后通过Iterator的next()方法返回第一个元素。
2)使用Iterator()的hasNext()方法判断容器中是否还有元素,如果有,可以使用next()方法获取下一个元素。
3)可以通过remove()方法删除迭代器返回的元素。
Iterator支持派生的兄弟成员。ListIterator只存在于List中,支持在迭代期间向List中添加或删除元素,并且可以在List中双向滚动。
Iterator的使用方法如下例所示:

package com.js;
/**
 * Iterator使用Demo
 */
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
 
public class Test {
	public void main(String[] args){
		List<String> ll = new LinkedList<String>();
		ll.add("first");
		ll.add("second");
		ll.add("third");
		ll.add("fourth");
		for(Iterator<String> iterator = ll.iterator();iterator.hasNext();){
			String string = (String)iterator.next();
			System.out.println(string);
		}
	}
}

运行结果为:

first
second
third
fourth

使用iterator()方法时经常会遇到ConcurrentModificationException异常,这通常是由于在使用Iteraor遍历容器的同时又对容器做增加或删除操作所导致的,或者由于多线程操作导致,当一个线程使用迭代器遍历容器的同时,另外一个线程对这个容器进行增加或删除操作。下列主要介绍单线程抛出ConcurrentModificationException的情况:

package com.js;
/**
 * Iterator使用Demo
 */
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
 
public class Test {
	public void main(String[] args){
		List<String> ll = new LinkedList<String>();
		ll.add("first");
		ll.add("second");
		ll.add("third");
		ll.add("fourth");
		for(Iterator<String> iterator = ll.iterator();iterator.hasNext();){
			String string = (String)iterator.next();
			System.out.println(string);
			if(string.equals("second"))
				ll.add("fifth");
		}
	}
}
  • Iterator使用Demo

运行结果为:

first
second
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.LinkedList$ListItr.checkForComodification(Unknown Source)
at java.util.LinkedList$ListItr.next(Unknown Source)
at com.js.Test.main(Test.java:17)

抛出上述异常的主要原因是当条用容器的iterator()方法返回Iterator对象时,把容器中包含对象的个数赋值给了一个变量expectedModCount,在调用next()方法时会比较变量expectedModCount与容器中实际对象的个数modCount的值是否相等,若二者不相等,则会抛出ConcurrentModificationException异常,因此在使用Iterator遍历容器的过程中,如果对容器进行增加或删除操作,就会改变容器中对象的数量,从而导致抛出异常。解决办法如下:在遍历的过程中把需要删除的对象保存到一个集合中,等遍历结束后再调用removeAll()方法来删除,或者使用iterator.remove()方法。