Java 基础面试
系列 - 面试
目录
警告
本文最后更新于 2023-01-23,文中内容可能已过时。
关键字
- 数据类型(9):
boolean
、byte
、char
、short
、int
、long
、float
、double
、void
- 保留数值(3):
null
、true
、false
- 循环语句(6):
while
、do
、for
、continue
、break
、goto
- 分支语句(5):
if
、else
、switch
、case
、default
- 其他语句(3):
instanceof
、assert
、return
- 访问限制(3):
public
、protected
、private
- 其他修饰(4):
static
、final
、abstract
、native
- 异常处理(5):
try
、catch
、finally
、throw
、throws
- 面向对象(8):
class
、interface
、enum
、extends
、implements
、this
、super
、new
- 包(2):
package
、import
- 多线程(3):
volatile
、synchronized
、transient
- 保留关键字(2):
const
,strictfp
- 关键字不能作为变量名。
- 变量名只能以字母、
_
和$
开始。- 变量名只能包含数字、字母、
_
和$
。- 内部保留关键字:
_
。
数值
Math.abs(Integer.MIN_VALUE)
|
|
1 / 0 和 1 / 0.0 的区别?
1 / 0
抛出java.lang.ArithmeticException: / by zero
异常。1 / 0.0
返回Infinity
。
容器
ArrayList
扩容机制
ArrayList()
使用长度为 0 的数组,第一次扩容时至少为 10。ArrayList(int initialCapacity)
使用指定容量大小的数组。ArrayList(Collection<? extends E> c)
使用 c 的大小作为数组容量。add(E e)
首次扩容为 10(无参初始化),之后每次存满时扩容为当前容量的 1.5 倍,向下取整。- 使用无参构造方法初始化时,第一次扩容为
10
,之后每次扩容为size + (size >> 1)
。 - 使用有参构造方法初始化时,第一次扩容为
1
,之后每次扩容为Math.max(size + (size >> 1), size + 1)
。
- 使用无参构造方法初始化时,第一次扩容为
addAll(Collection<? extends E> c)
:剩余空间不够时,扩容为当前容量的 1.5 倍和加上 c 的元素后的元素数量的更大值。- 使用无参构造方法初始化时,第一次扩容为
Math.max(10, c.size())
,之后每次扩容为Math.max(length + (length >> 1), size + c.size())
。 - 使用有参构造方法初始化时,第一次扩容为
size + c.size()
,之后每次扩容为Math.max(length + (length >> 1), size + c.size())
。
- 使用无参构造方法初始化时,第一次扩容为
|
|
ArrayList 和 LinkedList
ArrayList
- 线程不安全。
- 底层数组实现,需要分配连续空间。
- 可根据索引访问。实现了
RandomAccess
接口。 - 尾部插入/删除性能高;越靠近首部,插入/删除性能越低,因为需要移动元素。
- 因为是连续空间,所以可以利用缓存,实现高效访问。
LinkedList
- 线程不安全。
- 底层双向链表实现,元素不需要连续存储。
- 不能根据索引访问。
- 头部/尾部的插入/删除性能高;越靠近中间,插入/删除性能越低,需要遍历找到对应元素。
- 结点除了存储元素,还需要存储前/后指针,占用内存多。
|
|
HashMap
|
|
为什么 HashMap 的大小是 2 的幂次方
每个 key 的 hash 值很大,需要通过取模,即 hash(key) % n
得到数组索引,当 n 是 2 的幂次方时通过 (n - 1) & hash(key)
可以更快速的进行取模。
为什么 HashMap 多线程会导致死循环
JDK 1.8 之前存在这个问题,因为并发下扩容时的 rehash 会造成元素之间会形成⼀个循环链表。
当 HashMap 扩容时,HashMap 中的所有元素都需要被重新 hash 一遍,称为 rehash。
HashMap 和 HashTable
HashMap
- 线程不安全。
- key 和 value 都允许
null
。 - 初始大小为 16,扩容因子为 0.75,每次扩容为之前的 2 倍;当指定初始大小时会扩容为 2 的次方。
- 迭代器是 fail-fast。
HashTable
- 线程安全,使用了
volatile
和synchronized
。 - key 和 value 都不允许
null
。 - 初始大小为 11,扩容因子为 0.75,每次扩容为之前的 2 倍 + 1。
- 迭代器是 fail-safe。
- 线程安全,使用了
fail-fast 和 fail-safe
- fail-fast:遍历过程中若发现容器进行了修改,抛出
ConcurrentModificationException
。如ArrayList
、Vector
、LinkedList
。- Java 容器类中使用
modCount
表示容器的修改次数,若遍历过程中modCount
发生了改变,则说明容器发生了修改,抛出异常。
- Java 容器类中使用
- fail-safe:正常完成遍历,对容器发生的修改不可见。如
CopyOnWriteArrayList
。CopyOnWriteArrayList
容器进行修改时会复制一份新数组,不对原数组上进行修改,然后替换原数组。
|
|
Comparable 和 Comparator
Comparable
接口出自 java.lang 包,它有⼀个compareTo(Object obj)
方法⽤来排序。
|
|
Comparator
接口出自 java.util 包(需要导包),它有⼀个compare(Object obj1, Object obj2)
方法⽤来排序。
|
|
compareTo(Object obj)
方法和compare(Object obj1, Object obj2)
方法的返回值 小于 0 代表升序, 大于 0 代表降序。Collections.sort()
、TreeSet
、TreeMap
、PriorityQueue
使用自定义类时必须实现Comparable
接口或传入Comparator
接口的实现类。
Arrays.asList() 和 List.of()
Arrays.asList()
返回的对象是java.util.Arrays
类中的内部类java.util.Arrays$ArrayList
,而不是java.util.ArrayList
。java.util.Arrays$ArrayList
没有实现add()
、remove()
和clear()
方法,但实现了set()
方法,因此不能添加和删除元素,只能修改和读取元素。- 传入的数组必须是对象数组,不能是基本类型数组。
|
|
List.of()
返回的是AbstractImmutableList
接口的实现类,该接口中所有修改操作(add()
、remove()
、set()
等)全都会抛出异常。
|
|
String & StringBuilder & StringBuffer
String
- 初始化后则不可添加/修改,因为未提供修改接口。
- 底层
byte
数组实现(JDK 9 之前是char
数组)。
StringBuilder
- 可添加/修改字符,不会创建新的对象。
- 线程不安全。
StringBuffer
- 可添加/修改字符,不会创建新的对象。
- 线程安全,使用了
volatile
和synchronized
。
面向对象
封装
四种权限修饰符
public
- 修饰范围:外部类、内部类、方法、属性。
- 继承特性:可继承。
- 作用范围:可被任意类访问。
- 其他特性:一个 Java 文件只能有一个
public
外部类,且文件名与类名相同。
protected
- 修饰范围:内部类、方法、属性。
- 继承特性:可继承。
- 作用范围:可被同一个包下的类和子类访问。
- 修饰范围:外部类、内部类、方法、属性、局部变量。
- 继承特性:可被同一个包下的子类继承。
- 作用范围:可被同一个包下的类访问。
private
- 修饰范围:内部类、方法、属性。
- 继承特性:不可继承。
- 作用范围:只能本类访问。
protected
和private
只能修饰内部类,不能修饰外部类。
三种面向对象修饰符
abstract
- 修饰范围:外部类、内部类、方法(抽象类、接口)。
- 其他特性:
- 抽象方法无方法体。
- 抽象类的子类必须重写父类所有抽象方法后才能创建对象,否则也成为一个抽象类。
static
- 修饰范围:内部类、方法、属性。
- 其他特性:
- 通过类名访问。
- 所有实例共享。
- 静态方法中不能访问非静态方法和非静态属性。
- 静态方法中不能访问
this
和super
。
final
- 修饰范围:外部类、内部类、方法(非构造方法)、属性、局部变量。
- 其他特性:
- 修饰的类不可被继承,类中的方法也被
final
修饰。 - 修饰的方法不可被重写。
- 修饰的属性必须赋值(直接赋值、构造块赋值、构造方法赋值,三选一),且不可修改。
- 修饰的局部变量一旦赋值便不可修改。
- 若修饰的是引用类型的变量,则初始化后不可指向另一对象,但对象内容仍可发生改变。
- 修饰的类不可被继承,类中的方法也被
abstract
与private
、static
和final
均不可搭配。
private
方法会隐式地被指定为final
方法。
继承
多态
编码
- ASCII:共 127 个字符,每个字符占 1B。
- ISO-8859-1:共 256 个字符,每个字符占 1B。
- GB2312
- GBK
- Unicode(Java 默认):每个字符占 2B,每个中文字符占 2B。
- UTF-8:每个字符占 1-4B,每个中文字符占 3B。
- UTF-16:每个字符占 2B 或 4B。
- UTF-32:每个字符占 4B。
基本数据类型
boolean
:单独使用占 4B(当作int
),数组使用占 1B(当作byte
)。包装类为Boolean
。只能取true
或false
。byte
:占 1B。包装类为Byte
。short
:占 2B。包装类为Short
。char
:占 2B。包装类为Character
。int
:占 4B。包装类为Integer
。默认整数类型。float
:占 4B。包装类为Float
。如3.14f
。long
:占 8B。包装类为Long
。如1L
。double
:占 8B。包装类为Double
。默认浮点数类型。
byte
、short
、char
与整数发生运算时会被向上转型为int
。
float
与浮点数发生运算时会被向上转型为double
。
常量池
反射
程序在运行时可以获取任意类的所有信息,可以访问任意对象的所有方法和属性。
Class 类
每个 Java 类运行时都在 JVM 里表现为一个 Class 对象,包括数组、boolean
、byte
、char
、short
、int
、float
、long
、double
和void
。
- 每个类被编译后会产生一个 Class 对象(唯一),包含该类的类型信息,保存在同名的 .class 文件中。
- 每个类对应的 Class 对象只有一个(唯一),无论创建多少个实例对象,其依据的都是用一个 Class 对象。
- Class 类的构造方法私有,因此 Class 类的实例不能手动创建,只能由 JVM 创建和加载。
- Class 对象的作用是在运行时获取某个对象的类型信息。
获取 Class 对象的三种方式
|
|
反射操作对象
Person
|
|
- 简单操作
|
|
一般情况下,
getName()
和getCanonicalName()
返回值相同,但对于内部类、数组,两者返回值不同。只有getName()
返回的才是全限定名。
Constructor
:获取构造方法。
|
|
Method
:获取方法。
|
|
Field
:获取属性。
|
|
反射过程及实现
- 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
- 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;
- 反射也是考虑了线程安全的,放心使用;
- 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;
- 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
- 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;
- 调度反射方法,最终是由jvm执行invoke0()执行
注解
对象克隆
深拷贝 & 浅拷贝
浅拷贝(ShallowClone):
- 若变量是基本数据类型,则复制一份给克隆对象。
- 若变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象指向相同的内存地址。 深拷贝(DeepClone):
- 无论变量是基本数据类型还是引用类型,都复制一份给克隆对象。
clone() 方法
实现的clone()
需要满足以下条件均为真:
x.clone() != x
,强制。x.clone().getClass() == x.getClass()
,非强制。x.clone().equals(x)
,非强制。
|
|
|
|
新特性
JDK 5 新特性
JDK 8 新特性
- lambda
- Functional Interfaces
- Optionals
- Stream
- Parallel-Streams