总结了Java 面试指南 | JavaGuide的Java ⾯试中最最最常问的⼀些问题的答案及相关知识点,供自己查阅。目前本文章包含Java基础、Java集合和Java并发。后续会继续补充。
Java基础
1. Java 中的几种基本数据类型?
-
byte
:1字节,-2^7 到 2^7-1 -
short
:2字节,-2^15 到 2^15-1 -
int
:4字节,-2^31 到 2^31-1 -
long
:8字节,-2^63 到 2^63-1 -
float
:4字节,最小值2^−126×2^−23=2^−149≈1.4E−45,最大值(2−2^−23)×2^127≈3.4E38 -
double
:8字节,最小值2^-1074≈4.9E-324,最大值(2−2^−52)×2^1023≈1.8E308 -
char
:2字节,最小值\u0000
(十进制值为 0),最大值\uFFFF
(十进制值为 65,535) -
boolean
:1位,true/false
2. String 、 StringBuffer 和 StringBuilder ?
特性 | String |
StringBuffer |
StringBuilder |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全性 | 线程安全(不可变对象自动线程安全) | 线程安全(内部使用 synchronized ) |
不线程安全 |
性能 | 每次修改都会创建新的对象,性能较低 | 相比 String 性能更高,但因使用同步机制较慢 |
性能最优,适用于单线程环境 |
初始容量 | 不适用(固定不可变) | 默认初始容量 16,支持动态扩展 | 默认初始容量 16,支持动态扩展 |
扩容策略 | 不适用(不可变) | 当字符数超过容量时,容量增加一倍 | 当字符数超过容量时,容量增加一倍 |
适用场景 | 适合不可变字符串的操作,常用于常量或配置 | 适合需要线程安全的可变字符串操作 | 适合单线程环境下需要频繁修改字符串的操作 |
主要操作方法 | concat() 、replace() 、substring() 等 |
append() 、insert() 、delete() 、reverse() 等 |
append() 、insert() 、delete() 、reverse() 等 |
线程安全细节 | 不适用 | 使用 synchronized 锁保护方法 |
不使用锁,性能更好 |
3. String s1 = new String("abc")?
这段代码可能会创建一个或两个字符串对象,具体取决于是否字符串 "abc"
已经存在于字符串常量池中。
String s1 = new String("abc");
-
如果
"abc"
已经存在于字符串常量池中,则new String("abc")
会创建一个新的String
对象在堆中。因此,总共会创建 1 个 新的String
对象。 -
如果
"abc"
不存在于字符串常量池中,则"abc"
会被添加到常量池中,然后在堆中创建一个新的String
对象。因此,总共会创建 2 个String
对象,一个在字符串常量池中,一个在堆中。
4. == 与 equals? hashCode 与 equals ?
==
vs equals
-
==
:比较的是两个对象的内存地址,即它们是否引用同一个对象。用于判断两个对象是否是同一个实例。 -
equals
:比较的是两个对象的内容是否相等。默认的实现是Object
类中的equals
方法,它实际上使用的是==
来比较对象的内存地址,但可以在自定义类中重写equals
方法来比较对象的实际内容。
hashCode
与 equals
-
hashCode
:hashCode
方法返回对象的哈希码(一个整数),它用于在哈希表中定位对象。 -
equals
:equals
方法用于判断两个对象是否相等(重写后)。
5. 包装类型的缓存机制?
-
缓存范围:
-
对于
Integer
类型,Java 缓存了从-128
到127
的整数值。这是因为这些值在大多数程序中非常常用,并且在这个范围内创建对象的开销较大,因此通过缓存来节省内存和提高性能。 -
对于
Character
类型,缓存了从\u0000
到\u007F
的字符(即 ASCII 范围内的字符)。 -
对于其他类型,如
Byte
和Short
,也有类似的缓存机制,但范围可能有所不同。
-
-
实现机制:
-
Integer.valueOf(int)
: 这个方法会先检查传入的整数是否在缓存范围内。如果是,它会返回缓存中的对象;如果不是,它会创建一个新的Integer
对象。 -
Character.valueOf(char)
: 类似地,对于Character
类型,valueOf
方法会检查字符是否在缓存范围内,并返回缓存的字符对象。
-
-
为什么缓存:
-
性能优化: 对于小范围的常用值,缓存可以减少对象创建的次数,从而提高性能。
-
内存节约: 缓存可以避免重复创建相同值的对象,从而节省内存。
-
-
示例:
Integer a = 100; Integer b = 100; System.out.println(a == b); // 输出 true,因为 100 在缓存范围内,a 和 b 指向同一个对象 Integer c = 200; Integer d = 200; System.out.println(c == d); // 输出 false,因为 200 不在缓存范围内,c 和 d 是不同的对象
注意事项
-
缓存的局限性: 缓存机制只适用于特定范围的值。超出缓存范围的值会创建新的对象,即使它们的值相同。
-
new
关键字: 使用new
关键字创建包装对象时(如new Integer(100)
),不会使用缓存机制,而是每次都创建新的对象。
6.自动装箱与拆箱
自动装箱:当将基本数据类型赋值给其对应的包装类对象时,Java 编译器会自动调用包装类的 valueOf
方法,将基本数据类型转换为包装类型对象。示例:
int a = 10;
Integer b = a; // 自动装箱,将 int 转换为 Integer
Integer b = Integer.valueOf(a); //原理
Integer.valueOf(int)
方法会先检查值是否在缓存范围内(-128 到 127),如果在,则返回缓存中的 Integer
对象,否则创建新的 Integer
对象。
自动拆箱:当将包装类型对象赋值给基本数据类型或在表达式中使用时,Java 编译器会自动调用包装类的 xxxValue()
方法,将包装类型对象转换为基本数据类型。示例:
Integer a = 10;
int b = a; // 自动拆箱,将 Integer 转换为 int
int b = a.intValue(); //原理
注意事项
-
性能影响:
-
自动装箱和拆箱会导致隐式的对象创建和方法调用,可能会影响性能,特别是在高频率的装箱和拆箱操作中。
-
尤其在使用集合类(如
List<Integer>
)时,频繁的装箱和拆箱可能会导致不必要的对象分配和垃圾回收。
-
-
空指针异常(NullPointerException):
-
自动拆箱可能会导致空指针异常。例如,如果
Integer
对象为null
,尝试将其自动拆箱为int
时会抛出NullPointerException
。
Integer a = null; int b = a; // 这里会抛出 NullPointerException
-
-
比较操作:
自动装箱时需要注意使用==
比较两个包装对象时,可能会出现意外结果。因为==
比较的是对象的引用,而非值。要正确比较值,需要使用equals
方法。
7.深拷贝和浅拷贝区别
浅拷贝:复制对象时,基本类型字段被复制,但引用类型字段只复制引用。两个对象共享引用类型的成员,指向同一个对象。 实现方法:.clone()方法、构造函数复制
深拷贝:复制对象时,基本类型字段被复制,引用类型字段也会复制为新的对象副本。两个对象完全独立,不共享任何引用类型的成员。 实现方法:递归调用clone方法、使用序列化和反序列化实现深拷贝
8.谈谈对 Java 注解的理解
Java 注解(Annotations)是一种用于提供元数据的机制,允许开发者在代码中添加额外的信息而不影响实际的业务逻辑。注解在 Java 5 中被引入,用来替代 Java 中的标记接口和配置文件,增强了代码的可读性和灵活性。
注解是以 @
符号开头的,通常位于类、方法、字段、参数等的上方,注解本身并不会直接影响代码的执行,但它可以被编译器或运行时环境读取,并根据注解的信息采取相应的措施。示例:
@Override
public String toString() {
return "Example";
}
上面这个 @Override
注解告诉编译器这个方法是从父类或接口中重写的,如果没有正确重写,编译器会报错。
Java 注解解决了什么问题?
-
替代配置文件:
-
在 Java EE 和 Spring 等框架中,注解通常用来替代繁琐的 XML 配置文件。通过注解,配置可以直接和代码放在一起,减少了配置错误和冗余,提高了配置的可维护性。例如,Spring 中的
@Autowired
用于自动注入依赖,代替 XML 中的<bean>
配置。
@Autowired private MyService myService;
-
-
元数据提供:
-
注解允许开发者为代码添加元数据,这些元数据可以在编译时、类加载时或运行时通过反射机制被读取和处理。比如,
@Deprecated
注解标记某个方法已经过时,不推荐使用,这样在使用该方法时编译器就会发出警告。
@Deprecated public void oldMethod() { // Deprecated method }
-
-
简化代码:
-
注解简化了代码,通过注解的使用,可以减少代码重复,增强代码的可读性和维护性。比如,JPA 的
@Entity
注解将类声明为实体类,@Table
注解指定数据库表名等,大大简化了数据库表映射的工作。
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; }
-
-
提高编译期的安全性:
-
通过注解,可以在编译期进行一些检查,减少运行时错误。例如,
@Override
注解用于检测方法是否正确重写父类方法,防止拼写错误或方法签名不匹配等问题。
-
-
框架和库的扩展:
-
许多框架和库使用注解来简化开发工作。例如,JUnit 中的
@Test
注解标记测试方法,框架会自动识别并执行这些测试。注解让框架和库能够以一种简洁、灵活的方式进行扩展和定制。
-
-
编译时和运行时处理:
-
注解可以在编译时通过注解处理器进行处理(如
@Retention(RetentionPolicy.SOURCE)
),也可以在运行时通过反射机制进行处理(如@Retention(RetentionPolicy.RUNTIME)
)。这为开发者提供了灵活的手段来控制代码的行为。
-
常见的 Java 注解
-
内置注解:
-
@Override
: 用于标记重写父类方法。 -
@Deprecated
: 用于标记过时的方法、类或字段。 -
@SuppressWarnings
: 用于告诉编译器忽略特定的警告。
-
-
元注解:
-
@Retention
: 指定注解的保留策略(SOURCE、CLASS、RUNTIME)。 -
@Target
: 指定注解可以应用的代码元素(类、方法、字段等)。 -
@Inherited
: 指定注解是否可以被子类继承。 -
@Documented
: 指定注解是否会包含在 JavaDoc 中。
-
-
自定义注解:
-
开发者可以根据需求定义自己的注解,用来标记和处理特定的代码逻辑。例如,可以定义一个
@MyAnnotation
来标记需要特殊处理的方法。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value(); }
-
9.Exception
和 Error
的区别
Exception
和 Error
是 Java 中异常处理机制的重要组成部分,它们都是继承自 Throwable
类,但在概念和使用上有显著区别。
Throwable
是 Java 中所有异常和错误的超类,它有两个直接子类:Exception
和 Error
。
1.Exception
-
定义:
Exception
表示程序中可以捕获并处理的异常。它通常是由于代码错误或其他可预见的情况引发的,例如试图访问一个不存在的文件、除零操作、数组下标越界等。 -
子类:
-
Checked Exception(受检异常): 必须在编译时处理(即用
try-catch
块捕获或用throws
声明)。例如:IOException
,SQLException
。 -
Unchecked Exception(未受检异常): 是
RuntimeException
的子类,不需要在编译时显式捕获。例如:NullPointerException
,ArrayIndexOutOfBoundsException
。
-
-
处理方式: 开发者可以通过
try-catch
语句捕获和处理这些异常,也可以通过在方法签名中使用throws
关键字将异常抛出给调用者处理。
示例
try {
int result = 10 / 0; // 可能抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Caught an ArithmeticException: " + e.getMessage());
}
2.Error
-
定义:
Error
表示严重的错误,这类错误通常是由于运行环境的问题导致的,程序无法合理地从这些错误中恢复。典型的例子包括内存不足 (OutOfMemoryError
)、栈溢出 (StackOverflowError
) 等。 -
子类:
Error
的子类包括OutOfMemoryError
,StackOverflowError
,LinkageError
等。 -
处理方式: 一般不应该捕获
Error
,因为它们通常表示系统级别的错误,捕获和处理它们可能会隐藏更严重的问题,甚至导致系统不稳定。通常来说,这类错误应交由 JVM 处理。
示例
public class StackOverflowDemo { public static void recursiveMethod() { recursiveMethod(); // 递归调用导致 StackOverflowError } public static void main(String[] args) { try { recursiveMethod(); } catch (StackOverflowError e) { System.out.println("Caught a StackOverflowError: " + e.getMessage()); } } }
区别总结:
-
Exception
: 是开发者用于处理异常情况的工具,使得程序能够处理错误并继续执行。 -
Error
: 是 JVM 用来表示程序无法继续执行的严重问题。
在编写代码时,开发者通常只需要关注 Exception
及其子类的处理,而不需要显式地处理 Error
类及其子类。
10. Java 反射的概念(不懂
反射(Reflection)是Java的一项强大特性,它允许程序在运行时动态地获取类的结构信息并操作类的成员(如属性、方法、构造器等)。通过反射,程序可以在编译时不知道类的具体信息的情况下,在运行时对类进行操作。
反射的基本操作
获取类的 Class 对象
每个 Java 类都有一个 Class
对象,包含了与该类相关的元数据。可以通过以下方式获取:
Class<?> clazz = Class.forName("com.example.MyClass"); // 使用类的全限定名:包名.子包名.类名
Class<?> clazz2 = MyClass.class; // 直接使用类名.class
Class<?> clazz3 = instance.getClass(); // 使用对象.getClass()
获取类的信息
可以通过反射获取类的构造函数、方法、字段、注解等信息。
Method[] methods = clazz.getDeclaredMethods(); // 获取所有方法
Field[] fields = clazz.getDeclaredFields(); // 获取所有字段
Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // 获取所有构造函数
操作类的成员
通过反射,可以动态调用方法、设置字段值或创建类的实例。
Method method = clazz.getDeclaredMethod("methodName", String.class);
method.invoke(instance, "parameterValue"); // 动态调用方法
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 绕过访问控制检查
field.set(instance, "newValue"); // 动态设置字段值
反射的用途
-
框架和库
-
反射在框架中很常用,例如 Spring、Hibernate 等。它们利用反射来实例化对象、调用方法、依赖注入等,而不需要在编译时知道具体的类。
-
-
动态代理
-
反射用于动态生成代理类,动态代理在 AOP(面向切面编程)中尤为常见。代理类可以在运行时动态创建,处理方法调用并添加额外的逻辑。
-
-
序列化和反序列化
-
在 JSON 序列化工具(如 Jackson、Gson)中,反射用于动态地将对象转换为 JSON 字符串,或将 JSON 字符串转换为对象。
-
-
开发工具和 IDE
-
开发工具和 IDE 使用反射来提供功能,如代码自动补全、调试器的变量查看、JVM 内部状态的检查等。
-
反射的缺点
-
性能开销:反射操作比直接调用方法或访问字段慢得多,因为它绕过了常规的编译期优化。频繁使用反射可能会导致性能瓶颈。
-
安全问题:反射允许访问和修改私有成员,这可能破坏类的封装性和安全性。如果滥用,可能导致不可预知的错误或安全漏洞。
-
编译时类型检查缺失:由于反射是在运行时执行的,编译器无法对反射的操作进行类型检查。很多错误会在运行时才会暴露出来,增加了调试的复杂性。
-
可维护性降低:反射代码可能较难理解和维护,特别是当其被广泛使用时,会使得代码变得复杂且难以跟踪。
为什么框架需要反射?
框架使用反射主要是为了提供灵活性和动态性,使得程序员可以编写更少的代码,同时实现复杂的功能。例如,Spring 框架使用反射来自动装配依赖、管理 Bean 的生命周期,和实现 AOP 功能。这使得开发者可以专注于业务逻辑,而不需要处理繁琐的底层实现细节。
11. Java 泛型、类型擦除和通配符
泛型
Java 泛型是一种允许类、接口和方法在定义时使用类型参数的机制。泛型使得代码可以适用于多种数据类型,同时提供编译时的类型检查,增强了代码的安全性和可重用性。示例:
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
在这个示例中,Box
类使用了泛型类型参数 T
,因此可以在不修改 Box
类代码的情况下,创建适用于不同类型的 Box
对象。
类型擦除(Type Erasure)
类型擦除是 Java 泛型的一个重要特性。Java 编译器在编译过程中会删除或替换泛型信息,这意味着在运行时,所有的泛型类型参数都被擦除,转换为原始类型。
-
作用: 类型擦除允许 Java 泛型与现有的非泛型代码兼容,保持了 Java 的向后兼容性。
-
运行时表现: 由于类型擦除,泛型的类型参数在运行时并不存在。例如,
List<String>
和List<Integer>
在运行时都是List
,无法通过反射或其他方式区分它们。
类型擦除的过程: