硬核原创|Java 面试题全梳理(三)

简介: 笔记

讲一下 HashMap put 的过程


首先会使用 hash 函数来计算 key,然后执行真正的插入方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 如果table 为null 或者没有为table分配内存,就resize一次
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  // 指定hash值节点为空则直接插入,这个(n - 1) & hash才是表中真正的哈希
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  // 如果不为空
  else {
    Node<K,V> e; K k;
    // 计算表中的这个真正的哈希值与要插入的key.hash相比
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    // 若不同的话,并且当前节点已经在 TreeNode 上了
    else if (p instanceof TreeNode)
      // 采用红黑树存储方式
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    // key.hash 不同并且也不再 TreeNode 上,在链表上找到 p.next==null
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          // 在表尾插入
          p.next = newNode(hash, key, value, null);
          // 新增节点后如果节点个数到达阈值,则进入 treeifyBin() 进行再次判断
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        // 如果找到了同hash、key的节点,那么直接退出循环
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        // 更新 p 指向下一节点
        p = e;
      }
    }
    // map中含有旧值,返回旧值
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  // map调整次数 + 1
  ++modCount;
  // 键值对的数量达到阈值,需要扩容
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;
}

HashMap put 方法的核心就是在 putval 方法,它的插入过程如下

  • 首先会判断 HashMap 中是否是新构建的,如果是的话会首先进行 resize
  • 然后判断需要插入的元素在 HashMap 中是否已经存在(说明出现了碰撞情况),如果不存在,直接生成新的k-v 节点存放,再判断是否需要扩容。
  • 如果要插入的元素已经存在的话,说明发生了冲突,这就会转换成链表或者红黑树来解决冲突,首先判断链表中的 hash,key 是否相等,如果相等的话,就用新值替换旧值,如果节点是属于 TreeNode 类型,会直接在红黑树中进行处理,如果 hash ,key 不相等也不属于 TreeNode 类型,会直接转换为链表处理,进行链表遍历,如果链表的 next 节点是 null,判断是否转换为红黑树,如果不转换的话,在遍历过程中找到 key 完全相等的节点,则用新节点替换老节点


ConcurrentHashMap 底层实现


ConcurrentHashMap 是线程安全的 Map,它也是高并发场景下的首选数据结构,ConcurrentHashMap 底层是使用分段锁来实现的。


Integer 缓存池


Integer 缓存池也就是 IntegerCache ,它是 Integer 的静态内部类。

50.png

它的默认值用于缓存 -128 - 127 之间的数字,如果有 -128 - 127 之间的数字的话,使用 new Integer 不用创建对象,会直接从缓存池中取,此操作会减少堆中对象的分配,有利于提高程序的运行效率。

例如创建一个 Integer a = 24,其实是调用 Integer 的 valueOf ,可以通过反编译得出这个结论

51.png

然后我们看一下 valueOf 方法

52.png

如果在指定缓存池范围内的话,会直接返回缓存的值而不用创建新的 Integer 对象。

缓存的大小可以使用 XX:AutoBoxCacheMax 来指定,在 VM 初始化时,java.lang.Integer.IntegerCache.high 属性会设置和保存在 sun.misc.VM 的私有系统属性中。


UTF-8 和 Unicode 的关系


由于每个国家都有自己独有的字符编码,所以Unicode 的发展旨在创建一个新的标准,用来映射当今使用的大多数语言中的字符,这些字符有一些不是必要的,但是对于创建文本来说却是不可或缺的。Unicode 统一了所有字符的编码,是一个 Character Set,也就是字符集,字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储,不同的字符其存储空间不一样,有的需要一个字节就能存储,有的则需要2、3、4个字节。

UTF-8 只是众多能够对文本字符进行解码的一种方式,它是一种变长的方式。UTF-8 代表 8 位一组表示 Unicode 字符的格式,使用 1 - 4 个字节来表示字符。

U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

可以看到,UTF-8 通过开头的标志位位数实现了变长。对于单字节字符,只占用一个字节,实现了向下兼容 ASCII,并且能和 UTF-32 一样,包含 Unicode 中的所有字符,又能有效减少存储传输过程中占用的空间。


项目为 UTF-8 环境,char c = '中',是否合法


可以,因为 Unicode 编码采用 2 个字节的编码,UTF-8 是 Unicode 的一种实现,它使用可变长度的字符集进行编码,char c = '中' 是两个字节,所以能够存储。合法。


Arrays.asList 获得的 List 应该注意什么


Arrays.asList 是 Array 中的一个静态方法,它能够实现把数组转换成为 List 序列,需要注意下面几点

  • Arrays.asList 转换完成后的 List 不能再进行结构化的修改,什么是结构化的修改?就是不能再进行任何 List 元素的增加或者减少的操作。
public static void main(String[] args) {
  Integer[] integer = new Integer[] { 1, 2, 3, 4 };
  List integetList = Arrays.asList(integer);
  integetList.add(5);
}

结果会直接抛出

Exception in thread "main" java.lang.UnsupportedOperationException

我们看一下源码就能发现问题

// 这是 java.util.Arrays 的内部类,而不是 java.util.ArrayList 
private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable

继承 AbstractList 中对 add、remove、set 方法是直接抛异常的,也就是说如果继承的子类没有去重写这些方法,那么子类的实例去调用这些方法是会直接抛异常的。

下面是AbstractList中方法的定义,我们可以看到具体抛出的异常:

public void add(int index, E element) {
  throw new UnsupportedOperationException();
}
public E remove(int index) {
  throw new UnsupportedOperationException();
}
public E set(int index, E element) {
  throw new UnsupportedOperationException();
}

虽然 set 方法也抛出了一场,但是由于 内部类 ArrayList 重写了 set 方法,所以支持其可以对元素进行修改。

60.png

  • Arrays.asList 不支持基础类型的转换

Java 中的基础数据类型(byte,short,int,long,float,double,boolean)是不支持使用 Arrays.asList 方法去转换的


Collection 和 Collections 的区别


Collection 和 Collections 都是位于 java.util 包下的类

Collection 是集合类的父类,它是一个顶级接口,大部分抽象类比如说 AbstractListAbstractSet 都继承了 Collection 类,Collection 类只定义一节标准方法比如说 add、remove、set、equals 等,具体的方法由抽象类或者实现类去实现。

Collections 是集合类的工具类,Collections 提供了一些工具类的基本使用

  • sort 方法,对当前集合进行排序, 实现 Comparable 接口的类,只能使用一种排序方案,这种方案叫做自然比较
  • 比如实现线程安全的容器 Collections.synchronizedListCollections.synchronizedMap
  • reverse 反转,使用 reverse 方法可以根据元素的自然顺序 对指定列表按降序进行排序。
  • fill,使用指定元素替换指定列表中的所有元素。

有很多用法,读者可以翻阅 Collections 的源码查看,Collections 不能进行实例化,所以 Collections 中的方法都是由 Collections.方法 直接调用。


你知道 fail-fast 和 fail-safe 吗


fail-fast 是 Java 中的一种快速失败机制,java.util 包下所有的集合都是快速失败的,快速失败会抛出 ConcurrentModificationException 异常,fail-fast 你可以把它理解为一种快速检测机制,它只能用来检测错误,不会对错误进行恢复,fail-fast 不一定只在多线程环境下存在,ArrayList 也会抛出这个异常,主要原因是由于 modCount 不等于 expectedModCount

fail-safe 是 Java 中的一种 安全失败 机制,它表示的是在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException。java.util.concurrent 包下的容器都是安全失败的,可以在多线程条件下使用,并发修改。


ArrayList、LinkedList 和 Vector 的区别


这也是一道老生常谈的问题了

ArrayList、LinkedList、Vector 都是位于 java.util 包下的工具类,它们都实现了 List 接口。

  • ArrayList 的底层是动态数组,它是基于数组的特性而演变出来的,所以ArrayList 遍历访问非常快,但是增删比较慢,因为会涉及到数组的拷贝。ArrayList 是一个非线程安全的容器,在并发场景下会造成问题,如果想使用线程安全的容器的话,推荐使用 Collections.synchronizedList;ArrayList 在扩容时会增加 50% 的容量。
  • LinkedList 的底层是双向链表,所以 LinkedList 的增加和删除非常快,只需把元素删除,把各自的指针指向新的元素即可。但是 LinkedList 遍历比较慢,因为只有每次访问一个元素才能知道下一个元素的值。LinkedList 也是一个非线程安全的容器,推荐使用 Collections.synchronizedList
  • Vector 向量是最早出现的集合容器,Vector 是一个线程安全的容器,它的每个方法都粗暴的加上了 synchronized 锁,所以它的增删、遍历效率都很低。Vector 在扩容时,它的容量会增加一倍。


Exception 和 Error 有什么区别


Exception 泛指的是 异常,Exception 主要分为两种异常,一种是编译期出现的异常,称为 checkedException ,一种是程序运行期间出现的异常,称为 uncheckedException,常见的 checkedException 有 IOException,uncheckedException 统称为 RuntimeException,常见的 RuntimeException 主要有NullPointerExceptionIllegalArgumentExceptionArrayIndexOutofBoundException等,Exception 可以被捕获。

Error 是指程序运行过程中出现的错误,通常情况下会造成程序的崩溃,Error 通常是不可恢复的,Error 不能被捕获。


String、StringBuilder 和 StringBuffer 有什么区别


String 特指的是 Java 中的字符串,String 类位于 java.lang 包下,String 类是由 final 修饰的,String 字符串一旦创建就不能被修改,任何对 String 进行修改的操作都相当于重新创建了一个字符串。String 字符串的底层使用 StringBuilder 来实现的

StringBuilder 位于 java.util 包下,StringBuilder 是一非线程安全的容器,StringBuilder 的 append 方法常用于字符串拼接,它的拼接效率要比 String 中 + 号的拼接效率高。StringBuilder 一般不用于并发环境

StringBuffer 位于 java.util 包下,StringBuffer 是一个线程安全的容器,多线程场景下一般使用 StringBuffer 用作字符串的拼接

StringBuilder 和 StringBuffer 都是继承于AbstractStringBuilder 类,AbstractStringBuilder 类实现了 StringBuffer 和 StringBuilder 的常规操作。

相关文章
|
6月前
|
存储 安全 Java
【Java基础】全网最详细 - 从入门到转行
【Java基础】全网最详细 - 从入门到转行
138 0
|
设计模式 人工智能 Java
探究Java之路——从入门到进阶心得体会
探究Java之路——从入门到进阶心得体会
103 1
|
设计模式 缓存 IDE
Java进阶之路-如何写出高质量的代码——《我的Java打怪日记》
本文提出可读性是高质量代码最重要的特点,然后分析了好代码和坏代码的特点,然后从战略层面给出写出高质量代码的几条建议,最后给出几个Java领域比较出名的开源框架,并给出一些代码实践。
2363 0
|
存储 缓存 算法
全网最硬核Java程序员必备底层知识(一)
全网最硬核Java程序员必备底层知识(一)
全网最硬核Java程序员必备底层知识(一)
|
缓存 NoSQL Dubbo
【Java面试】第一章:P5级面试
【Java面试】第一章:P5级面试
164 0
|
存储 消息中间件 负载均衡
秋招面试题系列- - -Java工程师(十一)
秋招面试题系列- - -Java工程师(十一)