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
- Non-static Nested Class (Inner 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
anddefault
- They cannot be
private
orprotected
.
// ✅
public class MyClass {}
class MyClass {}
// ❌
private class MyClass {}
protected class MyClass {}
Inner Classes (Nested Classes)¶
- Inner classes can be:
public
,protected
,private
, anddefault
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
, anddefault
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).
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();
10
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 theabstract
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.
abstract class Car {
int mileage;
Car(int mileage) {
this.mileage = mileage;
}
public abstract void pressBrake();
public abstract void pressCluth();
public int getNumberOfWheels() {
return 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...");
}
}
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...");
}
}
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.
// child class object can be stored in parent class reference
Object obj1 = new Person(10);
Object obj2 = new Audi(15);
obj1.getClass(); // class Person
class REPL.$JShell$12$Person
obj2.getClass(); // class Audi
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).
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!
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.
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).
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, orif
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.
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();
}
}
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.
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.
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);
}
}
}
OuterClass outer = new OuterClass();
OuterClass.AnotherInnerClass another = outer.new AnotherInnerClass();
another.print();
50
Example 2: Inheriting a Static Nested Class from Another Class¶
class OuterClass {
static class NestedClass {
public void display() {
System.out.println("Inside Nested Static Class");
}
}
}
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¶
class OuterClass {
class NestedClass {
public void display() {
System.out.println("Inside Non-static Nested Class");
}
}
}
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¶
class Print {
Object value;
public Object getPrintValue() {
return this.value;
}
public void setPrintValue(Object value) {
this.value = value;
}
}
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
class Print<T> {
T value;
public T getPrintValue() {
return value;
}
public void setPrintValue(T value) {
this.value = value;
}
}
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
10 == 10.0f;
true
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
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();
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.
public class ColorPrint<T> extends Print<T> {}
ColorPrint<Integer> color_print = new ColorPrint<Integer>();
color_print.setPrintValue(15);
color_print.getPrintValue();
15
ColorPrint<Float> color_print = new ColorPrint<>();
color_print.setPrintValue(15.5f);
color_print.getPrintValue();
15.5
// 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();
}
}
Pair<String, Integer> pair = new Pair<>();
pair.put("Hello ", 420);
pair.get();
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.
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
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.
// generic way
Print<Float> print = new Print<Float>();
print.setPrintValue(20.0f);
print.getPrintValue();
20.0
// 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.).
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();
500
example.setPrintValue(500.45f);
example.getPrintValue();
500.45
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
class BoundedGenericMethod {
public static <T extends Number> int add(T num1, T num2) {
return num1.intValue() + num2.intValue();
}
}
BoundedGenericMethod.add(10, 20);
30
BoundedGenericMethod.add(200.6f, 20.8f);
220
BoundedGenericMethod.add(30.5, 20.5);
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.
interface Printable {
public void print();
}
interface Scannable {
public void scan();
}
class Device {
public void powerOn() {
System.out.println("Device is powered on.");
}
}
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();
}
}
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...");
}
}
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.
- Allows you to reference objects of type
- 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.
- Allows you to reference objects of type
- Unbounded wildcard:
<?>
- Represents any type.
- Typically used for read-only access where the specific type doesn’t matter.
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
// 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
// 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.
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;
}
}
Student student = new Student();
student.setCourse("Software Engineering");
student.getCourse();
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
anddefault
(package-private) - Nested Enum can be:
public
,private
,protected
anddefault
(package-private).- Nested enums are implicitly
static
- Nested enums are implicitly
- 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 anIllegalArgumentException
.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();
}
public enum EnumSample {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY;
}
for (EnumSample sample: EnumSample.values()) {
System.out.println(sample.ordinal());
}
0 1 2 3 4 5 6
for (EnumSample sample: EnumSample.values()) {
System.out.println(sample);
}
MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY
EnumSample sampleVariable = EnumSample.valueOf("SUNDAY");
sampleVariable.name();
SUNDAY
Enum with Custom Values¶
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;
}
}
EnumSample enumSample = EnumSample.fromValue(105);
enumSample.getComment();
5th day of the week
Overriding Methods in Enum Constants¶
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");
}
}
EnumSample saturdayEnum = EnumSample.SATURDAY;
saturdayEnum.dummyMethod();
Dummy Default
EnumSample mondayEnum = EnumSample.MONDAY;
mondayEnum.dummyMethod();
Dummy Monday
Enum with Abstract Methods¶
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();
}
EnumSample mondayEnum = EnumSample.MONDAY;
mondayEnum.dummyMethod();
Dummy Monday...
EnumSample tuesdayEnum = EnumSample.TUESDAY;
tuesdayEnum.dummyMethod();
Dummy Tuesday...
Implementing Interfaces in Enums¶
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();
monday
Advantages of Enums over Constant Variables¶
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);
}
isWeekend(1);
false
isWeekend(100);
false
isWeekend(6);
true
isWeekend(5);
true
isEnumWeekend(EnumSample.SATURDAY);
true
isEnumWeekend(EnumSample.MONDAY);
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 ofvolatile
to address memory consistency issues.
- This method minimizes the performance overhead by checking if the instance is
- 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¶
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¶
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
Synchronization Block¶
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¶
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
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¶
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
andfinal
. - 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.
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;
}
}
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));
}
}
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]
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
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;
}
}
}
// 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