面试问题java版-Ⅰ

Java 基础、字符串与集合高频问题整理

Posted by Ekko on July 28, 2020

这篇笔记围绕 Java 面试中的高频基础题展开,覆盖基本类型、值传递、字符串、集合、异常体系与容器实现等内容。整理目标不是只给结论,而是把容易混淆的边界条件和底层机制一起说明清楚。

原始问题主要整理自掘金等资料,这里在原有基础上补充了 JDK 版本差异、典型误区与使用注意点,适合作为面试复习时的回查笔记。

[TOC]


Java 基本类型

  • byte、1 字节,最小值-128(-2^7),最大值127(2^7-1)
  • short、2 字节,最小值-32768(-2^15),最大值32767(2^15 - 1)
  • int、4 字节,最小值是 -2,147,483,648(-2^31),最大值是 2,147, 483,647(2^31 - 1)
  • long、8字节,最小值是 -9,223,372,036,854,775,808(-2^63), 最大值是 9,223,372,036,854,775,807(2^63 -1)
  • double、8字节,双精度浮点数,位宽 64;尾数位 52,指数位 11,指数偏移量 1023
  • float、4字节,单精度浮点数,位宽 32;尾数位 23,指数位 8,指数偏移量 127
  • boolean、在 Java 语言规范层面只规定取值为 true / false,并没有像 byte、int 那样规定精确的存储字节数;在数组或对象中的实际布局由 JVM 实现决定
  • char、2 字节,最小值\u0000(即为0),最大值\uffff(即为65,535)

值传递和引用传递的区别

  1. 按值调用(call by value):表示方法接收的是调用者提供的值

  2. 按引用调用(call by reference):方法接收的是调用者提供的变量地址(如果是C语言的话来说就是指针,当然java并没有指针的概念)

根本区别:方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值

java程序设计语言确实是采用了按值调用,即call by value。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。但是java区分基本数据类型和引用数据类型(对象引用),而如果传递的是引用数据类型(对象),引用类型对应的值是可以被修改的。比如调用 user 对象的 set方法,这里涉及到内存概念。方法拷贝对象的引用进行传递,但是这两个引用指向的是同一块地址,所以拷贝的引用是可以修改原引用对象所对应的值

更多详细内容请参考博客


== 和 equals 区别是什么

== 比较运算符,如果比较的是基本类型:

  • 对于数值类型,会发生自动类型提升,值相同则返回 true
  • 对于 boolean,比较的是布尔值本身
1
2
3
int num = 2;
double num2 = 2.0;
System.out.println(num == num2); // true

如果比较的是引用类型,那么两个引用必须指向同一个对象,也就是比较两个变量保存的地址值是否相同

equals Object.equals()方法默认实现就是返回两个对象 == 的比较结果(指向的内存地址)

但是很多对象重写了equals方法,比如String、Date、Integer等,那么比较是所指向对象的内容。这要根据对象是否重写equals判断

  • equals在java中是逻辑相等
  • hashCode相等的两个对象equals不一定为true,但是equals为true的对象,hashCode值必须相等

String 中的 equals 方法是如何重写的

源码解读(JDK 8 版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean equals(Object anObject) {
    if (this == anObject) { // 如果指向的是同一块地址,那么直接返回true(同一个对象)
        return true;
    }
    if (anObject instanceof String) { // 先判断对象类型
        String anotherString = (String)anObject;
        // 如果长度相等才进行比较
        int n = value.length;
        // 在String中value的定义private final char value[];
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 通过遍历比较每个下标元素是否相等,char基本类型,所以是区分大小写的
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

核心逻辑可以概括为三步:

  • 先判断是否为同一对象,是则直接返回 true
  • 再判断参数是否为 String
  • 最后比较底层字符序列的长度与逐个字符内容

补充:JDK 9 之后 String 的底层实现由 char[] 调整为 byte[] + coder(Compact Strings),但 equals() 的语义没有变化,依然是比较字符串内容是否相同。


为什么要重写 equals、hashCode 方法

先看下Object类的equals方法的注释

1
2
3
4
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.

其中规定相等的对象必须具有相等的hash值

重写equals、hashCode方法,和hash表的数据结构有关,特别是在使用hashMap的时候,因为java hashMap是通过链地址法+红黑树解决hash值冲突。即同一个hash值下,会挂多个不同对象。如果向hashMap插入自定义对象的时候,可能会出现预想不到的结果。为了使两个逻辑相等的对象拥有相同的hashCode值,有必要重写hashCode()方法,同时equals默认比较的是对象的地址,所以也需要重写


String s1 = new String(“abc”)、String s2 = “abc”、s1 == s2 。语句1在内存中创建了几个对象

这个问题的关键在于:字符串常量池里 "abc" 是否已经存在

1
2
3
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1 == s2); // false
  • 执行 new String("abc") 时,new 一定会在堆上创建一个新的 String 对象
  • 同时,字面量 "abc" 会尝试进入字符串常量池;如果池中原本没有它,则还会额外创建一个池中对象
  • 执行 String s2 = "abc" 时,不会再新建堆对象,而是直接让 s2 指向常量池中的 "abc"

因此:

  • 如果常量池中原本没有 "abc",那么语句 String s1 = new String("abc") 会创建 2 个对象:一个堆对象、一个常量池对象
  • 如果常量池中已经有 "abc",那么只会创建 1 个对象:新的堆对象

s1 == s2false,是因为 s1 指向堆中的新对象,而 s2 指向常量池中的对象,二者不是同一个引用


String 为什么是不可变的,为什么这么设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        ...

        public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
}

在 Java 中,如果一个对象创建后其可观察状态不能再变化,就可以认为它是不可变对象。对 String 而言,不可变性主要体现在:一旦构造完成,后续对字符串的“修改”操作本质上都是返回新对象,而不是原地修改。

误区:对象和对象的引用

1
2
String s = "abc";
s = "123";

s 是对象引用,"abc" 才是字符串对象本身。上面的赋值只是让引用 s 指向了另一个对象,而不是修改了原来的 "abc"

从实现上看,JDK 8 中 String 底层是 final char[],JDK 9 之后改为 byte[] + coder,但设计原则一致:对外暴露的是不可变语义。replace()substring()concat() 等方法返回的都是新字符串。

这样设计的主要目的有:

  • 安全性:字符串广泛用于类加载、网络地址、文件路径、数据库连接参数等场景,不可变可以避免被悄悄篡改
  • 线程安全:不可变对象天然更容易在多线程间共享
  • 字符串常量池优化:内容不可变,池化复用才成立
  • 缓存 hashCodeString 的哈希值可以安全缓存,减少重复计算

请描述一下 static 关键字和 final 关键字的用法

static: 表示全局或者静态,可以修饰

  • 属性
  • 方法
  • 内部类
  1. static修饰属性和方法:不被某个对象拥有,而是成为类对象,被该类对象所有的实例对象共享
  2. static修饰块:永远只会被调用一次,和对象创建个数无关(如果是实例块的话,创建一次,就被调用一次)。一个类可以创建多个静态块,且被顺序执行
1
2
3
4
5
6
7
8
static{
    静态块
}

{
    实例块
}

  1. static只可以修饰内部类(静态内部类)

final: 表示最终,可以修饰

  • 属性
  • 方法
  • 形参
  1. 修饰类:被 final 修饰的类不能被继承,因此 finalabstract 不能同时出现在同一个类上;接口也不能使用 final 修饰
  2. 修饰属性:必须赋初始值,即使没有初始值,那么在构造方法中必须被赋值,一旦赋值后不能被修改
  3. 修饰方法:子类不能重写
  4. 修饰形参:修饰形参后,方法中不能再被赋值

接口和抽象类的区别是什么

设计思想不同:

  • 抽象类是自下而上的过程,是对类的抽象,通过继承的方式拥有某些相同特性;
  • 接口是对某一行为的规范,是对行为的抽象,一个类可以实现多个接口拥有多种行为

继承是“是不是”的关系,接口是“有没有”的关系

用法不同:

  1. 接口:
    • 在 Java 8 之前,接口中的方法默认是 public abstract;Java 8 之后还可以定义 default 方法和 static 方法,Java 9 之后还允许 private 方法供接口内部复用
    • 接口没有构造方法,不能直接实例化
    • 接口中的成员变量默认是 public static final
  2. 抽象类
    • 抽象类可以提供部分方法实现,也可以定义实例字段、构造方法和普通成员方法
    • 抽象类的成员可以使用 privateprotected、默认访问级别、public 等不同访问控制

简单理解:

  • 抽象类强调“是什么”,用于抽取一类对象的共性
  • 接口强调“能做什么”,用于定义行为能力

重载和重写的区别

重载(Overload): 表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同,返回类型不能作为重载函数的区分标准)

重载是编译时多态,静态的,通过编译后变成不同的函数

重写(Override): 表示子类中的方法可以与父类中的某个方法的名称和参数完全相同

重写是运行时多态,通过动态绑定实现,是父类与子类之间的多态


面向对象的三大特性

面向对象的三大特性:封装、继承、多态

封装: 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏

继承: 描述的是事物之间的所属关系,可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

多态: 即一个引用变量到底指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须由程序运行期间才能决定(回忆下重载和重写) java实现多态有三个必要条件:继承、重写、向上转型

byte 的取值范围是多少、怎么计算出来的

byte 在 Java 中占 8 bit,采用二进制补码表示,有 2^8 = 256 种状态。

  • 最高位是符号位
  • 正数范围:0000 00000111 1111,即 0 ~ 127
  • 负数范围:1000 00001111 1111,即 -128 ~ -1

因此 byte 的取值范围是 -128 ~ 127

之所以不是 -127 ~ 127,是因为补码表示中:

  • 1000 0000 表示 -128
  • 不存在单独的 -0

所以负数会比正数多一个取值


HashMap 相关

底层数据结构

  • JDK1.7: 数组 + 链表(拉链法解决哈希冲突)
  • JDK1.8: 数组 + 链表 + 红黑树(链表长度 >= 8 且数组长度 >= 64 时转为红黑树,红黑树节点数 <= 6 时退化为链表)

重要参数

参数 默认值 说明
initialCapacity 16 初始容量,必须是2的幂次方
loadFactor 0.75f 负载因子
threshold capacity * loadFactor 扩容阈值
TREEIFY_THRESHOLD 8 链表转红黑树的阈值
UNTREEIFY_THRESHOLD 6 红黑树退化为链表的阈值
MIN_TREEIFY_CAPACITY 64 链表转红黑树时数组最小长度

其中 loadFactor = 0.75 是空间和时间之间的折中值:负载过低会浪费空间,负载过高则更容易产生哈希冲突

hash计算

1
2
3
4
5
6
7
8
// JDK1.8的hash方法
static final int hash(Object key) {
    int h;
    // key为null时hash为0,所以HashMap允许key为null
    // 高16位与低16位异或,增加低位的随机性,减少碰撞
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 定位桶下标:(n - 1) & hash,等价于 hash % n(n为2的幂次时)

put流程(JDK1.8)

  1. 对key求hash值,计算桶下标 (n-1) & hash
  2. 如果桶为空,直接新建节点放入
  3. 如果桶不为空(发生碰撞):
    • 如果第一个节点key相同,直接覆盖value
    • 如果是TreeNode(红黑树),调用红黑树的插入方法
    • 否则遍历链表,尾插法插入(JDK1.7是头插法),遍历中如果发现key相同则覆盖
  4. 链表插入后判断长度是否 >= 8,是则尝试转红黑树
  5. 判断 ++size > threshold,是则扩容

扩容机制

  • 容量变为原来的2倍,阈值也变为原来的2倍
  • JDK1.8优化:扩容时不需要重新计算hash,只需看新增的那一位bit是0还是1
    • 是0:位置不变
    • 是1:新位置 = 原位置 + 旧容量

JDK1.7并发下的死循环问题

JDK1.7采用头插法,多线程扩容时可能导致链表形成环,后续get操作死循环。JDK1.8改为尾插法解决了此问题,但HashMap仍然是非线程安全的

HashMap vs Hashtable vs ConcurrentHashMap

对比项 HashMap Hashtable ConcurrentHashMap
线程安全 是(synchronized)
null key/value key可null,value可null 都不可null 都不可null
初始容量 16 11 16
扩容方式 2倍 2倍+1 近似 2 倍(实现细节较复杂)
底层结构 数组+链表+红黑树 数组+链表 JDK1.8: CAS+synchronized+数组+链表+红黑树
效率 低(全表锁) 高(锁粒度细)

ConcurrentHashMap的演进

  • JDK1.7: Segment分段锁(继承ReentrantLock),每个Segment维护一段HashEntry数组,默认16个Segment,并发度16
  • JDK1.8: 抛弃Segment,采用 CAS + synchronized 锁住链表/红黑树的头节点,锁粒度更细,并发度更高

Integer 缓存池

先上JDK1.8源码,Integer缓存是Integer类中的静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Integer的valueOf方法
 public static Integer valueOf(int i) {
    // 先判断数值是否在-128 - 127之间
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

/**
* Cache to support the object identity semantics of autoboxing for values between  (补充:jdk1.5支持自动装箱)
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage.  The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

需要注意区分 new Integer(i)Integer.valueOf(i)

  • new Integer(i) 每次都会创建新对象
  • Integer.valueOf(i)-128 ~ 127 范围内会优先从缓存池返回对象
  • 自动装箱本质上调用的也是 Integer.valueOf(i)

因此,缓存池优化的是自动装箱和 valueOf(),而不是 new Integer()


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

合法的

Unicode 统一了所有字符的编码,是一个 Character Set,也就是字符集,字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储,不同的字符其存储空间不一样,有的需要一个字节就能存储,有的则需要2、3、4个字节。UTF-8是Unicode的一种实现,”中”在UTF-8中占3个字节。但Java的char类型使用UTF-16编码,固定占2个字节,能表示Unicode基本多语言平面(BMP)中的所有字符,”中”的Unicode编码在BMP范围内,所以是合法的


Arrays.asList 获得的 List 使用时需要注意什么

Arrays.asList() 返回的并不是 java.util.ArrayList,而是 Arrays 的一个内部类视图。它的长度与原数组绑定,因此不能进行结构性修改,也就是不能 add()remove()

1
2
3
4
5
6
7
8
9
10
/**
* @serial include
*/
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    // Arrays的内部类ArrayList继承AbstractList类
    // 但是并没有重写add、remove方法
    // AbstractList的add、remove方法是直接抛异常
}

补充两点常见坑:

  • 可以调用 set() 修改元素,因为这不会改变列表长度
  • 如果传入的是基本类型数组,如 int[],那么 Arrays.asList(arr) 得到的不是元素列表,而是只包含一个 int[]List<int[]>

Collection 和 Collections 区别

Collection 是单列集合体系的顶级接口,常见子接口有 ListSetQueue。它定义的是集合通用操作,例如 add()remove()contains()size() 等。

Collections 是集合工具类,提供排序、查找、线程安全包装、不可变视图等静态方法,例如 sort()binarySearch()synchronizedList()unmodifiableList()


fail-fast 和 fail-safe

快速失败和安全失败是对迭代器而言的

fail-fast(快速失败) 在 java.util 包的集合类就都是快速失败的,比如:HashMap、ArrayList

在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值

每当迭代器使用 hasNext() / next() 遍历下一个元素之前,都会检测 modCount 是否仍然等于 expectedModCount;如果不相等,就抛出异常并终止遍历

fail-safe(通常也叫弱一致性遍历) 常见于并发容器,比如 ConcurrentHashMapCopyOnWriteArrayList

这类迭代器遍历时不会因为其他线程的修改而直接抛出 ConcurrentModificationException,但它们的实现方式并不完全相同:

  • CopyOnWriteArrayList 是基于快照遍历
  • ConcurrentHashMap 的迭代器是弱一致性的,能够容忍并发修改,但不保证一定看到所有最新元素

ArrayList、LinkedList 和 Vector 的区别

底层数据结构

  • ArrayList: Object数组(连续内存空间)
  • LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7改为非循环双向链表)
  • Vector: Object数组(与ArrayList相同)

线程安全

  • ArrayList: 非线程安全
  • LinkedList: 非线程安全
  • Vector: 线程安全,方法使用synchronized修饰

扩容机制

  • ArrayList: 无参构造时底层初始为空数组,第一次添加元素时通常扩为 10,后续再按 1.5 倍扩容(oldCapacity + (oldCapacity >> 1)
  • Vector: 默认初始容量10,扩容为原来的2倍(也可通过构造函数指定增量capacityIncrement
  • LinkedList: 链表结构不存在扩容概念

性能对比

操作 ArrayList LinkedList 原因
随机访问(get) O(1) O(n) 数组支持下标直接访问,链表需要遍历
尾部插入(add) O(1)均摊 O(1) ArrayList可能触发扩容
指定位置插入 O(n) O(n) ArrayList需要移动元素,LinkedList需要先遍历定位
删除 O(n) O(n) ArrayList需要移动元素,LinkedList需要先遍历定位

注:LinkedList虽然插入/删除理论是O(1),但定位到目标节点需要O(n),所以实际是O(n)

使用场景

  • ArrayList: 读多写少的场景,频繁随机访问
  • LinkedList: 频繁在头部/尾部插入删除(如队列、双端队列),LinkedList实现了Deque接口
  • Vector: 不推荐使用,需要线程安全可用 Collections.synchronizedList()CopyOnWriteArrayList

RandomAccess接口

ArrayList实现了RandomAccess接口(标记接口,无方法),表示支持快速随机访问。在Collections工具类的binarySearch方法中,会根据是否实现RandomAccess来选择不同的遍历方式:

1
2
3
4
5
6
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);  // for循环下标访问
    else
        return Collections.iteratorBinarySearch(list, key); // 迭代器访问
}

Set 和 List 区别、Set 如何保证元素不重复

两个接口都是继承自Collection,是常用来存放数据项的集合

  • List 允许重复元素,Set 不允许重复元素
  • List 支持按索引访问,Set 不支持按索引随机访问
  • List 一般保留插入顺序;Set 是否有序取决于具体实现,例如 HashSet 无序、LinkedHashSet 保留插入顺序、TreeSet 按排序规则有序

Set如何保证元素不重复: 通过HashSet源码了解一下添加元素过程

1
2
3
4
5
6
7
//HashSet中map属性
private transient HashMap<E,Object> map;

//HastSet的add方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

通过上面可以看到HashSet是根据Map的特性来校验重复元素,再看一下HashMap的put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode) // 转红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

代码主要是通过hash计算key的位置,判断该位置是否有值,其中会通过hash和equals进行判断。这也是为什么用Map插入自定义对象的时候,需要重写equals和hashCode方法


Exception 和 Error 有什么区别

1
2
3
4
5
public class Exception extends Throwable{}

public class Error extends Throwable{}

public class Throwable implements Serializable{}

两个类都继承自 ThrowableThrowable 是 Java 异常体系的根类,只有 Throwable 及其子类实例才能被 throw 抛出并被 catch 捕获

Exception 泛指的是异常 ,Exception 主要分为两种异常,一种是编译期出现的异常,称为 checkedException 受检异常,一种是程序运行期间出现的异常,称为 uncheckedException非受检异常,也被统称为RuntimeException运行时异常。Exception 可以被捕获

  • 常见的 checkedException 有 IOException
  • 常见的 RuntimeException 主要有 NullPointerExceptionIllegalArgumentExceptionArrayIndexOutOfBoundsExceptionClassCastException

Error 通常表示更严重的系统级问题,例如 OutOfMemoryErrorStackOverflowError。这类问题大多不是普通业务代码可以恢复的,因此一般不建议像处理业务异常那样去捕获


String、StringBuffer 和 StringBuilder 有什么区别

String: JDK1.0,字符串常量,每次操作String字符串实际上是不断的创建新的对象,而原来的对象变成了垃圾被GC回收

StringBuffer: JDK1.0,是一个线程安全的容器,多线程场景下一般使用 StringBuffer 用作字符串的拼接

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


地址栏输入 URL 发生了什么

一个相对完整的过程通常包括:

  1. 解析 URL,判断协议、主机、端口、路径等信息
  2. 先查浏览器缓存、系统缓存、Hosts,再进行 DNS 解析
  3. 与目标服务建立 TCP 连接;如果是 HTTPS,还会进行 TLS 握手
  4. 浏览器发送 HTTP 请求,请求可能经过负载均衡、反向代理、应用服务器
  5. 服务端处理请求并返回 HTML、CSS、JS、图片等资源
  6. 浏览器解析 HTML,构建 DOM;解析 CSS,构建 CSSOM;再生成渲染树并布局、绘制
  7. 如果遇到 JavaScript,还可能触发脚本执行、异步请求和页面再次渲染

更多详细内容请参考知乎-1知乎-2