Practice: Building Blocks
Five exercises covering what the slides in this module introduced — one-public-class-per-file rules, local variable scope and shadowing, default values for fields vs. locals, the order of field initializers, instance initializer blocks and constructors, and garbage collection eligibility. Try to answer before opening each explanation.
Exercise 1 — How many public classes can one file have?
java// File: Zoo.java
public class Zoo {
public static void main(String[] args) {
System.out.println("Zoo opens");
}
}
public class Animal {
String name;
}Does Zoo.java compile?
Answer
No — this fails to compile.
A single .java source file may declare at most one top-level public
class, and that class's name must match the filename exactly. Here both
Zoo and Animal are declared public in the same file (Zoo.java).
The compiler rejects the second one with something like class Animal is public, should be declared in a file named Animal.java.
Note that the rule is specifically about public — the file could legally
contain any number of non-public top-level classes alongside Zoo:
javapublic class Zoo { }
class Animal { } // fine — package-private, no filename requirement
class Habitat { } // also fineEach top-level class still compiles to its own .class file, but only one
per source file is allowed to be public.
Exercise 2 — Can a nested block redeclare a variable that shadows a field?
javapublic class Counter {
int value = 100;
void update(int value) {
value = value + 1;
{
int value = 5;
System.out.println(value);
}
System.out.println(value);
}
}Does update compile? If not, which line is the problem?
Answer
No — it fails to compile, at int value = 5; inside the nested
block, with a "variable value is already defined in method
update(int)" error.
Two different rules are in play here, and it's important to separate them:
- The parameter
valuelegally shadows the instance fieldvaluefor the entire body ofupdate. That part alone is fine — inside the method, plainvaluerefers to the parameter, andthis.valuewould be required to reach the field. Shadowing a field with a local variable or parameter is explicitly permitted. - A local variable (and a parameter counts as one) cannot be shadowed
by another local variable declared in a nested block within the same
method. The parameter
valueis in scope for the entire method body, including the nested{ }block — so trying to declare a secondvaluethere isn't shadowing, it's a duplicate declaration inside a scope where the name is already active, which the compiler rejects.
This is the asymmetry the exam likes to test: a local/parameter can hide a field of the same name, but it can never be hidden by another local declared anywhere inside its own scope, including deeper nested blocks.
Exercise 3 — Field defaults vs. local variable defaults
javapublic class Inventory {
static int itemCount;
boolean discontinued;
String sku;
double[] weights;
void describe() {
int pendingOrders;
System.out.println(itemCount + " " + discontinued + " " + sku + " " + weights);
System.out.println(pendingOrders);
}
}Does describe() compile? Whether it does or not, what are the default
values of itemCount, discontinued, sku, and weights immediately
after an Inventory object is constructed?
Answer
It does not compile — but not because of the System.out.println
line that reads the four fields; that line is completely fine. The
failure is the next line: pendingOrders is a local variable, and
local variables receive no default value in Java. Reading a local
variable before it's definitely assigned is a compile-time error
("variable pendingOrders might not have been initialized"), regardless of
whether the method ever actually gets called.
If that last println were deleted, the method would compile and print
the fields' defaults, because static and instance fields do get
type-based defaults the moment the object (and, for static, the class)
is initialized:
itemCount(static int) →0discontinued(boolean) →falsesku(String, a reference type) →nullweights(double[], also a reference type — an array is an object) →null, not an empty array and not an array of0.0s. The array reference itself defaults tonulluntil something explicitly assigns it anew double[...].
The general rule: every field (instance or static) gets a type-appropriate
default (0/0L/0.0/false/null); every local variable gets nothing
and must be definitely assigned before it's read, or the compiler stops
you.
Exercise 4 — Field initializers, instance initializer blocks, and constructor chaining
javapublic class Ticket {
private int price = 10;
{
price += 5;
System.out.println("Instance init: " + price);
}
public Ticket() {
System.out.println("No-arg constructor: " + price);
price = 100;
}
public Ticket(int discount) {
this();
System.out.println("Overloaded constructor: " + price);
price -= discount;
}
public static void main(String[] args) {
Ticket t = new Ticket(20);
System.out.println("Final price: " + t.price);
}
}What gets printed, in order?
Answer
plaintextInstance init: 15 No-arg constructor: 15 Overloaded constructor: 100 Final price: 80
The subtlety is that field initializers and instance initializer blocks
run exactly once per object, tied to the constructor invocation that
actually calls super() (implicitly or explicitly) — not once per
constructor in a this(...) chain.
Walking through new Ticket(20):
priceis first zeroed to its default (0), then the JVM begins runningTicket(int discount).- Its first statement is
this();, which delegates toTicket()before running any ofTicket(int discount)'s own field initializers — because delegating constructors don't repeat that step. Ticket()'s first (implicit) statement issuper(), callingObject(). Immediately after that returns, Java runs the field initializers and instance initializer blocks for this object, in source order — this is the one and only time they run:price = 10(field initializer), then the instance init block executesprice += 5→15and printsInstance init: 15.- Only now does the body of
Ticket()run: printsNo-arg constructor: 15, then setsprice = 100. - Control returns to the point right after
this();insideTicket(int discount). Its field initializers do not run again — they already ran in step 3. The rest of its body executes: printsOverloaded constructor: 100, thenprice -= 20→80. - Back in
main,t.priceis80.
The trap: it's tempting to think the field initializer and instance
initializer block run once for each constructor that "participates,"
but they run once total, at the point where the this(...)/super(...)
chain finally reaches the constructor whose first statement is (or
implicitly is) super(...).
Exercise 5 — Reference reassignment and a false "island of isolation"
javaclass Box {
Box link;
}
public class Warehouse {
public static void main(String[] args) {
Box a = new Box(); // Box #1
Box b = new Box(); // Box #2
Box c = new Box(); // Box #3
a.link = b; // #1.link -> #2
b.link = a; // #2.link -> #1
c = a; // c now points to #1
a = null;
// <-- HERE
}
}At the point marked HERE, which Box object(s), if any, are eligible
for garbage collection?
Answer
Only Box #3. Boxes #1 and #2 are still fully reachable and are not eligible.
Trace the references step by step:
- After the first three lines:
a→ #1,b→ #2,c→ #3 (each object has exactly one strong reference, from its own local variable). a.link = b;andb.link = a;make #1 and #2 point at each other — but both are also still directly referenced by the live localsaandb. Mutual references alone don't matter; what matters is whether the group is reachable from anything still live.c = a;repointscfrom #3 to #1. At this instant, #3 loses its only reference (nothing else ever pointed to it) — it becomes eligible for GC right here.a = null;clears theavariable. #1 loses that particular reference, but it is still reachable two other ways: directly throughc(which now points to #1), and indirectly throughb.link(b→ #2 →#2.link→ #1). #2 is still reachable directly throughb.
So at HERE: #3 is eligible for GC (zero references, since the line
that stole its last reference already executed). #1 and #2 look like a
classic mutually-referencing "island of isolation," but that pattern only
applies when no live variable reaches any member of the cycle — here
both c and b still reach into the pair from outside, so neither is
eligible. This is the pattern the exam exploits: don't assume a cycle of
references is automatically garbage; check whether any still-live
variable can reach a path into it.