关于集合与HashMap

     我们最开始接触编程的时候第一次使用的数据结构应该都是数组,数组是一块连续的内存的统称,如果拆开来看,那么就是一个一个的变量。数组的内存是静态分配的,也就是给一个数组开辟一块内存之后不能改变其大小。其实数组是变量(数据)和数字(索引)的关联,连续的内存决定数组的查找迅速,但对于删除、添加等操作则上十分麻烦。在此基础上,我们接触了java中的容器类:Map、CollectionIterator等接口,对于java的核心结合框架:Set、List、Map,简单的归纳如下:

List
LinkedList:对顺序访问做了优化,可以提供队列、双向队列、栈的功能
ArrayKList:一个用数组实现的List。能进行快速的随机访问,但是往列表中间插入和删除元素的时候比较慢
Set
LinkedHashSet在内部用散列来提高查询速度,但是它看上去像是用链表来保存元素的插入顺序的
HashSet:为优化查询速度而设计的Set,采用的数据结构是“专为快速查找而设计”的散列函数;
TreeSet:是一个有序的Set,其底层是一颗树。这样你就能从Set里面提取一个有序序列了,采用的是红黑树的数据结构为元素排序;
LinkedHashSet(JDK 1.4):一个在内部使用链表的Set,既有HashSet的查询速度,又能保存元素被加进去的顺序(插入顺序),LinkedHashSet在内部用散列来提高查询速度;
HashSet提供了最快的查询速度。而TreeSet则保持元素有序。LinkedHashSet保持元素的插入顺序。
Map
HashMap:采用哈希码算法,以便快速查找一个键值
关于哈希码算法:
 1:Object类的hashCode.返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。
 2:String类的hashCode.根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串内容相同,返回的哈希码也相同。
 3:Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值
TreeMap:用树的数据结构对键值进行了排序

其实弄懂了每个类型的数据结构的内部数据保存方式,那么对它的操作也就知道了,例如用数组就知道查找快而删除添加缓慢,用链表就知道删除添加迅速而查找不如数组那么快,当然,处理数组和链表之外,我们还有一种数据结构-------Map,键值对存储数据。让我们先看一下HashSet的构造器

public HashSet(int initialCapacity) { 
    map = new HashMap<E,Object>(initialCapacity); } 
public HashSet(int initialCapacity) {
    map = new HashMap<E,Object>(initialCapacity); }
 public HashSet(int initialCapacity) {
     map = new HashMap<E,Object>(initialCapacity); } 
public HashSet() { 
      map = new HashMap<E,Object>(); } 
 

 

 太明显了吧,HashSet完全是建立在HashMap之上的,那么,HashMap究竟是怎么实现的呢?

HashMap继承了AbstractMap类,实现了Map、Cloneable、Serializable接口,在HashmMap里面定义了下面的属性:

 

static final int DEFAULT_INITIAL_CAPACITY = 16;
 static final int MAXIMUM_CAPACITY = 1 << 30; 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 transient Entry[] table;
 transient int size; 
final float loadFactor; 
transient volatile int modCount; 

 其中默认的大小为16,最大的容量为1<<30,装载因子为0.75f,Entry[]是保存Key对象经过hash函数计算之后得到的索引 。

 

 

 

再来看看构造器:

 //构造器1
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
     	
    }
//构造器2
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    	    }
//构造器3
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
            }
//构造器4
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
           }
  

 在用构造器1的时候,由于

while (capacity < initialCapacity) capacity <<= 1;


 所以不管我们输入的创建Map的大小是多少,HashMap创建的Entry[]将是刚刚大于我们传入的数的2的整数次幂,这样有什么好处?我们再看一下HashMap的关于确定一个Entry保存位置的函数

 

int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); static int indexFor(int h, int length) { return h & (length-1); }


 i就是计算得出来的索引位置,也就是该对象的Hash码与表的大小-1相与的结果,刚开始对length-1很不理解,后来才知道这样是为了提供HashMap的效率。因为length是2的整数次幂,那么length的二进制表示就是:10000000......(指数个0),那么length-1就是1111......(指数个1),任何数与length-1相与之后仍然是原来的数,这样就能保证分布均匀,尽量减少了哈希冲突。

 

 

 构造器2建立在构造器1的基础上,构造器3采用默认的参数,构造器4在一个已经建立好的HashMap上新建一个HashMap。

当我们创建好HashMap对象之后,肯定是要往里面保存东西的,那么HashMap是怎么做的呢?关于添加的方法,HashMap如下:

public V put(K key, V value) {
 if (key == null) 
return putForNullKey(value);
 int hash = hash(key.hashCode());
 int i = indexFor(hash, table.length); 
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 Object k;
 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
V oldValue = e.value; 
e.value = value;
 e.recordAccess(this);
 return oldValue; }
 }
 modCount++;
 addEntry(hash, key, value, i);
 return null; 
} 
private V putForNullKey(V value) { 
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); 
return oldValue;
 } 
} modCount++;
 addEntry(0, null, value, 0);
 return null;
 } 

void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } 

 

 

 HashMap可以保存key为null时候的value,方法putForNullKey正是处理这个的。经过计算key的索引之后新建一个Entry对象保存key和value值,然后保存到table[i]的位置,如果哈希冲突了,就替换并返回旧值。但是当我们一直往一个HashMap里添加数据的时候,总会超过创建时候设置的大小,这个时候如何处理?当然是rehash了。在方法addEntry里面有这样一句

if (size++ >= threshold) resize(2 * table.length); //resize方法如下 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }


 首先,  threshold = (int)(capacity * loadFactor)也就是允许保存的最大容量是表的大小乘以装载因子,当存储的数据个数超过这个数的时候,表的大小就扩充为原来的二倍

 

 

 

 

 然后把原来的数据取出重新计算哈希值并保存。

保存完数据之后如何让取出?且看get方法

public V get(Object key) {
 if (key == null) 
return getForNullKey(); 
int hash = hash(key.hashCode());
 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 
Object k; 
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
return e.value;
 } 
return null;
 } 

 

 key为null的时候单独处理,不为null的时候计算hash值,如果在表的hash值位置已经挂链,那么就往这条链下面继续走下去一直到找的相同的key返回value值。

那么HashMap又是怎么删除数据的呢?

 

 

 

 

 

final Entry<K,V> removeEntryForKey(Object key) {
 int hash = (key == null) ? 0 : hash(key.hashCode());
 int i = indexFor(hash, table.length);
 Entry<K,V> prev = table[i]; 
Entry<K,V> e = prev; 
while (e != null) {
 Entry<K,V> next = e.next;
 Object k; 
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { 
modCount++; 
size--;
 if (prev == e) 
table[i] = next; 
else 
prev.next = next; 
e.recordRemoval(this); 
return e; }
 prev = e; 
e = next; 
} 
return e;
}
 

 当然,对于删除链表上的节点这属于链表的操作了,如果删除的是表上面的数据,就把下面剩余的挂链挂在表的空出来的位置的上。对于其他的方法clone、clear之类的就很简单了,put和get方法才是关键。

对于系统提供的HashMap虽然理解了主要的流程和数据结构,但在细微方面仍然有很多不理解:

1,hash()函数

static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }


 这样的算法为什么能最大限度的减少哈希冲突?

2.扩充原表格的大小是解决容量的最好办法?

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值