Java集合类中的哈希总结

时间:2021-04-05 17:00:53

JAVA集合类中的哈希总结

 目 录

  1、哈希表

  2、Hashtable、HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap区别

  3、Hashtable、HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap源码分析

  4、一致性哈希算法

  5、transient使用方法

  6、迭代器的强一致和弱一致

  7、总结

一、哈希表

  哈希表,是一种数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

  常用的散列函数方法有:取余数法、平方取中法、线性函数法、随机数法等。常见的解决冲突的方法有:链地址法、开发定址法、建立公共溢出区、多哈希函数法。

  Java中的哈希表,即类Hashtable。它的散列方法采用了除留取余数法;解决冲突的方法采用了链地址法。链地址法使用于频繁的插入和删除的操作类型。

二、Hashtable、HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap区别

  Hashtable是一个包含单向链表的二维数组,其数据结构的数组中是Entry<K,V>存储,entry对象。Hashtable有洁癖,不允许存入其中的key或者value为null。Hashtable是线程安全的,所有的方法均用synchronized修饰,这样在任一时刻,只有一个线程可以写Hashtable,因此,对于频繁写操作的业务逻辑,诸如写excel表等时候,速度会非常慢。

  HashMap是最常用的Map型数据结构,它根据键的hashCode()值存储数据。HashMap允许一个key为null,允许多个value为空,HashMap不支持线程的同步,即可能会出现在同一时刻有多个线程同时写HashMap,会产生数据的不一致。如果在修改代码的过程中,需要给HashMap限制为线程同步的,可以采用Collections.synchronizedMap(map);方法使得HashMap可以同步。

  ConcurrentHashMap是基于这样的考虑:降低锁的粒度。在Hashtable中的关键字是使用synchronized基于整张表结构的,锁的粒度太大,它每次通过锁住整张表让线程独占,来保证安全性。

  LinkedHashMap保存了记录的插入顺序,在使用Iterator遍历LinkedHashMap的时候,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢,因为HashMap是以O(1)来设计存取的。并且LinkedHashMap继承自HashMap,拥有它的全部特性。

  TreeMap是基于红黑树实现的,它是一种有序的存储结构,并且程序员可以自己定义排序器。TreeMap默认会按存入的键值key来排序,默认是按升序排序,当然也可以指定排序的比较器。TreeMap同样有洁癖,不允许存入null值。使用Iterator遍历出来的TreeMap往往是有序的。

  总结:常用HashMap,允许null插入;有两个子类:ConcurrentHashMap和LinkedHashMap。前者用来弥补线程安全,后者用来弥补有序。此外还有Hashtable和TreeMap。虽然CouncurrentHashMap性能明显优于Hashtable,但是并不能完全取代Hashtable,因为遍历ConcurrentHashMap的迭代器是弱一致的。TreeMap数据结构则可以帮助我们得到一个有序的结果,适用于需要输出排序结果的场景。

三、Hashtable、HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap源码分析

  Hashtable源码如下:

Java集合类中的哈希总结Java集合类中的哈希总结
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

    /**
     * The hash table data.
     */
    private transient Entry<K,V>[] table;

    /**
     * The total number of entries in the hash table.
     */
    private transient int count;

    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

       /**
     * Constructs a new, empty hashtable with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hashtable.
     * @param      loadFactor        the load factor of the hashtable.
     * @exception  IllegalArgumentException  if the initial capacity is less
     *             than zero, or if the load factor is nonpositive.
     */
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        initHashSeedAsNeeded(initialCapacity);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     * @exception IllegalArgumentException if the initial capacity is less
     *              than zero.
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

    /**
     * Returns the number of keys in this hashtable.
     *
     * @return  the number of keys in this hashtable.
     */
    public synchronized int size() {
        return count;
    }

    /**
     * Tests if this hashtable maps no keys to values.
     *
     * @return  <code>true</code> if this hashtable maps no keys to values;
     *          <code>false</code> otherwise.
     */
    public synchronized boolean isEmpty() {
        return count == 0;
    }
Hashtable部分源码

  HashMap等源码不一一列举。

四、一致性哈希算法

一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

五、transient使用方法

  在Java中一个对象只要实现了Serilizable接口,这个对象就可以被序列化。java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。在开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。示例代码如下:

Java集合类中的哈希总结Java集合类中的哈希总结
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class TransientTest {

    public static void main(String[] args) {

        User user = new User();
        user.setUsername("Alexia");
        user.setPasswd("123456");

        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());

        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("C:/user.txt"));
            os.writeObject(user); // 将User对象写进文件
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("C:/user.txt"));
            user = (User) is.readObject(); // 从流中读取User的数据
            is.close();

            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;

    private String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

}
TransientTest测试类示范

  java的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

六、迭代器的强一致和弱一致

  fail-fast机制,是Java集合中的一种错误机制。当多个线程对同一个集合中进行操作时,就可能会产生fail-fast事件。当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。

   java.util 包中的集合类都返回 fail-fast 迭代器,这意味着它们假设线程在集合内容中进行迭代时,集合不会更改它的内容。如果 fail-fast 迭代器检测到在迭代过程中进行了更改操作,那么它会抛出 ConcurrentModificationException,这是不可控异常。

七、总结

  ConcurrentHashMap是一个线程安全的Map结合,它采用锁分离技术,通过多个锁代替Hashtable中的单个锁(这么说,JDK中先有的hashtable,然后又的ConcurrentHashMap)。ConcurrentHashMap使用了ReentrantLock锁,而不是Sychronized锁。ConcurrentHashMap中的get、put、remove三个方法保证了数据同步,但是没有使用锁。

详细请参考:http://ifeve.com/java-concurrent-hashmap-2/

  具体使用细节,示例代码如下:

 

Java集合类中的哈希总结Java集合类中的哈希总结
import java.util.HashMap;

public class HashTableTest {
    public static void main(String args[]) {
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("weight", "85.4KG");
        map.put("height", "180cm");
        boolean isexists = map.containsKey("weight");
        for (String str : map.keySet()) {
            if (isexists) {
                System.err.println("name:" + str + ", value:" + map.get(str));
            }
        }
    }
}
keySet()使用