2021 Paper 1 Question 3 — Emulating Static Fields without Inheritance
Question
A programmer is using a cut-down version of Java that does not support static fields or inheritance (neither extends nor implements). Static methods and static inner classes are still supported.
(a) What are the implications of this for static methods in terms of:
- (i) access to static fields; [1 mark]
- (ii) access modifiers. [1 mark]
(b) Describe how a programmer might use a shared instance of an environment object to emulate static fields. Consider:
- (i) sharing state between all instances of a class; [4 marks]
- (ii) access-modifiers; and [6 marks]
- (iii) initialisation. [4 marks]
(c) What are the drawbacks and benefits of your scheme compared to static fields? [4 marks]
Worked Solution
(a)(i) Access to static fields — 1 mark
Model answer: Static methods cannot access static fields because static fields do not exist in this cut-down language. A static method can only access its own local variables, its method parameters, and any static fields of other classes that happen to be available (but the point is that static fields as a language feature are absent). More fundamentally, there is no shared per-class state that all instances and static methods can reference — the mechanism that normally underpins static int count being shared across all instances is gone.
Reasoning: This is testing whether you understand that static methods and static fields are separate concepts. A static method can exist without static fields (e.g., Math.sqrt() is a static method that only uses its parameters). The implication is that static methods are purely functional — they compute from their arguments, never from shared class state.
(a)(ii) Access modifiers — 1 mark
Model answer: Access modifiers on static methods work exactly as they do on instance methods. The absence of static fields does not affect the visibility rules: public, private, protected, and default access all function normally. A private static method is still only visible within its declaring class; a public static method is still visible everywhere.
However, there is an indirect implication: private static fields are often used to encapsulate state visible only to private static methods (a common pattern for caching or lazy initialisation). Without static fields, this use case disappears — private static methods become utility methods that only depend on their arguments, which is arguably better design anyway.
Reasoning: The question is checking you understand that access modifiers are orthogonal to the static/instance distinction.
(b)(i) Sharing state between all instances — 4 marks
Model answer: Create an environment object (or context object) that holds the shared state. This object is a regular instance of a regular class — it has instance fields that hold what would normally be static fields. All instances of the target class receive a reference to the same environment object (typically through their constructor), and access shared state through that reference.
class Counter {
private final Environment env; // reference to shared environment
Counter(Environment env) {
this.env = env;
}
void increment() {
env.totalCount++; // mutate shared state via env
}
int getCount() {
return env.totalCount; // read shared state via env
}
}
class Environment {
int totalCount = 0; // emulated "static field"
}
// Usage
Environment env = new Environment();
Counter c1 = new Counter(env);
Counter c2 = new Counter(env);
c1.increment(); // both see totalCount = 1
c2.increment(); // both see totalCount = 2
The key insight: instead of every instance magically sharing a static int totalCount, every instance holds a reference to the same Environment object, and reads/writes shared state through it. This is essentially dependency injection — the shared state is made explicit rather than implicit.
Why this is a good answer for 4 marks: It demonstrates understanding that “shared state” is about aliasing (multiple references to the same object), not about language keywords. It shows concrete code. It correctly addresses the requirement that all instances must see the same state without using static fields.
(b)(ii) Access-modifiers — 6 marks
Model answer: The emulated model gives us more fine-grained control over access than standard static fields do. There are several levels to consider:
1. Public shared state (equivalent to public static):
class Environment {
public int totalCount; // any class can read/write
}
All code that has a reference to the Environment can access totalCount directly.
2. Package-private shared state (equivalent to default-access static):
class Environment {
int totalCount; // same package only
}
3. Read-only shared state (no static equivalent in standard Java):
class Environment {
private int totalCount;
public int getTotalCount() { // public getter, no public setter
return totalCount;
}
void increment() { // only Environment's own methods mutate
totalCount++;
}
}
This is more restrictive than public static — callers can read the value but cannot arbitrarily mutate it.
4. Controlled access through specific methods:
By exposing the Environment only through an interface, we can restrict what different parts of the code can do:
interface ReadOnlyCount {
int getTotalCount();
}
interface MutableCount extends ReadOnlyCount {
void increment();
}
class Environment implements MutableCount {
private int totalCount;
public int getTotalCount() { return totalCount; }
public void increment() { totalCount++; }
}
Now, a Counter class receives a MutableCount reference and can increment, but if we pass only a ReadOnlyCount to a reporting class, it cannot modify the count. This is interface-based access control — far more flexible than Java’s public/private binary.
5. Instance-level vs class-level distinction:
Standard private static is visible to all instances of the class. The environment-object approach could restrict the environment to be accessible only from instances that are explicitly granted it (by only passing the reference to specific constructors), giving per-instance access control.
// Only AdminCounter gets a mutable reference
class AdminCounter {
AdminCounter(MutableCount env) { ... }
void reset() { env.reset(); }
}
// Regular Counter only gets read-only
class Counter {
Counter(ReadOnlyCount env) { ... }
}
Why 6 marks: This part expects detailed consideration of the access-modifier spectrum — public, package-private, read-only, interface-based, and instance-level. Show that the emulated model gives more control, not less, than standard static fields.
(b)(iii) Initialisation — 4 marks
Model answer: Initialisation of static fields in standard Java happens once, when the class is loaded, by the JVM. The environment-object approach gives us explicit, controllable initialisation:
1. Constructor initialisation:
Environment env = new Environment();
env.totalCount = 0;
Simple and explicit. The programmer controls exactly when and how the shared state is created.
2. Lazy initialisation (like a lazy Singleton):
class EnvironmentFactory {
private static Environment instance;
public static Environment getEnvironment() {
if (instance == null) {
instance = new Environment();
}
return instance;
}
}
The factory method creates the environment on first use — similar to how static fields are initialised when the class is first loaded.
3. Multiple environments (no static equivalent):
Environment testEnv = new Environment();
Environment prodEnv = new Environment();
Standard static fields are truly global — there is one value for the entire JVM. The environment-object approach lets us create separate environments for different contexts (e.g., testing vs production, or different configurations). This is a major benefit.
4. Dependency injection at application startup:
Environment env = new Environment();
Counter c1 = new Counter(env);
Counter c2 = new Counter(env);
// c1 and c2 share env, but a different set of Counters could share a different env
Initialisation is moved from “magical class-loading time” to “explicit application startup code”. This is more predictable and testable.
Why 4 marks: Cover the explicit constructor approach, the lazy/factory approach, and the benefit of multiple environments. Show that emulated initialisation is more flexible and explicit than JVM-managed static field initialisation.
(c) Drawbacks and benefits — 4 marks
Benefits:
-
Explicit dependencies — The shared state is passed through constructors, visible in the API. Unlike
staticfields (which are invisible global state), any reader can see thatCounterdepends on anEnvironment. -
Testability — You can create a mock or test-specific
Environmentand pass it to the objects under test. Withstaticfields, tests would share the same global state across test cases, causing test interference. -
Multiple configurations — You can have different environments for different subsystems. Standard
staticis JVM-global — one value for everyone. -
Fine-grained access control — Interface-based access (read-only, mutable, admin) gives more control than
public/privatealone.
Drawbacks:
-
Boilerplate — Every class that needs shared state must accept an
Environmentin its constructor and store a reference. Withstatic, you just writestatic int count;— one line, no plumbing. -
Constructor pollution — The
Environmentparameter appears in constructors even when it is an implementation detail. This can clutter the API. (Mitigated by factory methods or dependency injection frameworks.) -
Thread safety becomes the programmer’s problem — Standard
staticfield access is not thread-safe, but thestatickeyword at least signals “shared mutable state”. With the environment pattern, the sharing is implicit — the programmer must remember to makeEnvironmentthread-safe (e.g.,AtomicInteger,synchronizedmethods). -
No class-loading guarantees — The JVM guarantees
staticfields are initialised exactly once, thread-safely, during class loading. The environment pattern must implement its own thread-safe initialisation if lazy creation is needed.
Why 4 marks: Give a balanced assessment — 2 benefits and 2 drawbacks with concrete examples. Frame it as a trade-off: the environment approach is more explicit, flexible, and testable, but more verbose and requires more discipline.