Java八股文-java基础面试题
八股文面试篇(Java基础)
一.java语言的特点
- 跨平台
- 有自己的内存管理机制
- 面向对象
二.Java为什么跨平台
Java 之所以能够跨平台,是因为不同操作系统都有对应版本的 Java 虚拟机(JVM)。只要在目标平台上安装相应的 JVM,就能运行 Java 的字节码文件(.class)。
字节码本身是跨平台的,但 JVM 并不能跨平台。不同操作系统需要不同版本的 JVM 来解析 Java 字节码,并将其转换为该平台对应的机器码,以供执行。
三.JVM、JDK、JRE关系
好的,我给你梳理一下 JVM、JDK、JRE 的关系,可以类比成一个“运行和开发环境的套娃结构”:
1. JVM (Java Virtual Machine,Java虚拟机)
-
定位:运行 Java 字节码的虚拟机。
-
作用:把
.class
文件(字节码)解释/编译成机器能执行的指令。 -
关键点:
- 屏蔽了不同操作系统之间的差异,实现了 一次编译,到处运行。
- JVM 本身不包含编译器,只负责运行字节码。
- 包含垃圾回收(GC)、内存管理、多线程支持等。
2. JRE (Java Runtime Environment,Java运行时环境)
-
定位:运行 Java 程序所需的环境。
-
组成:
- JVM
- Java 核心类库(如
java.lang.*
,java.util.*
等) - 一些运行支持文件(配置文件、资源等)
-
关键点:
- JRE = JVM + 核心类库
- 只适合 运行 Java 程序,不包含开发工具。
3. JDK (Java Development Kit,Java开发工具包)
-
定位:提供开发 Java 程序所需的完整工具包。
-
组成:
-
JRE(所以 JDK 里包含 JVM)
-
开发工具:
javac
(Java 编译器,把.java
转成.class
字节码)java
(运行字节码)jdb
(调试器)javadoc
(生成文档)等
-
-
关键点:
- JDK = JRE + 开发工具
- 既能开发 Java 程序,也能运行。
4. 关系图(套娃关系)
JDK (开发工具包)┌──────────────────────────────┐│ 开发工具 (javac, jdb...) ││ ││ JRE (运行环境) ││ ┌───────────────────────┐ ││ │ Java核心类库 │ ││ │ │ ││ │ JVM (虚拟机) │ ││ └───────────────────────┘ │└──────────────────────────────┘
- JVM 只负责运行字节码
- JRE 提供运行环境(JVM + 核心类库)
- JDK 提供开发环境(JRE + 编译器和调试工具)
四.面向对象
什么是面向对象,什么是封装继承多态?
🔹 一、面向对象(OOP, Object Oriented Programming)
面向对象是一种编程思想,它把现实世界的事物抽象成程序中的对象,通过对象之间的交互来完成需求。
在 Java 中,对象是 类的实例,类定义了对象的 属性(字段) 和 行为(方法)。
核心思想可以总结为:
👉 “一切皆对象,通过对象交互解决问题。”
面向对象的三大特征就是:封装、继承、多态。
🔹 二、封装(Encapsulation)
-
定义:把数据(属性)和操作数据的方法(行为)打包到一个类里,并通过 访问修饰符 控制外部对数据的访问。
-
作用:
- 隐藏内部实现细节(只暴露必要接口)。
- 提高代码安全性和可维护性。
-
例子:
public class Person {private String name; // 私有属性,外部不能直接访问private int age;// 提供公共方法访问和修改public String getName() {return name;}public void setName(String name) {this.name = name;}
}
🔹 三、继承(Inheritance)
-
定义:子类可以继承父类的属性和方法,从而实现代码复用。
-
关键字:
extends
-
作用:
- 代码复用。
- 表达类之间的层次关系(is-a 关系)。
-
例子:
class Animal {public void eat() {System.out.println("动物吃东西");}
}class Dog extends Animal { // Dog继承Animalpublic void bark() {System.out.println("狗在汪汪叫");}
}public class Test {public static void main(String[] args) {Dog dog = new Dog();dog.eat(); // 继承自Animaldog.bark(); // 自己的行为}
}
🔹 四、多态(Polymorphism)
-
定义:同一个行为,在不同对象中表现不同的形态。
-
实现方式:
- 方法重写(Override):子类对父类方法进行不同实现。
- 接口实现:不同类实现相同接口,表现不同行为。
-
前提:必须有继承或接口,且方法签名相同。
-
例子:
class Animal {public void makeSound() {System.out.println("动物叫");}
}class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("汪汪汪");}
}class Cat extends Animal {@Overridepublic void makeSound() {System.out.println("喵喵喵");}
}public class Test {public static void main(String[] args) {Animal a1 = new Dog(); // 向上转型Animal a2 = new Cat();a1.makeSound(); // 输出:汪汪汪a2.makeSound(); // 输出:喵喵喵}
}
🔹 五、总结一句话
- 封装:隐藏细节,只暴露接口(对内保护)。
- 继承:代码复用,类之间有层次(对上复用)。
- 多态:同一接口,多种实现(对外灵活)。
- 面向对象:通过这三大特性,把现实世界的对象抽象成代码里的类和对象,用对象的交互来解决问题。
五.重载重写的区别
重载是对同一命名方法可以拥有不同的参数列表,编译器根据调用时参数类型来自动选择调用哪个方法
重写是子类重写父类同名方法,通过@override来标注
六.抽象类 和 实体类(普通类/具体类) 的区别。
🔹 1. 抽象类(abstract class)
-
定义:使用
abstract
修饰的类,不能直接创建对象。 -
作用:用来抽象出一类事物的 共性,为子类提供统一的模板。
-
特点:
- 可以包含 抽象方法(没有方法体的,必须由子类实现)。
- 也可以包含 具体方法(有方法体的)。
- 不能直接
new
,必须由子类继承并实现抽象方法后,才能实例化子类对象。
-
例子:
abstract class Animal {// 抽象方法(没有方法体)public abstract void makeSound();// 普通方法public void sleep() {System.out.println("动物在睡觉");}
}class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("汪汪汪");}
}public class Test {public static void main(String[] args) {// Animal a = new Animal(); // ❌ 抽象类不能实例化Animal dog = new Dog(); // ✅ 可以通过子类实例化dog.makeSound(); // 汪汪汪}
}
🔹 2. 实体类(Concrete Class,普通类)
-
定义:能直接创建对象的类。
-
作用:用来描述某个具体对象的属性和行为。
-
特点:
- 必须实现所有方法(不能有抽象方法)。
- 可以直接
new
出对象使用。
-
例子:
class Cat {public void makeSound() {System.out.println("喵喵喵");}
}public class Test {public static void main(String[] args) {Cat cat = new Cat(); // ✅ 普通类可以直接实例化cat.makeSound(); // 喵喵喵}
}
🔹 3. 区别总结表格
特性 | 抽象类 (abstract class) | 实体类 (Concrete Class) |
---|---|---|
是否能实例化 | ❌ 不能直接实例化 | ✅ 可以直接实例化 |
是否有抽象方法 | ✅ 可以有抽象方法,也可以有普通方法 | ❌ 不能有抽象方法,方法必须实现 |
主要作用 | 提供统一模板,约束子类 | 描述具体对象,实现具体逻辑 |
关键字 | abstract | 无(默认就是实体类) |
✅ 总结一句话:
- 抽象类:抽象的是“共性”,像设计蓝图,不能直接用。
- 实体类:具体实现了功能,能直接
new
出对象来用。
注意
如果一个类 继承了抽象类,那么它必须 实现父类中所有抽象方法,否则它自己也必须声明为 abstract。
如果子类 不想/不能实现所有抽象方法,那么这个子类本身也可以定义成 抽象类,把责任继续交给它的子类。
七.Java抽象类和接口的区别
🔹 1. 定义不同
-
抽象类(abstract class)
- 是一种 类,可以包含 抽象方法 和 具体方法。
- 用
abstract
关键字修饰,不能直接实例化。
-
接口(interface)
- 是一种 规范/契约,定义类必须实现哪些方法。
- 默认所有方法是
public abstract
(Java 8 之后支持default
、static
,Java 9 之后还支持private
)。
🔹 2. 关键区别对比
对比项 | 抽象类 (abstract class) | 接口 (interface) |
---|---|---|
关键字 | abstract class | interface |
继承/实现 | extends ,单继承 | implements ,可多实现 |
成员变量 | 可以有成员变量(可以是 private/protected/public ,也可以有 static 、final ) | 默认是 public static final 常量(即全局常量) |
方法 | 可以有抽象方法和普通方法 | Java 8 以前只有抽象方法;Java 8+ 可以有 default 、static 方法,Java 9+ 还能有 private 方法 |
构造器 | 可以有构造器(但不能直接 new ,主要用于子类调用) | 不能有构造器 |
继承关系 | 一个类只能继承一个抽象类(单继承) | 一个类可以实现多个接口(多实现) |
设计目的 | 抽取 共性(is-a 关系,模板化设计) | 规定 规范(can-do 关系,能力扩展) |
🔹 3. 例子对比
抽象类
abstract class Animal {String name;// 抽象方法public abstract void makeSound();// 普通方法public void sleep() {System.out.println("动物在睡觉");}
}class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("汪汪汪");}
}
接口
interface Flyable {void fly(); // 默认 public abstract
}interface Swimmable {void swim();
}class Duck implements Flyable, Swimmable {@Overridepublic void fly() {System.out.println("鸭子会飞一小段");}@Overridepublic void swim() {System.out.println("鸭子游泳");}
}
👉 Duck
同时实现了两个接口,但如果 Duck
想继承 Animal
的属性/方法,就必须用 抽象类继承 + 接口实现 结合。
🔹 4. 使用场景
-
抽象类:
- 当多个类有 相同的属性/方法,且希望代码复用时,用抽象类。
- 强调 继承关系(is-a)。
- 例子:
Animal
→Dog
/Cat
-
接口:
- 当多个类需要具有 相同行为能力,但不关心其继承体系时,用接口。
- 强调 能力扩展(can-do)。
- 例子:
Flyable
、Swimmable
🔹 5. 总结口诀
-
抽象类:有“血缘关系”的共性抽取,代码复用(is-a)。
-
接口:没有血缘关系的“能力扩展”,规定契约(can-do)。
-
类 → 抽象类 → 接口:
- 类 = 具体实现
- 抽象类 = 模板
- 接口 = 规范
八.抽象类有构造器吗?
🔹 为什么抽象类可以有构造器?
-
抽象类也是类
- 虽然不能实例化,但它仍然是类,所以可以有构造器。
- 构造器的主要作用不是给抽象类自己用,而是 供子类在实例化时调用(通过 super()),完成 父类部分属性的初始化。
-
继承时的初始化顺序
- 子类实例化时,会先调用父类构造器,再调用子类构造器。
- 如果父类是抽象类,它的构造器同样会在子类构造时执行。
🔹 示例代码
abstract class Animal {String name;// 抽象类构造器public Animal(String name) {this.name = name;System.out.println("Animal 的构造器被调用");}public abstract void makeSound();
}class Dog extends Animal {public Dog(String name) {super(name); // 调用抽象类的构造器System.out.println("Dog 的构造器被调用");}@Overridepublic void makeSound() {System.out.println(name + ":汪汪汪");}
}public class Test {public static void main(String[] args) {Dog dog = new Dog("小黑");dog.makeSound();}
}
输出:
Animal 的构造器被调用
Dog 的构造器被调用
小黑:汪汪汪
👉 可以看到:虽然 Animal
是抽象类,但它的构造器在 Dog
实例化时照常执行。
🔹 总结
- 抽象类 可以有构造器。
- 构造器作用是:供子类调用,完成父类属性初始化。
- 不能直接 new 抽象类,但子类实例化时一定会先执行抽象类的构造器。
好问题 👍,这是 Java 面试常考点。我帮你详细梳理一下 浅拷贝 vs 深拷贝。
九.深拷贝浅拷贝
🔹 1. 定义
-
浅拷贝(Shallow Copy)
- 复制对象时,只复制 对象本身及其基本类型的字段,
- 引用类型的字段仍然指向原对象的地址。
- → “拷贝壳子,里面的引用还是同一个”。
-
深拷贝(Deep Copy)
- 复制对象时,不仅复制对象本身,还会 复制其引用类型所指向的对象(递归复制)。
- → “拷贝整个对象树,原对象和新对象完全独立”。
🔹 2. 内存结构对比
假设有一个 Person
对象,里面有个 Address
对象:
class Address {String city;public Address(String city) {this.city = city;}
}class Person implements Cloneable {String name;Address address;public Person(String name, Address address) {this.name = name;this.address = address;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone(); // 默认浅拷贝}
}
浅拷贝(默认 clone())
Person1 (name="Tom", address -> 地址A)│└─> Address(city="Beijing") ← 同一份对象
Person2 (name="Tom", address -> 地址A)
👉 两个 Person 对象的 address
指向同一个内存地址。
深拷贝(手动实现)
Person1 (name="Tom", address -> 地址A)│└─> Address(city="Beijing")
Person2 (name="Tom", address -> 地址B)│└─> Address(city="Beijing")
👉 两个 Person 对象的 address
是不同对象,互不影响。
🔹 3. 示例代码
浅拷贝
Person p1 = new Person("Tom", new Address("Beijing"));
Person p2 = (Person) p1.clone();p2.name = "Jerry";
p2.address.city = "Shanghai";System.out.println(p1.name); // Tom (不影响)
System.out.println(p1.address.city); // Shanghai (被影响了)
👉 address
是引用类型,两个对象共享同一份数据。
深拷贝
class Person implements Cloneable {String name;Address address;public Person(String name, Address address) {this.name = name;this.address = address;}@Overrideprotected Object clone() throws CloneNotSupportedException {Person cloned = (Person) super.clone();cloned.address = new Address(this.address.city); // 手动复制引用对象return cloned;}
}
Person p1 = new Person("Tom", new Address("Beijing"));
Person p2 = (Person) p1.clone();p2.address.city = "Shanghai";System.out.println(p1.address.city); // Beijing (不受影响)
🔹 4. 区别总结表格
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
基本类型 | 拷贝值 | 拷贝值 |
引用类型 | 拷贝引用(同一对象) | 拷贝新的对象(完全独立) |
内存关系 | 部分共享 | 完全独立 |
性能 | 快(只复制一层) | 慢(递归复制所有层级) |
应用场景 | 数据只读、对象简单 | 需要完全隔离,防止互相影响 |
✅ 总结一句话:
- 浅拷贝:只复制一层,引用对象共享。
- 深拷贝:递归复制,引用对象独立。
十.浅拷贝中不拷贝的“引用数据类型”具体指哪些?
🔹 Java 里的数据类型分类
在 Java 中,数据类型分为两大类:
-
基本数据类型(primitive types,值类型)
byte
、short
、int
、long
float
、double
char
boolean
👉 浅拷贝时,这些会 直接复制值,互不影响。
-
引用数据类型(reference types,引用类型)
- 类(class) → 例如
String
、Integer
(包装类)、自定义的Person
、Address
- 数组(Array) → 例如
int[]
、String[]
- 接口(interface)实现类
- 枚举(enum)
👉 浅拷贝时,只会复制引用(地址),不会新建对象,所以两个对象指向同一块内存。
- 类(class) → 例如
🔹 总结
在浅拷贝中:
- 值类型(基本数据类型) → 复制值 ✅(独立)。
- 引用类型(类、数组、接口实现、枚举等) → 复制引用 ❌(共享同一对象)。
这个就是 Java 垃圾回收(GC)机制。
简单来说:在 Java 中,用 new
出来的对象,会在 JVM 堆内存中分配空间,当它“不再被引用”时,就会被 GC 回收。
十一.Java中new出来的对象什么时候回收
🔹 1. 对象创建
Person p = new Person();
new Person()
会在 堆(Heap) 上分配一块内存存储对象。- 变量
p
存在 栈(Stack) 上,保存着对这个堆中对象的引用地址。
🔹 2. 对象什么时候会被回收?
JVM 垃圾回收器(GC)会回收 不可达对象。
① 不再被任何引用指向时
Person p = new Person();
p = null; // 原来new出来的对象不再有引用 → 可被回收
② 引用被重新赋值时
Person p = new Person(); // 对象1
p = new Person(); // 对象1失去引用 → 可被回收
③ 方法执行结束,局部变量出栈
public void test() {Person p = new Person(); // test结束后,p出栈 → 对象可被回收
}
④ 循环引用不影响回收(Java 用可达性分析,不是引用计数)
class A { B b; }
class B { A a; }A a = new A();
B b = new B();
a.b = b;
b.a = a;a = null;
b = null; // 两个对象互相引用,但不可达 → GC 会回收
🔹 3. JVM 如何判断对象是否可回收?
JVM 使用 可达性分析(Reachability Analysis):
- 从 GC Roots(根对象,比如栈中的局部变量、静态变量等)出发,沿着引用链可达的对象,都是“活着”的。
- 没有被引用链关联到的对象,就是“不可达对象”,会被回收。
🔹 4. 回收的时机
- 不是立刻回收:
Java 中对象的回收由 GC 负责,开发者不能确定具体回收时间。 - 手动提示 GC:
可以调用System.gc()
或Runtime.getRuntime().gc()
,但只是 提示,JVM 不保证立即执行。
🔹 5. 总结一句话
- new 出来的对象存放在堆中。
- 只要还有引用指向,就不会被回收。
- 一旦不可达(没有任何引用指向它),就会在 GC 下次运行时被回收。
在 Java 中,final
是一个非常重要的关键字,用于修饰 变量、方法、类,它的作用在不同的上下文中略有不同。我给你详细梳理一下:
十二.什么是反射,反射的应用场景有哪些?
1. 修饰变量
-
基本类型:
final int a = 10; a = 20; // ❌ 编译报错,不能改变值
作用:一旦赋值后不能修改。保证变量成为常量。
-
引用类型:
final List<String> list = new ArrayList<>(); list = new ArrayList<>(); // ❌ 报错,引用不可变 list.add("hello"); // ✅ 可以修改对象内容
作用:
- 引用本身不可改变(不能指向另一个对象)
- 对象内部状态仍然可以修改(除非对象本身不可变)
-
注意:
final
变量必须在声明时或者构造器中初始化,否则编译报错。
2. 修饰方法
class Parent {public final void show() {System.out.println("Hello");}
}class Child extends Parent {public void show() { // ❌ 报错,不能被重写}
}
作用:
- 被
final
修饰的方法不能被子类重写 - 常用于保证核心方法逻辑不被篡改,提高安全性和稳定性
3. 修饰类
final class MyClass {// 类内容
}class SubClass extends MyClass { // ❌ 报错,不能继承 final 类
}
作用:
- 被
final
修饰的类不能被继承 - 常用于创建不可变类(比如
String
就是 final 类) - 提高安全性,同时 JVM 可以做一些优化(比如方法调用优化)
4. 和其他关键字组合
-
final + static:
public static final double PI = 3.14159;
- 表示常量(类级别共享,值不可改变)
-
final + 参数:
public void test(final int x) {// x = 5; // ❌ 报错 }
- 表示方法内部不能修改参数值
- 在匿名类或 lambda 表达式中,也要求捕获的变量是
final
或“实际 final”(值不变)
总结一下,final
的核心思想就是 不可变:
- 变量 → 值或引用不可变
- 方法 → 不可被重写
- 类 → 不可被继承
十三.什么是反射,反射的应用场景是什么?
在 Java 中,反射(Reflection) 是指程序在运行时能够 获取类的信息并操作类的属性、方法和构造器 的能力。换句话说,就是在运行时动态地操作类,而不是在编译时确定。
1. 反射的核心概念
Java 中主要通过 java.lang.reflect
包来实现反射,包括:
- Class 对象:代表一个类
- Field:类的属性
- Method:类的方法
- Constructor:类的构造器
获取 Class 对象的方式有三种:
// 1. 通过类名.class
Class<String> c1 = String.class;// 2. 通过对象.getClass()
String s = "hello";
Class<?> c2 = s.getClass();// 3. 通过 Class.forName("完整类名")
Class<?> c3 = Class.forName("java.lang.String");
2. 反射可以做的事情
-
获取类的完整信息
Class<?> clazz = Class.forName("java.util.ArrayList"); System.out.println(clazz.getName()); // 包名+类名 System.out.println(clazz.getSuperclass()); // 父类
-
创建对象
Class<?> clazz = Class.forName("java.util.ArrayList"); Object obj = clazz.getDeclaredConstructor().newInstance(); // 调用无参构造器
-
访问字段(属性)
Class<Person> clazz = Person.class; Field nameField = clazz.getDeclaredField("name"); nameField.setAccessible(true); // 私有属性也能访问 Person p = new Person(); nameField.set(p, "Alice"); // 设置值 System.out.println(nameField.get(p)); // 获取值
-
调用方法
Method method = clazz.getDeclaredMethod("sayHello", String.class); method.setAccessible(true); method.invoke(p, "Tom"); // 相当于 p.sayHello("Tom")
-
操作构造器
Constructor<Person> constructor = clazz.getConstructor(String.class, int.class); Person p = constructor.newInstance("Alice", 20);
3. 反射的应用场景
-
框架开发
- 依赖注入(DI)、Spring、MyBatis 都广泛使用反射来动态实例化对象和调用方法。
- ORM 框架根据数据库表映射生成对象。
-
工具类
- JSON 序列化/反序列化(如 Jackson、FastJSON)需要根据对象的字段动态赋值。
- BeanCopier、PropertyUtils 等工具实现对象拷贝。
-
动态代理
- AOP(面向切面编程)通过反射调用目标方法。
- JDK 动态代理或 CGLib 都依赖反射。
-
运行时动态加载类
- 插件式架构或模块化系统,可以根据配置加载不同的类实现。
-
测试
- 单元测试中可以访问私有字段和方法,验证内部逻辑。
⚠️ 注意点
- 性能开销:反射比普通方法调用慢,因为 JVM 很难做优化。
- 安全问题:反射可以访问私有字段,可能破坏封装。
- 可维护性差:大量使用反射可能导致代码难以阅读和调试。
十四.注解
四种标准原注解
@Retention
用来定义注解的生命周期,即注解在哪个阶段可用。
RetentionPolicy.SOURCE:注解仅在源代码中存在,编译时会被丢弃。
RetentionPolicy.CLASS:注解在编译后保留在字节码中,但不会在运行时可见(默认策略)。
RetentionPolicy.RUNTIME:注解会保留在字节码中,并且在运行时可通过反射访问。
@Target
用来指定注解可以应用的 Java 元素类型(如类、方法、字段等)。
ElementType.TYPE:注解可用于类、接口或枚举。
ElementType.FIELD:注解可用于字段。
ElementType.METHOD:注解可用于方法。
ElementType.PARAMETER:注解可用于方法参数。
ElementType.CONSTRUCTOR:注解可用于构造函数。
ElementType.LOCAL_VARIABLE:注解可用于局部变量。
ElementType.ANNOTATION_TYPE:注解可用于其他注解类型。
ElementType.PACKAGE:注解可用于包。
@Inherited
用于标记注解是否可以被继承。如果一个注解标记了 @Inherited,则该注解会被应用到该类的子类上(仅适用于类级别的注解)。
@Documented
表示该注解应该包含在 Javadoc 中。也就是说,当生成 Javadoc 时,使用该注解的类、方法等会在文档中显示出来。
好的,我们来详细梳理一下在Java面试中常被问到的“八大常见异常”以及更广泛的异常相关面试题目。
十五.Java中八大常见异常
所谓的“八大常见异常”并非官方定义,而是面试中频繁被提及的、具有代表性的几个异常。它们通常涵盖了运行时异常(Unchecked)和编译时异常(Checked)的主要类别。
-
NullPointerException
(空指针异常)- 原因:尝试在
null
对象上调用实例方法或访问实例字段。 - 场景:最常见的运行时异常之一。例如:
String str = null; str.length();
。 - 面试重点:如何避免?(判空、使用
Optional
、保证对象初始化等)
- 原因:尝试在
-
ArrayIndexOutOfBoundsException
(数组下标越界异常)- 原因:试图访问数组中不存在的索引位置。
- 场景:循环遍历时索引超出数组长度或为负数。例如:
int[] arr = new int[5]; arr[5] = 10;
。 - 面试重点:与
StringIndexOutOfBoundsException
类似,是IndexOutOfBoundsException
的子类。
-
ClassCastException
(类转换异常)- 原因:尝试将对象强制转换为不是其实例的子类。
- 场景:向下转型时类型不匹配。例如:
Object obj = new String("hello"); Integer i = (Integer) obj;
。 - 面试重点:使用
instanceof
进行类型检查可以避免。
-
IllegalArgumentException
(非法参数异常)- 原因:向方法传递了一个不合法或不正确的参数。
- 场景:方法内部逻辑检测到参数无效时主动抛出。例如:传入负数给要求非负数的方法。
- 面试重点:
NumberFormatException
是其子类。
-
NumberFormatException
(数字格式化异常)- 原因:尝试将不符合数字格式的字符串转换为数字类型。
- 场景:
Integer.parseInt("abc")
或Double.parseDouble("xyz")
。 - 面试重点:是
IllegalArgumentException
的子类,处理字符串转数字时必须注意。
-
ArithmeticException
(算术异常)- 原因:出现异常的算术条件。最常见的是除以零。
- 场景:
int result = 10 / 0;
。 - 面试重点:注意整数除以零会抛出此异常,而浮点数除以零会得到
Infinity
或NaN
,不会抛异常。
-
FileNotFoundException
(文件未找到异常)- 原因:试图打开指定路径的文件进行读取,但该文件不存在。
- 场景:
new FileInputStream("nonexistent.txt");
。 - 面试重点:是
IOException
的子类,属于编译时异常(受检异常),必须处理(try-catch或throws)。
-
IOException
(输入输出异常)- 原因:发生某种I/O异常。这是一个更广泛的类别,
FileNotFoundException
是其子类。 - 场景:网络连接中断、磁盘写满、文件读写权限问题等。
- 面试重点:是编译时异常(受检异常) 的代表,处理文件、网络等I/O操作时必须考虑。
- 原因:发生某种I/O异常。这是一个更广泛的类别,
十六.==和equals有什么区别
一、==
运算符
-
作用:
==
是一个运算符。- 它比较的是两个操作数的值是否相等。
-
比较内容:
- 对于基本数据类型(
int
,char
,boolean
,double
等):比较的是变量存储的实际数值。int a = 5; int b = 5; System.out.println(a == b); // true (数值5等于5)
- 对于引用数据类型(对象、数组、字符串等):比较的是两个引用变量存储的内存地址值是否相同。换句话说,它判断的是两个引用是否指向堆内存中的同一个对象。
String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1 == str2); // false // 因为str1和str2是两个不同的String对象,分别在堆中有自己的内存地址。
- 对于基本数据类型(
二、equals()
方法
-
作用:
equals()
是一个方法,定义在java.lang.Object
类中。- 它的设计初衷是比较两个对象的“逻辑内容”或“业务意义上的相等性”。
-
默认行为:
- 在
Object
类中,equals()
方法的默认实现就是使用==
进行比较:public boolean equals(Object obj) {return (this == obj); }
- 这意味着,对于没有重写
equals()
方法的类,equals()
的行为和==
完全一样,都是比较内存地址。
- 在
-
重写(Override):
- 为了让
equals()
能够比较对象的内容而不是地址,许多重要的Java类(如String
,Integer
,Date
,List
等)都重写了equals()
方法。 - 例如
String
类:String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1.equals(str2)); // true // String类重写了equals(),它会逐个字符比较两个字符串的内容是否相同。
- 为了让
三、核心区别总结
特性 | == | equals() |
---|---|---|
类型 | 运算符 | 方法 |
定义位置 | 语言内置 | java.lang.Object 类中定义 |
比较对象 | 基本类型:比较值 引用类型:比较内存地址 | 比较对象的内容/逻辑相等性(需重写方法) |
可重写性 | 不可重写 | 可以被子类重写 |
典型用例 | 比较基本类型值 检查对象是否为 null | 比较字符串内容 比较集合元素 比较自定义对象的业务逻辑相等性 |
四、重要示例分析
public class EqualsDemo {public static void main(String[] args) {// 示例1:基本类型int a = 1000;int b = 1000;System.out.println(a == b); // true (值相等)// System.out.println(a.equals(b)); // 编译错误!基本类型没有方法// 示例2:String (字符串常量池)String s1 = "hello"; // 字面量,放入字符串常量池String s2 = "hello"; // 指向常量池中已有的"hello"System.out.println(s1 == s2); // true (指向同一个对象)System.out.println(s1.equals(s2)); // true (内容相同)// 示例3:String (new创建)String s3 = new String("hello"); // 在堆中新建对象String s4 = new String("hello"); // 在堆中新建另一个对象System.out.println(s3 == s4); // false (两个不同的对象,地址不同)System.out.println(s3.equals(s4)); // true (String重写了equals,内容相同)// 示例4:自定义类 (未重写equals)Person p1 = new Person("Alice", 25);Person p2 = new Person("Alice", 25);System.out.println(p1 == p2); // false (两个不同的对象)System.out.println(p1.equals(p2)); // false (默认用==比较,地址不同)// 如果Person类重写了equals方法来比较name和age,则p1.equals(p2)可能返回true。}
}class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}// 注意:这里没有重写equals方法,使用的是Object的默认实现。
}
五、面试要点
- 核心回答:
==
比较的是值(基本类型)或内存地址(引用类型);equals()
默认比较内存地址,但通常被重写用来比较对象的内容。 - String是特例:要特别注意
String
对象使用==
和equals()
的区别,这与字符串常量池有关。 - 重写equals():当你需要根据对象的属性(如ID、姓名等)来判断两个对象是否“相等”时,必须重写
equals()
方法(通常也需要重写hashCode()
方法以保证一致性)。 - null安全:调用
equals()
时,如果左边的对象可能为null
,应避免null.equals(someObject)
(会抛NullPointerException
),而应使用Objects.equals(obj1, obj2)
或"literal".equals(variable)
。
十七.String、StringBuilder和StringBuffer有什么区别和特点
String
、StringBuilder
和 StringBuffer
是 Java 中用于处理字符串的三个核心类,它们在可变性、线程安全性、性能等方面有显著区别。理解它们的差异是 Java 基础的重要部分。
一、核心区别概览
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
可变性 | 不可变 (Immutable) | 可变 (Mutable) | 可变 (Mutable) |
线程安全 | 线程安全 (不可变即安全) | 非线程安全 | 线程安全 |
性能 | 频繁修改时低效 | 高效 | 高效,但略低于 StringBuilder |
所属包 | java.lang | java.lang | java.lang |
JDK 版本 | 所有版本 | JDK 1.5+ | JDK 1.0+ |
二、详细解析
1. String
- 不可变字符串
-
核心特点:不可变性 (Immutability)
- 一旦创建,其内容(字符序列)就无法被修改。
- 任何看似“修改”
String
的操作(如concat()
,substring()
,+
连接),实际上都是创建一个新的String
对象,并将引用指向新对象,原对象保持不变。
-
优点:
- 线程安全:因为不可变,多个线程可以安全地共享同一个
String
实例,无需同步。 - 安全性:防止内容被意外修改,适合用作方法参数、Map的key等。
- 字符串常量池 (String Pool):JVM 维护一个字符串常量池,相同字面量的
String
可以共享同一个对象,节省内存。例如:String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); // true (指向常量池中同一个对象)
- 线程安全:因为不可变,多个线程可以安全地共享同一个
-
缺点:
- 性能差:在需要频繁修改或拼接字符串的场景下,会产生大量中间的、无用的
String
对象,导致频繁的内存分配和垃圾回收(GC),严重影响性能。String result = ""; for(int i = 0; i < 1000; i++) {result += i; // 每次循环都创建一个新String对象,效率极低 }
- 性能差:在需要频繁修改或拼接字符串的场景下,会产生大量中间的、无用的
2. StringBuilder
- 可变、非线程安全
-
核心特点:可变性 和 非线程安全。
- 内部维护一个可变的字符数组 (
char[]
)。当进行追加、插入、删除等操作时,直接修改这个数组,不会创建新对象。 - 非线程安全:它的方法没有同步(synchronized)。如果多个线程同时修改同一个
StringBuilder
实例,可能会导致数据不一致或错误。
- 内部维护一个可变的字符数组 (
-
优点:
- 性能高:在单线程环境下进行字符串拼接、修改时,性能远优于
String
和StringBuffer
,因为它避免了对象创建和同步开销。
- 性能高:在单线程环境下进行字符串拼接、修改时,性能远优于
-
缺点:
- 非线程安全:不能在多线程环境中安全地共享和修改同一个实例。
-
使用场景:单线程中进行大量的字符串拼接、修改操作(如循环内构建字符串)。
3. StringBuffer
- 可变、线程安全
-
核心特点:可变性 和 线程安全。
- 功能和
StringBuilder
几乎完全相同,内部也是可变的字符数组。 - 线程安全:它的关键方法(如
append()
,insert()
,delete()
)都被synchronized
关键字修饰,保证了在多线程环境下的安全。
- 功能和
-
优点:
- 线程安全:可以在多线程环境中安全地共享和修改同一个实例。
- 性能较好:相比
String
,在频繁修改时性能好得多。
-
缺点:
- 性能开销:由于方法加了同步锁,在单线程环境下,性能略低于
StringBuilder
,因为存在不必要的同步开销。
- 性能开销:由于方法加了同步锁,在单线程环境下,性能略低于
-
使用场景:多线程环境中需要共享并修改同一个字符串缓冲区(但这种情况相对较少见)。
三、共同点
- 继承关系:都实现了
CharSequence
接口。 - 常用方法:都提供了类似
append()
,insert()
,delete()
,toString()
,length()
,charAt()
等方法(String
的append
等是通过+
或concat
体现)。 - 初始容量:
StringBuilder
和StringBuffer
内部都有一个容量(capacity)的概念。当内容超过当前容量时,会自动扩容(通常是原容量的2倍+2),并复制原有内容。可以通过构造函数指定初始容量以优化性能。
四、如何选择?
-
优先使用
String
:- 字符串内容基本不变。
- 需要作为Map的key或多线程共享且不修改。
- 进行少量的字符串连接(编译器会优化
+
操作)。
-
优先使用
StringBuilder
:- 在单线程中进行大量的字符串拼接、修改操作。
- 这是最常见的场景,性能最优。
-
使用
StringBuffer
:- 在多线程环境中,需要多个线程并发修改同一个字符串缓冲区(需谨慎设计,通常有更好的并发方案)。
- 由于
StringBuilder
是在 JDK 1.5 引入的,在维护非常老的代码(JDK 1.4 及以下)时可能需要使用StringBuffer
。
五、面试要点
- 核心三要素:回答时务必围绕可变性、线程安全性、性能这三点展开。
- 不可变性是关键:解释
String
的不可变性及其带来的影响(安全、常量池、性能问题)。 - StringBuilder vs StringBuffer:强调
StringBuilder
是StringBuffer
的非同步版本,性能更高,是单线程下的首选。 - 性能对比:明确指出在频繁修改场景下,
StringBuilder
>StringBuffer
>>String
。 - 实际应用:给出明确的选择建议,通常推荐
StringBuilder
用于拼接。
总结:String
如同“只读文档”,安全但修改成本高;StringBuilder
如同“个人草稿本”,高效但不共享;StringBuffer
如同“带锁的共享文档”,安全共享但访问稍慢。根据场景选择合适的工具。