ArrayList 内部基于动态数组 Object[]
实现,会根据实际存储的元素数量动态地扩容或缩容。不过 ArrayList 只能存储对象,对于基本数据类型,需要使用其对应的包装类,同时线程不安全。
ArrayList 有三个构造函数,其中以无参数构造方法创建 ArrayList 时,它会初始化一个空数组,但不分配实际容量,只有在添加第一个元素时,数组的容量才会扩展为默认大小(通常为 10)。
ArrayList()
ArrayList(int initialCapacity)
ArrayList(Collection<? extends E> c)
ArrayList 在空间不足时会进行动态扩容,扩容时首先会将容量变为原来的 1.5 倍左右(奇数会丢掉小数),然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量。
Vector 与 ArrayList 类似也采用 Object[]
实现,是 List 的古老实现类,同时线程安全,但是在增长时会以固定的幅度增加容量,而不是按倍数增加,这可能导致一些内存浪费,因此通常更倾向于使用 ArrayList,并使用显式的同步措施来确保线程安全性。
LinkedList 底层使用的是双向链表,不过需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且性能通常会更好。同时 ArrayList 也不是线程安全的。
ArrayDeque 和 LinkedList 都实现了 Deque 接口,ArrayDeque 基于动态数组实现,并且是循环数组,在队满时会扩容为原来的两倍。LinkedList 基于链表实现,速度较慢且存储密度低。因此一般优先考虑 ArrayDeque,不过 ArrayDeque 不支持存储 null
且线程不安全。
ArrayDeque 主要通过 addFirst()
、addLast()
、removeFirst()
和 removeLast()
实现双端队列的相关操作,这四个方法失败时都会抛出异常。
Set 主要包括 HashSet、LinkedHashSet 和 TreeSet,它们都能保证元素唯一,并且都不是线程安全的。
三者主要区别在于底层数据结构不同:
HashMap 是一个无序的键值对集合,可以存储 null
的 key 和 value,但 null
作为键只能有一个,null
作为值可以有多个。
创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16,之后每次扩充,容量翻倍。创建时如果给定了初始容量值,HashMap 会将其扩充至 2 n 2^n 2n 大小(主要是为了在减少哈希碰撞同时提高哈希运算的效率,如果数组的长度为 2 n 2^n 2n,那么在映射时只需要直接取键的低 n 位即可,通过位运算即可实现,不需要取余)。
HashMap 基于数组 + 链表 / 红黑树实现,默认采用数组 + 链表,当链表长度大于 8 时会通过 treeifyBin()
处理哈希冲突,如果此时数组长度小于 64 会优先对数组进行扩容,否则会将链表转化为红黑树以减少搜索时间。
HashMap 是非线程安全的,其并发版本为 ConcurrentHashMap。ConcurrentHashMap 通过 synchronized
锁定链表或红黑二叉树的首节点,从而保证并发安全。不过 ConcurrentHashMap 的 key 和 value 均不能为 null
,因为多线程情况下无法准确判断 null
表示的是不存在还是存在一个空的键或值。同时,ConcurrentMap 保证的只是单次操作的原子性,而不是多次操作,因此类似于先通过 containsKey()
判断键是否存在然后再通过 put()
插入元素的操作都是线程不安全的。如果想要执行复合操作,可以通过 putIfAbsent()
等复合函数代替。
LinkedHashMap 和 HashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。
LinkedHashMap 提供了两种顺序迭代元素的方式:
accessOrder
参数指定按照访问顺序迭代元素。当 accessOrder
为 true
时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。isEmpty()
方法,而不是 size() == 0
,因为 isEmpty()
方法的可读性更好并且效率更高,size() == 0
在某些情况下还要进行类型转换。collection.toArray(T[] array)
方法,有以下两种使用方式。import java.util.*;
class Solution {
public static void main(String[] args) {
List 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` 的一个内部类,因此需要一次附加的转换。import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[] {1, 2, 3};
List list = Arrays.asList(array);
System.out.println(list); // [1, 2, 3]
System.out.println(list.getClass()); // class java.util.Arrays$ArrayList
List trueList = new ArrayList<>(list);
trueList.remove(2);
System.out.println(trueList); // [1, 2]
System.out.println(trueList.getClass()); // class java.util.ArrayList
}
}
函数式接口和 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
});
}
}
方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 ::
表示,主要分为静态方法引用、实例方法引用、特定类型的方法引用和构造器引用。
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;
}
}
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() 方法) |
|
加工元素并返回新流 |
static |
合并 a 和 b 为一个流 |
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() |
把流中的元素收集到数组 |
java.lang.Throwable
是所有异常和错误的父类,它有 Exception 和 Error 两个子类,其中异常又可以分为受检查异常和不受检查异常。
NullPointerException
、IllegalArgumentException
、ArrayIndexOutOfBoundsException
和 ClassCastException
等异常。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
语句会被忽略。
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());
}
}
反射机制允许我们在运行时获取类的信息、调用类的方法、操作类的属性,而无需在编译时知道类的具体名称,但存在一定的安全问题,同时会影响性能。
获取 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 运行时数据区域主要分为程序计数器、虚拟机栈、本地方法栈、堆和元空间,其中只有堆和元空间为线程共享。
每个线程会通过自己的程序计数器记录当前要执行的字节码指令的地址。程序计数器一方面能够控制指令的执行顺序,实现分支、跳转和异常等逻辑,另一方面也能够在多线程执行情况下为当前线程记录 CPU 切换前指令的执行位置。
虚拟机栈的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。它由一个个栈帧组成,调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁。如果栈的大小不支持动态扩展,发生栈溢出时就会抛出 StackOverFlowError。如果栈的大小支持动态扩展,在扩展过程中无法申请到足够的内存空间时就会抛出 OutOfMemeoryError,可以通过虚拟机参数 -Xss
修改栈的大小。
每个栈帧由局部变量表、操作数栈和帧数据三部分组成:
try
代码块的覆盖范围以及出现或未出现异常时需要跳转到的字节码指令的地址。本地方法栈存储的是 native 本地方法的栈帧,不过在 HotSpot 虚拟机中,Java 虚拟机栈和本地方法栈使用的是同一个栈空间。
堆内存是空间最大的一块内存区域,创建出来的所有对象都保存在堆空间上。可以通过虚拟机参数 -Xms
修改堆的初始大小(total),通过 -Xmx
修改堆的最大大小(max)。
堆除了存储普通的对象,还存储了字符串常量池。字符串常量池主要存储了字符串字面值,从而实现字符串的重用。可以通过 intern()
方法手动将堆中字符串的引用放入字符串常量池。
元空间是在 JDK8 之后对方法区的具体实现(方法区只是逻辑上的概念,类似于 C++ 中的自由存储区),元空间使用的是直接内存。
元空间主要保存了类的基本信息(元信息)与运行时常量池:
java.lang.Class
对象。0xCAFEBABE
开头、主次版本号是否符合虚拟机要求)、元数据验证(例如类必须有父类)、字节码验证(例如方法内的执行指令跳转位置是否合理)、符号引用验证(例如类是否访问了其他类中的 private 方法)。java.lang.Class
对象没有在任何地方被引用。主动引用包括:
new
创建一个类的对象。Class.forName(String className)
。被动引用包括:
java.lang
和 java.util
等)以及被 -Xbootclasspath
参数指定的路径下的所有类。$JAVA_HOME/jre/lib/ext
目录下的类,它们通用但不重要)以及被 -Djava.ext.dirs
参数指定的路径下的所有类。双亲委派机制保证了类加载的安全性(所有核心类都有顶层类加载器加载,避免了恶意程序篡改核心类库),同时避免了类的重复加载。
双亲委派机制的执行流程:
loadClass()
方法,把这个请求委派给父类加载器去完成。这样所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。findClass()
方法来尝试自己去加载。loadClass()
方法即可打破双亲委派机制。Tomcat 就通过自定义类加载器为每个 Web 应用指定独立的类加载器,从而实现了应用之间类的隔离。可达性分析算法的基本思想就是以一系列的 GC 根节点(GC Roots)对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC 根节点没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
可作为 GC 根节点的对象主要有以下几种:
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:由于内存是连续的,所以在对象被删除之后内存中会出现很多内存碎片。同时,由于内存碎片的存在,需要维护一个空闲链表对内存空间进行管理。
优点:不会产生内存碎片,复制算法在复制时会将对象按顺序放入 To 空间,因此不存在内存碎片。
缺点:可用内存空间会缩小为总内存空间的一半,内存利用率低。同时,如果待复制的对象过大,复制开销也会增加。
优点:不会产生内存碎片,同时内存利用率较高(相对于复制算法)。
缺点:整理阶段会有较大的性能开销。
分代垃圾回收将整个内存区域划分为年轻代(存放存活时间比较短的对象)和老年代(存放存活时间比较长的对象),其中年轻代还可以再分为 Eden、Survivor 0 和 Survivor 1 三个区。
参考:
https://javaguide.cn/home.html