MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java编程中修饰符的使用场景

2021-04-281.2k 阅读

Java 修饰符概述

在 Java 编程中,修饰符是一种特殊的关键字,用于对类、方法、变量等元素进行修饰,以改变它们的特性和行为。修饰符可以从多个维度对这些元素进行限定,比如访问权限、是否为静态成员、是否可被继承等。Java 中的修饰符主要分为访问修饰符和非访问修饰符两大类。

访问修饰符

访问修饰符主要用于控制类、方法和变量的访问权限,决定哪些其他类能够访问这些元素。Java 中有四种访问修饰符:publicprotectedprivate 以及默认(即不写任何修饰符)。

public 修饰符

public 修饰符表示被修饰的元素具有最大的访问权限,对所有类都是可见的。无论是在同一个包内还是不同包中的类,只要能够访问到包含该元素的类,就可以访问 public 修饰的元素。

类的 public 修饰: 当一个类被 public 修饰时,它可以被任何其他类访问。例如:

public class PublicClass {
    // 类的成员
}

在其他类中可以这样使用:

public class AnotherClass {
    PublicClass publicClass = new PublicClass();
}

方法和变量的 public 修饰

public class PublicMemberExample {
    public int publicVariable;

    public void publicMethod() {
        System.out.println("This is a public method.");
    }
}

在任何其他类中都可以访问这些 public 成员:

public class AccessPublicMembers {
    public static void main(String[] args) {
        PublicMemberExample example = new PublicMemberExample();
        example.publicVariable = 10;
        example.publicMethod();
    }
}

private 修饰符

private 修饰符表示被修饰的元素只能在定义它的类内部被访问,对其他类完全不可见。这是一种非常严格的访问控制,主要用于隐藏类的实现细节,只暴露必要的接口给外部使用。

方法和变量的 private 修饰

public class PrivateMemberExample {
    private int privateVariable;

    private void privateMethod() {
        System.out.println("This is a private method.");
    }

    // 提供公共方法来访问私有变量和调用私有方法
    public void accessPrivateMembers() {
        privateVariable = 20;
        privateMethod();
    }
}

在外部类中,不能直接访问 PrivateMemberExample 的私有成员:

public class TryAccessPrivateMembers {
    public static void main(String[] args) {
        PrivateMemberExample example = new PrivateMemberExample();
        // 以下代码会报错
        // example.privateVariable = 10;
        // example.privateMethod();
        example.accessPrivateMembers();
    }
}

protected 修饰符

protected 修饰符的访问权限介于 privatepublic 之间。被 protected 修饰的元素可以在同一包内的所有类中访问,同时,如果一个类继承自包含 protected 元素的类,那么在子类中也可以访问这些 protected 元素,即使子类和父类不在同一个包中。

方法和变量的 protected 修饰: 假设我们有两个包 package1package2,在 package1 中有如下类:

package package1;

public class ProtectedExample {
    protected int protectedVariable;

    protected void protectedMethod() {
        System.out.println("This is a protected method.");
    }
}

package1 中的其他类可以访问 protected 成员:

package package1;

public class AccessInSamePackage {
    public static void main(String[] args) {
        ProtectedExample example = new ProtectedExample();
        example.protectedVariable = 30;
        example.protectedMethod();
    }
}

package2 中有一个子类继承自 ProtectedExample

package package2;

import package1.ProtectedExample;

public class Subclass extends ProtectedExample {
    public void accessProtectedMembers() {
        protectedVariable = 40;
        protectedMethod();
    }
}

默认访问修饰符(包访问权限)

当一个类、方法或变量没有显式地使用任何访问修饰符时,它具有默认的访问权限,也称为包访问权限。具有包访问权限的元素只能在同一个包内的类中被访问。

类的默认访问修饰

class DefaultClass {
    // 类的成员
}

在同一个包内的其他类可以访问 DefaultClass

class AnotherDefaultClass {
    DefaultClass defaultClass = new DefaultClass();
}

如果在不同包中尝试访问 DefaultClass,会导致编译错误。

非访问修饰符

除了访问修饰符,Java 还提供了一系列非访问修饰符,用于从其他方面改变类、方法和变量的特性。

static 修饰符

static 修饰符用于创建类级别的成员,即这些成员属于类本身,而不是类的实例。static 成员可以是变量、方法、代码块或内部类。

static 变量static 变量也称为类变量,它在类加载时就会被分配内存,并且无论创建多少个类的实例,static 变量只有一份。例如:

public class StaticVariableExample {
    static int staticVariable;

    public static void main(String[] args) {
        StaticVariableExample instance1 = new StaticVariableExample();
        StaticVariableExample instance2 = new StaticVariableExample();

        instance1.staticVariable = 10;
        System.out.println("Instance 1: " + instance1.staticVariable);
        System.out.println("Instance 2: " + instance2.staticVariable);

        instance2.staticVariable = 20;
        System.out.println("Instance 1: " + instance1.staticVariable);
        System.out.println("Instance 2: " + instance2.staticVariable);
    }
}

在上述代码中,通过不同实例对 staticVariable 的修改会影响到所有实例,因为它们共享同一个 static 变量。

static 方法static 方法也称为类方法,它可以在不创建类的实例的情况下被调用。static 方法只能访问 static 变量和调用 static 方法,不能访问非 static 成员。例如:

public class StaticMethodExample {
    static int staticVariable;

    static void staticMethod() {
        staticVariable = 30;
        System.out.println("Static method: " + staticVariable);
    }

    public static void main(String[] args) {
        StaticMethodExample.staticMethod();
    }
}

static 代码块static 代码块在类加载时执行,并且只执行一次。它通常用于对 static 变量进行初始化。例如:

public class StaticBlockExample {
    static int staticVariable;

    static {
        staticVariable = 40;
        System.out.println("Static block executed.");
    }

    public static void main(String[] args) {
        System.out.println("Static variable: " + staticVariable);
    }
}

final 修饰符

final 修饰符可以用于类、方法和变量,它表示“最终的”、“不可改变的”意思。

final 类: 当一个类被声明为 final 时,它不能被继承。这通常用于一些不希望被扩展的类,比如 java.lang.String 类就是 final 类。

final class FinalClass {
    // 类的成员
}

// 以下代码会报错,因为 FinalClass 不能被继承
// class Subclass extends FinalClass { }

final 方法final 方法不能在子类中被重写。这可以防止子类改变父类中关键方法的行为。例如:

class ParentClass {
    final void finalMethod() {
        System.out.println("This is a final method.");
    }
}

class ChildClass extends ParentClass {
    // 以下代码会报错,不能重写 final 方法
    // @Override
    // void finalMethod() {
    //     System.out.println("Trying to override final method.");
    // }
}

final 变量final 变量一旦被赋值就不能再改变。对于基本数据类型,final 变量的值不能被修改;对于引用数据类型,final 变量不能再指向其他对象,但对象内部的状态是可以改变的。例如:

public class FinalVariableExample {
    public static void main(String[] args) {
        final int finalInt = 10;
        // finalInt = 20; // 这行代码会报错,final 变量不能重新赋值

        final StringBuilder finalStringBuilder = new StringBuilder("Hello");
        finalStringBuilder.append(" World"); // 可以修改对象内部状态
        System.out.println(finalStringBuilder);
    }
}

abstract 修饰符

abstract 修饰符用于定义抽象类和抽象方法。

抽象类: 抽象类是一种不能被实例化的类,它主要用于为其他类提供一个通用的框架。抽象类可以包含抽象方法和具体方法。例如:

abstract class AbstractClass {
    // 抽象方法,没有方法体
    abstract void abstractMethod();

    // 具体方法
    void concreteMethod() {
        System.out.println("This is a concrete method.");
    }
}

因为抽象类不能被实例化,所以必须有子类继承自抽象类,并实现其抽象方法:

class Subclass extends AbstractClass {
    @Override
    void abstractMethod() {
        System.out.println("Implementing abstract method.");
    }
}

抽象方法: 抽象方法只有方法声明,没有方法体,必须在子类中被实现。抽象方法所在的类必须被声明为抽象类。例如:

abstract class AbstractMethodExample {
    abstract void abstractMethod();
}

class ImplementingClass extends AbstractMethodExample {
    @Override
    void abstractMethod() {
        System.out.println("Implemented abstract method.");
    }
}

synchronized 修饰符

synchronized 修饰符用于实现线程安全,确保在同一时间只有一个线程能够访问被修饰的代码块或方法。

synchronized 方法: 当一个方法被声明为 synchronized 时,每次只能有一个线程调用该方法。例如:

public class SynchronizedMethodExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
        System.out.println("Incremented count: " + count);
    }
}

假设有多个线程同时调用 increment 方法,synchronized 关键字会保证它们依次执行,避免数据竞争。

synchronized 代码块synchronized 代码块可以更细粒度地控制同步。例如:

public class SynchronizedBlockExample {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
            System.out.println("Incremented count: " + count);
        }
    }
}

在上述代码中,synchronized 代码块锁定了 this 对象,只有获取到该对象锁的线程才能执行代码块中的内容。

volatile 修饰符

volatile 修饰符用于确保被修饰的变量在多线程环境下的可见性。当一个变量被声明为 volatile 时,每次线程读取该变量的值时,都会从主内存中读取,而不是从线程的本地缓存中读取;当线程对 volatile 变量进行写操作时,会立即将新值刷新到主内存中。

例如,在以下代码中,如果 isRunning 没有被声明为 volatile,线程 t 可能无法及时感知到 isRunning 的变化:

public class VolatileExample {
    volatile boolean isRunning = true;

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();
        Thread t = new Thread(() -> {
            while (example.isRunning) {
                System.out.println("Thread is running...");
            }
            System.out.println("Thread stopped.");
        });
        t.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        example.isRunning = false;
    }
}

transient 修饰符

transient 修饰符用于标记那些在对象序列化时不需要被保存的变量。当一个对象被序列化时,被 transient 修饰的变量不会被写入到序列化流中。

例如,假设我们有一个包含敏感信息的变量,不希望在对象序列化时被保存:

import java.io.Serializable;

public class TransientExample implements Serializable {
    private String normalVariable = "Normal data";
    transient private String sensitiveVariable = "Sensitive data";

    public static void main(String[] args) {
        TransientExample example = new TransientExample();
        // 进行对象序列化和反序列化操作
        // 反序列化后,sensitiveVariable 会为 null
    }
}

native 修饰符

native 修饰符用于声明本地方法,即该方法的实现是由其他语言(如 C、C++)编写的。Java 通过本地方法接口(JNI)来调用本地方法。

例如,假设我们有一个本地方法 nativeMethod

public class NativeExample {
    // 声明本地方法
    native void nativeMethod();

    static {
        System.loadLibrary("NativeExample"); // 加载本地库
    }

    public static void main(String[] args) {
        NativeExample example = new NativeExample();
        example.nativeMethod();
    }
}

然后需要使用 C 或 C++ 编写 nativeMethod 的具体实现,并通过 JNI 与 Java 代码进行交互。

修饰符的组合使用

在实际编程中,常常需要组合使用不同的修饰符来满足复杂的需求。例如,一个类可能同时被 publicfinal 修饰,表示它是一个公开的、不可继承的类:

public final class PublicFinalClass {
    // 类的成员
}

一个方法可能被 privatestaticfinal 修饰,表示它是一个私有的、类级别的、不可重写的方法:

public class MethodModifiersExample {
    private static final void privateStaticFinalMethod() {
        System.out.println("This is a private static final method.");
    }
}

通过合理组合修饰符,可以精确地控制类、方法和变量的行为和访问权限,提高代码的安全性、可维护性和可扩展性。

在多线程编程中,可能会将 synchronizedvolatile 修饰符结合使用,以确保线程安全和变量的可见性。例如:

public class ThreadSafeExample {
    private volatile int sharedVariable;

    public synchronized void increment() {
        sharedVariable++;
    }

    public synchronized int getValue() {
        return sharedVariable;
    }
}

在上述代码中,volatile 保证了 sharedVariable 在多线程环境下的可见性,synchronized 保证了对 sharedVariable 的操作是线程安全的。

修饰符使用的最佳实践

  1. 合理控制访问权限:尽量使用最严格的访问修饰符,只有在必要时才放宽访问权限。例如,将类的成员变量声明为 private,通过 public 方法提供访问接口,这样可以更好地隐藏实现细节,提高代码的安全性和可维护性。
  2. 谨慎使用 static 成员static 成员虽然方便,但过度使用可能会导致代码的可测试性和可维护性下降。尽量将 static 成员用于真正属于类级别的数据和操作,避免在 static 方法中访问非 static 成员。
  3. 理解 final 的影响:在使用 final 修饰符时,要考虑清楚是否真的需要阻止类的继承或方法的重写。对于 final 变量,要明确其不可变性带来的影响,特别是在引用数据类型的情况下。
  4. 正确使用 synchronized 和 volatile:在多线程编程中,要根据具体需求准确选择 synchronizedvolatilesynchronized 用于保证线程安全的同步操作,volatile 主要用于保证变量的可见性。避免过度同步,以提高程序的性能。
  5. 避免滥用修饰符:不要为了使用修饰符而使用,要确保每个修饰符的使用都有明确的目的。过多不必要的修饰符可能会使代码变得复杂,难以理解和维护。

通过遵循这些最佳实践,可以在 Java 编程中更有效地使用修饰符,编写出高质量、健壮的代码。同时,随着对 Java 语言理解的深入,对修饰符的使用也会更加得心应手,能够根据不同的场景灵活运用各种修饰符来实现所需的功能。

在大型项目中,不同模块之间的交互需要严格控制访问权限,合理使用修饰符可以避免模块间的非法访问,提高整个系统的稳定性。例如,在一个企业级应用中,数据访问层的类可能将数据库操作方法声明为 privateprotected,只对外提供 public 的接口方法,以确保数据的安全访问。

在多线程并发场景下,正确使用 synchronizedvolatile 修饰符是保证程序正确性的关键。比如在一个高并发的电商系统中,对于库存数量等共享变量,需要使用 volatile 保证其在多线程环境下的可见性,同时使用 synchronized 方法或代码块来确保对库存数量的增减操作是线程安全的。

修饰符在 Java 编程中起着至关重要的作用,深入理解并正确使用它们是成为一名优秀 Java 开发者的必备技能。无论是小型应用还是大型企业级项目,合理运用修饰符都能够提高代码的质量和可维护性,为项目的成功开发奠定坚实的基础。