ArrayList的subList方法有什么需要注意的地方吗?

典型回答 List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。所以,我们不能把subList方法返回的List强制转换成ArrayList等类,因为他们之间没有继承关系。正如阿里巴巴Java编码规范所说: 另外,视图和原List的修改还需要注意几点,尤其是他们之间的相互影响: 1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。 2、对子List做结构性修改,操作同样会反映到父List上。 3、对父List做结构性修改,会抛出异常ConcurrentModificationException。 结构性修改:在集合中增加或者删除元素 非结构性:在集合中修改某个元素的内容 扩展知识 subList是List接口中定义的一个方法,该方法主要用于返回一个集合中的一段、可以理解为截取一个集合中的部分元素,他的返回值也是一个List。 如以下代码: 1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { List<String> names = new ArrayList<String>() {{ add("Hollis"); add("hollischuang"); add("H"); }}; List subList = names.subList(0, 1); System.out.println(subList); } 以上代码输出结果为: 1 [Hollis] 如果我们改动下代码,将subList的返回值强转成ArrayList试一下: ...

March 22, 2026 · 3 min · santu

ConcurrentHashMap在哪些地方做了并发控制

典型回答 对于JDK1.8来说,如果用一句话来讲的话,ConcurrentHashMap是通过synchronized和CAS自旋保证的线程安全,要想知道ConcurrentHashMap是如何加锁的,就要知道HashMap在哪些地方会导致线程安全问题,如初始化桶数组阶段和设置桶,插入链表,树化等阶段,都会有并发问题。 解决这些问题的前提,就要知道到底有多少线程在对map进行写入操作,这里ConcurrentHashMap通过sizeCtl变量完成,如果其为负数,则说明有多线程在操作,且Math.abs(sizeCtl)即为线程的数目。 初始化桶阶段 如果在此阶段不做并发控制,那么极有可能出现多个线程都去初始化桶的问题,导致内存浪费。所以Map在此处采用自旋操作和CAS操作,如果此时没有线程初始化,则去初始化,否则当前线程让出CPU时间片,等待下一次唤醒,源码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { // 省略 } } finally { sizeCtl = sc; } break; } } put元素阶段 如果hash后发现桶中没有值,则会直接采用CAS插入并且返回 ...

March 22, 2026 · 1 min · santu

HashMap用在并发场景中有什么问题?

这是一个非常典型的面试问题,但是只会出现在1.7及以前的版本,1.8之后就被修复了 典型回答 扩容过程 HashMap在扩容的时候,会将元素插入链表头部,即头插法。如下图,原来是A->B->C,扩容后会变成C->B->A” 如下图所示: 之所以选择使用头插法,是因为JDK的开发者认为,后插入的数据被使用到的概率更高,更容易成为热点数据,而通过头插法把它们放在队列头部,就可以使查询效率更高。 源代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); // 节点直接作为新链表的根节点 e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } } 并发现象 但是,正是由于直接把当前节点作为链表根节点的这种操作,导致了在多线程并发扩容的时候,产生了循环引用的问题。 ...

March 22, 2026 · 3 min · santu

HashMap的hash方法是如何实现的?

典型回答 hash方法的功能是根据Key来定位这个K-V在链表数组中的位置的。也就是hash方法的输入应该是个Object类型的Key,输出应该是个int类型的数组下标。 最简单的话,我们只要调用Object对象的hashCode()方法,该方法会返回一个整数,然后用这个数对HashMap或者HashTable的容量进行取模就行了。只不过,在具体实现上,考虑到效率等问题,HashMap的实现会稍微复杂一点。他的具体实现主要由两个方法int hash(Object k)和int indexFor(int h, int length)来实现的(JDK 1.8中不再单独有indexFor方法,但是在计算具体的table index时也用到了一样的算法逻辑,具体代码可以看putVal方法)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // JDK 1.7中的hash方法 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); } //JDK 1.8中的hash方法 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } hash :该方法主要是将Object转换成一个整型。 ...

March 22, 2026 · 2 min · santu

HashMap的容量设置多少合适?

典型回答 HashMap有扩容机制,就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity。 ✅HashMap是如何扩容的? 所以,如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。 所以,首先可以明确的是,我们建议开发者在创建HashMap的时候指定初始化容量。并且《阿里巴巴开发手册》中也是这么建议的: 那么,既然建议我们集合初始化的时候,要指定初始值大小,那么我们创建HashMap的时候,到底指定多少合适呢? 有些人会自然想到,我准备塞多少个元素我就设置成多少呗。比如我准备塞7个元素,那就new HashMap(7)。 但是,这么做不仅不对,而且以上方式创建出来的Map的容量也不是7。 因为,当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初识容量。 JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。 也就是说,当我们new HashMap(7)创建HashMap的时候,JDK会通过计算,帮我们创建一个容量为8的Map;当我们new HashMap(9)创建HashMap的时候,JDK会通过计算,帮我们创建一个容量为16的Map。 但是,这个值看似合理,实际上并不尽然。因为HashMap在根据用户传入的capacity计算得到的默认容量,并没有考虑到loadFactor这个因素,只是简单机械的计算出第一个大于这个数字的2的幂。 loadFactor是负载因子,当HashMap中的元素个数(size)超过 threshold = loadFactor * capacity时,就会进行扩容。 也就是说,如果我们设置的默认值是7,经过JDK处理之后,HashMap的容量会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。 那么,到底设置成什么值比较合理呢? 这里我们可以参考JDK8中putAll方法中的实现的,这个实现在guava(21.0版本)也被采用。并且在阿里巴巴Java开发手册中也有这样的规定 即:**<font style="color:#DF2A3F;">return (int) ((float) expectedSize / 0.75F + 1.0F);</font>** 比如我们计划向HashMap中放入7个元素的时候,我们通过expectedSize / 0.75F + 1.0F计算,7/0.75 + 1 = 10 ,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。 当HashMap内部维护的哈希表的容量达到75%时(默认情况下),会触发rehash,而rehash的过程是比较耗费时间的。所以初始化容量要设置成expectedSize/0.75 + 1的话,可以有效的减少冲突也可以减小误差。(大家结合这个公式,好好理解下这句话) 所以,我们可以认为,当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。 ...

March 22, 2026 · 1 min · santu

HashMap的数据结构是怎样的?

HashMap 的数据结构 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。 常用的哈希函数的冲突解决办法中有一种方法叫做链地址法,其实就是将数组和链表组合在一起,发挥了两者的优势,我们可以将其理解为链表的数组。 在JDK 1.8之前,HashMap就是通过这种数组+链表结构来存储数据的。而在JDK 1.8之后,采用的是数组+链表+红黑树的复合结构。 我们可以从上图看到,左边很明显是个数组,数组的每个成员是一个链表。该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。 我们根据元素的自身特征把元素分配到不同的链表中去,反过来我们也正是通过这些特征找到正确的链表,再从链表中找出正确的元素。其中,根据元素特征计算元素数组下标的方法就是哈希算法,即hash()函数(当然,还包括indexOf()函数)。 ✅HashMap的hash方法是如何实现的? 在JDK 1.8中为了解决因hash冲突导致某个链表长度过长,影响put和get的效率,引入了红黑树。 ✅为什么在JDK8中HashMap要转成红黑树

March 22, 2026 · 1 min · santu

hash冲突通常怎么解决?

典型回答 所谓hash冲突,是指在使用哈希函数时,两个或多个不同的输入值(key)被映射到了相同的哈希值(Hash Value)的情况。 打个比方,如果有n个盒子和n+1个球,那么至少有一个盒子里有两个球。同样,如果哈希函数产生的哈希值范围是有限的(比如0到m-1,共m个值),而我们要存储的数据有n个,当n>m时,必然会有至少两个不同的数据产生相同的哈希值,即冲突。 常见的hash冲突的解决有5种方法(具体过程和原理在后面扩展知识中讲): 开放定址法 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。 常见的开放寻址技术有线性探测、二次探测和双重散列。 这种方法的缺点是可能导致“聚集”问题,降低哈希表的性能。 链地址法 最常用的解决哈希冲突的方法之一。 每个哈希桶(bucket)指向一个链表。当发生冲突时,新的元素将被添加到这个链表的末尾。 在Java中,HashMap就是通过这种方式来解决哈希冲突的。Java 8之前,HashMap使用链表来实现;从Java 8开始,当链表长度超过一定阈值时,链表会转换为红黑树,以提高搜索效率。 再哈希法 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。 这种方法需要额外的计算,但可以有效降低冲突率。 建立公共溢出区 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。 一致性hash 主要用于分布式系统中,如分布式缓存。它通过将数据均匀分布到多个节点上来减少冲突。 ✅什么是一致性哈希? 扩展知识 链地址法 HashMap采用该方法,当出现hash冲突的时候,会使同一个hash的所有值形成一个链表。查询的时候,首先通过hash定位到该链表,然后再遍历链表获得结果。 此时,对于hash(5)和hash(6)的冲突来说,则会在hash表的第4个节点形成链表,如:hash[3]->5->6 优点: 处理冲突简单 适合经常插入和删除的情况下 适合没有预定空间的情况 缺点 当冲突较多的时候,查询复杂度趋近于O(n) 开放定址法 开放定址法是解决哈希表中哈希冲突的一种方法。与链地址法不同,开放寻址法在哈希表本身的数组中寻找空闲位置来存储冲突的元素。这种方法的关键在于,当一个新的键通过哈希函数定位到一个已被占用的槽位时,它将探索哈希表的其他槽位,直到找到一个空槽位。 开放寻址法主要有以下几种实现方式: 线性探测(Linear Probing): 当发生冲突时,顺序检查表中的下一个槽位。 如果该槽位也被占用,则继续向下检查,直到找到一个空槽位。 线性探测的问题在于“聚集”:一旦发生多次连续冲突,就会形成一长串被占用的槽位,这会影响后续插入和查找的效率。 如使用大小为7的hash表,依次存储5、8、15、1 二次探测(Quadratic Probing): 使用二次方的序列来探测下一个槽位(例如,1, 4, 9, 16, …)。 这种方法可以减少聚集的问题,但仍然可能存在较小范围的聚集。 同样是依次存储5、8、15、1,当存储到15和1的时候开始冲突,结果如下: 双重散列(Double Hashing): 使用两个不同的哈希函数。 当第一个哈希函数导致冲突时,使用第二个哈希函数来确定探测序列。 这种方法的优点是减少了聚集,并且能更好地分散键的分布。 同样是依次存储5、8、15、1,假设第二个哈希函数为 hash2(key) = 3 - (key % 3)。 ...

March 22, 2026 · 1 min · santu

Java 8中的Stream用过吗?都能干什么?

典型回答 Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。 Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。 这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。 Stream有以下特性及优点: 无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。 为函数式编程而生。对Stream的任何修改都不会修改背后的数据源,比如对Stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新Stream。 惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。 可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。 我们举一个例子,来看一下到底Stream可以做什么事情: 上面的例子中,获取一些带颜色塑料球作为数据源,首先过滤掉红色的、把它们融化成随机的三角形。再过滤器并删除小的三角形。最后计算出剩余图形的周长。 如上图,对于流的处理,主要有三种关键性操作:分别是流的创建、中间操作(intermediate operation)以及最终操作(terminal operation)。 Stream的创建 在Java 8中,可以有多种方法来创建流。 1、通过已有的集合来创建流 在Java 8中,除了增加了很多Stream相关的类以外,还对集合类自身做了增强,在其中增加了stream方法,可以将一个集合类转换成流。 List strings = Arrays.asList(“Hollis”, “HollisChuang”, “hollis”, “Hello”, “HelloWorld”, “Hollis”); Stream stream = strings.stream(); 以上,通过一个已有的List创建一个流。除此以外,还有一个parallelStream方法,可以为集合创建一个并行流。 这种通过集合创建出一个Stream的方式也是比较常用的一种方式。 2、通过Stream创建流 可以使用Stream类提供的方法,直接返回一个由指定元素组成的流。 Stream stream = Stream.of(“Hollis”, “HollisChuang”, “hollis”, “Hello”, “HelloWorld”, “Hollis”); 如以上代码,直接通过of方法,创建并返回一个Stream。 Stream中间操作 Stream有很多中间操作,多个中间操作可以连接起来形成一个流水线,每一个中间操作就像流水线上的一个工人,每人工人都可以对流进行加工,加工后得到的结果还是一个流。 以下是常用的中间操作列表: filter filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤掉空字符串: 1 2 3 List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis"); strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println); //Hollis, HollisChuang, H, hollis map map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数: ...

March 22, 2026 · 2 min · santu

Java中的集合类有哪些?如何分类的?

典型回答 Java的整个集合框架中,主要分为List,Set,Queue,Stack,Map等五种数据结构。其中,前四种数据结构都是单一元素的集合,而最后的Map则是以KV对的形式使用。 从继承关系上讲,List,Set,Queue都是Collection的子接口,Collection又继承了Iterable接口,说明这几种集合都是可以遍历的。 从功能上讲,List代表一个容器,可以是先进先出,也可以是先进后出。而Set相对于List来说,是无序的,同时也是一个去重的列表,既然会去重,就一定会通过equals,compareTo,hashCode等方法进行比较。Map则是KV的映射,也会涉及到Key值的查询等能力。 从实现上讲,List可以有链表实现或者数组实现,两者各有优劣,链表增删快,数组查询快。Queue则可以分为优先队列,双端队列等等。Map则可以分为普通的HashMap和可以排序的TreeMap等等。 知识扩展 Collection和Collections有什么区别? **Collection 是一个集合接口:**它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。是list,set等的父接口。 **Collections 是一个包装类:**它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。 日常开发中,不仅要了解Java中的Collection及其子类的用法,还要了解Collections用法。可以提升很多处理集合类的效率。 Java中的Collection如何遍历迭代? **传统的for循环遍历,基于计数器的:**遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后,停止。主要就是需要按元素的位置来读取元素。 **迭代器遍历,Iterator:**每一个具体实现的数据集合,一般都需要提供相应的Iterator。相比于传统for循环,Iterator取缔了显式的遍历计数器。所以基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现,都是需要保存当前遍历的位置。然后根据当前位置来向前或者向后移动指针。 **foreach循环遍历:**根据反编译的字节码可以发现,foreach内部也是采用了Iterator的方式实现,只不过Java编译器帮我们生成了这些代码。 **迭代器遍历:Enumeration:**Enumeration 接口是Iterator迭代器的“古老版本”,从JDK 1.0开始,Enumeration接口就已经存在了(Iterator从JDK 1.2才出现) **Stream:**JDK 1.8中新增Stream,使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。 Iterable和Iterator如何使用? Iterator和Iterable是两个接口,前者代表的是迭代的方式,如next和hasNext方法就是需要在该接口中实现。后者代表的是是否可以迭代,如果可以迭代,会返回Iterator接口,即返回迭代方式 常见的使用方式一般是集合实现Iterable表明该集合可以遍历,同时选择Iterator或者自定义一个Iterator的实现类去选择遍历方式,如: 1 2 3 4 5 6 7 class AbstractList<E> implements Iterable<E> { public Iterator<E> iterator() { return new Itr(); } private class Itr implements Iterator<E> {} } 为什么不把Iterable和Iterator合成一个使用 通过Javadoc文档我们可以发现,Iterable和Iterator并不是同时出现的,Iterator于1.2就出现了,目的是为了代替Enumeration,而Iterable则是1.5才出现的 将<是否可以迭代>和<迭代方式>抽出来,更符合单一职责原则,如果抽出来,迭代方式就可以被多个可迭代的集合复用,更符合面向对象的特点。

March 22, 2026 · 1 min · santu

Stream的并行流是如何实现的?

典型回答 Java中的Stream API提供了一种高效且易于使用的方式来处理数据集合。其中,Stream的并行流(parallel stream)是一种特别强大的工具,它可以显著提高数据处理的效率,特别是在处理大型数据集时。 1 2 3 4 5 6 7 List<String> list = Arrays.asList("Apple", "Banana", "Cherry", "Date"); // 创建一个串行流 Stream<String> stream = list.stream(); // 创建一个并行流 Stream<String> parallelStream = list.parallelStream(); 使用parallelStream方法就能获取到一个并行流。通过并发运行的方式执行流的迭代及操作。 并行流底层使用了Java 7中引入的Fork/Join框架。这个框架旨在帮助开发者利用多核处理器的并行处理能力。它工作的方式是将一个大任务分割(fork)成多个小任务,这些小任务可以并行执行,然后再将这些小任务的结果合并(join)成最终结果。 ✅ForkJoinPool和ThreadPoolExecutor区别是什么? 我们来看下他的具体实现方式,Stream的reduce方法是用来遍历这个Stream的,看下他的实现,是在ReferencePipeline这个实现类中的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) { return evaluate(ReduceOps.makeRef(accumulator)); } final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) { assert getOutputShape() == terminalOp.inputShape(); if (linkedOrConsumed) throw new IllegalStateException(MSG_STREAM_LINKED); linkedOrConsumed = true; return isParallel() ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags())); } � ...

March 22, 2026 · 2 min · santu

留言给博主