求java里面的Hash<Map>的用法和基本解释,谢谢
hash是一种算法 就是数据结构中的散列表 既是一种查找方法,也是数据存储方法,例如hashmap hashset
总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且hash_map的构造速度较慢。
现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用。
通过 HashMap、HashSet 的源代码分析其 Hash 存储机制
实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对。
在介绍集合存储之前需要指出一点:虽然集合号称存储的是 Java 对象,但实际上并不会真正将 Java 对象放入 Set 集合中,只是在 Set 集合中保留这些对象的引用而言。也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。
集合和引用
就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象的引用放入数组中,每个数组元素都是一个引用变量。
HashMap 的存储实现
当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
Java代码
HashMap<String , Double> map = new HashMap<String , Double>();
map.put("语文" , 80.0);
map.put("数学" , 89.0);
map.put("英语" , 78.2);
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。
当程序执行 map.put("语文" , 80.0); 时,系统将调用"语文"的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。
我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:
Java代码
public V put(K key, V value)
{
// 如果 key 为 null,调用 putForNullKey 方法进行处理
if (key == null)
return putForNullKey(value);
// 根据 key 的 keyCode 计算 Hash 值
int hash = hash(key.hashCode());
// 搜索指定 hash 值在对应 table 中的索引
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素
for (Entry<K,V> e = table[i]; e != null; e = e.next)
{
Object k;
// 找到指定 key 与需要放入的 key 相等(hash 值相同
// 通过 equals 比较放回 true)
if (e.hash == hash && ((k = e.key) == key
|| key.equals(k)))
{
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry
modCount++;
// 将 key、value 添加到 i 索引处
addEntry(hash, key, value, i);
return null;
}
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:
Java代码
static int hash(int h)
{
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
Java代码
static int indexFor(int h, int length)
{
return h & (length-1);
}
这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,这一点可参看后面关于 HashMap 构造器的介绍。
当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
根据上面 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 链的头部——具体说明继续看 addEntry() 方法的说明。
当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。
上面程序中还调用了 addEntry(hash, key, value, i); 代码,其中 addEntry 是 HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:
Java代码
void addEntry(int hash, K key, V value, int bucketIndex)
{
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex]; // ①
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
// 把 table 对象的长度扩充到 2 倍。
resize(2 * table.length); // ②
}
上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
JDK 源码
在 JDK 安装目录下可以找到一个 src.zip 压缩文件,该文件里包含了 Java 基础类库的所有源文件。只要读者有学习兴趣,随时可以打开这份压缩文件来阅读 Java 类库的源代码,这对提高读者的编程能力是非常有帮助的。需要指出的是:src.zip 中包含的源代码并没有包含像上文中的中文注释,这些注释是笔者自己添加进去的。
Hash 算法的性能选项
根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。
上面程序中还有这样两个变量:
* size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
* threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。
从上面程序中②号代码可以看出,当 size++ >= threshold 时,HashMap 会自动调用 resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。
上面程序中使用的 table 其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是 HashMap 的容量。HashMap 包含如下几个构造器:
* HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
* HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
* HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
当创建一个 HashMap 时,系统会自动创建一个 table 数组来保存 HashMap 中的 Entry,下面是 HashMap 中一个构造器的代码:
Java代码
// 以指定初始化容量、负载因子创建 HashMap
public HashMap(int initialCapacity, float loadFactor)
{
// 初始容量不能为负数
if (initialCapacity < 0)
throw new IllegalArgumentException(
"Illegal initial capacity: " +
initialCapacity);
// 如果初始容量大于最大容量,让出示容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子必须大于 0 的数值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(
loadFactor);
// 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// 设置容量极限等于容量 * 负载因子
threshold = (int)(capacity * loadFactor);
// 初始化 table 数组
table = new Entry[capacity]; // ①
init();
}
上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。
程序①号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。
对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:
图 1. HashMap 的存储示意
HashMap 的读取实现
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:
Java代码
public V get(Object key)
{
// 如果 key 是 null,调用 getForNullKey 取出对应的 value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 直接取出 table 数组中指定索引处的值,
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
// 搜索该 Entry 链的下一个 Entr
e = e.next) // ①
{
Object k;
// 如果该 Entry 的 key 与被搜索 key 相同
if (e.hash == hash && ((k = e.key) == key
|| key.equals(k)))
return e.value;
}
return null;
}
从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 Hash 算法来决定其存储位置;当需要取出一个 Entry 时,也会根据 Hash 算法找到其存储位置,直接取出该 Entry。由此可见:HashMap 之所以能快速存、取它所包含的 Entry,完全类似于现实生活中母亲从小教我们的:不同的东西要放在不同的位置,需要时才能快速找到它。
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。
如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。
如何在Android平台上使用JS直接调用Java方法
setIcon(R.drawable.icon);alertDialog.show();} });} } 在js中调用 jsb.reflection.callStaticMethod("org\/cocos2dx\/javascript\/AppActivity", "showAlertDialog", "(Ljava\/lang\/String;Ljava\/lang\/String;)V", "title", "hahahahha");这样调用就可以看到一个android原生的Alert对话框了。
Java培训需要多久学习哪些内容
韩同学感悟:Java+分布式开发学员 先借用罗翔的一句话:人生唯一确定的,就是不确定的人生。 本科我学的是计算机相关的专业,第一年考研时,班里正经考研的约有...1、Hadoop (Hadoop基础和环境搭建,HDFS体系结构,MapReduce;Hadoop的集群模式、HDFS联盟,利用ZooKeeper来实现Hadoop集群的HA(高可用性)功能2、Yarn的任务调度...
java编程培训学什么?
每个培训机构的课程都有一定的区别,并且根据行业变化和企业用人需求,有些靠谱的培训机构也会对课程进行实时更新。这里就给题主发一份蜗牛学院java全栈开发的学习路线:第一阶段:Java专业基础课程 阶段目标:1. 熟练掌握Java的开发环境与编程核心知识 2. 熟练运用Java面向对象知识进行程序开发 3. 对Java...
做大数据分析一般用什么工具呢?
并写到各种数据接受方(比如Kafka)的。Spark:它是用来弥补基于MapReduce处理数据速度上的缺点,它的特点是把数据装载到内存中计算而不是去读慢的要死进化还特别慢的硬盘。特别适合做迭代运算,所以算法流们特别稀饭它。它是用scala编写的。Java语言或者Scala都可以操作它,因为它们都是用JVM的。
PostgreSQL 实例备份和恢复工具 - pg_probackup
在备份和恢复过程中,可能出现的常见错误及解决方法包括: 备份超时失败:检查 WAL 文件归档速度和相关系统资源。 基于时间点还原后服务启动报错:确保还原时间点与实例状态兼容,避免还原到不一致状态。 Java 执行 pg_probackup 命令返回信息输出到 stderr:修改错误识别逻辑,避免误判执行异常。
Java就业培训教程的目录
2 变量及变量的作用域2.3 函数与函数的重载2.4 Java中的运算符2.5 程序的流程控制2.6 数组第三章 面向对象(上)3.1 面向对象的概念3.2 类与对象3.3 构造函数3.4 this引用句柄3.5 与垃圾回收有关的知识3.6 函数的参数传递3.7 Static关键字3.8 内部类3.9 使用Java的文档注释第四章...
电脑里常用的的英语有那些(汉语意思)?
Median and Selection 中位数 Generating Permutations 排列生成 Generating Subsets 子集生成 Generating Partitions 划分生成 Generating Graphs 图的生成 Calendrical Calculations 日期 Job Scheduling 工程安排 Satisfiability 可满足性 Graph Problems -- polynomial 图论-多项式算法 Connected Components 连通分支 T...
elasticsearch 在大数据中能实现哪些功能
后来,我们曾考虑过sphinx,并且sphinx也在之前的项目中成功实施过,但想想现在的数据量级,多台MySQL,以及搜索服务本身HA,还有后续扩容的问题,我们觉得sphinx并不是一个最优的选择。于是自然将目光放到了Elasticsearch上面。根据官网自己的介绍,Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard...
昌平java课程培训机构分享大数据学习都需要掌握哪些知识
接着再来了解大数据需要掌握那些技术 Hadoop核心 (1)分布式存储基石:HDFS HDFS简介入门演示构成及工作原理解析:数据块,NameNode,DataNode、数据写入与读取过程、数据复制、HA方案、文件类型、HDFS常用设置JavaAPI代码演示 (2)分布式计算基础:MapReduce MapReduce简介、编程模型、JavaAPI介绍、编程案例介绍、...
大数据的课程都有哪些?
大数据本身属于交叉学科,涵盖计算机、统计学、数学三个学科的专业知识。所以大数据的课程内容,基本上也是围绕着三个学科展开的。数理统计方面:数学分析、统计学习、高等代数、离散数学、概率与统计等课程是基本配置。计算机专业课程:数据结构、数据科学、程序设计、算法分析与设计、数据计算智能、数据库系统、...
段寒可成: 一、在Object类中的定义为:public native int hashCode(); 是一个本地方法,返回的对象的地址值.但是,同样的思路,在String等封装类中对此方法进行了重写.方法调用得到一个计算公式得到的 int值.二、在重写任何类得hashcode方法...
居巢区17258021715: java中HashSet怎样遍历取出里面的值?给个例子~~ - ?
段寒可成: HashSet<String> set=new HashSet<String>(); set.add("1"); set.add("2"); set.add("3"); Iterator<String> iterator=set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } 控制台输出:321 对于集合这块的类都可以这样写的,比如你把第一行换成:ArrayList<String> set=new ArrayList<String>(); 后面的程序不动,一样可以遍历输出的.不太懂的话可以百度Iterator的用法和原理
居巢区17258021715: java判断一句话中的字母个数?
段寒可成: public class Test { public HashMap<String, Integer> getcount(String content) { String chars = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"; String[] char_arry_tolower = chars.split(","); List<String> list = new ArrayList<String>(); for ...
居巢区17258021715: java中HashSet集合的一个小问题,求大神! - ?
段寒可成: HashSet 有一个toArray()的方法可以直接把HashSet转成数组,再通过数组取下标就行.如 HashSet<String> hs=new HashSet<String>(); hs.add("!231231"); hs.add("asdfasdfasd"); hs.add("Dfdfsda"); System.out.println(hs.toArray()[2]);
居巢区17258021715: 下面是用java写的一个小程序,求100内的素数,请大虾解释两个return: - ?
段寒可成: 如果return false之后就跳出 isPrime这个函数,不会到return true那了..
居巢区17258021715: 请大神们在下面求最大公约数和最小公倍数的java程序中加入一个异常类,简单点就好,谢谢 - ?
段寒可成: try{//执行的代码,其中可能有异常.一旦发现异常,则立即跳到catch执行.否则不会执行catch里面的内容 }cacth{//除非try里面执行代码发生了异常,否则这里的代码不会执行 }finally{//不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally }
居巢区17258021715: Java中的Hashtable如何根据值获取键? - ?
段寒可成: 没办法使用value,获取key.因为Hashtable被设计成一对多的关系,也就是说不同key,可以有相同的值
居巢区17258021715: java程序读一个文本文件并用hashmap进行存储,并对其中的信息按照姓名排序 - ?
段寒可成: 给你发一个完整的吧,反正我电脑上有,/*** 存储关联的键值对* @param key:键* @param value:值* @return*/public V put(K key, V value) {//当键值为null时,调用putForNullKey(value)的方法存储,//在该方法中调用recordAccess(...
居巢区17258021715: 急 ~~请求 编程循环语句杨辉倒三角?
段寒可成: #include<stdio.h> #define M 10 void main() { int a[M][M], i , j ; for(i=0;i<M;i++) for(j=0;j<=i;j++) { if(i==j||j==0) a[i][j]=1; else a[i][j]=a[i-1][j]+a[i-1][j-1]; printf("%5d",a[i][j]); if(i==j)printf("\n"); } }
居巢区17258021715: 编程求解约瑟夫问题?
段寒可成: /*编程求解约瑟夫问题*/ #include<stdio.h> #define M 8 void squeeze(int a[],int n)/*从字符串a中删除与n相同的字符并压缩字符串a*/ { int i=0,j=0; for(;a[i]!='\0';i++) if(a[i]!=n)a[j++]=a[i]; a[j]='\0'; } void main() { int i,j,k,N,n; static int a[M],b[M]; for(i=0;i...