Skip to content
Part IA Michaelmas Term

2024 Paper 1 Question 3 — Access Modifiers and Encapsulation

Question

(a) Explain the effect of the access modifiers public, protected, private, or default (no modifier) in Java. [4 marks]

(b) Consider the statement “public state of a Java class should be final and static”

  • (i) Explain why this is considered good programming practice. [4 marks]
  • (ii) Give one example of a member variable that should not be declared public, even if final and static. [3 marks]

(c) Consider a language identical to Java except that all class member variables are private. Class methods can still be public, protected, private, or default. The rationale for this change is that access can be provided via methods and so this simplifies the language. Compare and contrast this approach with the Java approach. [4 marks]

(d) The Python programming language does not have explicit private access modifiers in its classes. Instead all variables and methods are public and a convention is used whereby names prefixed with an underscore are to be considered hidden despite there being no enforcement of this by the compiler. Compare this to Java’s explicit access modifier approach. [5 marks]


Worked Solution

(a) Access modifiers — 4 marks

Model answer:

ModifierClassPackageSubclassWorld
publicYesYesYesYes
protectedYesYesYesNo
default (no modifier)YesYesNoNo
privateYesNoNoNo

public — Accessible from everywhere. Any class in any package can see and use this member. Used for the public API of a class — the methods and constants that external code is meant to use.

protected — Accessible within the same package AND by subclasses (even in different packages). Subclasses can access protected members of their parent class through inheritance, but only through a reference of the subclass type — not through a reference of the superclass type from an unrelated object.

Default (package-private) — No modifier keyword. Accessible only within the same package. Not visible to subclasses in different packages. This is the “package is the unit of encapsulation” approach — code you write together goes in a package and shares internal state.

private — Accessible only within the same class. Not even subclasses can see private members. This is the strongest form of encapsulation — implementation details are hidden from everything outside the class.

Why 4 marks: Give a clear table or explanation for all four levels with the visibility rules. Mention the crucial subtlety of protected: it adds subclass access to default access; it does not restrict to subclasses only.

(b)(i) “public state should be final and static” — good practice — 4 marks

Model answer: This statement says that if you make a class member public, it should be final (unchangeable) and static (belongs to the class, not instances). This is good practice because:

  1. Public constants are safe to expose — A public static final field is a constant: its value is fixed at compile time or class initialisation, and no code can modify it. There is no risk of external code corrupting the class’s internal state through the constant. Example: Math.PI, Integer.MAX_VALUE.

  2. Mutable public state breaks encapsulation — If a field is public and not final, any code anywhere can modify it at any time. The class loses control over its own invariants. For example, a BankAccount with a public double balance could be set to a negative value, bypassing any validation in withdraw().

  3. Instance-specific public state is suspicious — If a field is public but not static, each instance has its own copy that any code can modify. This means the class has no control over the lifecycle or consistency of its own objects. It violates the fundamental OOP principle that an object manages its own state.

  4. static avoids per-instance waste — Constants like Math.PI do not need a separate copy per instance. Making them static means one copy per class, which is more memory-efficient and signals that the value is a class-level constant, not instance-specific state.

Why 4 marks: Four distinct points covering safety (immutability), encapsulation (no external mutation), instance vs class scoping, and a concrete counterexample.

(b)(ii) Example of a static final field that should NOT be public — 3 marks

Model answer: A mutable object referenced by a public static final field. Even though the reference cannot be reassigned, the object itself can be mutated:

public class Configuration {
    public static final List<String> ALLOWED_ORIGINS = new ArrayList<>();
}

This compiles and ALLOWED_ORIGINS cannot be reassigned, but ANY code can do:

Configuration.ALLOWED_ORIGINS.add("evil-site.com");  // MUTATING the list!
Configuration.ALLOWED_ORIGINS.clear();                // DELETING all origins!

The final only prevents reassigning the reference; it does NOT make the referenced object immutable. An ArrayList is mutable, so this is effectively a public global mutable variable — it has all the encapsulation and thread-safety problems of a public non-final field.

Alternative examples:

  • public static final Date CREATION_DATE = new Date();Date is mutable; anyone can call CREATION_DATE.setTime(0).
  • public static final Map<String, String> SETTINGS = new HashMap<>(); — anyone can add, remove, or modify entries.

Better approach: Make the field private and provide a read-only accessor that returns an unmodifiable view or a defensive copy:

private static final List<String> ALLOWED_ORIGINS = new ArrayList<>();
public static List<String> getAllowedOrigins() {
    return Collections.unmodifiableList(ALLOWED_ORIGINS);
}

Why 3 marks: Name a specific field, explain why final does not make it safe, show how it can be misused, and give the correct alternative.

(c) All-variables-private language vs Java — 4 marks

Model answer:

Similarities/advantages of all-private:

  1. Simpler language — One fewer concept to learn. No need to decide between public fields and getters/setters — fields are always accessed through methods.
  2. Forces encapsulation by default — Beginners cannot accidentally expose mutable state. Every class’s internal representation is hidden, which leads to more maintainable designs.
  3. Consistent API — All access goes through methods, so changing the internal representation (e.g., computing a value instead of storing it) never breaks client code.

Differences/disadvantages of all-private (Java’s advantages):

  1. Constant values become verbose — Java allows public static final int MAX_SIZE = 100; — a simple, readable constant. In the all-private language, you must write private static final int MAX_SIZE = 100; public static int getMaxSize() { return MAX_SIZE; } — boilerplate for a simple use case.
  2. Performance overhead — Direct field access is faster than method calls (though JVM inlining mitigates this). For performance-critical code (e.g., inside tight loops in a game engine or numerics library), direct public final field access can be justified.
  3. Mutable configuration objects are awkward — In Java, a public field on a simple data-transfer object (DTO) or configuration holder can be pragmatic. Forcing all fields through methods adds ceremony without benefit when the class has no invariants to protect.
  4. Loss of fine-grained access control — Java allows a field to be public read but package-private write (via a public getter and a default-access setter). The all-private model would need language features to express this without boilerplate.

Verdict: The all-private model is philosophically purer (everything is encapsulated) but Java’s approach recognises that in practice, some fields (constants, simple data holders) benefit from direct access. The trade-off is between simplicity and pragmatism.

Why 4 marks: Balanced comparison — at least 2 points for the all-private approach and 2 points for Java’s approach, with concrete examples.

(d) Python convention vs Java access modifiers — 5 marks

Model answer:

Python’s approach (convention-based):

  • All members are public by default. There is no language-level enforcement of access control.
  • Convention: names prefixed with a single underscore (_internal) signal “this is an implementation detail — do not rely on it.” This is purely a social contract; the interpreter does not prevent access.
  • Names prefixed with double underscore (__private) trigger name mangling (the name is rewritten to _ClassName__private), which makes accidental access harder but still does not prevent deliberate access.
  • The philosophy: “We are all consenting adults.” The programmer is trusted to respect conventions. If they choose to access _internal details, they accept the risk of breakage.

Java’s approach (enforcement-based):

  • Access modifiers are enforced by the compiler. Attempting to access a private field from another class is a compile error — the program will not even run.
  • The type system guarantees encapsulation: you physically cannot violate it.
  • This provides confidence when refactoring — if you change a private implementation, you know no external code can break.

Comparison:

AspectPythonJava
EnforcementNone (convention)Compiler-enforced
FlexibilityHigh — can access anything if truly neededLow — must go through the API
Trust modelTrust the programmerTrust the compiler
Refactoring safetyLower — _internal users may breakHigher — compiler catches all access
Cultural fitDynamic/scripting — “get things done”Large-team engineering — “prevent mistakes”

When each is appropriate:

  • Python’s model works well for smaller teams, rapid prototyping, and scripting. The overhead of declaring access modifiers would slow down development in these contexts. The culture of “read the underscore as a warning” is well-established in the Python community.
  • Java’s model shines in large codebases with many developers over long periods. The compiler prevents a developer from accidentally depending on implementation details. When refactoring, you can change any private member without worrying about anyone else’s code.

Why 5 marks: Describe both approaches clearly, give a balanced comparison (at least 2-3 dimensions), and discuss the trade-off between trust-based conventions and compiler-enforced guarantees. Show understanding that neither is universally better — they serve different contexts and philosophies.