当前位置:网站首页 > Java基础 > 正文

java字符串转map集合(java字符串转decimal)



HashMap 和 Hashtable 的区别

  • 线程是否安全: 是非线程安全的, 是线程安全的,因为 内部的方法基本都经过 修饰。(如果你要保证线程安全的话就使用 吧!);
  • 效率: 因为线程安全的问题, 要比 效率高一点。另外, 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 。
  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值, 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 会直接使用你给定的大小,而 会将其扩充为 2 的幂次方大小( 中的方法保证,下面给出了源代码)。也就是说 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  • 底层数据结构: JDK1.8 以后的 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。 没有这样的机制。
  • 哈希函数的实现: 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而  直接使用键的  值。

 中带有初始容量的构造函数:

 

下面这个方法保证了  总是使用 2 的幂作为哈希表的大小。

 

HashMap 和 HashSet 区别

如果你看过 源码的话就应该知道: 底层就是基于 实现的。( 的源码非常非常少,因为除了 、、是 自己不得不实现之外,其他方法都是直接调用 中的方法。

HashMap 和 TreeMap 区别

和 都继承自 ,但是需要注意的是它还实现了接口和 接口。

 

实现 接口让 有了对集合内元素的搜索的能力。

接口提供了丰富的方法来探索和操作键值对:

  1. 定向搜索: , , 和 等方法可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。
  2. 子集操作: , 和 方法可以高效地创建原集合的子集视图,而无需复制整个集合。
  3. 逆序视图: 方法返回一个逆序的 视图,使得可以反向迭代整个 。
  4. 边界操作: , , 和 等方法可以方便地访问和移除元素。

 

这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log n),这让 成为了处理有序集合搜索问题的强大工具。

实现接口让 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:

 

输出:

 

可以看出, 中的元素已经是按照  的 age 字段的升序来排列了。

上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式:

 

 

综上,相比于来说,  主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet 如何检查重复?

以下内容摘自我的 Java 启蒙书《Head first java》第二版:

当你把对象加入时, 会先计算对象的值来判断对象加入的位置,同时也会与其他加入的对象的 值作比较,如果没有相符的 , 会假设对象没有重复出现。但是如果发现有相同 值的对象,这时会调用方法来检查 相等的对象是否真的相同。如果两者相同, 就不会让加入操作成功。

在 JDK1.8 中,的方法只是简单的调用了的方法,并且判断了一下返回值以确保是否有重复元素。直接看一下中的源码:

 

而在的方法中也能看到如下说明:

 

也就是说,在 JDK1.8 中,实际上无论中是否已经存在了某元素,都会直接插入,只是会在方法的返回值处告诉我们插入前是否存在相同元素。

HashMap 的底层实现

JDK1.8 之前

JDK1.8 之前 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 经过扰动函数处理过后得到 hash 值,然后通过 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

中的扰动函数( 方法)是用来优化哈希值的分布。通过对原始的 进行额外处理,扰动函数可以减小由于糟糕的 实现导致的碰撞,从而提高数据的分布均匀性。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。

 

对比一下 JDK1.7 的 HashMap 的 hash 方法源码.

 

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8 之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

我们来结合源码分析一下 链表到红黑树的转换。

1、  方法中执行链表转红黑树的判断逻辑。

链表的长度大于 8 的时候,就执行  (转换红黑树)的逻辑。

 

2、 方法中判断是否真的转换为红黑树。

 

将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。

HashMap 的长度为什么是 2 的幂次方

为了让 存取高效并减少碰撞,我们需要确保数据尽量均匀分布。哈希值在 Java 中通常使用 表示,其范围是 前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但是,问题是一个 40 亿长度的数组,内存是放不下的。所以,这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。

这个算法应该如何设计呢?

我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 的前提是 length 是 2 的 n 次方)。” 并且,采用二进制位操作 & 相对于 % 能够提高运算效率

除了上面所说的位运算比取余效率高之外,我觉得更重要的一个原因是:长度是 2 的幂次方,可以让 在扩容的时候更均匀。例如:

  • length = 8 时,length - 1 = 7 的二进制位
  • length = 16 时,length - 1 = 15 的二进制位

这时候原本存在 中的元素计算新的数组位置时 ,取决 hash 的第四个二进制位(从右数),会出现两种情况:

  1. 第四个二进制位为 0,数组位置不变,也就是说当前元素在新数组和旧数组的位置相同。
  2. 第四个二进制位为 1,数组位置在新数组扩容之后的那一部分。

这里列举一个例子:

 

⚠️注意:这里列举的场景看的是第四个二进制位,更准确点来说看的是高位(从右数),例如 时,,二进制为 ,这里看的就是第五个二进制位。

也就是说扩容之后,在旧数组元素 hash 值比较均匀(至于 hash 值均不均匀,取决于前面讲的对象的 方法和扰动函数)的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。

这样也使得扩容机制变得简单和高效,扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。

最后,简单总结一下 的长度是 2 的幂次方的原因:

  1. 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时, 等价于 。
  1. 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
  2. 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。

HashMap 多线程操作导致死循环问题

JDK1.7 及之前版本的 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 ,因为多线程下使用 还是会存在数据覆盖的问题。并发环境下,推荐使用 。

一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。如果想要详细了解 扩容导致死循环问题,可以看看耗子叔的这篇文章:Java HashMap 的死循环。

HashMap 为什么线程不安全?

JDK1.7 及之前版本,在多线程环境下, 扩容时会造成死循环和数据丢失的问题。

数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。

JDK 1.8 后,在 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 的 操作会导致线程不安全,具体来说会有数据覆盖的风险。

举个例子:

  • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
  • 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
  • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。
 

还有一种情况是这两个线程同时 操作导致 的值不正确,进而导致数据覆盖的问题:

  1. 线程 1 执行 判断时,假设获得 的值为 10,由于时间片耗尽挂起。
  2. 线程 2 也执行 判断,获得 的值也为 10,并将元素插入到该桶位中,并将 的值更新为 11。
  3. 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。
  4. 线程 1、2 都执行了一次 操作,但是 的值只增加了 1,也就导致实际上只有一个元素被添加到了 中。
 

HashMap 常见的遍历方式?

HashMap 的 7 种遍历方式与性能分析!

🐛 修正(参见:issue#1411)

这篇文章对于 parallelStream 遍历方式的性能分析有误,先说结论:存在阻塞时 parallelStream 性能最高, 非阻塞时 parallelStream 性能最低

当遍历不存在阻塞时, parallelStream 的性能是最低的:

 

加入阻塞代码后, parallelStream 的性能才是最高的:

 

ConcurrentHashMap 和 Hashtable 的区别

和 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构: JDK1.7 的 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 的结构一样,数组+链表/红黑二叉树。 和 JDK1.8 之前的 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要):

  • 在 JDK1.7 的时候, 对整个桶数组进行了分割分段(,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
  • 到了 JDK1.8 的时候, 已经摒弃了 的概念,而是直接用 数组+链表+红黑树的数据结构来实现,并发控制使用 和 CAS 来操作。(JDK1.6 以后 锁做了很多优化) 整个看起来就像是优化过且线程安全的 ,虽然在 JDK1.8 中还能看到 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    • (同一把锁) :使用 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

下面,我们再来看看两者底层数据结构的对比图。

 

是由 数组结构和 数组结构组成。

数组中的每个元素包含一个 数组,每个 数组属于链表结构。

JDK1.8 的 ConcurrentHashMap

JDK1.8 的 不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 。当冲突链表达到一定长度时,链表会转换成红黑树。

是存储红黑树节点,被包装。通过属性维护红黑树的根结点,因为红黑树在旋转的时候,根结点可能会被它原来的子节点替换掉,在这个时间点,如果有其他线程要写这棵红黑树就会发生线程不安全问题,所以在 中通过属性维护当前使用这棵红黑树的线程,来防止其他线程的进入。

 

首先将数据分为一段一段(这个“段”就是 )的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

是由 数组结构和 数组结构组成

继承了 ,所以 是一种可重入锁,扮演锁的角色。 用于存储键值对数据。

 

一个 里包含一个 数组, 的个数一旦初始化就不能改变。 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。

的结构和 类似,是一种数组和链表结构,一个 包含一个 数组,每个 是一个链表结构的元素,每个 守护着一个 数组里的元素,当对 数组的数据进行修改时,必须首先获得对应的 的锁。也就是说,对同一 的并发写入会被阻塞,不同 的写入是可以并发执行的。

JDK1.8 之后

Java 8 几乎完全重写了 ,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。

取消了 分段锁,采用 来保证并发安全。数据结构跟 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

Java 8 中,锁粒度更细, 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式:JDK 1.7 采用 分段锁来保证安全, 是继承自 。JDK1.8 放弃了 分段锁的设计,采用 保证线程安全,锁粒度更细, 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

ConcurrentHashMap 为什么 key 和 value 不能为 null?

的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 中的,还是因为找不到对应的键而返回的。

拿 get 方法取值来说,返回的结果为 null 存在两种情况:

  • 值没有在集合中 ;
  • 值本身就是 null。

这也就是二义性的由来。

具体可以参考 ConcurrentHashMap 源码分析 。

多线程环境下,存在一个线程操作该 时,其他的线程将该 修改的情况,所以无法通过 来判断否存在这个键值对,也就没办法解决二义性问题了。

与此形成对比的是, 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 修改的情况,所以可以通过 来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。

也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。

如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。

 

最后,再分享一下 作者本人 (Doug Lea)对于这个问题的回答:

The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if returns , you can't detect whether the key explicitly maps to vs the key isn't mapped. In a non-concurrent map, you can check this via , but in a concurrent one, the map might have changed between calls.

翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。

ConcurrentHashMap 能保证复合操作的原子性吗?

是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!

复合操作是指由多个基本操作(如、、、等)组成的操作,例如先判断某个键是否存在,然后根据结果进行插入或更新。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。

例如,有两个线程 A 和 B 同时对 进行复合操作,如下:

 

如果线程 A 和 B 的执行顺序是这样:

  1. 线程 A 判断 map 中不存在 key
  2. 线程 B 判断 map 中不存在 key
  3. 线程 B 将 (key, anotherValue) 插入 map
  4. 线程 A 将 (key, value) 插入 map

那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。

那如何保证 复合操作的原子性呢?

提供了一些原子性的复合操作,如 、、 、、等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。

上面的代码可以改写为:

 

或者:

 

很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 的初衷。在使用 的时候,尽量使用这些原子性的复合操作方法来保证原子性。

 工具类常用方法:

  • 排序
  • 查找,替换操作
  • 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

排序操作

 



查找,替换操作

 

同步控制

提供了多个方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

我们知道 ,,,,, 都是线程不安全的。 提供了多个静态方法可以把他们包装成线程同步的集合。

最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。

方法如下:

 

到此这篇java字符串转map集合(java字符串转decimal)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • java dateutils工具类(java中dateformat类)2025-04-05 14:54:09
  • yarn命令查看队列资源(java查看yarn队列信息)2025-04-05 14:54:09
  • java spring菜鸟教程(javascrip菜鸟教程)2025-04-05 14:54:09
  • python爬虫和java爬虫性能比较(java爬虫和java后端相比)2025-04-05 14:54:09
  • java面试基础知识点(java面试题基础知识)2025-04-05 14:54:09
  • java爬虫入门教程(java爬虫教学)2025-04-05 14:54:09
  • list转string用逗号隔开java(list<string>转list<integer>)2025-04-05 14:54:09
  • javajvm内存模型(jvm内存模型 知乎)2025-04-05 14:54:09
  • java面试八股文都是什么(java中的八股文)2025-04-05 14:54:09
  • java爬虫框架使用排行(java爬虫框架webmagic)2025-04-05 14:54:09
  • 全屏图片