ArrayList 内部基于动态数组 Object[] 实现,会根据实际存储的元素数量动态地扩容或缩容。不过 ArrayList 只能存储对象,对于基本数据类型,需要使用其对应的包装类,同时线程不安全。
ArrayList 有三个构造函数,其中无参数构造方法会初始化一个空数组,但不分配实际容量,只有在添加第一个元素时,数组的容量才会扩展为默认大小(通常为 10)。
ArrayList()
ArrayList(int initialCapacity)
ArrayList(Collection<? extends E> c)
ArrayList 在空间不足时会进行动态扩容,扩容时首先会将容量变为原来的 1.5 倍左右(奇数会丢掉小数),然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量。
Vector 与 ArrayList 类似也采用 Object[] 实现,是 List 的古老实现类,同时线程安全,但是在增长时会以固定的幅度增加容量,而不是按倍数增加,这可能导致一些内存浪费,因此通常更倾向于使用 ArrayList,并使用显式的同步措施来确保线程安全性。
LinkedList 底层使用的是双向链表,不过需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且性能通常会更好。同时 LinkedList 也不是线程安全的。
采用写时复制的线程安全 List。写时复制指的是读取时无需加锁,写入时不直接修改原始数据,而创建一个新的副本,然后在副本上进行修改,最后进行替换。因此 CopyOnWriteArrayList 适用于读多写少的场景。
CopyOnWriteArrayList 主要有两个问题:
CopyOnWriteArrayList 仅能保证数据的最终一致性,在写操作执行完成前的读操作读取到的数据均为旧数组的脏数据。ArrayDeque 和 LinkedList 都实现了 Deque 接口,ArrayDeque 基于动态数组实现,并且是循环数组,在队满时会扩容为原来的两倍。LinkedList 基于链表实现,速度较慢且存储密度低,因此一般优先考虑 ArrayDeque,不过 ArrayDeque 不支持存储 null 且二者都不是线程安全的。
ArrayDeque 主要通过 addFirst()、addLast()、removeFirst() 和 removeLast() 实现双端队列的相关操作,这四个方法失败时都会抛出异常。
PriorityQueue 本质上是完全二叉树实现的小根堆,能够在
O
(
l
o
g
n
)
O(logn)
O(logn) 的时间复杂度内插入元素和删除堆顶元素。不过 PriorityQueue 线程不安全,且不支持存储 null 。同时,为了进行比较,优先队列存储的元素类型需要实现 Comparable 接口,或者在队列构造时传入自定义比较器 Comparator。
HashMap 是一个无序的键值对集合,可以存储为 null 的 key 和 value,不过 null 作为键只能有一个,作为值可以有多个。
创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16,之后每次扩充,容量翻倍。创建时如果给定了初始容量值,HashMap 会将其扩充至大于此初始值的首个
2
n
2^n
2n 大小(主要是为了在减少哈希碰撞同时提高哈希运算的效率,如果数组的长度为
2
n
2^n
2n,那么在映射时只需要直接取键的低 n 位即可,通过位运算即可实现,不需要取余)。
HashMap 基于数组(桶) + 链表 / 红黑树实现,默认采用数组 + 链表,当链表长度大于 8(如果哈希函数足够优秀,节点在桶中的分布遵循泊松分布,链表长度大于 8 的概率相当低)时会通过 treeifyBin() 处理哈希冲突,如果此时数组长度小于 64 会优先对数组进行扩容,否则会将链表转化为红黑树以减少搜索时间。除了链表长度会导致扩容,当元素个数超过阈值时 HashMap 也会进行扩容,这个阈值默认为当前数组长度的 0.75 (默认的负载因子)倍。
class Solution {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
{
HashMap<Integer, Integer> map = new HashMap<>();
Class<? extends HashMap> clazz = map.getClass();
Field threshold = clazz.getDeclaredField("threshold");
threshold.setAccessible(true);
for (int i = 0; i <= 12; i++) {
map.put(i, null);
System.out.print(threshold.getInt(map) + " "); // 12 12 12 12 12 12 12 12 12 12 12 12 24
}
}
{
HashMap<Integer, Integer> map = new HashMap<>();
Class<? extends HashMap> clazz = map.getClass();
Field threshold = clazz.getDeclaredField("threshold");
threshold.setAccessible(true);
for (int i = 0; i <= 8; i++) {
map.put(i * 16, null);
System.out.print(threshold.getInt(map) + " "); // 12 12 12 12 12 12 12 12 24
}
}
}
}
HashMap 是非线程安全的,其并发版本为 ConcurrentHashMap。ConcurrentHashMap 大多数操作通过自旋 CAS 和 volatile 保证并发安全,只有部分操作通过 synchronized 加锁(比如向非空的桶中添加元素),同时只对单个桶加锁,加锁粒度小,并发效果优秀。不过 ConcurrentHashMap 的 key 和 value 均不能为 null,因为多线程情况下无法准确判断 null 表示的是不存在还是存在一个空的键或值。
注意:ConcurrentHashMap 保证的只是单次操作的原子性,而不是多次操作,因此类似于先通过 containsKey() 判断键是否存在然后再通过 put() 插入元素的操作都是线程不安全的。如果想要执行复合操作,可以通过 putIfAbsent() 以及 compute() 等复合函数代替。
LinkedHashMap 和 HashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。
LinkedHashMap 提供了两种顺序迭代元素的方式:
LinkedHashMap 的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。accessOrder 参数指定按照访问顺序迭代元素。当 accessOrder 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。EnumMap 是 Java 中的一种特殊的 Map 实现,它要求键的类型必须是枚举类型。
EnumMap 的主要特点:
EnumMap 要求键的类型必须是枚举类型,这样可以利用枚举的有限性和固定的值范围。EnumMap 会按照枚举常量的定义顺序来维护键值对的顺序。EnumMap 内部使用数组实现,因此在某些情况下,它比通用的 HashMap 更高效。TreeMap 基于红黑树实现,因此元素有序,同时为了保证 key 可比较,key 不允许为 null,线程不安全且性能稍逊于 HashMap。
Set 主要包括 HashSet、LinkedHashSet 和 TreeSet,本质上是只存储键的 Map,值为一个空的 Object,能保证元素唯一,并且都不是线程安全的。
isEmpty() 方法,而不是 size() == 0,因为 isEmpty() 方法的可读性更好并且效率更高,size() == 0 在某些情况下还要进行类型转换。collection.toArray(T[] array) 方法,有以下两种使用方式。class Solution {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Integer[] array = new Integer[3];
// 1. 返回参数
list.toArray(array);
for (int i = 0; i < array.length; i++) {
System.out.println(i); // 0 1 2
}
// 2. 通过 new Integer[0] 说明返回类型,0 是为了节省空间
array = list.toArray(new Integer[0]);
for (int i = 0; i < array.length; i++) {
System.out.println(i); // 0 1 2
}
}
}
Arrays.asList(array) 方法。不过该数组必须是对象数组,而不是基本类型数组。同时 Arrays.asList() 方法返回的并不是 java.util.ArrayList,而是 java.util.Arrays 的一个内部类,因此需要一次附加的转换。class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[] {1, 2, 3};
List<Integer> list = Arrays.asList(array);
System.out.println(list); // [1, 2, 3]
System.out.println(list.getClass()); // class java.util.Arrays$ArrayList
List<Integer> trueList = new ArrayList<>(list);
trueList.remove(2);
System.out.println(trueList); // [1, 2]
System.out.println(trueList.getClass()); // class java.util.ArrayList
}
}
java.lang.Throwable 是所有异常和错误的父类,它有 Exception 和 Error 两个子类,其中异常又可以分为受检查异常和不受检查异常。
RuntimeException 及其子类,例如 NullPointerException、IllegalArgumentException、ArrayIndexOutOfBoundsException 和 ClassCastException 等异常。RuntimeException 及其子类,其他的 Exception 类及其子类都属于受检查异常,例如 IOException、SQLException 和 ClassNotFoundException 等。处理可检查异常的方式可以使用 try-catch-finally 语句块进行捕获和处理,或者在方法签名中声明抛出该异常。OutOfMemoryError、StackOverflowError 等。对于系统错误,一般不建议进行捕获和处理,而是直接让 JVM 终止。Throwable 类常用方法:
String getMessage():返回异常发生时的简要描述。String toString():返回异常发生时的详细信息。void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。Exception 支持自定义新的异常类型,但是在项目中保持一个合理的异常继承体系是非常重要的,因此可以定义一个 BaseException 根异常继承自 RuntimeException,其他业务类型的异常再从根异常中派生。
注意:在通过 try-catch-finally 捕获和处理异常时,不应该在 finally 语句块中使用 return,因为当 try 语句和 finally 语句中都有 return 语句时,try 语句中的返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值,从而导致 try 语句块中的 return 语句会被忽略。同理,catch 中的 return 也会被 finally 中的 return 覆盖。
class Solution {
public static void main(String[] args) {
System.out.println(func()); // 2
}
private static int func() {
try {
return 1;
} finally {
return 2;
}
}
}
在 JDK7 之后还提供了 try-with-resources 用于管理文件、网络连接、数据库连接等所有实现了 java.lang.AutoCloseable 接口的资源类,它简化了资源管理的代码,并确保资源在使用后被正确关闭,以避免资源泄漏。
泛型是 JDK5 中引入的一个新特性,类似于 C++ 中的模板,泛型编程以一种独立于任何特定类型的方式编写代码。
Java 中的泛型主要有泛型类、泛型接口、泛型方法三种使用方式:
public class MyClass<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
class Solution {
public static void main(String[] args) {
MyClass<Integer> classInt = new MyClass<>();
classInt.setData(1);
System.out.println(classInt.getData()); // 1
MyClass<Double> classDouble = new MyClass<>();
classDouble.setData(1.0);
System.out.println(classDouble.getData()); // 1.0
}
}
public interface MyInterface<T> {
T func();
}
public class MyClass implements MyInterface<String> {
@Override
public String func() {
return "str";
}
}
class Solution {
public static void main(String[] args) {
MyInterface<String> myClass = new MyClass();
System.out.println(myClass.func()); // str
}
}
class Solution {
public static void main(String[] args) {
Integer i = 1;
Long l = 1L;
func(i); // class java.lang.Integer
func(l); // class java.lang.Long
}
private static <T> void func(T data) {
System.out.println(data.getClass());
}
}
泛型擦除:泛型擦除是指 Java 中的泛型只在编译期有效,在运行期间会被删除。也就是说所有泛型参数在编译后都会被清除掉,因此下面两个函数会存在重载异常。
class Solution {
public void func(List<String> list) {}
public void func(List<Integer> list) {}
}
Java 泛型本身不支持协变和逆变,但可以通过通配符及 extend 和 super 实现类似的效果。
public static class A {}
public static class B extends A {}
public static class C extends B {}
public static void main(String[] args) {
B[] array = new C[1]; // 数组支持协变
List<B> list = new ArrayList<C>(); // 泛型不支持协变
List<? extends B> list;
list = new ArrayList<A>(); // 编译错误
list = new ArrayList<B>();
list = new ArrayList<C>(); // 通过上界模拟协变
}
从 Java8 开始,时间通过 LocalDate、LocalTime 和 LocalDateTime 表示,其中 LocalDate 包含年、月、日信息,LocalTime 包含时、分、秒、和纳秒信息。LocalDateTime 包含了所有信息。
以 LocalDateTime 为例介绍相关 API:
package atreus.ink;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
// 创建 LocalDateTime 对象
LocalDateTime dateTime = LocalDateTime.of(2023, 11, 25, 10, 30, 30, 10);
System.out.println(dateTime); // 2023-11-25T10:30:30.000000010
dateTime = LocalDateTime.now();
System.out.println(dateTime); // 2023-11-16T10:24:00.149062
// 获取年、月、日、时、分、秒、纳秒
System.out.println(dateTime.getYear()); // 2023
System.out.println(dateTime.getMonth()); // NOVEMBER
System.out.println(dateTime.getMonthValue()); // 11
System.out.println(dateTime.getDayOfWeek()); // THURSDAY
System.out.println(dateTime.getDayOfMonth()); // 16
System.out.println(dateTime.getDayOfYear()); // 320
System.out.println(dateTime.getHour()); // 10
System.out.println(dateTime.getMinute()); // 24
System.out.println(dateTime.getSecond()); // 0
System.out.println(dateTime.getNano()); // 149062000
// 直接修改(返回新对象)
System.out.println(dateTime.withYear(2024)); // 2024-11-16T10:24:00.149062
// 增加与减少(返回新对象)
System.out.println(dateTime.plusYears(1)); // 2024-11-16T10:24:00.149062
System.out.println(dateTime.minusYears(1)); // 2022-11-16T10:24:00.149062
// 比较
System.out.println(dateTime.isBefore(dateTime.plusYears(1))); // true
// 格式化与解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss.SSS秒");
String s = dateTime.format(formatter);
System.out.println(s); // 2023年11月16日 10时24分00.149秒
LocalDateTime time = LocalDateTime.parse(s, formatter);
System.out.println(time); // 2024-11-16T10:24:00.149
}
}
Stream 在 Java8 中被引入,它提供了一种类似于 SQL 语句的方式来对 Java 集合进行操作和处理。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
class Solution {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String[] array = new String[10];
// 获取集合的 Stream 流
Stream<String> listStream = list.stream();
// 获取数组的 Stream 流
Stream<String> arrayStream = Arrays.stream(array);
}
}
| 常用中间方法 | 说明 |
|---|---|
Stream | 用于对流中的数据进行过滤 |
Stream | 按照指定规则排序 |
Stream | 获取前几个元素 |
Stream | 跳过前几个元素 |
Stream | 去除流中重复的元素(自定义对象需要重写 equals() 和 hashCode() 方法) |
| 加工元素并返回新流 |
| 合并 a 和 b 为一个流 |
Stream | 将通过 stream(int[] array) 得到的 IntStream 装箱为 Stream |
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.stream().filter(i -> i > 1).forEach(System.out::print); // 2 3
list.stream().sorted((o1, o2) -> o2 - o1).forEach(System.out::print); // 3 2 1
list.stream().map(i -> i + 1).forEach(System.out::print); // 2 3 4
}
}
| 常用终结方法 | 说明 |
|---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计元素个数 |
Optional | 获取最大值元素 |
Optional | 获取最小值元素 |
R collect(Collector collector) | 把流中的元素收集到集合 |
Object[] toArray() | 把流中的元素收集到数组 |
通过 collect() 实现 List 转 Map:
public class Main {
@Data
@AllArgsConstructor
private static class MyClass {
String userName;
}
public static void main(String[] args) {
List<MyClass> list = new ArrayList<>();
list.add(new MyClass("1"));
list.add(new MyClass("2"));
list.add(new MyClass("3"));
Map<String, MyClass> map = list.stream().collect(Collectors.toMap(MyClass::getUserName, Function.identity()));
for (Map.Entry<String, MyClass> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
1: Main.MyClass(userName=1)
2: Main.MyClass(userName=2)
3: Main.MyClass(userName=3)
可以通过 parallelStream() 从集合中创建并行流,也可以通过 parallel() 方法将顺序流转换为并行流,并通过 sequential() 方法将并行流转换回顺序流。
并行流内部默认使用 ForkJoinPool 线程池,默认的线程数量就是处理器的核心数。
public class Main {
public static void main(String[] args) {
List list = IntStream.rangeClosed(1, 100000).boxed().toList();
long start = System.nanoTime();
list.stream().map(integer -> integer + 1);
long cost = System.nanoTime() - start;
System.out.println(cost); // 335875
start = System.nanoTime();
list.parallelStream().map(integer -> integer + 1);
cost = System.nanoTime() - start;
System.out.println(cost); // 199292
}
}
函数式接口和 Lambda 表达式均为 Java8 新特性。函数式接口就是有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,可以被隐式转换为 Lambda 表达式。Lambda 表达式实际是匿名函数,其原型为:(参数列表) -> {函数体};。
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
Arrays.sort(array, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
List<Integer> list = Arrays.asList(array);
list.forEach((i)->{
System.out.println(i); // 3 2 1
});
}
}
方法引用其实是 Lambda 的语法糖,当我们的 Lambda 表达式只有一行并且恰好有一已经存在的方法作用跟他相同,我们就可以用方法引用替换 Lambda 表达式。方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 表示。
方法引用主要分为静态方法引用、实例方法引用、特定类型的方法引用和构造器引用:
public class MyCompare {
public static int compareFunc(int o1, int o2) {
return o2 - o1;
}
}
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
// 静态方法引用
Arrays.sort(array, MyCompare::compareFunc);
List<Integer> list = Arrays.asList(array);
list.forEach(System.out::println); // 3 2 1
}
}
public class MyCompare {
public int compareFunc(int o1, int o2) {
return o2 - o1;
}
}
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
MyCompare compare = new MyCompare();
// 实例方法引用
Arrays.sort(array, compare::compareFunc);
List<Integer> list = Arrays.asList(array);
list.forEach(System.out::println); // 3 2 1
}
}
import java.util.*;
class Solution {
public static void main(String[] args) {
String[] array = new String[]{"b", "A"};
// 特定类型的方法引用
Arrays.sort(array, String::compareToIgnoreCase);
List<String> list = Arrays.asList(array);
list.forEach(System.out::println); // A b
}
}
public class MyCompare {}
import java.util.function.Supplier;
class Solution {
public static void main(String[] args) {
Supplier<MyCompare> sup = MyCompare::new;
}
}
反射机制允许我们在运行时获取类的信息、调用类的方法、操作类的属性,而无需在编译时知道类的具体名称,但存在一定的安全问题,同时会影响性能。
获取 Class 对象的四种方式:
package atreus.ink;
public class MyClass {}
import atreus.ink.MyClass;
class Solution {
public static void main(String[] args) throws ClassNotFoundException {
{
// 1. 使用 .class 获取,不会触发类的初始化
Class<MyClass> clazz = MyClass.class;
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 2. 通过 Class.forName() 传入类的全路径获取
Class<?> clazz = Class.forName("atreus.ink.MyClass");
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 3. 通过对象实例的 getClass() 方法获取
MyClass myClass = new MyClass();
Class<? extends MyClass> clazz = myClass.getClass();
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 4. 通过类加载器的 loadClass() 方法传入类的全路径获取,不会触发类的初始化
ClassLoader classLoader = MyClass.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("atreus.ink.MyClass");
System.out.println(clazz); // class atreus.ink.MyClass
}
}
}
反射的一些基本操作:
package atreus.ink;
public class MyClass {
private String data;
public MyClass() {
data = "atreus";
}
private void privateMethod() {
System.out.println("private void privateMethod()");
}
public String publicMethod(String s) {
return s;
}
}
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class Solution {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 通过 Class.forName() 传入类的全路径获取 Class 对象
Class<?> clazz = Class.forName("atreus.ink.MyClass");
// 创建类的实例对象
Object instance = clazz.getDeclaredConstructor().newInstance();
// 获取类中定义的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
// 获取指定方法并调用
{
Method method = clazz.getDeclaredMethod("publicMethod", String.class);
Object result = method.invoke(instance, "public String publicMethod(String s)");
System.out.println(result);
}
{
Method method = clazz.getDeclaredMethod("privateMethod");
method.setAccessible(true);
method.invoke(instance);
}
// 获取指定参数并在修改后输出
Field field = clazz.getDeclaredField("data");
field.setAccessible(true);
field.set(instance, "new data");
System.out.println(field.get(instance));
}
}
public java.lang.String atreus.ink.MyClass.publicMethod(java.lang.String)
private void atreus.ink.MyClass.privateMethod()
public String publicMethod(String s)
private void privateMethod()
new data
注解可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供了某些信息供程序在编译或者运行时使用。
注解本质上是是一个接口,继承自 Annotation 类,注解属性本质则是抽象方法,使用注解实际上使用的是该接口的实现类。可以通过 @interface 自定义注解,且注解中如果只有一个 value 属性,使用注解时 value 名称可以不写
public @interface MyAnnotation {
public 属性类型 属性名() default 默认值;
}
public interface MyAnnotation extends Annotation {
public abstract 属性类型 属性名();
}
public @interface MyAnnotation {
String value();
}
class Solution {
@MyAnnotation("str") // 可以省略 value
public static void main(String[] args) {}
}
元注解是修饰注解的注解,主要分为 @Target 和 @Retention:
@Target:声明被修饰的注解能在哪些位置使用,如类、接口、成员变量、成员方法等。@Retention:声明注解的保留周期。SOURCE 表明只作用在源码阶段,字节码文件中不存在。CLASS 为默认值,表明保留到字节码文件中,但运行阶段不存在。RUNTIME 表明一直保留到运行阶段。AnnotatedElement 接口定义了与注解解析相关的方法。注解一般需要与反射结合使用,所有的类成分 Class、Method、Field 和 Constructor 都实现了 AnnotatedElement 接口,它们都拥有解析注解的能力。
主要解析方法有:
Annotation[] getDeclaredAnnotations():获得当前对象上使用的所有注解,返回注解数组。T getDeclaredAnnotation(Class annotationClass) :根据注解类型获得对应注解对象。boolean isAnnotationPresent(Class annotationClass) :判断当前对象是否使用了指定的注解。Java 中有四种引用类型:
new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候。SoftReference 修饰的对象被称为软引用,软引用指向的对象会在内存将要溢出(OOM)时被回收。软引用主要用于实现缓存,内存足够时可以避免重复读取,内存不足时可以直接释放。WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,只被弱引用指向的对象就会被回收。弱引用除了用于解决 ThreadLocal 中的内存泄露问题,本地缓存 Caffeine Cache 也支持软引用和弱引用。PhantomReference 进行定义。虚引用的主要用途是跟踪对象的垃圾回收,从而实现直接内存的回收等操作。Java 运行时数据区域主要分为程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中堆和方法区为线程共享。
每个线程会通过自己的程序计数器记录当前要执行的字节码指令的地址。
程序计数器主要有两个作用:
虚拟机栈的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。它由一个个栈帧组成,调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁。
如果栈的大小不支持动态扩展,发生栈溢出时就会抛出 StackOverFlowError。如果栈的大小支持动态扩展,在扩展过程中无法申请到足够的内存空间时就会抛出 OutOfMemeoryError,可以通过虚拟机参数 -Xss 修改栈的大小。
每个栈帧由局部变量表、操作数栈和帧数据三部分组成:
this 对象(非静态方法)、方法的参数(有参方法)以及方法体中声明的局部变量,数组的每个位置称之为槽,long 和 double 类型占用两个槽,其他类型(包括 this 等引用类型)占用一个槽。局部变量表的具体内容在编译成字节码文件时就已经确定,不过最大槽位数是固定的,因此为了节省空间,一旦槽中的某个局部变量不再被使用,当前槽就可以被其他局部变量复用。try 代码块的覆盖范围以及出现或未出现异常时需要跳转到的字节码指令的地址。本地方法栈存储的是 native 本地方法的栈帧,不过在 HotSpot 虚拟机中,Java 虚拟机栈和本地方法栈使用的是同一个栈空间。
堆内存是空间最大的一块内存区域,创建出来的所有对象都保存在堆空间上。可以通过虚拟机参数 -Xms 修改堆的初始大小(total),通过 -Xmx 修改堆的最大大小(max)。
堆除了存储普通的对象,还存储了字符串常量池。字符串常量池主要存储了字符串字面值,从而实现字符串的重用。可以通过 intern() 方法手动将堆中字符串的引用放入字符串常量池。
当虚拟机要使用一个类时,它会读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量(运行时常量池)、静态变量、即时编译器编译后的代码缓存等数据。
方法区只是逻辑上的概念,类似于 C++ 中的自由存储区。JDK 8 以前方法区的实现为永久代,JDK8 以后其实现变为元空间。
java.lang.Class 对象。0xCAFEBABE 开头、主次版本号是否符合虚拟机要求)、元数据验证(例如类必须有父类)、字节码验证(例如方法内的执行指令跳转位置是否合理)、符号引用验证(例如类是否访问了其他类中的 private 方法)。final 修饰,由于其常量性,会直接初始化为代码中的指定值。new 创建一个类的对象。final 修饰的静态变量或者调用静态方法。Class.forName(String className)。main() 函数所在的类。static final 常量(常量需要为基本类型,如果为引用类型仍然会触发初始化)。java.lang.Class 对象没有在任何地方被引用。java.lang 和 java.util 等)以及被 -Xbootclasspath 参数指定的路径下的所有类。$JAVA_HOME/jre/lib/ext 目录下的类,它们通用但不重要,如 javax.swing 和 javax.servlet 等)以及被 -Djava.ext.dirs 参数指定的路径下的所有类。双亲委派机制保证了类加载的安全性(所有核心类都有顶层类加载器加载,避免了恶意程序篡改核心类库),同时避免了类的重复加载。
双亲委派机制的执行流程:
loadClass() 方法,把这个请求委派给父类加载器去完成。这样所有的请求最终都会传送到顶层的启动类加载器中。findClass() 方法来尝试自己去加载。ClassNotFoundException 异常。可达性分析算法的基本思想就是以一系列的 GC 根节点(GC Roots)对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC 根节点没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
可作为 GC 根节点的对象主要有以下几种:
java.lang.Class 对象。synchronized 关键字持有的对象。优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:由于内存是连续的,所以在对象被删除之后内存中会出现很多内存碎片。同时,由于内存碎片的存在,需要维护一个空闲链表对内存空间进行管理。
优点:不会产生内存碎片,同时内存利用率较高(相对于复制算法)。
缺点:整理阶段会有较大的性能开销。
优点:不会产生内存碎片,复制算法在复制时会将对象按顺序放入 To 空间,因此不存在内存碎片。
缺点:可用内存空间会缩小为总内存空间的一半,内存利用率低。同时,如果待复制的对象过大,复制开销也会增加。
分代垃圾回收将整个内存区域划分为年轻代(存放存活时间比较短的对象)和老年代(存放存活时间比较长的对象),其中年轻代还可以再分为 Eden、Survivor 0 和 Survivor 1 三个区。
父类和子类中的成员变量初始化顺序如下:
public class A {
private static final Out outStatic = new Out("父类静态变量");
static {
System.out.println("父类静态代码块");
}
private final Out out = new Out("父类成员变量");
{
System.out.println("父类构造代码块");
}
public A() {
System.out.println("父类构造函数");
}
}
public class B extends A {
static {
System.out.println("子类静态代码块1");
}
private static final Out outStatic = new Out("子类静态变量");
static {
System.out.println("子类静态代码块2");
}
public B() {
System.out.println("子类构造函数");
}
{
System.out.println("子类构造代码块1");
}
private final Out out = new Out("子类成员变量")
{
System.out.println("子类构造代码块2");
}
}
public class Out {
public Out(String s) {
System.out.println(s);
}
}
父类静态变量
父类静态代码块
子类静态代码块1
子类静态变量
子类静态代码块2
父类成员变量
父类构造代码块
父类构造函数
子类构造代码块1
子类成员变量
子类构造代码块2
子类构造函数
Java 中的 final 关键字:
final 修饰变量表示该变量是一个常量,其值不可修改。常量在声明时必须初始化,一旦赋值后,就不能再次修改。final 修饰方法表示该方法不能被子类重写(覆盖)。final 修饰类表示该类不能被继承,即不能有子类。final 修饰方法参数表示该参数是只读的,不能在方法内部被修改。被 abstract 修饰的类即抽象类,一个抽象类中至少有一个抽象方法(通过关键字 abstract 定义,并且没有方法体)。从语法上来说,抽象类也可以没有抽象方法,但是这样的抽象类完全没有意义。
接口由 interface 关键字定义,接口可以包含变量和方法,变量会被隐式指定为 public static final 且只能是 public static final,方法会被隐式指定为 public abstract 且只能是 public abstract。
接口和抽象类的共同点:
default 关键字指定默认的实现。接口和抽象类的区别:
public abstract 抽象方法和 public static final 常量。volatile 关键字的作用:
volatile 时,线程在读取该变量的值时,会直接从主内存中读取,而不是从线程的本地缓存中读取。同样,在写入该变量的值时,会立即写入主内存,而不是仅仅更新线程的本地缓存。这确保了当一个线程修改了变量的值时,其他线程能够立即看到这个变化。volatile 关键字还禁止了编译器和处理器对指令的重排序,从而保证在多线程环境下,特定操作的执行顺序不会被改变。这对于某些并发场景下的正确性非常重要。String 是不可变的,可以将其理解为一个常量,每次修改都会产生一个新的 String 对象,而不会直接修改原有数据,也因此是线程安全的。
String 不可变主要有以下两点原因:
private 修饰且没有提供任何能够直接修改底层数组的方法。String 类本身被 final 修饰,从而避免了子类破坏其不可变性,注意:String 底层数组被 final 修饰并不是其不可变的根本原因,因为 final 数组只能保证数组引用不被修改,无法保证里面的元素不能修改。
final int[] arr = new int[]{1, 2, 3};
arr = new int[3]; // 无法将值赋给 final 变量 'arr'
arr[1] = 5; // 成功执行
System.out.println(Arrays.toString(arr)); // [1, 5, 3]
String 不可变且线程安全,但也意味着每次修改都会生成一个新的字符串对象,因此修改的性能比较低,尤其是在需要进行大量字符串修改的情况下。
StringBuilder 和 StringBuffer 均继承自 AbstractStringBuilder,他们的修改都是基于类本身进行的修改,因此修改性能较高。二者的唯一区别是 StringBuffer 线程安全。
因此:
String。StringBuilder。StringBuffer。String 的 + 和 += 为语法糖,底层会构造一个 StringBuilder 然后通过 append() 方法添加元素,因此对于少量的字符串拼接,使用 + 还是 StringBuilder 差别不大。
下面代码中的两种写法完全等价:
StringBuilder sb = new StringBuilder().append("1").append("2").append("3").append("4").append("5");
String s = sb.toString();
String s = "1" + "2" + "3" + "4" + "5";
但如果是在循环中进行字符串拼接,+ 会创建大量的无用对象,效率较低,因此优先考虑使用 StringBuilder。
参考: