0
点赞
收藏
分享

微信扫一扫

Java-常见面试题收集(三)

云卷云舒xj 03-22 19:30 阅读 2

八 集合

1 List,Set,Map 是否继承自 Collection 接口

  List,Set 接口继承于 Collection 接口
  Map 没有继承于 Collection 接口

2 List,Set,Map 三者的区别

  ① List、Set 都是继承自 Collection 接口,Map 则不是

  ② List 特点:元素有放入顺序,元素可重复 ,Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals()方法 ,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无序,无法用下标来取得想要的值。)

  ③ Set 和 List 对比:
  Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
  List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
  ④ Map 适合储存键值对的数据

  ⑤ 线程安全集合类与非线程安全集合类: LinkedList、ArrayList、HashSet 是非线程安全的,Vector 是线程安全的;HashMap 是非线程安全的,HashTable 是线程安全的;

3 Array 和 ArrayList 的区别

  即数组和 ArrayList 的区别
  ①数组的长度是固定的,集合的长度是可变的(自动拓展容量)。
  ②使用 Java 类封装出一个个容器类,开发者只需要直接调用即可,不用程序员再手写容器类。

4 ArrayList 和 Vector 的区别

  这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的

  ①同步性:
  Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它的
方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线
程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考
虑和编写线程安全的代码。

  ②数据增长:
  ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要
增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多
个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。
即 Vector 增长原来的一倍,ArrayList

5 Arraylist 与 LinkedList 区别

  ① 是否保证线程安全:ArrayList和 LinkedList 都是不同步的,也就是不保证线程安全;
  ② 底层数据结构: Arraylist底层使用的是 Object 数组;LinkedList 底层使用的是双向链表数据结构
  ③ 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index) 方法)。
  ④ 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而
LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

6 数组和 List 之间的转换

  数组转 List:使用 Arrays. asList(array) 进行转换。
  List 转数组:使用 List 自带的 toArray() 方法。

7 为什么重写 equals 时必须重写hashCode 方法

  在 Java 中,当你重写 equals() 方法时,通常也需要重写 hashCode() 方法。这是因为 equals() 和 hashCode() 方法在 Java 的一些集合类(如 HashMap、HashSet 等)中起着至关重要的作用,它们共同维护着对象在这些集合中的唯一性和一致性。

  ① 合同要求:Java 规范中对于 equals() 和 hashCode() 的关系有一个明确的合同要求,即如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象的 hashCode 方法必须产生相同的整数结果。

  ② 性能考虑:在哈希集合中,对象是根据其哈希码分配到不同的桶(bucket)中的。如果两个对象相等(即 equals() 返回 true)但哈希码不同,那么它们可能会被分配到不同的桶中,导致在集合中无法正确地检索或删除这些对象。

  ③ 一致性:如果在对象的状态改变时,equals() 方法的比较逻辑也随之改变,那么 hashCode() 方法也应该相应地改变,以确保哈希集合的正确性。

  因此,为了避免潜在的问题和错误,当你重写 equals() 方法时,通常也需要重写 hashCode() 方法,以确保它们满足上述合同要求,并且在哈希集合中正确地工作。

  一个常见的 hashCode() 实现方式是使用对象的字段来计算哈希码,这些字段在 equals() 方法中也被用于比较。这样可以确保两个相等的对象具有相同的哈希码。

  需要注意的是,虽然重写 equals() 时通常需要重写 hashCode(),但反过来并不总是成立。有些情况下,你可能只需要重写 hashCode() 而不需要重写 equals(),但这通常较少见。

8 Comparable 和 Comparator 的区别

  Comparable 和 Comparator 都是 Java 中用于排序的接口,但它们之间存在明显的区别。以下是它们之间的主要差异:

  Comparable
  定义:Comparable 是一个接口,它允许对象之间进行比较。实现 Comparable 接口的类必须重写 compareTo(Object o) 方法,以定义对象之间的自然排序规则。
  使用场景:当类的自然顺序就是排序顺序时,使用 Comparable。例如,Integer、String 等类都实现了 Comparable 接口,因此可以直接对它们进行排序。
  限制:每个类只能有一个自然排序规则。如果需要对同一个类的实例进行多种不同的排序,那么 Comparable 可能不是最佳选择。

  Comparator
  定义:Comparator 是一个接口,它允许对象按照不同的规则进行比较。实现 Comparator 接口的类必须重写 compare(Object o1, Object o2) 方法,以定义比较逻辑。
  使用场景:当需要为对象定义多种排序规则时,使用 Comparator。你可以为同一个类创建多个不同的 Comparator 实现,每个实现定义一种不同的排序规则。
  灵活性:Comparator 比 Comparable 更灵活,因为它允许你在运行时选择排序规则,而不仅仅是依赖于类的自然顺序。

  示例
  假设有一个 Person 类,包含 name 和 age 两个字段。

  如果你想按照 age 对 Person 对象进行排序,并且这种排序是 Person 类的自然顺序,那么可以让 Person 类实现 Comparable 接口,并在 compareTo 方法中定义基于 age 的比较逻辑。
但是,如果你还想按照 name 对 Person 对象进行排序,或者想同时考虑 name 和 age 进行排序,那么可以创建一个或多个 Comparator 实现,每个实现定义一种不同的排序规则。

  总结
  Comparable 用于定义类的自然排序规则。
  Comparator 用于定义多种不同的排序规则。
  当类的自然顺序就是排序顺序时,使用 Comparable。
  当需要为对象定义多种排序规则时,使用 Comparator。

9 HashMap 和 Hashtable 的区别

  HashMap 是 Hashtable 的轻量级实现(非线程安全的实现),他们都完成了 Map 接口,主要区别在于HashMap 允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable

九 源码分析

1 ArrayList 的拓容机制

  ArrayList的扩容机制是其动态数组实现中的关键部分,当向ArrayList中添加元素而其当前大小不足以容纳新元素时,就会触发扩容操作。以下是ArrayList扩容机制的主要步骤:

  检查容量:当尝试向ArrayList添加新元素时,首先会检查当前ArrayList的容量是否足够。如果当前元素数量已经达到数组的容量上限,就需要进行扩容。

  计算新容量:ArrayList的扩容操作通常会将当前容量增加到原来的1.5倍,这个增长因子可以根据具体的实现有所不同。这种扩容策略可以平衡内存使用和性能,避免频繁扩容带来的性能开销。

  创建新数组:一旦确定了新的容量,ArrayList就会创建一个新的数组,其大小等于计算出的新容量。

  复制元素:接下来,ArrayList会将原数组中的所有元素复制到新数组中。这个复制操作通常使用System.arraycopy()方法或者类似的机制来完成,以确保复制过程的效率。

  更新引用:复制完成后,ArrayList会更新其内部的数组引用,使其指向新的数组,并丢弃旧的数组。这样,ArrayList现在就可以使用具有更大容量的新数组来存储元素了。

  添加新元素:最后,新的元素可以被添加到扩容后的ArrayList中,而不会出现容量不足的问题。

  值得注意的是,ArrayList的初始容量可以通过构造函数来指定。如果没有指定初始容量,那么ArrayList会使用一个默认的初始容量(通常是10)。在后续的扩容过程中,容量会按照上述的扩容策略进行增长。

  另外,需要注意的是,ArrayList的扩容操作可能会导致性能开销,特别是在大量添加元素的情况下。因此,如果可能的话,最好在创建ArrayList时就指定一个合适的初始容量,以减少后续扩容的次数。同时,由于ArrayList在扩容时需要创建新的数组并复制元素,所以这个过程并不是线程安全的。在多线程环境下使用ArrayList时,需要特别注意线程安全问题。

2 HashMap 和 ConcurrentHashMap 的区别

  ① HashMap 不是线程安全的,而 ConcurrentHashMap 是线程安全的。
  ② ConcurrentHashMap 采用锁分段技术,将整个 Hash 桶进行了分段 segment,也就是将这个大的
数组分成了几个小的片段 segment,而且每个小的片段 segment 上面都有锁存在,那么在插入元素的时
候就需要先找到应该插入到哪一个片段 segment,然后再在这个片段上面进行插入,而且这里还需要获取
segment 锁。
  ③ ConcurrentHashMap 让锁的粒度更精细一些,并发性能更好

3 HashMap 的底层原理

  基于 hashing 的原理,jdk8 后采用数组+链表+红黑树的数据结构。我们通过 put和 get 存储和获取对象。当我们给 put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在 bucket 数组中的位置来存储 Entry 对象。当获取对象时,通过 get 获取到 bucket 的位置,再通过键对象的 equals()方法找到正确的键值对,然后在返回值对象。

4 HashMap 中 put 是如何实现的

1.计算关于 key 的 hashcode 值(与 Key.hashCode 的高 16 位做异或运算)
2.如果散列表为空时,调用 resize()初始化散列表
3.如果没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(hashCode 值相同),进行三种判断
4.1:若 key 地址相同或者 equals 后内容相同,则替换旧值
4.2:如果是红黑树结构,就调用树的插入方法
4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之
后判断链表个数是否到达变成红黑树的阙值 8;也可以遍历到有节点与插入元素
的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则 resize 进行扩容

5 HashMap 中什么时候需要进行扩容,扩容 resize()又是如何实现的

  HashMap在以下情况下需要进行扩容:

  当HashMap中的元素数量超过了其容量的0.75倍时,会触发扩容操作。这个阈值通常是HashMap容量的0.75倍,也就是说,当HashMap中的元素数量超过了容量的75%时,就会触发扩容操作。扩容的目的是为了减少哈希冲突,提高查询效率。当HashMap中的元素数量过多时,哈希冲突的概率就会增大,导致查询效率降低。通过扩容,可以使元素分布到更多的桶中,降低哈希冲突的概率,从而提高查询效率。

  当HashMap在put元素时,如果当前数组的容量小于树化的容量的最小值(默认是64),也会触发扩容条件,从而调用resize方法。

  HashMap的扩容操作是通过resize()方法实现的。该方法在初始化或者扩容的时候会调用,主要包含扩容(扩大容量capacity和threshold)及迁移元素两个过程。在扩容时,会创建一个新的数组,其容量是原数组的两倍,然后重新计算原数组中所有元素的哈希值,并将它们放入新的数组。这个过程需要一定的计算资源,因此扩容是一个相对耗时的操作。

  在迁移元素时,会先分裂链表或者红黑树,然后批量迁移。具体的迁移过程可能因Java版本和具体实现而有所不同,但大体上都是将原数组中的元素按照新的哈希值重新分布到新的数组中。

  总之,HashMap的扩容机制是为了保证其在存储大量元素时仍能保持良好的性能。通过合理地调整容量和重新分布元素,HashMap可以在保持高效查询的同时,避免过多的哈希冲突和内存浪费。

6 HashMap 中 get 是如何实现的

  对 key 的 hashCode 进行 hashing,与运算计算下标获取 bucket 位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash 冲突,则利用 equals 方法去遍历链表查找节点。

举报

相关推荐

0 条评论