A is correct. No compilation or run-time error. What’s being assigned is the compile-time type and methods.


For overriding methods in general:

  • Exceptions: can throw same, narrower, or fewer — never broader checked exceptions
  • Access: can widen, never narrow
  • Return type: must be same or a covariant subtype (Override Dog from Animal)
  • @Override: optional, doesn’t affect correctness
  • Parameters: must match exactly (after type parameter substitution) Parent<S> is not the same as Parent<? extends S>

Generics Practice

Producer (everything you take out must at least be a T or a subtype of T) Consumer(must be T or more general)

Always think about the data flow. Are we reading values from it (producer) or using it as an input (consumer)

  • If it goes INTO something (you pass it as an argument) → consumer position? super
  • If it comes OUT OF something (you read/return it) → producer position? extends
  1. Comparable<? super T> says: T can compare itself to T or any supertype of T, meaning the compareTo method can live on a parent class.
  2. List<? extends T> — list is a producer of T → extends
  3. BinaryOperator<R> where R is both input and output, it has to be invariant, no wildcards

X need extend the interface to use m(b) b is consumed by m’s parameter (consumer position), so I’s T must be a supertype of X → I<? super X>.

If we added return a;

  • return a — a is type X, returned as type X. But wait — is X used as a wildcard inside any generic? No. We’re just returning a, whose type is the bare X.
  • It’s just like normally returning a of type X.

The “both → invariant” rule applies when:

  • You have a single type parameter
  • It appears inside a single generic type (like BinaryOperator<R>)
  • AND that generic type uses it in both producer AND consumer positions

It does NOT apply when:

  • A method just happens to take inputs and return outputs
  • Different type parameters are in different positions
  • A type parameter is used as a bare type (not inside a wildcard)

static <T> void foo(List<T> list, T item)
                    ^^^^^^^^^     ^
                    generic type  bare T (no <>)
                    has a slot    no slot
                    for wildcard  for wildcard
                    
PECS only works on generic types LMAOOO

Streams

limit(3) operates on what comes into it, not on the source limit(3) says: “let only the first 3 elements that reach me pass through to the terminal op.” So the question is: what are the first 3 elements that reach limit(3)? Look at the pipeline order:

iterate(1, x -> x+1)  →  filter(even)  →  map(x*x)  →  limit(3)  →  reduce(sum)
   1, 2, 3, 4, 5, ...     2, 4, 6, ...    4, 16, 36...   4, 16, 36

So limit(3) takes the first 3 elements after filtering and mapping — which are 4, 16, 36.


Remember that they go element by element.


Stream.of(1, 2, 3, 4)
    .filter(x -> { System.out.println("f " + x); return x % 2 == 0; })
    .map(x -> { System.out.println("m " + x); return x * 10; })
    .findFirst();
  • map(x -> Stream.of(x, x*10))Stream.of(Stream.of(1,10), Stream.of(2,20), Stream.of(3,30)) — nested
  • flatMap(x -> Stream.of(x, x*10))Stream.of(1, 10, 2, 20, 3, 30) — flat

1 enters flatMap → produces stream (1, 10) → 1 → forEach prints “1” → 10 → forEach prints “10” 2 enters flatMap → produces stream (2, 20) → 2 → forEach prints “2” → 20 → forEach prints “20” 3 enters flatMap → produces stream (3, 30) → 3 → forEach prints “3” → 30 → forEach prints “30”


Given List<List<Integer>> nested, return a single List<Integer> of even numbers, doubled. Input: [[1, 2, 3], [4, 5, 6], [7, 8]] Output: [4, 8, 12, 16]

return nested.stream()
    .flatMap(List::stream)         // List<List<Integer>> → Stream<Integer>
    .filter(x -> x % 2 == 0)       // keep evens
    .map(x -> x * 2)               // double them
    .toList();                     // collect

Return x.method


Lists are immutable. list.add() throws exceptions


Caching

You probably treated b.get() and c.get() as independent calls — each with its own evaluation chain. But b and c share the same upstream a. Once any path has forced a, a’s cache is set, and every other path uses the cache.

Lazy<Integer> a = Lazy.of(() -> {
    System.out.print("A;");
    return 1;
});
Lazy<Integer> b = a.map(x -> x + 10);
Lazy<Integer> c = a.map(x -> x + 100);
 
System.out.print(b.get() + ";");
System.out.print(c.get() + ";");
System.out.print(b.get() + ";");

Total output: A;11;101;11; Cache A so u don’t run it

After the first .get() on a (or any Lazy derived from a that needs a’s value), a’s supplier never runs again for the rest of the program. All future accesses just return 1 directly. No print, no computation.


Yes, the .get() is part of flatMap’s implementation. You don’t write it — flatMap writes it for you. You see this in your code:

javav1.flatMap(x Lazy.of(() { print “Eval 4;”; return x + 4; }))

You see no .get(). That’s because flatMap’s job is to handle the unwrap for you. You give it a function T → Lazy<R>, and flatMap takes care of pulling the R out of the inner Lazy.

Lazy<Integer> v1 = Lazy.of(() -> { System.out.print("Eval 1;"); return 1; });
Lazy<Integer> v2 = Lazy.of(() -> { System.out.print("Eval 2;"); return 2; });
Lazy<Integer> v3 = v1.map(x -> x + 2);
Lazy<Integer> v4 = v1.flatMap(x ->
    Lazy.of(() -> { System.out.print("Eval 4;"); return x + 4; }));

List<Lazy<Integer>> xs = List.of(v1, v2, v3, v4);
System.out.print(xs.size());
System.out.print(xs.get(2).get());
System.out.print(xs.get(3).get());
System.out.print(xs.get(0).get());
System.out.print(xs.get(1).get());

Lazy<Integer> v4 = v1.flatMap(x ->
    Lazy.of(() -> { System.out.print("Eval 4;"); return x + 4; }));
    
there's a get() in flatMap that activates the newly built Lazy

Parallel and CompleteableFuture

How combining works in parallel

For Form 2, the combiner IS the accumulator (same type and operation). So if accumulator is associative, combiner is too.

Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b); // 6

For Form 3, the combiner is separate. Used only when the stream runs in parallel and partial results from different threads need merging.

Stream.of("hello", "world").reduce(
    0,                           // identity (U = Integer)
    (sum, str) -> sum + str.length(),  // accumulator: U + T -> U
    (u1, u2) -> u1 + u2          // combiner: U + U -> U
);  // 10

For most parallel reduce questions, just check:

  • Form 2 (one operator): is the operator associative? If yes, safe.
  • Form 3 (separate combiner): does compatibility hold? If yes, safe.

D is associative — and yes, parallel-safe

String concatenation is associative:

("a" + "b") + "c"  =  "ab" + "c"  =  "abc"
"a" + ("b" + "c")  =  "a" + "bc"  =  "abc"

Same result regardless of how you parenthesize. That’s the definition of associative.

What you confused: associativity vs commutativity

  • Associative: order of grouping doesn’t matter. (a+b)+c == a+(b+c)
  • Commutative: order of operands doesn’t matter. a+b == b+a

These are different properties. Parallel reduce only needs associativity, not commutativity.

You said “I might get cba” — that would only happen if parallelism shuffled the order of operands. It doesn’t. Parallel splits the stream into chunks, each chunk preserves order, and the final combine respects chunk order.

Sequential:  "" + "a" + "b" + "c"  →  "abc"

Parallel split [a, b] and [c]:
  Thread 1: "" + "a" + "b"  →  "ab"
  Thread 2: "" + "c"  →  "c"
  Combine: "ab" + "c"  →  "abc"  ✅ same result

double result = Stream.of(2.0, 4.0, 8.0)
    .parallel()
    .reduce(100.0, (a, b) -> a / b);

Sequential gives 100 / 2 / 4 / 8 = 1.5625. Parallel may not.

double result = Stream.of(2.0, 4.0, 8.0)
    .parallel()
    .map(x -> 1 / x)              // transform each element to its reciprocal
    .reduce(100.0, (a, b) -> a * b);  // now multiplication, which IS associative

if i just did a * 1 / b, it’s the same thing ah LOL

you can also

double result = Stream.of(2.0, 4.0, 8.0)
    .parallel()
    .reduce(
        100.0,                          // identity
        (acc, t) -> acc / t,            // accumulator: divides
        (u1, u2) -> u1 * u2 / 100.0     // combiner: multiplies but corrects for double identity
    );

but that shit complicated


The general technique to memorise Non-associative ops can sometimes be rewritten as map-then-associative-reduce.

division → reciprocal map + multiplication subtraction → negation map + addition average → sum reduce, then divide by count separately


// Form 2:
T reduce(T identity, BinaryOperator<T> op);
// op: (T, T) -> T  — both inputs and output are the SAME type

// Form 3:
<U> U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner);
// accumulator: (U, T) -> U  — different types
// combiner: (U, U) -> U

if you tried

int result = Stream.of("ab", "cde", "f")
    .parallel()
    .reduce(
        0,
        (sum, str) -> sum + str.length(),
        (s1, s2) -> s1 + s2
    );

you can’t use form 2 because reduce requires both to be the same type for the accumulator


For reduce(identity, op) to be parallel-safe, the identity must be a true identity for op:

op(identity, x) x for any x op(x, identity) x for any x

*0 is identity for +. 1 is identity for . "" is identity for string concat. 10 is NOT identity for +.

When identity isn’t a true identity, parallel reduce produces wrong (and potentially varying) results. This is the second condition for parallel reduce safety, alongside associativity. Memorise: associativity AND true identity, both required.

NEED BOTH IDENTITY + ASSOCIATIVITY


CompleteableFuture

CompletableFuture — what it actually is

A CompletableFuture<T> (CF) is a placeholder for a value of type T that doesn’t exist yet. You schedule work to compute it on a thread, then chain operations onto it.

Think of it as Lazy<T> but for asynchronous computation:

  • Lazy = “compute when forced, on the calling thread”
  • CF = “compute now, on a different thread, ready when you call .join()”

The .join() call is the equivalent of Lazy’s .get() — it forces you to wait for the value.

Sequential code blocks:

main thread: ── do A ── do B ── do C ── done

Async with CF:

main thread:  ── kick off A, B, C ────────── join ── done
thread 1:        └─ doing A ──┘
thread 2:        └─ doing B ────────┘
thread 3:        └─ doing C ──┘

The point: A, B, C run in parallel on separate threads. Total time = max(A, B, C) instead of A+B+C.

join() is like a wait for the answer before you continue. and it just tells you ahead of time this is what you can do with the value.

CF<Integer> v1 = supplyAsync(() -> { sleep(2); return 1; });

You’re not just starting computation. You’re declaring:

  • “There will eventually be an Integer here.”
  • “Here’s how it gets computed.”
  • “Other code can chain onto it (.thenApply, .thenCombine) before I even know the value.”

  • v1.thenCompose(f) reads as “v1 first, then run f on its result”. The Compose here means “compose async computations” — i.e. flatMap.

  • it’s like CF(CF(x)) but thenCompose removes that issue

  • thenCombine is you run two threads parallely then combine afterwards


Override


Overload picked by compile-time types of arguments. Override picked by runtime type of receiver.

class A {
    void foo(A x) { System.out.println("A.foo(A)"); }
}
class B extends A {
    void foo(A x) { System.out.println("B.foo(A)"); }
    void foo(B x) { System.out.println("B.foo(B)"); }
}

B b = new B();
A a = new B();
b.foo(a);

we’re dealing with overloaded which are all chosen during compile time so b compile time type is B a compile time type is A so we do foo(A x)


class A<T> {
    public T foo(T x) { return x; }
}

class B extends A<Integer> {
    @Override
    public Integer foo(Integer x) { return x + 1; }
}

Now because it’s override, so during run time A points to B and does that instead Override requires exact same signature!


Functor & Monad

Functor laws (2) — about map

A class is a functor if it has a map method satisfying:

1. Identity law

m.map(x -> x)  ≡  m

Mapping with the identity function changes nothing.

2. Composition law

m.map(f).map(g)  ≡  m.map(g.compose(f))
                ≡  m.map(x -> g(f(x)))

Mapping with f then g is the same as mapping once with the composition.


Monad laws (3) — about flatMap and of

A class is a monad if it has of and flatMap satisfying:

1. Left identity

of(x).flatMap(f)  ≡  f.apply(x)

Wrapping a value with of then flatMapping with f is the same as just calling f on x directly.

2. Right identity

m.flatMap(x -> of(x))  ≡  m

flatMapping with the “wrap” function changes nothing.

3. Associativity

m.flatMap(f).flatMap(g)  ≡  m.flatMap(x -> f(x).flatMap(g))

Order of grouping doesn’t matter when chaining flatMaps.

m.map(x x) = m m.map(f).map(g) = m.map(x g(f(x)))

of(x).flatmap(f) = f.apply(x) m.flatmap(x of(x)) = m m.fmap(f).fmap(g) = m.map(x f(x).flatmap(g))


Implementing flatmap and map

@Override
<R> Maybe<R> map(Function<? super T, ? extends R> f) {
    return new Some<>(f.apply(this.value));
    //                ^^^^^^^^^^^^^^^^^^^
    //                f returns plain R (e.g. plain Integer)
    //                so we wrap it in new Some<>
    //                result type: Some<R> ✓ which is Maybe<R> ✓
}

@Override
<R> Maybe<R> flatMap(Function<? super T, ? extends Maybe<? extends R>> f) {
    return new Some<>(f.apply(this.value));
    //                ^^^^^^^^^^^^^^^^^^^
    //                f returns Maybe<R> (e.g. Maybe<Integer>)
    //                wrapping that in new Some<> gives:
    //                result type: Some<Maybe<R>>  ❌
    //                which is Maybe<Maybe<R>>  ❌
    //                NOT Maybe<R>  ❌ COMPILE ERROR
}

Because im using flatmap on things that return wrapped elements.

That's why the answer is

return f.apply(this.value);

For lazy implementation

// map:
() -> f.apply(this.get())

// flatMap = map's body + .get() to unwrap
() -> f.apply(this.get()).get()

Eager to Lazy

private final String log;
->
private final Supplier<String> log; // ✓ field changed private 
if it's int, change to Supplier(Integer)

private Logger(String l) { this.log = l; }
->
Logger(Supplier<String> l) { this.log = l; } // ✓ constructor takes supplier 

static Logger of(String s) { return new Logger(s); }
->
of(String s) { return new Logger(() -> s); } // ✓ of wraps value
Logger append(String s) {
        System.out.println("append fired: " + s);
        return new Logger(this.log + s);
    }
    ->
Logger append(String s) {
    return new Logger(() -> {                
        System.out.println("append fired: " + s);  
        return this.log.get() + s});                 
}

Remember to end with })
int get() { return this.value; }
->
int get() { return this.value.get(); }   // forces the chain

Issues in append

Logger append(String s) {
    return () -> {                                 // ❌ returning a Supplier<?>, not a Logger
        System.out.println("append fired: " + s);
        return new Logger(this.log + s);           // ❌ Supplier<String> + String doesn't work
    };
}

Two problems:

  1. Wrong wrapping level. You wrapped the entire return new Logger(...) in a supplier. But append is supposed to return a Logger, not a Supplier<Logger>. The supplier should wrap only what’s deferred — the actual log computation — not the construction of the new Logger.
  2. this.log + s is wrong. this.log is now a Supplier<String>, not a String. To get the string, you need this.log.get().
  • Return type stays the same on lazy versions
  • Reading state to force .get() (because i changed the return type of State)

Exceptions

A return in finally swallows pending exceptions and replaces pending returns. Finally wins

static int foo() {
    try {
        throw new RuntimeException("A");
    } catch (RuntimeException e) {
        throw new RuntimeException("B");
    } finally {
        return 5;
    }
}
 
System.out.println(foo());
 

Ans is 5.


Comparable

Comparable<Orange>, but it inherits Comparable<Fruit> So if i put orange i should be able to the parent comparable(fruit)