背景
本文是《Java 后端从小白到大神》修仙系列第七篇,正式进入Java后端世界,本篇文章主要聊Java基础中的修饰符。修饰符是Java语言中用于控制类、方法、变量行为和访问权限的关键字,掌握它们对于编写高质量的Java代码至关重要。若想详细学习请点击首篇博文,我们开始吧。
文章概览
- 访问控制修饰符
- public 修饰符
- protected 修饰符
- 默认(包私有)修饰符
- private 修饰符
- 非访问控制修饰符
- static 修饰符
- final 修饰符
- abstract 修饰符
- synchronized 修饰符
- transient 修饰符
- volatile 修饰符
- 修饰符的组合使用
- 最佳实践和常见错误
1. 访问控制修饰符
访问控制修饰符用于控制类、方法、变量的可见性范围,共 4 种(从宽到严):
| 修饰符 |
作用范围 |
适用位置 |
public |
所有类均可访问 |
类、方法、变量、构造器 |
protected |
同包类 + 不同包的子类可访问 |
方法、变量、构造器 |
| 默认 |
同包类可访问(不写修饰符即默认) |
类、方法、变量、构造器 |
private |
仅本类可访问 |
方法、变量、构造器 |
1.1 public 修饰符
public 是最宽松的访问修饰符,表示任何类都可以访问。
适用场景:
- 类:当需要被其他包的类访问时
- 方法:当需要被其他类调用时,如公共API
- 变量:一般不推荐将变量声明为public,应通过getter/setter方法访问
代码示例:
1
2
3
4
5
6
7
8
9
10
|
// 公共类,可被任何包访问
public class PublicClass {
// 公共方法,可被任何类调用
public void publicMethod() {
System.out.println("This is a public method");
}
// 公共变量(不推荐直接使用)
public String publicVariable = "public";
}
|
1.2 protected 修饰符
protected 同包能用 + 子类也能用,既认包,又认儿子。
适用场景:
- 方法:当需要被子类重写或访问时
- 变量:当需要被子类访问时
生活场景:
像家里钥匙
→ 一个小区(同包)能进
→ 不是一个小区,但是亲儿子,也能进
代码示例:
父类
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.example;
public class ParentClass {
// protected 变量
protected String protectedVar = "protected 变量";
// protected 方法
protected void protectedMethod() {
System.out.println("protected 方法");
}
}
|
同包类(不管是不是子类都能用)
1
2
3
4
5
6
7
8
9
10
|
package com.example;
public class SamePackageTest {
public void test() {
ParentClass p = new ParentClass();
// 同包 → 可以访问 protected
System.out.println(p.protectedVar);
p.protectedMethod();
}
}
|
不同包 子类(可以用)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package com.other;
import com.example.ParentClass;
// 不同包,但继承了父类
public class DifferentPackageChild extends ParentClass {
public void test() {
// 不同包 + 是子类 → 可以访问 protected
System.out.println(protectedVar);
protectedMethod();
}
}
|
1.3 默认修饰符
默认修饰符(不写任何修饰符)表示仅同包类可以访问。
适用场景:
- 类:当仅需要在同包内使用时
- 方法:当仅需要在同包内调用时
- 变量:当仅需要在同包内访问时
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.example;
// 默认修饰符的类,仅同包可访问
class DefaultClass {
// 默认修饰符的变量,仅同包可访问
String defaultVariable = "default";
// 默认修饰符的方法,仅同包可访问
void defaultMethod() {
System.out.println("This is a default method");
}
}
package com.example;
public class SamePackageClass {
public void accessDefault() {
DefaultClass dc = new DefaultClass();
System.out.println(dc.defaultVariable); // 可以访问
dc.defaultMethod(); // 可以访问
}
}
package com.other;
import com.example.DefaultClass;
public class DifferentPackageClass {
public void accessDefault() {
// DefaultClass dc = new DefaultClass(); // 编译错误,无法访问默认修饰符的类
}
}
|
1.4 private 修饰符
private 是最严格的访问修饰符,表示仅本类可以访问。
适用场景:
- 方法:内部辅助方法,仅本类使用
- 变量:类的私有成员,通过getter/setter方法控制访问
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public class PrivateClass {
// 私有变量,仅本类可访问
private String privateVariable = "private";
// 私有方法,仅本类可访问
private void privateMethod() {
System.out.println("This is a private method");
}
// 公共方法,提供对私有变量的访问
public String getPrivateVariable() {
return privateVariable;
}
public void setPrivateVariable(String value) {
this.privateVariable = value;
}
public void callPrivateMethod() {
privateMethod(); // 本类内可以访问
}
}
public class OtherClass {
public void accessPrivate() {
PrivateClass pc = new PrivateClass();
// System.out.println(pc.privateVariable); // 编译错误,无法访问私有变量
// pc.privateMethod(); // 编译错误,无法访问私有方法
// 通过公共方法访问
System.out.println(pc.getPrivateVariable());
pc.setPrivateVariable("new value");
}
}
|
2. 非访问控制修饰符
非访问控制修饰符用于控制类、方法、变量的行为特性,共 6 种:
| 修饰符 |
作用描述 |
适用位置 |
static |
类级别共享(无需实例化) |
方法、变量、代码块、内部类 |
final |
不可变(类不可继承,方法不可覆盖,变量为常量) |
类、方法、变量 |
abstract |
抽象(类不可实例化,方法需子类实现) |
类、方法 |
synchronized |
线程同步(同一时间仅一个线程访问) |
方法、代码块 |
transient |
序列化时忽略该字段 |
变量 |
volatile |
多线程中保证变量可见性(直接读写主存) |
变量 |
2.1 static 修饰符
static 表示类级别的成员,不属于实例,无需创建对象即可访问。
适用场景:
- 变量:需要在多个实例间共享的数据,如计数器、常量等
- 方法:不需要访问实例成员的工具方法
- 代码块:类加载时执行的初始化代码
- 内部类:仅与外部类相关,不依赖于外部类实例的内部类
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
public class StaticExample {
// 静态变量(类变量)
public static int counter = 0;
public static final double PI = 3.14159;
// 静态代码块(类加载时执行)
static {
System.out.println("Static block executed");
counter = 10;
}
// 静态方法
public static void staticMethod() {
System.out.println("Static method called");
System.out.println("Counter: " + counter);
}
// 实例方法
public void instanceMethod() {
System.out.println("Instance method called");
System.out.println("Counter: " + counter); // 可以访问静态变量
}
// 静态内部类
public static class StaticInnerClass {
public void innerMethod() {
System.out.println("Static inner class method");
System.out.println("Counter: " + counter); // 可以访问外部类的静态变量
}
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
// 直接访问静态变量
System.out.println(StaticExample.counter);
System.out.println(StaticExample.PI);
// 直接调用静态方法
StaticExample.staticMethod();
// 创建静态内部类实例
StaticExample.StaticInnerClass inner = new StaticExample.StaticInnerClass();
inner.innerMethod();
// 创建实例
StaticExample instance = new StaticExample();
instance.instanceMethod();
}
}
|
2.2 final 修饰符
final 表示不可变,具体含义取决于修饰的对象:
- 类:不可被继承
- 方法:不可被重写
- 变量:不可被重新赋值(常量)
适用场景:
- 类:当不需要被继承时,如工具类
- 方法:当不需要被重写时,如核心方法
- 变量:当值不需要改变时,如常量
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// final类,不可被继承
public final class FinalClass {
// final变量,不可被重新赋值
public final int FINAL_VARIABLE = 100;
public final String FINAL_STRING;
// 构造器中初始化final变量
public FinalClass(String value) {
this.FINAL_STRING = value;
}
// final方法,不可被重写
public final void finalMethod() {
System.out.println("Final method");
}
}
// 尝试继承final类会编译错误
// public class SubClass extends FinalClass {}
public class OtherClass {
public void testFinal() {
FinalClass fc = new FinalClass("test");
// fc.FINAL_VARIABLE = 200; // 编译错误,不可重新赋值
System.out.println(fc.FINAL_VARIABLE);
System.out.println(fc.FINAL_STRING);
fc.finalMethod();
}
}
|
2.3 abstract 修饰符
abstract 表示抽象,用于定义抽象类和抽象方法:
- 抽象类:不可直接实例化,只能作为父类被继承
- 抽象方法:没有实现体,需要子类实现
适用场景:
- 类:当需要定义一组子类共享的结构,但不提供具体实现时
- 方法:当需要子类必须实现某个方法时
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 抽象类
public abstract class AbstractAnimal {
// 抽象方法,没有实现体
public abstract void eat();
// 普通方法,有实现体
public void sleep() {
System.out.println("Animal is sleeping");
}
}
// 具体子类,必须实现抽象方法
public class Dog extends AbstractAnimal {
@Override
public void eat() {
System.out.println("Dog is eating bones");
}
}
public class Cat extends AbstractAnimal {
@Override
public void eat() {
System.out.println("Cat is eating fish");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
// 不能直接实例化抽象类
// AbstractAnimal animal = new AbstractAnimal(); // 编译错误
// 可以创建子类实例
AbstractAnimal dog = new Dog();
dog.eat(); // 调用Dog的实现
dog.sleep(); // 调用父类的实现
AbstractAnimal cat = new Cat();
cat.eat(); // 调用Cat的实现
cat.sleep(); // 调用父类的实现
}
}
|
2.4 synchronized 修饰符
synchronized 用于线程同步,确保同一时间只有一个线程可以访问被修饰的代码。
适用场景:
- 方法:当方法需要线程安全时
- 代码块:当只需要同步代码的一部分时
工作原理:
- 修饰方法时,锁是当前对象实例(非静态方法)或类对象(静态方法)
- 修饰代码块时,锁是指定的对象
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public class SynchronizedExample {
private int count = 0;
private static int staticCount = 0;
// 同步实例方法
public synchronized void increment() {
count++;
System.out.println("Instance count: " + count);
}
// 同步静态方法
public static synchronized void incrementStatic() {
staticCount++;
System.out.println("Static count: " + staticCount);
}
// 同步代码块
public void incrementWithBlock() {
synchronized (this) {
count++;
System.out.println("Instance count with block: " + count);
}
}
// 同步代码块(使用类对象作为锁)
public void incrementStaticWithBlock() {
synchronized (SynchronizedExample.class) {
staticCount++;
System.out.println("Static count with block: " + staticCount);
}
}
}
// 多线程测试
public class Main {
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 创建多个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) {
example.increment();
example.incrementStatic();
}
}).start();
}
}
}
|
2.5 transient 修饰符
transient 用于序列化,表示序列化时忽略该字段。
适用场景:
- 变量:当变量不需要或不应该被序列化时,如临时数据、敏感信息等
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import java.io.*;
public class TransientExample implements Serializable {
private String name;
private transient int age; // 序列化时忽略
private transient String password; // 敏感信息,不序列化
public TransientExample(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "TransientExample{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}
// 序列化测试
public class Main {
public static void main(String[] args) throws Exception {
// 创建对象
TransientExample original = new TransientExample("John", 30, "secret123");
System.out.println("Original: " + original);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(original);
oos.close();
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
TransientExample deserialized = (TransientExample) ois.readObject();
ois.close();
System.out.println("Deserialized: " + deserialized);
// 注意:transient字段会被初始化为默认值(int为0,String为null)
}
}
|
2.6 volatile 修饰符
volatile 用于多线程,确保变量的可见性和禁止指令重排序。
适用场景:
工作原理:
- 保证变量的可见性:一个线程对变量的修改会立即被其他线程看到
- 禁止指令重排序:确保变量的操作按照代码顺序执行
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class VolatileExample {
// volatile变量,保证多线程可见性
private volatile boolean flag = false;
private int count = 0;
public void setFlag() {
flag = true;
System.out.println("Flag set to true");
}
public void checkFlag() {
while (!flag) {
// 空循环,等待flag变为true
}
System.out.println("Flag is now true");
}
// 测试volatile的可见性
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
// 线程1:检查flag
new Thread(example::checkFlag).start();
// 线程2:设置flag
try {
Thread.sleep(1000); // 等待线程1启动
} catch (InterruptedException e) {
e.printStackTrace();
}
example.setFlag();
}
}
|
3. 修饰符的组合使用
修饰符可以组合使用,但需要遵循一定的规则:
- 访问控制修饰符只能使用一个:public、protected、默认、private 中选择一个
- 某些修饰符不能组合:
- abstract 和 final:抽象类需要被继承,final类不能被继承
- abstract 和 private:抽象方法需要被子类实现,private方法不能被子类访问
- abstract 和 static:抽象方法需要实例化才能调用,static方法不需要实例化
- abstract 和 synchronized:抽象方法没有实现体,synchronized需要实现体
合法的组合示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// public + static + final
public static final int MAX_VALUE = 100;
// protected + abstract
protected abstract void abstractMethod();
// private + final
private final void finalMethod() {}
// public + synchronized
public synchronized void synchronizedMethod() {}
// static + synchronized
public static synchronized void staticSynchronizedMethod() {}
|
4. 最佳实践和常见错误
4.1 最佳实践
-
访问控制:
- 遵循最小权限原则:优先使用private,仅在必要时使用更宽松的修饰符
- 类的成员变量通常设置为private,通过getter/setter方法访问
- 公共API使用public,内部实现使用private或default
-
static使用:
- 静态变量用于共享数据
- 静态方法用于工具方法,不依赖于实例状态
- 静态代码块用于类加载时的初始化
-
final使用:
- 常量使用final static修饰
- 不需要被继承的类使用final修饰
- 不需要被重写的方法使用final修饰
-
线程安全:
- 多线程环境中共享变量使用volatile
- 临界区代码使用synchronized
- 优先使用并发集合和原子类
-
序列化:
- 敏感信息使用transient修饰
- 不需要序列化的临时数据使用transient修饰
4.2 常见错误
-
访问控制错误:
- 将变量声明为public,直接暴露给外部
- 使用private修饰需要被子类访问的成员
-
static使用错误:
- 在静态方法中访问实例变量
- 过度使用static,导致代码难以测试和维护
-
final使用错误:
- 尝试修改final变量
- 继承final类
- 重写final方法
-
线程安全错误:
- 多线程环境中共享变量未使用volatile
- 过度使用synchronized,导致性能问题
- 错误的锁对象选择
-
序列化错误:
- 忘记使用transient修饰敏感信息
- 序列化包含不可序列化字段的对象
总结
| 类型 |
关键字 |
核心作用 |
最佳实践 |
| 访问控制符 |
public |
最大化开放访问权限 |
用于公共API和需要被广泛访问的类 |
|
protected |
限制为子类或同包访问 |
用于需要被子类访问的成员 |
|
默认 |
同包内可见 |
用于包内共享的实现细节 |
|
private |
仅本类可见 |
用于类的内部实现,通过方法控制访问 |
| 非访问控制符 |
static |
类级别共享资源 |
用于共享数据和工具方法 |
|
final |
定义不可变性(类、方法、变量) |
用于常量、不需要被继承的类和不需要被重写的方法 |
|
abstract |
定义抽象行为(需子类实现) |
用于定义接口和抽象基类 |
|
synchronized |
线程同步控制 |
用于临界区代码,确保线程安全 |
|
transient |
序列化时跳过字段 |
用于临时数据和敏感信息 |
|
volatile |
多线程变量可见性 |
用于多线程环境中共享的标志变量 |
选择原则:
- 数据安全 → 优先用
private,通过方法控制访问
- 共享资源 → 用
static
- 线程安全 →
synchronized 或 volatile
- 序列化优化 →
transient 忽略非必要字段
- 代码设计 → 合理使用
final 和 abstract 提高代码质量
通过合理使用修饰符,我们可以编写更加安全、高效、可维护的Java代码。修饰符是Java语言的重要特性,掌握它们对于成为一名优秀的Java开发者至关重要。