Week 1: Revision

Nagraj MathNagraj Math
6 min read

Hey peeps! We are done with some major chunk in our Java Journey.

Let’s revisit our old learnings and redo it all over again.

I will be using ChatGPT to generate few code snippets, try to understand it and answer the questions! I’ll be writing here my own answers.

Code Snippet 1

class Animal {
    String name = "Animal";

    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    String name = "Dog";

    @Override
    void sound() {
        System.out.println("Dog barks");
    }

    void printNames() {
        System.out.println("Child name: " + name);
        System.out.println("Parent name: " + super.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sound();

        Dog d = new Dog();
        d.printNames();
    }
}

Questions:

  1. What will the output of a.sound() be and why?

  2. How is super.name different from name in printNames()?

  3. Is the object a an Animal or a Dog?

  4. What OOP concepts are demonstrated here?

Answers:

  1. Dog barks, method overriding

    Even though a is declared as Animal, it holds a Dog object.
    Since sound() is overridden, the runtime type (Dog) decides which version is called — this is runtime polymorphism.

  2. super always refers to the parent class. here it prints dog and then animal

    So name inside Dog refers to "Dog",
    and super.name refers to the parent (Animal)’s name, which is "Animal".

  3. It is a dog object with the class reference of animal

    This is the power of upcasting

  4. Polymorphism, Inheritance

Code Snippet 2

interface Calculator {
    int operation(int a, int b);
}

public class MathOps {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator multiply = new Calculator() {
            @Override
            public int operation(int a, int b) {
                return a * b;
            }
        };

        System.out.println("Add: " + add.operation(3, 4));
        System.out.println("Multiply: " + multiply.operation(3, 4));
    }
}

Questions:

  1. What is the difference between add and multiply declarations?

  2. Why is the lambda syntax allowed for add?

  3. Can we create a class that implements Calculator instead of these approaches?

  4. What concept allows us to use lambda expressions here?

Answers:

  1. add declaration uses lambda expression to reduce the implementation of the interface class Calculator. Whereas multiply declaration explicitly declares the whole implementation of the interface through anonymous inner class.

  2. because calculator is a single abstract method interface (functional interface)

  3. yess and even there the operation method must be defined.

  4. Functional interfaces

Code Snippet 3

import java.util.*;

public class GenericTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) list.add(i);  // [0, 1, 2, 3, 4]

        list.remove(2);  // removing what?

        System.out.println(list);  // what’s printed?
    }
}

Questions:

  1. What will the output be?

  2. What exactly is removed by list.remove(2) and why?

  3. How would you remove the number 2 instead of index 2?

  4. What Java feature causes this confusion?

Answers: I got most of these wrong :(

  1. Java automatically calls .toString() → prints the elements in array-style.

  2. refers to index, so the element at index 2 (which is 2) will be removed.

  3. list.remove(2) → calls remove(int index)

    list.remove(Integer.valueOf(2)) → calls remove(Object o)

    This is where autoboxing causes confusion.

  4. Because of autoboxing, 2 can be:

    • a primitive int (calls remove(index))

    • or an Integer object (calls remove(Object))

      The confusion arises from method overloading + autoboxing.

Code Snippet 4

public class ExecutionOrder {

    static {
        System.out.println("Static block of outer class");
    }

    {
        System.out.println("Instance block of outer class");
    }

    ExecutionOrder() {
        System.out.println("Constructor of outer class");
    }

    class Inner {
        {
            System.out.println("Instance block of inner class");
        }

        Inner() {
            System.out.println("Constructor of inner class");
        }
    }

    public static void main(String[] args) {
        ExecutionOrder eo = new ExecutionOrder();
        ExecutionOrder.Inner inner = eo.new Inner();
    }
}

Questions:

  1. What is the exact output, line by line?

  2. Why does the outer class’s static block run only once?

  3. In what order do the constructor and instance blocks run in both classes?

  4. What would happen if Inner was marked as static?

Answers:

  1. static block of outer class

    instance block of outer class

    constructor of outer class

    instance block of inner class

    Constructor of inner class

  2. Because a class is loaded only once by ClassLoader. And static blocks run only once the class is loaded

  3. Instance block first, constructor second

  4. It becomes a static nested class instead of an inner (non-static) class

    You no longer need an instance of ExecutionOrder to create it:

    ExecutionOrder.Inner inner = new ExecutionOrder.Inner();

    Also: The constructor is still a constructor regardless of static or not.

Code Snippet 5

public class ThreadDemo {

    static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " - Task Started");

            try {
                Thread.sleep(1000); // simulate work
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " - Interrupted!");
            }

            System.out.println(Thread.currentThread().getName() + " - Task Ended");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyTask(), "Worker-1");

        System.out.println("Before start - State: " + t1.getState());

        t1.start();

        System.out.println("After start - State: " + t1.getState());

        t1.join(); // Wait for thread to finish

        System.out.println("After join - State: " + t1.getState());
    }
}

Questions:

  1. What are the 3 thread states you expect to see printed?

  2. Why do we use Thread.sleep(1000) inside run()?

  3. What happens if we call start() on t1 a second time?

  4. Why use join() here?

  5. What does Thread.currentThread().getName() do?

Answers:

  1. new, runnable, terminated

  2. to simulate work, any CRUD or some processing takes time to complete.

  3. You cannot restart a thread once it's started or finished. Threads are one-time-use.

    To "restart", you’d need to create a new Thread object with the same Runnable.

    Correct Answer: IllegalThreadStateException is thrown.

  4. to await for the thread to complete, asynchronous.

    join() tells the main thread to wait until t1 finishes

  5. gets Worker-1, the name assigned during thread creation.

Bonus Snippet


import java.util.*;

public class ListTrap {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>(Arrays.asList("apple", "banana", "cherry", "banana"));

        for (String item : items) {
            if (item.equals("banana")) {
                items.remove(item);
            }
        }

        System.out.println(items);
    }
}

Questions:

  1. Will this code compile and run? If not, what exception (if any) is thrown?

  2. What’s the issue here?

  3. How would you fix this properly? Give 2 ways.

  4. Why doesn’t this happen in a for loop with indices?

Answers:

  1. It will compile, but at runtime it throws: CopyEditjava.util.ConcurrentModificationException

  2. The loop uses the enhanced for-loop, which under the hood uses an Iterator.

    The moment you modify the list directly (items.remove(item)) while iterating, you violate the fail-fast behavior of ArrayList's iterator.

    Even though there’s no actual thread concurrency, the internal iterator detects that the list has changed during iteration.

  3. (a) Use Iterator and iterator.remove()Safe removal during iteration

     Iterator<String> iterator = items.iterator();
     while (iterator.hasNext()) {
         String item = iterator.next();
         if (item.equals("banana")) {
             iterator.remove(); // ✅ safe
         }
     }
    

    (b) Use removeIf() method (Java 8+) (ideal, modern way)

     items.removeIf(item -> item.equals("banana"));
    
  4. Because you're not using an iterator — you're just indexing into the list manually.

    But if you don’t i--, you might skip elements because the list shrinks.

     for (int i = 0; i < items.size(); i++) {
         if (items.get(i).equals("banana")) {
             items.remove(i);
             i--; // 👈 prevent skipping next element
         }
     }
    

That’s it for our revision, if the need arises, I’ll add another revision blog on the leftover topics!
Stay tuned, everyone!

0
Subscribe to my newsletter

Read articles from Nagraj Math directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nagraj Math
Nagraj Math