背景

本文是《Java 后端从小白到大神》修仙系列第七篇,正式进入Java后端世界,本篇文章主要聊Java基础中的修饰符。修饰符是Java语言中用于控制类、方法、变量行为和访问权限的关键字,掌握它们对于编写高质量的Java代码至关重要。若想详细学习请点击首篇博文,我们开始吧。

文章概览

  1. 访问控制修饰符
    • public 修饰符
    • protected 修饰符
    • 默认(包私有)修饰符
    • private 修饰符
  2. 非访问控制修饰符
    • static 修饰符
    • final 修饰符
    • abstract 修饰符
    • synchronized 修饰符
    • transient 修饰符
    • volatile 修饰符
  3. 修饰符的组合使用
  4. 最佳实践和常见错误

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. 修饰符的组合使用

修饰符可以组合使用,但需要遵循一定的规则:

  1. 访问控制修饰符只能使用一个:public、protected、默认、private 中选择一个
  2. 某些修饰符不能组合
    • 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 最佳实践

  1. 访问控制

    • 遵循最小权限原则:优先使用private,仅在必要时使用更宽松的修饰符
    • 类的成员变量通常设置为private,通过getter/setter方法访问
    • 公共API使用public,内部实现使用private或default
  2. static使用

    • 静态变量用于共享数据
    • 静态方法用于工具方法,不依赖于实例状态
    • 静态代码块用于类加载时的初始化
  3. final使用

    • 常量使用final static修饰
    • 不需要被继承的类使用final修饰
    • 不需要被重写的方法使用final修饰
  4. 线程安全

    • 多线程环境中共享变量使用volatile
    • 临界区代码使用synchronized
    • 优先使用并发集合和原子类
  5. 序列化

    • 敏感信息使用transient修饰
    • 不需要序列化的临时数据使用transient修饰

4.2 常见错误

  1. 访问控制错误

    • 将变量声明为public,直接暴露给外部
    • 使用private修饰需要被子类访问的成员
  2. static使用错误

    • 在静态方法中访问实例变量
    • 过度使用static,导致代码难以测试和维护
  3. final使用错误

    • 尝试修改final变量
    • 继承final类
    • 重写final方法
  4. 线程安全错误

    • 多线程环境中共享变量未使用volatile
    • 过度使用synchronized,导致性能问题
    • 错误的锁对象选择
  5. 序列化错误

    • 忘记使用transient修饰敏感信息
    • 序列化包含不可序列化字段的对象

总结

类型 关键字 核心作用 最佳实践
访问控制符 public 最大化开放访问权限 用于公共API和需要被广泛访问的类
protected 限制为子类或同包访问 用于需要被子类访问的成员
默认 同包内可见 用于包内共享的实现细节
private 仅本类可见 用于类的内部实现,通过方法控制访问
非访问控制符 static 类级别共享资源 用于共享数据和工具方法
final 定义不可变性(类、方法、变量) 用于常量、不需要被继承的类和不需要被重写的方法
abstract 定义抽象行为(需子类实现) 用于定义接口和抽象基类
synchronized 线程同步控制 用于临界区代码,确保线程安全
transient 序列化时跳过字段 用于临时数据和敏感信息
volatile 多线程变量可见性 用于多线程环境中共享的标志变量

选择原则

  • 数据安全 → 优先用 private,通过方法控制访问
  • 共享资源 → 用 static
  • 线程安全 → synchronizedvolatile
  • 序列化优化 → transient 忽略非必要字段
  • 代码设计 → 合理使用 finalabstract 提高代码质量

通过合理使用修饰符,我们可以编写更加安全、高效、可维护的Java代码。修饰符是Java语言的重要特性,掌握它们对于成为一名优秀的Java开发者至关重要。