62. Java 类和对象 - 访问修饰符

86 阅读6分钟

62. Java 类和对象 - 访问修饰符

在 Java 中,访问修饰符用于控制类及其成员(字段、方法、内部类等)的可见性和可访问性。合理地设置访问权限不仅能保护内部实现细节,还能降低类之间的耦合度,从而提高代码的安全性和可维护性。


1. 顶层(类级别)的访问修饰符

对于顶层类(非内部类),Java 只允许使用两种访问修饰符:

  • public

    • 表示类对“全世界”都可见。任何包中的任意类都能访问该类。

    • 示例:

      public class MyClass {
          // 类内容
      }
      
  • 包级私有(package-private)

    (不写修饰符)

    • 如果不加任何修饰符,则该类只在同一包内可见,包外的类无法访问。

    • 示例:

      class MyClass {
          // 仅同包内可见
      }
      

注意:顶层类只能使用 public 或包级私有两种修饰符,不能使用 privateprotected


2. 成员级别的访问修饰符

当修饰符应用于类的成员时(如字段、方法、内部类),可以使用以下四种访问修饰符:

2.1 public

  • 特性: 成员对所有类都可见,无论是在同一包还是其他包中。

  • 示例:

    public class Person {
        public String name;
        public void sayHello() {
            System.out.println("Hello, " + name);
        }
    }
    

2.2 private

  • 特性: 成员只能在所属的类内部访问,外部类(包括子类)都无法直接访问。

  • 用途: 用于隐藏类的内部实现细节,实现封装。

  • 示例:

    public class Person {
        private int age;  // 只能在 Person 类内部访问
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    

2.3 protected

  • 特性: 成员在同一包内可见,并且在其他包中的子类中也可访问。

  • 示例:

    public class Person {
        protected String address;  // 同包或子类中均可访问
    }
    

2.4 包级私有(默认,无修饰符)

  • 特性: 成员仅在当前包内可见,包外的类(包括子类)无法访问。

  • 示例:

    public class Person {
        int score;  // 包级私有,仅限同包使用
    }
    
访问修饰符总结表
修饰符本类(class)同包(package)同包子类不同包子类(subclass)任何类(world)
public访问访问访问访问访问
protected访问访问访问访问不访问
默认(无修饰符)访问访问访问不访问不访问
private访问不访问不访问不访问不访问

3. 访问修饰符的影响与设计建议

3.1 访问修饰符的影响

  • 对外部类使用: 当使用 JDK 或第三方库的类时,访问修饰符决定了你是否可以调用它的方法或访问它的字段。
  • 内部设计: 在自己编写类时,合理设置字段和方法的访问级别,可以防止外部类过度依赖内部实现,从而提高灵活性并降低耦合度。

3.2 设计建议

  1. 最小可见原则
    • 除非确有必要公开,否则尽量将成员设为私有或包级私有,保持最小的可见范围。这可以避免不必要的耦合和误用。
    • 实践:通常将字段设为 private,并通过 publicprotected 方法(如 getter/setter)对外提供访问接口。
  2. 谨慎使用 public 字段
    • 非常量的 public 字段会使其成为公共 API 的一部分,后续修改将受到限制,因此应尽量避免。
  3. 封装为核心
    • 内部实现细节应尽可能隐藏,只暴露外部需要的部分,既能保护数据,也能让后续重构更灵活。

4. 示例讲解

下面以一个简单的 Person 类为例,展示如何设置不同的访问修饰符:

public class Person {
    // 私有字段,只能本类访问
    private String name;
    private int age;

    // 受保护字段:同包或子类访问
    protected String address;

    // 包级私有构造函数:仅在本包内可使用
    Person(String name) {
        this.name = name;
    }

    // 公共构造函数:可以从任意地方调用
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 公共 getter/setter 方法,提供对私有字段的访问
    public String getName() {
        return name;
    }
    public void setName(String newName) {
        this.name = newName;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int newAge) {
        this.age = newAge;
    }
}

在这个示例中:

  • nameage 字段被声明为 private,只有 Person 类内部可以直接访问。
  • address 字段使用 protected,允许同包或子类访问。
  • 构造函数中,一个是 public,一个是包级私有,提供不同的对象构造方式。
  • 对外仅通过 gettersetter 方法来访问和修改 nameage,符合封装原则。

示例 1:同一包内的调用

在同一包下的调用示例(文件:TestAccess.java):

// 文件:TestAccess.java
package com.example;

public class TestAccess {
    public static void main(String[] args) {
        Person p = new Person("Alice", 30);
        
        // 通过公共方法访问和修改私有字段(间接访问)
        p.setName("Alice");
        p.setAge(30);
        System.out.println("Name: " + p.getName());
        System.out.println("Age: " + p.getAge());
        
        // 同一包内,受保护字段和默认字段都可以直接访问
        p.address = "123 Main Street";
        p.phone = "555-1234";
        
        // 直接访问私有字段会编译错误(下面的代码注释掉)
        // p.name = "Bob";  // 错误:name 是 private
        
        p.displayInfo();
    }
}

运行结果将会正确输出姓名、年龄、地址和电话等信息,因为在同一包内可以直接访问受保护和默认的成员。


示例 2:跨包(子类)中的调用

假设我们在另一个包 com.other 下创建一个子类 SubPerson,继承自 com.example.Person

// 文件:SubPerson.java
package com.other;

import com.example.Person;

public class SubPerson extends Person {
    public SubPerson(String name, int age) {
        super(name, age);
    }
    
    // 子类可以访问受保护字段,但不能访问包级私有字段
    public void updateAddress(String newAddress) {
        // 受保护字段在子类中可直接访问
        this.address = newAddress;
    }
    
    public void tryAccessPhone() {
        // 下面代码会编译错误,因为 phone 为包级私有(默认),在不同包中不可见
        // this.phone = "555-0000"; // 编译错误
    }
}

在跨包调用中,子类可以访问父类中受保护(protected)的成员,但不能访问默认(包级私有)的成员。以下是跨包调用的测试示例(文件:TestSub.java):

// 文件:TestSub.java
package com.other;

public class TestSub {
    public static void main(String[] args) {
        SubPerson sp = new SubPerson("Bob", 25);
        sp.updateAddress("456 Other Street");
        
        // 通过公共方法仍然可以获取私有数据
        System.out.println("Name: " + sp.getName() + ", Age: " + sp.getAge());
        
        // 无法直接访问 phone,因为 phone 是包级私有,不对 com.other 包中的类开放
        // sp.phone = "555-0000"; // 编译错误
        
        sp.displayInfo();
    }
}

通过这个示例,我们可以看到:

  • public 成员:无论在哪个包中都可以访问。
  • protected 成员:在同一包和跨包的子类中可以访问。
  • 默认(包级私有) 成员:只能在同一包内访问,跨包的子类无法直接访问。
  • private 成员:只能在类内部访问,其他任何地方都无法直接访问。

小结

  1. 同一包内:所有 publicprotected 和默认(无修饰符)的成员都可以直接访问;private 成员只能通过公共方法访问。
  2. 跨包中的子类:子类可以直接访问父类中受保护(protected)的成员,但不能访问默认(包级私有)和 private 成员。
  3. 设计建议:遵循最小可见原则,默认使用 private;仅对外暴露必要的 publicprotected 接口,确保封装性和代码安全性。