这篇笔记围绕 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)
值传递和引用传递的区别
-
按值调用(call by value):表示方法接收的是调用者提供的值
-
按引用调用(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 == s2 为 false,是因为 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() 等方法返回的都是新字符串。
这样设计的主要目的有:
- 安全性:字符串广泛用于类加载、网络地址、文件路径、数据库连接参数等场景,不可变可以避免被悄悄篡改
- 线程安全:不可变对象天然更容易在多线程间共享
- 字符串常量池优化:内容不可变,池化复用才成立
- 缓存 hashCode:
String的哈希值可以安全缓存,减少重复计算
请描述一下 static 关键字和 final 关键字的用法
static: 表示全局或者静态,可以修饰
- 属性
- 方法
- 块
- 内部类
- static修饰属性和方法:不被某个对象拥有,而是成为类对象,被该类对象所有的实例对象共享
- static修饰块:永远只会被调用一次,和对象创建个数无关(如果是实例块的话,创建一次,就被调用一次)。一个类可以创建多个静态块,且被顺序执行
1
2
3
4
5
6
7
8
static{
静态块
}
{
实例块
}
- static只可以修饰内部类(静态内部类)
final: 表示最终,可以修饰
- 类
- 属性
- 方法
- 形参
- 修饰类:被
final修饰的类不能被继承,因此final和abstract不能同时出现在同一个类上;接口也不能使用final修饰 - 修饰属性:必须赋初始值,即使没有初始值,那么在构造方法中必须被赋值,一旦赋值后不能被修改
- 修饰方法:子类不能重写
- 修饰形参:修饰形参后,方法中不能再被赋值
接口和抽象类的区别是什么
设计思想不同:
- 抽象类是自下而上的过程,是对类的抽象,通过继承的方式拥有某些相同特性;
- 接口是对某一行为的规范,是对行为的抽象,一个类可以实现多个接口拥有多种行为
继承是“是不是”的关系,接口是“有没有”的关系
用法不同:
- 接口:
- 在 Java 8 之前,接口中的方法默认是
public abstract;Java 8 之后还可以定义default方法和static方法,Java 9 之后还允许private方法供接口内部复用 - 接口没有构造方法,不能直接实例化
- 接口中的成员变量默认是
public static final
- 在 Java 8 之前,接口中的方法默认是
- 抽象类
- 抽象类可以提供部分方法实现,也可以定义实例字段、构造方法和普通成员方法
- 抽象类的成员可以使用
private、protected、默认访问级别、public等不同访问控制
简单理解:
- 抽象类强调“是什么”,用于抽取一类对象的共性
- 接口强调“能做什么”,用于定义行为能力
重载和重写的区别
重载(Overload): 表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同,返回类型不能作为重载函数的区分标准)
重载是编译时多态,静态的,通过编译后变成不同的函数
重写(Override): 表示子类中的方法可以与父类中的某个方法的名称和参数完全相同
重写是运行时多态,通过动态绑定实现,是父类与子类之间的多态
面向对象的三大特性
面向对象的三大特性:封装、继承、多态
封装: 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
继承: 描述的是事物之间的所属关系,可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
多态: 即一个引用变量到底指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须由程序运行期间才能决定(回忆下重载和重写) java实现多态有三个必要条件:继承、重写、向上转型
byte 的取值范围是多少、怎么计算出来的
byte 在 Java 中占 8 bit,采用二进制补码表示,有 2^8 = 256 种状态。
- 最高位是符号位
- 正数范围:
0000 0000到0111 1111,即0 ~ 127 - 负数范围:
1000 0000到1111 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)
- 对key求hash值,计算桶下标
(n-1) & hash - 如果桶为空,直接新建节点放入
- 如果桶不为空(发生碰撞):
- 如果第一个节点key相同,直接覆盖value
- 如果是TreeNode(红黑树),调用红黑树的插入方法
- 否则遍历链表,尾插法插入(JDK1.7是头插法),遍历中如果发现key相同则覆盖
- 链表插入后判断长度是否 >= 8,是则尝试转红黑树
- 判断
++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 是单列集合体系的顶级接口,常见子接口有 List、Set、Queue。它定义的是集合通用操作,例如 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(通常也叫弱一致性遍历) 常见于并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList
这类迭代器遍历时不会因为其他线程的修改而直接抛出 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{}
两个类都继承自 Throwable。Throwable 是 Java 异常体系的根类,只有 Throwable 及其子类实例才能被 throw 抛出并被 catch 捕获
Exception 泛指的是异常 ,Exception 主要分为两种异常,一种是编译期出现的异常,称为 checkedException 受检异常,一种是程序运行期间出现的异常,称为 uncheckedException非受检异常,也被统称为RuntimeException运行时异常。Exception 可以被捕获
- 常见的 checkedException 有 IOException
- 常见的 RuntimeException 主要有
NullPointerException、IllegalArgumentException、ArrayIndexOutOfBoundsException、ClassCastException等
Error 通常表示更严重的系统级问题,例如 OutOfMemoryError、StackOverflowError。这类问题大多不是普通业务代码可以恢复的,因此一般不建议像处理业务异常那样去捕获
String、StringBuffer 和 StringBuilder 有什么区别
String: JDK1.0,字符串常量,每次操作String字符串实际上是不断的创建新的对象,而原来的对象变成了垃圾被GC回收
StringBuffer: JDK1.0,是一个线程安全的容器,多线程场景下一般使用 StringBuffer 用作字符串的拼接
StringBuilder: JDK1.5,非线程安全的容器,StringBuilder 的 append 方法常用于字符串拼接,它的拼接效率要比 String 中 + 号的拼接效率高。StringBuilder 一般不用于并发环境
地址栏输入 URL 发生了什么
一个相对完整的过程通常包括:
- 解析 URL,判断协议、主机、端口、路径等信息
- 先查浏览器缓存、系统缓存、Hosts,再进行 DNS 解析
- 与目标服务建立 TCP 连接;如果是 HTTPS,还会进行 TLS 握手
- 浏览器发送 HTTP 请求,请求可能经过负载均衡、反向代理、应用服务器
- 服务端处理请求并返回 HTML、CSS、JS、图片等资源
- 浏览器解析 HTML,构建 DOM;解析 CSS,构建 CSSOM;再生成渲染树并布局、绘制
- 如果遇到 JavaScript,还可能触发脚本执行、异步请求和页面再次渲染