java进阶笔记之常用(通用)Map(Hash,Tree,Linked,Properties等)

简介

Map是将键映射到值( key-value )的对象。
一个映射不能包含重复的键;每个键最多只能映射到一个值。
Map 接口提供三种collection 视图,允许以键集(keySet())、值集(values())或
键-值映射关系集(entrySet())的形式查看某个映射的内容( 即获取键值对的内容 )。

映射顺序定义为迭代器在映射的 collection 视图上返回其元素的顺序,
即可以映射得到键、值和键-值的Set集合,元素的顺序是由得到的Set集合所决定的。
某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类 。

简单来说Map中的有序指的是:存入键值对的顺序和顺序遍历得到的元素的顺序相同;
而无序则是不一定存入和遍历的顺序相同。

Map的层次如下:

这里写图片描述

接口和抽象类说明

  • Map
    Map是一个接口,Map中存储的内容是键值对(key-value)。

  • AbstractMap
    实现一个用于帮助实现您自己的 Map 类的抽象类

  • SortedMap
    SortedMap也是一个接口,它继承与Map接口。
    SortedMap中的内容与Map中的区别在于,它是有序的键值对,里面排序的方法是通过比较器(Comparator)实现的。

  • NavigableMap
    NavigableMap也是一个接口,它继承与SortedMap接口,所以它肯定也是有序的,
    另外,NavigableMap还有一些导航的方法:如获取“大于或等于某个对象的键值对”等等。

  • Enumeration
    Enumeration接口中定义了一些方法,通过这些方法可以枚举(一次获得一个)对象集合中的元素。
    这种传统接口已被迭代器取代,虽然Enumeration 还未被遗弃,但在现代代码中已经被很少使用了。
    尽管如此,它还是使用在诸如Vector和Properties这些传统类所定义的方法中.
    一些Enumeration声明的方法:

方法名称说明
boolean hasMoreElements( )测试此枚举是否包含更多的元素
Object nextElement( )如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素。
//枚举遍历示例
public static void main(String args[]) {
      Enumeration<String> days;
      Vector<String> dayNames = new Vector<String>();
      dayNames.add("Sunday");
      dayNames.add("Monday");
      dayNames.add("Tuesday");
      dayNames.add("Wednesday");
      dayNames.add("Thursday");
      dayNames.add("Friday");
      dayNames.add("Saturday");
      days = dayNames.elements();
      while (days.hasMoreElements()){
         System.out.println(days.nextElement());
      }
   }
  • Dictionary
    Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。
    给出键和值,你就可以将值存储在Dictionary对象中。
    一旦该值被存储,就可以通过它的键来获取它。所以和Map一样, Dictionary 也可以作为一个键/值对列表。
    Dictionary定义的抽象方法如下表所示:
序号方法描述
1Enumeration elements( )返回此 dictionary 中值的枚举。
2Object get(Object key)返回此 dictionary 中该键所映射到的值。
3boolean isEmpty( )测试此 dictionary 是否不存在从键到值的映射。
4Enumeration keys( )返回此 dictionary 中的键的枚举。
5Object put(Object key, Object value)将指定 key 映射到此 dictionary 中指定 value。
6Object remove(Object key)从此 dictionary 中移除 key (及其相应的 value)。
7int size( )返回此 dictionary 中条目(不同键)的数量。

Dictionary类已经过时了,Map接口可以理解为是Dictionary的替代者。

常见的Map和原理

通用Map

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

HashMap

HashMap是基于哈希表的Map接口实现,为线程不安全,且是无序的。
HashMap在java7以前都是用一个Entry数组,数组中是Entry链表的头节点的方式存储数据。
HashMap在java7以及之后才用“数组”+链表+红黑树的方式实现,这里说下java7以前的实现。

  • 底层实现原理
1.其内部有一个叫做table的的Entry数组。Entry是其一个静态内部类,存储键值对。
而table数组的大小默认是16,可以初始化指定大小。Entry的数据结构是一个链表。
Entry 的定义:




static class Entry implements Map.Entry
{
        final K key;
        V value;
        Entry next;
        final int hash;
}  

2.每当往hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,
然后根据key的hashcode()方法计算出来的hash值(来决定)来
计算出此key在Entry数组的索引,然后根据索引将第键值对加入到此索引下的Entry的尾部,
如果找到key相同的(equals相等),则替换其Entry中的value的值。
所以HashMap中的值是不能够重复的。

3.对key做null检查。如果key是null,会被存储到table[0],所以null的hash值总是0。

4.table的索引在逻辑上叫做“桶”(bucket),这个索引位置的值存储了链表的第一个元素。

5.key的hashcode()方法用来找到Entry对象所在的桶。

6.如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。

7.key的equals()方法用来确保key的唯一性。

8.value对象的equals()和hashcode()在hashMap中没有用到。

  • 源码查看
    构造方法:
// 指定“初始容量大小”和“加载因子”的构造函数
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);
        // Find a power of 2 >= initialCapacity
        int capacity = 1; 
        //当capacity 的值小于initialCapacity时,循环,用位移运算将capacity 
       //的值扩大至原来的2倍,直到capacity 不小于initialCapacity。
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity]; //这里建立Entry的数组来存储Entry,而Entry中存储具体的键值对
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
}

put方法:

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     * <tt>null</tt> if there was no mapping for <tt>key</tt>.
     * (A <tt>null</tt> return can also indicate that the map
     * previously associated <tt>null</tt> with <tt>key</tt>.)
     */
public V put(K key, V value) {
        //其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
        if (key == null)
            return putForNullKey(value);
        //通过调用hash方法对key进行哈希,得到哈希之后的数值,其目的是为了尽可能的让键值对可以分不到不同的桶中 
        int hash = hash(key);
        //根据上一步骤中求出的hash得到在数组中是索引
        int i = indexFor(hash, table.length);
        //如果i处的Entry不为null,则通过其next指针不断遍历e元素的下一个元素。
        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;
}

addEntry方法:

/**
     * 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.
     *
     * 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);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
        // 获取指定 bucketIndex 索引处的 Entry
        Entry<K,V> e = table[bucketIndex];
        // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entr
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

hash方法:
hash(int h)方法根据 key 的 hashCode 重新计算一次散列。
此算法加入了高位计算;防止低位不变,高位变化时,造成的 hash 冲突。

final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
        //得到k的hashcode值
        h ^= k.hashCode();
        //进行计算
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

indexFor方法:
indexFor(int h, int length)用来计算该对象应该保存在 table 数组的哪个索引处。

/**
     * Returns index for hash code h.
     */
static int indexFor(int h, int length) {
    return h & (length-1);
}

它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap 底层数组的长度总是 2 的 n 次方,
这是 HashMap 在速度上的优化。在 HashMap 构造器中有如下代码:

// Find a power of 2 >= initialCapacity
int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

这段代码保证初始化时 HashMap 的容量总是 2 的 n 次方,即底层数组的长度总是为 2 的 n 次方。
当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。

get方法:

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}. (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }

resize(rehash)方法:
HashMap 的 当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高,因为数组的长度是固定的。
所以为了提高查询的效率,就要对 HashMap 的数组进行扩容;
而在 HashMap 数组扩容是最消耗性能的,因为原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。

当 HashMap 中的元素个数超过数组大小 * loadFactor时,就会进行数组扩容,
loadFactor的默认值为 0.75,也就是说,默认情况下,数组大小为 16,
那么当 HashMap 中元素个数超过 16 * 0.75=12 的时候,就把数组的大小扩展为 2*16=32,
即扩大一倍,然后重新计算每个元素在数组中的位置;
所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。

注意:

HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。
因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

Hashtable

Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口
Hashtable 的函数都是同步的,这意味着它是线程安全的
它的key、value都不可以为null,Hashtable中的映射不是有序的 。
其构造函数同HashMap:

//通过初始容量和加载因子(默认0.75)
public Hashtable(int initialCapacity, float loadFactor)

HashTable的实现大致与HashMap相同,区别如下:

1.HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,
因为contains方法容易让人引起误解。
Hashtable则保留了contains,containsValue和containsKey三个方法,
其中contains和containsValue功能相同。

2.HashTable是线程安全的,而HashMap不是。

3.HashTable的键和值都不能是Null,而HashMap得键和值都可以是null。

4.HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。
所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,
但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

5.Hashtable中hash数组默认大小是11,增加的方式是 old*2+1。
HashMap中hash数组的默认大小是16,而且扩容后容量一定是2的指数。

6.HashTbale是用的hashcode进行 模运算(%取余的方式),

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap是强制容量为2的幂,重新根据hashcode计算hash值,
在使用hash 位与 (hash表长度 – 1),也等价取模,
HashMap更加高效,取得的位置更加分散,偶数,奇数保证了都会分散到。

Properties

Properties继承自HashTable,是线程安全的,其键值不能是Null。。
Properties支持文本方式和xml方式的数据存储读取。
在文本方式中,格式为key:value,其中分隔符可以是:冒号(:)、等号(=)、空格。
其中空格可以作为key的结束,同时获取的值会将分割符号两端的空格去掉。

Properties类常用来读取纯文本配置文件,后缀名一般为(.properties),
且其只支持编码是ASCII的内容(所以需要转码,或者用其余的配置读取方式)。
load(InputStream):从byte stream中加载key/value键值对,
要求所有的key/value键值对是按行存储,同时是用ISO-8859-1编译的。
文件的内容格式为“key=value”,文本注释可以使用”#”或者感叹号”!”来注释。
配置文件示例:

#配置文件示例
key=test
name=测试内容
  • 初始化
    Properties提供两种方式来创建Properties对象,第一种是不指定默认values对象的创建方法,
    另外一种是指定默认values对象的创建方法。
    但是此时是没有加载属性值的,加载key/value属性必须通过专门的方法来加载。
/**
     * Creates an empty property list with no default values.
     */
    public Properties() {
        this(null);
    }
    /**
     * Creates an empty property list with the specified defaults.
     *
     * @param defaults the defaults.
     */
    public Properties(Properties defaults) {
        this.defaults = defaults;
    }

load方法:

/**
     * Reads a property list (key and element pairs) from the input
     * byte stream. The input stream is in a simple line-oriented
     * format as specified in
     * {@link #load(java.io.Reader) load(Reader)} and is assumed to use
     * the ISO 8859-1 character encoding; that is each byte is one Latin1
     * character. Characters not in Latin1, and certain special characters,
     * are represented in keys and elements using Unicode escapes as defined in
     * section 3.3 of
     * <cite>The Java&trade; Language Specification</cite>.
     * <p>
     * The specified stream remains open after this method returns.
     *
     * @param inStream the input stream.
     * @exception IOException if an error occurred when reading from the
     * input stream.
     * @throws IllegalArgumentException if the input stream contains a
     * malformed Unicode escape sequence.
     * @since 1.2
     */
    public synchronized void load(InputStream inStream) throws IOException {
        load0(new LineReader(inStream));
    }

LineReader :

class LineReader {
        /**
         * 根据字节流创建LineReader对象
         *
         * @param inStream
         * 属性键值对对应的字节流对象
         */
        public LineReader(InputStream inStream) {
            this.inStream = inStream;
            inByteBuf = new byte[8192];
        }
        /**
         * 根据字符流创建LineReader对象
         *
         * @param reader
         * 属性键值对对应的字符流对象
         */
        public LineReader(Reader reader) {
            this.reader = reader;
            inCharBuf = new char[8192];
        }
        // 字节流缓冲区, 大小为8192个字节
        byte[] inByteBuf;
        // 字符流缓冲区,大小为8192个字符
        char[] inCharBuf;
        // 当前行信息的缓冲区,大小为1024个字符
        char[] lineBuf = new char[1024];
        // 读取一行数据时候的实际读取大小
        int inLimit = 0;
        // 读取的时候指向当前字符位置
        int inOff = 0;
        // 字节流对象
        InputStream inStream;
        // 字符流对象
        Reader reader;
        /**
         * 读取一行,将行信息保存到{@link lineBuf}对象中,并返回实际的字符个数
         *
         * @return 实际读取的字符个数
         * @throws IOException
         */
        int readLine() throws IOException {
            // 总的字符长度
            int len = 0;
            // 当前字符
            char c = 0;
            boolean skipWhiteSpace = true;
            boolean isCommentLine = false;
            boolean isNewLine = true;
            boolean appendedLineBegin = false;
            boolean precedingBackslash = false;
            boolean skipLF = false;
            while (true) {
                if (inOff >= inLimit) {
                    // 读取一行数据,并返回这一行的实际读取大小
                    inLimit = (inStream == null) ? reader.read(inCharBuf) : inStream.read(inByteBuf);
                    inOff = 0;
                    // 如果没有读取到数据,那么就直接结束读取操作
                    if (inLimit <= 0) {
                        // 如果当前长度为0或者是改行是注释,那么就返回-1。否则返回len的值。
                        if (len == 0 || isCommentLine) {
                            return -1;
                        }
                        return len;
                    }
                }
                // 判断是根据字符流还是字节流读取当前字符
                if (inStream != null) {
                    // The line below is equivalent to calling a ISO8859-1 decoder.
                    // 字节流是根据ISO8859-1进行编码的,所以在这里进行解码操作。
                    c = (char) (0xff & inByteBuf[inOff++]);
                } else {
                    c = inCharBuf[inOff++];
                }
                // 如果前一个字符是换行符号,那么判断当前字符是否也是换行符号
                if (skipLF) {
                    skipLF = false;
                    if (c == '\n') {
                        continue;
                    }
                }
                // 如果前一个字符是空格,那么判断当前字符是不是空格类字符
                if (skipWhiteSpace) {
                    if (c == ' ' || c == '\t' || c == '\f') {
                        continue;
                    }
                    if (!appendedLineBegin && (c == '\r' || c == '\n')) {
                        continue;
                    }
                    skipWhiteSpace = false;
                    appendedLineBegin = false;
                }
                // 如果当前新的一行,那么进入该if判断中
                if (isNewLine) {
                    isNewLine = false;
                    // 如果当前字符是#或者是!,那么表示该行是一个注释行
                    if (c == '#' || c == '!') {
                        isCommentLine = true;
                        continue;
                    }
                }
                // 根据当前字符是不是换行符号进行判断操作
                if (c != '\n' && c != '\r') {
                    // 当前字符不是换行符号
                    lineBuf[len++] = c;// 将当前字符写入到行信息缓冲区中,并将len自增加1.
                    // 如果len的长度大于行信息缓冲区的大小,那么对lineBuf进行扩容,扩容大小为原来的两倍,最大为Integer.MAX_VALUE
                    if (len == lineBuf.length) {
                        int newLength = lineBuf.length * 2;
                        if (newLength < 0) {
                            newLength = Integer.MAX_VALUE;
                        }
                        char[] buf = new char[newLength];
                        System.arraycopy(lineBuf, 0, buf, 0, lineBuf.length);
                        lineBuf = buf;
                    }
                    // 是否是转义字符
                    // flip the preceding backslash flag
                    if (c == '\\') {
                        precedingBackslash = !precedingBackslash;
                    } else {
                        precedingBackslash = false;
                    }
                } else {
                    // reached EOL
                    if (isCommentLine || len == 0) {
                        // 如果这一行是注释行,或者是当前长度为0,那么进行clean操作。
                        isCommentLine = false;
                        isNewLine = true;
                        skipWhiteSpace = true;
                        len = 0;
                        continue;
                    }
                    // 如果已经没有数据了,就重新读取
                    if (inOff >= inLimit) {
                        inLimit = (inStream == null) ? reader.read(inCharBuf) : inStream.read(inByteBuf);
                        inOff = 0;
                        if (inLimit <= 0) {
                            return len;
                        }
                    }
                    // 查看是否是转义字符
                    if (precedingBackslash) {
                        // 如果是,那么表示是另起一行,进行属性的定义,len要自减少1.
                        len -= 1;
                        // skip the leading whitespace characters in following line
                        skipWhiteSpace = true;
                        appendedLineBegin = true;
                        precedingBackslash = false;
                        if (c == '\r') {
                            skipLF = true;
                        }
                    } else {
                        return len;
                    }
                }
            }
        }
    }

我们可以看出一些特征:readLine这个方法每次读取一行数据;如果我们想在多行写数据,那么可以使用’\’来进行转义,在该转义符号后面换行,是被允许的。

load0方法:

private void load0(LineReader lr) throws IOException {
        char[] convtBuf = new char[1024];
        // 读取的字符总数
        int limit;
        // 当前key所在位置
        int keyLen;
        // value的起始位置
        int valueStart;
        // 当前字符
        char c;
        //
        boolean hasSep;
        // 是否是转义字符
        boolean precedingBackslash;
        while ((limit = lr.readLine()) >= 0) {
            c = 0;
            // key的长度
            keyLen = 0;
            // value的起始位置默认为limit
            valueStart = limit;
            //
            hasSep = false;
            precedingBackslash = false;
            // 如果key的长度小于总的字符长度,那么就进入循环
            while (keyLen < limit) {
                // 获取当前字符
                c = lr.lineBuf[keyLen];
                // 如果当前字符是=或者是:,而且前一个字符不是转义字符,那么就表示key的描述已经结束
                if ((c == '=' || c == ':') && !precedingBackslash) {
                    // 指定value的起始位置为当前keyLen的下一个位置
                    valueStart = keyLen + 1;
                    // 并且指定,去除空格
                    hasSep = true;
                    break;
                } else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) {
                    // 如果当前字符是空格类字符,而且前一个字符不是转义字符,那么表示key的描述已经结束
                    // 指定value的起始位置为当前位置的下一个位置
                    valueStart = keyLen + 1;
                    break;
                }
                // 如果当前字符为'\',那么跟新是否是转义号。
                if (c == '\\') {
                    precedingBackslash = !precedingBackslash;
                } else {
                    precedingBackslash = false;
                }
                keyLen++;
            }
            // 如果value的起始位置小于总的字符长度,那么就进入该循环
            while (valueStart < limit) {
                // 获取当前字符
                c = lr.lineBuf[valueStart];
                // 判断当前字符是否是空格类字符,达到去空格的效果
                if (c != ' ' && c != '\t' && c != '\f') {
                    // 当前字符不是空格类字符,而且当前字符为=或者是:,并在此之前没有出现过=或者:字符。
                    // 那么value的起始位置继续往后移动。
                    if (!hasSep && (c == '=' || c == ':')) {
                        hasSep = true;
                    } else {
                        // 当前字符不是=或者:,或者在此之前出现过=或者:字符。那么结束循环。
                        break;
                    }
                }
                valueStart++;
            }
            // 读取key
            String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
            // 读取value
            String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
            // 包括key/value
            put(key, value);
        }
    }

我们可以看到,在这个过程中,会将分割符号两边的空格去掉,并且分割符号可以是=,:,空格等。
而且=和:的级别比空格分隔符高,即当这两个都存在的情况下,是按照=/:分割的。
可以看到在最后会调用一个loadConvert方法,该方法主要是做key/value的读取,并将十六进制的字符进行转换。

loadFromXML:
该方法主要是提供一个从XML文件中读取key/value键值对的方法。
底层是调用的XMLUtil的方法,加载完对象属性后,流会被显示的关闭。
xml格式如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>comments</comment>
<entry key="key7">value7</entry>
<entry key="key6">value7</entry>
<entry key="key4">value4 </entry>
<entry key="key3">vlaue3</entry>
<entry key="key2">value2</entry>
<entry key="key1">value1</entry>
</properties>

Properties继承的是HashTable,当setProperties的时候,利用的是HashTable的put进行存储。
当我们store配置信息到文件中的时候,直接将内存中的key、value全部存储到文件中。

store(InputStream/Reader,String)方法 :
该方法主要是将属性值写出到文本文件中,并写出一个comment的注释。
底层调用的是store0方法。针对store(InputStream,String)方法,在调用store0方法的时候,
进行字节流封装成字符流,并且指定字符集为8859-1。源码如下:

private void store0(BufferedWriter bw, String comments, boolean escUnicode) throws IOException {
        if (comments != null) {
            // 写出注释, 如果是中文注释,那么转化成为8859-1的字符
            writeComments(bw, comments);
        }
        // 写出时间注释
        bw.write("#" + new Date().toString());
        // 新起一行
        bw.newLine();
        // 进行线程间同步的并发控制
        synchronized (this) {
            for (Enumeration e = keys(); e.hasMoreElements();) {
                String key = (String) e.nextElement();
                String val = (String) get(key);
                // 针对空格进行转义,并根据是否需要进行8859-1编码
                key = saveConvert(key, true, escUnicode);
                /*
                 * No need to escape embedded and trailing spaces for value,
                 * hence pass false to flag.
                 */
                // value不对空格进行转义
                val = saveConvert(val, false, escUnicode);
                // 写出key/value键值对
                bw.write(key + "=" + val);
                bw.newLine();
            }
        }
        bw.flush();
    }

常用读取方式:

  Properties prop = new Properties();
 InputStream in = Test.class.getClassLoader().getResourceAsStream( "test.properties"); 
  prop.load(in);

getClassLoader().getResourceAsStream()方法直接获得字节输入流,这种方式不用考虑路径中是否包含中文的问题。
getClassLoader().getResource()方法,因为该方法返回值是URL,如果项目的目录中有中文命名,
则获得的URL会出现乱码,所以使用

String path=URLDecoder.decode(url.getFile(), "utf-8");

LinkedHashMap

LinkedHashMap是HashMap的子类,所以其实有很多地方类似。
其不同点主要如下:
1.LinkedHashMap是有序的,而HashMap不一定有序。

  • 实现
    LinkedHashMap除了将元素用HashMap方式存储外,还将所有Entry节点链入一个双向链表中。
    Entry双向链表中的数据是HashMap中链表数据的引用,即没有额外生成新元素,只是扩展了Entry加入的引用字段。
    因此对于每次put进来Entry,除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部。
    同时类里有两个成员变量head tail,分别指向内部双向链表的表头、表尾。
//双向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;

LinkedHashMap中的Entry属性:

属性名称
hash,key ,value ,next同HashMap中的桶中的Entry节点中的属性
before,afterLinkedHashMap中扩展的属性,用于维护双向链表的顺序

注意:
next用于维护HashMap各个桶中Entry的连接顺序,before、after用于维护Entry插入的先后顺序。

LinkedHashMap的LRU算法支持:
使用LinkedHashMap实现LRU的必要前提是将accessOrder标志位设为true以便开启按访问顺序排序的模式。

/**
     * This override alters behavior of superclass put method. It causes newly
     * allocated entry to get inserted at the end of the linked list and
     * removes the eldest entry if appropriate.
     *
     * LinkedHashMap中的addEntry方法
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //创建新的Entry,并插入到LinkedHashMap中
        createEntry(hash, key, value, bucketIndex); // 重写了HashMap中的createEntry方法
        //双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
        Entry<K,V> eldest = header.after;
        //如果有必要,则删除掉该近期最少使用的节点,
        //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            //扩容到原来的2倍
            if (size >= threshold)
                resize(2 * table.length);
        }
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 向哈希表中插入Entry,这点与HashMap中相同
        //创建新的Entry并将其链入到数组对应桶的链表的头结点处,
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
        table[bucketIndex] = e;
        //在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,
        //这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素(LinkedHashMap根据双向链表重写了迭代器)
        //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现
        e.addBefore(header);
        size++;
    }

removeEldestEntry方法:

 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

当其值返回true的时候,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。
实际上是因为在put数据的时候会改变双向链表的顺序,是最新的数据放置到链表尾部。
而开启accessOrder后,访问数据时同样会将最新的数据放置到链表尾部。
即不开启accessOrder时遍历双向链表中的顺序就是插入顺序,而开始后则是数据的增删改查后更新的顺序。
依次循环会导致最不常用的数据下沉到header后面的那个节点。

containsValue方法:
LinkedHashMap重写了containsValue方法,相比HashMap的实现,更为高效。

    public boolean containsValue(Object value) {
        //遍历一遍链表,去比较有没有value相等的节点,并返回
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }

而HashMap的containsValue,是用两个for循环遍历,相对低效。

    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

IdentityHashMap

IdentityHashMap由于平常使用的较少,所以这里简单介绍下:

  • 功能
    IdentityHashMap通用存放键值对,规则是当两个Key引用的对象完全相等(即用 == 符号判断返回true时)
    时,才会这更新/删除对应的值。HashMap是用的equals方法作为对比。

IdentityHashMap的层次:

public class IdentityHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, java.io.Serializable, Cloneable

IdentityHashMap中同样可以存放键或值是null的数据。

@Test
    public void identityHashMapTest(){
        IdentityHashMap idMap = new IdentityHashMap();
        idMap.put(null,null);
        idMap.put(516,null);
        idMap.put(null,123);
        idMap.forEach( (K,V)->{
            System.out.println(K + " : " + V);
        });
    }
  • 实现原理
    IdentityHashMap的底层是一个Object数组,并且直接用此Object数组来保存键值数据。
    其保存方式是,用hash计算的数组索引来得到key,在保存key的位置的下一个位置来保存此key对应的值。
    其默认数组长度是32,最小长度4,最大长度2的29次方。

其初始化和扩容时默认将会数组大小调整为2的n次方-1的长度,这点和HashMap中的一样,
可以利用位运算&来进行快速取模运算,得到其键的hash值对应的数组的索引。

如果索引冲突,那么从当前数组所在位置向后遍历找到没使用的数组节点,在找到数组尾部后
仍然没找到的话,则从数组头部开始再次遍历,直到找到空位置。

如果在put时,发现当前数据数量(即键值对的数量,一个键值对算一个) 乘以3的长度大于数组长度,
则会对原数组进行扩容,同时重新计算hash和进行值拷贝。
默认扩容后的数组长度是原长度的2倍。

其对于Key是Null的数据,会有一个定义好的Key来替代Null作为Key保存数据。

public class IdentityHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, java.io.Serializable, Cloneable
{
    // 缺省容量大小
    private static final int DEFAULT_CAPACITY = 32;
    // 最小容量
    private static final int MINIMUM_CAPACITY = 4;
    // 最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 29;
    // 用于存储实际元素的表
    transient Object[] table;
    // 大小
    int size;
    // 对Map进行结构性修改的次数
    transient int modCount;
    // null key所对应的值
    static final Object NULL_KEY = new Object();
}

put方法:

public V put(K key, V value) {
        // 保证null的key会转化为Object(NULL_KEY)
        final Object k = maskNull(key);
        retryAfterResize: for (;;) {
            final Object[] tab = table;
            final int len = tab.length;
            int i = hash(k, len);
            for (Object item; (item = tab[i]) != null;
                 i = nextKeyIndex(i, len)) {
                if (item == k) { // 经过hash计算的项与key相等
                    @SuppressWarnings("unchecked")
                        // 取得值
                        V oldValue = (V) tab[i + 1];
                    // 将value存入
                    tab[i + 1] = value;
                    // 返回旧值
                    return oldValue;
                }
            }
            // 大小加1
            final int s = size + 1;
            // Use optimized form of 3 * s.
            // Next capacity is len, 2 * current capacity.
            // 如果3 * size大于length,则会进行扩容操作
            if (s + (s << 1) > len && resize(len))
                // 扩容后重新计算元素的值,寻找合适的位置进行存放
                continue retryAfterResize;
            // 结构性修改加1
            modCount++;
            // 存放key与value
            tab[i] = k;
            tab[i + 1] = value;
            // 更新size
            size = s;
            return null;
        }
    }
  • remove方法
    删除对应的键值(实际上是键值对的size–,以及把对应索引的引用对象置为null)后需要进行后续处理,
    把之前由于冲突往后挪的元素移到前面来(所以不冲突的元素位置仍然不动)。

TreeMap

TreeMap的数据存储时基于红黑树的结构来存储键值对,
所以TreeMap是会自动对key进行排序(次序由Comparable或Comparator决定)。

1.TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,
也因此不需要key覆写hashCode方法和equals方法,就可以排除掉重复的key,
而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。

2.TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap。

3.TreeMap的key不能为null,而HashMap的key可以为null。

实现Comparable结构的类可以和其他对象进行比较,即实现Comparable可以进行比较的类。
而实现Comparator接口的类是比较器,用于比较两个对象的大小。

笔者这里不分析相关算法,有兴趣可以查看:
红黑树 资料:https://www.cnblogs.com/CarpenterLee/p/5503882.html
常见树形数据结构:https://zhuanlan.zhihu.com/p/27700617

WeakHashMap

WeakHashMap适用于作为缓存Map,这样当内存不够时,其会自动回收内存来压缩缓存空间,
而不会导致内存溢出现象。

weakHashMap的继承关系:

java.lang.Object
   ↳ java.util.AbstractMap<K, V>
         ↳ java.util.WeakHashMap<K, V>
public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {}

WeakHashMap中使用一个Entry数组中装载Entry链表头节点的方式来保存数据。
所以当remove(实际上就是删除链表中的某个节点)后,链表的引用不在存在。
put方法返回旧值,实际上也是替换原值或者插入Entry节点到Entry数组中。

WeakHashMap其键值可以是Null。

其实现大致和HashMap相同,但是增加了一个ReferenceQueue queue,来保存被java虚拟机收回的对象的虚引用。
如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
接着,WeakHashMap会根据“引用队列”,来删除“WeakHashMap中已被GC回收的‘弱键’对应的键值对”。

其初始容量是16,最大长度是2的30次方,扩容因子是0.75,扩容时容量变为原来的2倍。
由于

    public class WeakHashMap<K,V> 
    extends AbstractMap<K,V>
    implements Map<K,V> {
    // 默认的初始容量是16,必须是2的幂。
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认加载因子
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 存储数据的Entry数组,长度是2的幂。
    // WeakHashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表
    private Entry[] table;
    // WeakHashMap的大小,它是WeakHashMap保存的键值对的数量
    private int size;
    // WeakHashMap的阈值,用于判断是否需要调整WeakHashMap的容量(threshold = 容量*加载因子)
    private int threshold;
    // 加载因子实际大小
    private final float loadFactor;
    // queue保存的是“已被GC清除”的“弱引用的键”。
    // 弱引用和ReferenceQueue 是联合使用的:如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
    private final ReferenceQueue<K> queue = new ReferenceQueue<K>();
    // WeakHashMap被改变的次数
    private volatile int modCount;

WeakReference的Entry:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        int hash;
        Entry<K,V> next;
        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
   ....
}

 此外,在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,
以清理持有弱引用的key的表象。
可以看到每调用一次expungeStaleEntries()方法,就会在引用队列中寻找是否有被清除的key对象,
如果有则在table中找到其值,并将value设置为null,next指针也设置为null,让GC去回收这些资源。

private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {//清理弱引用队列中的数据
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

ConcurrentHashMap

jdk1.7及以下版本采用的分段锁技术,jdk1.8采用CAS算法技术。这里只简单说下分段锁技术。
Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的性能更好
ConcurrentHashMap的键或值不能是null。
(在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为
之提供外同步(Collections.synchronizedMap))。
ConcurrentHashMap使用的分段锁技术,首先将数据分成一段一段的存储,
然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,
其他段的数据也能被其他线程访问,这样大大提高了并发性能。

ConcurrentHashMap中由Segment数组结构保存数据。
Segment<K,V>继承自ReentrantLock,而其V的值得结构和HashMap类似,是一个链表Entry头节点的数组。
Segment数组的最大长度是65536,即2的16次方。
而其默认值是concurrentLevel,可以手动指定长度大小,系统会自动调整为大于等于传入参数值得2的n次方。

简单来说其实就是利用二次Hash技术将输入分段再分段来存储和加锁,所以极限情况
是存入对象的hash值过于集中的时候会大大降低同步和遍历存储的效率。

是否需要扩容:
在插入元素前会先判断Segment里的HashEntry数组是否超过容量,如果超过阀值,数组进行扩容。
Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,
如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

哈希映射

哈希映射结构由一个存储元素的内部数组组成。
由于内部采用数组存储,因此必然存在一个用于确定任意键访问数组的索引机制。
实际上,该机制需要提供一个小于数组大小的整数索引值。该机制称作哈希函数。

在 Java 基于哈希的 Map 中,哈希函数将对象转换为一个适合内部数组的整数。
每个对象都包含一个返回整数值的 hashCode() 方法。
要将该值映射到数组,只需将其转换为一个正值,然后在将该值除以数组大小后取余数即可。

哈希函数将任意对象映射到一个数组位置,但如果两个不同的键映射到相同的位置,这称作冲突。
Map 处理这些冲突的方法是在索引位置处插入一个链接列表,并将元素添加到此链接列表尾部。

常用操作和性能

优化 Hasmap

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

List和Map互转

推荐使用java8中的lambda实现list和Map互转。
list转Map:

public void ListToMap(){
    List<Integer> list = new ArrayList();
    list.add(1);
    list.add(2);
    list.stream().collect(Collectors.toMap(item->item.toString(), Function.identity(), (key1, key2) -> key2));
}

Collectors.toMap(item->item.toString(), Function.identity(), (key1, key2) -> key2)说明:
这里的是三个参数本分是:map的key值,map的value值,map中key相同的话去重的方式。
item->item.toString()中,item表示list中的对象,这里表示用toString()方法的返回值作为map的key。
Function.identity() 表示使用list中的对象作为value值,也可以写成v->v 的格式,即用v表示list中的对象,返回返回v值作为value的值。
(key1, key2) -> key2 表示当一个list中有几个key值相同时,以key2(即list中更后面的元素生成的key)的值为准。

Map转List,可以用传统的遍历add到list的方式。
推荐用java8的lambda表达式:

public void readTest() {
    Map<Integer,Integer> map = new HashMap();
        map.put(66, 6);
        map.put(55, 5);
        map.put(77, 7);
    List<Integer> lsit =  map.keySet().stream().collect(Collectors.toList());
}

Map的遍历

注意:
除非数据超级多,一般遍历的性能差距不大。
通过iterator的遍历方式都可以用foreach方式来使用:

for(String key : keySet){
    ...
}

Map不同的遍历方式和性能影响:
第一种 ,通过entry节点的iterator对象:

  Map map = new HashMap();
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();

iterator的效率较高。

第二种,通过keySet的iterator遍历
这种方式因为在便利时的值是通过get方法来取的,
有一个查找过程,所以效率相对较低。

  Map map = new HashMap();
  Iterator iter = map.keySet().iterator();
  while (iter.hasNext()) {
  Object key = iter.next();
  Object val = map.get(key);
  }

java8中的遍历方式:

idMap.forEach( (K,V)->{
            System.out.println(K + " : " + V);
        });

Fail-Fast 机制

fail-fast 机制是 java 集合(Collection)中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程
中有其他线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。

这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,
对 HashMap 内容(当然不仅仅是 HashMap 才会有,其他例如 ArrayList 也会)的修改都将增加这个值
(大家可以再回头看一下其源码,在很多操作中都有 modCount++ 这句),
那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。

HashIterator() {
    expectedModCount = modCount;
    if (size > 0) { // advance to first entry
    Entry[] t = table;
    while (index < t.length && (next = t[index++]) == null)
        ;
    }
}

在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相
等就表示已经有其他线程修改了 Map:
注意到 modCount 声明为 volatile,保证线程之间修改的可见性。

final Entry<K,V> nextEntry() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();

在 HashMap 的 API 中指出:
由所有 HashMap 类的“collection 视图方法”所返回的迭代器都是快速失败的:
在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,
其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。

因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。
快速失败迭代器尽最大努力抛出 ConcurrentModificationException。
因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

解决方案
在上文中也提到,fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,
因为 JDK 并不保证 fail-fast 机制一定会发生。
若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。

参考

HashMap原理:
https://blog.csdn.net/wushiwude/article/details/76041751
Properties分析:
https://www.cnblogs.com/liuming1992/p/4360310.html
list转Map用法:
https://zacard.net/2016/03/17/java8-list-to-map/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值