INFO1113 Semester 1
Student Questions
From time to time, students may ask me questions that I’m not sure about. If that is the case, I’ll write the question in here, and find an answer. Students can then refer to this to get an answer. Alternatively, students can also request answers to questions on ed.
Tom’s Ed Public Workspaces
My Ed public workspaces are where I will generally write the code during the labs so that you can follow along.
General Assessment Feedback
Optional Explanation Videos
-
Tutorial 3, Reading from a File - Coming soon
Blog Posts
- Java Wildcards: Taming Generic Invariance - Now finished!
Miscellaneous
Quick Introduction to Generics by Claude!
Generics let you write a class or method that works with any type, while still being type-safe.
**The Problem Without Generics:**
// Without generics — you'd need a separate class for each type
public class IntBox {
private int value;
public int get() { return value; }
}
public class StringBox {
private String value;
public String get() { return value; }
}
That’s a lot of repetition for the same idea!
**The Solution — Generics**
// One class that works for ANY type
public class Box<T> {
// ↑ T is a placeholder for whatever type you want
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Now you can use it with any type:
Box<Integer> intBox = new Box<>();
Box<String> stringBox = new Box<>();
Box<Alien> alienBox = new Box<>();
**Why Not Just Use `Object`?**
// You could do this...
public class Box {
private Object value;
public Object get() { return value; }
}
Box box = new Box();
box.set("hello");
String s = (String) box.get(); // ❌ need to cast manually
// ❌ could crash at runtime if wrong type
Generics catch type errors at compile time instead of crashing at runtime.
Note: The T is just a convention, but it can be any letter.
Quick Introduction to Modulo by Claude!
The modulo operator (%) gives you the remainder after division.
For example, 10 % 3 = 1 because 10 ÷ 3 = 3 with 1 left over.
Think of it like splitting items into groups: if you have 10 cookies and want to share them equally among 3 friends, each gets 3 cookies — and you’re left holding 1. That leftover is your modulo result.
A few quick examples:
8 % 2 = 0 — 8 divides evenly, nothing left over 7 % 4 = 3 — 4 goes into 7 once, with 3 remaining 5 % 10 = 5 — 10 is bigger than 5, so the whole thing is the remainder Where you’ll actually use it:
Checking even/odd: if n % 2 == 0, the number is even
Wrapping around: keeping a value within a range (e.g., clock hours cycling 0–23)
Every nth item: running code every 5 iterations with if i % 5 == 0
The key insight: when the result is 0, the numbers divide perfectly. Any other result tells you what’s left over.
Quick Introduction to Wildcards in Java Generics by Claude!
In Java, even though Integer extends Number, a List<Integer> is not a List<Number>. Generic types do not inherit the same subtype relationships as their type parameters. Wildcards are how we get flexibility back.
**Upper-bounded: `? extends B`**
B is the ceiling. The unknown type must be B or a subtype of B. For example, if B is Number:
Object❌ — above the ceilingNumber✅ — the ceiling itselfInteger✅ —IntegerextendsNumberFloat✅ —FloatextendsNumberDouble✅ —DoubleextendsNumber
List<? extends Number> l;
l = new ArrayList<Integer>(); // ✅ Integer extends Number
l = new ArrayList<Double>(); // ✅ Double extends Number
l = new ArrayList<Object>(); // ❌ Object is above the ceiling
Use this when you want to read from a structure — you know everything in it is at least a B.
**Lower-bounded: `? super A`**
A is the floor. The unknown type must be A or a supertype of A. For example, if A is Integer:
Object✅ —Objectis a supertype ofIntegerNumber✅ —Numberis a supertype ofIntegerInteger✅ —Integeris a supertype of itself (the floor)Double❌ —Doubleis a sibling, not a supertype
List<? super Integer> l;
l = new ArrayList<Integer>(); // ✅ Integer is the floor
l = new ArrayList<Number>(); // ✅ Number is a supertype of Integer
l = new ArrayList<Object>(); // ✅ Object is a supertype of Integer
l = new ArrayList<Double>(); // ❌ Double is a sibling, not a supertype
Use this when you want to write into a structure — you know it can hold at least an A.
**A real example — `Collections.sort()`**
public static <T> void sort(List<T> list, Comparator<? super T> c)
If you sort a List<Integer>, then T = Integer. The comparator just needs to be capable of comparing Integers — which any of these can do:
Comparator<Integer> c1 = ...; // ✅ Integer is the floor
Comparator<Number> c2 = ...; // ✅ Number is a supertype of Integer
Comparator<Object> c3 = ...; // ✅ Object is a supertype of Integer
Restricting to only Comparator<Integer> would be unnecessarily strict — c2 and c3 are perfectly capable of comparing Integers, so ? super Integer lets all three through.
Why ? extends is Read-Only and ? super is Restricted-Write (Claude Explanation)
⚠️ Updated: earlier versions of this note oversimplified
? superas “write-only / writing is safe.” That’s misleading — writes are only safe for the bound type (Integer) and its subtypes, not for arbitrary supertypes. See the? super Integersection for the corrected reasoning.
The setup: Java generics are invariant
List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number:
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // ✗ compile error!
Wildcards (? extends and ? super) are the escape hatch. They let one method work with whole families of related types — but each comes with a restriction. Those restrictions aren’t arbitrary; they’re the only rules under which the compiler can keep your code type-safe.
The class hierarchy that drives everything
Object
↑
Number ← abstract class
↑ ↑ ↑ ↑
Integer Double Long Float ...
- An
Integeris-aNumber(and is-anObject). - A
Numberis not necessarily anInteger— it might be aDouble.
The “is-a” relationship only goes one way: up the hierarchy.
⚠️ Small wrinkle: the primitive
intis not aNumber— primitives aren’t objects, and you can’t writeList<int>at all. But the wrapper classIntegeris aNumber. When you writelist.add(42), Java auto-boxes theintinto anIntegerat the call site.
? extends Number — reads return the bound, writes are forbidden
Read this as: “a list of some specific subtype of Number, but I don’t know which one.”
The actual list might really be a List<Integer>, a List<Double>, a List<Long>, or just a List<Number> (every type is a subtype of itself). The compiler doesn’t know which.
Reading as the bound is safe ✓
Whatever the actual list is, every element is guaranteed to be at least a Number:
List<? extends Number> nums = someList; // could be List<Integer>, List<Double>, ...
Number n = nums.get(0); // ✓ definitely a Number
Integer i = nums.get(0); // ✗ compile error — might be a Double
Number is the strongest type you get. Even if the underlying list is really a List<Integer>, the compiler will only let you read elements as Number — because from the wildcard alone it can’t tell.
Writing is forbidden ✗
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // ✗ compile error — might corrupt a List<Integer>!
nums.add(42); // ✗ rejected — compiler doesn't know the actual subtype;
// could be List<Double>, List<Long>, etc., where Integer wouldn't fit
Even adding an Integer is rejected. The compiler doesn’t know what subtype is actually there — it just sees “some subtype of Number” — so it has to forbid all writes (except null, which fits everywhere).
Mental model: ? extends Number is open on the bottom — the element type could be any subtype of Number. Reads give you the bound (Number). Writes are forbidden because no specific value is guaranteed to fit every possible subtype.
? super Integer — writes accept the bound, reads return Object
Read this as: “a list of Integer or some supertype of Integer.”
The actual list might really be a List<Integer>, a List<Number>, or a List<Object>. Again, the compiler doesn’t know which.
Writing the bound (or a subtype) is safe ✓
An Integer is-an Integer, is-a Number, is-an Object — so an Integer fits into every possible underlying list type:
List<? super Integer> sink = someList; // could be List<Integer>, List<Number>, List<Object>, ...
sink.add(42); // ✓ Integer fits in any of those possibilities
Writing a supertype is forbidden ✗
This is the part it’s easy to get wrong. ? super Integer does not mean “you can write anything.” You can only write Integer (or a subtype of Integer, of which there are essentially none in practice).
List<? super Integer> sink = someList;
sink.add(1.5); // ✗ compile error — Double doesn't fit if list is List<Integer>
sink.add(new Object()); // ✗ compile error — Object doesn't fit if list is List<Integer> or List<Number>
Number n = 3.14;
sink.add(n); // ✗ compile error — same reason
You might think: “but if the underlying list is really a List<Number>, then 1.5 would be fine!” True — but the compiler doesn’t know whether the underlying list is List<Number> or List<Integer>. It has to assume the most restrictive possibility, which is List<Integer>. A Double doesn’t fit there, so the write is rejected.
The rule: for List<? super T>, you can write T or subtypes of T. Supertypes of T are rejected even when they’d happen to work for some underlying list types.
Reading is limited ✗
List<? super Integer> sink = new ArrayList<Object>();
Integer i = sink.get(0); // ✗ compile error — best the compiler can promise is Object
Object o = sink.get(0); // ✓ everything is an Object
The compiler knows the element type is some supertype of Integer — Integer, Number, Object, etc. — but not which one. The most specific type it can guarantee for every possibility is Object, so sink.get(0) has static type Object. Assigning Object to Integer without a cast fails.
(Separately: if the underlying list really is a List<Object>, the value pulled out might genuinely not be an Integer — somebody could have put a String in there. But that’s a consequence of the same uncertainty, not the cause of the compile error.)
Mental model: ? super Integer is open on the top — the element type could be any supertype of Integer. Writes accept the bound (Integer) and below. Reads give you Object (the one type guaranteed to be a supertype of every possible element type).
The real symmetry
The “read-only vs write-only” slogan for PECS is catchy but loose. The accurate symmetry is:
| Reads | Writes | |
|---|---|---|
? extends Number | ✓ as Number (the bound) — anything more specific rejected | ✗ forbidden (except null) |
? super Integer | ✓ as Object only | ✓ Integer (the bound) and below — anything more general rejected |
In both cases, the bound is the safe type, and the compiler refuses anything off the bound (more specific for ? extends reads, more general for ? super writes) because it would only work for some of the possible underlying list types — not all.
Why the restrictions aren’t arbitrary
The whole point of generics is compile-time type safety — catching bad assignments before runtime. The restrictions are exactly what the compiler needs to keep that guarantee:
- If
? extends Numberallowed writes → you could insert aDoubleinto aList<Integer>. - If
? super Integerallowed writing supertypes → you could insert aNumberinto aList<Integer>. - If
? super Integerallowed reads as the bound → you could pull aNumberout and assign it to anIntegerwithout a cast.
The restrictions aren’t the language designers being awkward — they’re the only rules under which wildcards can be safe.