一、第一优先级

1.1 面向对象三大特征

面向对象三大特征是:封装、继承、多态

1. 封装

封装就是把对象内部的实现细节隐藏起来,对外只提供统一的访问接口。

比如类中的属性一般用 private 修饰,外部不能直接访问,只能通过 getter/setter 方法进行访问和修改。

封装的好处是:

  • 提高代码安全性;

  • 降低外部使用成本;

  • 隐藏内部实现,方便后续维护和修改。

2. 继承

继承就是子类可以复用父类已有的属性和方法。

Java 中使用 extends 实现继承。

继承的好处是:

  • 提高代码复用性;

  • 建立类与类之间的层级关系;

  • 为多态提供基础。

3. 多态

多态指的是:同一个方法调用,在不同对象上表现出不同的行为。

比如父类 Animal 有一个 eat() 方法,子类 DogCat 都重写了这个方法。当父类引用指向不同子类对象时,调用 eat() 会执行不同子类自己的实现。


1.2 多态是什么?

多态的核心是:

父类引用指向子类对象,调用被子类重写的方法时,实际执行的是子类的方法。

例如:

Animal animal = new Dog();
animal.eat();

虽然左边是 Animal 类型,但右边实际创建的是 Dog 对象,所以调用 eat() 时,执行的是 Dog 类中重写后的 eat() 方法。

多态的实现条件

Java 中实现多态一般需要三个条件:

1. 继承或者实现接口

子类和父类之间要有继承关系,或者类和接口之间要有实现关系。

2. 方法重写

子类要对父类中的方法进行重写。

注意:@Override 注解不是必须的,但推荐加上。它可以帮助编译器检查这个方法是否真的重写成功。

3. 向上转型

创建子类对象时,使用父类类型来接收。

例如:

Animal animal = new Dog();

这就叫向上转型。

多态的好处

多态最大的好处是:提高代码扩展性,降低代码耦合度。

没有多态时,如果新增一个子类,可能需要修改大量已有代码。

有了多态之后,只要新增子类并重写父类方法,原来的调用逻辑可以尽量保持不变。

例如:

public void feed(Animal animal) {
    animal.eat();
}

这个方法既可以接收 Dog,也可以接收 Cat,以后新增 Bird 类,只要继承 Animal 并重写 eat() 方法即可。


1.3 重载和重写

重载是什么?

重载是指:同一个类中,方法名相同,但参数列表不同。

参数列表不同可以是:

  • 参数个数不同;

  • 参数类型不同;

  • 参数顺序不同。

例如:

public void add(int a, int b) {}

public void add(double a, double b) {}

public void add(int a, int b, int c) {}

重载发生在同一个类中,属于编译期行为。

重写是什么?

重写是指:子类继承父类之后,对父类已有的方法重新实现。

例如:

class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}

重写发生在父子类之间,属于运行期行为。

重写和多态的关系

重写是多态实现的重要条件。

多态的本质是:父类引用指向子类对象,调用方法时,根据实际对象类型执行对应的重写方法。


1.4 抽象类和接口

1.4.1 什么是抽象类?

抽象类是使用 abstract 修饰的类。

例如:

abstract class Animal {
    public abstract void eat();
}

抽象类的特点:

  • 抽象类不能直接创建对象;

  • 抽象类可以有抽象方法;

  • 抽象类也可以有普通方法;

  • 抽象类可以有成员变量;

  • 抽象类可以有构造方法。

抽象类一般用于描述一类事物的共同特征。

1.4.2 什么是接口?

接口是使用 interface 定义的一种行为规范。

例如:

interface Flyable {
    void fly();
}

接口更强调某种能力。

比如鸟、飞机、无人机都可以飞,但它们不是同一个父类,这时候就可以抽象出一个 Flyable 接口。

1.4.3 抽象类和接口的核心区别

抽象类更强调 “是什么”,接口更强调 “能做什么”

对比项

抽象类

接口

关键字

abstract class

interface

继承数量

只能单继承

可以多实现

构造方法

可以有

不可以有

成员变量

可以有普通成员变量

默认是 public static final

方法

可以有抽象方法和普通方法

可以有抽象方法、默认方法、静态方法

使用场景

有明显父子关系

表示某种能力或规范

1.4.4 接口和抽象类分别可以有构造方法吗?

构造方法是创建对象时自动执行的方法,主要用于初始化对象。

接口不能有构造方法,因为接口不能被实例化。

抽象类可以有构造方法。虽然抽象类不能直接创建对象,但子类创建对象时,会先调用父类抽象类的构造方法。

1.4.5 什么时候用抽象类?什么时候用接口?

如果多个类之间有明显的父子关系,并且有一些共同属性和方法,可以使用抽象类。

比如:

abstract class Animal {}

如果多个类之间没有明显的父子关系,但它们都具备某种能力,可以使用接口。

比如:

interface Runnable {}
interface Flyable {}
interface Serializable {}

一句话总结:

抽象类表示“是不是同一类东西”,接口表示“有没有某种能力”。


1.5 ==equals

== 比较什么?

== 分两种情况:

如果比较的是基本数据类型,比较的是具体的值。

例如:

int a = 10;
int b = 10;
System.out.println(a == b); // true

如果比较的是引用数据类型,比较的是对象的内存地址。

例如:

User u1 = new User();
User u2 = new User();
System.out.println(u1 == u2); // false

因为 u1u2 指向的是两个不同对象。

equals() 比较什么?

equals()Object 类中的方法。

默认情况下,equals()== 一样,也是比较对象地址。

但是很多类会重写 equals() 方法,比如 String 类重写后比较的是字符串内容。

例如:

String s1 = new String("abc");
String s2 = new String("abc");

System.out.println(s1 == s2);      // false
System.out.println(s1.equals(s2)); // true

1.6 equals()hashCode()

equals() 是干什么的?

equals() 用来判断两个对象是否相等,返回值是 boolean 类型。

hashCode() 是干什么的?

hashCode() 用来返回对象的哈希值,返回值是 int 类型。

它主要用于哈希表结构中,比如 HashMapHashSet

二者的关系

如果两个对象通过 equals() 判断相等,那么它们的 hashCode() 必须相等。

但是如果两个对象的 hashCode() 相等,它们不一定通过 equals() 判断相等。

也就是说:

equals 相等 => hashCode 一定相等
hashCode 相等 => equals 不一定相等

原因是哈希冲突。

为什么重写 equals 必须重写 hashCode?

因为像 HashMapHashSet 这类集合,底层会先根据 hashCode() 找位置,再用 equals() 判断对象是否真的相等。

如果只重写 equals(),不重写 hashCode(),可能会导致两个逻辑上相等的对象被放到不同位置,集合判断出错。


1.7 String 为什么不可变?

String 不可变指的是:

String 对象一旦创建,它里面保存的字符串内容就不能再被修改。

例如:

String s = "abc";
s = s + "def";

这里看起来像是修改了 s,但实际上不是修改原来的 "abc",而是创建了一个新的字符串对象 "abcdef",然后让 s 指向新对象。

String 不可变的原因

1. 安全性

字符串经常用于文件路径、网络地址、用户名、密码、类加载信息等场景。

如果字符串可以被随意修改,可能会带来安全问题。

2. 支持字符串常量池

因为 String 不可变,所以字符串常量池中的字符串可以被多个引用共享,不用担心被某个引用修改。

例如:

String a = "abc";
String b = "abc";

ab 可以指向常量池中的同一个 "abc"

3. 方便缓存 hashCode

String 经常作为 HashMap 的 key。

因为 String 不可变,所以它的哈希值可以被缓存,提高查找效率。

4. 底层设计保证不可变

String 类本身被 final 修饰,不能被继承。

同时,底层保存字符内容的数组也是私有的,外部不能直接修改。

注意:不能简单地说“String 不可变只是因为 final 修饰”。final 只是其中一个原因,更重要的是 String 没有对外提供修改内部内容的方法。


1.8 String、StringBuilder、StringBuffer 区别

String

String 是不可变的。

每次字符串拼接,可能都会创建新的字符串对象。

适合少量字符串操作。

StringBuilder

StringBuilder 是可变的。

适合频繁进行字符串拼接。

它的优点是效率高,缺点是线程不安全。

StringBuffer

StringBuffer 也是可变的。

它和 StringBuilder 类似,但方法上加了 synchronized,所以线程安全。

不过因为加锁,性能一般比 StringBuilder 低。

总结

类型

是否可变

是否线程安全

使用场景

String

不可变

安全

少量字符串操作

StringBuilder

可变

不安全

单线程频繁拼接

StringBuffer

可变

安全

多线程频繁拼接


1.9 Integer 缓存机制

示例:

Integer a = 127;
Integer b = 127;

Integer c = 128;
Integer d = 128;

System.out.println(a == b);
System.out.println(c == d);

输出结果通常是:

true
false

原因是:

Integer 在自动装箱时,会调用 Integer.valueOf() 方法。

Integer 默认缓存范围是:

-128 到 127

在这个范围内,多个相同值的 Integer 对象会复用缓存中的对象。

超过这个范围,一般会重新创建对象。

所以:

Integer a = 127;
Integer b = 127;

ab 指向的是同一个缓存对象,所以 a == btrue

而:

Integer c = 128;
Integer d = 128;

cd 一般是两个不同对象,所以 c == dfalse

注意

包装类比较值时,不建议用 ==,应该用 equals()

Integer x = 128;
Integer y = 128;

System.out.println(x.equals(y)); // true

1.10 Java 异常体系

Java 异常体系的顶层父类是 Throwable

Throwable 下面主要分为两大类:

  • Error

  • Exception

结构如下:

Throwable
 ├── Error
 └── Exception
      ├── Checked Exception
      └── RuntimeException

Throwable 是什么?

Throwable 是 Java 异常体系的顶层父类。

只有 Throwable 及其子类,才能被 throw 抛出,也才能被 try-catch 捕获。

Error 是什么?

Error 通常表示 JVM 层面或者系统层面的严重问题,程序一般无法处理。

常见的 Error 有:

  • OutOfMemoryError:内存溢出;

  • StackOverflowError:栈溢出。

一般情况下,程序不应该主动捕获和处理 Error

Exception 是什么?

Exception 表示程序运行过程中可以处理的异常。

Exception 又可以分为:

  • 编译时异常,也叫 Checked Exception;

  • 运行时异常,也叫 RuntimeException。

编译时异常

编译时异常是编译阶段就要求必须处理的异常。

要么使用 try-catch 捕获,要么在方法上使用 throws 声明。

比如:

IOException
SQLException

运行时异常

运行时异常是编译器不强制处理的异常,程序运行时才可能出现。

常见的运行时异常有:

NullPointerException
ArrayIndexOutOfBoundsException
ClassCastException
ArithmeticException

throw 和 throws 区别

throw 写在方法内部,用来主动抛出一个具体的异常对象。

例如:

throw new RuntimeException("参数错误");

throws 写在方法声明上,表示这个方法可能会抛出某些异常,把异常处理责任交给调用者。

例如:

public void readFile() throws IOException {
    
}

一句话总结:

throw 是真正抛出异常,throws 是声明可能抛出异常。


二、第二优先级

2.1 JVM、JRE、JDK 区别

JVM 是什么?

JVM 是 Java 虚拟机。

它的作用是运行 Java 编译后的 .class 字节码文件。

JVM 是 Java 实现跨平台的核心。

Java 程序先被编译成字节码文件,然后交给不同平台上的 JVM 去执行。

所以 Java 可以实现:

一次编译,到处运行。

JRE 是什么?

JRE 是 Java 运行环境。

它包含:

  • JVM;

  • Java 核心类库。

如果只是运行 Java 程序,有 JRE 就够了。

JDK 是什么?

JDK 是 Java 开发工具包。

它包含:

  • JRE;

  • 编译工具,比如 javac

  • 打包工具,比如 jar

  • 其他开发工具。

如果要开发 Java 程序,就需要安装 JDK。

三者关系

JDK > JRE > JVM

一句话总结:

JVM 负责运行字节码,JRE 负责提供运行环境,JDK 负责提供开发环境。


2.2 Java 基本数据类型

Java 有 8 种基本数据类型:

整数类型

byte
short
int
long

浮点类型

float
double

字符类型

char

布尔类型

boolean

除了基本数据类型,其他大多数都是引用数据类型。

常见引用类型有:

  • 类;

  • 接口;

  • 数组;

  • 枚举;

  • 注解;

  • String。

注意:String 不是基本数据类型,它是引用数据类型。


2.3 自动装箱和拆箱

什么是装箱?

装箱就是把基本数据类型转换成对应的包装类。

例如:

int a = 10;
Integer b = a;

这里就是自动装箱。

什么是拆箱?

拆箱就是把包装类转换成基本数据类型。

例如:

Integer a = 10;
int b = a;

这里就是自动拆箱。

基本类型和包装类对应关系

基本类型

包装类

byte

Byte

short

Short

int

Integer

long

Long

float

Float

double

Double

char

Character

boolean

Boolean

特殊需要记住:

  • int 对应 Integer

  • char 对应 Character

其他基本都是首字母大写。

为什么需要包装类?

因为 Java 中很多地方需要使用对象,而基本数据类型不是对象。

比如集合中不能直接存基本类型:

List<int> list; // 错误
List<Integer> list; // 正确

包装类的作用是:

  • 让基本类型可以作为对象使用;

  • 可以放入集合;

  • 可以使用包装类提供的方法;

  • 可以表示 null


2.4 static 关键字

static 可以修饰:

  • 成员变量;

  • 成员方法;

  • 代码块;

  • 内部类。

static 的作用

static 修饰的成员属于类,而不是属于某个具体对象。

所以可以直接通过类名调用:

ClassName.method();
ClassName.value;

static 修饰变量

静态变量被所有对象共享。

例如:

class Student {
    static String school = "桂电";
}

所有 Student 对象共享同一个 school

static 修饰方法

静态方法可以直接通过类名调用。

例如:

Math.max(1, 2);

注意:静态方法中不能直接访问非静态成员,因为非静态成员属于对象,而静态方法属于类。

static 代码块

静态代码块在类加载时执行,通常用于初始化静态资源。

例如:

static {
    System.out.println("类加载时执行");
}

一句话总结:

static 的核心作用是让成员从对象级别提升到类级别,不需要创建对象就可以访问,并且被所有对象共享。


2.5 final、finally、finalize 区别

final

final 是一个关键字。

它可以修饰类、方法和变量。

final 修饰类

类不能被继承。

例如:

public final class String {}

final 修饰方法

方法不能被子类重写。

final 修饰变量

变量只能赋值一次。

如果是基本类型,值不能变。

如果是引用类型,引用地址不能变,但对象内部内容仍然可能改变。

例如:

final List<String> list = new ArrayList<>();
list.add("abc"); // 可以

list = new ArrayList<>(); // 不可以

finally

finally 一般和 try-catch 一起使用。

它的特点是:无论是否发生异常,finally 代码块一般都会执行。

例如:

try {
    System.out.println("try");
} catch (Exception e) {
    System.out.println("catch");
} finally {
    System.out.println("finally");
}

finally 通常用于释放资源,比如关闭文件、关闭数据库连接等。

finalize

finalize()Object 类中的一个方法。

它的作用是:对象被垃圾回收之前,JVM 可能会调用这个方法。

但是 finalize() 不可靠,不能保证一定执行,现在也不推荐使用。

一句话总结:

final 是关键字,finally 是异常处理中的代码块,finalize 是 Object 类中的方法。


2.6 Java 只有值传递

Java 中只有值传递,没有引用传递。

什么是值传递?

值传递指的是:方法调用时,传进去的是实参的一份拷贝。

方法内部修改形参,不会直接改变实参本身。

基本类型传递

public static void change(int x) {
    x = 100;
}

public static void main(String[] args) {
    int a = 10;
    change(a);
    System.out.println(a); // 10
}

因为传进去的是 a 的值的拷贝,所以方法里修改 x 不影响外面的 a

引用类型传递

public static void change(User user) {
    user.name = "Tom";
}

引用类型传递时,传进去的是对象地址值的一份拷贝。

所以方法内部可以通过这个地址找到对象,并修改对象属性。

但是如果在方法内部让形参重新指向新对象,不会影响外面的引用。

public static void change(User user) {
    user = new User();
}

这里改变的是形参 user 的指向,不会改变外面实参的指向。

一句话总结:

Java 参数传递永远是值传递。基本类型传的是值的拷贝,引用类型传的是地址值的拷贝。


2.7 new String("abc") 创建几个对象?

代码:

String s = new String("abc");

这行代码最多创建 2 个对象,最少创建 1 个对象。

情况一:常量池中没有 "abc"

如果字符串常量池中还没有 "abc",那么会先在字符串常量池中创建一个 "abc" 对象。

然后 new String("abc") 会在堆中再创建一个新的 String 对象。

所以一共创建 2 个对象。

情况二:常量池中已经有 "abc"

如果字符串常量池中已经存在 "abc",那么只会在堆中创建一个新的 String 对象。

所以一共创建 1 个对象。

注意

String s1 = "abc";
String s2 = new String("abc");

s1 指向字符串常量池中的 "abc"

s2 指向堆中新创建的 String 对象。

所以:

System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

2.8 try-catch-finally 执行顺序

正常执行顺序是:

  1. 先执行 try

  2. 如果 try 中出现异常,就执行对应的 catch

  3. 最后执行 finally

例如:

try {
    System.out.println("try");
} catch (Exception e) {
    System.out.println("catch");
} finally {
    System.out.println("finally");
}

没有异常时

执行顺序:

try -> finally

有异常且被捕获时

执行顺序:

try -> catch -> finally

有异常但没被捕获时

执行顺序:

try -> finally -> 抛给调用者

finally 的特点

finally 一般都会执行,即使 trycatch 中有 return,也会先执行 finally,再返回。

但是如果执行了 System.exit(0),JVM 直接退出,finally 就不会执行。


2.9 Java IO、BIO、NIO、AIO

Java IO 流怎么分类?

Java IO 流可以从三个角度分类。

1. 按数据流方向分类

输入流:从数据源读取数据到程序中。

输出流:把程序中的数据写到目标位置。

2. 按数据处理单位分类

字节流:以字节为单位读写数据,适合处理图片、视频、音频、压缩包等二进制文件。

字符流:以字符为单位读写数据,适合处理文本文件。

3. 按功能分类

节点流:直接连接数据源或目标位置。

处理流:对已有流进行包装和增强。

比如:

BufferedInputStream
BufferedReader
DataInputStream
ObjectInputStream

为什么有字节流,还要有字符流?

因为计算机底层所有数据本质上都是字节。

但是处理文本时,还涉及字符编码问题,比如 UTF-8、GBK。

字符流帮我们封装了字节到字符之间的编码和解码过程,所以处理文本更方便。

一句话总结:

字节流适合处理所有类型的数据,字符流更适合处理文本数据。

IO 流用到了什么设计模式?

Java IO 流大量使用了装饰器模式。

装饰器模式的核心思想是:

不改变原有对象结构的前提下,动态地给对象添加新的功能。

比如:

InputStream inputStream = new FileInputStream("a.txt");
BufferedInputStream bis = new BufferedInputStream(inputStream);

FileInputStream 负责基本的文件读取。

BufferedInputStream 在它的基础上增加了缓冲功能。

这就是对原有流的增强。


BIO、NIO、AIO 区别

BIO

BIO 是阻塞 IO。

特点是:

一个连接通常对应一个线程,线程在读写数据时会阻塞等待。

如果客户端很多,服务端就需要创建大量线程,线程资源消耗比较大。

BIO 适合连接数较少、并发不高的场景。

NIO

NIO 是非阻塞 IO。

核心组件包括:

  • Channel;

  • Buffer;

  • Selector。

NIO 的核心思想是:

一个线程可以通过 Selector 管理多个客户端连接。

Selector 可以理解成一个连接管理器。

它会监听多个 Channel 上的事件,比如连接事件、读事件、写事件。

当某个连接真正准备好读写时,线程再去处理它。

NIO 适合高并发连接场景。

AIO

AIO 是异步 IO。

核心思想是:

发起 IO 请求后立即返回,IO 完成后再通知程序处理结果。

线程不需要一直等待 IO 完成,可以继续执行其他任务。

AIO 适合连接数多、IO 操作时间长的场景。

总结

类型

特点

核心思想

适用场景

BIO

阻塞 IO

一个连接一个线程

并发较低

NIO

非阻塞 IO

一个线程管理多个连接

高并发连接

AIO

异步 IO

请求后立即返回,完成后通知

异步处理场景

一句话总结:

BIO 是阻塞等待,NIO 是多路复用,AIO 是异步回调。


2.10 Java 创建对象方式和反射

Java 创建对象有哪些方式?

常见方式有:

1. 使用 new 关键字

User user = new User();

这是最常见的创建对象方式。

2. 使用反射

Class<?> clazz = User.class;
User user = (User) clazz.getDeclaredConstructor().newInstance();

3. 使用 clone

User user2 = user1.clone();

前提是类要实现 Cloneable 接口。

4. 使用反序列化

把对象从文件、网络或者字节流中恢复出来。

5. 使用工厂方法

比如 Spring 中通过容器创建 Bean,本质上也属于框架帮我们创建对象。


三、反射

3.1 什么是反射?

反射指的是:

Java 程序在运行期间,可以动态获取一个类的信息,并且可以动态创建对象、调用方法、访问属性。

简单来说,反射就是:

程序运行时,自己去查看一个类,并且还能操作这个类的对象。

比如程序运行时可以知道:

  • 这个类叫什么名字;

  • 有哪些属性;

  • 有哪些方法;

  • 有哪些构造方法;

  • 能不能创建对象;

  • 能不能调用某个方法。


3.2 反射是怎么实现的?

反射主要依赖:

java.lang.Class
java.lang.reflect

常用类有:

  • Class:表示类的字节码对象;

  • Constructor:表示构造方法;

  • Method:表示成员方法;

  • Field:表示成员变量。

例如:

Class<?> clazz = User.class;

Constructor<?> constructor = clazz.getConstructor();

Object obj = constructor.newInstance();

Method method = clazz.getMethod("setName", String.class);

method.invoke(obj, "Tom");

3.3 反射的原理是什么?

每个类被 JVM 加载之后,JVM 都会为这个类维护一个对应的 Class 对象。

这个 Class 对象关联了类的元信息,比如:

  • 类名;

  • 父类;

  • 接口;

  • 字段;

  • 方法;

  • 构造器。

通过这个 Class 对象,我们就可以在运行时动态地创建对象、调用方法和访问字段。

一句话总结:

反射的核心入口是 Class 对象,Class 对象保存了类的元信息。


3.4 反射有哪些应用场景?

1. Spring 框架

Spring 大量使用反射来创建对象、注入属性、调用方法。

比如:

@Autowired
private UserService userService;

Spring 能够自动创建并注入对象,底层就离不开反射。

2. 动态代理

Java 动态代理中,会在运行时创建代理对象,并调用目标对象的方法。

3. JDBC 加载驱动

早期 JDBC 中常见写法:

Class.forName("com.mysql.cj.jdbc.Driver");

这就是通过反射加载数据库驱动类。

4. 框架通用能力

很多框架都需要在不知道具体类名的情况下,动态创建对象和调用方法。

比如:

  • Spring;

  • MyBatis;

  • Hibernate;

  • JUnit。


3.5 反射的优缺点

优点

反射提高了程序的灵活性。

程序可以在运行时动态操作类,不需要在编译期写死。

很多框架就是靠反射实现通用能力的。

缺点

反射也有缺点:

  • 性能比直接调用低;

  • 破坏封装性;

  • 代码可读性变差;

  • 可能带来安全问题。

所以反射一般不在普通业务代码中大量使用,更多出现在框架底层。


四、面试总结版

JavaSE 高频面试题可以抓住几条主线:

第一条是面向对象,包括封装、继承、多态、抽象类、接口、重载、重写。

第二条是 Object 常用方法,包括 equals()hashCode()toString()

第三条是 String 相关问题,包括 String 不可变、字符串常量池、StringBuilder、StringBuffer。

第四条是包装类和基本类型,包括自动装箱、自动拆箱、Integer 缓存机制。

第五条是异常体系,包括 Throwable、Error、Exception、运行时异常、编译时异常、throw 和 throws。

第六条是 Java 基础关键字,包括 static、final、finally、finalize。

第七条是 IO,包括字节流、字符流、BIO、NIO、AIO、Selector、装饰器模式。

第八条是反射,包括 Class 对象、Method、Field、Constructor,以及 Spring 等框架中的应用。