Day17 | Java包与访问修饰符详解

67 阅读9分钟

Java的包和访问修饰符是实现封装和模块化的基石。

本文将详细探讨Java包的概念、各种访问修饰符的含义和应用,并追溯从传统的访问控制到现代模块化设计的演进路径。

一、包(Package)

在Java中,包是一系列相关类、接口、枚举和注解的集合,它提供了命名空间管理和访问保护机制。

1、包的用途:

  • 组织代码:把功能相近或相关的代码组织在一起,便于查找和管理。比如,java.util包含了各种工具类,java.io包含了输入输出相关的类。
  • 防止命名冲突:不同包里可以有同名的类。通过使用完全限定名(包名.类名),可以唯一地标识一个类。比如,java.util.Date 和 java.sql.Date。
  • 访问控制:包是默认访问权限的作用域,我们将在后续讨论访问修饰符时详述。

2、声明包:

在Java源文件的开头,使用package关键字声明这个文件中的类所属的包。

如果没有package声明,这个类属于一个未命名的默认包。一般不建议在生产代码中使用默认包,因为使用默认包代码很难组织和重用。

默认包直接在根目录下:

3、包的命名约定

一般情况下使用反向的组织域名作为包名的前缀,来确保全局唯一性。

比如,我公司的域名是snail.lazy.com,那我的项目Java100Day里的工具类可以放在 com.lazy.snail.utils 包中。包名通常全小写。

4、导入包中的类

如果需要使用其他包中的类时,可以使用import语句将其导入,从而在代码中直接使用类名,这样就不需要写完全限定名了。

  • 导入特定类:import java.util.ArrayList;
  • 按需导入(导入整个包中的公共类和接口):import java.util.*; (注意:这并不会导入子包的内容)
  • 静态导入(static import):允许导入类的静态成员(字段和方法),从而可以直接使用静态成员名。

5、包和目录结构

Java包的结构通常直接映射到文件系统的目录结构。

例如,包com.lazy.snail.day17中的Day17Demo文件,他的物理路径通常是是:项目根目录/src/com/lazy/snail/day17/Day17Demo.java。

二、访问修饰符

访问修饰符用于控制类、接口、构造器、字段和方法的可见性(即可访问范围)。Java提供了四个主要的访问修饰符:

1、private

  • 可见范围:仅在声明它的同一个类内部可见。
  • 适用对象:成员变量、成员方法、构造器、内部类。
  • 用途:实现类的封装,隐藏内部实现细节,是限制性最强的修饰符。
package com.lazy.snail.day17;

/**
 * @ClassName Day17Demo
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 9:32
 * @Version 1.0
 */
public class Day17Demo {
    private double balance;

    private void calculateInterest() {

    }

    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
            calculateInterest();
        }
    }
}

balance是private修饰的私有成员变量,calculateInterest是private修饰的私有方法。

在Day17Demo类中,都可以直接访问。

2、default

  • 可见范围:仅在同一个包内的其他类可见。如果没有明确指定访问修饰符,默认就是default。
  • 适用对象:类(顶级类)、接口(顶级接口)、成员变量、成员方法、构造器、内部类。
  • 用途:当希望某些类或成员仅供同一包内的其他相关类使用时,这是一种有用的封装方式。
package com.lazy.snail.day17.graphics;

/**
 * @ClassName Shape
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 9:54
 * @Version 1.0
 */
class Shape {
    int colorCode;
    void draw() {

    }
}

Circle和Shape同属一个包:

package com.lazy.snail.day17.graphics;

/**
 * @ClassName Circle
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 9:54
 * @Version 1.0
 */
public class Circle extends Shape {
    public void render() {
        colorCode = 10;
        draw();
    }
}

Circle类中可以直接访问Shape类中没有修饰符的colorCode变量和draw方法(没有修饰符等价于default修饰)。

如果在com.lazy.snail.day17.graphics2包中定义一个Triangle类,尝试访问Shape。

package com.lazy.snail.day17.graphics2;

import com.lazy.snail.day17.graphics.Shape;


/**
 * @ClassName Triangle
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 10:14
 * @Version 1.0
 */
public class Triangle {
    Shape shape;
}

直接出现了编译错误。

3、protected

  • 可见范围:同一个包内的其他类;不同包中的子类(通过继承关系访问父类的protected成员)。
  • 适用对象:成员变量、成员方法、构造器、内部类。(注意:顶级类和接口不能是protected)
  • 用途:常用于希望基类向其子类暴露某些方法或字段,同时对包外的非子类保持隐藏。
package com.lazy.snail.day17.vehicles;

/**
 * @ClassName Vehicle
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 10:22
 * @Version 1.0
 */
public class Vehicle {
    protected String model;

    protected void startEngine() {
        System.out.println("启动引擎");
    }
}

Truck和Vehicle同属一个包com.lazy.snail.day17.vehicles,Truck可以直接访问Vehicle中protected修饰的model和startEngine。

package com.lazy.snail.day17.cars;

import com.lazy.snail.day17.vehicles.Vehicle;

/**
 * @ClassName Car
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 10:24
 * @Version 1.0
 */
public class Car extends Vehicle {
    public void ignition() {
        model = "奥拓";
        startEngine();
    }
}

Car和Vehicle不属于同一个包,但是Car通过extends关键字继承了Vehicle,Car中同样可以访问Vehicle中protected修饰的model和startEngine。

package com.lazy.snail.day17.cars;

import com.lazy.snail.day17.vehicles.Vehicle;

/**
 * @ClassName Garage
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/6/3 10:24
 * @Version 1.0
 */
public class Garage {
    public void serviceVehicle() {
        Vehicle v = new Vehicle();
        v.model = "五菱宏光";
        v.startEngine();
    }
}

Garage既不与Vehicle同属一个包,又没有继承Vehicle,所以无法访问Vehicle中protected修饰的model和startEngine。

4、public

  • 可见范围:对所有类可见,无论它们在哪个包中。
  • 适用对象:类(顶级类)、接口(顶级接口)、成员变量、成员方法、构造器、内部类。
  • 用途:定义应用程序的公共API,是限制性最弱的修饰符。

5、四个修饰符对比小结

修饰符同类内部同包不同包子类不同包非子类
private
default
protected
public

三、传统的封装

在Java 9之前,包和访问修饰符是Java实现封装和访问控制的主要手段。

通过把类的字段设为private,并提供public或protected的getter/setter方法,可以控制对内部状态的访问和修改。

public类和public/protected成员构成了库或框架的公共API。default和private成员则用于隐藏内部实现。 default访问级别在某种程度上模拟了C++中的“友元”概念,允许一个包内的类紧密协作,共享实现细节,而对包外隐藏这些细节。

这些机制在大多数情况下,都非常有效,但是在某些情况下还是有一定的局限性,比如:

如果CLASSPATH中存在多个版本的同一个库,或者存在名称冲突的类,程序可能会加载错误版本的类,导致意外行为或LinkageError。

一旦一个类被声明成public,它对所有能访问到这个JAR包的代码都是可见的。

没办法做到“这个public类只对我的另一个特定JAR包可见,而对其他JAR包不可见”。

我们很难真正隐藏那些不应作为公共API但又必须是public的内部实现类(比如,为了跨包的内部框架使用)。

JAR文件本身不包含关于其依赖项的元数据。我们需要手动管理依赖,或者依赖Maven/Gradle等构建工具,但这主要是在构建时解决,运行时还是可能遇到问题。

这些局限性主要体现在构建大型、模块化系统的时候,做一般开发时,其实比较少遇到。

针对这些局限性,Java官方还是对封装访问机制做了迭代升级,就是下一章讲的模块化。

四、模块化

Java 9引入了Java平台模块系统,就是为了解决第三章描述的局限性,提供更强的封装和更可靠的配置。

1、什么是模块?

模块是一个命名的、自描述的代码和数据集合。

它通常包含一组相关的包和一个模块描述符(module-info.java)文件。

模块描述符定义了模块的名称、它依赖的其他模块、它向其他模块导出的包等。

2、模块的核心概念和关键字

module-info.java:位于模块根目录下的特殊文件,用于声明模块。

/**
 * Defines the JDBC API.
 *
 * @uses java.sql.Driver
 *
 * @moduleGraph
 * @since 9
 */
module java.sql {
    requires transitive java.logging;
    requires transitive java.transaction.xa;
    requires transitive java.xml;

    exports java.sql;
    exports javax.sql;

    uses java.sql.Driver;
}

requires <module.name>;:声明这个模块依赖于另一个模块。

这意味着本模块可以访问目标模块导出的所有public类型。

requires transitive <module.name>;:传递性依赖。如果模块A requires transitive B,模块B requires C,那么依赖模块A的模块也会隐式地依赖模块B和C(如果B也传递了C的依赖)。

exports <package.name>;:把指定包中的所有public类型(及其public和protected成员)暴露给所有依赖本模块的其他模块。这是模块定义其公共API的主要方式。

uses <service.interface.name>;:声明本模块使用一个服务接口。

3、模块怎么改变访问控制

即使一个类是public的,如果它所在的包没有被其模块exports,那么其他模块也没办法访问这个类。

这是模块系统提供的核心优势。一个模块可以拥有很多public的类供模块内部使用,但只选择性地exports一小部分作为其稳定的公共API。

模块之间形成了边界。一个模块默认不能读取另一个模块的内容,除非存在明确的requires关系,并且目标模块exports了相应的包。

在模块化世界中,一个类型是public仅仅意味着它在包内是公共的。要使其在模块外可见,其所在的包必须被模块导出,并且调用方模块必须依赖当前模块。

4、模块化有什么好处

模块显式声明依赖,JVM可以在启动时校验依赖关系是否存在。

模块内部的非导出包得到了真正的隐藏,即使其中的类是public的。这使得库开发者可以更自由地组织内部结构,而不用担心内部实现被外部误用。

JDK本身也被模块化了,允许开发者创建只包含应用程序所需模块的自定义JRE,减小部署体积。

结语

理解包的组织作用和四种访问修饰符 (private, default, protected, public) 的精确含义是Java开发的基础。

掌握Java平台模块系统,特别是requires和exports等概念,是在现代Java开发中构建清晰、可靠、可伸缩应用程序的关键。

下一篇预告

Day18 | 深入理解Object类

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

更多文章请关注我的公众号《懒惰蜗牛工坊》