Day 5: Interfaces, Exceptions & Threads

It is 20 minutes past 12 in the noon, dated 31st of May.
We will be learning more about Interfaces, Exceptions and Threads today.
Let’s get going!
Types of Interfaces
Functional or SAM (Single Abstract Method) Interface
It has only one public method.
Can be annotated using @FunctionalInterface.
@FunctionalInterface
interface Storable{
public String storeItem(String item);
}
class MagicBag implements Storable{
public String storeItem(String item){
return "Magic Bag stores " + item;
}
}
public class Demo{
public static void main(String[] args) {
MagicBag myBag = new MagicBag();
System.out.println(myBag.storeItem("Sword"));
}
}
//Output
Magic Bag stores Sword
Here we observe:
A new class MagicBag implements our functional interface Storable which has only one abstract method.
We can shorten this code using Lambda Expressions which only work on Functional Interfaces:
@FunctionalInterface
interface Storable{
public String storeItem(String item);
}
public class Demo{
public static void main(String[] args) {
Storable myBag = (String item) -> {
return "My bag stores " + item;
};
System.out.println(myBag.storeItem("Sword"));
}
}
//Output
My bag stores Sword
Lambda Expressions allow us to write the inline behavior without having to create separate classes.
This code could be simplified further:
@FunctionalInterface
interface Storable{
public String storeItem(String item);
}
public class Demo{
public static void main(String[] args) {
Storable myBag = item -> "My bag stores " + item;
System.out.println(myBag.storeItem("Sword"));
}
}
Marker Interface
empty interfaces (no methods) used to mark or tag a class for a specific purpose.
A common example is java.io.Serializable, which tells the JVM that objects of the class can be serialized—that is, their state can be converted into a byte stream for storage or transfer.
Deserialization is the reverse process, reconstructing the object from the stored state.
Marker interfaces are often used in scenarios like game state saving/loading, security checks, or cloning objects (Cloneable).
Exceptions
There are three types of errors:
Compile-time errors: They are syntax errors identified by the compiler during compile-time itself.
Run-time errors: These cause the execution of the program to stop. They are of 2 types:
Errors: which we cannot attempt to handle like InternalError, OutOfMemoryError, etc.
Exceptions: which we can attempt to catch and handle. They can be of two types:
Checked Exceptions - should be handled like SQLException, IOException
Unchecked Exceptions - need not be handled explicitly like NullPointerException, ArrayOutOfBoundsException etc.
Logical errors which return unexpected outputs. These can only be identified through testing.
try-catch block
We use try-catch block to handle Exceptions. The try block executes the critical statements which may throw an exception, which is then caught as an object in the catch block. This Exception object can be then handled like logging it or displaying a message to the user.
The finally block is optional with try-catch which is used to close the resources. The statements inside finally block are executed regardless of the exception being caught.
Different exceptions need to be handled through different catch blocks. Avoid catching Exception broadly, use specific exceptions.
public class Demo{
public static void main(String[] args) {
int arr[] = new int[5];
try{
int result = 5/10;
System.out.println(arr[5]);
}
catch(ArithmeticException e){
System.out.println("Exception Caught: " + e);
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Exception Caught: " + e);
}
finally{
System.out.println("Program ended!");
}
}
}
//Output
Exception Caught: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
Program ended!
throw and throws
throw allows to manually throw a specific exception, its useful when we create our custom exceptions
throws makes the caller class to handle the exception thrown. It is also called ‘ducking’ the exception.
enum CharacterType{
Warrior, Mage, Healer
}
class Weapon{
public String weaponName;
public CharacterType characterType;
public Weapon(String weaponName, CharacterType characterType){
this.weaponName = weaponName;
this.characterType = characterType;
}
}
class Warrior{
public CharacterType characterType = CharacterType.Warrior;
public Weapon equippedWeapon;
public void equipWeapon(Weapon w) throws WeaponMismatchException{
if(this.characterType != w.characterType)
throw new WeaponMismatchException("Equipping wrong weapon!");
else{
this.equippedWeapon = w;
System.out.println("Successfully Equipped " + equippedWeapon.weaponName);
}
}
}
class WeaponMismatchException extends Exception{
WeaponMismatchException(String msg){
super(msg);
}
}
public class Demo{
public static void main(String[] args) {
Weapon broadSword = new Weapon("Beginner Broad Sword", CharacterType.Warrior);
Weapon shortWand = new Weapon("Beginner Short Wand", CharacterType.Mage);
Warrior myWarrior = new Warrior();
try {
myWarrior.equipWeapon(shortWand);
myWarrior.equipWeapon(broadSword);
}
catch(WeaponMismatchException e){
System.out.println("Exception Caught: " + e.getMessage());
}
finally{
System.out.println("Equipment Screen closed!");
}
}
}
//Output
Exception Caught: Equipping wrong weapon!
Equipment Screen closed!
Here we observe:
The method equipWeapon(shortWand) generates an Exception manually by throw new WeaponMismatchException whenever a Warrior tries to equip a weapon not of Warrior type.
The Exception object WeaponMismatchException is not handled in the class but in the caller method which is, the main( ) method. This is because we have mentioned throws WeapoMismatchException with the equipWeapon( ) method declaration.
The next method equipWeapon(broadSword) doesn’t get called because after the exception is thrown the finally block gets executed.
Input in Java
- Use BufferedReader
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Demo{
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Write Some Characters!");
String a = br.readLine();
System.out.println(a);
System.out.println("Write Some Numbers!");
int b = Integer.parseInt(br.readLine());
System.out.println(b);
br.close();
}
}
//Output
Write Some Characters!
Attack!
Attack!
Write Some Numbers!
888
888
System.in is an InputStream (byte-based).
InputStreamReader converts it into a Reader (character-based).
BufferedReader wraps that reader to efficiently read text (like full lines using .readLine()).
br.close() is called at the end to release system resources associated with the stream.
- Use Scanner
import java.util.Scanner;
public class Demo{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Write Some Characters!");
String a = sc.nextLine();
System.out.println(a);
System.out.println("Write Some Number!");
int b = sc.nextInt();
System.out.println(b);
}
}
//Output
Write Some Characters!
Alohomora
Alohomora
Write Some Number!
666
666
Use BufferedReader for faster, simple line input.
Use Scanner for ease and parsing different data types directly.
Threads
Threads are smallest unit of code that can be executed.
Multiple threads run concurrently at a time, allowing multitasking in a program.
We can create threads in 2 ways:
- Extending Thread class
class HundredFoldSpell extends Thread{
private String spellName;
HundredFoldSpell(String spellName){
this.spellName = spellName;
}
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.spellName + " casted!");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MultiCasting{
public static void main(String[] args) {
HundredFoldSpell flameSpell = new HundredFoldSpell("Flame Spell");
HundredFoldSpell iceSpell = new HundredFoldSpell("Ice Spell");
flameSpell.setPriority(10);
System.out.println(flameSpell.getPriority());
System.out.println(iceSpell.getPriority());
flameSpell.start();
iceSpell.start();
}
}
//Output
10
5
Ice Spell casted!
Flame Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Here we observe:
HundredFoldSpell extends Thread and overrides run() to print a spell 100 times.
Thread.sleep(10) adds a slight delay to make thread outputs more readable and interleaved.
Even with sleep(), thread scheduling is unpredictable outputs may still repeat or vary.
setPriority(10) suggests preference to flameSpell, but it's not guaranteed by the JVM.
Default priority of all threads is 5.
Threads run concurrently, demonstrating independent execution paths.
- Implements Runnable
class HundredFoldSpell implements Runnable{
private String spellName;
HundredFoldSpell(String spellName){
this.spellName = spellName;
}
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.spellName + " casted!");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MultiCasting{
public static void main(String[] args) {
HundredFoldSpell flameSpell = new HundredFoldSpell("Flame Spell");
HundredFoldSpell iceSpell = new HundredFoldSpell("Ice Spell");
Thread t1 = new Thread(flameSpell);
Thread t2 = new Thread(iceSpell);
t1.start();
t2.start();
}
}
//Output
Ice Spell casted!
Flame Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Ice Spell casted!
Flame Spell casted!
Ice Spell casted!
Here we observe:
HundredFoldSpell implements Runnable, so it defines the run() method that contains the logic for each thread.
Two HundredFoldSpell objects are created with different spell names. These are passed to two Thread objects (t1 and t2) which are started using start().
Thread.sleep(10) is used to slow down each loop iteration, making thread interleaving visible in the output.
Even with sleep, thread output may appear interleaved or duplicated because thread scheduling is unpredictable and controlled by the OS.
Note: The method t1.join( ) can be used to make the method wait for the thread to complete. If used, all the Flame Spell will be casted first and then the Ice Spell.
Race Conditions
When two threads try to access same variable (shared variable) we may end up with unpredictable output due to race conditions.
Hence, we must make our variable thread-safe.
Apart from join( ) method of waiting for the termination of one thread, we can also use synchronized keyword with the method.
This ensures only one thread accesses the variable or executes the method at a time.
class HundredFoldSpell implements Runnable{
private String spellName;
static int castSpellCount;
HundredFoldSpell(String spellName){
this.spellName = spellName;
}
public void run(){
for(int i=0;i<100;i++){
addSpellCount();
}
}
public synchronized void addSpellCount(){
castSpellCount++;
System.out.println(this.spellName + " casted! Count: " + castSpellCount);
}
}
public class MultiCasting{
public static void main(String[] args) {
HundredFoldSpell flameSpell = new HundredFoldSpell("Flame Spell");
HundredFoldSpell iceSpell = new HundredFoldSpell("Ice Spell");
Thread t1 = new Thread(flameSpell);
Thread t2 = new Thread(iceSpell);
t1.start();
t2.start();
}
}
//Output (Last 6 lines)
Ice Spell casted! Count: 196
Flame Spell casted! Count: 194
Ice Spell casted! Count: 197
Flame Spell casted! Count: 198
Ice Spell casted! Count: 199
Flame Spell casted! Count: 200
Here we observe:
Without using a synchronized method, we may get inconsistent values like 100, 157, etc., due to race conditions.
The synchronized keyword ensures that only one thread at a time (either t1 or t2) can execute the addSpellCount() method, protecting the shared variable.
By moving the logic into a separate synchronized method, we lock only the critical section (the increment operation), allowing the rest of the thread to run concurrently, improving performance.
Synchronizing just the addSpellCount() method instead of the entire run() method enables better parallelism while still maintaining thread safety.
Thread Life Cycle
Extra Concepts on Thread:
For implementing Runnable, the run() method needs to be implemented which does not return anything, while for a Callable, the call() method needs to be implemented which returns a result on completion. Note that a thread can’t be created with a Callable, it can only be created with a Runnable.
Another difference is that the call() method can throw an exception whereas run() cannot.
In Java, daemon threads are low-priority threads that run in the background to perform tasks such as garbage collection or provide services to user threads. The life of a daemon thread depends on the mercy of user threads, meaning that when all user threads finish their execution, the Java Virtual Machine (JVM) automatically terminates the daemon thread.
A semaphore controls access to a shared resource through the use of a counter. If the counter is greater than zero, then access is allowed. If it is zero, then access is denied. What the counter is counting are permits that allow access to the shared resource. Thus, to access the resource, a thread must be granted a permit from the semaphore.
We will unlock further concepts in Thread in our later conquests!
For today, this is the end!
Next blog will be our final chapter of the part Java Core.
Subscribe to my newsletter
Read articles from Nagraj Math directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
