关于Map的内部及底层原理的详细概述详解

    技术2025-02-18  19

    Map(深层理解详述)

    目录:

    ​ 一、Map的实现类的结构: *****************************Map集合概述和特点

    ​ 二、Map结构的理解

    ​ 三、HashMap的底层实现原型?以jdk7为例说明:

    ​ HashMap map= new HashMap()

    ​ *****************************jdk8 相较于dk7在底层实现方面的不同

    ​ *****************************HashMap源码中的重要常量

    ​ 四、LinkedHashMap的底层实现原理(了解)

    ​ 五、Map 排序:

    ​ *****************************HashMap、Hashtable、LinkedHashMap排序

    ​ *****************************TreeMap排序

    ​ *****************************按value排序(通用)

    ​ 六、Map接口:常用方法:

    ​ *****************************·添加、删除、修改操作

    ​ *****************************·元素查询的操作

    ​ *****************************·元视图操作的方法

    ​ *****************************总结:常用方法

    ​ *****************************关于Properties

    ​ 七、Collections工具类(操作数组的工具类:Arrays)

    ​ *****************************Collections常用方法

    ​ 八、摘要:

    ​ *****************************核心 Map

    ​ *****************************内部哈希: 哈希映射技术

    ​ *****************************优化 Hasmap

    ​ *****************************调整 Map 实现的大小

    ​ *****************************Map 选择


    一、Map的实现类的结构:

    Map集合概述和特点

    概述: 将键映射到值的对象 一个映射不能包含重复的键 每个键最多只能映射到一个值 Map接口和Collection接口的不同 Map是双列的,Collection是单列的 Map的键唯一,Collection的子体系Set是唯一的 Map集合的数据结构针对键有效,跟值无关;Collection集合的数据结构是针对元素有效

    Map概括:

    ----Map:双列数据,存储key-value对的数据—-类似于高中的函数:y=f(x)

    ---------HashMap:作为Map的主要实现类;线程不安全的,效率高;存储nuLL的key和value

    -----------------LinkedHashMap:保证在遍map元素时,可以按照添加的顺序实现遍历。

    ​ 原因:在原有MashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。 对于频繁的遍历操作,此类执行效率高于HashMap。

    ---------TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。

    ​ 此时考虑key的自然排序或定制排序底层使用红黑树

    -----------------Hashtable:作为古老的实现类;线程安全的,效率低;不能存储uLL的key value

    -----------------Properties:常用来处理配置文件。key value都是string类型

    HashMap的底层:数组+链表(😩jdk7及之前)

    ​ 数组+链表+红黑树(😩jdk 8)

    二、Map结构的理解:

    Map 中的key:无序的、不可重复的,使用Set存储所有的key–>key所在的类要重写equals()和hashcode() (以HashMap为例)

    Map 中的value:无序的、可重复的,使用collection存储所有的value–>vaLue所在的类要重写equals()

    一个键值对:key-value构成了一个Entry对象。

    Map 中的entry:无序的、不可重复的,使用Set存储所有的entr

    三、HashMap的底层实现原型?以jdk7为例说明:

    HashMap map= new HashMap():

    1…可能已经执行过多次put… 2.在实例化以后,底层创建了长度是16的一维数组Entry[] table。

    ​ 3.首先,调用key1所在类的hashcode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。

    ​ 4.如果此位置上的数帮为空,此时的key1-value1添加成功。----情况1

    ​ 5.如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

    ​ -----------a. 如果key1的哈希值与已经存在的教据的哈希值都不相同,此时key1-value1添加成功。----情况2

    ​ -----------b.如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals()方法,比较:

    ​ -----------------------b.1如果equals()返false:此时key1-value1添加成功。----情况3

    ​ -----------------------b.2 如果equals()返回true:使用value1替换相同key的value值。

    ​ 6.补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

    ​ 7.在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

    jdk8 相较于dk7在底层实现方面的不同:

    ​ 1.new HashMap():底层没有创建一个长度为为6的数组

    ​ 2.jdk 8底层的数组是:Node[],而非Entry[]

    ​ 3.首次调用put()方法时,底层创建长度为6的数组

    ​ 4.jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。

    ​ 当数组的某一个索引位置上的元素以链表形式存在的数据个数(8且当前数组的长度)64时,此时此索引位置上的所有数据改为使用红黑树存储。

    HashMap源码中的重要常量:

    DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16

    DEFAUL T_LOAD_ FACTOR:HashMap的默认加载因子:0.75

    threshold:扩容的临界值,=容量填充因子:160.75=>12

    TREEIFY_ THRESHOLD:Bucket 中链表长度大于该默认值,转化为红黑树:8

    MIN_TREEIFY_CAPACITY:福中的Node被树化时最小的hash表容量:64

    四、LinkedHashMap的底层实现原理(了解)

    五、Map 排序

    HashMap、Hashtable、LinkedHashMap排序

    注:

    TreeMap也可以使用此方法进行排序,但是更推荐下面的方法。

    Map<String, String> map = new HashMap<String, String>(); map.put("a", "c"); map.put("b", "b"); map.put("c", "a"); // 通过ArrayList构造函数把map.entrySet()转换成list List<Map.Entry<String, String>> list = new ArrayList<Map.Entry<String, String>>(map.entrySet()); // 通过比较器实现比较排序 Collections.sort(list, new Comparator<Map.Entry<String, String>>() { public int compare(Map.Entry<String, String> mapping1, Map.Entry<String, String> mapping2) { return mapping1.getKey().compareTo(mapping2.getKey()); } }); for (Map.Entry<String, String> mapping : list) { System.out.println(mapping.getKey() + " :" + mapping.getValue()); }

    TreeMap排序

    TreeMap默认按key进行升序排序,如果想改变默认的顺序,可以使用比较器:

    Map<String, String> map = new TreeMap<String, String>(new Comparator<String>() { public int compare(String obj1, String obj2) { return obj2.compareTo(obj1);// 降序排序 } }); map.put("a", "c"); map.put("b", "b"); map.put("c", "a"); for (String key : map.keySet()) { System.out.println(key + " :" + map.get(key)); }

    按value排序(通用)

    Map<String, String> map = new TreeMap<String, String>(); map.put("a", "c"); map.put("b", "b"); map.put("c", "a"); // 通过ArrayList构造函数把map.entrySet()转换成list List<Map.Entry<String, String>> list = new ArrayList<Map.Entry<String, String>>(map.entrySet()); // 通过比较器实现比较排序 Collections.sort(list, new Comparator<Map.Entry<String, String>>() { public int compare(Map.Entry<String, String> mapping1, Map.Entry<String, String> mapping2) { return mapping1.getValue().compareTo(mapping2.getValue()); } }); for (String key : map.keySet()) { System.out.println(key + " :" + map.get(key)); }

    六、Map接口:常用方法

    ·添加、删除、修改操作:

    ​ Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中

    ​ void putAll(Map m):将m中的所有key-value对存放到当前map中

    ​ Object remove(Object key):移除指定key的key-value对,并返回value

    ​ void clear():清空当前map中的所有数据

    ·元素查询的操作:

    ​ Object get(Object key):获取指定key对应的value

    ​ boolean containsKey(Object key):是否包含指定的key

    ​ boolean cortainsValue(Object value):是否包含指定的value

    ​ int size():返回map中key-value对的个数

    ​ boolean isEmpty():判断当前map是否为空

    ​ boolean equals(Object obj):判断当前map和参数对象obj是否相等

    ·元视图操作的方法:

    Set keySet():返回所有key构成的Set集合

    Collection values():返回所有value构成的Collection集合

    Set entrySet():返回所有key-value对构成的Set集合

    总结:常用方法:

    ​ 1.添加:put(object key,Object value)

    ​ 2.删除:remove(Object key)

    ​ 3.修改:put(Object key,Object value)

    ​ 4.查询:get(Object key)

    ​ 5.长度:size()

    ​ 6.遍历:keySet()/values()/entrySet()

    package JiHe; import org.junit.Test; import java.util.*; /** * @author: 小不点 * @date 2020/6 22:04 */ public class MapTest1 { @Test public void test1(){ Map hashMap = new HashMap(); hashMap.put(null, null);//可以写为null } @Test public void test2(){ Map map = new HashMap();//{23=BB, 56=CC, 123=AA}//不是按添加顺序 map = new LinkedHashMap();//{123=AA, 23=BB, 56=CC}//是按添加顺序 map.put(123,"AA"); map.put(23,"BB"); map.put(56,"CC"); System.out.println(map); } /* 添加、删除、修改操作: 1.Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中 2.void putAll(Map m):将m中的所有key-value对存放到当前map中 3.Object remove(Object key):移除指定key的key-value对,并返回value 4.void clear():清空当前map中的所有数据 */ @Test public void test3() { //Object put(Object key,Object value)--> Map map = new HashMap(); //"AA"添加 map.put("AA", 456); map.put(123, 45); map.put("BB", 46); //"AA"修改 map.put("AA", 45); System.out.println(map);//{AA=45, BB=46, 123=45} Map map1 = new HashMap(); map1.put("CC", 46); map1.put("GG", 46); //void putAll(Map m)--> map.putAll(map1); System.out.println(map);//{AA=45, BB=46, CC=46, GG=46, 123=45} //Object remove(Object key)--> Object value = map.remove("CC"); System.out.println(value);//46 System.out.println(map);//{AA=45, BB=46, GG=46, 123=45} //void clear()--> map.clear();//与map=null操作不同 System.out.println(map.size());//0 System.out.println(map);//{} } /* 元素查询的操作: 1. Object get(Object key):获取指定key对应的value 2. boolean containsKey(Object key):是否包含指定的key 3. boolean cortainsValue(Object value):是否包含指定的value 4. int size():返回map中key-value对的个数 5. boolean isEmpty():判断当前map是否为空 6. boolean equals(Object obj):判断当前map和参数对象obj是否相等 */ @Test public void test4() { Map map = new HashMap(); map.put("AA", 456); map.put(123, 45); map.put("BB", 46); map.put("AA", 45); //Object get(Object key)--> System.out.println(map.get(123));//45 System.out.println(map.get(154654));//null System.out.println(map.get("AA"));//45 //containsKey(Object key)--> boolean isExist = map.containsKey("BB");//看是否Key存在 System.out.println(isExist);//true //cortainsValue(Object value)--> isExist = map.containsValue(46);//看是否value存在 System.out.println(isExist);//true //int size()--> System.out.println(map.size());//3 //isEmpty():判断当前map是否为空--> System.out.println(map.isEmpty());//false //boolean equals(Object obj)--> Map map2 = new HashMap(); map2.put("AA", 456); map2.put(123, 45); map2.put("BB", 46); map2.put("AA", 45); System.out.println(map.equals(map2));//true } /* 元视图操作的方法 1. Set keySet():返回所有key构成的Set集合 2. void putAll(Map m):将m中的所有key-value对存放到当前map中 3. Set entrySet():返回所有key-value对构成的Set集合 */ @Test public void test5() { Map map = new HashMap(); map.put(123, "AA"); map.put(23, "BB"); map.put(56, "CC"); //Set keySet():返回所有key构成的Set集合--> Set ste = map.keySet(); Iterator iterator = ste.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next());//23 56 123 } System.out.println();//换个行 //void putAll(Map m):将m中的所有key-value对存放到当前map中--> Collection values = map.values(); for(Object obj : values){ System.out.println(obj);//BB CC AA } System.out.println();//换个行 //返回所有key-value对构成的Set集合--> //放式一:Set entrySet(): Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()){ Object obj = iterator1.next(); //entrySet菜合中的元素都是entry Map.Entry entry= (Map.Entry)obj; System.out.println(entry.getKey()+"-->"+entry.getValue()); //23-->BB 56-->CC 123-->AA } System.out.println();//换个行 //放式二: Set keySet = map.keySet(); Iterator iterator2 = ste.iterator(); while (iterator2.hasNext()) { Object key = iterator2.next(); Object value = map.get(key); System.out.println(key+"--"+value); //23--BB 56--CC 123--AA } } } package JiHe; import org.junit.Test; import java.util.*; /** * @author: 小不点 * @date 2020/7 12:40 */ public class TreeMapTest { //向TreeMap中添加key-value,要求key必须是由同一个类创建的对象 //因为要按照key进行排序:自然排序、定制排序 @Test//自然排序 public void test6() { TreeMap map = new TreeMap(); //引用的User1的类,放在了下面的备用代码块中,可直接查看 User1 u1 = new User1("jaba", 20); User1 u2 = new User1("jcba", 13); User1 u3 = new User1("jcek", 63); User1 u4 = new User1("ebea", 89); map.put(u1, 62); map.put(u2, 31); map.put(u3, 45); map.put(u4, 66); Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()) { Object obj = iterator1.next(); //entrySet菜合中的元素都是entry Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-->" + entry.getValue()); //输出结果: //User1{name='ebea', age=89}-->66 //User1{name='jaba', age=20}-->62 //User1{name='jcba', age=13}-->31 //User1{name='jcek', age=63}-->45 } } @Test public void test7() { TreeMap map = new TreeMap(new Comparator() { @Override public int compare(Object o1, Object o2) { if(o1 instanceof User1 && o2 instanceof User1) { User1 u1 = (User1) o1; User1 u2 = (User1) o2; //按照年龄排序 return Integer.compare(u1.getAge(), u2.getAge()); } throw new RuntimeException("输入的类型不匹配!"); } }); //引用的User1的类,放在了下面的备用代码块中,可直接查看 User1 u1 = new User1("jaba", 20); User1 u2 = new User1("jcba", 13); User1 u3 = new User1("jcek", 63); User1 u4 = new User1("ebea", 89); map.put(u1, 62); map.put(u2, 31); map.put(u3, 45); map.put(u4, 66); Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()) { Object obj = iterator1.next(); //entrySet菜合中的元素都是entry Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-->" + entry.getValue()); //User1{name='jcba', age=13}-->31 //User1{name='jaba', age=20}-->62 //User1{name='jcek', age=63}-->45 //User1{name='ebea', age=89}-->66 } } }

    上述调用类:

    package JiHe; /** * @author: 小不点 * @date 2020/7 12:59 */ public class User1 implements Comparable { private String name; private int age; public User1() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User1(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "User1{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User1 user1 = (User1) o; if (age != user1.age) return false; return name != null ? name.equals(user1.name) : user1.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } //按照姓名从小到大排列 @Override public int compareTo(Object o) { if (o instanceof User1) { User1 user1 = (User1) o; return this.name.compareTo(user1.name); } else { throw new RuntimeException("输入的类型不匹"); } } }

    关于Properties:

    package JiHe; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Properties; /** * @author: 小不点 * @date 2020-07 13:45 */ public class PropertiesTest { //Properties:常用来处理配置文件。key value都是string类型 public static void main(String[] args) throws Exception { FileInputStream fil = null; try { Properties pro = new Properties(); //调用的properties代码块在下面 fil = new FileInputStream("jdbc.properties"); pro.load(fil);//加载流对应的文件 String name = pro.getProperty("name"); String password = pro.getProperty("password"); System.out.println("name="+ name + ", password=" + password); //name=Tom, password=abc123 } catch (IOException e) { e.printStackTrace(); } finally { if (fil != null){ try { fil.close(); } catch (IOException e) { e.printStackTrace(); } } } } }

    上述调用类(jdbc.properties):

    name=Tom password=abc123

    七、Collections工具类(操作数组的工具类:Arrays)

    ·Collections 是一个操作Set、List和Map等集合的工具类 ·Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法 ·排序操作:(均为static方法)

    ​ 1.reverse(List):反转List中元素的顺序

    ​ 2.shuffle(List):对List 集合元素进行随机排序

    ​ 3.sort(List):根据元素的自然顺序对指定 List集合元素按升序排序

    ​ 4.sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序

    ​ 5.swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换

    Collections常用方法

    查找、替换 1.Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

    2.Object max(Collection,Comparator):根据Comparator指定的顺序,返回 给定集合中的最大元素

    3.Object min(Collection)

    4.Object min(Collection,Comparator)

    5.int frequency(Collection,Object):返回指定集合中指定元素的出现次数

    6.void copy(List dest,List src):将src中的内容复制到dest中

    7.boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List对象的所有旧值

    说明:ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转 换为线程的。 使用synchronizedList(List list)和synchronizedMap(Map map)

    package JiHe; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * @author: 小不点 * @date 2020-07 18:56 * * Collections:操作Collection、Map的工具类 * * 1.reverse(List):反转List中元素的顺序 * 2.shuffle(List):对List 集合元素进行随机排序 * 3.sort(List):根据元素的自然顺序对指定 List集合元素按升序排序 * 4.sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序 * 5.swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换 * * 6.Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素 * 7.Object max(Collection,Comparator):根据Comparator指定的顺序,返回 * 给定集合中的最大元素 * 8.Object min(Collection) * 9.Object min(Collection,Comparator) * 10.int frequency(Collection,Object):返回指定集合中指定元素的出现次数 * 11.void copy(List dest,List src):将src中的内容复制到dest中 * 12.boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 * List对象的所有旧值 */ public class CollectionsTest { @Test public void test8(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(-98); list.add(0); System.out.println(list);//[123, 45, -98, 0] //reverse(List):反转List中元素的顺序--> Collections.reverse(list); System.out.println(list);//[0, -98, 45, 123] } @Test public void test9(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(-98); list.add(0); System.out.println(list);//[123, 45, -98, 0] //shuffle(List):对List 集合元素进行随机排序--> Collections.shuffle(list); System.out.println(list);//[-98, 123, 45, 0]//随机 } @Test public void test10(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(-98); list.add(0); System.out.println(list);//[123, 45, -98, 0] //sort(List):根据元素的自然顺序对指定 List集合元素按升序排序--> Collections.sort(list); System.out.println(list);//[-98, 0, 45, 123] } @Test public void test11(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(-98); list.add(0); System.out.println(list);//[123, 45, 45, 45, -98, 0] //swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换--> Collections.swap(list,1,2); System.out.println(list);//[123, -98, 45, 0] } @Test public void test12(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(45); list.add(45); list.add(-98); list.add(0); System.out.println(list);//[123, 45, -98, 0] //int frequency(Collection,Object):返回指定集合中指定元素的出现次数--> int frequency = Collections.frequency(list, 45); System.out.println(frequency);//3 } @Test public void test13(){ ArrayList list = new ArrayList(); list.add(123); list.add(45); list.add(-98); list.add(0); //错误: //报异常:IndexOutofBoundsException("Source does not fit in dest") //List dest=new Arraylist(); //Collections.copy(dest,List); //正确: List dest = Arrays.asList(new Object[list.size()]); System.out.println(dest.size());//4 //void copy(List dest,List src):将src中的内容复制到dest中--> Collections.copy(dest,list); System.out.println(dest);//[123, 45, -98, 0] /* Collections 类中提供了多个synchronizedXxx()方法, 该方法可使将指定集合包装成线程同步的集合,从而可以解决 多线程并发访问集合时的线程安全问题 */ //返回的List1即为线程安全的List List list1=Collections.synchronizedList(list); } }

    八、摘要

    核心 Map

    Java 自带了各种 Map 类。 这些 Map 类可归为三种类型:

    通用 Map ,用于在应用程序中管理映射,通常在 java.util 程序包中实现 o HashMap o Hashtable o Properties o LinkedHashMap o IdentityHashMap o TreeMap o WeakHashMap o ConcurrentHashMap 2. 专用 Map ,您通常不必亲自创建此类 Map,而是通过某些其他类对其进行访问 o java.util.jar.Attributes o javax.print.attribute.standard.PrinterStateReasons o java.security.Provider o java.awt.RenderingHints o javax.swing.UIDefaults 3. 一个用于帮助实现您自己的 Map 类的抽象类 o AbstractMap

    内部哈希: 哈希映射技术

    几乎所有通用 Map 都使用哈希映射。 这是一种将元素映射到数组的非常简单的机制,您应了解哈希映射的工作原理,以便充分利用 Map 。 哈希映射结构由一个存储元素的内部数组组成。 由于内部采用数组存储,因此必然存在一个用于确定任意键访问数组的索引机制。 实际上,该机制需要提供一个小于数组大小的整数索引值。 该机制称作哈希函数。 在 Java 基于哈希的 Map 中,哈希函数将对象转换为一个适合内部数组的整数。 您不必为寻找一个易于使用的哈希函数而大伤脑筋: 每个对象都包含一个返回整数值的 hashCode() 方法。 要将该值映射到数组,只需将其转换为一个正值,然后在将该值除以数组大小后取余数即可。 以下是一个简单的、适用于任何对象的 Java 哈希函数

    int hashvalue = Maths.abs(key.hashCode()) % table.length;

    (% 二进制运算符(称作模)将左侧的值除以右侧的值,然后返回整数形式的余数。)

    实际上,在 1.4 版发布之前,这就是各种基于哈希的 Map 类所使用的哈希函数。 但如果您查看一下代码,您将看到

    int hashvalue = (key.hashCode() & 0x7FFFFFFF) % table.length;

    它实际上是使用更快机制获取正值的同一函数。 在 1.4 版中,HashMap 类实现使用一个不同且更复杂的哈希函数, 该函数基于 Doug Lea 的 util.concurrent 程序包(稍后我将更详细地再次介绍 Doug Lea 的类)。

    如果看一下各种基于哈希的 Map 的源代码,您将发现这基本上就是它们的工作原理。 此外,还有一些需要进一步考虑的事项,如处理空键和值以及调整内部数组。 此处定义的 put() 方法还包含相应 get() 的算法,这是因为插入包括搜索映射索引处的项以查明该键是否已经存在。 (即 get() 方法与 put() 方法具有相同的算法,但 get() 不包含插入和覆盖代码。) 使用链接列表并不是解决冲突的唯一方法,某些哈希映射使用另一种 “开放式寻址 ”方案,本文对其不予介绍。

    优化 Hasmap

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

    调整 Map 实现的大小

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

    使用 1.4.2 JVM 运行一个简单的测试,即用大量的项(数目超过一百万)填充 HashMap 。 表 5 显示了结果,并将所有时间标准化为已预先设置大小的服务器模式(关联文件中的 。 对于已预先设置大小的 JVM ,客户端和服务器模式 JVM 运行时间几乎相同(在放弃 JIT 编译阶段后)。 但使用 Map 的默认大小将引发多次调整大小操作,开销很大,在服务器模式下要多用 50% 的时间,而在客户端模式下几乎要多用两倍的时间!

    表 5: 填充已预先设置大小的 HashMap 与填充默认大小的 HashMap 所需时间的比较

    客户端模式 服务器模式 预先设置的大小 100% 100% 默认大小 294% 157%

    使用负载因子 为确定何时调整大小, 而不是对每个存储桶中的链接列表的深度进行记数, 基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。 Map 在调整大小之前,使用名为 “负载因子 ”的参数指示 Map 将承担的 “负载 ”量,即它的负载程度。 负载因子、项数( Map 大小)与容量之间的关系简单明了:

    如果(负载因子) x(容量) >(Map 大小),则调整 Map 大小

    例如,如果默认负载因子为 0.75 ,默认容量为 11,则 11 x 0.75 = 8.25 ,该值向下取整为 8 个元素。 因此,如 果将第 8 个项添加到此 Map ,则该 Map 将自身的大小调整为一个更大的值。 相反,要计算避免调整大小所需的初始容量,用将要添加的项数除以负载因子,并向上取整,例如,

    对于负载因子为 0.75 的 100 个项,应将容量设置为 100/0.75 = 133.33 ,并将结果向上取整为 134(或取 整为 135 以使用奇数)

    奇数个存储桶使 map 能够通过减少冲突数来提高执行效率。 虽然我所做的测试 (关联文件中的 并未表明质数可以始终获得更好的效率,但理想情形是容量取质数。 1.4 版后的某些 Map(如 HashMap 和 LinkedHashMap ,而非Hashtable 或 IdentityHashMap )使用需要 2 的幂容量的哈希函数,但下一个最高 2 的幂容量由这些 Map 计算, 因此您不必亲自计算。

    负载因子本身是空间和时间之间的调整折衷。 较小的负载因子将占用更多的空间,但将降低冲突的可能性,从而将加快访问和更新的速度。 使用大于 0.75 的负载因子可能是不明智的, 而使用大于 1.0 的负载因子肯定是不明知的,这是因为这必定会引发一次冲突。 使用小于 0.50 的负载因子好处并不大,但只要您有效地调整 Map 的大小,应不会对小负载因子造成性能开销,而只会造成内存开销。 但较小的负载因子将意味着如果您未预先调整 Map 的大小,则导致更频繁的调整大小,从而降低性能,因此在调整负载因子时一定要注意这个问题。

    选择适当的 Map

    应使用哪种 Map? 它是否需要同步? 要获得应用程序的最佳性能, 这可能是所面临的两个最重要的问题。 当使用通用 Map 时,调整 Map 大小和选择负载因子涵盖了 Map 调整选项。 以下是一个用于获得最佳 Map 性能的简单方法

    1.将您的所有 Map 变量声明为 Map ,而不是任何具体实现,即不要声明为 HashMap 或 Hashtable , 或任何其他 Map 类实现。

    Map criticalMap = new HashMap(); // 好

    HashMap criticalMap = new HashMap(); // 差

    这使您能够只更改一行代码即可非常轻松地替换任何特定的 Map 实例。

    2.下载 Doug Lea 的 util.concurrent 程序包 (http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html )。 将 ConcurrentHashMap 用作默认 Map 。 当移植到 1.5 版时,将 java.util.concurrent.ConcurrentHashMap 用作您的默认 Map 。 不要将 ConcurrentHashMap 包装 在同步的包装器中,即使它将用于多个线程。 使用默认大小和负载因子。

    3.监测您的应用程序。 如果发现某个 Map 造成瓶颈,则分析造成瓶颈的原因, 并部分或全部更改该 Map 的以下内容: Map 类; Map 大小;负载因子;关键对象 equals() 方法实现。 专用的 Map 的基 本上都需要特殊用途的定制 Map 实现,否则通用 Map 将实现您所需的性能目标。

    Map 选择

    也许您曾期望更复杂的考量,而这实际上是否显得太容易? 好的,让我们慢慢来。 首先,您应使用哪种 Map ? 答案很简单: 不要为您的设计选择任何特定的 Map ,除非实际的设计需要指定一个特殊类型的 Map。 设计时通常不需要选择具体的 Map 实现。 您可能知道自己需要一个 Map ,但不知道使用哪种。 而这恰恰就是使用 Map 接口的意义所在。 直到需要时再选择 Map 实现 — 如果随处使用 “Map”声明的变量,则更改应用程序中任何特殊 Map 的 Map 实现只需要更改一行, 这是一种开销很少的调整选择。 是否要使用默认的 Map 实现? 我很快将谈到这个问题。

    同步 Map

    同步与否有何差别? (对于同步,您既可以使用同步的 Map ,也可以使用Collections.synchronizedMap() 将未同步的 Map 转换为同步的 Map 。 后者使用 “同步的包装器 ”)这是一个异常复杂的选择,完全取决于您如何根据多线程并发访问和更新使用 Map ,同时还需要进行维护方面的考虑。 例如,如果您开始时未并发更新特定 Map ,但它后来更改为并发更新, 情况将如何? 在这种情况下, 很容易在开始时使用一个未同步的 Map,并在后来向应用程序中添加并发更新线程时忘记将此未同步的 Map 更改为同步的 Map。 这将使您的应用程序容易崩溃 (一种要确定和跟踪的最糟糕的错误)。 但如果默认为同步,则将因随之而来的可怕性能而序列化执行多线程应用程序。 看起来,我们需要某种决策树来帮助我们正确选择。

    Doug Lea 是纽约州立大学奥斯威戈分校计算机科学系的教授。 他创建了一组公共领域的程序包(统称 util.concurrent ),该程序包包含许多可以简化高性能并行编程的实用程序类。 这些类中包含两个 Map ,即ConcurrentReaderHashMap 和 ConcurrentHashMap 。 这些 Map 实现是线程安全的,并且不需要对并发访问或 更新进行同步,同时还适用于大多数需要 Map 的情况。 它们还远比同步的 Map(如 Hashtable )或使用同步的包装器更具伸缩性,并且与 HashMap 相比,它们对性能的破坏很小。 util.concurrent 程序包构成了 JSR166 的基础; JSR166 已经开发了一个包含在 Java 1.5 版中的并发实用程序,而 Java 1.5 版将把这些 Map 包含在一个新的 java.util.concurrent 程序包中。

    Processed: 0.016, SQL: 9