面向对象的三大特性是什么?
- 封装:属性私有化,对外提供GET、SET方法
- 继承:子类继承父类的属性和方法。
- 多态 :父类型引用指向子类型对象,例如
Animal a = new Cate()
重载(Overload)和重写(Override)的区别?
答案:
- 重载:同一个类中,方法名相同,参数列表不同(类型、个数、顺序),与返回值无关。编译时多态。
- 重写:子类中,方法名、参数列表、返回值类型(或子类)完全相同,访问权限不能更严格,不能抛出更大的异常。运行时多态。
接口和抽象类的区别?
答案:
| 特性 | 抽象类 | 接口(JDK8+) |
|---|---|---|
| 关键字 | abstract class |
interface |
| 构造方法 | 可以有 | 不能有 |
| 实例变量 | 可以有 | 只能有 static final 常量 |
| 普通方法 | 可以有具体方法 | 抽象方法、默认方法(default)、静态方法 |
| 继承/实现 | 单继承 | 多实现 |
| 访问修饰符 | 任意 | 方法默认 public abstract(可写可不写) |
| 新增方法 | 直接加,不影响子类 | 加抽象方法会破坏实现类,但加默认方法不影响 |
| 使用场景 | 表示一种"is-a"关系 | 表示一种"can-do"能力或契约 |
为什么 Java 是单继承,但可以实现多个接口?
- 多继承会带来菱形继承问题(如 C++),造成方法调用的歧义和状态的重复。
- 接口没有状态(属性),只有方法声明,因此多接口相对安全。
Java 8 后默认方法带来状态,如果两个没有继承关系 的接口提供了同名同参数的默认方法,实现类必须显式覆盖该方法,否则编译错误。
java
interface A {
default void go() { System.out.println("A"); }
}
interface B {
default void go() { System.out.println("B"); }
}
class C implements A, B {
// 编译错误!必须覆盖 go() 方法
@Override
public void go() {
A.super.go(); // 选择调用 A 的默认方法
// 或 B.super.go();
}
}
内部类有哪些?
- 成员内部类:普通内部类,可以访问外部类的所有成员(包括 private)。
- 静态内部类:不持有外部类引用,相当于独立类。
- 局部内部类:定义在方法内部,只能在该方法内使用。
- 匿名内部类:没有名字的类,常用于实现接口
静态内部类和非静态内部类(成员内部类)的区别?TODO
| 特性 | 静态内部类 | 非静态内部类(成员内部类) |
|---|---|---|
| 是否持有外部类引用 | 否 | 是(隐含 Outer.this) |
| 实例化方式 | new Outer.StaticInner() |
outer.new Inner() |
| 能否有静态成员 | 可以 | 不能(JDK 16 前) |
| 能否访问外部类静态成员 | 可以 | 可以 |
| 能否访问外部类实例成员 | 不能(无外部类引用) | 可以 |
| 使用场景 | 辅助类,与外部类实例无关 | 需要访问外部类实例状态 |
this() 和 super() 可以同时出现在一个构造器中吗?为什么?
不能 。因为 this() 和 super() 都必须作为构造器的第一条语句,二者矛盾。
this()调用当前类的其他构造器。super()调用父类的构造器。- 如果都没有显式写出,编译器会默认插入
super()。
代码块(静态、实例、构造器)的执行顺序?
父类 → 子类,静态 → 实例 → 构造器。
- 父类静态代码块
- 子类静态代码块
- 父类实例代码块
- 父类构造器
- 子类实例代码块
- 子类构造器
每次创建对象时,实例代码块和构造器都会执行;静态代码块只在类首次加载时执行一次。
static 关键字的作用有哪些?
- 静态变量:所有该类的对象共享同一个变量,内存中只有一份。
- 静态方法 :不需要创建对象,直接通过
类名.方法名()就能调用。 - 静态代码块:类加载时执行一次,用于初始化静态资源。
- 静态内部类 :被
static修饰的内部类。它不依赖外部类的对象,可以独立创建实例。
final 修饰的变量就是常量吗?修饰引用类型时有什么陷阱?
final修饰基本类型:值不可变,是真正的常量。final修饰引用类型:不能指向其他对象,但对象内部属性可以改变。
java
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // 允许
sb = new StringBuilder("new"); // 编译错误
Object 类中常用的方法有哪些?(至少说出 5 个)
equals(Object obj):判断对象是否相等。hashCode():返回哈希码,与equals协同。toString():返回对象的字符串表示,通常重写。getClass():返回运行时类对象。clone():创建并返回对象副本(需实现Cloneable接口)。finalize():GC 前调用(已过时,不推荐使用)。wait()/notify()/notifyAll():线程同步(多线程范畴,了解即可)。
枚举(enum)是什么?有什么优点?
-
枚举是一种特殊的类,用于定义固定数量的常量集合(如季节、星期、状态码)。
-
优点:
- 类型安全,编译期检查。
- 可以添加字段、方法、构造器,实现接口。
- 单例模式的首选方案(
Enum保证序列化/反射安全)。 - 自带
values()方法遍历所有枚举值。
说明:
类型安全意味着:一个变量只能被赋予其类型所允许的值,任何不匹配的赋值或操作都会被编译器阻止,不会在运行时才出现意外。
对于枚举而言:
- 枚举变量只能指向该枚举类型中定义的常量之一,或者
null(虽然允许null,但通常避免)。 - 不能赋值为其他类型的值(如整数、字符串、其他枚举常量)。
java// 限制赋值:拒绝非法值 public enum Season { SPRING, SUMMER, AUTUMN, WINTER } Season s = Season.SPRING; // ✅ 正确 Season s = SPRING; // ✅ 正确(静态导入后) Season s = "SPRING"; // ❌ 编译错误:类型不匹配(String 无法转为 Season) Season s = 1; // ❌ 编译错误:int 无法转为 Season Season s = null; // ✅ 允许(但使用时需判空) Season s = Season.MONDAY; // ❌ 编译错误:MONDAY 不是 Season 的成员
Java 是值传递还是引用传递?
Java 只有值传递。
- 基本类型传递的是值的副本,修改形参不影响实参。
- 引用类型传递的是对象地址的副本,形参和实参指向同一对象,因此修改对象的属性会影响原对象;但重新赋值形参不会影响实参。
java
class Person {
String name;
Person(String name) { this.name = name; }
}
public class ReferenceTest {
public static void changeName(Person p) {
p.name = "李四"; // 通过地址副本找到同一个对象,修改属性
}
public static void reassign(Person p) {
p = new Person("王五"); // 只改变形参的指向,不影响实参
}
public static void main(String[] args) {
Person person = new Person("张三");
changeName(person);
System.out.println(person.name); // 李四 ------ 对象属性被修改
reassign(person);
System.out.println(person.name); // 仍然是李四,实参未指向新对象
}
}
1. Java 的基本数据类型有哪些?对应的包装类是什么?
| 基本类型 | 字节数 | 包装类 |
|---|---|---|
byte |
1 | Byte |
short |
2 | Short |
int |
4 | Integer |
long |
8 | Long |
float |
4 | Float |
double |
8 | Double |
char |
2(Unicode) | Character |
boolean |
通常 1 字节 | Boolean |
注意:
boolean在 JVM 中通常用int表示,单个boolean占 4 字节,数组中的每个元素占 1 字节。
什么是自动装箱与拆箱?
- 装箱 :基本类型 → 包装类(如
int→Integer),编译期调用Integer.valueOf()。 - 拆箱 :包装类 → 基本类型(如
Integer→int),编译期调用xxxValue()。
包装类常量池
Integer、Short、Byte、Long、Character都缓存了-128 ~ 127之间的值。- 例:
Integer a = 100; Integer b = 100; a == b为true;Integer c = 128; Integer d = 128; c == d为false(超出缓存范围,创建新对象)。
为什么需要包装类?
- 支持泛型和集合:泛型和集合只能使用引用数据类型,无法使用基本类型。
- 引用数据类型可以表达
null值。 - 包装类提供了更丰富的
API。
Integer 的 valueOf() 和 parseInt() 的区别?
parseInt(String s):返回int基本类型 。
解析字符串为十进制有符号整数,无法解析时抛出NumberFormatException。valueOf(String s):返回Integer包装类对象 。
内部实际调用了parseInt()获取int值,然后通过Integer.valueOf(int)装箱成Integer对象,因此会利用到Integer的缓存机制(-128~127)。
Integer 类中 int 的最大值和最小值是多少?如何获取?
- 最大值 (
MAX_VALUE) :2^31 - 1,即 2,147,483,647 。获取:Integer.MAX_VALUE; - 最小值 (
MIN_VALUE) :-2^31,即 -2,147,483,648 。获取:Integer.MIN_VALUE;
对包装类进行算术运算(如 Integer i = 1; i++)发生了什么?
对包装类(如 Integer)进行算术运算(例如 i++ 或 i = i + 1),其本质是自动拆箱、基本类型运算、自动装箱三个步骤的结合。
short s = 1; s = s + 1; 和 short s = 1; s += 1; 哪个正确?为什么?
s = s + 1编译错误 :s + 1自动提升为int,赋值给short需要强制转型。s += 1正确 :+=是 Java 规定的复合赋值运算符,隐含了强制转型,相当于s = (short) (s + 1)。
switch 语句支持哪些数据类型?
- 原始支持:
byte、short、int、char。 - 包装类:
Byte、Short、Character、Integer(自动拆箱)。 String(Java 7+)。enum(枚举)。
switch 语句能作用于 long 类型吗?
在 Java 5 及之前:不能。Java 7 开始支持 String 和枚举,但仍然不支持 long 。支持的类型:byte、short、char、int、String、enum 以及它们的包装类(Byte、Short、Character、Integer)。
原因:switch 的底层使用 lookupswitch 或 tableswitch 指令,这些指令只支持 int 及以下类型。
break 与 continue 的区别?支持标签吗?
break:跳出当前循环体(或switch),不再执行剩余迭代。continue:跳过本次循环剩余语句,继续下一次迭代。- Java 支持带标签的
break和continue,可以跳出多层循环。
java
outer: for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) break outer;
}
}
& 和 && 的区别?| 和 || 的区别?
&&是短路与:左边为 false 时,右边不执行;&是逻辑与,两边都会执行。
||是短路或:左边为 true 时,右边不执行;|是逻辑或,两边都会执行。
float 和 double 的精度问题?如何解决?
float和double是二进制浮点数,无法精确表示某些十进制小数(如0.1),导致计算误差(如0.1 + 0.2 == 0.3返回false)。- 解决方法 :使用
BigDecimal,尤其是金融计算。
java
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("0.2");
System.out.println(bd1.add(bd2).equals(new BigDecimal("0.3"))); // true
注意:
BigDecimal构造器应使用String参数,避免使用double构造器。
String为什么不可变?有什么好处?
- 底层数组被
final和private修饰 :
String 内部使用一个数组来存储字符数据(JDK 8 及以前是char[] value,JDK 9 及以后优化为byte[] value)。这个数组被private修饰,外部无法直接访问;同时被final修饰,保证了数组的引用一旦初始化就永远不能指向其他数组。 - 没有提供任何修改内部数组的方法
- 类本身被
final修饰 :
String 类被final关键字修饰,意味着它不能被继承。这防止了子类通过继承来篡改 String 的不可变特性。
好处:1、实现字符串常量池,极大节省内存 2、保证线程安全
String、StringBuffer、StringBuilder 的区别?
答案:
String:不可变(final修饰字符数组),线程安全,适合少量字符串操作。StringBuffer:可变,线程安全(方法使用synchronized修饰)。因为加锁的开销导致性能略低。适合多线程环境下的字符串操作。StringBuilder:可变,非线程安全。性能最高,推荐在单线程下使用。
字符串常量池的作用?
- 节省内存空间:通过共享相同内容的字符串,确保它们在内存中只保留一份,避免重复创建对象。
- 提升程序性能 :可以直接使用
==进行高效的引用地址比较,从而避免使用.equals()逐个字符比对的开销。
字符串常量池的位置(随 JDK 版本变化)
| JDK 版本 | 存储位置 | 说明 |
|---|---|---|
| JDK 6 及以前 | 方法区(永久代) | 字符串常量池位于永久代(PermGen),与 Java 堆分离。 |
| JDK 7 | 堆(Heap) | 将字符串常量池从永久代移到堆中,因为永久代空间有限,移入堆可减少 OOM 风险。 |
| JDK 8+ | 堆(Heap) | 永久代被元空间(Metaspace)取代,但字符串常量池仍保留在堆中。 |
总结(当前主流环境 JDK 8+) :字符串常量池存储在 堆 中
String s = new String("abc") 创建了几个对象?
- 若字符串常量池中没有
"abc",则创建 2 个 :一个在堆中的String对象,一个在常量池中的"abc"字符串实例(实际上池中存储的是引用,但通常说创建了对象)。 - 若池中已存在,则只创建 1 个 (堆中的
String对象)。
== 和 equals() 的区别?
答案:
==:对于基本类型(int, char, boolean 等),比较的是值 ;对于引用类型(对象),比较的是内存地址(是否同一个对象)。equals():是Object类的方法,默认实现也是比较地址(==)。但很多类(如String、Integer、Date)重写了equals(),用于比较内容是否相等。
异常体系结构
Throwable
├── Error(严重错误,程序无法处理,如 OOM、StackOverflowError)
└── Exception
├── 编译时异常(受检异常,如 IOException、SQLException)
└── RuntimeException(如空指针异常、数组越界异常)
受检异常(Checked Exception)和非受检异常(Unchecked Exception)的区别?
核心区别在于编译器是否强制要求开发者处理该异常
| 对比项 | 受检异常 | 非受检异常 |
|---|---|---|
| 是否需要处理异常 | 必须 try-catch 或 throws 声明 |
不强制处理 |
| 继承关系 | 继承自 Exception 类,但不包括 RuntimeException 及其子类 |
包括 RuntimeException 及其子类,以及 Error 及其子类。 |
| 常见例子 | IOException, ClassNotFoundException |
NullPointerException, 数组越界异常 |
3. finally 块一定会执行吗?
- 正常情况下会执行(无论是否发生异常,也无论
try中是否有return)。 - 以下几种情况不执行 :
- 在
try或catch中调用System.exit(0)。 - 虚拟机崩溃。
- 守护线程被强制终止。
- 在
4. try-with-resources 是什么?
- 用于自动关闭实现了
AutoCloseable接口的资源(如流、数据库连接)。 - 示例:
java
try (FileInputStream fis = new FileInputStream("test.txt")) {
// 使用 fis
} catch (IOException e) {
e.printStackTrace();
}
// fis 自动关闭
final、finally、finalize 的区别?
答案:
final:关键字。- 修饰类:类不能被继承。
- 修饰方法:方法不能被重写。
- 修饰变量:变量一旦赋值后不可修改(基本类型值不变,引用类型不能指向其他对象,但对象内部状态可变)。
finally:异常处理的一部分。无论是否捕获异常,finally块中的代码都会执行。通常用于释放资源(关闭文件、数据库连接)。finalize():Object类的方法。垃圾回收器在回收对象前会调用此方法(仅一次)。JDK 9 已标记为弃用。
try-catch-finally 中,如果 catch 和 finally 都有 return,哪个生效?
finally 中的 return 会覆盖 catch 中的 return 。
同样,finally 中的 return 也会覆盖 try 中的 return。
这是危险行为,编译器会警告,实际开发中应避免在 finally 中使用 return。
throws 和 throw 的区别?
throw 在方法体内实际抛出异常对象,throws 在方法签名处声明可能抛出的异常类型。
大量异常 catch 块的顺序有什么要求?
在多个 catch 块中,子类异常必须写在父类异常之前,否则编译报错。
对性能而言,try-catch 放在循环内部和外部哪个好?为什么?
放在循环外部更好 。原因:如果 try-catch 放在循环内部,每次循环迭代都会进入和退出 try 块,JVM 需要反复检查异常表结构,增加额外开销;而放在外部只需建立一次异常处理上下文,避免了这种重复成本。此外,如果循环内抛出异常,外部 catch 可以统一处理后终止或继续循环,逻辑也更清晰。
如何自定义异常?什么时候需要自定义异常?
自定义异常只需继承 Exception(受检异常)或 RuntimeException(非受检异常),并提供构造方法(通常提供无参构造和带消息的构造)。
java
// 自定义受检异常
public class BusinessException extends Exception {
public BusinessException() {}
public BusinessException(String message) {
super(message);
}
}
// 自定义非受检异常
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
什么时候需要自定义异常?
当标准异常(如 IllegalArgumentException)无法清晰表达业务错误含义时
异常处理中 e.printStackTrace() 有什么问题?生产环境应该怎么记录异常?
e.printStackTrace() 会输出到标准错误流(System.err)、难以集成日志系统且可能阻塞性能,生产环境应使用 SLF4J 等日志框架并调用 log.error("描述", e) 记录完整堆栈。
什么是泛型?泛型原理?类型擦除?List<String和List<Integer在运行中一样吗?什么情况下泛型擦除会出问题?
泛型是 JDK 5 引入的类型参数化机制,允许在定义类、接口、方法时使用类型参数(如 <T>),在使用时再传入具体类型(如 String、Integer)。
泛型的作用:
提供编译时类型安全检查:避免运行时出现 ClassCastException
避免强制类型转换:使用泛型后,从集合或泛型对象中获取数据就是目标类型,不需要强制类型转换。
代码复用:通过泛型编写通用算法/数据结构(如 ArrayList<T>),适用于多种类型。
Java 泛型的实现原理可以概括为:
编译时类型检查 + 运行时类型擦除。
类型擦除:编译后泛型信息被移除,替换为原始类型(Object或上限类型),并插入强制转换代码。
List 和 List 在运行时是同一个类型吗?为什么?
是,运行时会进行类型擦除,List<String> 和 List<Integer> 在运行时都是List。
类型擦除会导致哪些问题?
1、无法用泛型类型做 instanceof 检查
java
// 编译错误:Illegal generic type for instanceof
if (obj instanceof List<String>) { ... }
只能检查原始类型:obj instanceof List。
2、不能创建泛型数组
java
// 编译错误:Cannot create a generic array of T
T[] array = new T[10];
因为擦除后类型信息丢失,运行时无法知道应该分配什么类型的数组。可以创建 ArrayList<T> 代替。
3、不能实例化泛型类型(new T()、new T[])
java
public <T> void create() {
T obj = new T(); // 编译错误
}
泛型有哪些使用场景?
- 泛型类
场景:定义容器类、集合框架、工具类,使其能处理任意类型。
java
public class Box<T> {
private T item;
public void set(T t) { this.item = t; }
public T get() { return item; }
}
// 使用:Box<String> box = new Box<>();
- 泛型接口
场景:定义生成器、比较器、工厂等通用契约,让实现类指定具体类型。
java
public interface Comparator<T> {
int compare(T o1, T o2);
}
// 实现:class StringComparator implements Comparator<String>
- 泛型方法
场景:编写独立于类的通用算法,例如交换数组元素、集合转换等。
java
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
// 调用:String s = getMiddle("a", "b", "c");
什么是原始类型(Raw Type)?使用原始类型有什么风险?
原始类型 是指在使用泛型类或泛型接口时,省略了类型参数的写法。例如:
java
List list = new ArrayList(); // 原始类型,等价于 List<Object>
Comparable c = "hello"; // 原始类型
原始类型主要是为了 兼容 JDK 5 之前没有泛型的旧代码。
风险:丧失编译时的类型安全检查,可能导致运行时异常。
泛型通配符 ?、? extends T、? super T 的区别?
?:无界通配符,表示任意类型,只能读不能写(除了null)。? extends T:上界通配符,表示类型是T或T的子类。可以读为T,但不能写入(因为不知道具体子类型)。? super T:下界通配符,表示类型是T或T的父类。可以写入T及其子类,但读取时只能得到Object。- PECS 原则 :Producer Extends, Consumer Super(生产数据用
extends,消费数据用super)。
怎么理解生产者?
在泛型编程中,当我们声明一个 List<? extends Number> 时,这个列表是作为参数传入当前方法的。对于当前方法而言,它不需要往里面塞东西,而是需要从中**获取(提取)已有的数据来进行计算或处理。
因为它是向当前方法提供、产出(Produce)**数据的源头,所以被称为"生产者"。
1. 无界通配符 ?:我只负责"看",不负责"装"
场景:你需要写一个通用的工具方法,比如打印任意类型的集合,或者判断集合是否为空。你根本不关心集合里装的是猫、狗还是数字。
java
public void printList(List<?> list) {
// ✅ 能读:因为不管里面是什么,它一定是一个 Object
for (Object obj : list) {
System.out.println(obj);
}
// ❌ 不能写:编译器彻底懵了,它不知道这个 List 原本装的是 String 还是 Integer
// list.add("hello"); // 编译报错!万一你传进来的是 List<Integer> 呢?
// ✅ 唯一能写的只有 null,因为 null 是所有类型的子集
list.add(null);
}
核心逻辑 :用了 ?,等于告诉编译器:"我也不知道这里面是啥"。所以编译器为了绝对安全,除了 null,禁止你往里面放任何东西。
2. 上界通配符 ? extends T:只进不出(Producer)
场景 :假设有一个动物继承体系:Animal(父类)、Dog(子类)、Cat(子类)。你需要写一个方法,接收一堆动物并让它们叫。
java
public void makeSound(List<? extends Animal> animals) {
// ✅ 能读:虽然我不知道具体是 Dog 还是 Cat,但我知道它们肯定都是 Animal
Animal a = animals.get(0);
a.shout();
// ❌ 不能写:编译器会想,"万一你传进来的是一个专门装 Cat 的笼子(List<Cat>),
// 我却让你塞进去一只 Dog,那这个笼子不就乱套了吗?"
// animals.add(new Dog()); // 编译报错!
}
核心逻辑 :extends 限制了上限。因为无法确定具体的子类是谁,为了防止把"狗"塞进"猫"的笼子里,编译器直接禁止写入。它就像一个生产者(Producer),只负责往外提供数据。
3. 下界通配符 ? super T:只出不进(Consumer)
场景 :你需要写一个方法,往一个集合里批量添加一堆 Dog 对象。
java
public void addDogs(List<? super Dog> dogs) {
// ✅ 能写:编译器会想,"这个笼子要么是装 Dog 的,要么是装 Dog 的父类(比如 Animal 或 Object)的。
// 那我往里面塞 Dog 或者 Dog 的子类(比如 Puppy),肯定都是安全的!"
dogs.add(new Dog());
dogs.add(new Puppy());
// ❌ 不能精准读:编译器会想,"这个笼子可能是 List<Object>,
// 我如果让你读出来直接当 Dog 用,万一里面其实混进了一个 Cat 怎么办?"
// Dog d = dogs.get(0); // 编译报错!
// ✅ 只能读成 Object,因为 Object 是所有类的老祖宗,最安全
Object obj = dogs.get(0);
}
核心逻辑 :super 限制了下限。因为保证了容器里的东西一定比 Dog 更"大"或相等,所以往里塞 Dog 绝对没问题。它就像一个消费者(Consumer),只负责把数据吃进去。
无界通配符 ? 和 Object 作为类型参数有什么区别?
<?>(无界通配符):代表一个**"未知但确定"**的具体类型。它表示这个容器内部装的是某种特定的数据类型(可能是 String,也可能是 Integer),只是编译器目前不知道具体是哪一种。Object:是一个明确且具体 的类型,它是 Java 类继承树的顶层父类。List<Object>明确表示这个列表里存放的就是 Object 及其子类对象。
什么是反射?优缺点?应用场景?
答案:
- 反射:在运行时动态获取类的完整信息(构造器、方法、属性)并操作对象,甚至可以访问私有成员。
- 核心类 :
Class、Constructor、Method、Field。
优点:在运行时动态创建对象、调用方法或访问字段,极大提升了代码的灵活性和框架的通用性。
缺点:性能较低(比直接调用慢,因为需要解析元数据)。破坏封装性,有安全隐患。
应用场景:
- Spring / MyBatis / Hibernate等框架开发:例如动态创建 Bean、依赖注入、ORM 映射。
- 动态代理(JDK 动态代理)
- 序列化与反序列化(JSON/XML 转换)
如何获取 Class对象?
- 类名.class:
Class<Person> clazz = Person.class; - 对象.getClass():
Person p = new Person(); Class<?> clazz = p.getClass(); - Class.forName("全限定类名"):
Class<?> clazz = Class.forName("com.example.Person");
如何通过反射创建对象?有哪两种方式?
方式一:使用 Class 类的 newInstance() 方法
java
Class<?> clazz = Person.class;
Object obj = clazz.newInstance(); // 要求类必须有 public 无参构造方法
- 特点 :只能调用 无参构造方法 ,且构造方法必须是
public的。 - 注意 :从 JDK 9 开始,此方法已被标记为 过时,推荐使用方式二。
方式二:使用 Constructor 类的 newInstance() 方法
java
Class<?> clazz = Person.class;
// 获取指定参数类型的构造方法
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("张三", 25); // 可传入实际参数
- 特点 :可以调用 任意有参/无参构造方法 (包括
private构造方法,需先setAccessible(true))。 - 优势 :更灵活、更安全,是 推荐的反射创建对象方式。
如何通过反射获取类的构造方法、方法和字段?
- 获取
Class对象(反射的入口) - 使用
Class对象提供的 API 获取对应成员 - 通过
setAccessible(true)访问私有成员
java
// 获取Class对象
Class<?> aClass = Class.forName("com.atguigu.product.Reflect");
// 创建对象
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class, int.class);
Object obj = constructor.newInstance("zhangsan", 16);
System.out.println(obj);
// 获取方法
Method method = aClass.getDeclaredMethod("doSome", String.class, String.class);
method.setAccessible(true);
method.invoke(obj,"userName","passWd");
// 获取属性
Field field = aClass.getDeclaredField("name");
field.setAccessible(true);
Object o = field.get(obj);
field.set(obj,"lisi");
System.out.println(field.get(obj));
反射中 setAccessible(true) 的作用是什么?有什么风险和注意事项?
通过 setAccessible(true) 访问私有成员,破坏封装性、有安全隐患(恶意代码可能通过反射获取敏感数据)、性能较低(JVM 禁用优化(如内联),调用速度比直接访问慢很多)。
反射与注解:如何获取类、方法、字段上的注解信息?
使用 Class、Method、Field 的 getAnnotation() ˌænəˈteɪʃn或 getAnnotations() 方法即可获取运行时注解信息。
注:被获取的注解必须使用 @Retention(RetentionPolicy.RUNTIME) 元注解标注,否则运行时无法读取。
java
// 定义注解(运行时保留)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface MyAnnotation {
String value();
}
// 使用注解
@MyAnnotation("Class")
public class Demo {
@MyAnnotation("Field")
private String name;
@MyAnnotation("Method")
public void run() {}
}
// 反射获取注解信息
public class AnnotationTest {
public static void main(String[] args) throws Exception {
Class<?> clazz = Demo.class;
// 1. 获取类上的注解
MyAnnotation classAnno = clazz.getAnnotation(MyAnnotation.class);
System.out.println(classAnno.value()); // "Class"
// 2. 获取字段上的注解
Field field = clazz.getDeclaredField("name");
MyAnnotation fieldAnno = field.getAnnotation(MyAnnotation.class);
System.out.println(fieldAnno.value()); // "Field"
// 3. 获取方法上的注解
Method method = clazz.getDeclaredMethod("run");
MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
System.out.println(methodAnno.value()); // "Method"
// 获取所有注解(包括继承的)
Annotation[] allAnno = clazz.getAnnotations();
// 仅获取直接声明的
Annotation[] declaredAnno = clazz.getDeclaredAnnotations();
}
}
注解(Annotation)的作用与使用场景?
- 提供元数据信息,供编译器或运行时框架读取。
- 场景 :
- 编译检查(如
@Override防止重写错误) - 框架配置(Spring 的
@Autowired、@Service) - 测试(JUnit 的
@Test) - 代码生成(如 Lombok 的
@Data生成 getter/setter)
- 编译检查(如
注解按运行机制分为哪几类?@Retention 的作用是什么?取值有哪些?
| 保留策略 | 对应 @Retention 取值 |
注解信息保留范围 | 典型应用 |
|---|---|---|---|
| 源码注解 | SOURCE |
仅保留在源代码中,编译时被丢弃 | @Override、@SuppressWarnings(仅编译期检查,不进入字节码) |
| 编译时注解 | CLASS(默认值) |
保留在 .class 字节码文件中,但运行时 JVM 无法读取 |
注解处理器(APT)在编译期处理,如 Lombok、ButterKnife;运行时不使用 |
| 运行时注解 | RUNTIME |
保留在字节码中,且运行时可通过反射读取 | Spring (@Autowired)、JUnit (@Test)、自定义框架注解 |
@Retention 的作用
@Retention 是一个元注解 (用于注解的注解),用来指定被标注的注解可以保留到哪个阶段,即注解的生命周期。
@Retention 的取值
java
public enum RetentionPolicy {
SOURCE, // 仅源代码,编译后丢弃
CLASS, // 保留到字节码,但运行时不可见(默认值)
RUNTIME // 保留到运行时,可通过反射获取
}
注意 :若要注解在运行时通过反射被读取,必须使用
@Retention(RetentionPolicy.RUNTIME)。
@Target 元注解的作用是什么?常用的 ElementType 有哪些?
@Target 元注解的作用
@Target 用于限制注解可以应用在哪些 Java 元素上 (如类、方法、字段等)。如果不指定 @Target,该注解可以用于任何位置。
常用的 ElementType 取值
ElementType |
说明 | 示例 |
|---|---|---|
TYPE |
类、接口、枚举、注解类型 | public class MyClass {} |
FIELD |
字段(包括枚举常量) | private String name; |
METHOD |
方法 | public void run() {} |
PARAMETER |
方法参数 | void say(String name) 中的 name |
CONSTRUCTOR |
构造方法 | public MyClass() {} |
LOCAL_VARIABLE |
局部变量 | int i = 0; |
ANNOTATION_TYPE |
注解类型 | 用于元注解 |
PACKAGE |
包 | package-info.java |
TYPE_PARAMETER (Java 8+) |
泛型类型参数 | class Box<T> { } 中的 T |
TYPE_USE (Java 8+) |
类型使用(任何类型出现的地方) | List<@NonNull String> |
说明 :
TYPE_USE可出现在类型使用的任何位置(如泛型、强制转换、异常等),用于细化类型检查。
注解可以继承吗?@Inherited 的作用是什么?
注解本身不支持继承 :在 Java 中,注解不能使用 extends 关键字去继承另一个注解(注解的声明上不允许使用 extends)。每个注解都隐式继承自 java.lang.annotation.Annotation 接口,但这是语言内置的,不是用户可扩展的继承关系。
@Inherited 元注解的作用
@Inherited 用于控制注解是否对子类产生继承效果 。当一个注解被 @Inherited 修饰后:
- 如果该注解被用在某个类 上,那么它的子类将自动继承该注解(即子类上也存在该注解)。
- 只对类继承有效,对接口、方法、字段等无效。
- 如果子类自己显式使用了该注解,则子类的注解会覆盖(不继承)父类的。
示例
java
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation {}
@MyAnnotation
class Parent {}
class Child extends Parent {} // Child 也自动拥有 @MyAnnotation
如何定义一个自定义注解?注解中可以有哪些类型的属性?
使用 @interface 关键字声明,并通常配合元注解(如 @Retention、@Target)来指定保留策略和作用目标。
java
// 定义一个运行时注解,可用于类和方法
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时
@Target({ElementType.TYPE, ElementType.METHOD}) // 可用于类和方法
public @interface MyAnnotation {
// 属性
String value() default ""; // 带有默认值
int count() default 0;
String[] tags() default {};
}
注解中可以包含哪些类型的属性?
注解中定义的属性实质上是无参数方法,其返回值类型只能是以下类型之一:
| 类型 | 示例 |
|---|---|
| 基本类型(8种) | int、boolean、double 等 |
String |
String name(); |
Class |
Class<?> type(); |
枚举(enum) |
Level level(); |
| 注解类型 | MyAnnotation2 ref(); |
| 上述类型的数组 | String[] tags(); 或 int[] ids(); |
注意:
- 不支持包装类型(如
Integer、Boolean),也不支持Object或集合类型(如List)。 - 可以为属性提供默认值(使用
default关键字),也可以不提供(使用时必须显式赋值,除非有默认值)。 - 特殊属性
value:如果注解只有一个value属性,使用时可以省略属性名,直接写值。
注解的属性可以有默认值吗?如何设置?
注解的属性可以有默认值 ,使用 default 关键字来设置。
Java IO 流分类?
- 按流向 :输入流(
InputStream/Reader)、输出流(OutputStream/Writer)。 - 按单位 :字节流(
InputStream/OutputStream)、字符流(Reader/Writer)。 - 按功能 :节点流(直接连接数据源,如
FileInputStream)、处理流(包装其他流,如BufferedInputStream)。
字符流和字节流的区别?
- 字节流以 字节(8 bit) 为单位,适合处理二进制数据(图片、音视频等)。
- 字符流以 字符(Unicode code unit) 为单位,适合处理文本数据,自动处理编码转换。
- 字节流没有缓冲区,字符流内部使用缓冲区(需要
flush)。
什么是节点流?什么是处理流?它们的区别是什么?
- 节点流 是直接连接数据源(比如文件、内存数组、网络连接)的流,它负责最底层的实际读写操作。像
FileInputStream、ByteArrayOutputStream这些就是节点流。 - 处理流 不能独立存在,它必须包装在另一个流之上,用于增强功能。比如
BufferedInputStream提供缓冲、ObjectOutputStream支持对象序列化、InputStreamReader实现字节转字符等。
BIO、NIO、AIO 的区别?
| 模型 | 描述 | 特点 | 适用场景 |
|---|---|---|---|
| BIO(同步阻塞) | 一个连接一个线程,线程在读写数据时阻塞 | 简单,但线程开销大,并发低 | 连接数少、长连接的场景(如传统 Tomcat 7) |
| NIO(同步非阻塞) | 基于 Selector,一个线程管理多个 Channel,轮询事件 | 高并发,编程复杂 | Netty、Tomcat 8+ |
| AIO(异步非阻塞) | 由 OS 完成读写后回调通知 | 适合大量读写操作且耗时长的场景 | 目前在 Java 中应用较少 |
NIO 核心组件:
- Channel (通道):双向数据传输的通道。
- Buffer (缓冲区):所有数据都通过 Buffer 进行读写。
- Selector (选择器):一个线程可以管理成千上万个 Channel。它通过轮询的方式检查哪些 Channel 已经准备好进行 I/O 操作(如可读、可写)。
Java IO 的四大抽象基类是什么?
| 抽象类 | 数据单位 | 方向 | 核心方法 |
|---|---|---|---|
InputStream |
字节 | 输入 | int read()、read(byte[] b) |
OutputStream |
字节 | 输出 | void write(int b)、write(byte[] b) |
Reader |
字符 | 输入 | int read()、read(char[] c) |
Writer |
字符 | 输出 | void write(int c)、write(char[] c) |
InputStream 的 read() 方法为什么返回 int 而不是 byte?
byte的取值范围是 -128 ~ 127 ,其中-1是一个合法的字节值(对应十六进制0xFF),选用byte时无法区分是字节值还是流结束。int的取值范围更大,可以用-1专门表示流结束,而0 ~ 255表示实际读取到的无符号字节值。
什么是序列化?transient 关键字的作用?
- 序列化 :将类实现了
Serializable接口的 Java 对象转换为字节流,用于持久化或网络传输;反序列化是将字节流转换成Java对象。 transient修饰的字段不会被序列化 ,反序列化后其值为默认值(null、0等),适用于敏感信息字段。
hashCode() 和 equals() 的关系?
重写 equals 时必须重写 hashCode,否则在Hash集合(如 HashSet、HashMap)中可能无法正确去重。
- 如果两个对象
equals相等,则hashCode必须相等。 - 反之,
hashCode相等不一定equals相等(哈希冲突)。
2. 浅拷贝和深拷贝的区别?
- 浅拷贝:复制对象时,只复制基本类型和引用地址,不复制引用指向的对象。克隆后的对象与原对象共享引用类型属性。
- 深拷贝:递归复制所有引用对象,使拷贝对象与原对象完全独立。
- 实现:实现
Cloneable接口并重写clone()方法(浅拷贝);深拷贝可通过序列化或手动递归实现。
3. 如何实现对象克隆?
- 实现
Cloneable接口,重写clone()方法(浅拷贝)。 - 使用序列化(
Serializable)实现深拷贝。
Java 8 中 java.time 包如何格式化日期?
java
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(dtf); // 2025-03-01 14:30:00
LocalDateTime parsed = LocalDateTime.parse("2025-03-01 14:30:00", dtf);
DateTimeFormatter 是线程安全的,可以定义为 static final 全局使用。
SimpleDateFormat 是线程安全的吗?如何处理?
不是线程安全的 ,因为其内部使用了 Calendar 对象,多线程并发调用 format() 或 parse() 会产生竞态条件。
解决方案:
- 每次使用时
new一个实例(开销小)。 - 使用
ThreadLocal包装。 - 改用 Java 8+ 的
DateTimeFormatter(不可变、线程安全)。
java
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String format = formatter.format(now);
System.out.println(format);
LocalDateTime localDateTime = LocalDateTime.parse(format, formatter);
System.out.println(localDateTime);
14. 什么是 native 方法?用途是什么?
native关键字修饰的方法表示由本地代码(如 C/C++)实现,不提供 Java 方法体。- 用途 :
- 调用底层操作系统 API。
- 提高性能。某些计算密集型的算法(如矩阵运算、加密)用 C/C++ 实现,性能优于 Java。
15. Java 中的 Math.round(-1.5) 返回值是多少?
- 四舍五入规则:向正无穷方向取整 (或常说"加 0.5 后向下取整")。
Math.round(-1.5)=-1(因为 -1.5 + 0.5 = -1.0,向下取整得 -1)。
验证:Math.round(-1.6)=-2。 - 对于
float返回int,double返回long。
函数式接口有哪几个?
函数式接口是只包含一个抽象方法 的接口,可以用 @FunctionalInterface 注解标记(非强制)。Java 8 在 java.util.function 包中定义了四大基础函数式接口,以及许多扩展版本。
一、四大基础函数式接口
| 接口名 | 抽象方法 | 参数 | 返回值 | 用途 |
|---|---|---|---|---|
Function<T,R> |
R apply(T t) |
1个 (T) | R | 类型转换:T → R |
Predicate<T> |
boolean test(T t) |
1个 (T) | boolean | 条件判断/过滤 |
Consumer<T> |
void accept(T t) |
1个 (T) | void | 消费一个参数(无返回值) |
Supplier<T> |
T get() |
无 | T | 提供/生产数据 |
其他常见函数式接口(非 java.util.function 包)
| 接口 | 抽象方法 | 说明 |
|---|---|---|
Runnable |
void run() |
无参无返回值,常用于线程任务 |
Callable<V> |
V call() throws Exception |
有返回值,可抛异常 |
Comparator<T> |
int compare(T o1, T o2) |
比较两个对象 |
单例模式的核心目标是什么?有哪些实现方式?
- 目标:确保一个类仅存在一个实例,并提供全局唯一的访问入口。
- 常见实现:饿汉式、懒汉式(线程不安全/安全)、双重检查锁(DCL)、静态内部类、枚举。
java
// 懒汉式(同步方法,不推荐,性能差)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 双检:DCL
public class Singleton {
private static volatile Singleton instance; // 必须 volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
// 饿汉式(简单,类加载时即创建,线程安全)
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
// 静态内部类实现(延迟加载,线程安全,推荐)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
// 枚举实现(最简洁、绝对防止多次实例化,且防反射/序列化)
public enum Singleton {
INSTANCE;
// 可添加业务方法
public void doSomething() {
System.out.println("枚举单例");
}
}
// 调用:Singleton.INSTANCE.doSomething();
总结对比表格
| 实现方式 | 线程安全 | 懒加载 | 效率 | 防反射/反序列化 | 推荐程度 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | 高 | ❌(可被反射破坏) | 适合简单场景 |
| 懒汉式(线程不安全) | ❌ | ✅ | 高 | ❌ | 不推荐 |
| 懒汉式(同步方法) | ✅ | ✅ | 低 | ❌ | 不推荐 |
| DCL | ✅ | ✅ | 高 | ❌ | 可用,需注意 volatile |
| 静态内部类 | ✅ | ✅ | 高 | ❌ | 推荐 |
| 枚举 | ✅ | ❌ | 高 | ✅ | 最安全推荐 |
枚举单例的优势是什么?
天然避免线程安全问题,且能防止反射破坏单例、防止序列化/反序列化破坏单例。
- 线程安全:JVM 在类加载时创建枚举实例,天然线程安全。
- 防反射:Java 禁止通过反射创建枚举实例,会直接抛异常。
- 防反序列化:反序列化时只根据名称返回已有常量,不会新建对象。
double-check单例为什么要加volatile?
为了防止指令重排序,new Singleton() 不是原子操作,可能先分配内存、再将引用赋值给 instance、最后初始化对象。不加 volatile 时,其他线程可能拿到未初始化完成的对象。
volatile如何解决?
volatile 会禁止指令重排序 (通过内存屏障),保证对象完全初始化后 才将引用赋值给 instance,从而避免其他线程拿到半成品对象。
不加volatile关键词存在的问题:
instance = new Singleton(); 在 JVM 中不是原子操作,大致分为三步:
- 分配内存空间
- 调用构造方法,初始化对象
- 将
instance引用指向内存地址
正常情况下,步骤 2 和 3 的顺序是 2 → 3。但 指令重排序 可能导致顺序变为 3 → 2(先分配地址,后初始化)。
如果此时另一个线程调用 getInstance(),发现 instance != null(指向了地址),就直接返回该对象,但该对象的构造方法可能还未执行 ,内部字段都是默认值(如 0、null),使用时就会出错。
枚举单例如何防止反射攻击?
反射的 newInstance() 会直接禁止创建枚举实例,抛出 IllegalArgumentException。
项目中你用的哪种?
看场景:一般无特殊要求用静态内部类 ;如果需要防御反射/序列化破坏,或作为工具类单例,用枚举。
静态内部类单例的实现原理是什么?和饿汉式有什么区别?
原理 :外部类加载时不加载内部类,首次调用 getInstance() 时 JVM 才会加载内部类并创建实例,利用 JVM 类加载机制保证线程安全和唯一性。
与饿汉式的区别:
| 维度 | 静态内部类 | 饿汉式 |
|---|---|---|
| 加载时机 | 懒加载(首次使用时) | 立即加载(类加载时) |
| 内存占用 | 不浪费 | 可能浪费(未使用也创建) |
简单工厂、工厂方法、抽象工厂的区别是什么?
| 模式 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 核心职责 | 一个工厂类负责创建所有类型的产品。 | 每个具体工厂只负责创建一种具体产品。 | 一个工厂负责创建一族(多个相关联)的产品。 |
| 扩展性 | 差。新增产品需要修改工厂类代码(违反开闭原则)。 | 好。新增产品只需新增对应的工厂类(符合开闭原则)。 | 横向扩展好,纵向扩展差。新增产品族容易,但新增产品种类需要修改所有工厂接口(违反开闭原则)。 |
| 解决问题 | 解决单一产品的简单创建,客户端无需知道具体类名。 | 解决单一产品等级结构(如不同品牌的手机)的创建。 | 解决多产品族(如不同品牌的手机+对应的充电器)的配套创建。 |
简单工厂:
java
// 1. 产品接口
interface Phone {
void call();
}
// 2. 具体产品
class IPhone implements Phone {
public void call() { System.out.println("iPhone 打电话"); }
}
class HuaweiPhone implements Phone {
public void call() { System.out.println("华为 打电话"); }
}
// 3. 简单工厂 (核心)
class SimpleFactory {
// 只要传入名字,我就给你造出来
public Phone createPhone(String type) {
if ("apple".equals(type)) {
return new IPhone();
} else if ("huawei".equals(type)) {
return new HuaweiPhone();
} else {
throw new IllegalArgumentException("不支持的手机");
}
}
}
// 4. 客户端调用
// SimpleFactory factory = new SimpleFactory();
// Phone p = factory.createPhone("apple");
工厂方法:
java
// 1. 工厂接口 (每个工厂只负责造一种手机)
interface PhoneFactory {
Phone createPhone();
}
// 2. 具体工厂
class IPhoneFactory implements PhoneFactory {
public Phone createPhone() {
return new IPhone(); // 专门造 iPhone
}
}
class HuaweiFactory implements PhoneFactory {
public Phone createPhone() {
return new HuaweiPhone(); // 专门造 华为
}
}
// 3. 客户端调用
// PhoneFactory factory = new IPhoneFactory();
// Phone p = factory.createPhone();
抽象工厂
java
// 1. 定义多个产品接口
interface Phone { void call(); }
interface Charger { void charge(); }
// 2. 具体产品 (苹果族)
class IPhone implements Phone { public void call() { System.out.println("iPhone 打电话"); } }
class AppleCharger implements Charger { public void charge() { System.out.println("苹果 充电器"); } }
// 3. 具体产品 (华为一族)
class HuaweiPhone implements Phone { public void call() { System.out.println("华为 打电话"); } }
class HuaweiCharger implements Charger { public void charge() { System.out.println("华为 充电器"); } }
// 4. 抽象工厂 (核心:能造一整套东西)
interface AbstractFactory {
Phone createPhone();
Charger createCharger();
}
// 5. 具体工厂 (苹果全家桶工厂)
class AppleFactory implements AbstractFactory {
public Phone createPhone() { return new IPhone(); }
public Charger createCharger() { return new AppleCharger(); }
}
// 6. 具体工厂 (华为全家桶工厂)
class HuaweiFactory implements AbstractFactory {
public Phone createPhone() { return new HuaweiPhone(); }
public Charger createCharger() { return new HuaweiCharger(); }
}
// 7. 客户端调用
// AbstractFactory factory = new AppleFactory();
// Phone p = factory.createPhone(); // 造出 iPhone
// Charger c = factory.createCharger(); // 造出 苹果充电器
抽象工厂模式的应用场景?举例说明。
当你的系统需要创建"一整套"相互关联或依赖的产品,并且要保证它们能配套使用时使用抽象工厂。
举例:数据库的适配
很多大型系统为了兼容不同的客户环境,需要同时支持 MySQL、Oracle、DB2 等多种数据库。数据库操作通常涉及连接(Connection)和命令执行(Command/Statement)。
- 定义抽象工厂
DBFactory,包含createConnection()和createCommand()。 - MySQL 工厂:专门创建 MySQL 连接 + MySQL 命令。
- Oracle 工厂:专门创建 Oracle 连接 + Oracle 命令。
代理模式的核心作用是什么?分为哪几类?
代理模式的核心作用是:在不修改目标对象代码的前提下,通过引入代理对象来控制对目标对象的访问,从而实现对目标对象功能的增强或访问控制。
分为静态代理和动态代理,动态代理又包括 JDK 动态代理(基于接口)和 CGLIB 动态代理(基于子类)。Spring AOP 正是根据目标类是否实现接口,自动选择 JDK 或 CGLIB 代理。