面试官:java 修饰符有几个?我答出 7 个后,他追问了 volatile 和 transient
从访问权限到 jvm 指令,从内存可见性到序列化,这篇带你彻底吃透 java 修饰符
我们之前聊过 数据类型(值长什么样)和 变量类型(值存在哪、活多久)。今天我们来补上 java 数据体系的最后一块拼图:修饰符(modifiers)。
如果说变量是“房间”,修饰符就是“门锁”和“特殊功能按钮”:
- 访问控制修饰符:决定谁能打开这扇门(
public、private等) - 非访问修饰符:决定这扇门有什么特殊功能(能否被继承、是否线程安全、是否参与序列化等)
面试中,如果只能答出 public、static、final,那顶多算入门。真正的加分项在于 volatile、transient、synchronized 这些涉及 jvm 底层内存模型的修饰符。
下面我们一一拆解。
一、访问控制修饰符(4 种)—— 封装的第一道防线
这 4 个修饰符决定了“谁可以访问这个类/方法/变量”。它们的权限从宽到严依次是:
| 修饰符 | 同一个类 | 同一个包 | 子类(不同包) | 任何地方(全局) |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | ✅ |
| protected | ✅ | ✅ | ✅(必须通过继承) | ❌ |
| default(无修饰符) | ✅ | ✅ | ❌ | ❌ |
| private | ✅ | ❌ | ❌ | ❌ |
关键细节(面试高频)
protected的“特殊加成”:它不仅允许同包访问,还允许不同包但存在继承关系的子类访问父类的protected成员。这是实现模板方法模式的基础。default(包私有):没有写任何修饰符时就是它。这个权限在大型项目内部模块划分中非常有用——包内的工具类不想对外暴露,就用default。- 类和接口的顶级修饰符:顶层类(非内部类)只能用
public或default,不能用private或protected(因为顶级类没有“父容器”)。
二、非访问修饰符(8+ 种)—— 真正的“硬核”知识点
java 提供了丰富的非访问修饰符,用来赋予变量、方法、类特殊的行为。其中 static、final 是基础,volatile、transient、synchronized 是进阶区分点。
1.static—— 属于类,不属于对象
- 修饰变量:全局唯一,存储在方法区(元空间),所有对象共享一份。
- 修饰方法:类方法,可以直接通过
类名.方法名()调用,不能访问非静态成员。 - 修饰代码块:静态代码块,类加载时执行一次,常用于初始化配置。
public class counter {
public static int count = 0; // 静态变量,全局唯一
public static void increment() { // 静态方法
count++;
}
static { // 静态代码块,类加载时执行
system.out.println("counter 类已加载");
}
}
与之前的联动:我们聊变量类型时说过,静态变量生命周期最长,伴随整个 jvm 进程,务必谨慎使用,避免内存泄漏。
2.final—— 不可变性(“上了锁”)
- 修饰变量:基本类型值不可变,引用类型引用地址不可变(但对象内容可变)。
- 修饰方法:方法不可被重写(override)。
- 修饰类:类不可被继承(如
string类)。
final class animal {} // 无法被继承
class dog extends animal {} // ❌ 编译报错!
public class parent {
public final void speak() { // 子类不能重写此方法
system.out.println("hello");
}
}
延伸:final + static 常用于定义常量(public static final int max_size = 100;)。这种常量在编译期会被直接替换为字面量,性能最优。
3.abstract—— 抽象(“只定规矩,不做事”)
- 修饰类:抽象类,不能被实例化,只能被继承。
- 修饰方法:抽象方法,只有声明没有实现(
{}都没有),必须由子类实现。
public abstract class shape {
public abstract double area(); // 只有方法签名,没有方法体
}
规则:abstract 不能和 final 一起用(因为 final 禁止继承,abstract 强制继承,矛盾)。abstract 也不能和 static、private 一起修饰方法(因为抽象方法要被子类实现,而 static/private 无法被重写)。
4.synchronized—— 同步锁(线程安全基石)
- 修饰方法:同一时刻只允许一个线程进入该方法(锁的是当前对象实例或类的 class 对象)。
- 修饰代码块:更精细地控制锁粒度。
public class safecounter {
private int count = 0;
public synchronized void increment() { // 同步方法,线程安全
count++;
}
public void decrement() {
synchronized (this) { // 同步代码块,粒度更细
count--;
}
}
}
进阶:synchronized 依赖于 jvm 底层的 monitor(管程) 机制,在 jdk 1.6 之后做了大量优化(偏向锁、轻量级锁、重量级锁),这也是面试高并发时的必考点。
5.volatile—— 内存可见性(轻量级同步)
修饰变量:强制线程每次读取该变量时都从主内存中读取,而不是从 cpu 缓存(工作内存)中读取。禁止指令重排序。
public class status {
private volatile boolean flag = true; // 一个线程修改 flag,其他线程立刻可见
}
volatile vs synchronized 区别(高频面试题):
volatile仅保证可见性和有序性,不保证原子性(如i++操作依然线程不安全)。synchronized保证可见性、有序性和原子性,但性能开销更大。
经典应用场景:状态标志位(boolean flag)、单例模式中的双重检查锁(double-checked locking)。
6.transient—— 不参与序列化
修饰变量:当对象被序列化(如写入文件或网络传输)时,被 transient 修饰的变量会被忽略,不会被保存。
public class user implements serializable {
private string name; // 会序列化
private transient string password; // 不会序列化(敏感信息,不存储)
}
通常用于标记密码、临时缓存等不需要持久化或敏感的数据。配合 serializable 接口使用。
7.native—— 调用本地代码(jni)
修饰方法:表示该方法的实现由非 java 语言(如 c/c++)编写,通过 jni(java native interface)调用。
public class system {
public static native void arraycopy(...); // 底层由 c 实现,性能极高
}
日常开发极少使用,但面试中能说出这是“jni 调用本地方法”就足以加分。
8.strictfp—— 浮点数精度严格模式(极少用)
修饰类、接口、方法:保证浮点数运算在不同平台(如 x86 和 arm)上结果完全一致,使用 ieee 754 规范。现代硬件差异已经很小,几乎用不到。
9.default(接口特有)—— 接口的默认方法(java 8+)
注意:这里的 default 和访问控制中的“包私有(无修饰符)”完全是两码事。
在 接口(interface) 中,用 default 修饰的方法可以有默认实现,实现类可以选择重写或不重写。
public interface myinterface {
default void log() { // 接口中的默认方法
system.out.println("默认日志输出");
}
}
三、修饰符组合规则(哪些能一起用?)
很多修饰符之间是“水火不容”的,这是面试中的隐藏考点:
| 组合 | 是否合法 | 原因 |
|---|---|---|
abstract + final | ❌ 非法 | abstract 强制子类实现,final 禁止继承,互斥 |
abstract + static(方法) | ❌ 非法 | static 方法属于类,不能被重写;abstract 方法强制重写 |
abstract + private(方法) | ❌ 非法 | private 方法无法被子类访问,无法重写 |
static + final(变量) | ✅ 合法 | 经典常量组合(public static final) |
synchronized + static(方法) | ✅ 合法 | 锁的是类的 class 对象,而非实例对象 |
volatile + final | ❌ 非法(某些版本编译报错) | final 变量不可变,volatile 保证可见性,语义冲突 |
transient + static | ✅ 合法 | 静态变量不参与序列化,加上 transient 可读性更好(虽多余) |
四、修饰符在 jvm 字节码中的体现(进阶)
所有修饰符在编译后都会变成 .class 文件中的 访问标志(access flags)。例如:
public static final int max = 10;在字节码中对应acc_public | acc_static | acc_final。synchronized方法在字节码中会添加acc_synchronized标志,jvm 执行时会自动获取对象的监视器锁。volatile变量在字节码中会被标记为acc_volatile,jvm 会在读写该变量时插入内存屏障(memory barrier),防止指令重排序。
五、实战避坑指南
滥用 static 导致内存泄漏:在微服务或长期运行的应用中,static 集合如果不断添加数据而不清理,会直接撑爆元空间或堆内存。
volatile 的原子性陷阱:volatile int count = 0; count++ 并不是线程安全的!因为 count++ 包含了“读-改-写”三步,volatile 只保证读的可见性,不保证三步的原子性。必须使用 atomicinteger 或 synchronized。
transient 反序列化时的默认值:被 transient 修饰的变量,反序列化后会被赋予默认值(如 int 变 0,string 变 null)。如果业务上需要有默认值,需要在 readobject() 中手动处理。
final 修饰引用类型的“陷阱”:
final list<string> list = new arraylist<>();
list.add("hello"); // ✅ 合法!对象内容可以变
list = new arraylist<>(); // ❌ 编译报错!引用地址不能变
六、总结(一张表速查所有修饰符)
| 修饰符 | 分类 | 修饰目标 | 核心作用 |
|---|---|---|---|
public | 访问控制 | 类、方法、变量 | 全局可访问 |
protected | 访问控制 | 方法、变量 | 同包 + 子类(继承)可访问 |
default(无) | 访问控制 | 类、方法、变量 | 仅同包可访问(包私有) |
private | 访问控制 | 方法、变量、内部类 | 仅本类可访问 |
static | 非访问 | 变量、方法、代码块 | 属于类,不属于对象 |
final | 非访问 | 类、方法、变量 | 不可继承、不可重写、不可变 |
abstract | 非访问 | 类、方法 | 抽象定义,需子类实现 |
synchronized | 非访问 | 方法、代码块 | 保证线程同步(锁) |
volatile | 非访问 | 变量 | 保证内存可见性,禁止重排序 |
transient | 非访问 | 变量 | 不参与序列化 |
native | 非访问 | 方法 | jni 调用本地代码 |
strictfp | 非访问 | 类、方法 | 严格浮点数精度 |
default(接口) | 非访问 | 接口方法 | 接口中提供默认实现 |
七、思考题(检验是否真的懂了)
下面这段代码中,有几处编译错误?分别是什么原因?
public abstract final class test { // 1
private static final int a = 10;
public abstract static void method1(); // 2
private abstract void method2(); // 3
public static final void method3() { } // 4
public final transient string name; // 5
}
答案(选中下方空白区域查看):
abstract final不能同时修饰类(冲突)—— ❌abstract static不能同时修饰方法(冲突)—— ❌private abstract不能同时修饰方法(冲突)—— ❌- 没有错误,
static final组合合法。 final transient组合虽然编译可能通过(视 jdk 版本),但语义上transient对final变量意义不大,因为final变量值固定,序列化与否不影响;且name作为final未初始化,编译报错。
以上就是java中修饰符的终极指南:一文掌握12种修饰符的底层原理的详细内容,更多关于java修饰符的资料请关注代码网其它相关文章!
发表评论