Java,
Week 1
Programming Concepts using Java, Week 1: why languages exist, types, how memory works (stack, heap, garbage collection), abstraction and modularity, and the first look at objects and classes. No Java syntax yet: this week builds the ideas that every Java interview tests.
Chapters
Programming Languages, Abstraction and Styles
Why languages exist, what abstraction buys us, compilers and interpreters, imperative versus declarative style, and a first look at types, ADTs and objects.
1.1What is a programming language?
A language is a medium for communication. A programming language is the medium in which we communicate computational instructions to a machine.
Originally, a language was directly tied to the architecture of the computer. Memory locations store values and registers do the arithmetic. Even adding two numbers takes three separate steps:
- Load a value from memory location M into register R.
- Add the contents of registers R1 and R2 and store the result back in R1.
- Write the value in R1 to memory location M'.
So the lecture's first point: working directly with the machine is tedious and error prone. That is the reason programming languages exist.
1.2Abstraction: what a high-level language gives us
The ideas we already use in computational thinking are abstractions. A good language lets us express them directly, without thinking about registers:
| Idea | Python | Java (preview) |
|---|---|---|
| Named variable | x = 10 | int x = 10; |
| Conditional execution | if x > 5: | if (x > 5) { ... } |
| Iteration | for v in data: | for (int v : data) { ... } |
| Function / procedure, recursion | def f(a): | static int f(int a) { ... } |
| Aggregate data (array, list, dictionary) | [1, 2], {"a": 1} | int[], ArrayList, HashMap |
The language then translates this "high level" code into "low level" machine language. The two translators are the compiler and the interpreter.
1.3Compiler, interpreter and where Java sits (extra)
| Translator | How it works | Example |
|---|---|---|
| Compiler | Translates the whole program first, then you run the result. Errors are reported before running. | C, C++ |
| Interpreter | Reads and executes the program line by line. Errors show when that line is reached. | Python |
| Both | Compile to an intermediate bytecode first, then a virtual machine runs it. | Java |
| Term | What it is | Memory hook |
|---|---|---|
| JVM | Java Virtual Machine: runs bytecode (interprets it and JIT-compiles hot parts). | the engine |
| JRE | JVM + the standard class libraries: enough to run programs. | engine + parts |
| JDK | JRE + development tools such as javac: needed to write and build. | the whole workshop |
.class file runs on every machine that has a JVM. That is "write once, run anywhere". Only the JVM is platform specific.1.4The trade-off: expressiveness for efficiency
A higher level language gives us less control over how our code is mapped to the architecture. We give that control up and get something worth more: fewer errors from a mismatch between what we intended and what we implemented.
- Hand-tuning every machine instruction
- Some raw speed (extra layers, garbage collection, checks)
- Code that states our intent clearly
- Fewer bugs, faster development, portability
- Every language is a compromise. This is the sentence the lecture ends on, and it explains why there are so many languages.
1.5Styles of programming: imperative vs declarative
How to compute. Step-by-step instructions on what is to be done.
What the computation should produce. Often uses induction: express it in terms of smaller computations. Usually no intermediate variables. A combination of small transformations is called functional programming.
Example 1: add the values in a list
Imperative: intermediate values sum and x, and an explicit loop that examines each element.
def sum_list(l):
sum = 0
for x in l:
sum += x
return sum
print(sum_list([1, 2, 3]))6Declarative: describe the answer by induction. No intermediate variables.
def sum_list(l):
if l == []:
return 0
else:
return l[0] + sum_list(l[1:])
print(sum_list([1, 2, 3]))6- Base case: the empty list has sum
0. - Inductive step: add the first element to the sum of the rest of the list.
Example 2: sum of squares of the even numbers up to n
def sum_square_even(n):
sum = 0
for x in range(n + 1):
if x % 2 == 0:
sum += x * x
return sum
print(sum_square_even(10))220def even(x):
return x % 2 == 0
def square(x):
return x * x
def sum_square_even(n):
return sum(map(square, filter(even, range(n + 1))))
print(sum_square_even(10))220Both give 220 (0 + 4 + 16 + 36 + 64 + 100). In the second version each tiny job has a name: even keeps, square transforms, sum combines.
even and square can be used elsewhere).The same program in Java. Each Python function becomes a static method with declared types, and filter and map become a stream pipeline (Java 8+):
import java.util.stream.IntStream;
public class Main {
static boolean even(int x) {
return x % 2 == 0;
}
static int square(int x) {
return x * x;
}
static int sumSquareEven(int n) {
return IntStream.rangeClosed(0, n)
.filter(Main::even)
.map(Main::square)
.sum();
}
public static void main(String[] args) {
System.out.println(sumSquareEven(10));
}
}220IntStream.rangeClosed(0, n)is Python'srange(n + 1): it includesn.Main::evenhands the methodeventofilter, like passing the functionevenin Python.- Every name now has a declared type (
int,boolean): the static typing of Chapter 2.
| Imperative | Declarative / functional | |
|---|---|---|
| Focus | how, step by step | what the result is |
| Variables | intermediate variables that change | usually none |
| Repetition | loops (for, while) | recursion, map, filter |
| Typical languages | C, Java, Python | SQL, Haskell, Java streams |
1.6Names, types and values
- Internally everything is a sequence of bits. There is no difference between data and instructions, let alone between numbers, characters and booleans.
- For a compiler or interpreter, our code is its data: it reads our program as input.
- So we impose a notion of type to create discipline: it says how to read the bits, what values are allowed, and which operations are permitted.
Strict type-checking catches bugs early, like a dimension mismatch in science (you cannot add 5 metres to 3 seconds). There are two kinds of slip:
- Incorrect expression evaluation: an operation applied to the wrong kinds of values.
- Incorrect assignment: the value does not match the variable's type.
print(5 + "3")TypeError: unsupported operand type(s) for +: 'int' and 'str'int x = "hello"; // Java refuses to even compile thiserror: incompatible types: String cannot be converted to intPython found the problem only when the line ran; Java reports it before anything runs. Chapter 2 is entirely about this difference.
1.7Collections, abstract data types and objects (preview)
Collections matter: arrays, lists, dictionaries. An abstract data type (ADT) is a structured collection with a fixed interface, and its implementation is kept separate from that interface.
- A stack is a sequence, but it only allows
pushandpop. - A priority queue allows
insertanddelete-max. It can be implemented with a sorted list, an unsorted list or a heap, and the users cannot tell which.
import heapq
class PQList: # unsorted list inside
def __init__(self):
self.items = []
def insert(self, x):
self.items.append(x)
def delete_max(self):
m = max(self.items)
self.items.remove(m)
return m
class PQHeap: # heap inside
def __init__(self):
self.h = []
def insert(self, x):
heapq.heappush(self.h, -x)
def delete_max(self):
return -heapq.heappop(self.h)
for pq in (PQList(), PQHeap()):
for v in (5, 9, 2):
pq.insert(v)
print(pq.delete_max(), pq.delete_max())9 5
9 5Same interface, same answers, different insides. (The two lines of output are the two implementations.)
| Implementation | insert | delete-max |
|---|---|---|
| sorted list | slow: find the place (up to n steps) | fast: take the last item |
| unsorted list | fast: just append | slow: scan for the max (n steps) |
| heap | about log n steps | about log n steps |
Object-oriented programming, in one line
OOP focuses on data types, and functions are invoked through the object instead of passing the data to the function. Python shows both styles side by side:
a = [3, 1, 2]
r = a.sort() # method of the object: sorts a itself
print(a, r)
b = [3, 1, 2]
c = sorted(b) # function given the data: returns a new list
print(b, c)[1, 2, 3] None
[3, 1, 2] [1, 2, 3]a.sort() changes the list and returns None, so a = a.sort() destroys your list. This exact trap is asked a lot.1.8What is yet to come, and why so many languages
| Concept | Where it is going |
|---|---|
| Object-oriented programming | Classes, objects, subtyping, inheritance (Lectures 5 and 6 of this week, then the rest of the course) |
| Exception handling | Dealing with runtime errors gracefully |
| Concurrency | Several things happening at once |
| Event-driven programming | Programs that react to clicks, messages, timers |
- Java is the illustrative language: imperative, object oriented, and it incorporates almost all of these features.
- Design decisions are discussed wherever relevant, because every language makes some compromises.
- That is why there is a whole "zoo" of languages, and why new ones are still created.
1.9Exam tips and common mistakes
- Imperative = "how, with steps and variables". Declarative = "what, by describing the result". If a question mentions a loop and a running total, it is imperative.
- For a recursive definition always name the base case and the inductive step.
- Learn the trade-off sentence: less control over the architecture, but fewer intent-vs-implementation errors.
- Calling Java "only compiled" or "only interpreted". It is both:
javacthen the JVM. - Thinking a declarative program has no steps. The steps are just hidden inside
map,filterand recursion. - Writing
a = a.sort()and then usinga: it is nowNone. - Mixing up JDK, JRE and JVM.
1.10Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Is Java compiled or interpreted? | Very likely | Both. javac compiles source to bytecode; the JVM interprets it and JIT-compiles the hot parts to machine code. |
| JDK vs JRE vs JVM? | Very likely | JVM runs bytecode; JRE = JVM + libraries (to run); JDK = JRE + tools like javac (to develop). |
| Why is Java platform independent? | Very likely | The compiler emits bytecode, not CPU code, and every platform has its own JVM that understands it. The JVM itself is platform dependent. |
| Imperative vs declarative? Which is Java? | Likely | Imperative (how) vs declarative (what). Java is imperative and object oriented, with functional features (lambdas, streams) since Java 8. |
| Why are there so many programming languages? | Sometimes | Every language trades something for something else (speed, safety, expressiveness, ease), and new needs keep appearing. |
Types
What types are for, dynamic versus static typing, why static analysis helps, type synonyms, and type inference.
2.1The role of types
Everything is stored as bits. A type is what turns a pile of bits into something meaningful. Types do three jobs:
| Job | What it means | Example |
|---|---|---|
| 1. Interpret the bits | View the bit sequence as an integer, a float, a character...; fixes the range of values and the allowed operations. | 01000001 → 65 or 'A' |
| 2. Name and structure ideas | Especially at a higher level: give names to concepts in our program. | Point instead of (float, float) |
| 3. Catch bugs early | Detect incorrect expression evaluation and incorrect assignment. | 5 + "3" |
n = 65
print(format(n, '08b')) # the bits of 65
print(chr(n)) # the same number read as a character01000001
A2.2Dynamic vs static typing
Every variable has a type. The question is how that type is decided.
The type is derived from the current value. The same name can hold an int now and a float later. An uninitialised name has no type at all.
A type is associated with the name in advance. You must declare names and their types (int x), and an incompatible value (x = 7.5) is illegal.
x = 10
print(type(x))
x = 7.5
print(type(x))<class 'int'>
<class 'float'>print(y)NameError: name 'y' is not definedIn Java the same attempt is stopped by the compiler:
int x = 10;
x = 7.5; // not allowederror: incompatible types: possible lossy conversion from double to int| Dynamic (Python) | Static (Java) | |
|---|---|---|
| Type decided | from the current value | from the declaration, before running |
| Can a name change type? | yes | no |
| Type errors found | only when the line runs | at compile time |
| Speed | slower: types are watched while running | faster: checks already done |
| Writing effort | less typing | declarations needed |
2.3Why static typing catches what dynamic typing misses
With dynamic typing it is difficult to catch errors such as typos. This function should return all factors of n, but one name is misspelt:
def factors(n):
factorlist = []
for i in range(1, n + 1):
if n % i == 0:
factorlst = factorlist + [i] # typo here!
return factorlist
print(factors(6))[]No error at all. The typo silently created a new variable factorlst, and the real list stayed empty. The answer should have been [1, 2, 3, 6]. A statically typed language needs every name declared first, so the misspelt name is flagged immediately:
factorlst = factorlist + i; // factorlst was never declarederror: cannot find symbolEmpty user-defined objects
A linked list is a sequence of Node objects, and the empty list is conveniently written as None. Without declaring the type of l, Python cannot say what type it has once l = None:
class Node:
def __init__(self, v):
self.value = v
self.next = None
l = None
print(type(l).__name__)
l = Node(1)
print(type(l).__name__)NoneType
NodeNode l = null; // l is declared as a Node, and is empty for now
l = new Node(1); // still a Node2.4Types as organising concepts
Even simple type synonyms clarify code. A 2D point is a pair of floats and a 3D point is a triple; naming them makes the intent transparent for whoever writes, reads or maintains the code.
Point2D = tuple[float, float]
Point3D = tuple[float, float, float]
def shift(p: Point2D, dx: float) -> Point2D:
return (p[0] + dx, p[1])
print(shift((1.0, 2.0), 0.5))(1.5, 2.0)Java has no typedef; you create a real type instead (a class, or a record in newer Java):
record Point2D(double x, double y) { } // Java 16+The bigger idea is abstract data types and objects. In a banking application Account and Customer are separate types:
| Operation | Applies to |
|---|---|
| deposit, withdraw, transfer | Account (not Customer) |
| update personal details | Customer (not Account) |
customer.deposit(500); // a Customer has no deposit()error: cannot find symbol2.5Static analysis
- Find errors as early as possible: it saves cost and effort.
- In general a compiler cannot check that a program will work correctly. This is the halting problem (Alan Turing): no program can decide, for every program, whether it will stop or what it will finally do.
- But with variable declarations the compiler can detect type errors at compile time. That is static analysis.
- Dynamic typing would catch these only when the code runs, and running is also slower because types are monitored at the same time.
The compiler can also optimise using what static analysis knows:
| Optimisation | Idea |
|---|---|
| Re-order statements | arrange them to optimise reads and writes |
| Reuse computed expressions | if x*y appears twice, compute it once and store it |
cannot find symbol, incompatible types). A run-time error happens while it runs (dividing by zero, a missing file). Static typing moves a whole class of mistakes from the second kind to the first.2.6Type inference
Some languages deduce the types of variables statically, from the context in which they are used. After x = 7, the line y = x + 15 forces y to be an int. If the inferred type is consistent across the whole program, everything is fine.
var x = 7; // Java 10+: inferred as int
var y = x + 15; // inferred as int
y = "hello"; // still an errorerror: incompatible types: String cannot be converted to intvar is not dynamic typing. The compiler works the type out once, at compile time, and then it is fixed forever. It also works only for local variables that have an initial value.2.7Strong vs weak typing (extra)
People mix up two different questions. Static vs dynamic asks when types are checked. Strong vs weak asks how strictly mismatched types are refused.
| Types checked at compile time | Types checked at run time | |
|---|---|---|
| Strict (strong) | Java | Python (5 + "3" is an error) |
| Lenient (weak) | C (many silent conversions) | JavaScript (5 + "3" gives "53") |
"Total: " + 5 is legal in Java because + with a String means concatenation. That is a rule of the language, not a loose type check.2.8Exam tips and common mistakes
- "When is the error found?" Static: before running. Dynamic: when that line runs.
- Static analysis can catch type errors; it can not prove a program correct (halting problem).
- Quote the three uses of types: make sense of bits, organise concepts, help the compiler (bugs and optimisation).
- Saying static typing guarantees a correct program. It only guarantees type consistency.
- Treating
varas dynamic typing. - Thinking Python is "weakly typed" because it is dynamic. It is dynamic and strong.
- Forgetting that an uninitialised Python name has no type: using it is a
NameError.
2.9Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Static vs dynamic typing? Which is Java? | Very likely | Static: types are fixed at declaration and checked at compile time. Dynamic: types come from values and are checked at run time. Java is statically typed. |
| Advantages of static typing? | Very likely | Errors caught before running, better tooling and documentation, and faster code because the compiler can optimise and nothing is type-checked at run time. |
| Strongly vs weakly typed? | Likely | Strong languages refuse mismatched operations instead of silently converting. Python and Java are strong; JavaScript is weak. It is a different axis from static/dynamic. |
What does var do? Is Java dynamic then? | Likely | Local type inference: the compiler deduces the type from the initial value. The type is still static and fixed, so Java is not dynamic. |
| Why can't a compiler catch every bug? | Sometimes | Whether a general program is correct or even halts is undecidable (halting problem). Compilers check types and structure, not full correctness. |
| Compile-time error vs run-time error? | Very likely | Compile-time: the code is rejected before it runs (cannot find symbol). Run-time: it fails while running (ArithmeticException, NullPointerException). |
Memory Management
Scope and lifetime, activation records on the stack, how arguments are passed, the heap, and garbage collection.
3.1Keeping track of variables: scope and lifetime
Variables store intermediate values. Most are local to a function; some are global, outside any function; and some data is created dynamically, like the nodes of a list. Two ideas keep this straight:
When the variable is available for use: can this line use this name?
How long the storage stays allocated: does the memory still exist?
The lecture's example: f loops over x and calls g(x), and g also loops with its own x. The x of f is not in scope inside g, yet its storage is still allocated: it is alive but hidden. This is the "hole in the scope".
def g(m):
for x in range(m):
pass
return m
def f(l):
y = 0
for x in l:
y = y + g(x)
print("f still sees its own x =", x)
return y
print(f([2, 3]))f still sees its own x = 2
f still sees its own x = 3
5class Counter {
int x = 5; // a field: lives as long as the object
void show() {
int x = 10; // a local x hides the field x
System.out.println(x); // 10
System.out.println(this.x); // 5 (the hidden one, reached explicitly)
}
}{ } block. Java does not let a local hide another local (variable x is already defined), but a local may hide a field, as above. There are no true global variables in Java: shared data lives in static fields of a class.3.2The memory stack and activation records
- Each function needs storage for its local variables. When it is called, an activation record is created for it.
- Activation records are stacked: the newest is on top, and it is popped when the function exits.
- The control link points to the start of the previous record (who called me).
- The return value link tells where to store the result.
Read the picture carefully, because it is a favourite for questions. The top record is the newest call, factorial(2). Its control link goes back to the start of the record of factorial(3), and its return value link goes to the empty factorial(n-1) slot of factorial(3), which is where its answer will be written.
def factorial(n, depth=0):
pad = " " * depth
print(pad + "call factorial(" + str(n) + ") stack depth", depth + 1)
if n <= 1:
result = 1
else:
result = n * factorial(n - 1, depth + 1)
print(pad + "return factorial(" + str(n) + ") =", result)
return result
factorial(3)call factorial(3) stack depth 1
call factorial(2) stack depth 2
call factorial(1) stack depth 3
return factorial(1) = 1
return factorial(2) = 2
return factorial(3) = 6| Step | Stack (bottom to top) | What happens |
|---|---|---|
| 1 | f(3) | n = 3; needs factorial(2), so it waits |
| 2 | f(3), f(2) | a new record is pushed on top |
| 3 | f(3), f(2), f(1) | base case: 1 |
| 4 | f(3), f(2) | f(1) popped; its 1 is written to the slot of f(2) |
| 5 | f(3) | f(2) popped after 2 * 1 = 2 |
| 6 | (empty) | f(3) popped after 3 * 2 = 6 |
- Scope of a variable: it is in the activation record at the top of the stack.
- Lifetime: the storage is still allocated on the stack, so it is alive even when buried under newer records.
A finite stack can overflow
A stack has a fixed size. A function that calls itself with no stopping condition keeps pushing records until it runs out:
def forever(n):
return forever(n + 1)
forever(0)RecursionError: maximum recursion depth exceededJava does the same and reports a StackOverflowError. Both are the stack running out, caused by missing or unreachable base cases.
3.3Passing arguments to a function
When a function is called, the arguments are substituted for the formal parameters. The parameters are part of the activation record, filled in at the call, as if there were implicit assignment statements at the start of the function. For f(x, myl) with def f(a, l): that means a = x and l = myl.
Copy the value. Updating the value inside the function has no side effect outside.
The parameter points to the same location as the argument. It can have side effects: it can update the contents.
def f(a, l):
a = a + 1 # changes only the copy
l.append(99) # changes the shared list
l = [0] # only re-points l
print("inside:", a, l)
x = 7
myl = [8, 9, 10]
f(x, myl)
print("outside:", x, myl)inside: 8 [0]
outside: 7 [8, 9, 10, 99]append was seen outside, but l = [0] was not.Java: always pass by value (the classic interview question)
static void change(int a, int[] arr) {
a = 99; // changes only the copy
arr[0] = 99; // changes the shared array
arr = new int[] {0}; // only re-points the local copy of the reference
}
int x = 7;
int[] m = {8, 9, 10};
change(x, m);
// x is still 7; m is now {99, 9, 10}- Java always copies the value of the argument.
- For a primitive (
int,double...) the value is the number itself, so the caller never sees a change. - For an object or array the value that is copied is the reference. Both names then reach the same object, so changing its contents is visible, but re-pointing your copy of the reference is not.
&, C# ref) the parameter is an alias for the caller's variable, so re-pointing it would change the caller. Java never does that. The lecture's "cannot change the reference itself" describes the Java/Python behaviour.3.4The heap
Consider a function that inserts a value into a linked list. The storage for the new node is allocated inside the function, yet the node must persist after the function exits. It cannot live in the activation record, because that is popped. We need a separate area for such dynamically allocated data: the heap.
class Node:
def __init__(self, v, nxt=None):
self.value = v
self.next = nxt
def push_front(head, v):
node = Node(v, head) # created inside the function
return node # ...but it outlives the call
head = None
for v in (9, 7, 3):
head = push_front(head, v)
cur = head
while cur is not None:
print(cur.value, end=" ")
cur = cur.next3 7 9 - The heap is not the "heap" data structure used for priority queues. It is just a memory area.
- Conceptually it is allocated from the opposite end of memory to the stack; the free space is in between.
- Heap storage outlives the activation record. It is reached through some variable that is in scope.
| Stack | Heap | |
|---|---|---|
| Holds | activation records: parameters, local variables, links | dynamically allocated data (objects, arrays, nodes) |
| Freed | automatically when the function exits | when it is unreachable (garbage collector) or freed by hand |
| Runs out as | StackOverflowError (too deep recursion) | OutOfMemoryError (too many live objects) |
| In Java | local variables and references | every object created with new, and arrays |
3.5Managing heap storage: manual vs garbage collection
On the stack, storage is freed when the function exits. On the heap, after we delete a node from a linked list it becomes dead storage: nothing points to it any more. How do we get that memory back?
- The programmer requests and returns storage:
p = malloc(...)andfree(p). - Error prone: forget
freeand you get a memory leak; free too early and you get an invalid access.
- The run-time environment checks and cleans up dead storage.
- Mark all storage reachable from program variables; return all unmarked cells to free space.
- Trade-off: convenience for the programmer vs a performance penalty.
import gc, weakref
class Node:
pass
a = Node()
b = Node()
a.other = b
b.other = a # a and b point at each other (a cycle)
probe = weakref.ref(a) # a probe that does not keep a alive
del a, b # no variable reaches them any more
print(probe() is None) # still in memory?
gc.collect() # a collection pass
print(probe() is None)False
TrueNothing could reach the two nodes after del, yet they were still in memory (False) because they pointed at each other. Reachability from variables, not "is anything pointing at me", is the test, so one collection pass freed them (True).
- Java's collector is generational: new objects start in a young area that is cleaned often, and survivors are moved to an old area cleaned rarely.
- Collection can pause the program briefly: that is the performance penalty from the lecture.
System.gc()is only a request; you cannot force a collection.- Garbage collection does not make leaks impossible: an object that is still reachable but no longer needed (for example kept in a static list that only grows) is never freed.
3.6Where does each thing live? (a Java snapshot)
static void demo() {
int count = 3; // value 3 on the stack
int[] marks = new int[3]; // reference on the stack, array object on the heap
Account a = new Account(); // reference on the stack, Account object on the heap
} // record popped; the objects become garbage
// once nothing reaches them| Thing | Where | Freed when |
|---|---|---|
count | stack (in the record of demo) | demo returns |
marks (the reference) | stack | demo returns |
| the array object | heap | after no variable can reach it, at the garbage collector's pleasure |
3.7Exam tips and common mistakes
- Draw the stack when asked about recursion: one record per call, newest on top, popped on return.
- "Scope" = visible now. "Lifetime" = storage exists. A variable can be alive but out of scope, never the reverse.
- For parameter passing, decide first: is it a number (copied) or an object (reference copied)?
- Control link = who called me. Return value link = where my answer goes.
- Saying Java passes objects "by reference". It passes the reference by value.
- Confusing the heap memory area with the heap data structure.
- Thinking the garbage collector frees an object when its variable goes out of scope. It frees it when it is unreachable.
- Mixing up
StackOverflowError(too deep recursion) andOutOfMemoryError(heap full).
3.8Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Stack vs heap? | Very likely | Stack: per-call activation records (locals, parameters), freed automatically on return. Heap: dynamically allocated objects that outlive the call, reclaimed by the garbage collector. |
| Is Java pass by value or pass by reference? | Very likely | Always by value. For objects, the value copied is the reference, so the callee can change the object's contents but cannot re-point the caller's variable. |
| How does garbage collection work? Can you force it? | Very likely | It marks everything reachable from the program variables and reclaims the rest (modern JVMs are generational). System.gc() is only a hint. |
| Can Java have memory leaks? | Likely | Yes: objects that stay reachable (static collections, caches, listeners never removed) are never collected even though they are no longer needed. |
| StackOverflowError vs OutOfMemoryError? | Very likely | Stack overflow: the call stack is exhausted, usually by runaway recursion. Out of memory: the heap cannot satisfy an allocation. |
| What is scope vs lifetime? Give a case where they differ. | Likely | Scope: where a name is usable. Lifetime: how long its storage exists. A local hidden by an inner variable of the same name (a "hole in the scope") is alive but out of scope. |
| What is an activation record? | Likely | The block of stack storage made for one call: parameters, locals, a control link to the caller's record and a place for the return value. |
Abstraction and Modularity
Stepwise refinement (program and data), components with interfaces and specifications, and the language features that support abstraction.
4.1Stepwise refinement
- Begin with a high-level description of the task.
- Refine the task into subtasks, then elaborate each subtask further.
- Subtasks can be coded by different people.
- Program refinement focuses on the code; the data structures hardly change.
This is the classic top-down method. Each level says what in a little more detail until the last level is real code. Here is level 3 turned into Python:
def first_primes(count): # fill table p with the first `count` primes
p = []
n = 2
while len(p) < count:
if all(n % q != 0 for q in p if q * q <= n):
p.append(n)
n += 1
return p
p = first_primes(1000) # declare table p, then fill it
print(len(p), p[0], p[-1]) # print table p (summary only)1000 2 79191000 primes, the first is 2 and the 1000th is 7919. Notice that the three top-level steps (declare, fill, print) can be written, tested and replaced independently.
4.2Data refinement, and why it cascades
Take a banking application. Typical functions: CreateAccount(), Deposit() / Withdraw() and PrintStatement(). How do we represent each amount? We only need the current balance, so overall an array of balances is enough.
balance = [0, 0] # one number per account
def deposit(acc, amt):
balance[acc] += amt
def withdraw(acc, amt):
balance[acc] -= amt
def print_statement(acc):
print("balance", balance[acc])
deposit(0, 1000)
withdraw(0, 200)
withdraw(0, 300)
print_statement(0)balance 500Now the bank asks for PrintTransactions(). We must record every transaction of each account, so the data representation changes. That is data refinement, and it has a cascading impact: every function that touches accounts must be rewritten.
transactions = [[], []] # a list of amounts per account
def deposit(acc, amt):
transactions[acc].append(+amt) # rewritten
def withdraw(acc, amt):
transactions[acc].append(-amt) # rewritten
def print_statement(acc):
print("balance", sum(transactions[acc])) # rewritten
def print_transactions(acc): # new
for t in transactions[acc]:
print(t)
deposit(0, 1000)
withdraw(0, 200)
withdraw(0, 300)
print_statement(0)
print_transactions(0)balance 500
1000
-200
-300This is the problem that objects solve. If the data and the functions that use it live together in one class, refining the data representation only touches that class. Users of Account call deposit() and never see the list:
class Account:
def __init__(self):
self.transactions = [] # private business of Account
def deposit(self, amt):
self.transactions.append(+amt)
def withdraw(self, amt):
self.transactions.append(-amt)
def balance(self):
return sum(self.transactions)
def print_transactions(self):
for t in self.transactions:
print(t)
a = Account()
a.deposit(1000)
a.withdraw(200)
a.withdraw(300)
print(a.balance())
a.print_transactions()500
1000
-200
-300| Program refinement | Data refinement | |
|---|---|---|
| What changes | the code gets more detailed | the data representation itself |
| Effect on other code | small, local | cascades to every user of the data |
| Example | "fill table p" becomes a loop | balances become transaction lists |
4.3Modular software development
- Use refinement to divide the solution into components.
- Build a prototype of each component to validate the design.
- Improve each component independently, preserving its interface and specification.
| A component is described by | Meaning | For a function |
|---|---|---|
| Interface | what is visible to the other components (typically function calls) | header, argument types, return type |
| Specification | the behaviour of the component as seen through the interface | the intended input-output behaviour |
In Java the word interface is literal: it lists the operations and says nothing about how they are done. The specification travels as a documentation comment.
interface PriorityQueue {
/** Adds x to the queue. */
void insert(int x);
/** Removes and returns the largest value. Must not be called on an empty queue. */
int deleteMax();
}
class ListPQ implements PriorityQueue { /* unsorted list inside */ }
class HeapPQ implements PriorityQueue { /* heap inside */ }- The main challenge is a suitable language to write specifications: it must balance abstraction and detail, and should not become another programming language.
- We cannot algorithmically check that a specification is met. That is the halting problem again.
4.4How a language supports abstraction
| Kind | Mechanism | What it gives | In Java |
|---|---|---|---|
| Control abstraction | functions and procedures | encapsulate a block of code and reuse it in different contexts | methods |
| Data abstraction | abstract data types (ADTs) | a set of values plus the permitted operations; the representation is hidden; interaction only through the public interface | classes with private fields, interfaces |
| Object-oriented programming | ADTs in a hierarchy | implicit reuse of implementations: subtyping and inheritance | extends, implements |
The lecture's example of broken data abstraction: if a stack is implemented as a list, we should not be able to observe or modify the internal elements. Python cannot stop us:
class Stack:
def __init__(self):
self.items = []
def push(self, x):
self.items.append(x)
def pop(self):
return self.items.pop()
s = Stack()
s.push(1)
s.push(2)
s.items[0] = 99 # reaches inside the representation
print(s.pop(), s.pop())2 99A real stack would give 2 1. The outsider rewrote the bottom element through a back door. In Java, private List<Integer> items; closes that door at compile time. Chapter 6 shows why this matters.
4.5Top-down vs bottom-up
| Top-down | Bottom-up | |
|---|---|---|
| Start from | the whole task | simple building blocks |
| Then | refine into subtasks | combine blocks into bigger ones |
| Example here | print first thousand primes | even, square then sum(map(...)) |
4.6Exam tips and common mistakes
- Interface = what is visible. Specification = what it promises. Implementation = how (hidden, replaceable).
- Program refinement changes code; data refinement changes data and cascades.
- When asked "why can't we check the specification automatically?", answer: the halting problem.
- Using "interface" and "implementation" as if they were the same thing.
- Thinking modularity means "many files". It means components that can change independently.
- Forgetting that Python lets outsiders reach inside an object; the abstraction is only a convention there.
4.7Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Abstraction vs encapsulation? | Very likely | Abstraction: show what something does, hide how (a design idea, e.g. an interface). Encapsulation: bundle data with its methods and restrict access (private), the mechanism that protects the hidden part. |
| What is an interface in Java and why use one? | Very likely | A contract listing operations without implementing them. Code depends on the contract, so implementations can be swapped (list-based vs heap-based priority queue) without changing callers. |
| What is loose coupling and high cohesion? | Likely | Cohesion: one component does one related job. Coupling: how much components depend on each other's insides. Data refinement cascading everywhere is a symptom of tight coupling. |
| Top-down vs bottom-up design? | Sometimes | Top-down: break the task into subtasks. Bottom-up: combine simple blocks. Real design mixes both. |
| How do you document the behaviour of a method? | Sometimes | As a specification: a Javadoc comment (what it takes, returns, and any conditions), plus tests. It cannot be proven automatically in general. |
Object-Oriented Programming
What an object is, where OOP came from, and its four distinguishing features: abstraction, subtyping, dynamic lookup and inheritance.
5.1Objects
An object is like an abstract data type: hidden data with a set of public operations. All interaction goes through the operations, which are called messages, methods or member functions.
An object is a uniform way of packaging any mix of data and behaviour. It can hold one integer, such as a counter, or it can be an entire file system or database.
class Counter:
def __init__(self):
self.count = 0
def click(self):
self.count += 1
def value(self):
return self.count
c = Counter()
c.click()
c.click()
print(c.value())2The lecture names four distinguishing features of object-oriented programming. Each gets a section below.
| Feature | One-line meaning |
|---|---|
| Abstraction | public interface, private implementation |
| Subtyping | a hierarchy of types; interfaces are compatible |
| Dynamic lookup | the method that runs is chosen at run time |
| Inheritance | reuse of implementations |
5.2History: Simula and the event queue
Objects were first introduced in Simula, a simulation language from the 1960s. Event-based simulation follows one basic pattern:
- Maintain a queue of events to be simulated.
- Simulate the event at the head of the queue.
- Add all the events it spawns to the queue.
The challenge. A well-typed queue holds values of one type, yet this queue must hold all kinds of events, and we want one generic simulate step without elaborate checking of cases. First the clumsy way:
def simulate(event):
if event == "arrival":
print("customer joins")
elif event == "departure":
print("customer leaves")
elif event == "breakdown":
print("machine stops")
# a new kind of event means editing this function again
for e in ["arrival", "departure", "breakdown", "arrival"]:
simulate(e)customer joins
customer leaves
machine stops
customer joinsAnd the object way: every kind of event is its own type with its own simulate:
class Event:
def simulate(self):
raise NotImplementedError
class Arrival(Event):
def simulate(self):
print("customer joins")
class Departure(Event):
def simulate(self):
print("customer leaves")
class Breakdown(Event):
def simulate(self):
print("machine stops")
queue = [Arrival(), Departure(), Breakdown(), Arrival()]
for e in queue:
e.simulate() # one generic call, no case checkscustomer joins
customer leaves
machine stops
customer joinsMaintenance event you write one new class. Nothing that already works is edited. This "open for extension, closed for modification" idea is behind a lot of interview design questions.5.3Feature 1: Abstraction
- Objects are similar to abstract data types: public interface, private implementation.
- Changing the implementation should not affect interactions with the object.
- It is a data-centric view: focus on what data we need to maintain and manipulate.
- Recall stepwise refinement: it could affect code and data. Tying methods to data makes that easier to coordinate, because refining the data naturally goes with updating the methods that use it.
public class Counter {
private int count; // hidden
public void click() { count++; } // public operations
public int value() { return count; }
}5.4Feature 2: Subtyping
A well-typed queue holds values of a fixed type, yet the simulation queue holds many kinds. The way out is to arrange types in a hierarchy. A subtype is a specialisation of a type.
If A is a subtype of B, then wherever an object of type B is needed, an object of type A can be used. Every A is also a B. Think of sets: if , every is also in .
- If
f()is a method inBandAis a subtype ofB, then every object ofAalso supportsf(). - The implementation of
f()can be different inA.
Event e = new Arrival(); // Arrival is a subtype of Event, so this is allowed
Event[] queue = { new Arrival(), new Departure(), new Breakdown() };B should still behave correctly when it is given an A. If a subtype breaks what callers expect, it is a bad subtype even when it compiles.5.5Feature 3: Dynamic lookup
- Whether a method can be called on an object is a static property: type-checking decides it.
- How the method acts is a dynamic property of how the object is implemented.
- In a graphics program, different kinds of objects are rendered with the same operation; each object knows how to render itself.
class Shape:
def draw(self):
print("some shape")
class Circle(Shape):
def draw(self):
print("circle")
class Square(Shape):
def draw(self):
print("square")
for s in [Circle(), Square(), Shape()]:
s.draw()circle
square
some shapeDifferent from overloading. The operation + is addition for int and for float: the internal implementation differs, but the choice is made from the static types. With dynamic lookup a variable v of static type B may refer to an object of subtype A, and the implementation that runs depends on the runtime type A.
static void show(Shape s) { System.out.println("shape"); }
static void show(Circle c) { System.out.println("circle"); }
Shape s = new Circle(); // static type Shape, runtime type Circle
show(s); // prints "shape" : overloads are picked by the STATIC type
s.draw(); // Circle's draw() : overriding is picked by the RUNTIME type| Overloading | Overriding (dynamic lookup) | |
|---|---|---|
| Same name, ... | different parameter lists | same signature in a subclass |
| Chosen | at compile time, by static types | at run time, by the object's real type |
| Also called | static / compile-time polymorphism | dynamic / run-time polymorphism |
5.6Feature 4: Inheritance
Inheritance is re-use of implementations. Example: different types of employees. Employee objects store basic personal data and the date of joining. Manager objects retain all of that and add more: a date of promotion and a seniority in the current role.
class Employee:
def __init__(self, name, joined):
self.name = name
self.joined = joined
def describe(self):
return self.name + " joined " + str(self.joined)
class Manager(Employee):
def __init__(self, name, joined, promoted):
super().__init__(name, joined) # reuse Employee's setup
self.promoted = promoted
def seniority(self, today):
return today - self.promoted
m = Manager("Asha", 2015, 2020)
print(m.describe()) # inherited, not rewritten
print(m.seniority(2026))Asha joined 2015
6- Usually one hierarchy of types captures both subtyping and inheritance:
Acan inherit fromBifAis a subtype ofB. - Philosophically they differ: subtyping is a relationship of interfaces; inheritance is a relationship of implementations.
class Manager extends Employee {
private int promoted;
Manager(String name, int joined, int promoted) {
super(name, joined); // reuse Employee's constructor
this.promoted = promoted;
}
}5.7Subtyping vs inheritance: the Deque example
- A deque is a double-ended queue:
insert-front(),delete-front(),insert-rear(),delete-rear(). - A stack can be built from it using only
insert-frontanddelete-front; a queue using onlyinsert-rearanddelete-front. - So
StackandQueueinherit fromDeque, reusing its implementation. - But they are not subtypes of
Deque. Ifvof typeDequepoints to aStack, we cannot safely callinsert-rear()ordelete-rear(). Their interfaces are not compatible. - In fact
Dequeis a subtype of bothStackandQueue: it supports every operation they do.
class Deque:
def __init__(self):
self.items = []
def insert_front(self, x):
self.items.insert(0, x)
def delete_front(self):
return self.items.pop(0)
def insert_rear(self, x):
self.items.append(x)
def delete_rear(self):
return self.items.pop()
class Stack(Deque): # inherits ALL of Deque's code
pass
s = Stack()
s.insert_front(1)
s.insert_front(2)
print(s.delete_front(), s.delete_front()) # stack behaviour
s.insert_rear(7) # Python lets a Stack do this...
print(s.items)2 1
[7]The inheritance worked (the code was reused), but the last two lines show the problem: a Stack that offers insert_rear is not a real stack interface. Python does not stop us; Java's design pushes you towards the right choice:
| You want | In Java use |
|---|---|
| Subtyping only (an interface that others can rely on) | interface + implements |
| Subtyping and reuse of code | class B extends A |
| Reuse of code without becoming a subtype | composition: keep an object inside as a private field |
class Stack {
private final Deque deque = new Deque(); // reuse by composition
void push(int x) { deque.insertFront(x); }
int pop() { return deque.deleteFront(); }
// insertRear is simply not available on a Stack
}extend only one class but implement many interfaces. This avoids the "diamond problem" (two parents providing conflicting code for the same method) while still allowing a type to have several subtype relationships.5.8Lecture words vs interview words
Interviewers usually ask for the "four pillars". They are the same ideas under slightly different names, so link them in your head:
| Interview pillar | Lecture feature | How to say it |
|---|---|---|
| Encapsulation | Abstraction (private implementation) | data and the methods that use it bundled together, with the data hidden behind private |
| Abstraction | Abstraction (public interface), ADTs | callers see what an object does through an interface, not how |
| Inheritance | Inheritance | a subclass reuses and extends a superclass's implementation |
| Polymorphism | Subtyping + Dynamic lookup | one call, many behaviours: overloading (compile time) and overriding (run time) |
int, double...) that are not objects, and static members that belong to a class, not to an object. That is a deliberate compromise for speed, in the spirit of "every language makes compromises".5.9Exam tips and common mistakes
- Subtyping = interfaces. Inheritance = implementations. Say these two words in the answer.
- Subtype arrow direction: the subtype supports more or equal operations. That is why
Dequeis the subtype ofStack. - For dynamic lookup always separate: can I call it? (static type) and what runs? (runtime type).
- Assuming that inheriting always means being a subtype.
- Mixing up overloading (static, different parameters) and overriding (dynamic, same signature).
- Saying a subtype "has fewer" methods. A subtype can only add or specialise, never remove.
- Using inheritance just to reuse code when the relationship is not is-a.
5.10Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Explain the four pillars of OOP. | Very likely | Encapsulation (data + methods bundled, data hidden), abstraction (expose what, hide how), inheritance (reuse and extend a parent), polymorphism (one call, behaviour chosen by type). Give one tiny example each. |
| Overloading vs overriding? | Very likely | Overloading: same name, different parameters, resolved at compile time by static types. Overriding: a subclass redefines an inherited method with the same signature, resolved at run time by the object's real type. |
| Compile-time vs run-time polymorphism? | Very likely | Compile-time = overloading (static binding). Run-time = overriding through a superclass reference (dynamic dispatch / late binding). |
| Inheritance vs composition: when which? | Very likely | Inheritance for a true is-a relationship where the subtype can always stand in for the parent. Otherwise compose: hold the other object as a private field and delegate. It is more flexible and does not leak the parent's whole interface. |
| Why does Java not allow multiple inheritance of classes? | Very likely | To avoid the diamond problem and ambiguity. Multiple interfaces are allowed, since they give subtyping without conflicting implementations. |
| Is Java purely object oriented? | Very likely | No: primitives are not objects and static members belong to the class. |
| What is the Liskov substitution principle? | Likely | Objects of a subtype must be usable wherever the supertype is expected without breaking the program's correct behaviour. |
| What is dynamic dispatch (late binding)? | Likely | The JVM picks the method implementation at run time from the object's actual class, not from the variable's declared type. |
Classes and Objects
Class versus object, constructors and methods, changing a class's internals, and why Python's lack of enforcement leads to Java.
6.1Programming with objects: class and object
- A template for a data type
- how the data is stored
- how the public functions manipulate it
- A concrete instance of that template
- each object keeps its own copy of the local data
- calling a method = sending a message to the object
Example: 2D points. A point has coordinates (x, y). Each point object stores its own x and y; these are called instance variables. For a point p they are p.x and p.y. Inside the class, self is a special name for the current object (Java calls it this).
6.2The constructor
When we create an object we need to set it up. A constructor function with a fixed name is called implicitly. In Python it is __init__() and its first parameter is always self.
class Point:
def __init__(self, a=0, b=0):
self.x = a
self.y = b
p = Point(5, 7)
q = Point()
print(p.x, p.y, q.x, q.y)5 7 0 0self.y = y. That is a slip: the parameter is called b, and y does not exist, so it would raise a NameError. The class above uses the correct self.y = b (the lecture's later slide has it right).public class Point {
private double x, y; // instance variables
public Point(double a, double b) { // constructor: same name as the class, no return type
this.x = a;
this.y = b;
}
public Point() { this(0, 0); } // Java has no default arguments: overload instead
}| Python | Java | |
|---|---|---|
| Constructor | __init__(self, ...) | method named like the class, no return type |
| Current object | self (written in every method) | this (implicit) |
| Default values | default arguments (a=0) | overloaded constructors |
| Create | Point(5, 7) | new Point(5, 7) |
6.3Adding methods: changing state vs just reading it
Translation shifts a point by : . It updates the instance variables. Distance from the origin, , does not change the object: its state is unchanged.
import math
class Point:
def __init__(self, a=0, b=0):
self.x = a
self.y = b
def translate(self, dx, dy): # changes the state
self.x += dx
self.y += dy
def odistance(self): # only reads the state
return math.sqrt(self.x * self.x + self.y * self.y)
p = Point(3, 4)
print(p.odistance())
p.translate(3, 8)
print(p.x, p.y)
print(p.odistance())5.0
6 12
13.416407864998739| Kind of method | Changes the object? | Example | Common name |
|---|---|---|---|
| Mutator | yes | translate | setter, setX() |
| Accessor / query | no | odistance | getter, getX() |
6.4Changing the internal implementation
Suppose we store the point in polar coordinates instead, with and . Now the distance from the origin is just r. Translation must convert back to using , add , and recompute .
import math
class Point:
def __init__(self, a=0, b=0):
self.r = math.sqrt(a * a + b * b)
if a == 0:
self.theta = math.pi / 2
else:
self.theta = math.atan(b / a)
def odistance(self):
return self.r
def translate(self, dx, dy):
x = self.r * math.cos(self.theta)
y = self.r * math.sin(self.theta)
x += dx
y += dy
self.r = math.sqrt(x * x + y * y)
if x == 0:
self.theta = math.pi / 2
else:
self.theta = math.atan(y / x)
p = Point(3, 4)
print(p.odistance())
p.translate(3, 8)
print(round(p.odistance(), 6))5.0
13.416408Same interface (Point(a, b), translate, odistance), same answers, completely different insides. Users need not know whether the representation is or . That is abstraction working.
math.atan(b / a) only returns angles between -90 and +90 degrees, so it loses the quadrant when x is negative. Run through the polar version a point such as (-3, 4) and it comes back as (3, -4) (checked by running it). The correct tool is math.atan2(b, a). The lecture's point is the idea of swapping representations, not production-ready geometry, but do not copy this code.import math
a, b = -3, 4
theta = math.atan(b / a) # lecture version
r = math.sqrt(a * a + b * b)
print(round(r * math.cos(theta), 6), round(r * math.sin(theta), 6))
theta = math.atan2(b, a) # correct version
print(round(r * math.cos(theta), 6), round(r * math.sin(theta), 6))3.0 -4.0
-3.0 4.06.5When abstraction is not enforced: breaking changes
Python allows direct access to instance variables from outside the class:
class Point:
def __init__(self, a=0, b=0):
self.x = a
self.y = b
p = Point(5, 7)
p.x = 4 # the point is now (4, 7)
print(p.x, p.y)4 7This defeats the purpose of abstraction. If another programmer wrote p.x = 4 and we later switch to the polar representation, that code breaks. A change to the internals that damages other code is called a breaking change:
import math
class Point: # now the polar version
def __init__(self, a=0, b=0):
self.r = math.sqrt(a * a + b * b)
self.theta = math.atan2(b, a)
p = Point(5, 7)
print(p.x)AttributeError: 'Point' object has no attribute 'x'6.6Subtyping and inheritance in classes: Rectangle and Square
Define Square as a subtype of Rectangle: a different constructor, the same instance variables.
class Rectangle:
def __init__(self, w=0, h=0):
self.width = w
self.height = h
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Square(Rectangle):
def __init__(self, s=0):
self.width = s
self.height = s
s = Square(5)
print(s.area(), s.perimeter())25 20Square inherits area() and perimeter() from Rectangle, and it all works. Now the subclass author wants a nicer instance variable, side:
class Rectangle:
def __init__(self, w=0, h=0):
self.width = w
self.height = h
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Square(Rectangle):
def __init__(self, s=0):
self.side = s
s = Square(5)
print(s.area(), s.perimeter())AttributeError: 'Square' object has no attribute 'width'A subtype is not forced to be an extension of its parent: width and height were never defined for a Square. Worse, the subclass and the parent class are usually developed separately. The author of Rectangle may rename the variables:
class Rectangle:
def __init__(self, w=0, h=0):
self.wd = w
self.ht = h
def area(self):
return self.wd * self.ht
def perimeter(self):
return 2 * (self.wd + self.ht)
class Square(Rectangle):
def __init__(self, s=0):
self.width = s
self.height = s
s = Square(5)
print(s.area())AttributeError: 'Square' object has no attribute 'wd'Squareconstructor setswidthandheight, but the parent now useswdandht: the names changed.- We need a way to hide private implementation details: declare each component
privateorpublic. - But then how can the
Squareconstructor set the privatewdandht?Squaredoes not, and should not, know their names. - So we need elaborate declarations: the type and the visibility of every variable, with static type checking to catch mistakes early.
How Java solves it
class Rectangle {
private double width, height; // hidden: no outsider, no subclass, can touch them
Rectangle(double w, double h) { width = w; height = h; }
double area() { return width * height; }
double perimeter() { return 2 * (width + height); }
}
class Square extends Rectangle {
Square(double side) {
super(side, side); // ask the parent to set up ITS OWN variables
}
}- The fields are
private, so renaming them cannot break a subclass: nobody else could use the names. super(side, side)lets the child construct the parent part without knowing the parent's variable names.- Static typing catches a wrong use immediately:
sq.wdfrom outside is "has private access", a compile-time error.
setWidth and setHeight), a Square cannot honour both independently, so substituting a Square for a Rectangle can break code written for rectangles. That is the classic violation of the Liskov substitution principle. Say this and you stand out.6.7Language support: what Python leaves to discipline
| Concern | Python | Java |
|---|---|---|
| Hide implementation details | convention only (_name); anyone can still reach it | private is enforced by the compiler |
| Declare types | optional hints, not enforced | mandatory, checked at compile time |
| Subtype consistency | programmer discipline | extends / implements checked by the compiler |
| Java access level | Who can use it |
|---|---|
private | only the same class |
| (none, "package-private") | classes in the same package |
protected | same package and subclasses |
public | everyone |
- Python has no mechanism to enforce privacy. You can illegally manipulate private instance variables, and can introduce inconsistencies between subtypes and parent types.
- So: use strong declarations to enforce privacy and types. Do not rely on programmer discipline. Catch bugs early through type checking. This is the reason for learning Java.
6.8Exam tips and common mistakes
- Class = template (no data of its own). Object = instance (has its own data). One class, many objects.
- Mutator changes instance variables; accessor only returns something computed from them.
- If a question shows a subclass that sets different variables than the parent uses, the answer is a run-time error in Python and an access/construction error in Java.
- Always explain a "breaking change" with: outside code depended on the internals.
- Forgetting
self.in front of an instance variable inside a method (it becomes a local variable). - Thinking
Square(Rectangle)automatically getswidthandheight. They exist only if the constructor of someone sets them. - Believing a class has its own data. Only objects do (static fields excepted).
- Copying the lecture's
atan-based polar code as if it handled all quadrants.
6.9Interview corner: what they may ask
| Likely question | Chance | Crisp answer |
|---|---|---|
| Class vs object? | Very likely | A class is the blueprint (fields + methods); an object is one instance with its own field values, created with new. Objects live on the heap. |
| What is a constructor? Can it be overloaded? Does it return? | Very likely | Special code run when an object is created, named like the class, with no return type. It can be overloaded; Java has no default arguments. |
What are this and super? | Very likely | this: the current object (the lecture's self). super: the parent part, used to call the parent constructor or an overridden method. |
| Why make fields private and use getters/setters? | Very likely | So the internal representation can change without breaking callers, and so values can be validated in one place. Lecture 6's Point and Rectangle/Square demos are exactly this. |
| Is a Square a Rectangle? (Liskov) | Likely | Mathematically yes, but with mutable setters it breaks substitutability, so as a class design it is a poor is-a. |
| What is a breaking change? | Likely | A change to a public interface or exposed internals that makes existing client code stop working. |
| Difference between mutator and accessor? | Sometimes | A mutator changes the object's state (translate); an accessor only reads (odistance). |
Interview Question Bank
What is likely to be asked from Week 1, with 30-second answers, deeper explanations and code-reading practice.
Interviewers use these concepts to test whether you understand Java, not whether you can type it. Every answer below starts with a 30-second version; open the button, then read the details.
One clear sentence: what it is.
Three or four lines of code, or a picture in words.
Why it exists: what goes wrong without it.
7.1Interview heat-map: what is most likely
var"Is Java statically typed? Does var change that?"Notes 2.2 - 2.6this.x do when a local is also named x?"Notes 3.17.2Fundamentals
A very common opener. Answer with trade-offs, not loyalty.
Java trades some brevity for safety: static types and enforced private catch whole classes of bugs at compile time and make big programs safe to refactor. Python is quicker for small scripts.
Go deeper
- Static typing: type errors and typos are caught at compile time (
factorlstwould not compile). - Enforced privacy: outside code cannot depend on internals, so classes can be refactored safely.
- Performance and portability: bytecode, a JIT-compiling JVM, the same
.classeverywhere. - The cost: more typing and ceremony. Every language is a compromise.
Walk through it from the source file to the CPU.
javac compiles .java to bytecode in .class files; the JVM loads it, interprets it and JIT-compiles hot methods, and manages memory with stack frames, a heap and a garbage collector.
Go deeper
javac Hello.javachecks syntax and types and writesHello.class.java Hellostarts the JVM, which loads the class and begins atmain.- Bytecode is portable: only the JVM is platform specific.
- JDK = JRE + tools (
javac); JRE = JVM + libraries; JVM runs bytecode.
x = 7.5 after x = 10 but Java refuses?Explain dynamic versus static typing through this one example.
In Python the type belongs to the value, so a name can point at anything. In Java the type belongs to the variable, fixed at declaration, so the compiler rejects x = 7.5 before the program runs.
Go deeper
- Gain: early errors, speed, tooling.
- Cost: declarations and less flexibility.
- Static typing cannot prove a program correct (halting problem); it only guarantees type consistency.
The lecture's closing line, turned into an example.
Java gives up some raw speed and control (garbage collection, bounds checks) to get safety and fewer bugs.
Go deeper
- Safety vs speed: array bounds are checked and memory is garbage collected, so Java is a little slower than hand-managed C but far safer.
- Purity vs speed: primitives are not objects.
- Simplicity vs power: single inheritance of classes avoids the diamond problem but is less flexible.
7.3Memory
Take x = 7, an array m holding 8, 9, 10, and a StringBuilder s holding "hi", then call change(x, m, s).
static void change(int a, int[] arr, StringBuilder sb) {
a = 99;
arr[0] = 99;
sb.append("!");
arr = new int[] {0};
sb = new StringBuilder("new");
}Always by value. Java copies the argument; for an object the copy is the reference, so the callee can change the object's contents but cannot make the caller's variable point elsewhere.
Go deeper
xis still 7: the number was copied.m[0]is now 99 andsis "hi!": the reference was copied, so both names reach the same object.mandsstill point at the originals: re-pointing the local copies did nothing outside. In true pass by reference they would have changed.
swap(int a, int b) method. Why can't it work in Java?Draw the memory for this method while it runs.
void demo() {
int count = 3;
int[] marks = new int[3];
Account a = new Account();
}Locals and references live in the method's stack frame; every object and array lives on the heap, reached through a reference.
Go deeper
count: stack (a primitive local).marksanda(the references): stack.- The array and the
Accountobject: heap. - When
demo()returns, its frame is popped; the two heap objects become garbage once nothing else refers to them.
a in a static list?Explain it, then say when it cannot help.
It marks everything reachable from the program's variables (the roots) and reclaims everything unmarked, so even an unreachable cycle is freed.
Go deeper
- Roots: local variables on the stacks and static fields.
- Modern JVMs are generational: most objects die young, so the young area is collected often and cheaply.
- You cannot force it:
System.gc()is a hint. - It does not stop leaks: an object still reachable but no longer needed is never freed.
Connect recursion to the stack of activation records.
Each call pushes an activation record and the stack is finite, so too many nested calls throw StackOverflowError, usually from a missing base case.
Go deeper
- Fix the base case.
- Or rewrite as a loop.
- Rarely, raise the stack size with
-Xss. - Do not confuse it with
OutOfMemoryError, which is the heap running out.
7.4Object-oriented programming
Use a Shape variable holding a Circle.
Shape s = new Circle();
s.draw(); // runs Circle's draw(), not Shape'sOne call, many behaviours. Overloading is chosen at compile time from the static types; overriding is chosen at run time from the object's real type (dynamic lookup).
Go deeper
- Run-time: the declared type
Shapeallowsdraw(); the real typeCircledecides which code runs. - Compile-time: same method name, different parameters.
- The benefit: the Simula event loop never asks "what kind of event?". New kinds are new classes, with no edits to old code.
The most mixed-up pair in interviews.
public class Account {
private int balance; // hidden data
public void deposit(int amt) { balance += amt; }
public int balance() { return balance; }
}Abstraction shows what and hides how through a small interface. Encapsulation is the mechanism: data and the methods that use it live together, behind private.
Go deeper
- Encapsulation:
balanceis private, so nobody can set it to a wrong value directly. - Abstraction: callers think "deposit, read balance" and do not care whether it is one number or a list of transactions (Lecture 4's data refinement).
The lecture's own example.
Inherit when the child is truly an is-a and can stand in for the parent. Otherwise hold the other object as a private field and delegate to it.
Go deeper
- A
Stackbuilt onDequewould also inheritinsertRear, so it could wrongly be used as aDeque: code reused, but not a subtype. - Composition reuses the code and exposes only what a stack should.
super?Lecture 6's Square forgot its parent. What does Java do?
The compiler inserts an implicit super(). If the parent has no no-argument constructor, that is a compile-time error and you must write super(args) yourself.
Go deeper
- Python's
Squarenever setwidthandheight, so it failed only at run time. - Java makes that mistake hard to commit.
Lecture 5's subtyping-versus-inheritance split in Java clothes.
An interface is a pure contract (subtyping only). An abstract class is a partly built parent (subtyping plus shared code and state). A class can implement many interfaces but extend only one class.
Go deeper
| Interface | Abstract class | |
|---|---|---|
| Purpose | a contract | a partial implementation to inherit |
| Inheriting | many allowed | only one |
| State, constructors | no instance state | allowed |
Use an interface when unrelated classes share a role; an abstract class when subclasses really share code and state.
Builds on mutator versus accessor.
Make it final, make every field private final, set them only in the constructor, give no mutators, and defensively copy any mutable object that goes in or out.
Go deeper
- An immutable class has accessors only: no
translate-like methods. - Such objects are safe to share, so they are also safe across threads.
7.5Code-reading questions to expect
Interviewers love a snippet and the question "what does this print?". Try each one before you open the answer.
static void tweak(int[] a, int[] b) {
a[0] = 1;
b = new int[] {9};
b[0] = 2;
}
// main:
int[] p = {0}, q = {0};
tweak(p, q);
System.out.println(p[0] + " " + q[0]);1 0ashares the array ofp, soa[0] = 1shows outside.b = new int[]{9}only re-points the local copy;q's array is untouched and the2goes to the new array.
class Animal { void speak() { System.out.println("..."); } }
class Dog extends Animal { void speak() { System.out.println("woof"); } }
static void call(Animal a) { System.out.println("animal"); }
static void call(Dog d) { System.out.println("dog"); }
Animal x = new Dog();
x.speak();
call(x);woof
animalx.speak()is overriding: the run-time typeDogdecides.call(x)is overloading: the static typeAnimaldecides.
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;Both objects are garbage.
- They point at each other, but no variable on the stack can reach either.
- Reachability from variables is the test, so a marking collector frees them.
Week 1 Summary Sheet
Week 1 on one page: a lecture-by-lecture glance, key terms, the pairs to tell apart, the errors you can now read, a checklist and corrections to the source notes.
8.1Week 1 at a glance
Abstraction hides the machine; Java compiles to bytecode for the JVM; imperative says how, declarative says what.
Types give bits meaning and catch bugs early. Static (Java) is checked at compile time, dynamic (Python) at run time.
Calls live on the stack, long-lived data on the heap; Java passes by value; the garbage collector frees the unreachable.
Refine top-down; a component is interface plus specification; data refinement cascades.
Abstraction, subtyping, dynamic lookup, inheritance. Subtyping is about interfaces, inheritance about implementations.
A class is a template, an object an instance. Hide the representation or a change becomes a breaking change.
8.2Key terms, lecture by lecture
- Abstraction
- Using something through what it does, not how it is built.
- Compiler
- Translates the whole program first.
- Interpreter
- Runs the program line by line.
- Imperative
- How: steps, variables, loops.
- Declarative
- What: describe the result, often by induction.
- ADT
- Values plus permitted operations, with a hidden representation.
- Static typing
- Types declared in advance, checked at compile time.
- Dynamic typing
- Types from current values, checked at run time.
- Type inference
- The compiler deduces a type from context (
var). - Static analysis
- Finding errors and optimising without running the code.
- Halting problem
- No general algorithm can decide what any program will do.
- Scope
- Where a name can be used.
- Lifetime
- How long the storage stays allocated.
- Activation record
- Stack storage of one call: parameters, locals, links.
- Control link
- Points to the calling (previous) record.
- Return value link
- Says where this call writes its result.
- Call by value
- The argument is copied into the parameter.
- Heap
- Memory for data that outlives a call.
- Garbage collection
- Automatic freeing of unreachable storage.
- Program refinement
- Adding detail to the code; data stays.
- Data refinement
- Changing the representation; it cascades to users.
- Interface
- What is visible to other components.
- Specification
- The promised behaviour seen through the interface.
- Object
- Hidden data plus public methods.
- Subtype
- A can be used wherever B is needed (A ⊆ B).
- Dynamic lookup
- The runtime type picks the method that runs.
- Overloading
- Chosen from the static types.
- Inheritance
- Reuse of another class's implementation.
- Class
- The template for objects.
- Constructor
- Code that sets an object up when it is created.
- Instance variable
- Data each object keeps its own copy of.
- Mutator / accessor
- Changes the state / only reads it.
- Breaking change
- An internal change that breaks code depending on it.
8.3The pairs you must tell apart
8.4Messages and errors you can now read
| What went wrong | What you will see | When |
|---|---|---|
| value does not fit the declared type | error: incompatible types: possible lossy conversion from double to int | compile time |
| wrong kind of object assigned | error: incompatible types: String cannot be converted to int | compile time |
| misspelt or undeclared name, or a method that does not exist | error: cannot find symbol | compile time |
| same local name declared twice in one scope | error: variable x is already defined in method ... | compile time |
| outside code touching a private field | error: x has private access in Point | compile time |
| runaway recursion | java.lang.StackOverflowError | run time |
| heap exhausted | java.lang.OutOfMemoryError: Java heap space | run time |
| Python: missing attribute (the Rectangle/Square demos) | AttributeError: 'Square' object has no attribute 'width' | run time |
| Python: runaway recursion | RecursionError: maximum recursion depth exceeded | run time |
8.5Last-minute checklist
- Why languages exist; machine code is tedious
- Java pipeline:
.java→javac→ bytecode → JVM - JDK / JRE / JVM
sum_listandsum_square_evenboth ways
- Three jobs of types
- Static vs dynamic; the
factorlsttypo - Static analysis and the halting problem
- Type inference and
var
- Scope vs lifetime; hole in the scope
- The factorial stack diagram
- Control link vs return value link
- Call by value; Java is always by value
- Heap, and mark-and-sweep garbage collection
- Top-down vs bottom-up
- Program vs data refinement; cascading impact
- Component = interface + specification
- Control abstraction vs data abstraction
- The four features
- Subtype rule (A ⊆ B)
- Dynamic lookup vs overloading
- Deque: reuse is not subtyping
- Class vs object; constructor;
self/this - Changing internals; breaking changes
- Rectangle / Square and privacy
- Why Python's lack of enforcement leads to Java
8.6Corrections made to the source notes
| Where | The source says | Correct |
|---|---|---|
Lecture 6, first Point | self.y = y | self.y = b |
Lecture 6, polar Point | atan(b / a) is enough | loses the quadrant for negative x; use atan2(b, a) (checked by running) |
| Lecture 4, primes figure | third block is cut off at the bottom in the PDF | redrawn in full from the three levels |
| Spelling in the PDF | "well-types", "shows down", "peformance", "withing" | well-typed, slows down, performance, within |
javac + JVM
imperative vs declarative
static analysis
var = inference
stack + activation records
heap + garbage collection
interface + specification
data refinement cascades
dynamic lookup, inheritance
Deque: reuse is not subtype
breaking changes
private + super + static types