W11. Interface & Implementation, Final Methods & Classes, Interfaces, Enumerations, Java Collections Framework

Author

Eugene Zouev, Munir Makhmutov

Published

November 13, 2025

1. Summary

1.1 Interface & Implementation Design

When designing object-oriented programs, a critical question arises: what should be inherited from a base class—just the interface (method signatures) or both the interface and its implementation (actual code)? This distinction is crucial for writing maintainable, bug-resistant code.

1.1.1 The Problem with Inheriting Implementation

Consider a scenario where multiple airplane models need to implement a fly() method. If we create a concrete base class Airplane with a default fly() implementation, all subclasses automatically inherit that implementation:

class Airplane {
    public void fly() {
        // Standard Airbus flying algorithm
    }
}

class AirbusA extends Airplane { }
class AirbusB extends Airplane { }
class Boeing extends Airplane { }

This creates a serious problem: Boeing now uses Airbus’s flying algorithm! The compiler won’t detect this error because the code is syntactically correct. The bug is semantic—the wrong behavior is inherited silently.

1.1.2 The Solution: Separate Interface from Implementation

The proper approach is to separate the interface from the implementation using abstract classes and methods:

abstract class Airplane {
    public abstract void fly();  // Interface only
    
    protected void defaultFly() {  // Optional default implementation
        // Standard flying algorithm
    }
}

class AirbusA extends Airplane {
    public void fly() { defaultFly(); }  // Uses default
}

class Boeing extends Airplane {
    public void fly() {
        // Boeing's own flying algorithm
    }
}

Now the compiler forces each subclass to explicitly implement fly(). Airbus models can choose to use the default algorithm, while Boeing must provide its own. This prevents silent inheritance of incorrect behavior.

1.1.3 Design Guidelines for Base Classes

When designing a base class, follow these principles:

  1. Interface only: Make the method abstract if you want subclasses to provide their own implementation. Hide any default implementation in a separate helper method (like defaultFly()).
  2. Interface and implementation: Make the method virtual (explicitly in C++/C#, implicitly in Java) if you want to provide a default implementation that subclasses can optionally override.
  3. Fixed behavior: Make the method non-virtual (C++/C#) or final (Java) if subclasses should not modify the behavior.
1.2 Final Methods

Final methods are methods that cannot be overridden by subclasses. They provide two key benefits: guaranteed behavior and performance optimization.

1.2.1 Preventing Method Overriding

The final keyword prevents subclasses from changing a method’s behavior:

class Base {
    public final void method() {
        System.out.println("Base's method");
    }
}

class Derived extends Base {
    public void method() {  // ERROR: Cannot override final method
        System.out.println("Derived's method");
    }
}

This ensures that method() will always behave exactly as defined in Base, regardless of the dynamic type of the object.

1.2.2 Performance Benefits

Final methods enable early binding (static dispatch) instead of late binding (dynamic dispatch):

  • Late binding: The method to call is determined at runtime based on the object’s dynamic type. This requires extra overhead (virtual method table lookup).
  • Early binding: The method to call is determined at compile time based on the static type. This is faster.

When a method is final, the compiler knows it cannot be overridden, so it can:

  1. Generate more efficient code for method calls
  2. Inline the method body directly into the caller, eliminating method call overhead entirely for small methods
1.3 Final Classes

A final class cannot be subclassed (inherited from). This is useful when you want to prevent extension of a class entirely.

final class Base {
    // Class implementation
}

class Derived extends Base {  // ERROR: Cannot subclass final class
}

Key properties:

  • Declaring a class as final implicitly makes all its methods final
  • It’s illegal to declare a class as both abstract and final (this would be contradictory—an abstract class is incomplete and must be extended)
1.4 Interfaces

An interface is a pure abstraction that defines a contract of behavior without any implementation. Interfaces represent an alternative way of thinking about object-oriented design: focusing on what objects can do rather than what objects are.

1.4.1 Two Views of the World
  • Class-based approach: The world consists of objects with state and relationships. Emphasis on “what things are.”
  • Interface-based approach: All entities are defined by their behavior (what they can do). Emphasis on “what things can do.”

Interfaces embody the interface-based approach and provide a cleaner alternative to multiple inheritance.

1.4.2 Defining Interfaces

An interface declares method signatures without implementations:

interface Features {
    int numOfLegs();
    boolean canFly();
    boolean canSwim();
}

Properties of interfaces:

  • No method bodies (classes provide implementations)
  • All methods are implicitly public and abstract
  • Cannot be instantiated (no new operator)
  • Cannot have instance variables (only constants)
  • Represents a contract: “any class implementing this interface must provide these methods”
1.4.3 Implementing Interfaces

A class implements an interface using the implements keyword:

class Lion implements Features {
    public int numOfLegs() { return 4; }
    public boolean canFly() { return false; }
    public boolean canSwim() { return true; }
}

Features f = new Lion();  // Treat Lion as a set of features
if (f.canFly()) { /* ... */ }

The class must implement all methods declared in the interface, or the code won’t compile.

1.4.4 Multiple Interfaces

A class can implement multiple interfaces, providing a safer alternative to multiple inheritance:

class Person implements iBodyParams, iSkills, iRelations {
    // Must implement all methods from all three interfaces
}

Person john = new Person();
iSkills johnsSkills = john;  // View John as a set of skills

This allows objects to be viewed from different perspectives (facets) depending on the context.

1.4.5 Interface Inheritance

Interfaces can extend other interfaces:

interface SpeedFeatures {
    float maxSpeed();
    float maxAcceleration();
}

interface EngineFeatures extends SpeedFeatures {
    float numOfCyls();
    float enginePower();
}

class Car implements EngineFeatures {
    // Must implement methods from both interfaces
}

Classes can also inherit interface implementations:

interface HasLegs {
    int noLegs();
}

class Mammal implements HasLegs {
    public int noLegs() { return 4; }
}

class Lion extends Mammal {
    // Inherits the noLegs() implementation
}
1.4.6 Interfaces with Inheritance

Interfaces and class inheritance can be combined:

interface ColorFeatures {
    Color color();
    Border border();
}

abstract class Shape {
    abstract void draw();
}

class Rectangle extends Shape {
    void draw() { /* ... */ }
}

class ColoredRectangle extends Rectangle implements ColorFeatures {
    // Inherits from Rectangle AND implements ColorFeatures
    public Color color() { /* ... */ }
    public Border border() { /* ... */ }
}

Interfaces are orthogonal to the inheritance mechanism—they provide a separate dimension of abstraction.

1.4.7 Type Checks with Interfaces

The instanceof operator works with interfaces:

interface Printable { void print(); }
interface Movable { void move(); }

class Rectangle extends Shape implements Printable, Movable {
    // Implementation
}

Shape a = new Rectangle();
if (a instanceof Printable) {
    ((Printable)a).print();
}
if (a instanceof Movable) {
    ((Movable)a).move();
}

Interfaces can even be empty, serving as “tags” or markers to identify objects with certain properties.

1.4.8 Nested Interfaces

Interfaces can be nested inside classes:

class SomeClass {
    public interface Nested {
        boolean isNotNegative(int x);
    }
}

class MyClass implements SomeClass.Nested {
    public boolean isNotNegative(int x) {
        return x >= 0;
    }
}

SomeClass.Nested obj = new MyClass();
1.4.9 Interfaces as Facets

Interfaces can be viewed as different facets or views of an object, representing different clients’ perspectives on the same underlying object. Each interface provides a specific view tailored to a particular use case.

1.5 Interfaces vs Abstract Classes

Understanding the distinction between interfaces and abstract classes is fundamental:

Similarities:

  • Both represent abstractions
  • Neither can be instantiated directly

Differences:

  • Interface: Pure abstraction of behavior. Defines what an object can do, but not how or what state it has. (Note: Modern Java allows default methods and constants, but the conceptual purpose remains behavioral abstraction.)
  • Abstract class: Can contain:
    • Abstract method declarations (interface)
    • Concrete method implementations (behavior)
    • Instance variables (state)

When to use which:

  • Use interfaces when you want to define a capability or behavior that unrelated classes might share
  • Use abstract classes when you want to provide common state and behavior for closely related classes
1.6 Enumerations

Enumerations (enums) are a special type used to define a fixed set of named constants. They make code more readable and type-safe compared to using arbitrary integer constants.

1.6.1 Why Enumerations?

Consider modeling traffic light states. The conventional approach uses integers:

final int GREEN = 0;
final int YELLOW = 1;
final int RED = 2;

int trafficLight = GREEN;
trafficLight = 777;  // Nothing prevents this!

The problem: why these specific numbers? What prevents invalid values like 777?

The enumeration approach:

enum Lights {
    GREEN,
    YELLOW,
    RED
}

Lights tl = Lights.GREEN;
tl = 777;  // Compiler ERROR: type mismatch

Now the compiler enforces type safety. You cannot assign arbitrary values to tl.

1.6.2 Basic Enumerations

Enumerations define a set of named constants (called enumerators):

enum Compass {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

Note: Enumerator names are traditionally written in UPPERCASE (a convention from C and assembly language, not a requirement).

Behind the scenes, enumerators are associated with integer values starting from 0 (SUNDAY = 0, MONDAY = 1, etc.), but you generally don’t need to know or use these values.

1.6.3 Using Enumerations

Enumerations work naturally with switch statements:

public void tellItLikeItIs(Day day) {
    switch (day) {
        case MONDAY:
            System.out.println("Mondays are bad.");
            break;
        case FRIDAY:
            System.out.println("Fridays are better.");
            break;
        default:
            System.out.println("Midweek days are so-so.");
            break;
    }
}
1.6.4 Java Enum Extensions

Java enums are much more powerful than enums in languages like C or C++. In Java, enums are actually classes! This enables advanced features:

1. Enums can have fields and constructors:

enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    
    private final int value;
    
    Coin(int value) {
        this.value = value;
    }
}

Coin c = Coin.DIME;  // c is associated with value 10

Each enumerator can be initialized with specific values. The constructor is called automatically when the enum is loaded.

2. Enums can have methods:

enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    
    private final int value;
    
    Coin(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

Coin c = Coin.DIME;
int v = c.getValue();  // Returns 10

3. Enum predefined methods:

All enums automatically have these methods:

  • values(): Returns an array of all enum constants in declaration order
enum Season { WINTER, SPRING, SUMMER, FALL }

for (Season s : Season.values()) {
    System.out.println(s);
}
// Output: WINTER, SPRING, SUMMER, FALL
  • ordinal(): Returns the position of the constant (starting from 0)
for (Season s : Season.values()) {
    System.out.println(s.ordinal());
}
// Output: 0, 1, 2, 3

4. Enum members can have bodies:

Each enum constant can override methods:

enum Operation {
    PLUS { double eval(double x, double y) { return x + y; } },
    MINUS { double eval(double x, double y) { return x - y; } },
    TIMES { double eval(double x, double y) { return x * y; } },
    DIVIDE { double eval(double x, double y) { return x / y; } };
    
    abstract double eval(double x, double y);
}

// Usage:
for (Operation op : Operation.values()) {
    System.out.println("2.0 " + op + " 4.0 = " + op.eval(2.0, 4.0));
}
// Output:
// 2.0 PLUS 4.0 = 6.0
// 2.0 MINUS 4.0 = -2.0
// 2.0 TIMES 4.0 = 8.0
// 2.0 DIVIDE 4.0 = 0.5

5. Enum constructor behavior:

Enum constructors are called automatically when the enum is first loaded:

enum Color {
    RED, GREEN, BLUE;
    
    private Color() {
        System.out.println("Constructor called for: " + this.toString());
    }
}

Color c = Color.RED;
// Output:
// Constructor called for: RED
// Constructor called for: GREEN
// Constructor called for: BLUE

All enum constants are constructed when the enum is first accessed.

1.7 Java Collections Framework

The Java Collections Framework provides a unified architecture for storing and manipulating groups of objects. It relieves programmers from implementing common data structures from scratch and provides powerful, standardized abstractions.

Collections can store any reference type (objects). They provide dynamic sizing, eliminating the need to manually manage array sizes.

1.7.1 Core Collection Interfaces

There are three main collection types:

1. List: An ordered collection where elements are accessed by index (position). Lists allow duplicate elements.

2. Set: An unordered collection of unique elements. Duplicates are automatically ignored. At most one null element is allowed.

3. Map: A collection that maps keys to values. Each key can map to at most one value, but multiple keys can map to the same value.

1.7.2 Collection Hierarchy

The Collections Framework has a clear inheritance hierarchy:

  • Collection (root interface)
    • List (ordered, indexed)
      • ArrayList (resizable array)
      • LinkedList (doubly-linked list)
      • Vector (synchronized array)
        • Stack (LIFO stack)
    • Set (unique elements)
      • HashSet (hash table)
      • LinkedHashSet (hash table + insertion order)
      • SortedSet (sorted set)
        • TreeSet (red-black tree)
    • Queue (FIFO queue)
      • Deque (double-ended queue)
        • LinkedList (also implements Deque)
  • Map (separate hierarchy, not part of Collection)
    • HashMap (hash table)
    • LinkedHashMap (hash table + insertion order)
    • Hashtable (legacy synchronized hash table)
    • SortedMap (sorted map)
      • TreeMap (red-black tree)
1.7.3 Lists

A List is an ordered collection where elements are stored in the order they are added. Elements are accessed by integer index.

Creating and using a List:

import java.util.List;
import java.util.ArrayList;

List<String> list = new ArrayList<>();
list.add("Geeks");
list.add("Geeks");
list.add(1, "For");  // Insert at index 1
System.out.println(list);  // [Geeks, For, Geeks]

Common List methods:

  • int size(): Returns number of elements
  • boolean isEmpty(): Checks if list is empty
  • void clear(): Removes all elements
  • boolean add(E element): Appends element to end
  • void add(int index, E element): Inserts element at index
  • E get(int index): Returns element at index
  • E set(int index, E element): Replaces element at index
  • E remove(int index): Removes element at index
  • boolean remove(Object o): Removes first occurrence of object

Key characteristics:

  • Allows duplicate elements
  • Maintains insertion order
  • Provides positional access
1.7.4 Sets

A Set is an unordered collection that cannot contain duplicate elements. Only one null element is allowed.

Creating and using a Set:

import java.util.Set;
import java.util.HashSet;

Set<String> set = new HashSet<>();
set.add("Geeks");
set.add("For");
set.add("Geeks");  // Duplicate, will be ignored
set.add("Example");
set.add("Set");
System.out.println(set);  // Order not guaranteed: [Geeks, Example, Set, For]

Common Set methods:

  • int size(): Returns number of elements
  • boolean isEmpty(): Checks if set is empty
  • void clear(): Removes all elements
  • boolean add(E element): Adds element (returns false if already present)
  • boolean remove(Object o): Removes element
  • boolean contains(Object o): Checks if element exists

Key characteristics:

  • No duplicate elements
  • No guaranteed order (unless using LinkedHashSet or TreeSet)
  • Fast membership testing

Set implementations comparison:

Implementation Element Ordering Complexity (find) Data Structure
HashSet No \(O(1)\) Hash table
LinkedHashSet Insertion order \(O(1)\) Hash table + linked list
TreeSet Sorted (ascending) \(O(\log n)\) Red-black tree
1.7.5 Maps

A Map stores key-value pairs. Each unique key maps to exactly one value. Keys must be unique, but values can be duplicated.

Creating and using a Map:

import java.util.Map;
import java.util.HashMap;

Map<String, Integer> map = new HashMap<>();
map.put("a", 100);
map.put("b", 200);
map.put("c", 300);
map.put("a", 150);  // Replaces previous value for key "a"

// Traversing the map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Common Map methods:

  • int size(): Returns number of key-value pairs
  • boolean isEmpty(): Checks if map is empty
  • void clear(): Removes all mappings
  • V put(K key, V value): Associates value with key (returns previous value)
  • V get(K key): Returns value for key (or null)
  • V remove(K key): Removes mapping for key
  • boolean containsKey(K key): Checks if key exists
  • boolean containsValue(V value): Checks if value exists
  • Set<K> keySet(): Returns set of all keys
  • Collection<V> values(): Returns collection of all values
  • Set<Map.Entry<K,V>> entrySet(): Returns set of key-value pairs

Key characteristics:

  • Each key maps to at most one value
  • Keys must be unique
  • Values can be duplicated
  • Fast lookup by key

Map implementations comparison:

Implementation Element Ordering Null as Value Thread Safe Complexity (find) Data Structure
HashMap No Yes No \(O(1)\) Hash table
Hashtable No No Yes \(O(1)\) Hash table
TreeMap Sorted by key Yes/No* No \(O(\log n)\) Red-black tree

*TreeMap allows null values but not null keys

List vs Set vs Map comparison:

List Set Map
Index-based access Unordered collection Stores key-value pairs
Allows duplicates No duplicates Keys are unique
Maintains insertion order Duplicate additions ignored Look up values by key
1.7.6 Iterator

An Iterator is an object used to loop through collections. It provides a uniform way to traverse any collection type.

Using an Iterator:

import java.util.Iterator;
import java.util.HashSet;
import java.util.Set;

Set<Integer> set = new HashSet<>();
set.add(5);
set.add(1);
set.add(3);

Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
    Integer value = iterator.next();
    System.out.print(value + " ");
}

Iterator methods:

  • boolean hasNext(): Returns true if more elements exist
  • E next(): Returns the next element
  • void remove(): Removes the last element returned by next()

Example with ArrayList:

import java.util.ArrayList;
import java.util.Iterator;

ArrayList<String> cars = new ArrayList<>();
cars.add("Volvo");
cars.add("BMW");
cars.add("Ford");
cars.add("Mazda");

Iterator<String> it = cars.iterator();
System.out.println(it.next());  // Volvo

The iterator() method can be called on any collection implementing the Collection interface.

1.8 UML Class Diagrams

UML (Unified Modeling Language) Class Diagrams provide a visual representation of class structure and relationships in object-oriented systems. They are essential for planning and communicating system design before implementation.

1.8.1 Class Representation

A class is represented as a rectangle divided into three sections:

  1. Top section: Class name
  2. Middle section: Attributes (fields) with visibility and type
  3. Bottom section: Methods with visibility, parameters, and return type

Visibility indicators:

  • + : public
  • - : private
  • # : protected
  • ~ : package-private
1.8.2 Relationships

1. Association: A general relationship between classes (one class uses another)

  • Represented by a solid line
  • Shows multiplicity: 1, *, 0..1, 1..*, etc.

2. Navigable Association: Indicates direction of relationship

  • Solid line with an arrow

3. Inheritance (Generalization): “is-a” relationship

  • Solid line with hollow triangle pointing to parent class

4. Realization/Implementation: A class implements an interface

  • Dashed line with hollow triangle pointing to interface

5. Dependency: One class depends on another (weaker than association)

  • Dashed line with arrow

6. Aggregation: “has-a” relationship where the part can exist independently

  • Solid line with hollow diamond on the container side

7. Composition: “has-a” relationship where the part cannot exist independently

  • Solid line with filled diamond on the container side

Example:

University (1) ---- (1..*) Department
Department (1) <>---- (1..*) Professor  [aggregation]
Professor ---|> Person  [inheritance]

This shows:

  • A University has many Departments
  • A Department aggregates many Professors
  • Professor inherits from Person

2. Definitions

  • Interface (concept): The public method signatures that a class exposes, defining what operations can be performed on objects of that class.
  • Implementation: The actual code that defines how methods work, including algorithms, data structures, and internal logic.
  • Abstract Method: A method declared without a body (implementation), which must be implemented by subclasses.
  • Abstract Class: A class that cannot be instantiated and may contain abstract methods. Used as a base class to define common interface and partial implementation.
  • Final Method: A method that cannot be overridden by subclasses, declared with the final keyword in Java.
  • Final Class: A class that cannot be extended (subclassed), declared with the final keyword.
  • Late Binding (Dynamic Dispatch): The process of determining which method to call at runtime based on the dynamic type of the object. Used for virtual/overridable methods.
  • Early Binding (Static Dispatch): The process of determining which method to call at compile time based on the static type. Used for non-virtual methods, final methods, and static methods.
  • Method Inlining: A compiler optimization where the method body is inserted directly at the call site, eliminating method call overhead. Possible with final methods.
  • Interface (language construct): A special type in Java that defines a contract of method signatures without implementation. Classes can implement multiple interfaces.
  • Interface Implementation: The process by which a class provides concrete implementations for all methods declared in an interface using the implements keyword.
  • Multiple Interface Implementation: The ability of a single class to implement multiple interfaces, providing a safer alternative to multiple inheritance.
  • Interface Inheritance: When one interface extends another interface, inheriting all method declarations.
  • Facet: A view or perspective on an object, often represented by an interface, showing only certain aspects of the object’s capabilities.
  • Empty Interface (Marker Interface): An interface with no methods, used as a “tag” to mark classes with certain properties.
  • Nested Interface: An interface defined inside a class or another interface.
  • Ad-hoc Polymorphism (Duck Typing): The ability to treat an object as a certain type if it has the required methods, regardless of explicit type declarations. Not directly supported in Java.
  • Enumeration (Enum): A special type that defines a fixed set of named constants (enumerators), providing type safety for categorical values.
  • Enumerator: A named constant within an enumeration.
  • Enum Constructor: A special constructor in Java enums that initializes each enum constant with specific values.
  • Enum Methods: Methods defined in an enum to provide behavior associated with enum constants.
  • Collection: A data structure that stores a group of objects, providing operations for adding, removing, and accessing elements.
  • List: An ordered collection interface where elements are accessed by integer index. Allows duplicates and maintains insertion order.
  • Set: An unordered collection interface that contains no duplicate elements. At most one null element is allowed.
  • Map: An interface for storing key-value pairs where each unique key maps to exactly one value.
  • ArrayList: A resizable-array implementation of the List interface, providing fast random access.
  • LinkedList: A doubly-linked list implementation of List and Deque interfaces, providing efficient insertion and deletion.
  • HashSet: A hash table implementation of the Set interface, providing constant-time performance for basic operations.
  • HashMap: A hash table implementation of the Map interface, providing constant-time performance for basic operations.
  • TreeSet: A red-black tree implementation of Set that maintains elements in sorted ascending order.
  • TreeMap: A red-black tree implementation of Map that maintains keys in sorted order.
  • Iterator: An object that enables traversal through a collection, providing methods to check for more elements and retrieve the next element.
  • Generic Type Parameter: A placeholder for a specific type (like <E>, <K,V>) that allows collections to be type-safe at compile time.
  • Entry (Map.Entry): A key-value pair stored in a Map, accessible when iterating through a Map’s entrySet.
  • UML (Unified Modeling Language): A standardized visual language for modeling software systems.
  • UML Class Diagram: A type of UML diagram that shows classes, their attributes, methods, and relationships between classes.
  • Association (UML): A relationship where one class uses or interacts with another class.
  • Aggregation (UML): A “has-a” relationship where the part can exist independently of the whole, shown with a hollow diamond.
  • Composition (UML): A “has-a” relationship where the part cannot exist independently of the whole, shown with a filled diamond.
  • Multiplicity (UML): Notation showing how many instances of one class relate to instances of another class (e.g., 1, *, 0..1, 1..*).

3. Examples

3.1. Enum Constructor Behavior (Lab 10, Task 1)

Demonstrate when enum constructors are called.

Click to see the solution

Key Concept: Enum constructors are called automatically when the enum is first accessed, once for each constant.

enum Color {
    RED,
    GREEN,
    BLUE;
    
    private Color() {
        System.out.println("Constructor called for: " + this.toString());
    }
    
    public void colorInfo() {
        System.out.println("Universal Color");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println("Before accessing enum");
        
        Color c1 = Color.RED;
        System.out.println(c1);
        c1.colorInfo();
        
        System.out.println("\nAccessing another color:");
        Color c2 = Color.GREEN;
        System.out.println(c2);
    }
}

Output:

Before accessing enum
Constructor called for: RED
Constructor called for: GREEN
Constructor called for: BLUE
RED
Universal Color

Accessing another color:
GREEN

Explanation: All enum constants are constructed when the enum type is first accessed, not individually when each constant is used.

Answer: The enum constructor is called once for each constant when the enum class is loaded, which happens on first access to any enum constant or method.

3.2. Animals List Program (Lab 10, Task 3)

Write a program to create a List of animals with methods for adding, removing, updating, and displaying.

Click to see the solution

Key Concept: Encapsulate List operations in methods for better organization and reusability.

import java.util.ArrayList;
import java.util.List;

public class AnimalManager {
    private List<String> animals;
    
    public AnimalManager() {
        animals = new ArrayList<>();
    }
    
    // Method to add an animal
    public void addAnimal(String animal) {
        animals.add(animal);
        System.out.println("Added: " + animal);
    }
    
    // Method to remove an animal
    public void removeAnimal(String animal) {
        if (animals.remove(animal)) {
            System.out.println("Removed: " + animal);
        } else {
            System.out.println("Animal not found: " + animal);
        }
    }
    
    // Method to update an animal at a specific index
    public void updateAnimal(int index, String newAnimal) {
        if (index >= 0 && index < animals.size()) {
            String old = animals.set(index, newAnimal);
            System.out.println("Updated: " + old + " -> " + newAnimal);
        } else {
            System.out.println("Invalid index: " + index);
        }
    }
    
    // Method to display all animals
    public void displayAnimals() {
        System.out.println("\nCurrent animals:");
        if (animals.isEmpty()) {
            System.out.println("  (no animals)");
        } else {
            for (int i = 0; i < animals.size(); i++) {
                System.out.println("  " + i + ": " + animals.get(i));
            }
        }
    }
    
    public static void main(String[] args) {
        AnimalManager manager = new AnimalManager();
        
        // Adding animals
        manager.addAnimal("Lion");
        manager.addAnimal("Tiger");
        manager.addAnimal("Bear");
        manager.addAnimal("Elephant");
        manager.displayAnimals();
        
        // Updating an animal
        manager.updateAnimal(1, "Leopard");
        manager.displayAnimals();
        
        // Removing an animal
        manager.removeAnimal("Bear");
        manager.displayAnimals();
        
        // Try removing non-existent animal
        manager.removeAnimal("Giraffe");
    }
}

Output:

Added: Lion
Added: Tiger
Added: Bear
Added: Elephant

Current animals:
  0: Lion
  1: Tiger
  2: Bear
  3: Elephant
Updated: Tiger -> Leopard

Current animals:
  0: Lion
  1: Leopard
  2: Bear
  3: Elephant
Removed: Bear

Current animals:
  0: Lion
  1: Leopard
  2: Elephant
Animal not found: Giraffe

Answer: The program encapsulates four key operations on a List: add, remove, update, and display. Each method provides appropriate feedback and error handling.

3.3. Remove Odd-Length Strings from Set (Lab 10, Task 4)

Write a program that creates a Set with strings and removes all elements with odd length, leaving only even-length elements.

Click to see the solution

Key Concept: When removing elements while iterating, use an Iterator to avoid ConcurrentModificationException.

import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;

public class StringSetFilter {
    public static void main(String[] args) {
        // Create a Set with strings
        Set<String> strings = new HashSet<>();
        strings.add("cat");        // length 3 (odd)
        strings.add("dog");        // length 3 (odd)
        strings.add("bird");       // length 4 (even)
        strings.add("fish");       // length 4 (even)
        strings.add("elephant");   // length 8 (even)
        strings.add("tiger");      // length 5 (odd)
        strings.add("bear");       // length 4 (even)
        
        System.out.println("Original set: " + strings);
        
        // Remove odd-length strings using Iterator
        Iterator<String> it = strings.iterator();
        while (it.hasNext()) {
            String str = it.next();
            if (str.length() % 2 != 0) {  // Odd length
                System.out.println("Removing: " + str + " (length " + str.length() + ")");
                it.remove();  // Safe removal using iterator
            }
        }
        
        System.out.println("Final set (even lengths only): " + strings);
    }
}

Possible Output:

Original set: [cat, bear, bird, fish, dog, elephant, tiger]
Removing: cat (length 3)
Removing: dog (length 3)
Removing: tiger (length 5)
Final set (even lengths only): [bear, bird, fish, elephant]

Note: Order may vary since HashSet is unordered.

Alternative solution using removeIf (Java 8+):

import java.util.Set;
import java.util.HashSet;

public class StringSetFilterLambda {
    public static void main(String[] args) {
        Set<String> strings = new HashSet<>();
        strings.add("cat");
        strings.add("dog");
        strings.add("bird");
        strings.add("fish");
        strings.add("elephant");
        strings.add("tiger");
        strings.add("bear");
        
        System.out.println("Original set: " + strings);
        
        // Remove odd-length strings using removeIf
        strings.removeIf(str -> str.length() % 2 != 0);
        
        System.out.println("Final set (even lengths only): " + strings);
    }
}

Answer: We use an Iterator to safely remove elements while iterating. Calling remove() on the iterator removes the current element without causing a ConcurrentModificationException. Alternatively, Java 8’s removeIf() method provides a more concise solution.

3.4. Map with Repetitive Value Counter (Lab 10, Task 5)

Write a program that creates a Map<String, Integer> from user input and reports if there are repetitive values and their count.

Click to see the solution

Key Concept: Use a Map to count occurrences, then analyze the values to find repetitions.

import java.util.Map;
import java.util.HashMap;
import java.util.Scanner;

public class RepetitiveValueCounter {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Map<String, Integer> wordCount = new HashMap<>();
        
        System.out.println("Enter words (type 'done' to finish):");
        
        // Read input and build the map
        while (true) {
            String word = scanner.next();
            if (word.equalsIgnoreCase("done")) {
                break;
            }
            
            // Count occurrences
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }
        
        System.out.println("\nWord counts:");
        for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // Find and report repetitive values
        System.out.println("\nAnalysis:");
        boolean hasRepetitions = false;
        
        for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
            if (entry.getValue() > 1) {
                hasRepetitions = true;
                System.out.println("'" + entry.getKey() + "' appears " + 
                                 entry.getValue() + " times (repetitive)");
            }
        }
        
        if (!hasRepetitions) {
            System.out.println("No repetitive values found.");
        }
        
        scanner.close();
    }
}

Example run:

Enter words (type 'done' to finish):
apple banana apple orange banana apple done

Word counts:
apple: 3
banana: 2
orange: 1

Analysis:
'apple' appears 3 times (repetitive)
'banana' appears 2 times (repetitive)

Alternative interpretation (checking if different keys map to same value):

import java.util.Map;
import java.util.HashMap;
import java.util.Scanner;
import java.util.List;
import java.util.ArrayList;

public class RepetitiveValueChecker {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Map<String, Integer> map = new HashMap<>();
        
        System.out.println("Enter key-value pairs (format: key value)");
        System.out.println("Type 'done' as key to finish:");
        
        // Read key-value pairs
        while (true) {
            System.out.print("Key: ");
            String key = scanner.next();
            if (key.equalsIgnoreCase("done")) {
                break;
            }
            System.out.print("Value: ");
            int value = scanner.nextInt();
            map.put(key, value);
        }
        
        System.out.println("\nMap contents: " + map);
        
        // Check for repetitive values
        Map<Integer, List<String>> valueToKeys = new HashMap<>();
        
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            int value = entry.getValue();
            valueToKeys.putIfAbsent(value, new ArrayList<>());
            valueToKeys.get(value).add(entry.getKey());
        }
        
        System.out.println("\nAnalysis:");
        boolean hasRepetitions = false;
        
        for (Map.Entry<Integer, List<String>> entry : valueToKeys.entrySet()) {
            if (entry.getValue().size() > 1) {
                hasRepetitions = true;
                System.out.println("Value " + entry.getKey() + 
                                 " is mapped by " + entry.getValue().size() + 
                                 " keys: " + entry.getValue());
            }
        }
        
        if (!hasRepetitions) {
            System.out.println("No repetitive values found.");
        }
        
        scanner.close();
    }
}

Example run:

Enter key-value pairs (format: key value)
Type 'done' as key to finish:
Key: apple
Value: 100
Key: banana
Value: 200
Key: orange
Value: 100
Key: grape
Value: 200
Key: done

Map contents: {apple=100, banana=200, orange=100, grape=200}

Analysis:
Value 100 is mapped by 2 keys: [apple, orange]
Value 200 is mapped by 2 keys: [banana, grape]

Answer: The first solution counts word occurrences using a Map. The second solution checks if multiple keys map to the same value by inverting the map structure.

3.5. Online Book Reader System (Lab 10, Homework 1)

Design and implement a basic online book reader system with the following requirements:

Functionality:

  • Searching the database of books and reading a book
  • User membership creation and extension
  • Only one active user at a time and only one active book by this user

Design Hints:

The OnlineReaderSystem class represents the main program. Instead of implementing everything in one class (which would make it too large), separate concerns into:

  • Library: Manages the collection of books
  • UserManager: Handles user management
  • Display: Manages the user interface
  • Book: Represents a book entity
  • User: Represents a user entity

Note: This exercise was asked in interviews at Amazon, Microsoft, and many other companies.

Click to see the solution

Key Concept: Use object-oriented design principles to separate concerns. Each class should have a single, well-defined responsibility.

Step 1: Design the UML class diagram

Before coding, plan the class structure and relationships:

  • OnlineReaderSystem aggregates Library, UserManager, and Display
  • Library contains a collection of Book objects
  • UserManager manages a collection of User objects
  • User has an association with Book (current book being read)

Step 2: Implementation

import java.util.*;

// Book class
class Book {
    private String id;
    private String title;
    private String author;
    private String content;
    
    public Book(String id, String title, String author, String content) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.content = content;
    }
    
    public String getId() { return id; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getContent() { return content; }
    
    @Override
    public String toString() {
        return title + " by " + author;
    }
}

// User class
class User {
    private String userId;
    private String name;
    private String membershipType;
    private Book currentBook;
    
    public User(String userId, String name, String membershipType) {
        this.userId = userId;
        this.name = name;
        this.membershipType = membershipType;
        this.currentBook = null;
    }
    
    public String getUserId() { return userId; }
    public String getName() { return name; }
    public String getMembershipType() { return membershipType; }
    public Book getCurrentBook() { return currentBook; }
    
    public void setCurrentBook(Book book) {
        this.currentBook = book;
    }
    
    public void extendMembership(String newType) {
        this.membershipType = newType;
        System.out.println("Membership extended to: " + newType);
    }
    
    @Override
    public String toString() {
        return name + " (ID: " + userId + ", " + membershipType + ")";
    }
}

// Library class - manages books
class Library {
    private Map<String, Book> books;
    
    public Library() {
        books = new HashMap<>();
    }
    
    public void addBook(Book book) {
        books.put(book.getId(), book);
        System.out.println("Added book: " + book);
    }
    
    public Book findBook(String id) {
        return books.get(id);
    }
    
    public List<Book> searchBooks(String keyword) {
        List<Book> results = new ArrayList<>();
        for (Book book : books.values()) {
            if (book.getTitle().toLowerCase().contains(keyword.toLowerCase()) ||
                book.getAuthor().toLowerCase().contains(keyword.toLowerCase())) {
                results.add(book);
            }
        }
        return results;
    }
    
    public void displayAllBooks() {
        System.out.println("\n=== Library Books ===");
        for (Book book : books.values()) {
            System.out.println(book);
        }
    }
}

// UserManager class - manages users
class UserManager {
    private Map<String, User> users;
    private User activeUser;
    
    public UserManager() {
        users = new HashMap<>();
        activeUser = null;
    }
    
    public User createUser(String userId, String name, String membershipType) {
        User user = new User(userId, name, membershipType);
        users.put(userId, user);
        System.out.println("User created: " + user);
        return user;
    }
    
    public User findUser(String userId) {
        return users.get(userId);
    }
    
    public boolean setActiveUser(String userId) {
        User user = users.get(userId);
        if (user != null) {
            activeUser = user;
            System.out.println("Active user set to: " + user);
            return true;
        }
        System.out.println("User not found: " + userId);
        return false;
    }
    
    public User getActiveUser() {
        return activeUser;
    }
    
    public void logoutActiveUser() {
        if (activeUser != null) {
            System.out.println("User logged out: " + activeUser);
            activeUser = null;
        }
    }
}

// Display class - manages display output
class Display {
    private Book activeBook;
    private User activeUser;
    
    public void displayBook(Book book) {
        if (book == null) {
            System.out.println("No book to display.");
            return;
        }
        activeBook = book;
        System.out.println("\n========== READING ==========");
        System.out.println("Title: " + book.getTitle());
        System.out.println("Author: " + book.getAuthor());
        System.out.println("-----------------------------");
        System.out.println(book.getContent());
        System.out.println("=============================\n");
    }
    
    public void displayUser(User user) {
        if (user == null) {
            System.out.println("No active user.");
            return;
        }
        activeUser = user;
        System.out.println("\n=== Active User ===");
        System.out.println(user);
        if (user.getCurrentBook() != null) {
            System.out.println("Currently reading: " + user.getCurrentBook());
        } else {
            System.out.println("Not reading any book.");
        }
        System.out.println("===================\n");
    }
    
    public void refreshDisplay(User user, Book book) {
        displayUser(user);
        if (book != null) {
            displayBook(book);
        }
    }
}

// Main OnlineReaderSystem class
class OnlineReaderSystem {
    private Library library;
    private UserManager userManager;
    private Display display;
    
    public OnlineReaderSystem() {
        library = new Library();
        userManager = new UserManager();
        display = new Display();
    }
    
    public Library getLibrary() { return library; }
    public UserManager getUserManager() { return userManager; }
    public Display getDisplay() { return display; }
    
    public void setActiveUserAndBook(String userId, String bookId) {
        User user = userManager.findUser(userId);
        if (user == null) {
            System.out.println("User not found.");
            return;
        }
        
        userManager.setActiveUser(userId);
        
        Book book = library.findBook(bookId);
        if (book == null) {
            System.out.println("Book not found.");
            return;
        }
        
        user.setCurrentBook(book);
        display.refreshDisplay(user, book);
    }
    
    public void searchAndDisplay(String keyword) {
        List<Book> results = library.searchBooks(keyword);
        System.out.println("\n=== Search Results for: " + keyword + " ===");
        if (results.isEmpty()) {
            System.out.println("No books found.");
        } else {
            for (Book book : results) {
                System.out.println(book);
            }
        }
    }
    
    public static void main(String[] args) {
        OnlineReaderSystem system = new OnlineReaderSystem();
        
        // Add books to library
        system.getLibrary().addBook(new Book("B001", "Java Programming", 
            "John Smith", "This is a comprehensive guide to Java..."));
        system.getLibrary().addBook(new Book("B002", "Data Structures", 
            "Jane Doe", "Learn about arrays, lists, trees..."));
        system.getLibrary().addBook(new Book("B003", "Design Patterns", 
            "Bob Johnson", "Explore common software design patterns..."));
        
        // Display all books
        system.getLibrary().displayAllBooks();
        
        // Create users
        system.getUserManager().createUser("U001", "Alice", "Premium");
        system.getUserManager().createUser("U002", "Bob", "Basic");
        
        // Set active user and read a book
        System.out.println("\n--- Alice starts reading ---");
        system.setActiveUserAndBook("U001", "B001");
        
        // Search for books
        system.searchAndDisplay("Data");
        
        // Extend membership
        User alice = system.getUserManager().findUser("U001");
        alice.extendMembership("Gold");
        
        // Logout
        system.getUserManager().logoutActiveUser();
        
        // Another user
        System.out.println("\n--- Bob starts reading ---");
        system.setActiveUserAndBook("U002", "B003");
    }
}

Output:

Added book: Java Programming by John Smith
Added book: Data Structures by Jane Doe
Added book: Design Patterns by Bob Johnson

=== Library Books ===
Java Programming by John Smith
Data Structures by Jane Doe
Design Patterns by Bob Johnson
User created: Alice (ID: U001, Premium)
User created: Bob (ID: U002, Basic)

--- Alice starts reading ---
Active user set to: Alice (ID: U001, Premium)

=== Active User ===
Alice (ID: U001, Premium)
Currently reading: Java Programming by John Smith
===================

========== READING ==========
Title: Java Programming
Author: John Smith
-----------------------------
This is a comprehensive guide to Java...
=============================

=== Search Results for: Data ===
Data Structures by Jane Doe
Membership extended to: Gold
User logged out: Alice (ID: U001, Gold)

--- Bob starts reading ---
Active user set to: Bob (ID: U002, Basic)

=== Active User ===
Bob (ID: U002, Basic)
Currently reading: Design Patterns by Bob Johnson
===================

========== READING ==========
Title: Design Patterns
Author: Bob Johnson
-----------------------------
Explore common software design patterns...
=============================

Design Principles Applied:

  1. Single Responsibility Principle: Each class has one clear purpose
    • Library: Book management
    • UserManager: User management
    • Display: UI concerns
    • OnlineReaderSystem: Coordination
  2. Encapsulation: Each class manages its own data with private fields and public methods
  3. Aggregation: OnlineReaderSystem contains but doesn’t own Library, UserManager, and Display
  4. Association: User is associated with Book (can exist independently)

Answer: This design separates concerns effectively, making the system maintainable and extensible. Each component can be modified independently without affecting others. The system enforces the constraint of one active user and one active book at a time through the UserManager and User classes.

3.6. Separating Interface from Implementation (Lecture 10, Example 1)

Consider an airplane hierarchy where different airplane models need different flying algorithms. Implement this correctly by separating interface from implementation.

Click to see the solution

Key Concept: Use abstract methods to force explicit implementation while providing optional default implementations through protected helper methods.

Incorrect approach (problem):

class Airplane {
    public void fly() {
        // Standard flying algorithm for Airbus
    }
}

class AirbusA extends Airplane { }  // Inherits Airbus algorithm
class AirbusB extends Airplane { }  // Inherits Airbus algorithm
class Boeing extends Airplane { }   // WRONG: Also inherits Airbus algorithm!

Problem: Boeing automatically inherits Airbus’s flying algorithm. The compiler doesn’t detect this semantic error.

Correct approach (solution):

abstract class Airplane {
    // Interface: must be implemented by all subclasses
    public abstract void fly();
    
    // Optional default implementation
    protected void defaultFly() {
        // Standard flying algorithm
        System.out.println("Standard flying algorithm");
    }
}

class AirbusA extends Airplane {
    public void fly() {
        defaultFly();  // Can choose to use default
    }
}

class AirbusB extends Airplane {
    public void fly() {
        defaultFly();  // Can choose to use default
    }
}

class Boeing extends Airplane {
    public void fly() {
        // MUST provide its own implementation
        System.out.println("Boeing's own flying algorithm");
    }
}

Usage:

Airplane a = new AirbusA();
a.fly();  // Standard flying algorithm

Airplane b = new AirbusB();
b.fly();  // Standard flying algorithm

Airplane c = new Boeing();
c.fly();  // Boeing's own flying algorithm

Answer: By making fly() abstract, we force each subclass to explicitly implement it, preventing accidental inheritance of wrong behavior. The defaultFly() helper method provides an optional default implementation.

3.7. Using Final Methods (Lecture 10, Example 2)

Create a base class with a final method and demonstrate that it cannot be overridden.

Click to see the solution

Key Concept: The final keyword on a method prevents subclasses from overriding it, ensuring consistent behavior.

class Base {
    public final void method() {
        System.out.println("Base's method");
    }
}

class Derived extends Base {
    // This would cause a compilation error:
    // public void method() {
    //     System.out.println("Derived's method");
    // }
    // Error: Cannot override the final method from Base
}

Testing the code:

Base b1 = new Base();
b1.method();  // Output: Base's method

Base b2 = new Derived();
b2.method();  // Output: Base's method (same implementation)

Answer: Final methods guarantee that the behavior defined in the base class cannot be changed by subclasses, enabling compiler optimizations and ensuring consistent behavior.

3.8. Creating a Final Class (Lecture 10, Example 3)

Demonstrate creating a final class and show that it cannot be extended.

Click to see the solution
final class Base {
    public void method() {
        System.out.println("Base's method");
    }
}

// This would cause a compilation error:
// class Derived extends Base {
// }
// Error: The type Derived cannot subclass the final class Base

Why can’t a class be both abstract and final?

// This is illegal and won't compile:
// abstract final class InvalidClass {
//     abstract void method();
// }

Explanation: An abstract class is incomplete by design—it requires subclasses to provide implementations. A final class forbids subclasses. These two concepts are contradictory, so using both is illegal.

Answer: Final classes prevent inheritance entirely. All methods in a final class are implicitly final. A class cannot be both abstract and final because abstract classes require inheritance to be complete.

3.9. Implementing an Interface (Lecture 10, Example 4)

Create an interface Features and implement it in a Lion class.

Click to see the solution

Key Concept: An interface defines a contract. Any class implementing it must provide implementations for all declared methods.

interface Features {
    int numOfLegs();
    boolean canFly();
    boolean canSwim();
}

class Lion implements Features {
    public int numOfLegs() {
        return 4;
    }
    
    public boolean canFly() {
        return false;
    }
    
    public boolean canSwim() {
        return true;
    }
}

Usage:

// Can treat Lion as a Features interface
Features f = new Lion();

System.out.println("Number of legs: " + f.numOfLegs());

if (f.canFly()) {
    System.out.println("Can fly");
} else {
    System.out.println("Cannot fly");
}

if (f.canSwim()) {
    System.out.println("Can swim");
}

Output:

Number of legs: 4
Cannot fly
Can swim

Answer: The Lion class implements the Features interface by providing concrete implementations for all three methods. We can then treat a Lion object as a Features reference.

3.10. Multiple Interface Implementation (Lecture 10, Example 5)

Create a Person class that implements multiple interfaces representing different aspects of a person.

Click to see the solution

Key Concept: A class can implement multiple interfaces, providing a safe alternative to multiple inheritance.

interface iBodyParams {
    int getHeight();
    int getWeight();
}

interface iSkills {
    String getProgrammingLanguage();
    int getExperienceYears();
}

interface iRelations {
    String getFriendName();
}

class Person implements iBodyParams, iSkills, iRelations {
    private int height;
    private int weight;
    private String programmingLanguage;
    private int experienceYears;
    private String friendName;
    
    public Person(int height, int weight, String language, 
                  int experience, String friend) {
        this.height = height;
        this.weight = weight;
        this.programmingLanguage = language;
        this.experienceYears = experience;
        this.friendName = friend;
    }
    
    // iBodyParams implementation
    public int getHeight() { return height; }
    public int getWeight() { return weight; }
    
    // iSkills implementation
    public String getProgrammingLanguage() { return programmingLanguage; }
    public int getExperienceYears() { return experienceYears; }
    
    // iRelations implementation
    public String getFriendName() { return friendName; }
}

Usage (viewing from different perspectives):

Person john = new Person(180, 75, "Java", 5, "Alice");

// View John as a set of skills
iSkills johnsSkills = john;
System.out.println("Programming language: " + johnsSkills.getProgrammingLanguage());
System.out.println("Experience: " + johnsSkills.getExperienceYears() + " years");

// View John's body parameters
iBodyParams johnsBody = john;
System.out.println("Height: " + johnsBody.getHeight() + " cm");

// View John's relationships
iRelations johnsRelations = john;
System.out.println("Friend: " + johnsRelations.getFriendName());

Answer: A Person can be viewed through different interfaces depending on the context. Each interface represents a “facet” or perspective on the person.

3.11. Interface Inheritance (Lecture 10, Example 6)

Demonstrate how interfaces can extend other interfaces and how this affects implementing classes.

Click to see the solution

Key Concept: An interface can extend another interface, inheriting all its method declarations.

interface SpeedFeatures {
    float maxSpeed();
    float maxAcceleration();
}

interface EngineFeatures extends SpeedFeatures {
    float numOfCyls();
    float enginePower();
}

class Car implements EngineFeatures {
    // Must implement ALL methods from both interfaces
    
    // From SpeedFeatures
    public float maxSpeed() {
        return 200.0f;
    }
    
    public float maxAcceleration() {
        return 5.5f;
    }
    
    // From EngineFeatures
    public float numOfCyls() {
        return 6.0f;
    }
    
    public float enginePower() {
        return 300.0f;
    }
}

Usage:

Car myCar = new Car();

// Can be treated as EngineFeatures
EngineFeatures engine = myCar;
System.out.println("Engine power: " + engine.enginePower() + " HP");
System.out.println("Cylinders: " + engine.numOfCyls());

// Can also be treated as SpeedFeatures
SpeedFeatures speed = myCar;
System.out.println("Max speed: " + speed.maxSpeed() + " km/h");
System.out.println("Max acceleration: " + speed.maxAcceleration() + " m/s²");

Answer: Car must implement methods from both SpeedFeatures and EngineFeatures because EngineFeatures extends SpeedFeatures. The object can be viewed as either interface type.

3.12. Type Checking with Interfaces (Lecture 10, Example 7)

Use instanceof to check if an object implements specific interfaces and perform appropriate actions.

Click to see the solution

Key Concept: The instanceof operator works with interfaces to check if an object implements a given interface.

interface Printable {
    void print();
}

interface Movable {
    void move();
}

interface Serializable {
    void serialize();
}

abstract class Shape {
    abstract void draw();
}

class Rectangle extends Shape implements Printable, Movable, Serializable {
    public void draw() {
        System.out.println("Drawing rectangle");
    }
    
    public void print() {
        System.out.println("Printing rectangle");
    }
    
    public void move() {
        System.out.println("Moving rectangle");
    }
    
    public void serialize() {
        System.out.println("Serializing rectangle");
    }
}

Usage:

Shape a = new Rectangle();

// Check and use different interfaces
if (a instanceof Printable) {
    ((Printable)a).print();  // Prints: Printing rectangle
}

if (a instanceof Movable) {
    ((Movable)a).move();  // Prints: Moving rectangle
}

if (a instanceof Serializable) {
    ((Serializable)a).serialize();  // Prints: Serializing rectangle
}

// This would be false
if (a instanceof String) {
    System.out.println("Never executes");
}

Answer: instanceof checks if an object’s class implements a particular interface. After checking, we can safely cast the object to that interface type.

3.13. Nested Interface (Lecture 10, Example 8)

Create a nested interface inside a class and implement it.

Click to see the solution
class SomeClass {
    public interface Nested {
        boolean isNotNegative(int x);
    }
}

class MyClass implements SomeClass.Nested {
    public boolean isNotNegative(int x) {
        return x >= 0;
    }
}

class Demo {
    public static void main(String[] args) {
        SomeClass.Nested obj = new MyClass();
        
        if (obj.isNotNegative(10)) {
            System.out.println("10 is not negative");
        }
        
        if (obj.isNotNegative(-5)) {
            System.out.println("Never prints");
        } else {
            System.out.println("-5 is negative");
        }
    }
}

Output:

10 is not negative
-5 is negative

Answer: Nested interfaces are accessed using the enclosing class name (e.g., SomeClass.Nested). They can be implemented by any class, not just nested classes.

3.14. Basic Enumeration (Lecture 10, Example 9)

Create an enumeration for days of the week and use it in a switch statement.

Click to see the solution

Key Concept: Enumerations provide type-safe constants and work naturally with switch statements.

public enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

public class DayDemo {
    public void tellItLikeItIs(Day day) {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;
            case FRIDAY:
                System.out.println("Fridays are better.");
                break;
            case SATURDAY:
            case SUNDAY:
                System.out.println("Weekends are best.");
                break;
            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }
    
    public static void main(String[] args) {
        DayDemo demo = new DayDemo();
        
        demo.tellItLikeItIs(Day.MONDAY);     // Mondays are bad.
        demo.tellItLikeItIs(Day.FRIDAY);     // Fridays are better.
        demo.tellItLikeItIs(Day.SATURDAY);   // Weekends are best.
        demo.tellItLikeItIs(Day.WEDNESDAY);  // Midweek days are so-so.
    }
}

Answer: Enumerations provide readable, type-safe constants. The compiler ensures only valid Day values can be passed to tellItLikeItIs().

3.15. Enumeration with Fields and Constructor (Lecture 10, Example 10)

Create a Coin enumeration with associated values (in cents) and methods.

Click to see the solution

Key Concept: Java enums can have fields, constructors, and methods, making them more powerful than simple constants.

enum Coin {
    PENNY(1),
    NICKEL(5),
    DIME(10),
    QUARTER(25);
    
    private final int value;
    
    // Constructor is private by default
    Coin(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

public class CoinDemo {
    public static void main(String[] args) {
        Coin c = Coin.DIME;
        System.out.println("A dime is worth " + c.getValue() + " cents");
        
        // Print all coins and their values
        System.out.println("\nAll coins:");
        for (Coin coin : Coin.values()) {
            System.out.println(coin + ": " + coin.getValue() + " cents");
        }
    }
}

Output:

A dime is worth 10 cents

All coins:
PENNY: 1 cents
NICKEL: 5 cents
DIME: 10 cents
QUARTER: 25 cents

Answer: Each enum constant is initialized with a value through the constructor. The values() method returns an array of all enum constants in declaration order.

3.16. Enum with Method Bodies (Lecture 10, Example 11)

Create an Operation enum where each constant has its own implementation of an eval() method.

Click to see the solution

Key Concept: Each enum constant can have its own method implementation, enabling operation-specific behavior.

enum Operation {
    PLUS {
        double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        double eval(double x, double y) {
            return x / y;
        }
    };
    
    // Abstract method that each constant must implement
    abstract double eval(double x, double y);
    
    public static void main(String[] args) {
        double x = 2.0;
        double y = 4.0;
        
        for (Operation op : Operation.values()) {
            System.out.println(x + " " + op + " " + y + " = " + op.eval(x, y));
        }
    }
}

Output:

2.0 PLUS 4.0 = 6.0
2.0 MINUS 4.0 = -2.0
2.0 TIMES 4.0 = 8.0
2.0 DIVIDE 4.0 = 0.5

Answer: Each enum constant provides its own implementation of the abstract eval() method, creating a clean alternative to switch statements.

3.17. Enum with values() and ordinal() Methods (Tutorial 10, Task 1)

Demonstrate the predefined values() and ordinal() methods available in all enums.

Click to see the solution

Key Concept: All Java enums automatically have values() (returns all constants) and ordinal() (returns position) methods.

public class Test {
    enum Season {
        WINTER,
        SPRING,
        SUMMER,
        FALL
    }
    
    public static void main(String[] args) {
        System.out.println("All seasons:");
        for (Season s : Season.values()) {
            System.out.println(s);
        }
        
        System.out.println("\nSeasons with ordinals:");
        for (Season s : Season.values()) {
            System.out.println(s + " has ordinal " + s.ordinal());
        }
        
        System.out.println("\nOrdinal of SUMMER: " + Season.SUMMER.ordinal());
    }
}

Output:

All seasons:
WINTER
SPRING
SUMMER
FALL

Seasons with ordinals:
WINTER has ordinal 0
SPRING has ordinal 1
SUMMER has ordinal 2
FALL has ordinal 3

Ordinal of SUMMER: 2

Answer: values() returns an array of all enum constants in declaration order. ordinal() returns the zero-based position of each constant.

3.18. Creating and Using an ArrayList (Tutorial 10, Task 2)

Create an ArrayList of strings and perform basic operations.

Click to see the solution

Key Concept: ArrayList is a resizable array implementation of the List interface.

import java.util.List;
import java.util.ArrayList;

public class GFG {
    public static void main(String args[]) {
        // Creating an ArrayList
        List<String> al = new ArrayList<>();
        
        // Adding elements
        al.add("Geeks");
        al.add("Geeks");
        al.add(1, "For");  // Insert at index 1
        
        // Print all elements
        System.out.println(al);
        
        // Access by index
        System.out.println("Element at index 0: " + al.get(0));
        System.out.println("Element at index 1: " + al.get(1));
        
        // Size
        System.out.println("Size: " + al.size());
        
        // Remove
        al.remove(2);  // Remove element at index 2
        System.out.println("After removal: " + al);
    }
}

Output:

[Geeks, For, Geeks]
Element at index 0: Geeks
Element at index 1: For
Size: 3
After removal: [Geeks, For]

Answer: ArrayList maintains insertion order and allows duplicates. Elements are accessed by zero-based index.

3.19. Creating and Using a HashSet (Tutorial 10, Task 3)

Create a HashSet and demonstrate that duplicates are automatically ignored.

Click to see the solution

Key Concept: HashSet stores unique elements only. Attempting to add a duplicate has no effect.

import java.util.Set;
import java.util.HashSet;

public class GFG {
    public static void main(String[] args) {
        // Create a HashSet
        Set<String> hashSet = new HashSet<>();
        
        // Adding elements
        hashSet.add("Geeks");
        hashSet.add("For");
        hashSet.add("Geeks");    // Duplicate - will be ignored
        hashSet.add("Example");
        hashSet.add("Set");
        
        // Print (order not guaranteed)
        System.out.println(hashSet);
        
        // Check if contains
        System.out.println("Contains 'Geeks': " + hashSet.contains("Geeks"));
        System.out.println("Contains 'Java': " + hashSet.contains("Java"));
        
        // Size
        System.out.println("Size: " + hashSet.size());
    }
}

Possible Output:

[Geeks, Example, Set, For]
Contains 'Geeks': true
Contains 'Java': false
Size: 4

Note: The order of elements in the output may vary since HashSet doesn’t maintain any order.

Answer: HashSet automatically filters out duplicate elements. The second “Geeks” is ignored, so the set contains only 4 elements instead of 5.

3.20. Creating and Using a HashMap (Tutorial 10, Task 4)

Create a HashMap to store key-value pairs and iterate through it.

Click to see the solution

Key Concept: HashMap stores key-value pairs where each unique key maps to one value.

import java.util.Map;
import java.util.HashMap;

public class HashMapDemo {
    public static void main(String args[]) {
        // Create a HashMap
        Map<String, Integer> hm = new HashMap<>();
        
        // Adding key-value pairs
        hm.put("a", 100);
        hm.put("b", 200);
        hm.put("c", 300);
        hm.put("d", 400);
        
        // Replacing a value
        hm.put("a", 150);  // Replaces previous value for key "a"
        
        System.out.println("HashMap contents:");
        // Traversing through the map
        for (Map.Entry<String, Integer> me : hm.entrySet()) {
            System.out.println(me.getKey() + ": " + me.getValue());
        }
        
        // Access by key
        System.out.println("\nValue for key 'b': " + hm.get("b"));
        
        // Check if key exists
        System.out.println("Contains key 'c': " + hm.containsKey("c"));
        System.out.println("Contains key 'z': " + hm.containsKey("z"));
        
        // Size
        System.out.println("Size: " + hm.size());
    }
}

Possible Output:

HashMap contents:
a: 150
b: 200
c: 300
d: 400

Value for key 'b': 200
Contains key 'c': true
Contains key 'z': false
Size: 4

Note: The order of entries may vary since HashMap doesn’t maintain insertion order.

Answer: HashMap maps unique keys to values. When we put a value for an existing key, the old value is replaced. We iterate using entrySet() which returns key-value pairs.

3.21. Using an Iterator (Tutorial 10, Task 5)

Use an Iterator to traverse through an ArrayList.

Click to see the solution

Key Concept: An Iterator provides a uniform way to traverse any collection.

import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        // Create a collection
        ArrayList<String> cars = new ArrayList<>();
        cars.add("Volvo");
        cars.add("BMW");
        cars.add("Ford");
        cars.add("Mazda");
        
        // Get the iterator
        Iterator<String> it = cars.iterator();
        
        // Print all elements using iterator
        System.out.println("All cars:");
        while (it.hasNext()) {
            String car = it.next();
            System.out.println(car);
        }
        
        // Create a new iterator for another traversal
        Iterator<String> it2 = cars.iterator();
        
        // Print the first item
        System.out.println("\nFirst car: " + it2.next());
    }
}

Output:

All cars:
Volvo
BMW
Ford
Mazda

First car: Volvo

Answer: iterator() returns an Iterator object. We use hasNext() to check if more elements exist and next() to retrieve them. Note that an iterator is consumed after use—we need a new iterator for a second traversal.