Sessions 12-15|Classes¶

  • Concrete Class
  • Abstract Class
  • Superclass and Subclass
  • Nested Class
    • Non-static Nested Class (Inner Class)
      • Anonymous Inner Class
      • Member Inner Class
      • Local Inner Class
    • Static Nested Class
  • Generic Class
  • POJO Class
  • Enum Class
    • Normal Enum Class
    • Custom Enum Class
    • Enum with Abstract Method
    • Method Override by Constant
  • Final Class
  • Singleton Class
  • Immutable Class
  • Wrapper Class

Access Modifiers¶

Top-Level Classes¶

  • Top-level classes can only be: public and default
  • They cannot be private or protected.
// ✅
public class MyClass {}  
class MyClass {} 

// ❌
private class MyClass {}  
protected class MyClass {}

Inner Classes (Nested Classes)¶

  • Inner classes can be: public, protected, private, and default
class Outer {
  private class Inner {}
  protected class Inner2 {}
  public class Inner3 {}
  class Inner4 {} 
}

Methods and Variables¶

  • Instance or static methods and fields can be: public, protected, private, and default
public int a;
protected int b;
private int c;
int d;

Concrete Class¶

📘 Concrete Class in Java – Baeldung

  • A concrete class is a class that can be instantiated using the new keyword.
  • All methods in a concrete class have complete implementations
  • It can extend an abstract class or implement one or more interfaces.
  • The access modifier of a concrete class can be public or package-private (default).
In [1]:
public class Person {
    private int empId;
    
    Person(int empId) {
        this.empId = empId;
    }
    
    public int getEmpId() {
        return this.empId;
    }
}

Person person = new Person(10);
person.getEmpId();
Out[1]:
10
In [2]:
public interface Shape {
    public void computeArea();
}

public class Rectangle implements Shape {
    @Override
    public void computeArea() {
        System.out.println("Calculating area of the rectangle.");
    }
}

Rectangle rectangle = new Rectangle();
rectangle.computeArea();
Calculating area of the rectangle.

Abstract Class¶

📘 Abstract Classes in Java – Baeldung

An abstract class defines a blueprint for its subclasses, exposing essential functionality while optionally providing or hiding implementation details. It supports 0 to 100% abstraction, meaning it can include:

  • No abstract methods
  • Some abstract methods
  • Only abstract methods

To Achieve Abstraction¶

  • Declare a class as abstract using the abstract keyword.
  • An abstract class can contain both:
    • Abstract methods
    • Concrete methods
  • An abstract class cannot be instantiated directly.
  • Abstract classes are useful when multiple subclasses share common features or behaviors.
  • Abstract classes can have constructors, which can be called from subclasses using the super keyword.
In [3]:
abstract class Car {
    int mileage;
    
    Car(int mileage) {
        this.mileage = mileage;
    }
    
    public abstract void pressBrake();
    public abstract void pressCluth();
    
    public int getNumberOfWheels() {
        return 4;
    }
}
In [4]:
abstract class LuxuryCar extends Car {
    LuxuryCar(int mileage) {
        super(mileage);
    }
    
    public abstract void pressDualBrakeSystem();
    
    @Override
    public void pressBrake() {
        System.out.println("LuxuryCar braking system activated...");
    }
}
In [5]:
class Audi extends LuxuryCar {
    Audi(int mileage) {
        super(mileage);
    }
    
    @Override
    public void pressCluth() {
        System.out.println("Engaging the clutch in Audi...");
    }
    
    @Override
    public void pressDualBrakeSystem() {
        System.out.println("Activating Audi's dual braking system...");
    }

    @Override
    public void pressBrake() {
        super.pressBrake();
        System.out.println("Applying Audi's standard brakes...");
    }
}
In [6]:
Audi audi = new Audi(20);
audi.pressDualBrakeSystem();
audi.pressCluth();
audi.pressBrake();
Activating Audi's dual braking system...
Engaging the clutch in Audi...
LuxuryCar braking system activated...
Applying Audi's standard brakes...

Superclass and Subclass¶

📘 Inner Classes vs. Subclasses in Java – Baeldung

  • A subclass is a class that inherits from another class.
  • The class being inherited from is called the superclass.
  • In Java, if no explicit superclass is defined, a class implicitly inherits from the Object class.
  • It is the root class of the Java class hierarchy.
  • It provides several commonly used methods, such as: clone(), toString(), equals(), notify(), wait(), and others.
In [7]:
// child class object can be stored in parent class reference
Object obj1 = new Person(10);
Object obj2 = new Audi(15);

obj1.getClass(); // class Person
Out[7]:
class REPL.$JShell$12$Person
In [8]:
obj2.getClass(); // class Audi
Out[8]:
class REPL.$JShell$21$Audi

Nested Class¶

📘 Nested Classes in Java – Baeldung

A nested class is a class defined within the scope of another class. It helps logically group classes that are only used in one place, improving encapsulation and code readability.

When to Use?¶

If class A is only relevant to class B, you can define A as a nested class inside B instead of creating a separate file (A.java). This keeps related code organized and avoids unnecessary exposure to other parts of the application.

Scope¶

The scope of a nested class is tied to its enclosing (outer) class. Access levels and member visibility depend on whether the nested class is static or non-static.

Types of Nested Class¶

Nested classes are primarily of two types:

  • Static Nested Class
  • Non-static Nested Class (Inner Class)
    • Local Inner Class
    • Member Inner Class
    • Anonymous Inner Class

Static Nested Class¶

  • Declared with the static keyword.
  • Cannot access non-static members (fields or methods) of the outer class directly.
  • Can be instantiated without creating an object of the outer class.
  • Can have any access modifier: private, protected, public, or default (package-private).
In [9]:
class OuterClass {
    int instanceVariable = 10;
    static String greeting = "¡Hola!";
    static int classVariable = 20;

    // A static nested class can access the static members of its outer class 
    // regardless of whether the nested class method is static or not.
    static class NestedClass {
        public void print() {
            System.out.println(greeting);
        }

        public static void staticPrint() {
            System.out.println(classVariable);
        }
    }
}

OuterClass.NestedClass nestedObj = new OuterClass.NestedClass();
nestedObj.print();
¡Hola!
In [10]:
OuterClass.NestedClass.staticPrint();
20

A nested class can have public, protected, private, or default (package-private) access. If a static nested class is declared private, it can only be instantiated from within the enclosing outer class.

In [11]:
class OuterClass {
    int instanceVariable = 10;
    static int classVariable = 200;
    
    private static class NestedClass {
        public void print() {
            System.out.println(classVariable);
        }
    }
    
    public void display() {
        NestedClass nestedObj = new NestedClass();
        nestedObj.print();
    }
}

OuterClass outerclass = new OuterClass();
outerclass.display();
200

Non-static Nested Class¶

  • It has access to all instance and static members of the outer class.
  • The object of the inner class can only be instantiated after the object of the outer class is created.

Member Inner Class¶

  • A member inner class can have any of the following access modifiers: private, protected, public, or package-private (default).
In [12]:
class OuterClass {
    int instanceVariable = 10;
    static int classVariable = 200;
    
    class InnerClass {
        public void print() {
            System.out.println(instanceVariable + classVariable);
        }
    }
}

OuterClass outerclass = new OuterClass();

OuterClass.InnerClass nestedclass = outerclass.new InnerClass();
nestedclass.print();
210

Local Inner Class¶

  • A local inner class is a class defined within a method, constructor, or any control structure like a for loop, while loop, or if block.
  • A local inner class cannot have access modifiers, as it is local to a block and not a member of the outer class. It is accessible only within the block in which it is defined.
  • A local inner class can only be instantiated within the block in which it is defined.
In [13]:
class OuterClass {
    int instanceVariable = 1;
    static int classVariable = 2;
    
    public void display() {
        int methodLocalVariable = 3;
        
        class LocalInnerClass {
            int localInnerVariable = 4;
            
            public void print() {
                System.out.println(instanceVariable + methodLocalVariable + localInnerVariable + classVariable);
            }
        }
        
        LocalInnerClass local = new LocalInnerClass();
        local.print();
    }
}
In [14]:
OuterClass outer = new OuterClass();
outer.display();
10

Anonymous Inner Class¶

An anonymous inner class is a class without a name, typically used to define a class in place, usually for a one-time use.

When to use?

  • Use an anonymous inner class when you need to override the behavior of a method without the need to create a separate subclass.
In [15]:
public abstract class Car {
    public abstract void pressBrake();
}

// Behind the scenes, the compiler creates an anonymous subclass of the `Car` class.
// An object of this anonymous subclass is instantiated, and its reference is assigned to the "audiCar" variable.
Car audiCar = new Car() {
    @Override
    public void pressBrake() {
        System.out.println("Audi car braking system activated.");
    }
};

audiCar.pressBrake();
Audi car braking system activated.

Inheritance in Nested Classes¶

Example 1: One Nested Class Inheriting Another Nested Class within the Same Outer Class¶

In [16]:
class OuterClass {
    int instanceVar = 1;
    static int classVar = 2;
    
    class InnerClass {
        int innerClassVar = 3;
    }
    
    class AnotherInnerClass extends InnerClass {
        int localInnerVariable = 44;
            
        public void print() {
            System.out.println(instanceVar + innerClassVar + classVar + localInnerVariable);
        }
    }
}
In [17]:
OuterClass outer = new OuterClass();
OuterClass.AnotherInnerClass another = outer.new AnotherInnerClass();
another.print();
50

Example 2: Inheriting a Static Nested Class from Another Class¶

In [18]:
class OuterClass {
    static class NestedClass {
        public void display() {
            System.out.println("Inside Nested Static Class");
        }
    }
}
In [19]:
public class SomeOtherClass extends OuterClass.NestedClass {
    @Override
    public void display() {
        super.display();
        System.out.println("Inside Concrete Class");
    }
}

SomeOtherClass some = new SomeOtherClass();
some.display();
Inside Nested Static Class
Inside Concrete Class

Example 3: Inheriting a Non-static Inner Class from Another Class¶

In [20]:
class OuterClass {
    class NestedClass {
        public void display() {
            System.out.println("Inside Non-static Nested Class");
        }
    }
}
In [21]:
public class SomeOtherClass extends OuterClass.NestedClass {
    
    SomeOtherClass(OuterClass outer) {
        // In Java, super() is used to invoke the constructor of the parent class, 
        // but for a non-static inner class, it requires an instance of the outer class.
        outer.super();
    }
    
    @Override
    public void display() {
        super.display();
        System.out.println("Inside Concrete Class");
    }
}

OuterClass outer = new OuterClass();
SomeOtherClass some = new SomeOtherClass(outer);
some.display();
Inside Non-static Nested Class
Inside Concrete Class

Generic Class¶

A Generic Class allows you to define a class with type parameters, making it type-safe and reusable for different data types.

📘 The Basics of Java Generics – Bealdung
📘 Java Generics Interview Questions (+Answers) – Bealdung

Generic Object Class¶

In [22]:
class Print {
    Object value;

    public Object getPrintValue() {
        return this.value;
    }

    public void setPrintValue(Object value) {
        this.value = value;
    }
}
In [23]:
Print print = new Print();
print.setPrintValue(10.0f);
print.setPrintValue(99);

Object value = print.getPrintValue();
System.out.println(value);

// you can't use `value` directly, you've to cast before use it else you'll get compile time error
if ((int)value == 10) {
    System.out.println("Hello, World");
} else {
    System.out.println("Hello, Moscow");
}
99
Hello, Moscow

Limitations¶

  • Type <T> cannot be a primitive type directly (e.g., Box<int> is invalid; use Box<Integer> instead). It must be a non-primitive type.
  • Type erasure removes generic type information at runtime.

Generic Class¶

In [24]:
class Print<T> {
    T value;

    public T getPrintValue() {
        return value;
    }

    public void setPrintValue(T value) {
        this.value = value;
    }
}
In [25]:
Print<Float> print = new Print<Float>();
print.setPrintValue(10.0f);

Float value = print.getPrintValue();
System.out.println(value);

if (value == 10) {
    System.out.println("Hello, Moscow");
}
10.0
Hello, Moscow
In [26]:
10 == 10.0f;
Out[26]:
true
In [27]:
Print<String> print = new Print<String>();

print.setPrintValue("Good, Morning");
String value = print.getPrintValue();
System.out.println(value);

if (value == "10") {
    System.out.println("Hello, World");
} else {
    System.out.println("Hello, Moscow");
}
Good, Morning
Hello, Moscow

Inheritance with Generic Classes¶

1. Non-Generic Subclass¶

When a subclass is not generic, you must specify the type argument explicitly while extending the generic superclass.

In [28]:
class DigitalPrint<t> {
    t colors;

    public t getPrintColors() {
        return this.colors;
    }

    public void setPrintColors(t colors) {
        this.colors = colors;
    }
}

public class ColorPrint extends DigitalPrint<String> {}

ColorPrint colorPrint = new ColorPrint();
colorPrint.setPrintColors("RED, GREEN, BLUE");
colorPrint.getPrintColors();
Out[28]:
RED, GREEN, BLUE

2. Generic Subclass¶

A generic subclass can either declare its own type parameters or inherit the type parameters from its generic superclass.

In [29]:
public class ColorPrint<T> extends Print<T> {}

ColorPrint<Integer> color_print = new ColorPrint<Integer>();
color_print.setPrintValue(15);
color_print.getPrintValue();
Out[29]:
15
In [30]:
ColorPrint<Float> color_print = new ColorPrint<>();
color_print.setPrintValue(15.5f);
color_print.getPrintValue();
Out[30]:
15.5
In [31]:
// Multiple generic types example
public class Pair<K, V> {
    private K key;
    private V value;
    
    public void put(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public String get() {
        return key + value.toString();
    }
}
In [32]:
Pair<String, Integer> pair = new Pair<>();
pair.put("Hello ", 420);
pair.get();
Out[32]:
Hello 420

Generic Method¶

What if you want only a method to be generic, rather than the entire class?
In such cases, you can define generic methods.

  • The type parameter must be declared before the return type in the method signature.
  • The scope of the type parameter is limited only to the method where it is defined.
In [33]:
public class Utility {
    public static <T> void printElement(T element) {
        System.out.println("Element: " + element);
    }
}

Utility.printElement("Hello");
Utility.printElement(123);
Utility.printElement(456.89);
Element: Hello
Element: 123
Element: 456.89
In [34]:
public class ArrayUtils {
    public static <t> void printArray(t[] array) {
        for (t element: array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

Integer[] intArray = {1, 2, 3, 4};
String[] strArray = {"apple", "banana", "cherry"};
ArrayUtils.printArray(intArray);
ArrayUtils.printArray(strArray);
1 2 3 4 
apple banana cherry 

Raw Type¶

A raw type is a generic class or interface that is used without specifying its type parameters, whether in declarations, method signatures, or object creation.

In [35]:
// generic way
Print<Float> print = new Print<Float>();
print.setPrintValue(20.0f);
print.getPrintValue();
Out[35]:
20.0
In [36]:
// raw way
Print raw_print = new Print(); // Print<Object> print = new Print<>();
raw_print.setPrintValue(100);

if (raw_print.getPrintValue() instanceof Integer){
    System.out.println("Type: Integer");   
}
Type: Integer

Bounded Generics¶

Bounded means "restricted". It can be applied to both generic classes and methods, allowing you to restrict the types that a method accepts.

Upper Bound Generic¶

<T extends Number> means that T must be Number or any of its subclasses (Integer, Double, Float, etc.).

In [37]:
class BoundedExample<T extends Number> {
    T value;

    public T getPrintValue() {
        return this.value;
    }

    public void setPrintValue(T value) {
        this.value = value;
    }
}

BoundedExample<Number> example = new BoundedExample<>();
example.setPrintValue(500);
example.getPrintValue();
Out[37]:
500
In [38]:
example.setPrintValue(500.45f);
example.getPrintValue();
Out[38]:
500.45
In [39]:
BoundedExample<String> example = new BoundedExample<>();
example.setPrintValue("Hola");
example.getPrintValue();
|   BoundedExample<String> example = new BoundedExample<>();
type argument java.lang.String is not within bounds of type-variable T

|   BoundedExample<String> example = new BoundedExample<>();
incompatible types: cannot infer type arguments for BoundedExample<>
    reason: inference variable T has incompatible bounds
      equality constraints: java.lang.String
      upper bounds: java.lang.Number
In [40]:
class BoundedGenericMethod {
    public static <T extends Number> int add(T num1, T num2) {
        return num1.intValue() + num2.intValue();
    }
}

BoundedGenericMethod.add(10, 20);
Out[40]:
30
In [41]:
BoundedGenericMethod.add(200.6f, 20.8f);
Out[41]:
220
In [42]:
BoundedGenericMethod.add(30.5, 20.5);
Out[42]:
50

Multi-Bound Generic¶

Syntax: <T extends ClassName & Interface1 & Interface2 ...>

  • The first bound must be a class (can be abstract or concrete).
  • Any additional bounds must be interfaces.
In [43]:
interface Printable {
    public void print();
}

interface Scannable {
    public void scan();
}

class Device {
    public void powerOn() {
        System.out.println("Device is powered on.");
    }
}
In [44]:
class MultiFunction<T extends Device & Printable & Scannable> {
    private T device;

    MultiFunction(T device) {
        this.device = device;
    }

    public void operate() {
        device.powerOn();
        device.scan();
        device.print();
    }
}
In [45]:
class SmartPrinter extends Device implements Scannable, Printable {
    @Override
    public void print() {
        System.out.println("Printing document...");
    }

    @Override
    public void scan() {
        System.out.println("Scanning document...");
    }
}
In [46]:
SmartPrinter printer = new SmartPrinter();
MultiFunction<SmartPrinter> multi = new MultiFunction<>(printer);
multi.operate();
Device is powered on.
Scanning document...
Printing document...

Wildcards in Generics¶

📘 Java Generics – <?> vs <? extends Object> – Baeldung
📘 Type Parameter vs Wildcard in Java Generics – Baeldung

  • Upper-bounded wildcard: <? extends UpperBoundClassName> className and below
    • Allows you to reference objects of type UpperBoundClassName or any of its subclasses.
    • Useful when you want to read from generic object.
  • Lower-bounded wildcard: <? super LowerBoundClassName> className and above
    • Allows you to reference objects of type LowerBoundClassName or any of its superclasses.
    • Useful when you want to write objects into generic.
  • Unbounded wildcard: <?>
    • Represents any type.
    • Typically used for read-only access where the specific type doesn’t matter.
In [47]:
import java.util.ArrayList;
import java.util.List;

// You can read values as Number or its subclasses but cannot add new elements
class UpperBoundExample {
    public static void printNumbers(List<? extends Number> list) {
        for (Number num: list) {
            System.out.print(num + " ");
        }
    }
}

List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);

UpperBoundExample.printNumbers(intList);
1 2 3 
In [48]:
// You can add Integer or its subclasses but can only safely read objects as Object
class LowerBoundExample {
    public static void addIntegers(List<? super Integer> list) {
        list.add(10);
        list.add(20);

        // When using a lower bound <? super T>, you can add T or its subtypes
        // However, when reading from such a list, the elements are treated as Object
        // This is because you don't know the exact type of the list, only that it's
        // a supertype of Integer. So, list.get(0) will return an Object.
        Object obj = list.get(0);
        System.out.println(obj);
    }
}

List<Number> numberList = new ArrayList<>();
LowerBoundExample.addIntegers(numberList);
10
In [49]:
// Used when the type doesn’t matter — typically for read-only access.
class UnboundedExample {
    public static void printList(List<?> list) {
        for (Object ele: list) {
            System.out.println(ele);
        }
    }
}

List<String> strings = new ArrayList<>();
strings.add("Apple");
strings.add("Banana");
strings.add("Cherry");

UnboundedExample.printList(strings);
Apple
Banana
Cherry

Generic Class Erasure¶

📘 Type Erasure in Java – Baeldung

public class Print<T extends Number> {
  T value;

  public void setValue(T value) {
    this.value = value;
  }
}

After converting to bytecode

public class Print {
  Number value;

  public void setValue(Number value) {
    this.value = value;
  }
}

POJO Class¶

📘 What Is a Pojo Class? – Baeldung

  • POJO stands for Plain Old Java Object.
  • It is a simple Java class that:
    • Contains private variables
    • Provides public getter and setter methods to access and modify those variables.
    • Includes a public no-argument constructor
  • The class itself is typically marked as public.
  • No special annotations should be used (@Entity, @Table, @Id, etc.).
  • A POJO should not extend any class or implement any interface — it must remain a plain object without dependencies or behavior inheritance.
In [50]:
public class Student {
    private int id;
    private String name;
    private String course;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCourse() {
        return this.course;
    }

    public void setCourse(String course) {
        this.course = course;
    }
}
In [51]:
Student student = new Student();
student.setCourse("Software Engineering");
student.getCourse();
Out[51]:
Software Engineering

Enum Class¶

📘 Attaching Values to Java Enum – Baeldung
📘 A Guide to Java Enums – Baeldung

  • An Enum class defines a collection of constants.
  • All constants in an Enum are implicitly public static final.
  • It cannot extend other classes because it implicitly extends java.lang.Enum class.
  • It can implement interfaces.
  • An Enum can include variables, constructors, and methods.
  • It cannot be instantiated directly because:
    • The constructor is implicitly private. Even if you declare it with package-private (default) or any other access modifier, the compiler automatically marks it as private in the bytecode.
  • No other class can extend an Enum class.
  • An Enum can have abstract methods, and each constant must provide an implementation for these abstract methods.
  • Each Enum constant is an instance of the Enum class.

Access Modifiers¶

  • Top-Level Enum can be: public and default (package-private)
  • Nested Enum can be: public, private, protected and default (package-private).
    • Nested enums are implicitly static
  • Enums cannot be defined inside methods or other blocks.

Default Values¶

If no explicit values are assigned to Enum constants, they are automatically assigned ordinal values starting from 0 and incrementing by 1 in the order of declaration.

Built-in Methods¶

Java Enums provide the following methods inherited from java.lang.Enum:

  • values(): Returns an array of all Enum constants in the order they are declared.
  • ordinal(): Returns the zero-based index of the Enum constant based on its declaration order.
  • valueOf(String name): Returns the Enum constant matching the specified string name (case-sensitive). If no constant matches, it throws an IllegalArgumentException.
  • name(): Returns the name of the Enum constant as a string, exactly as declared.
public enum Beer {
  KF, KO, RC, FO;
}

// internally, every enum is implemented as a class
public class Beer {
  public static final Beer KF = new Beer();
  public static final Beer KO = new Beer();
  public static final Beer RC = new Beer();
  public static final Beer FO = new Beer();
}
In [52]:
public enum EnumSample {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;
}
In [53]:
for (EnumSample sample: EnumSample.values()) {
    System.out.println(sample.ordinal());
}
0
1
2
3
4
5
6
In [54]:
for (EnumSample sample: EnumSample.values()) {
    System.out.println(sample);
}
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
In [55]:
EnumSample sampleVariable = EnumSample.valueOf("SUNDAY");
sampleVariable.name();
Out[55]:
SUNDAY

Enum with Custom Values¶

In [56]:
public enum EnumSample {
    // MONDAY(val=100, comment="0 day of the week")
    MONDAY (101, "1st day of the week"),
    TUESDAY (102, "2nd day of the week"),
    WEDNESDAY (103, "3rd day of the week"),
    THURSDAY (104, "4th day of the week"),
    FRIDAY (105, "5th day of the week"),
    SATURDAY (106, "1st day of the weekend"),
    SUNDAY (107, "2nd day of the weekend");

    private int val;
    private String comment;

    EnumSample(int val, String comment) {
        this.val = val;
        this.comment = comment;
    }

    public int getValue() {
        return this.val;
    }

    public String getComment() {
        return this.comment;
    }

    public static EnumSample fromValue(int val) {
        for (EnumSample sample: EnumSample.values()) {
            if (sample.val == val) {
                return sample;
            }
        }
        return null;
    }
}
In [57]:
EnumSample enumSample = EnumSample.fromValue(105);
enumSample.getComment();
Out[57]:
5th day of the week

Overriding Methods in Enum Constants¶

In [58]:
public enum EnumSample {
    MONDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Dummy Monday");
        }
    },
    TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    // default method for all Enum constants
    public void dummyMethod() {
        System.out.println("Dummy Default");
    }
}
In [59]:
EnumSample saturdayEnum = EnumSample.SATURDAY;
saturdayEnum.dummyMethod();
Dummy Default
In [60]:
EnumSample mondayEnum = EnumSample.MONDAY;
mondayEnum.dummyMethod();
Dummy Monday

Enum with Abstract Methods¶

In [61]:
public enum EnumSample {
    MONDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Dummy Monday...");
        }
    },
    TUESDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Dummy Tuesday...");
        }
    };

    public abstract void dummyMethod();
}
In [62]:
EnumSample mondayEnum = EnumSample.MONDAY;
mondayEnum.dummyMethod();
Dummy Monday...
In [63]:
EnumSample tuesdayEnum = EnumSample.TUESDAY;
tuesdayEnum.dummyMethod();
Dummy Tuesday...

Implementing Interfaces in Enums¶

In [64]:
public interface MyInterface {
    public String toLowerCase();
}

public enum EnumSample implements MyInterface {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;

    // common implementation for all constants
    @Override
    public String toLowerCase() {
        return this.name().toLowerCase();
    }
}

EnumSample mondayEnum = EnumSample.MONDAY;
mondayEnum.toLowerCase();
Out[64]:
monday

Advantages of Enums over Constant Variables¶

In [65]:
public class WeekConstants {
    public static final int MONDAY = 0;
    public static final int TUESDAY = 1;
    public static final int WEDNESDAY = 2;
    public static final int THURSDAY = 3;
    public static final int FRIDAY = 4;
    public static final int SATURDAY = 5;
    public static final int SUNDAY = 6;
}

// improved readability and full control over the parameters being passed.
public enum EnumSample {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;
}

// Main.java
public static boolean isWeekend(int day) {
    return (WeekConstants.SATURDAY == day || WeekConstants.SUNDAY == day);
}

public static boolean isEnumWeekend(EnumSample day) {
    return (EnumSample.SATURDAY == day || EnumSample.SUNDAY == day);
}
In [66]:
isWeekend(1);
Out[66]:
false
In [67]:
isWeekend(100);
Out[67]:
false
In [68]:
isWeekend(6);
Out[68]:
true
In [69]:
isWeekend(5);
Out[69]:
true
In [70]:
isEnumWeekend(EnumSample.SATURDAY);
Out[70]:
true
In [71]:
isEnumWeekend(EnumSample.MONDAY);
Out[71]:
false

Singleton Class¶

The purpose of the Singleton class is to ensure that only one instance of the class is created throughout the application lifecycle.

📘 Singletons in Java – Baeldung
📘 Drawbacks of the Singleton Design Pattern – Baeldung

Common Approaches to Implement Singleton:¶

  • Eager Initialization:
    • The instance is created at the time of class loading. This is simple and thread-safe but may lead to resource wastage if the instance is never used.
  • Lazy Initialization:
    • The instance is created only when it is requested for the first time. This saves resources but is not thread-safe unless properly synchronized.
  • Synchronization Block:
    • This approach synchronizes the method or block of code to ensure that only one thread can access it at a time.
  • Double-Checked Locking:
    • This method minimizes the performance overhead by checking if the instance is null before entering the synchronized block. However, it requires the use of volatile to address memory consistency issues.
  • Bill Pugh Solution:
    • A highly efficient and thread-safe approach that uses a static inner helper class to create the Singleton instance. The instance is created only when the inner class is loaded, making it both lazy and thread-safe.
  • Enum Singleton:
    • Considered the most robust Singleton implementation in Java. It’s simple, inherently thread-safe, and also guards against serialization and reflection attacks.

Eager Initialization¶

In [72]:
public class EagerSingleton {
    // instance is created at the time of class loading
    private static final EagerSingleton instance = new EagerSingleton();
    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return instance;
    }
}

Lazy Initialization¶

In [73]:
public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

Synchronization Block¶

In [74]:
public class SyncBlockSingleton {
    private static SyncBlockSingleton instance;
    private SyncBlockSingleton() {}

    synchronized public static SyncBlockSingleton getInstance() {
        if (instance == null) {
            instance = new SyncBlockSingleton();
        }
        return instance;
    }
}

Double Check Locking¶

In [75]:
public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;
    private DoubleCheckedLockingSingleton() {}

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

Bill Pug Solution¶

📘 Bill Pugh Singleton Implementation – Baeldung

In [76]:
public class BillPughSingleton {
    private static class SingletonHelper {
        // the instance is created when the inner class is loaded
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    private BillPughSingleton() {}

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Enum Singleton¶

In [77]:
public enum EnumSingleton {
    INSTANCE;

    public void someMethod() {}
}

Immutable Class¶

An immutable class is a class whose instances cannot be modified after creation.

  • The class should be declared as final, so it cannot be subclassed.
  • All fields must be private and final.
  • Fields should be initialized only once, typically via a constructor.
  • The class should not provide any setter methods.
  • Only getter methods should be provided, and if the field is mutable, return a defensive copy to preserve immutability.
  • Common examples: String, Integer, Double, and other wrapper classes in Java.

Do immutable classes have to be final?¶

Not strictly, but it's recommended. Declaring an immutable class as final ensures that no subclass can break the immutability by introducing setters or mutable fields.

In [78]:
public final class Employee {
    private final int id;
    private final String name;

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
In [79]:
import java.util.Collections;

public final class ImmutableClass {
    private final String name;
    private final List<Object> nameList;

    public ImmutableClass(String name, List<Object> nameList) {
        this.name = name;
        this.nameList = new ArrayList<>(nameList);
    }

    public String getName() {
        return this.name;
    }

    public List<Object> getNameList() {
        // Return an unmodifiable copy to ensure true immutability
        return Collections.unmodifiableList(new ArrayList<>(nameList));
    }
}
In [80]:
List<Object> originalList = Arrays.asList("Apple", "Banana", "Cherry");

ImmutableClass myObject = new ImmutableClass("Fruit List", originalList);
System.out.println("Name: " + myObject.getName());
System.out.println("List: " + myObject.getNameList());
Name: Fruit List
List: [Apple, Banana, Cherry]
In [81]:
List<Object> fetchedList = myObject.getNameList();
fetchedList.add("Durian");
---------------------------------------------------------------------------
java.lang.UnsupportedOperationException: null
	at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1091)
	at .(#168:1)

Final Class¶

A final class is a class that cannot be extended or subclassed. This is useful when you want to prevent inheritance for security, immutability, or design reasons.

While the final keyword can be applied to classes, methods, and variables, immutability specifically refers to the state of an object that cannot change after it's created.

Examples:

  • String
  • Math
  • StringBuilder
In [82]:
public final class BankAccount {
    private final String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public void withdraw(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
        }
    }
}
In [83]:
// this will cause a compile-time error
public class SavingsAccount extends BankAccount {}
|   public class SavingsAccount extends BankAccount {}
cannot inherit from final BankAccount

constructor BankAccount in class BankAccount cannot be applied to given types;
  required: java.lang.String,double
  found:    no arguments
  reason: actual and formal argument lists differ in length