Changing Java Members from Public to Package-Private Breaks External Access
0 reputation · 17 Nov 2023, 05:36 UTC
0 reputation · 17 Nov 2023, 05:36 UTC
Goal: Restrict a Java library's internal classes and members to package-private access so code outside the package cannot accidentally call them.
Constraint: Removing the public modifier is a source and binary compatibility break for any external caller already depending on those members. Package-private access is defined by the Java Language Specification and is stable across Java SE releases, but it does not block reflective access, and strong encapsulation in Java 9 and later modules may add a separate boundary.
Uncertainty: Whether the project should treat this as a major version breaking change, provide deprecated public wrappers, or rely on module exports instead of package-private access.
public to package-private is a binary-incompatible change. Existing compiled callers will throw IllegalAccessError at runtime. Under semantic versioning this mandates incrementing the MAJOR version (e.g., 1.2.3 → 2.0.0).module-info.java, reflective access (including setAccessible(true)) is also blocked. For defense-in-depth, keep internals package-private and do not export the package.jdeps --jdk-internals --recursive --class-path '...' on downstream projects, or use animal-sniffer-maven-plugin / japicmp to compare API signatures between versions. IDE inspections (IntelliJ "Access can be package-private") and Error Prone's PackagePrivateAccess check also flag external usages at build time.The Java Language Specification defines package-private (default) access as visible only within the same package. Removing public changes the class-file access_flags from ACC_PUBLIC to package-private. Any already-compiled caller that references the member will fail with IllegalAccessError when the JVM resolves the symbol at runtime. This is a binary incompatibility, not just a source incompatibility, so consumers must recompile.
If the library has a module-info.java, the package containing the internal classes should be omitted from exports and opens clauses. Strong encapsulation then prevents reflective access even if a caller uses setAccessible(true). Without a module descriptor (automatic module or classpath), package-private alone does not stop reflection. Therefore, combine both: keep the member package-private and ensure the package is not exported.
If you control the library and want to soften the break, provide deprecated public wrappers that delegate to the new package-private implementation:
@Deprecated(forRemoval = true, since = "2.0")
public void oldPublicMethod() {
internalPackagePrivateMethod();
}
This allows existing callers to compile against the new major version while receiving deprecation warnings. Remove the wrappers in the following major release.
IllegalAccessError on first access.japicmp --old old.jar --new new.jar and confirm it reports BINARY_INCOMPATIBLE with visibility-decrease messages for each changed member.-Xep:PackagePrivateAccess:ERROR. It should fail on any remaining references.java --describe-module <module> and verify the package is absent from exports and opens.Is the library already modularized (i.e., does it have a module-info.java)? If yes, the module system can enforce the boundary at runtime; if no, you rely solely on package-private and must accept that reflection can bypass it.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.