Category: Java

  • File Handling

    Java.io.File Class in Java

    Java’s File class represents the pathname of a file or directory. Since file systems vary across platforms, using a simple string is not enough to handle file or directory names. The File class provides various methods to work with pathnames, such as deleting or renaming files, creating new directories, listing directory contents, and checking file or directory properties.

    Key Features of the File Class

    The File class acts as an abstract representation of file and directory pathnames. The pathname can either be absolute or relative, and you can obtain the parent directory by calling the getParent() method. An instance of the File class is immutable; once created, the pathname it represents doesn’t change.

    The file system can impose certain access restrictions like read, write, or execute permissions on files or directories, referred to as access permissions.

    How to Create a File Object

    File object is created by passing a string representing a file or directory name. You can use either a string or another File object. For example:

    File file = new File("/home/user/docs/myfile.txt");

    This creates a File object representing the myfile.txt file in the /home/user/docs directory.

    Fields in the File Class

    FieldTypeDescription
    pathSeparatorStringString used to separate paths in a file system.
    pathSeparatorCharcharCharacter used to separate paths in a file system.
    separatorStringDefault name separator character, represented as a string.
    separatorCharcharDefault name separator character.
    Constructors of the File Class

    Methods of the File Class

    1. File(File parent, String child): Creates a File instance from a parent directory and a child pathname.
    2. File(String pathname): Creates a File instance from a string pathname.
    3. File(String parent, String child): Creates a File instance from a parent directory string and a child pathname.
    4. File(URI uri): Creates a File instance from a URI object.

    MethodDescriptionReturn Type
    canExecute()Checks if the file can be executed.boolean
    canRead()Checks if the file can be read.boolean
    canWrite()Checks if the file can be written to.boolean
    compareTo(File pathname)Compares two pathnames lexicographically.int
    createNewFile()Atomically creates a new empty file.boolean
    delete()Deletes the file or directory.boolean
    exists()Checks if the file or directory exists.boolean
    getAbsolutePath()Returns the absolute pathname string.String
    list()Returns an array of names of files and directories.String[]
    getFreeSpace()Returns the number of unallocated bytes in the partition.long
    getName()Returns the name of the file or directory.String
    isDirectory()Checks if the pathname is a directory.boolean
    isFile()Checks if the pathname is a regular file.boolean
    isHidden()Checks if the file is hidden.boolean
    length()Returns the length of the file in bytes.long
    mkdir()Creates a new directory.boolean
    renameTo(File dest)Renames the file or directory.boolean
    toString()Returns the string representation of the pathname.String
    toURI()Returns a URI representing the pathname.URI
    import java.util.*;
    public class Main {
        public static void main(String[] args) {
            System.out.println("Hello, World!");
        }
    }

    Example 1: Check if a File or Directory Exists

    This program takes a filename or directory name as input, then checks if the file or directory exists and displays its properties.

    import java.io.File;
    
    class FileProperties {
        public static void main(String[] args) {
            String filename = args[0];
            File file = new File(filename);
    
            System.out.println("File Name: " + file.getName());
            System.out.println("Path: " + file.getPath());
            System.out.println("Absolute Path: " + file.getAbsolutePath());
            System.out.println("Parent: " + file.getParent());
            System.out.println("Exists: " + file.exists());
    
            if (file.exists()) {
                System.out.println("Writable: " + file.canWrite());
                System.out.println("Readable: " + file.canRead());
                System.out.println("Is Directory: " + file.isDirectory());
                System.out.println("File Size (bytes): " + file.length());
            }
        }
    }

    Output:

    File Name: file.txt
    Path: file.txt
    Absolute Path: /home/user/file.txt
    Parent: null
    Exists: true
    Writable: true
    Readable: true
    Is Directory: false
    File Size (bytes): 100

    Example 2: Display Directory Contents

    This program accepts a directory path from the user and lists its contents.

    import java.io.File;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.IOException;
    
    class DirectoryContents {
        public static void main(String[] args) throws IOException {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    
            System.out.println("Enter directory path:");
            String dirPath = br.readLine();
    
            File dir = new File(dirPath);
    
            if (dir.exists() && dir.isDirectory()) {
                String[] contents = dir.list();
                System.out.println("Contents of " + dirPath + ":");
    
                for (String item : contents) {
                    File f = new File(dirPath, item);
                    if (f.isFile()) {
                        System.out.println(item + " (File)");
                    } else if (f.isDirectory()) {
                        System.out.println(item + " (Directory)");
                    }
                }
            } else {
                System.out.println("Directory not found.");
            }
        }
    }

    Output:

    Enter directory path:
    /home/user/docs
    Contents of /home/user/docs:
    file1.txt (File)
    file2.txt (File)
    subfolder (Directory)

    Java Program to Create a New File

    Steps to Create a New File in Java

    1. First Step: To create a new file in Java, we need to handle any potential exceptions properly. This is important because the file operations may throw exceptions. We will be using Java’s try-catch block for this purpose, which is one of the standard ways to handle exceptions.

    2. Second Step: Next, we will import the File class, which is required to work with files in Java.

    Example to create a File object:

    File fileObject = new File(directoryPath);

    Methods to Create a New File in Java

    There are two main ways to create a file in Java:

    1. Using the File class.

    2. Using the FileOutputStream class.

    These classes provide a variety of methods for performing file operations such as creating files, checking if a file exists, and more.

    Let’s now look at examples of both approaches.

    Example 1: Creating a File Using the File Class

    In this approach, we use the File class to create a new file. This class represents an abstract path and allows us to work with the file without requiring its physical existence until necessary.

    Code Example:

    // Import necessary libraries
    import java.io.File;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    
    public class FileCreator {
    
        public static void main(String[] args) {
            // Creating a new file using a custom method
            FileCreator creator = new FileCreator();
            creator.createNewFile();
        }
    
        // Method to create a new file
        public void createNewFile() {
            String filePath = "", fileName = "";
    
            // Use try-catch block to handle exceptions
            try {
                // Using BufferedReader to take user input for file name and path
                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
                System.out.println("Please enter the file name:");
    
                // Reading the file name from user input
                fileName = reader.readLine();
                System.out.println("Please enter the directory path:");
    
                // Reading the file path from user input
                filePath = reader.readLine();
    
                // Creating a new File object with the provided file name and path
                File file = new File(filePath + "/" + fileName + ".txt");
    
                // Method to create a new blank file
                if (file.createNewFile()) {
                    System.out.println("File created: " + file.getName());
                } else {
                    System.out.println("File already exists.");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    Please enter the file name:
    newFile
    Please enter the directory path:
    /Users/username/Desktop/Folder
    File created: newFile.txt

    Explanation:

    • The program prompts the user to enter a file name and directory path.
    • It uses the createNewFile() method from the File class to create the file.
    • If the file is successfully created, it outputs the file name and path.
    public static void main(String[] args) {
        // program logic
    }

    Example 2: Creating a File Using the FileOutputStream Class

    Another way to create a file in Java is by using the FileOutputStream class. This class is used to write data to a file, and it can also be used to create a new file if one doesn’t already exist.

    Code Example:

    // Import necessary libraries
    import java.io.FileOutputStream;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    
    public class FileCreator {
    
        public static void main(String[] args) {
            // Creating a new file using a custom method
            FileCreator creator = new FileCreator();
            creator.createNewFileWithStream();
        }
    
        // Method to create a new file using FileOutputStream
        public void createNewFileWithStream() {
            String filePath = "", fileName = "";
    
            // Use try-catch block to handle exceptions
            try {
                // Using BufferedReader to take user input for file name and path
                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
                System.out.println("Please enter the file name:");
    
                // Reading the file name from user input
                fileName = reader.readLine();
                System.out.println("Please enter the directory path:");
    
                // Reading the file path from user input
                filePath = reader.readLine();
    
                // Creating a new FileOutputStream object with the provided file name and path
                FileOutputStream fos = new FileOutputStream(filePath + "/" + fileName + ".txt");
                System.out.println("File created using FileOutputStream.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    Please enter the file name:
    newFile
    Please enter the directory path:
    /Users/username/Desktop/Folder
    File created using FileOutputStream.

    Java Program to Write into a File

    In Java, several classes and methods are available to write data into files. The FileWriter class is commonly used for writing character-oriented data into files, and it plays a crucial role in Java’s file handling system. This article will discuss multiple ways to write data into a file using Java.

    Methods to Write into a File in Java:

    1. Using writeString() Method
    2. Using FileWriter Class
    3. Using BufferedWriter Class
    4. Using FileOutputStream Class

    Method 1: Using writeString() Method

    Introduced in Java 11, the writeString() method allows writing text into a file. It requires the file path and character sequence as mandatory parameters. This method is simple and useful when the file content is relatively small.

    Example:

    // Import required classes
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    
    public class FileWriteExample {
    
        public static void main(String[] args) {
            // Define content to be written
            String content = "Hello, Java File Handling\nLet's write to a file!";
    
            // Define the path of the file
            Path filePath = Path.of("output.txt");
    
            try {
                // Write content to the file
                Files.writeString(filePath, content);
    
                // Read and print the content from the file
                String fileContent = Files.readString(filePath);
                System.out.println(fileContent);
    
            } catch (IOException e) {
                System.err.println("Error occurred: " + e.getMessage());
            }
        }
    }

    Output:

    Hello, Java File Handling
    Let's write to a file!

    Method 2: Using FileWriter Class

    FileWriter writes a stream of characters into a file. It’s ideal for smaller content. The following example demonstrates how to use the FileWriter class to write data to a file.

    Example:

    // Import necessary classes
    import java.io.FileWriter;
    import java.io.IOException;
    
    public class FileWriterExample {
    
        public static void main(String[] args) {
            // Content to write into the file
            String content = "Learning Java File Handling.";
    
            try {
                // Create FileWriter object
                FileWriter writer = new FileWriter("example.txt");
    
                // Write content to the file
                writer.write(content);
    
                // Print content
                System.out.println("Content written: " + content);
    
                // Close the writer
                writer.close();
    
                System.out.println("File has been written successfully.");
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    Output:

    Content written: Learning Java File Handling.
    File has been written successfully.

    Method 3: Using BufferedWriter Class

    The BufferedWriter class provides better performance when writing larger content because it uses a buffer to write text efficiently. It is recommended for writing large files.

    Example:

    // Import necessary classes
    import java.io.BufferedWriter;
    import java.io.FileWriter;
    import java.io.IOException;
    
    public class BufferedWriterExample {
    
        public static void main(String[] args) {
            // Content to write
            String content = "Buffered Writer in Java\nOptimized for large content.";
    
            try {
                // Create BufferedWriter object
                BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("buffered_output.txt"));
    
                // Write content to file
                bufferedWriter.write(content);
    
                // Print the content
                System.out.println("Content written: " + content);
    
                // Close the writer
                bufferedWriter.close();
    
                System.out.println("File written successfully.");
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    Output:

    Content written: Buffered Writer in Java
    Optimized for large content.
    File written successfully.

    Delete a File Using Java

    In Java, files can be permanently deleted using various methods, and unlike traditional delete operations in operating systems, these files do not go to the recycle bin or trash. Below is the method available in Java for deleting files.

    Using java.io.File.delete() Method

    The delete() method of the File class can be used to delete files or directories. It returns true if the file or directory is successfully deleted, and false otherwise.

    Syntax:

    public boolean delete()

    Returns:

    • true if the file or directory is deleted successfully.
    • false if the file or directory cannot be deleted.

    Example:

    import java.io.File;
    
    public class DeleteFileExample {
    
        public static void main(String[] args) {
            // Creating a file object
            File file = new File("example.txt");
    
            // Deleting the file
            if (file.delete()) {
                System.out.println("File deleted successfully.");
            } else {
                System.out.println("Failed to delete the file.");
            }
        }
    }

    Output:

    File deleted successfully.

    File Permissions in Java

    Java File Permission Management

    Java offers several methods to check and modify file permissions, which can be useful when restricting or allowing certain file operations. For instance, a file that is read-only can be updated to allow write operations or vice versa.

    Checking Current File Permissions

    A file in Java can be in any of the following permission states, and these can be checked using specific methods from the File class.

    MethodAction Performed
    canExecute()Returns true if the file is executable and the application has permission to execute the file.
    canRead()Returns true if the file is readable.
    canWrite()Returns true if the file is writable and the application has permission to write to the file.

    Example:

    This Java program demonstrates how to check a file’s current permissions (readable, writable, executable).

    import java.io.File;
    
    public class FilePermissionCheck {
    
        public static void main(String[] args) {
            // Create a file object
            File file = new File("sample.txt");
    
            // Check if the file exists
            if (file.exists()) {
                // Display current file permissions
                System.out.println("Executable: " + file.canExecute());
                System.out.println("Readable: " + file.canRead());
                System.out.println("Writable: " + file.canWrite());
            } else {
                System.out.println("File not found.");
            }
        }
    }

    Output:

    Executable: false
    Readable: true
    Writable: true
    Changing File Permissions

    Java provides several methods to alter file permissions for readability, writability, and executability.

    MethodAction Performed
    setExecutable()Sets the execute permission for the file.
    setReadable()Sets the read permission for the file.
    setWritable()Sets the write permission for the file.

    Note:

    • The setReadable() and setWritable() methods may fail if the underlying file system does not support changing permissions or if the user lacks appropriate privileges.

    Example:

    This program shows how to change a file’s permissions using Java methods.

    import java.io.File;
    
    public class FilePermissionModify {
    
        public static void main(String[] args) {
            // Create a file object
            File file = new File("document.txt");
    
            // Check if the file exists
            if (file.exists()) {
                // Modify the file permissions
                file.setExecutable(true);
                file.setReadable(true);
                file.setWritable(false);
                System.out.println("File permissions changed.");
    
                // Display updated permissions
                System.out.println("Executable: " + file.canExecute());
                System.out.println("Readable: " + file.canRead());
                System.out.println("Writable: " + file.canWrite());
            } else {
                System.out.println("File not found.");
            }
        }
    }

    Output:

    File permissions changed.
    Executable: true
    Readable: true
    Writable: false

    FileWriter Class in Java

    Java FileWriter Class Overview

    The Java FileWriter class, part of the java.io package, is used to write data in character format to files. It is designed for handling character-based input/output and extends the OutputStreamWriter class, which in turn inherits from the Writer class.

    Hierarchy of Java FileWriter Class
    • FileWriter extends the OutputStreamWriter and Writer classes.
    • It implements the CloseableFlushableAppendable, and AutoCloseable interfaces.

    The FileWriter class creates the output file if it doesn’t already exist. It is specifically meant for character-based writing. If you need to write raw bytes, the FileOutputStream class should be used instead.

    Constructors of FileWriter Class

    1. FileWriter(File file): Creates a FileWriter object using a File object.

    FileWriter fw = new FileWriter(new File("myfile.txt"));

    2. FileWriter(File file, boolean append): Allows appending to an existing file if append is true; otherwise, it overwrites the file.

    FileWriter fw = new FileWriter(new File("myfile.txt"), true);

    3. FileWriter(FileDescriptor fd): Constructs a FileWriter using a file descriptor.

    FileWriter fw = new FileWriter(FileDescriptor.out);

    4. FileWriter(File file, Charset charset): Creates a FileWriter using a specific file and character set.

    FileWriter fw = new FileWriter(new File("myfile.txt"), Charset.forName("UTF-8"));

    5. FileWriter(String fileName): Creates a FileWriter using a file name.

    FileWriter fw = new FileWriter("myfile.txt");

    6. FileWriter(String fileName, boolean append): Creates a FileWriter to append or overwrite based on the boolean value.

    FileWriter fw = new FileWriter("myfile.txt", true); // append

    Example 1: Writing Data to a File

    import java.io.FileWriter;
    import java.io.IOException;
    
    public class FileWriterExample {
    
        public static void main(String[] args) {
            String data = "Hello, World!";
            try {
                FileWriter fw = new FileWriter("example.txt");
                fw.write(data);
                System.out.println("Data written successfully.");
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    Data written successfully.

    The file example.txt will contain the text: Hello, World!.

    Overwriting vs Appending to a File
    • Overwrite: When a file is created using a constructor with only the file name, any existing data in the file will be replaced.
    FileWriter fw = new FileWriter("output.txt"); // overwrites
    • Append: To append data to an existing file, a second parameter true is passed.
    FileWriter fw = new FileWriter("output.txt", true); // appends

    Example : Appending Data to a File

    import java.io.FileWriter;
    import java.io.IOException;
    
    public class AppendFileExample {
    
        public static void main(String[] args) {
            String newData = " Welcome back!";
            try {
                FileWriter fw = new FileWriter("example.txt", true); // appending
                fw.write(newData);
                System.out.println("Data appended successfully.");
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    Data appended successfully.
    Basic Methods of FileWriter Class

    1. write(int a): Writes a single character.

    fw.write(65); // writes 'A' (ASCII value)

    2. write(char[] cbuf): Writes an array of characters.

    char[] data = {'H', 'i'};
    fw.write(data);

    3. write(String str): Writes a string to the file.

    fw.write("Hello");

    Example 4: Getting Encoding

    The getEncoding() method retrieves the character encoding used by the FileWriter.

    import java.io.FileWriter;
    import java.nio.charset.Charset;
    import java.io.IOException;
    
    public class EncodingExample {
    
        public static void main(String[] args) {
            try {
                FileWriter fw1 = new FileWriter("output1.txt");
                FileWriter fw2 = new FileWriter("output2.txt", Charset.forName("UTF-8"));
    
                System.out.println("Encoding of fw1: " + fw1.getEncoding());
                System.out.println("Encoding of fw2: " + fw2.getEncoding());
    
                fw1.close();
                fw2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    Encoding of fw1: Cp1252
    Encoding of fw2: UTF-8

    Java.io.FileDescriptor in Java

    java.io.FileDescriptor

    The java.io.FileDescriptor class represents an opaque handle to machine-specific structures such as open filessockets, or other byte streams. Its main use is to be passed to a FileInputStream or FileOutputStream for reading or writing data. Instances of this class should not be created by applications directly, as Java handles the underlying system details.

    The FileDescriptor class offers several useful methods and fields for interacting with files and streams, including handles to the standard input, output, and error streams.

    Fields:

    • err: Handle for the standard error stream.
    • in: Handle for the standard input stream.
    • out: Handle for the standard output stream.

    Declaration:

    public final class FileDescriptor
    extends Object

    Methods:

    1. sync():

    This method ensures that all the data in the file descriptor’s buffers is written to the actual device. It is especially useful for making sure that written data is saved immediately.

    Syntax:

    public void sync()

    2. Return Typevoid

    ExceptionSyncFailedException: Thrown if synchronization cannot be guaranteed.

    Example for sync() method:

    import java.io.*;
    
    public class SyncExample {
        public static void main(String[] args) throws IOException {
            FileDescriptor fileDescriptor = null;
            FileOutputStream fileOut = null;
    
            byte[] data = {65, 66, 67, 68};  // ASCII for 'ABCD'
    
            try {
                fileOut = new FileOutputStream("output.txt");
                fileDescriptor = fileOut.getFD();
    
                fileOut.write(data);
    
                // Synchronize data with the underlying device
                fileDescriptor.sync();
                System.out.println("Data synced successfully.");
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (fileOut != null) {
                    fileOut.close();
                }
            }
        }
    }

    Output:

    Data synced successfully.

    After running the above code, the file output.txt will contain the text:

    ABCD

    3. valid(): The valid() method checks if a FileDescriptor is valid.

    Syntax:

    public boolean valid()

    4. Return Type:

    • true: If the file descriptor is valid.
    • false: If it is invalid.

    Example:

    import java.io.*;
    
    public class ValidExample {
        public static void main(String[] args) throws IOException {
            FileDescriptor fileDescriptor = null;
            FileInputStream fileIn = null;
    
            try {
                fileIn = new FileInputStream("output.txt");
                fileDescriptor = fileIn.getFD();
    
                // Check if the FileDescriptor is valid
                boolean isValid = fileDescriptor.valid();
                System.out.println("FileDescriptor valid: " + isValid);
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (fileIn != null) {
                    fileIn.close();
                }
            }
        }
    }

    Output:

    FileDescriptor valid: true
  • Synchronization

    Synchronization in Java

    In multithreaded applications, there are instances where multiple threads attempt to access shared resources simultaneously, which can lead to inconsistencies and unexpected results.

    Why Use Synchronization in Java?

    Java provides synchronization to ensure that only one thread can access a shared resource at any given time, thus avoiding conflicts.

    Java Synchronized Blocks

    Java offers a way to synchronize the tasks performed by multiple threads through synchronized blocks. A synchronized block is synchronized on a specific object, which acts as a lock. Only one thread can execute the code within the synchronized block while holding the lock, and other threads must wait until the lock is released.

    General Form of Synchronized Block:

    synchronized(lock_object) {
       // Code that needs synchronized access
    }

    Example:

    import java.util.*;
    public class Main {
        public static void main(String[] args) {
            System.out.println("Hello, World!");
        }
    }

    Output:

    Hello, World!

    This mechanism is implemented using monitors or locks in Java. A thread must acquire a lock on the object before entering the synchronized block, and it releases the lock once the block is exited.

    Types of Synchronization:

    1. Process Synchronization: Coordinates the execution of multiple processes to ensure shared resources are managed safely.
    2. Thread Synchronization: Manages thread execution in multithreaded programs, with two main approaches:

    • Mutual Exclusion (Synchronized methods, synchronized blocks, static synchronization)
    • Cooperation (Inter-thread communication)

    Example:

    // Java program demonstrating synchronization
    
    class Task {
        public void performTask(String message) {
            System.out.println("Executing\t" + message);
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted.");
            }
            System.out.println("\n" + message + " Completed");
        }
    }
    
    class TaskRunner extends Thread {
        private String message;
        Task task;
    
        TaskRunner(String msg, Task taskInstance) {
            message = msg;
            task = taskInstance;
        }
    
        public void run() {
            synchronized (task) {
                task.performTask(message);
            }
        }
    }
    
    public class SyncExample {
        public static void main(String[] args) {
            Task task = new Task();
            TaskRunner runner1 = new TaskRunner("Task 1", task);
            TaskRunner runner2 = new TaskRunner("Task 2", task);
    
            runner1.start();
            runner2.start();
    
            try {
                runner1.join();
                runner2.join();
            } catch (Exception e) {
                System.out.println("Thread interrupted");
            }
        }
    }

    Output:

    Executing     Task 1
    
    Task 1 Completed
    Executing     Task 2
    
    Task 2 Completed
    Alternative Implementation Using Synchronized Method

    We can also define the entire method as synchronized to achieve the same behavior without explicitly synchronizing blocks inside the thread’s run() method:

    class Task {
        public synchronized void performTask(String message) {
            System.out.println("Executing\t" + message);
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted.");
            }
            System.out.println("\n" + message + " Completed");
        }
    }

    In this case, we don’t need to add the synchronized block in the run() method since the performTask() method itself is synchronized.

    Example with Partial Synchronization of a Method

    Sometimes, we may want to synchronize only part of the method instead of the entire method. Here’s how it can be done:

    class Task {
        public void performTask(String message) {
            synchronized (this) {
                System.out.println("Executing\t" + message);
                try {
                    Thread.sleep(800);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted.");
                }
                System.out.println("\n" + message + " Completed");
            }
        }
    }

    This is useful when only certain parts of the method need exclusive access to shared resources, while other parts can run concurrently.

    Example of Synchronized Method Using Anonymous Class

    class NumberPrinter {
        synchronized void printNumbers(int base) {
            for (int i = 1; i <= 3; i++) {
                System.out.println(base + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
        }
    }
    
    public class AnonymousSyncExample {
        public static void main(String[] args) {
            final NumberPrinter printer = new NumberPrinter();
    
            Thread thread1 = new Thread() {
                public void run() {
                    printer.printNumbers(10);
                }
            };
    
            Thread thread2 = new Thread() {
                public void run() {
                    printer.printNumbers(20);
                }
            };
    
            thread1.start();
            thread2.start();
        }
    }

    Output:

    11
    12
    13
    21
    22
    23

    Importance of Thread Synchronization in Java

    Introduction to Multithreading

    Multithreading is a technique where multiple parts of a program run simultaneously, optimizing resource utilization. Threads, which are essentially lightweight processes, enable concurrent execution within a single process. For instance, imagine you are editing a document in MS Word while also playing music and browsing the internet. These activities are different processes happening at the same time. Within each application, like a music player, there are multiple threads that handle tasks like loading songs, managing the playlist, and adjusting volume. In this way, threads represent smaller tasks within a larger process, and multithreading allows these tasks to run concurrently.

    Multithreading becomes particularly relevant when considering thread synchronization, which is crucial to avoid inconsistencies when multiple threads attempt to access shared resources simultaneously.

    Thread Priorities

    In Java, every thread has a priority that indicates how it should be treated compared to other threads. Threads with higher priorities may be given more CPU time or preempt threads with lower priorities. However, when two threads of the same priority compete for the same resource, managing their execution becomes more complex and can lead to errors.

    Consider a scenario where multiple computers are connected to a single printer. If two computers attempt to print documents at the same time, the printer could mix the two print jobs, producing invalid output. Similarly, when multiple threads with the same priority try to access a shared resource in a program, the results can become inconsistent.

    Java addresses this issue through thread synchronization.

    Thread Synchronization

    Thread synchronization ensures that only one thread can access a shared resource at a time, preventing interference between threads and avoiding data inconsistency. Synchronization in Java is implemented using locks or monitors. When a thread acquires a lock on a resource, no other thread can access it until the lock is released. This ensures safe and orderly execution of threads.

    There are two main types of synchronization:

    1. Mutual Exclusion : Mutual exclusion is a technique to prevent multiple threads from interfering with each other while sharing a resource. It can be implemented through:

    • Synchronized Methods
    • Synchronized Blocks
    • Static Synchronization
    • Synchronized Methods : Using the synchronized keyword, we can ensure that a method is executed by only one thread at a time, making it thread-safe. Example:
    class Printer {
        public void printJob(int jobNumber) {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Job " + jobNumber + " is printing...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    class Job1 extends Thread {
        Printer printer;
        Job1(Printer p) { printer = p; }
        public void run() { printer.printJob(1); }
    }
    
    class Job2 extends Thread {
        Printer printer;
        Job2(Printer p) { printer = p; }
        public void run() { printer.printJob(2); }
    }
    
    public class Main {
        public static void main(String[] args) {
            Printer p = new Printer();
            Job1 job1 = new Job1(p);
            Job2 job2 = new Job2(p);
            job1.start();
            job2.start();
        }
    }

    Output:

    Job 1 is printing...
    Job 2 is printing...
    Job 1 is printing...
    Job 2 is printing...
    ...

    In this output, the jobs are printing simultaneously, leading to mixed and overlapping output.

    Example 2: With Synchronized Method

    class Printer {
        synchronized public void printJob(int jobNumber) {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Job " + jobNumber + " is printing...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    class Job1 extends Thread {
        Printer printer;
        Job1(Printer p) { printer = p; }
        public void run() { printer.printJob(1); }
    }
    
    class Job2 extends Thread {
        Printer printer;
        Job2(Printer p) { printer = p; }
        public void run() { printer.printJob(2); }
    }
    
    public class Main {
        public static void main(String[] args) {
            Printer p = new Printer();
            Job1 job1 = new Job1(p);
            Job2 job2 = new Job2(p);
            job1.start();
            job2.start();
        }
    }

    Output:

    Job 1 is printing...
    Job 1 is printing...
    Job 1 is printing...
    ...
    --------------------------
    Job 2 is printing...
    Job 2 is printing...
    ...

    With synchronization, one job completes before the other starts, ensuring consistent output.

    •  Synchronized Block: synchronized block allows you to synchronize only a portion of the code rather than the entire method. This can be useful for optimizing performance when only specific code needs to be synchronized. Example:
    class Printer {
        public void printJob(int jobNumber) {
            synchronized(this) {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("Job " + jobNumber + " is printing...");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("Job " + jobNumber + " completed.");
        }
    }
    
    class Job1 extends Thread {
        Printer printer;
        Job1(Printer p) { printer = p; }
        public void run() { printer.printJob(1); }
    }
    
    class Job2 extends Thread {
        Printer printer;
        Job2(Printer p) { printer = p; }
        public void run() { printer.printJob(2); }
    }
    
    public class Main {
        public static void main(String[] args) {
            Printer p = new Printer();
            Job1 job1 = new Job1(p);
            Job2 job2 = new Job2(p);
            job1.start();
            job2.start();
        }
    }

    Output:

    Job 1 is printing...
    Job 1 is printing...
    ...
    Job 1 completed.
    Job 2 is printing...
    ...
    • Static Synchronization in Java : Static synchronization in Java is used to synchronize static methods. When a static method is synchronized, the class-level lock is obtained. This ensures that only one thread can access any of the static synchronized methods of a class at a time, even if multiple threads are accessing different objects of the class. Example:
    class Table {
        // Synchronized static method
        synchronized static void printTable(int n) {
            for (int i = 1; i <= 5; i++) {
                System.out.println(n * i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
        }
    }
    
    class MyThread1 extends Thread {
        public void run() {
            Table.printTable(5);
        }
    }
    
    class MyThread2 extends Thread {
        public void run() {
            Table.printTable(10);
        }
    }
    
    public class StaticSynchronizationExample {
        public static void main(String[] args) {
            MyThread1 t1 = new MyThread1();
            MyThread2 t2 = new MyThread2();
    
            t1.start();
            t2.start();
        }
    }

    Output:

    5
    10
    15
    20
    25
    50
    100
    150
    200
    250

    2. Inter-Thread Communication in Java: Inter-thread communication in Java allows threads to communicate with each other using methods like wait()notify(), and notifyAll(). These methods are part of the Object class and must be called within synchronized blocks or methods.

    This mechanism is useful when one thread needs to wait for a specific condition to be fulfilled by another thread.

    class SharedResource {
        private int data;
        private boolean isProduced = false;
    
        synchronized void produce(int value) {
            while (isProduced) {
                try {
                    wait(); // Wait until data is consumed
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
            data = value;
            System.out.println("Produced: " + data);
            isProduced = true;
            notify(); // Notify the consumer
        }
    
        synchronized void consume() {
            while (!isProduced) {
                try {
                    wait(); // Wait until data is produced
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
            System.out.println("Consumed: " + data);
            isProduced = false;
            notify(); // Notify the producer
        }
    }
    
    class Producer extends Thread {
        SharedResource resource;
    
        Producer(SharedResource resource) {
            this.resource = resource;
        }
    
        public void run() {
            for (int i = 1; i <= 5; i++) {
                resource.produce(i);
            }
        }
    }
    
    class Consumer extends Thread {
        SharedResource resource;
    
        Consumer(SharedResource resource) {
            this.resource = resource;
        }
    
        public void run() {
            for (int i = 1; i <= 5; i++) {
                resource.consume();
            }
        }
    }
    
    public class InterThreadCommunicationExample {
        public static void main(String[] args) {
            SharedResource resource = new SharedResource();
            Producer producer = new Producer(resource);
            Consumer consumer = new Consumer(resource);
    
            producer.start();
            consumer.start();
        }
    }

    Output:

    Produced: 1
    Consumed: 1
    Produced: 2
    Consumed: 2
    Produced: 3
    Consumed: 3
    Produced: 4
    Consumed: 4
    Produced: 5
    Consumed: 5

    Method and Block Synchronization

    Need for Synchronization

    In a multithreaded environment, multiple threads often share access to the same fields and objects. While this form of communication can be highly efficient, it introduces potential issues such as thread interference and memory consistency errors. Synchronization constructs are crucial to prevent these errors.

    Example of Synchronization Need

    Consider the following example:

    // Java program demonstrating the need for synchronization
    class Counter {
        private int count = 0;
    
        public void increment() {
            count++;
        }
    
        public int getCount() {
            return count;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            Counter counter = new Counter();
            counter.increment();
            System.out.println(counter.getCount());
        }
    }

    Output:

    1

    In this example, three key operations occur:

    1. Fetch the current value of count.
    2. Increment the fetched value.
    3. Store the new value back in the count variable.

    If multiple threads access this shared object, here’s what could happen:

    • Thread 1 fetches the value of count (initially 0), increments it to 1.
    • Thread 2 fetches the value of count, which is still 0 (because Thread 1 hasn’t saved it yet) and also increments it.

    After both threads complete, the value of count is incorrectly set to 1 instead of 2. This shows why synchronization is necessary.

    Synchronization in Java

    In Java, synchronization ensures that only one thread at a time can access a shared resource, preventing the corruption of the object’s state. If multiple threads are only reading shared resources without modification, synchronization is not necessary.

    Java provides two primary forms of synchronization:

    1. Method-level synchronization
    2. Block-level synchronization

    1. Method Synchronization

    Synchronized methods offer a straightforward way to prevent thread interference and memory consistency errors. If multiple threads call a synchronized method on the same object, only one thread can execute it at any given time.

    Here’s an example of unsynchronized access to a shared resource:

    // Example: Multiple threads accessing the same object without synchronization
    class Printer {
        public void printNumbers() {
            for (int i = 0; i < 3; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(500);  // Simulate time-consuming task
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
        }
    }
    
    class Worker extends Thread {
        Printer printer;
    
        Worker(Printer printer) {
            this.printer = printer;
        }
    
        public void run() {
            printer.printNumbers();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Printer printer = new Printer();
            Worker thread1 = new Worker(printer);
            Worker thread2 = new Worker(printer);
    
            thread1.start();
            thread2.start();
        }
    }

    Output:

    0
    0
    1
    1
    2
    2

    Multiple threads are accessing the shared Printer object simultaneously, leading to interleaved output.

    Now, let’s use synchronization:

    // Example: Synchronized method ensuring only one thread can access the method at a time
    class Printer {
        synchronized public void printNumbers() {
            for (int i = 0; i < 3; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
            }
        }
    }
    
    class Worker extends Thread {
        Printer printer;
    
        Worker(Printer printer) {
            this.printer = printer;
        }
    
        public void run() {
            printer.printNumbers();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Printer printer = new Printer();
            Worker thread1 = new Worker(printer);
            Worker thread2 = new Worker(printer);
    
            thread1.start();
            thread2.start();
        }
    }

    Output:

    0
    1
    2
    0
    1
    2

    Now, both threads access the printNumbers method in a synchronized way, ensuring that only one thread at a time can run it.

    2. Block Synchronization

    Sometimes, we need to synchronize only a portion of the code within a method instead of the entire method. This is called block-level synchronization. For example, if a method has 100 lines of code, but only 10 lines modify a shared resource, it’s efficient to synchronize just those 10 lines.

    Example:

    // Block synchronization example
    import java.util.List;
    import java.util.ArrayList;
    
    class Person {
        String name = "";
        public int changeCount = 0;
    
        public void updateName(String newName, List<String> nameList) {
            synchronized (this) {
                name = newName;
                changeCount++;  // Keep track of how many times name is updated
            }
    
            // The rest of the code does not need synchronization
            nameList.add(newName);
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            Person person = new Person();
            List<String> nameList = new ArrayList<>();
    
            person.updateName("Alice", nameList);
            System.out.println(person.name);
        }
    }

    Output:

    Alice

    Lock framework vs Thread synchronization

    Thread synchronization can also be achieved using the Lock framework, introduced in java.util.concurrent.locks package. The Lock framework provides more control over locks compared to synchronized blocks. While synchronized blocks lock the entire method or a block of code, the Lock framework allows more flexibility and advanced locking mechanisms.

    This new framework was introduced to address the limitations of traditional synchronization, such as when you have multiple methods that require synchronization. With traditional synchronized blocks, only one thread can access one synchronized method at a time. This can lead to performance issues, especially when many methods require synchronization.

    The Lock framework overcomes this limitation by allowing different locks to be assigned to different sets of methods, thus increasing concurrency and improving overall performance.

    Example Usage:

    Lock lock = new ReentrantLock();
    lock.lock();
    
    // Critical section
    lock.unlock();

    The lock() method acquires the lock, and unlock() releases it. It is essential to ensure that every call to lock() is followed by a corresponding call to unlock(). Forgetting to call unlock() will result in a deadlock.

    Key Considerations:

    • Acquiring a lock without releasing it will result in deadlock.
    • The number of lock() calls should always match the number of unlock() calls.
    • Unlocking without having first acquired the lock will throw an exception.

    Example Scenario

    In the following example, a shared resource class (Resource) contains two methods, each with its own lock. The DisplayTask and ReadTask classes represent two different jobs that will be executed by multiple threads. Using different locks for these two methods allows the tasks to be executed concurrently without interference.

    import java.util.Date;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // Main class demonstrating the Lock framework
    public class LockExample {
        public static void main(String[] args) {
            SharedResource resource = new SharedResource();
            Thread[] threads = new Thread[10];
    
            // Creating threads for DisplayTask
            for (int i = 0; i < 5; i++) {
                threads[i] = new Thread(new DisplayTask(resource), "Thread " + i);
            }
    
            // Creating threads for ReadTask
            for (int i = 5; i < 10; i++) {
                threads[i] = new Thread(new ReadTask(resource), "Thread " + i);
            }
    
            // Starting all threads
            for (int i = 0; i < 10; i++) {
                threads[i].start();
            }
        }
    }
    
    // Task for displaying a record
    class DisplayTask implements Runnable {
        private SharedResource resource;
    
        DisplayTask(SharedResource resource) {
            this.resource = resource;
        }
    
        @Override
        public void run() {
            System.out.println("Executing display task");
            resource.displayRecord();
        }
    }
    
    // Task for reading a record
    class ReadTask implements Runnable {
        private SharedResource resource;
    
        ReadTask(SharedResource resource) {
            this.resource = resource;
        }
    
        @Override
        public void run() {
            System.out.println("Executing read task");
            resource.readRecord();
        }
    }
    
    // Shared resource class with two methods having different locks
    class SharedResource {
        private final Lock displayLock = new ReentrantLock();
        private final Lock readLock = new ReentrantLock();
    
        // Synchronized displayRecord method using displayLock
        public void displayRecord() {
            displayLock.lock();
            try {
                long duration = (long) (Math.random() * 10000);
                System.out.println(Thread.currentThread().getName() + ": Displaying record for " + (duration / 1000) + " seconds :: " + new Date());
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + ": Display completed");
                displayLock.unlock();
            }
        }
    
        // Synchronized readRecord method using readLock
        public void readRecord() {
            readLock.lock();
            try {
                long duration = (long) (Math.random() * 10000);
                System.out.println(Thread.currentThread().getName() + ": Reading record for " + (duration / 1000) + " seconds :: " + new Date());
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + ": Read completed");
                readLock.unlock();
            }
        }
    }

    Output:

    Executing display task
    Executing display task
    Executing display task
    Executing display task
    Executing display task
    Executing read task
    Executing read task
    Executing read task
    Executing read task
    Executing read task
    Thread 0: Displaying record for 3 seconds :: Mon Oct 16 11:59:42 IST 2024
    Thread 5: Reading record for 5 seconds :: Mon Oct 16 11:59:42 IST 2024
    Thread 0: Display completed
    Thread 1: Displaying record for 4 seconds :: Mon Oct 16 11:59:45 IST 2024
    Thread 5: Read completed
    Thread 6: Reading record for 5 seconds :: Mon Oct 16 11:59:47 IST 2024
    Thread 1: Display completed
    Differences Between Lock and Synchronized:
    FeatureLock FrameworkSynchronized
    Method FlexibilityLock can be implemented across different methods.Synchronized cannot be shared across methods.
    Try to Acquire LockSupports tryLock() with timeout to attempt acquiring the lock.Not supported.
    Fair Lock ManagementYes, with fair lock option for long-waiting threads.Not supported.
    Waiting Threads ListYou can see the list of waiting threads.Not possible.
    Handling ExceptionsNeeds careful handling to avoid leaving a lock held during exceptions.Synchronized automatically releases the lock.

    Deadlock in Java Multithreading

    The synchronized keyword in Java is used to ensure that a class or method is thread-safe, meaning that only one thread at a time can hold the lock for the synchronized method or block. This forces other threads to wait until the lock is released. It becomes essential in multi-threaded environments where multiple threads are running concurrently. However, this can also introduce a problem known as deadlock.

    What is Deadlock?

    Deadlock occurs when two or more threads are blocked forever, each waiting on the other to release the lock. This happens when multiple synchronized blocks or methods are trying to acquire locks on each other’s objects, leading to an indefinite waiting state.

    Example of Deadlock

    In the following example, two threads attempt to call synchronized methods on two shared objects. This causes a deadlock because each thread holds a lock that the other needs in order to proceed.

    class Utility {
        // Method to sleep the current thread for a specified time
        static void sleep(long millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    // Shared class used by both threads
    class SharedResource {
        // Synchronized method that locks the current object and tries to call another method
        synchronized void method1(SharedResource resource) {
            System.out.println(Thread.currentThread().getName() + " enters method1 of " + this);
            Utility.sleep(1000);
            resource.method2();
            System.out.println(Thread.currentThread().getName() + " exits method1 of " + this);
        }
    
        synchronized void method2() {
            System.out.println(Thread.currentThread().getName() + " enters method2 of " + this);
            Utility.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " exits method2 of " + this);
        }
    }
    
    // Thread1 tries to call method1 on shared resources
    class ThreadOne extends Thread {
        private SharedResource resource1;
        private SharedResource resource2;
    
        public ThreadOne(SharedResource resource1, SharedResource resource2) {
            this.resource1 = resource1;
            this.resource2 = resource2;
        }
    
        @Override
        public void run() {
            resource1.method1(resource2);
        }
    }
    
    // Thread2 tries to call method1 on shared resources
    class ThreadTwo extends Thread {
        private SharedResource resource1;
        private SharedResource resource2;
    
        public ThreadTwo(SharedResource resource1, SharedResource resource2) {
            this.resource1 = resource1;
            this.resource2 = resource2;
        }
    
        @Override
        public void run() {
            resource2.method1(resource1);
        }
    }
    
    public class DeadlockExample {
        public static void main(String[] args) {
            SharedResource resource1 = new SharedResource();
            SharedResource resource2 = new SharedResource();
    
            ThreadOne threadOne = new ThreadOne(resource1, resource2);
            threadOne.setName("Thread1");
            threadOne.start();
    
            ThreadTwo threadTwo = new ThreadTwo(resource1, resource2);
            threadTwo.setName("Thread2");
            threadTwo.start();
    
            Utility.sleep(2000);  // Allow threads to attempt execution
        }
    }

    Output:

    Thread1 enters method1 of SharedResource@1540e19d
    Thread2 enters method1 of SharedResource@677327b6

    Explanation:

    1. Thread1 acquires a lock on resource1 and enters the method1() method.
    2. At the same time, Thread2 acquires a lock on resource2 and enters its method1() method.
    3. Both threads try to acquire locks on the other’s resource (i.e., Thread1 tries to lock resource2, and Thread2 tries to lock resource1), causing a deadlock where both are stuck indefinitely waiting for the other to release the lock.

    Detecting Deadlock

    Deadlock detection can be done by collecting a thread dump of your program. On Windows, you can use the following command:

    jcmd <PID> Thread.print

    Where <PID> is the Process ID of your running program, which can be obtained using the jps command. The thread dump will reveal if a deadlock condition has occurred by indicating threads that are waiting for each other.

    Avoiding Deadlock

    While deadlock is difficult to eliminate entirely, you can reduce its likelihood by adopting the following practices:

    1. Avoid Nested Locks: Deadlock often occurs when multiple locks are required. Try to avoid locking more than one resource at a time.
    2. Avoid Unnecessary Locks: Only lock resources when absolutely necessary to minimize contention.
    3. Use Thread Join with Timeout: Deadlocks can occur when threads wait indefinitely for each other. Using Thread.join() with a timeout ensures that threads won’t wait forever and can recover if necessary.

    Reentrant Lock in Java

    In Java, traditional thread synchronization is typically achieved through the use of the synchronized keyword. While this provides basic synchronization, it can be somewhat rigid. For instance, a thread can acquire a lock only once, and there’s no built-in mechanism to manage waiting threads. This can lead to situations where certain threads are starved of resources, potentially for long periods of time.

    To offer more flexibility, Java provides ReentrantLocks, which are part of the java.util.concurrent.locks package. They allow more advanced control over thread synchronization, overcoming some of the limitations of the synchronized keyword.

    What are Reentrant Locks?

    The ReentrantLock class implements the Lock interface and provides synchronization capabilities for methods accessing shared resources. In code, you can wrap the critical section (the part of the code that manipulates shared resources) with lock() and unlock() calls. This ensures that only the thread holding the lock can access the shared resource, while others are blocked.

    As the name suggests, ReentrantLock allows the same thread to acquire the lock multiple times. Each time a thread locks the resource, a “hold count” is incremented. This count is decremented each time the thread calls unlock(), and the resource is only truly unlocked when the count reaches zero.

    A notable feature of ReentrantLock is its fairness parameter. When fairness is enabled (by passing true to the constructor), the lock is granted to the thread that has been waiting the longest, thereby preventing thread starvation.

    Example Usage

    public void someMethod() {
        reentrantLock.lock();
        try {
            // Perform operations on shared resource
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    The unlock() call is placed inside the finally block to ensure that the lock is released even if an exception occurs during the execution of the try block.

    Key Methods of ReentrantLock
    • lock(): Acquires the lock and increments the hold count.
    • unlock(): Decrements the hold count. When the count reaches zero, the lock is released.
    • tryLock(): Attempts to acquire the lock without blocking. If the lock is available, the method returns true; otherwise, it returns false.
    • tryLock(long timeout, TimeUnit unit): Waits for the specified time to acquire the lock before giving up.
    • lockInterruptibly(): Acquires the lock unless the thread is interrupted.
    • getHoldCount(): Returns the number of times the current thread has acquired the lock.
    • isHeldByCurrentThread(): Checks if the lock is held by the current thread.
    • isLocked(): Checks if the lock is held by any thread.
    • hasQueuedThreads(): Checks if any threads are waiting to acquire the lock.
    • newCondition(): Returns a Condition object for more complex thread interactions.

    ReentrantLock Example

    Below is an example that demonstrates how to use ReentrantLock in a multi-threaded scenario:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.ReentrantLock;
    
    class Worker implements Runnable {
        private String taskName;
        private ReentrantLock lock;
    
        public Worker(ReentrantLock lock, String taskName) {
            this.lock = lock;
            this.taskName = taskName;
        }
    
        @Override
        public void run() {
            boolean taskCompleted = false;
    
            while (!taskCompleted) {
                if (lock.tryLock()) {
                    try {
                        // Outer lock acquired
                        Date now = new Date();
                        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
                        System.out.println(taskName + " - Acquired outer lock at " + formatter.format(now));
                        Thread.sleep(1000);
    
                        // Inner lock
                        lock.lock();
                        try {
                            now = new Date();
                            System.out.println(taskName + " - Acquired inner lock at " + formatter.format(now));
                            System.out.println("Hold count: " + lock.getHoldCount());
                            Thread.sleep(1000);
                        } finally {
                            // Release inner lock
                            System.out.println(taskName + " - Releasing inner lock");
                            lock.unlock();
                        }
                        taskCompleted = true;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // Release outer lock
                        System.out.println(taskName + " - Releasing outer lock");
                        lock.unlock();
                    }
                } else {
                    System.out.println(taskName + " - Waiting for lock");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    public class ReentrantLockExample {
        private static final int THREAD_COUNT = 3;
    
        public static void main(String[] args) {
            ReentrantLock lock = new ReentrantLock();
            ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
    
            executorService.execute(new Worker(lock, "Task1"));
            executorService.execute(new Worker(lock, "Task2"));
            executorService.execute(new Worker(lock, "Task3"));
    
            executorService.shutdown();
        }
    }

    Output:

    Task1 - Acquired outer lock at 10:15:30
    Task2 - Waiting for lock
    Task3 - Waiting for lock
    Task1 - Acquired inner lock at 10:15:31
    Hold count: 2
    Task1 - Releasing inner lock
    Task1 - Releasing outer lock
    Task2 - Acquired outer lock at 10:15:32
    Task2 - Acquired inner lock at 10:15:33
    Hold count: 2
    Task2 - Releasing inner lock
    Task2 - Releasing outer lock
    Task3 - Acquired outer lock at 10:15:34
    Task3 - Acquired inner lock at 10:15:35
    Hold count: 2
    Task3 - Releasing inner lock
    Task3 - Releasing outer lock

    Difference Between Lock and Monitor in Java Concurrency

    Java Concurrency

    Java concurrency involves handling multiple threads to maximize CPU usage by ensuring efficient processing of tasks and reducing idle CPU time. The need for synchronization in multithreading has given rise to constructs like locks (or mutex) and monitors, which help control access to shared resources. Originally, locks were used for thread synchronization, but later, monitors provided a more robust and error-free mechanism.

    Before diving into the differences between locks and monitors, let’s first look at their individual characteristics.

    Overview of Lock (Mutex)

    Locks were originally used as part of thread management to control access to shared resources. Threads would check flags to determine whether a resource was available (unlocked) or in use (locked). Now, Java provides a more explicit way of using locks through the Lock interface in the concurrency API. This method gives developers better control over locking mechanisms than the traditional implicit locking provided by monitors.

    Here is an example that demonstrates basic lock usage:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class SharedResource {
        private Lock lock = new ReentrantLock();
    
        public void performTask(String threadName) {
            lock.lock();  // Acquiring lock
            try {
                System.out.println(threadName + " has acquired the lock.");
                Thread.sleep(1000); // Simulating some work
                System.out.println(threadName + " is performing the task.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // Releasing lock
                System.out.println(threadName + " has released the lock.");
            }
        }
    }
    
    public class LockExample {
        public static void main(String[] args) {
            SharedResource resource = new SharedResource();
            Thread t1 = new Thread(() -> resource.performTask("Thread 1"));
            Thread t2 = new Thread(() -> resource.performTask("Thread 2"));
    
            t1.start();
            t2.start();
        }
    }
    Overview of Monitor

    Monitors provide a more structured approach to synchronization in Java. They ensure that only one thread at a time can access a critical section of code. Monitors are implemented in Java through the synchronized keyword (applied to methods or code blocks), ensuring mutual exclusion between threads. Additionally, they allow threads to cooperate when working on shared tasks.

    Let’s look at a simple example where two threads are synchronized to use a shared resource:

    class SharedPrinter {
    
        // Synchronized method to ensure one thread at a time
        synchronized public void printMessage(String message) {
            for (char c : message.toCharArray()) {
                System.out.print(c);
                try {
                    Thread.sleep(100);  // Simulate some delay
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println();
        }
    }
    
    class PrinterThread extends Thread {
        private SharedPrinter printer;
        private String message;
    
        public PrinterThread(SharedPrinter printer, String message) {
            this.printer = printer;
            this.message = message;
        }
    
        public void run() {
            printer.printMessage(message);
        }
    }
    
    public class MonitorExample {
        public static void main(String[] args) {
            SharedPrinter printer = new SharedPrinter();
            Thread t1 = new PrinterThread(printer, "Hello");
            Thread t2 = new PrinterThread(printer, " World!");
    
            t1.start();
            t2.start();
        }
    }

    Output:

    HWeolrlod!
    Key Differences Between Lock and Monitor in Java
    Lock (Mutex)Monitor
    Locks have been around since the early stages of multithreading.Monitors were introduced later in the evolution of concurrency mechanisms.
    Typically implemented as a flag or data field to manage coordination.Synchronization is built-in using Java’s synchronized keyword or similar constructs.
    Critical section and locking mechanisms are managed by the thread itself.Mutual exclusion and cooperation are managed by the shared resource (or monitor).
    Threads handle the synchronization, making it prone to errors in certain cases, such as when a thread’s time slice expires before releasing the lock.Monitors manage synchronization more efficiently, especially in small thread pools, but may face challenges when inter-thread communication is necessary.
    Lock-based mechanisms are relatively less structured and leave synchronization entirely to threads.Monitors provide a structured and robust synchronization approach at the object level.
    Queuing of threads is either managed by the operating system or absent.Threads are queued and managed directly by the shared object being accessed.
    Less common and used only when explicit control is required.Monitors are widely used as they inherently use inter-thread locks.
  • Multithreading in Java

    Introduction

    Multithreading is a key feature in Java that enables the concurrent execution of multiple parts of a program, maximizing CPU utilization. Each independent path of execution is called a thread, which is a lightweight process.

    Java provides two primary ways to create threads:

    1. By extending the Thread class
    2. By implementing the Runnable interface

    Creating a Thread by Extending the Thread Class

    In this approach, you create a class that extends java.lang.Thread and override the run() method. The thread starts when you call start(), which internally invokes run() on a new thread.

    Example

    // Java code for thread creation by extending the Thread class
    class ThreadDemo extends Thread {
        public void run() {
            try {
                // Displaying the ID of the thread that is running
                System.out.println("Thread " + Thread.currentThread().getId() + " is active");
            } catch (Exception e) {
                System.out.println("An exception occurred");
            }
        }
    }
    
    // Main Class
    public class ThreadExample {
        public static void main(String[] args) {
            int numberOfThreads = 5;
            for (int i = 0; i < numberOfThreads; i++) {
                ThreadDemo threadInstance = new ThreadDemo();
                threadInstance.start();
            }
        }
    }
    

    Sample Output (varies by system):

    Thread 11 is active
    Thread 13 is active
    Thread 12 is active
    Thread 15 is active
    Thread 14 is active
    

    Creating a Thread by Implementing the Runnable Interface

    In this approach, you implement the Runnable interface and define the thread task inside run(). Then you pass the Runnable object to a Thread and call start().

    Example

    // Java code for thread creation by implementing the Runnable interface
    class ThreadRunnableDemo implements Runnable {
        public void run() {
            try {
                System.out.println("Thread " + Thread.currentThread().getId() + " is active");
            } catch (Exception e) {
                System.out.println("An exception occurred");
            }
        }
    }
    
    // Main Class
    public class RunnableExample {
        public static void main(String[] args) {
            int numberOfThreads = 5;
            for (int i = 0; i < numberOfThreads; i++) {
                Thread threadInstance = new Thread(new ThreadRunnableDemo());
                threadInstance.start();
            }
        }
    }
    

    Why Runnable is often preferred: your class can still extend another class (Java doesn’t allow multiple inheritance of classes).


    Thread Life Cycle (Thread States)

    A thread can exist in only one state at a time. The main states are:

    1. New
    2. Runnable
    3. Blocked
    4. Waiting
    5. Timed Waiting
    6. Terminated

    State Explanations

    • New: Thread object is created but start() hasn’t been called.
    • Runnable: Thread is ready to run (it may actually be running or waiting for CPU).
    • Blocked: Thread is waiting to acquire a monitor lock (e.g., trying to enter a synchronized block).
    • Waiting: Thread waits indefinitely (e.g., join() or wait() without timeout).
    • Timed Waiting: Thread waits for a fixed time (e.g., sleep(ms) or wait(timeout)).
    • Terminated: Thread has finished execution.

    Java Enum Constants for Thread States

    Java provides these states via Thread.State:

    • NEW
    • RUNNABLE
    • BLOCKED
    • WAITING
    • TIMED_WAITING
    • TERMINATED

    Example: Demonstrating Thread States

    // Java program to demonstrate thread states
    class MyThread implements Runnable {
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("State of thread1 after calling join() on thread2 - "
                    + ThreadExample.thread1.getState());
    
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public class ThreadExample implements Runnable {
        public static Thread thread1;
        public static ThreadExample obj;
    
        public static void main(String[] args) {
            obj = new ThreadExample();
            thread1 = new Thread(obj);
    
            System.out.println("State of thread1 after creating it - " + thread1.getState());
            thread1.start();
    
            System.out.println("State of thread1 after calling .start() method - " + thread1.getState());
        }
    
        public void run() {
            MyThread task = new MyThread();
            Thread thread2 = new Thread(task);
    
            System.out.println("State of thread2 after creating it - " + thread2.getState());
            thread2.start();
    
            System.out.println("State of thread2 after calling .start() method - " + thread2.getState());
    
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("State of thread2 after calling .sleep() method - " + thread2.getState());
    
            try {
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("State of thread2 after it has completed execution - " + thread2.getState());
        }
    }
    

    Java Thread Priority in Multithreading

    Thread priority is an integer from 1 to 10:

    • Thread.MIN_PRIORITY = 1
    • Thread.NORM_PRIORITY = 5 (default)
    • Thread.MAX_PRIORITY = 10

    You can use:

    • getPriority() to read priority
    • setPriority(int) to change priority

    Note: Priority influences scheduling, but the OS/JVM scheduler may still behave differently.

    Example: Getting and Setting Priorities

    class PriorityExample extends Thread {
        public void run() {
            System.out.println("Inside run method of thread: " + Thread.currentThread().getName());
        }
    
        public static void main(String[] args) {
            PriorityExample t1 = new PriorityExample();
            PriorityExample t2 = new PriorityExample();
            PriorityExample t3 = new PriorityExample();
    
            System.out.println("t1 priority: " + t1.getPriority());
            System.out.println("t2 priority: " + t2.getPriority());
            System.out.println("t3 priority: " + t3.getPriority());
    
            t1.setPriority(3);
            t2.setPriority(7);
            t3.setPriority(10);
    
            System.out.println("Updated t1 priority: " + t1.getPriority());
            System.out.println("Updated t2 priority: " + t2.getPriority());
            System.out.println("Updated t3 priority: " + t3.getPriority());
        }
    }
    

    Child Thread Inherits Parent Priority

    class ChildThreadExample extends Thread {
        public void run() {
            System.out.println("Running thread: " + Thread.currentThread().getName());
        }
    
        public static void main(String[] args) {
            Thread.currentThread().setPriority(6);
            System.out.println("Main thread priority: " + Thread.currentThread().getPriority());
    
            ChildThreadExample t1 = new ChildThreadExample();
            System.out.println("Child thread priority: " + t1.getPriority());
        }
    }
    

    Main Thread in Java

    When the Java program starts, the first thread is the main thread.
    It often:

    1. creates child threads
    2. finishes last (because it may coordinate shutdown)

    Example: Controlling the Main Thread

    public class MainThreadControl {
        public static void main(String[] args) {
    
            Thread mainThread = Thread.currentThread();
    
            System.out.println("Current thread: " + mainThread.getName());
    
            mainThread.setName("PrimaryThread");
            System.out.println("After name change: " + mainThread.getName());
    
            System.out.println("Main thread priority: " + mainThread.getPriority());
    
            mainThread.setPriority(Thread.MAX_PRIORITY);
            System.out.println("Main thread new priority: " + mainThread.getPriority());
        }
    }
    

    Deadlock Using Main Thread (Concept Example)

    Calling join() on the current thread causes it to wait for itself → deadlock.

    public class DeadlockExample {
        public static void main(String[] args) {
            try {
                System.out.println("Entering Deadlock");
                Thread.currentThread().join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    Thread Naming in Java

    1) Naming at Creation (Direct Method)

    class CustomThread extends Thread {
        CustomThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            System.out.println(getName() + " is running...");
        }
    }
    
    public class ThreadNamingDemo {
        public static void main(String[] args) {
            CustomThread t1 = new CustomThread("Worker1");
            CustomThread t2 = new CustomThread("Worker2");
    
            System.out.println("Thread 1 name: " + t1.getName());
            System.out.println("Thread 2 name: " + t2.getName());
    
            t1.start();
            t2.start();
        }
    }
    

    2) Naming with setName() (Indirect Method)

    class TaskThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " executing...");
        }
    }
    
    public class ThreadRenameDemo {
        public static void main(String[] args) {
            TaskThread thread1 = new TaskThread();
            TaskThread thread2 = new TaskThread();
    
            System.out.println("Initial Thread 1 name: " + thread1.getName());
            System.out.println("Initial Thread 2 name: " + thread2.getName());
    
            thread1.setName("TaskRunner1");
            thread2.setName("TaskRunner2");
    
            thread1.start();
            thread2.start();
        }
    }
    

    Fetching the Current Thread Name

    public class CurrentThreadDemo {
        public static void main(String[] args) {
            System.out.println("Current thread: " + Thread.currentThread().getName());
        }
    }
    

    What Does start() Do?

    Why start() instead of calling run()?

    • start() creates a new thread and a new call stack
    • JVM then calls run() on that new thread
    • Calling run() directly runs on the same thread (no new thread created)

    Example: run() vs start()

    class CustomThread extends Thread {
        @Override
        public void run() {
            System.out.println("Executing on thread ID: " + Thread.currentThread().getId());
        }
    }
    
    public class RunVsStartDemo {
        public static void main(String[] args) {
            CustomThread t = new CustomThread();
            t.run();   // runs in main thread
            t.start(); // runs in a new thread
        }
    }
    

    Thread.sleep() in Java

    sleep() pauses the current thread temporarily.

    ✅ Java provides two overloads:

    1. Thread.sleep(long millis)
    2. Thread.sleep(long millis, int nanos)

    It throws InterruptedException.

    Example 1: Using sleep() in Main Thread

    public class MainThreadSleepExample {
        public static void main(String[] args) {
            try {
                for (int i = 1; i <= 4; i++) {
                    Thread.sleep(2000);
                    System.out.println("Iteration: " + i);
                }
            } catch (InterruptedException e) {
                System.out.println("Main thread interrupted: " + e);
            }
        }
    }
    

    Example 2: Using sleep() in a Custom Thread

    class CustomThreadSleepExample extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println("Custom Thread Iteration: " + i);
                }
            } catch (InterruptedException e) {
                System.out.println("Custom thread interrupted: " + e);
            }
        }
    
        public static void main(String[] args) {
            new CustomThreadSleepExample().start();
        }
    }
    

    Example 3: Negative Sleep Time → IllegalArgumentException

    public class NegativeSleepTimeExample {
        public static void main(String[] args) {
            try {
                Thread.sleep(-500);
            } catch (IllegalArgumentException e) {
                System.out.println("Caught IllegalArgumentException: Timeout value is negative");
            } catch (InterruptedException e) {
                System.out.println("Interrupted: " + e);
            }
        }
    }

  • Exception

    Exception in java

    Exception Handling in Java is one of the effective means to manage runtime errors and preserve the regular flow of the application. Java’s mechanism for handling runtime errors like ClassNotFoundExceptionIOExceptionSQLException, and RemoteException ensures that exceptions are caught and handled appropriately.

    What are Java Exceptions?

    In Java, an Exception is an unwanted or unexpected event that occurs during the execution of a program, i.e., at runtime, which disrupts the normal flow of the program. Java provides mechanisms to catch and handle exceptions using the try-catch block. When an exception occurs, an exception object is created, containing information such as the name, description, and the program state at the time the exception occurred.

    Major Reasons for Exceptions:
    • Invalid user input
    • Device failure
    • Loss of network connection
    • Out of disk memory
    • Code errors
    • Array index out of bounds
    • Null reference
    • Type mismatch
    • Attempt to open an unavailable file
    • Database errors
    • Arithmetic errors (e.g., division by zero)

    Errors like memory leaksstack overflow, and out of memory are irrecoverable conditions typically beyond the control of the programmer. Errors should not be handled.

    Difference between Error and Exception:
    • Error: Represents a serious problem that the application should not attempt to catch.
    • Exception: Indicates a condition that a reasonable application might attempt to catch and handle.
    Exception Hierarchy

    In Java, all exceptions and errors are subclasses of the Throwable class. The two branches are:

    • Exception: User-defined and built-in exceptions such as NullPointerException.
    • Error: System-level errors like StackOverflowError, indicating issues with the JVM.
    Types of Exceptions

    1. Built-in Exceptions: Java has a wide range of built-in exceptions divided into two categories:

    • Checked Exceptions: These exceptions are checked at compile-time. Examples include IOExceptionSQLException.
    • Unchecked Exceptions: These exceptions occur at runtime and are not checked during compilation. Examples include ArrayIndexOutOfBoundsException and NullPointerException.

    2. User-Defined Exceptions: When built-in exceptions do not adequately describe an issue, Java allows for the creation of custom exceptions.

    Example of Exception Handling Methods:

    1. printStackTrace() Prints the name, description, and stack trace of the exception.

    public class Main {
        public static void main(String[] args) {
            try {
                int a = 5;
                int b = 0;
                System.out.println(a / b);
            } catch (ArithmeticException e) {
                e.printStackTrace();
            }
        }
    }

    Output:

    java.lang.ArithmeticException: / by zero
    at Main.main(Main.java:5)

    2. toString() Prints the name and description of the exception.

    public class Main {
        public static void main(String[] args) {
            try {
                int a = 5;
                int b = 0;
                System.out.println(a / b);
            } catch (ArithmeticException e) {
                System.out.println(e.toString());
            }
        }
    }

    Output:

    java.lang.ArithmeticException: / by zero

    3. getMessage() Prints only the description of the exception.

    public class Main {
        public static void main(String[] args) {
            try {
                int a = 5;
                int b = 0;
                System.out.println(a / b);
            } catch (ArithmeticException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    Output:

    / by zero
    JVM Exception Handling Flow

    When an exception occurs in a method, the method creates an Exception Object and passes it to the JVM. The JVM looks for an appropriate exception handler in the call stack, starting with the method where the exception occurred and moving backward. If no handler is found, the default exception handler terminates the program and prints the exception details.

    Example of JVM Handling:

    public class Main {
        public static void main(String[] args) {
            String str = null;
            System.out.println(str.length()); // NullPointerException
        }
    }

    Output:

    Exception in thread "main" java.lang.NullPointerException
    at Main.main(Main.java:4)
    Programmer Handling Exception with Custom Code:

    Using trycatchfinallythrow, and throws, Java allows programmers to handle exceptions gracefully.

    Example:

    public class Main {
        static int divideByZero(int a, int b) {
            return a / b;  // ArithmeticException if b is 0
        }
    
        static int computeDivision(int a, int b) {
            try {
                return divideByZero(a, b);
            } catch (NumberFormatException e) {
                System.out.println("NumberFormatException occurred");
                return 0;
            }
        }
    
        public static void main(String[] args) {
            try {
                int result = computeDivision(10, 0);
                System.out.println("Result: " + result);
            } catch (ArithmeticException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }
    }

    Output:

    Error: / by zero

    Try-Catch Clause Usage Example:

    public class Main {
        public static void main(String[] args) {
            int[] arr = new int[4];
            try {
                int value = arr[4]; // This will throw ArrayIndexOutOfBoundsException
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("Array index is out of bounds.");
            }
            System.out.println("Program continues...");
        }
    }

    Output:

    Array index is out of bounds.
    Program continues...

    Types of Exception in Java

    Built-in Exceptions in Java

    Java has several pre-defined exceptions that relate to its standard library classes. These exceptions help explain certain error conditions and can be caught and handled. Below are some important built-in exceptions:

    • ArithmeticException: Thrown when an illegal arithmetic operation is performed, like division by zero.
    • ArrayIndexOutOfBoundsException: Occurs when attempting to access an array with an invalid index, either negative or beyond the array’s length.
    • ClassNotFoundException: Triggered when an application tries to load a class that cannot be found.
    • FileNotFoundException: Raised when attempting to access a file that does not exist or is unavailable.
    • IOException: Signals an issue with input-output operations, such as reading from a file.
    • InterruptedException: Happens when a thread is interrupted during sleep, waiting, or performing certain tasks.
    • NoSuchFieldException: Raised when trying to access a class field that does not exist.
    • NoSuchMethodException: Raised when attempting to invoke a method that doesn’t exist.
    • NullPointerException: Occurs when trying to call a method on an object reference that is null.
    • NumberFormatException: Thrown when trying to convert a string into a number but the string is not a valid number.
    • RuntimeException: Represents an error during program execution that is not checked at compile time.
    • StringIndexOutOfBoundsException: Thrown when attempting to access characters outside of a string’s bounds.
    • IllegalArgumentException: Raised when a method receives an inappropriate argument.
    • IllegalStateException: Triggered when a method is invoked at an illegal or inappropriate time.

    Examples of Built-in Exceptions

    • ArithmeticException
    class ArithmeticExceptionExample {
        public static void main(String[] args) {
            try {
                int num1 = 50, num2 = 0;
                int result = num1 / num2;  // Cannot divide by zero
                System.out.println("Result: " + result);
            } catch (ArithmeticException e) {
                System.out.println("Division by zero is not allowed.");
            }
        }
    }

    Output:

    Division by zero is not allowed.
    • NullPointerException
    class NullPointerExceptionExample {
        public static void main(String[] args) {
            try {
                String str = null;
                System.out.println(str.length()); // Null reference
            } catch (NullPointerException e) {
                System.out.println("Caught a NullPointerException.");
            }
        }
    }

    Output:

    Caught a NullPointerException.
    • StringIndexOutOfBoundsException
    class StringIndexOutOfBoundsExceptionExample {
        public static void main(String[] args) {
            try {
                String sample = "Java is fun"; // Length is 11
                char ch = sample.charAt(15);   // Accessing out of bounds
            } catch (StringIndexOutOfBoundsException e) {
                System.out.println("String index out of bounds.");
            }
        }
    }

    Output:

    String index out of bounds.
    • FileNotFoundException
    import java.io.*;
    
    class FileNotFoundExceptionExample {
        public static void main(String[] args) {
            try {
                File file = new File("C://invalid_path.txt");
                FileReader reader = new FileReader(file);
            } catch (FileNotFoundException e) {
                System.out.println("The specified file is not found.");
            }
        }
    }

    Output:

    The specified file is not found.
    • NumberFormatException
    class NumberFormatExceptionExample {
        public static void main(String[] args) {
            try {
                int number = Integer.parseInt("abc123");  // Invalid number format
            } catch (NumberFormatException e) {
                System.out.println("Invalid format for a number.");
            }
        }
    }

    Output:

    Invalid format for a number.
    • ArrayIndexOutOfBoundsException
    class ArrayIndexOutOfBoundsExceptionExample {
        public static void main(String[] args) {
            try {
                int[] numbers = {1, 2, 3, 4, 5};
                System.out.println(numbers[6]);  // Invalid index access
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("Array index is out of bounds.");
            }
        }
    }

    Output:

    Invalid format for a number.
    • ArrayIndexOutOfBoundsException
    class ArrayIndexOutOfBoundsExceptionExample {
        public static void main(String[] args) {
            try {
                int[] numbers = {1, 2, 3, 4, 5};
                System.out.println(numbers[6]);  // Invalid index access
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("Array index is out of bounds.");
            }
        }
    }

    Output:

    Array index is out of bounds.
    • IOException
    import java.io.*;
    
    class IOExceptionExample {
        public static void main(String[] args) {
            try {
                BufferedReader reader = new BufferedReader(new FileReader("nonexistentfile.txt"));
                String line = reader.readLine();
                System.out.println(line);
            } catch (IOException e) {
                System.out.println("Input-output error occurred.");
            }
        }
    }

    Output:

    Input-output error occurred.
    User-Defined Exceptions

    In addition to built-in exceptions, Java allows developers to create custom exceptions that describe unique error situations.

    Steps:

    1. Create a class extending Exception.
    2. Write a constructor that passes a message to the Exception class.
    3. Use throw to raise this custom exception in your program.

    Example of User-Defined Exception

    class LowBalanceException extends Exception {
        LowBalanceException(String message) {
            super(message);
        }
    }
    
    class CustomExceptionExample {
        public static void main(String[] args) {
            double balance = 400.00;
            try {
                checkBalance(balance);
            } catch (LowBalanceException e) {
                System.out.println(e.getMessage());
            }
        }
    
        static void checkBalance(double balance) throws LowBalanceException {
            if (balance < 500) {
                throw new LowBalanceException("Balance is below the minimum threshold!");
            } else {
                System.out.println("Your balance is sufficient.");
            }
        }
    }

    Output:

    Balance is below the minimum threshold!

    Checked vs Unchecked Exceptions in Java

    In Java, an exception is an event that disrupts the normal flow of a program during its execution. Java categorizes exceptions into two types:

    1. Checked Exceptions:

    These exceptions are checked at compile time. If a method throws a checked exception, it must either handle the exception using a try-catch block or declare it using the throws keyword. Checked exceptions typically occur in scenarios that are beyond the control of the program, such as reading from a file or a network issue.

    Example of a Checked Exception:

    import java.io.*;
    
    public class CheckedExceptionDemo {
        public static void main(String[] args) throws IOException {
            // Trying to read from a non-existing file
            FileReader fileReader = new FileReader("D:\\data.txt");
            BufferedReader bufferedReader = new BufferedReader(fileReader);
    
            // Reading the first three lines of the file
            for (int i = 0; i < 3; i++) {
                System.out.println(bufferedReader.readLine());
            }
    
            // Closing the file reader
            bufferedReader.close();
        }
    }

    Output:

    Exception in thread "main" java.io.FileNotFoundException: D:\data.txt (The system cannot find the file specified)
    at java.io.FileInputStream.open0(Native Method)
    at java.io.FileInputStream.open(FileInputStream.java:195)
    ...
    2. Unchecked Exceptions:

    These exceptions are not checked at compile time. They are usually caused by programming errors like trying to access an array out of bounds or dividing by zero. Unchecked exceptions are derived from RuntimeException. Unlike checked exceptions, you are not required to handle or declare them in your method signature.

    Example of an Unchecked Exception:

    public class UncheckedExceptionDemo {
        public static void main(String[] args) {
            // Dividing by zero will cause ArithmeticException
            int a = 5;
            int b = 0;
            int result = a / b;  // This will throw an exception
        }
    }

    Output:

    Exception in thread "main" java.lang.ArithmeticException: / by zero
    at UncheckedExceptionDemo.main(UncheckedExceptionDemo.java:5)

    Here, ArithmeticException is an unchecked exception that occurs at runtime due to division by zero.

    Other Examples of Checked and Unchecked Exceptions:

    1. Checked Exception – Handling with throws:

    import java.io.*;
    
    public class HandleCheckedException {
        public static void main(String[] args) throws IOException {
            FileReader reader = new FileReader("D:\\info.txt");
            BufferedReader bufferedReader = new BufferedReader(reader);
    
            System.out.println(bufferedReader.readLine());
    
            bufferedReader.close();
        }
    }

    Output:

    Exception in thread "main" java.io.FileNotFoundException: D:\info.txt (The system cannot find the file specified)

    2. Unchecked Exception – NullPointerException:

    public class NullPointerExceptionDemo {
        public static void main(String[] args) {
            String data = null;
            System.out.println(data.length());  // This will cause a NullPointerException
        }
    }

    Output:

    Exception in thread "main" java.lang.NullPointerException
    at NullPointerExceptionDemo.main(NullPointerExceptionDemo.java:5)

    3. Unchecked Exception – ArrayIndexOutOfBoundsException:

    public class ArrayIndexOutOfBoundsExceptionDemo {
        public static void main(String[] args) {
            int[] numbers = {1, 2, 3};
            System.out.println(numbers[5]);  // This will throw ArrayIndexOutOfBoundsException
        }
    }

    Output:

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
    at ArrayIndexOutOfBoundsExceptionDemo.main(ArrayIndexOutOfBoundsExceptionDemo.java:5)

    Java Try Catch Block

    In Java, an exception is an “undesirable or unexpected event” that occurs during the program’s execution, causing it to terminate unexpectedly. To prevent this abrupt termination, Java provides mechanisms like the try-catch block for handling exceptions. In this explanation, we will discuss the usage of trycatchthrowthrows, and finally in Java.

    Reasons Why an Exception May Occur:

    An exception can be triggered by multiple factors such as:

    • Issues with network connectivity
    • Incorrect input from the user
    • Attempting to open a file that doesn’t exist, etc.
    Exception Handling Constructs and Keywords:

    1. try Block : The try block encompasses the code that may potentially cause an exception. If an exception occurs in this block, it is transferred to the corresponding catch block.

    try {
        // code that might generate an exception
    }

    2. catch Block: The catch block handles exceptions thrown by the try block. It is always placed after the try block to process any exceptions that occur.

    catch (Exception e) {
        // code to handle the exception
        // e.g., closing resources, logging errors
    }

    3. throw Keyword: The throw keyword explicitly throws an exception, typically used to pass control from a try block to the catch block. It is often used when custom exceptions need to be raised.

    Example:

    // Java program demonstrating the use of throw
    class CustomExceptionExample {
        static void demonstrateThrow() {
            try {
                throw new IllegalArgumentException("Invalid argument");
            } catch (IllegalArgumentException e) {
                System.out.println("Caught in demonstrateThrow().");
                throw e;  // rethrowing the exception
            }
        }
    
        public static void main(String[] args) {
            try {
                demonstrateThrow();
            } catch (IllegalArgumentException e) {
                System.out.println("Caught in main with message:");
                System.out.println(e);
            }
        }
    }

    Output:

    Caught in demonstrateThrow().
    Caught in main with message:
    java.lang.IllegalArgumentException: Invalid argument

    4. throws Keyword: The throws keyword is used to declare exceptions in a method signature without handling them within the method itself. This allows the calling method to handle the exception instead.

    Example:

    // Java program demonstrating throws
    class ThrowsDemo {
        // This method declares an exception
        static void riskyMethod() throws IllegalStateException {
            System.out.println("Inside riskyMethod.");
            throw new IllegalStateException("Critical error");
        }
    
        public static void main(String[] args) {
            try {
                riskyMethod();
            } catch (IllegalStateException e) {
                System.out.println("Caught in main.");
            }
        }
    }

    Output:

    Inside riskyMethod.
    Caught in main.

    5. finally Block: The finally block is always executed after the try-catch blocks, regardless of whether an exception was thrown or not. It is typically used for code that needs to execute no matter what, such as closing resources.

    Example:

    // Java program demonstrating try, catch, and finally
    class FinalBlockExample {
        public static void main(String[] args) {
            int num1 = 20, num2 = 10, num3 = 10, result;
    
            try {
                result = num1 / (num2 - num3);  // Will cause division by zero
                System.out.println("Result: " + result);
            } catch (ArithmeticException e) {
                System.out.println("Exception caught: Division by zero");
            } finally {
                System.out.println("This is the finally block.");
            }
        }
    }

    Output:

    Exception caught: Division by zero
    This is the finally block.

    Flow control in try catch finally in Java

    In this article, we will explore all the possible combinations of try-catch-finally blocks and how control flow behaves when an exception is thrown. We’ll cover different cases that can arise, such as exceptions being caught, not caught, and cases where no exception occurs.

    Control Flow in try-catch or try-catch-finally Blocks:

    Exception Occurs in try Block and Is Handled in catch Block

    When an exception occurs in the try block, the remaining code in that block will not be executed. The control is passed to the corresponding catch block, where the exception is handled. After the catch block, if a finally block exists, it will execute, and then the rest of the program will continue.

    Control Flow Example with try-catch:

    // Java program to demonstrate control flow
    // when an exception occurs in the try block
    // and is handled in the catch block
    class Example1 {
        public static void main(String[] args) {
            int[] arr = new int[4];
            try {
                int i = arr[4];  // Exception occurs here
                System.out.println("Inside try block");
            } catch (ArrayIndexOutOfBoundsException ex) {
                System.out.println("Exception caught in catch block");
            }
            System.out.println("Outside try-catch block");
        }
    }

    Output:

    Exception caught in catch block
    Outside try-catch block

    Control Flow Example with try-catch-finally:

    // Java program to demonstrate control flow
    // with try-catch-finally when an exception occurs
    class Example2 {
        public static void main(String[] args) {
            int[] arr = new int[4];
            try {
                int i = arr[4];  // Exception occurs here
                System.out.println("Inside try block");
            } catch (ArrayIndexOutOfBoundsException ex) {
                System.out.println("Exception caught in catch block");
            } finally {
                System.out.println("finally block executed");
            }
            System.out.println("Outside try-catch-finally block");
        }
    }

    Output:

    Exception caught in catch block
    finally block executed
    Outside try-catch-finally block

    Control Flow in try-finally:

    In the try-finally block, the finally block always executes, regardless of whether an exception occurs or not. The control flow differs depending on whether an exception is raised.

    Exception Occurs in try Block

    // Java program to demonstrate control flow
    // when an exception occurs in try-finally block
    class Example7 {
        public static void main(String[] args) {
            int[] arr = new int[4];
            try {
                int i = arr[4];  // Exception occurs here
                System.out.println("Inside try block");
            } finally {
                System.out.println("finally block executed");
            }
            System.out.println("Outside try-finally block");
        }
    }

    Output:

    finally block executed
    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4

    throw and throws in Java

    In Java, exception handling is a crucial mechanism to manage runtime errors, ensuring that the normal flow of a program isn’t interrupted. Common exceptions include ClassNotFoundExceptionIOExceptionSQLException, and RemoteException, among others.

    This article will delve into two essential components of exception handling in Java: the throw and throws keywords, explaining their use and providing practical examples.

    Java throw

    The throw keyword is used to explicitly raise an exception from a method or block of code. Both checked and unchecked exceptions can be thrown using throw, and it is especially useful for raising custom exceptions.

    Syntax:

    throw instance;

    Where instance is an object of type Throwable or its subclass. For example, Exception is a subclass of Throwable, and user-defined exceptions generally extend the Exception class. Unlike languages like C++, Java does not allow basic data types (like int or char) or non-throwable classes to be used as exceptions.

    When a throw statement is executed, the program’s control flow is immediately transferred to the nearest enclosing try-catch block that can handle the exception. If no matching catch block is found, the program terminates with an error.

    Java throw Examples
    Example 1:

    // Java program to demonstrate the use of throw
    class CustomThrowExample {
        static void checkException() {
            try {
                throw new IllegalArgumentException("Demo Exception");
            } catch (IllegalArgumentException e) {
                System.out.println("Caught inside checkException().");
                throw e;  // rethrowing the exception
            }
        }
    
        public static void main(String[] args) {
            try {
                checkException();
            } catch (IllegalArgumentException e) {
                System.out.println("Caught in main.");
            }
        }
    }

    Output:

    Caught inside checkException().
    Caught in main.

    User-defined Custom Exception

    In Java, an exception is a runtime issue that interrupts the normal execution flow of a program. When an exception occurs, the program is terminated unexpectedly, and any code following the exception-generating statement is not executed.

    Java allows developers to create their own exceptions, which are subclasses of the Exception class. This is referred to as a custom exception or user-defined exception. Custom exceptions are primarily used to suit specific requirements by adding custom error-handling logic.

    For instance, in the following example, the class CustomException extends the Exception class to create a new custom exception.

    Why Use Custom Exceptions?

    While Java provides a wide range of built-in exceptions, there are scenarios where custom exceptions are beneficial. Below are some reasons for creating custom exceptions:

    1.Specific Exception Handling: Custom exceptions can target a specific subset of existing exceptions, allowing more refined exception handling.
    Business Logic Exceptions: Custom exceptions are useful for handling business logic errors, making it easier for developers and users to understand the nature of the problem in the workflow.

    To define a custom exception, you need to extend the Exception class, which is part of the java.lang package.

    Example:

    // A class representing a user-defined exception
    class CustomException extends Exception {
        public CustomException(String message) {
            // Call the constructor of the parent Exception class
            super(message);
        }
    }
    
    // A class that uses the CustomException
    public class MainApp {
        // Main method
        public static void main(String[] args) {
            try {
                // Throw an instance of the custom exception
                throw new CustomException("Custom exception occurred");
            } catch (CustomException e) {
                System.out.println("Exception caught");
    
                // Print the message from the CustomException object
                System.out.println(e.getMessage());
            }
        }
    }

    Output:

    Exception caught
    Custom exception occurred

    Chained Exceptions in Java

    Chained Exceptions in Java allow developers to associate one exception with another, establishing a relationship between them. This is helpful when one exception is a direct result of another. For instance, imagine a scenario where a method throws an ArithmeticException due to division by zero, but the real cause was an I/O error that led to the divisor being zero. In such a case, only the ArithmeticException would be reported, making it difficult for the caller to understand the root cause. Chained Exceptions solve this problem by allowing the original exception to be linked with the final exception.

    Constructors in the Throwable Class Supporting Chained Exceptions:

    1.Throwable(Throwable cause): Accepts the cause of the current exception as an argument.

    2. Throwable(String msg, Throwable cause):Takes a custom error message (msg) and the cause (cause) as arguments.

    Methods in the Throwable Class Supporting Chained Exceptions:

    1. getCause():Retrieves the original cause of the exception.

    2. initCause(Throwable cause):Allows setting the cause for the current exception.

    Example of Chained Exception Usage:

    // Java program demonstrating chained exceptions
    public class ChainedExceptionDemo {
        public static void main(String[] args) {
            try {
                // Create a new ArithmeticException
                ArithmeticException ex = new ArithmeticException("Arithmetic error occurred");
    
                // Set the cause of this exception to an I/O-related issue
                ex.initCause(new IllegalStateException("Caused by a file error"));
    
                // Throw the exception
                throw ex;
            } catch (ArithmeticException ex) {
                // Display the exception message
                System.out.println(ex);
    
                // Retrieve and display the actual cause of the exception
                System.out.println("Caused by: " + ex.getCause());
            }
        }
    }

    Output:

    java.lang.ArithmeticException: Arithmetic error occurred
    Caused by: java.lang.IllegalStateException: Caused by a file error
    finally block executed
    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
  • Packages in Java

    What is a Package?

    A package in Java is a namespace that groups related classes, interfaces, subpackages, enums, and annotations. Packages help organize large projects and provide modularity.

    Why Use Packages?

    Packages provide several important benefits:

    1. Avoid name conflicts
      Example:
      university.department.cs.Student
      university.department.ee.Student
    2. Better organization
      Related classes are grouped logically.
    3. Access control
      • protected: accessible within the same package and subclasses
      • default (no modifier): accessible only within the same package
    4. Encapsulation (data hiding)
      Internal implementation can be hidden while exposing public APIs.
    5. Reusability
      Classes from packages can be reused across applications.

    How Packages Work

    Package names map directly to directory structures.

    Example:

    package university.department.cs;
    

    Directory structure:

    university/
     └── department/
         └── cs/
    

    Java uses the CLASSPATH to locate packages and classes at runtime.


    Package Naming Conventions

    Java package names usually follow reverse domain naming:

    com.company.project.module
    org.organization.application
    university.department.math
    

    This guarantees global uniqueness.


    Adding Classes to a Package

    To add a class to a package:

    1. Declare the package at the top of the file
    2. Save the file in the corresponding directory
    3. Compile the file
    package mypack;
    
    public class MyClass {
        public void display() {
            System.out.println("Hello from MyClass in mypack.");
        }
    }
    

    Subpackages

    A subpackage is a package inside another package.

    java.util
    java.util.concurrent
    

    ⚠️ Subpackages are not automatically imported with parent packages.

    import java.util.*;          // Does NOT import java.util.concurrent
    

    Importing Packages

    Import a Specific Class

    import java.util.ArrayList;
    

    Import All Classes from a Package

    import java.util.*;
    

    Subpackages are excluded.


    Accessing Classes in a Package

    import java.util.List;
    
    public class DemoImport {
        public static void main(String[] args) {
            List<String> names = new ArrayList<>();
    
            java.util.LinkedList<String> items =
                    new java.util.LinkedList<>();
        }
    }
    

    Types of Packages in Java

    1. Built-in Packages

    Provided by Java API.

    Common examples:

    • java.lang
    • java.util
    • java.io
    • java.net
    • java.awt

    2. User-Defined Packages

    // File: mypack/MyClass.java
    package mypack;
    
    public class MyClass {
        public void display() {
            System.out.println("Hello from MyClass in mypack.");
        }
    }
    

    Usage:

    import mypack.MyClass;
    
    public class TestPackage {
        public static void main(String[] args) {
            MyClass obj = new MyClass();
            obj.display();
        }
    }
    

    Output

    Hello from MyClass in mypack.
    

    Creating a Package (Compile & Run)

    package myPackage;
    
    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello from myPackage!");
        }
    }
    

    Compile

    javac -d . HelloWorld.java
    

    Run

    java myPackage.HelloWorld
    

    Static Import in Java

    Static import allows direct access to static members without class name.

    import static java.lang.Math.*;
    
    public class StaticImportExample {
        public static void main(String[] args) {
            System.out.println(PI);
            System.out.println(sqrt(16));
        }
    }
    

    Handling Name Conflicts

    When two packages contain classes with the same name, use fully qualified names.

    import java.util.Date;
    import java.sql.*;
    
    public class ConflictExample {
        public static void main(String[] args) {
            java.util.Date utilDate = new java.util.Date();
            java.sql.Date sqlDate =
                    new java.sql.Date(System.currentTimeMillis());
    
            System.out.println(utilDate);
            System.out.println(sqlDate);
        }
    }
    

    Directory Structure Mapping

    com.example.shapes.Circle
    ↓
    BASE_DIR/com/example/shapes/Circle.class
    

    Important Built-in Packages


    java.util Package

    Provides utility classes for:

    • Collections
    • Date & time
    • Random numbers
    • Locale & formatting
    • Timers

    Example

    import java.util.ArrayList;
    
    public class Example {
        public static void main(String[] args) {
            ArrayList<String> fruits = new ArrayList<>();
            fruits.add("Apple");
            fruits.add("Banana");
            fruits.add("Cherry");
    
            System.out.println(fruits);
        }
    }
    

    java.lang Package

    Automatically imported in every Java program.

    Key classes:

    • Object
    • String
    • Math
    • System
    • Thread
    • Wrapper classes (Integer, Double, etc.)

    Eg:

    public class Example {
        public static void main(String[] args) {
            String msg = "Hello Java";
            System.out.println(msg.length());
        }
    }
    

    java.io Package

    Handles:

    • File input/output
    • Streams
    • Serialization
    • Buffered I/O

    Example: File Copy

    import java.io.*;
    
    public class FileCopyExample {
        public static void main(String[] args) {
            try (FileInputStream in = new FileInputStream("source.txt");
                 FileOutputStream out = new FileOutputStream("dest.txt")) {
    
                int data;
                while ((data = in.read()) != -1) {
                    out.write(data);
                }
                System.out.println("File copied successfully");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    Buffered Reader & Writer Example

    import java.io.*;
    
    public class BufferedExample {
        public static void main(String[] args) {
            try (BufferedReader reader =
                         new BufferedReader(new FileReader("input.txt"));
                 BufferedWriter writer =
                         new BufferedWriter(new FileWriter("output.txt"))) {
    
                String line;
                while ((line = reader.readLine()) != null) {
                    writer.write(line);
                    writer.newLine();
                }
                System.out.println("File processed successfully.");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    Summary

    • Packages organize Java code logically
    • Prevent naming conflicts
    • Improve encapsulation and security
    • Support modular, reusable development
    • Java provides rich built-in packages
    • Developers can create custom packages easily

  • Classes in Java

    Understanding Classes and Objects

    Object-Oriented Programming (OOP) refers to the concept of structuring software as a collection of objects that include both data and behavior. In this approach, programs revolve around objects, which helps simplify software development and maintenance. Instead of focusing solely on actions or logic, OOP allows for more flexible and maintainable software. It makes understanding and working with the program easier by bringing data and methods into a single location: the object.

    Key Concepts of OOP:
    • Object
    • Class
    • Encapsulation
    • Inheritance
    • Polymorphism
    • Abstraction
    Importance of Classes and Objects in OOP

    Classes:

    A class acts as a blueprint or prototype from which objects are created. It defines a set of attributes and behaviors that are common to all objects of that type. The primary reasons classes are essential in OOP are:

    • They offer a structure for creating objects that bind data and methods together.
    • They contain method and variable definitions.
    • They support inheritance, allowing for the maintenance of a class hierarchy.
    • They enable the management of access to member variables.

    Objects:

    An object is the core unit of OOP. It represents real-life entities and combines attributes and behaviors.

    Objects consist of:

    • State: Represented by the object’s attributes.
    • Behavior: Represented by the object’s methods.
    • Identity: A unique identifier for each object, allowing it to interact with other objects.

    In OOP, objects are important because they can call non-static functions not present in the main method but existing within the class.

    Example of Creating and Using Objects and Classes:

    To better understand this, let’s take an example where we add two numbers. By creating separate objects for each number, we can perform the necessary operations. Here’s a demonstration of the use of objects and classes:

    // Java program to demonstrate objects and classes
    
    public class Animal {
        // Instance variables
        String name;
        String species;
        int age;
    
        // Constructor for the Animal class
        public Animal(String name, String species, int age) {
            this.name = name;
            this.species = species;
            this.age = age;
        }
    
        // Method to return the animal's name
        public String getName() {
            return name;
        }
    
        // Method to return the animal's species
        public String getSpecies() {
            return species;
        }
    
        // Method to return the animal's age
        public int getAge() {
            return age;
        }
    
        // Method to print the animal's details
        @Override
        public String toString() {
            return "This is a " + species + " named " + name + " and it is " + age + " years old.";
        }
    
        public static void main(String[] args) {
            // Creating an object of the Animal class
            Animal animal1 = new Animal("Buddy", "Dog", 3);
            System.out.println(animal1.toString());
        }
    }

    Output:

    This is a Dog named Buddy and it is 3 years old.
    Object Creation Techniques in Java:

    1. Using the new Keyword:

    This is the simplest and most common way to create an object in Java.

    // Java program to demonstrate object creation using the new keyword
    
    class Vehicle {
        String type;
        String model;
    
        Vehicle(String type, String model) {
            this.type = type;
            this.model = model;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            // Creating two objects of the Vehicle class
            Vehicle car = new Vehicle("Car", "Sedan");
            Vehicle bike = new Vehicle("Bike", "Cruiser");
    
            // Accessing object data
            System.out.println(car.type + ": " + car.model);
            System.out.println(bike.type + ": " + bike.model);
        }
    }

    Output:

    Car: Sedan
    Bike: Cruiser

    2. Using Class.newInstance():

    This method dynamically creates objects, invoking a no-argument constructor.

    // Java program to demonstrate object creation using Class.newInstance()
    
    class Example {
        void displayMessage() {
            System.out.println("Welcome to OOP!");
        }
    }
    
    public class Test {
        public static void main(String args[]) {
            try {
                Class<?> cls = Class.forName("Example");
                Example obj = (Example) cls.newInstance();
                obj.displayMessage();
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }

    Output:

    Welcome to OOP!

    3. Using the clone() Method:

    This method creates a copy (or clone) of an existing object. The class must implement the Cloneable interface.

    // Java program to demonstrate object creation using the clone() method
    
    class Person implements Cloneable {
        int id;
        String name;
    
        // Constructor
        Person(int id, String name) {
            this.id = id;
            this.name = name;
        }
    
        // Cloning method
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            try {
                // Creating original object
                Person person1 = new Person(101, "John");
    
                // Cloning person1
                Person person2 = (Person) person1.clone();
    
                System.out.println(person1.id + ", " + person1.name);
                System.out.println(person2.id + ", " + person2.name);
            } catch (CloneNotSupportedException e) {
                System.out.println(e);
            }
        }
    }

    Output:

    101, John
    101, John

    Singleton Method Design Pattern

    In object-oriented programming, a singleton class in Java is a class designed to allow only one instance to exist at any given time. When multiple variables attempt to instantiate this class, they all reference the same instance. Any changes made through one reference are visible to all others, because they all point to the same object.

    Key Aspects of Defining a Singleton Class:

    1. Private constructor: The constructor is made private to prevent direct instantiation from outside the class.
    2. Static method: A static method, using lazy initialization, returns the single instance of the class.

    Purpose of a Singleton Class:

    The singleton pattern is primarily used to limit the number of instances of a class to just one. This is particularly useful for controlling access to resources such as a database connection or system configuration, where having multiple instances might lead to inconsistent behavior or unnecessary resource use.

    Benefits of a Singleton:
    • Avoids memory wastage: By restricting instance creation to just one, it avoids the overhead of multiple object creations.
    • Reusability: The single instance can be reused as needed, making the singleton pattern useful in scenarios such as logging, caching, or connection pooling.

    Example Use Case:

    In situations where only one connection (e.g., to a database) is allowed or needed, a singleton ensures that all threads share that same connection, rather than creating multiple ones.

    Steps to Create a Singleton Class in Java:

    1. Ensure only one instance exists:

    • Declare the class constructor as private.
    • Provide a static method to get the single instance (using lazy initialization).

    2. Provide global access:

    • Store the single instance as a private static variable.
    • Use a static method to return this instance when needed.
    Difference Between a Normal Class and a Singleton Class:

    A normal class allows multiple instances to be created using a constructor. In contrast, a singleton class restricts this by providing the instance through a static method (like getInstance()).

    While the normal class disappears at the end of an application’s lifecycle, the singleton’s instance may persist and be reused across the application’s duration.

    Types of Singleton Patterns:

    1. Eager Initialization: The instance is created when the class is loaded.

    2. Lazy Initialization: The instance is created only when it’s requested for the first time.

    Example 1: Using getInstance() Method

    // Singleton Class Implementation
    class SingletonExample {
        // Static variable to hold the one and only instance
        private static SingletonExample singleInstance = null;
    
        // A variable to demonstrate instance behavior
        public String message;
    
        // Private constructor to prevent instantiation
        private SingletonExample() {
            message = "This is a part of the SingletonExample class";
        }
    
        // Static method to provide access to the single instance
        public static synchronized SingletonExample getInstance() {
            if (singleInstance == null) {
                singleInstance = new SingletonExample();
            }
            return singleInstance;
        }
    }
    
    // Main class to demonstrate Singleton behavior
    public class MainClass {
        public static void main(String[] args) {
            // Accessing Singleton class through different references
            SingletonExample a = SingletonExample.getInstance();
            SingletonExample b = SingletonExample.getInstance();
            SingletonExample c = SingletonExample.getInstance();
    
            // Print hash codes for the instances
            System.out.println("Hashcode of a: " + a.hashCode());
            System.out.println("Hashcode of b: " + b.hashCode());
            System.out.println("Hashcode of c: " + c.hashCode());
    
            // Checking if all references point to the same instance
            if (a == b && b == c) {
                System.out.println("All variables point to the same instance.");
            } else {
                System.out.println("Different instances are created.");
            }
        }
    }

    Output:

    Hashcode of a: 12345678
    Hashcode of b: 12345678
    Hashcode of c: 12345678
    All variables point to the same instance.

    In this example, ab, and c all refer to the same instance, as demonstrated by their identical hash codes.

    Example 2: Singleton Class Using Class Name as Method

    // Singleton Class with Method Named After Class
    class Singleton {
        private static Singleton singleInstance = null;
    
        // A variable to store a message
        public String message;
    
        // Private constructor
        private Singleton() {
            message = "This is part of the Singleton class";
        }
    
        // Static method with the same name as class to return the instance
        public static Singleton Singleton() {
            if (singleInstance == null) {
                singleInstance = new Singleton();
            }
            return singleInstance;
        }
    }
    
    // Main class to test
    public class MainTest {
        public static void main(String[] args) {
            // Get instances from Singleton class
            Singleton first = Singleton.Singleton();
            Singleton second = Singleton.Singleton();
            Singleton third = Singleton.Singleton();
    
            // Modify the variable through the first reference
            first.message = first.message.toUpperCase();
    
            // Print the message from each reference
            System.out.println("Message from first: " + first.message);
            System.out.println("Message from second: " + second.message);
            System.out.println("Message from third: " + third.message);
    
            // Modify the variable through the third reference
            third.message = third.message.toLowerCase();
    
            // Print the message again
            System.out.println("Message from first: " + first.message);
            System.out.println("Message from second: " + second.message);
            System.out.println("Message from third: " + third.message);
        }
    }

    Output:

    Message from first: THIS IS PART OF THE SINGLETON CLASS
    Message from second: THIS IS PART OF THE SINGLETON CLASS
    Message from third: THIS IS PART OF THE SINGLETON CLASS
    
    Message from first: this is part of the singleton class
    Message from second: this is part of the singleton class
    Message from third: this is part of the singleton class

    Object Class in Java

    The Object class is part of the java.lang package and serves as the superclass of every class in Java. This means all classes in Java, either directly or indirectly, inherit from the Object class. If a class doesn’t explicitly extend another class, it automatically becomes a direct child of the Object class. However, if a class extends another class, it indirectly inherits from the Object class. Consequently, all methods provided by the Object class are available to all Java classes. This makes the Object class the root of Java’s inheritance hierarchy.

    Methods of the Object Class

    The Object class provides several important methods, which are:

    1. toString() method.
    2. hashCode() method
    3. equals(Object obj) method
    4. finalize() method
    5. getClass() method
    6. clone() method
    7. wait(), notify(), and notifyAll() methods (used for thread synchronization)

    1. toString() Method : The toString() method returns a string representation of an object. The default implementation provided by the Object class includes the class name, followed by the @ symbol and the hexadecimal representation of the object’s hash code. The method is defined as:

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    It’s a common practice to override the toString() method to give a more meaningful string representation of an object.

    Example:

    class Employee {
        String name;
        int id;
    
        Employee(String name, int id) {
            this.name = name;
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "Employee [Name: " + name + ", ID: " + id + "]";
        }
    
        public static void main(String[] args) {
            Employee e = new Employee("John", 101);
            System.out.println(e);
            System.out.println(e.toString());
        }
    }

    Output:

    Employee [Name: John, ID: 101]
    Employee [Name: John, ID: 101]

    2. hashCode() Method : The hashCode() method returns a hash code value for an object, which is typically used in hashing algorithms and data structures like HashMap. Each object has a unique hash code by default, but you can override this method to customize the hash code calculation.

    class Car {
        String model;
        int year;
    
        Car(String model, int year) {
            this.model = model;
            this.year = year;
        }
    
        @Override
        public int hashCode() {
            return year + model.hashCode();
        }
    
        public static void main(String[] args) {
            Car c = new Car("Toyota", 2020);
            System.out.println("Car's hash code: " + c.hashCode());
        }
    }

    Output:

    Car's hash code: 207319

    3. equals(Object obj) Method : The equals() method checks whether two objects are equal. By default, it compares object references. However, this method is often overridden to compare the actual content of the objects.

    Example:

    class Book {
        String title;
        String author;
    
        Book(String title, String author) {
            this.title = title;
            this.author = author;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
    
            Book book = (Book) obj;
            return title.equals(book.title) && author.equals(book.author);
        }
    
        public static void main(String[] args) {
            Book b1 = new Book("1984", "George Orwell");
            Book b2 = new Book("1984", "George Orwell");
            System.out.println(b1.equals(b2));
        }
    }

    Output:

    true

    4. getClass() Method : The getClass() method returns the runtime class of the object. This method is final and cannot be overridden.

    Example:

    class Animal {
        public static void main(String[] args) {
            Animal a = new Animal();
            System.out.println("Class of the object: " + a.getClass().getName());
        }
    }

    Output:

    Class of the object: Animal

    5. finalize() Method : The finalize() method is called by the garbage collector when there are no more references to an object. It is often used to clean up resources.

    Example:

    class Demo {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Finalize method called.");
        }
    
        public static void main(String[] args) {
            Demo d = new Demo();
            d = null;
            System.gc(); // Requesting garbage collection
        }
    }

    Output:

    Finalize method called.

    6. clone() Method : The clone() method creates and returns a copy (clone) of the object. To use this method, a class must implement the Cloneable interface.

    Example:

    class Product implements Cloneable {
        String name;
        double price;
    
        Product(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        @Override
        protected Product clone() throws CloneNotSupportedException {
            return (Product) super.clone();
        }
    
        public static void main(String[] args) throws CloneNotSupportedException {
            Product p1 = new Product("Laptop", 999.99);
            Product p2 = p1.clone();
            System.out.println(p1.name + " - " + p2.name);
        }
    }
    Laptop - Laptop

    Inner Class

    Understanding Inner Classes in Java

    In Java, an inner class is a class defined within another class or an interface. This concept was introduced to bring logically related classes together, following Java’s object-oriented principles, making the code more intuitive and closer to real-world representations. But why were inner classes introduced? Let’s dive into their advantages:

    Advantages of Inner Classes:
    • They help in creating cleaner and more readable code.
    • Inner classes can access private members of their outer class, adding flexibility in designing real-world scenarios.
    • They help optimize code modules by grouping closely related logic together.

    As we progress through Java’s object-oriented programming concepts, you will see inner classes becoming more common, especially when you want certain operations to have limited access to other classes. We will now explore the different types of inner classes in Java, along with detailed examples.

    Types of Inner Classes in Java

    There are four main types of inner classes:

    1. Member Inner Class . (Nested Inner Class)
    2. Method Local Inner Classes
    3. Static Nested Classes
    4. Anonymous Inner Classes

    We will examine each type with examples.

    1. Member Inner Class (Nested Inner Class): A member inner class is defined within the body of another class. It can access all the members of the outer class, including private members.

    Example:

    class Outer {
        private String message = "Welcome to the inner class!";
    
        class Inner {
            public void displayMessage() {
                System.out.println(message);  // Accessing outer class's private field
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Outer outer = new Outer();
            Outer.Inner inner = outer.new Inner();  // Creating an instance of inner class
            inner.displayMessage();
        }
    }

    Output:

    Welcome to the inner class!

    2. Method Local Inner Class A method-local inner class is defined within a method of the outer class. This class is only accessible within the method and can access the final or effectively final local variables of the method.

    Example:

    class Outer {
        public void outerMethod() {
            System.out.println("Inside outerMethod");
    
            class Inner {
                public void innerMethod() {
                    System.out.println("Inside innerMethod");
                }
            }
    
            Inner inner = new Inner();  // Creating an instance of the local inner class
            inner.innerMethod();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Outer outer = new Outer();
            outer.outerMethod();
        }
    }

    Output:

    Inside outer method
    Inside inner method

    3. Static Nested Class : Static nested classes are not considered true inner classes because they don’t have access to the outer class’s instance variables. Instead, they function like static members of the outer class.

    // Outer class
    class OuterClass {
    
        // Static nested class
        static class StaticNestedClass {
            void display() {
                System.out.println("Inside static nested class");
            }
        }
    }
    
    // Main class
    public class MainClass {
        public static void main(String[] args) {
            // Create an instance of static nested class
            OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
            nested.display();
        }
    }

    Output:

    Inside static nested class

    3. Anonymous Inner Class : Anonymous inner classes have no name and are typically used for quick implementations of interfaces or to extend classes for single-use cases.

    Example 1: Extending a Class

    // Base class
    class Greeting {
        void sayHello() {
            System.out.println("Hello from Greeting class");
        }
    }
    
    // Main class
    public class MainClass {
        public static void main(String[] args) {
    
            // Anonymous inner class extending Greeting
            Greeting greet = new Greeting() {
                @Override
                void sayHello() {
                    System.out.println("Hello from anonymous class");
                }
            };
    
            greet.sayHello();
        }
    }

    Output:

    Hello from anonymous class

    Throwable Class

    Classes and Objects form the foundation of Object-Oriented Programming, and the concept revolves around real-world entities. A class is a user-defined template or blueprint from which objects are instantiated. It represents a collection of properties or methods that are common to all objects of a particular type. In this article, we will explore the Throwable class, its constructors, and various methods available in this class.

    The Throwable class serves as the superclass for every error and exception in the Java language. Only objects that are a subclass of Throwable can be thrown by either the “Java Virtual Machine” (JVM) or by the Java throw statement. For compile-time exception checking, Throwable and its subclasses (excluding Error and RuntimeException) are treated as checked exceptions.

    The Throwable class is at the root of the Java Exception Hierarchy and is extended by two primary subclasses:

    1. Exception
    2. Error

    The Throwable class implements the Serializable interface, and its direct known subclasses are Error and Exception. It contains a snapshot of the execution stack of its thread at the time of its creation. It may also include a message string to provide additional context about the error. Furthermore, it can suppress other throwables from propagating.

    Users can create their own custom throwable by extending the Throwable class.

    Example:

    class MyCustomThrowable extends Throwable {
        // Custom Throwable created by the user
    }
    
    class Example {
        public void testMethod() throws MyCustomThrowable {
            // Custom throwable used here
            throw new MyCustomThrowable();
        }
    }

    Declaration of java.lang.Throwable:

    public class Throwable extends Object implements Serializable
    Constructors:

    Any class can have constructors, which are of three main types: default, parameterized, and non-parameterized. The Throwable class has the following constructors:

    1. Throwable(): A non-parameterized constructor that creates a new Throwable with a null detailed message.
    2. Throwable(String message): A parameterized constructor that creates a new Throwable with a specific detailed message.
    3. Throwable(String message, Throwable cause): A parameterized constructor that creates a new Throwable with a specific message and a cause.
    4. Throwable(Throwable cause): A parameterized constructor that creates a new Throwable with the specified cause and a message derived by calling the toString() method on the cause.

    Protected Constructors:

    Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace): Constructs a new throwable with the specified message, cause, and options for enabling/disabling suppression and stack trace writability.

    Methods:
    The Throwable class provides several predefined methods:

    1. addSuppressed(Throwable exception): Appends the specified exception to the list of suppressed exceptions.

    public final void addSuppressed(Throwable exception)

    2. fillInStackTrace(): Records the current stack trace into the Throwable object.

    public Throwable fillInStackTrace()

    3. getCause(): Returns the cause of the Throwable or null if the cause is unknown.

    public Throwable getCause()

    4. getLocalizedMessage(): Provides a localized description of the Throwable.

    public String getLocalizedMessage()

    5. getMessage(): Returns the detailed message string.

    public String getMessage()

    6. getStackTrace(): Returns an array of stack trace elements representing the stack frames.

    public StackTraceElement[] getStackTrace()

    7. getSuppressed(): Returns an array containing all suppressed exceptions.

    public final Throwable[] getSuppressed()

    8. initCause(Throwable cause): Initializes the cause of the Throwable.

    public Throwable initCause(Throwable cause)

    9. printStackTrace(): Prints the throwable and its stack trace to the standard error stream.

    public void printStackTrace()

    10. setStackTrace(StackTraceElement[] stackTrace): Sets the stack trace elements for the Throwable.

    public void setStackTrace(StackTraceElement[] stackTrace)

    11. toString(): Returns a short description of the Throwable.

    public String toString()
  • Access Modifiers

    Public vs Protected vs Package vs Private Access Modifier in Java

    Access Modifiers in Java

    In Java, access modifiers are used to control the visibility and accessibility of classes, methods, and variables. By using these modifiers, we provide the JVM with information like whether a class can be accessed from outside its package, whether child classes can be created, and whether object instantiation is allowed.

    Modifier 1: Public Access Modifier

    When a class is declared as public, it can be accessed from anywhere. The same applies to methods and variables declared as public within that class.

    Example:

    // Creating a package
    package pack1;
    
    // Declaring a public class
    public class MyClass1 {
    
        // Declaring a public method
        public void display() {
            System.out.println("Public Access Modifier Example");
        }
    }

    In another package, you can import this class and use it:

    // Creating a package
    package pack2;
    
    // Importing the class from pack1
    import pack1.MyClass1;
    
    public class Main {
        public static void main(String[] args) {
            // Creating an object of class MyClass1
            MyClass1 obj = new MyClass1();
            // Calling the public method
            obj.display();
        }
    }

    Output:

    Public Access Modifier Example

    If MyClass1 was not public, you would receive a compile-time error stating that MyClass1 is not accessible from another package.

    Modifier 2: Protected Access Modifier

    The protected modifier is applicable to data members, methods, and constructors but not for top-level classes or interfaces. When a member is declared as protected, it is accessible within the same package and in subclasses of other packages.

    Example:

    package pack1;
    
    // Declaring a parent class
    class MyClass2 {
    
        // Declaring a protected method
        protected void show() {
            System.out.println("Protected Access Modifier Example");
        }
    }

    Now, in another package, we can access it through inheritance:

    package pack2;
    
    // Importing the class from pack1
    import pack1.MyClass2;
    
    class ChildClass extends MyClass2 {
        public static void main(String[] args) {
            // Creating an instance of the child class
            ChildClass obj = new ChildClass();
            // Accessing the protected method
            obj.show();
        }
    }

    Output:

    Protected Access Modifier Example

    We can access the protected method in a subclass even from a different package, but not directly from a non-child class in another package.

    Modifier 3: Private Access Modifier

    The private modifier restricts access to members within the same class only. Neither child classes nor other classes, even within the same package, can access private members.

    Example:

    // Defining a class
    class MyClass3 {
    
        // Declaring a private method
        private void secret() {
            System.out.println("Private Access Modifier Example");
        }
    
        public void accessPrivate() {
            // Accessing the private method within the same class
            secret();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyClass3 obj = new MyClass3();
            // Accessing the public method
            obj.accessPrivate();
        }
    }

    Output:

    Private Access Modifier Example

    Modifier 4: Default (Package) Access Modifier

    When no access modifier is specified, the default access level (also called “package-private”) applies. This means that the class or its members are accessible only within the same package and not from other packages.

    Example:

    // Defining a class with default access
    class MyClass4 {
    
        // Declaring a default-access method
        void displayMessage() {
            System.out.println("Default Access Modifier Example");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            // Creating an object of MyClass4
            MyClass4 obj = new MyClass4();
            // Calling the default-access method
            obj.displayMessage();
        }
    }

    Output:

    Default Access Modifier Example
    Summary of Differences Between Access Modifiers
    ModifierApplicabilityAccessibility From Same PackageAccessibility From Different PackageAccessibility from Subclass Outside PackageAccessibility from Non-subclass Outside Package
    PublicClasses, Methods, FieldsYesYesYesYes
    ProtectedMethods, FieldsYesNoYes (only in subclasses)No
    PrivateMethods, FieldsNoNoNoNo
    Default (Package)Classes, Methods, FieldsYesNoNoNo

    Access and Non Access Modifiers in Java

    Java is one of the most popular and widely-used programming languages, known for its speed, reliability, and security. Java applications can be found everywhere—from desktop software to web applications, scientific supercomputers to gaming consoles, and mobile phones to the Internet. In this guide, we’ll explore how to write a simple Java program.

    Steps to Implement a Java Program

    To implement a Java application, follow these key steps:

    1. Creating the Program
    2. Compiling the Program
    3. Running the Program

    If you’re looking to dive deeper into Java and gain a strong understanding of the entire development process, consider enrolling in a structured Java programming course. These courses provide hands-on experience and cover everything from basic to advanced topics, allowing you to develop efficient and scalable applications.

    Example of a Simple Java Program

    Here’s a basic example to illustrate the process:

    // Class with multiple access modifiers
    class AccessExample {
    
        public int publicVar = 10;
        private int privateVar = 20;
        protected int protectedVar = 30;
        int defaultVar = 40; // default access level (package-private)
    
        // Public method
        public void publicMethod() {
            System.out.println("Public Method");
        }
    
        // Private method
        private void privateMethod() {
            System.out.println("Private Method");
        }
    
        // Protected method
        protected void protectedMethod() {
            System.out.println("Protected Method");
        }
    
        // Default method
        void defaultMethod() {
            System.out.println("Default Method");
        }
    }
    
    public class MainClass {
        public static void main(String[] args) {
            AccessExample obj = new AccessExample();
    
            // Accessing variables
            System.out.println("Public variable: " + obj.publicVar);
            System.out.println("Protected variable: " + obj.protectedVar);
            System.out.println("Default variable: " + obj.defaultVar);
    
            // Accessing methods
            obj.publicMethod();
            obj.protectedMethod();
            obj.defaultMethod();
        }
    }

    Output:

    Public variable: 10
    Protected variable: 30
    Default variable: 40
    Public Method
    Protected Method
    Default Method

    The private member privateVar and method privateMethod() are not accessible from the MainClass since they are declared as private.

    Non-Access Modifiers

    Non-access modifiers in Java provide additional functionalities beyond access control. Java has several non-access modifiers, which can be applied to classes, methods, and variables to convey specific behavior to the JVM:

    1. static: The static modifier is used to declare class-level methods and variables, meaning they belong to the class rather than to instances.
    2. final: The final modifier is used to declare constants (final variables), methods that cannot be overridden, and classes that cannot be subclassed.
    3. abstract: This modifier applies to classes that cannot be instantiated directly and to methods that must be implemented by subclasses.
    4. synchronized: The synchronized keyword is used to control access to methods by multiple threads to ensure that only one thread executes the method at a time.
    transient: This modifier is used to declare that a variable should not be serialized.
    5. volatile: The volatile modifier informs the JVM that a variable’s value may change in a way that is not visible to other threads.
    6. native: The native keyword indicates that a method is implemented in platform-specific code, typically in C or C++.

    Example: Non-Access Modifiers

    // Class with non-access modifiers
    class NonAccessExample {
    
        // Static variable
        static int staticVar = 50;
    
        // Final variable
        final int finalVar = 100;
    
        // Transient variable
        transient int transientVar = 200;
    
        // Static method
        static void staticMethod() {
            System.out.println("Static Method");
        }
    
        // Final method
        final void finalMethod() {
            System.out.println("Final Method");
        }
    
        // Synchronized method
        synchronized void synchronizedMethod() {
            System.out.println("Synchronized Method");
        }
    }
    
    public class MainClassNonAccess {
        public static void main(String[] args) {
            NonAccessExample obj = new NonAccessExample();
    
            // Accessing static members
            System.out.println("Static variable: " + NonAccessExample.staticVar);
            NonAccessExample.staticMethod();
    
            // Accessing final member and method
            System.out.println("Final variable: " + obj.finalVar);
            obj.finalMethod();
    
            // Calling synchronized method
            obj.synchronizedMethod();
        }
    }

    Output:

    Static variable: 50
    Static Method
    Final variable: 100
    Final Method
    Synchronized Method
    Key Differences Between Access and Non-Access Modifiers
    • Access modifiers control the visibility and access of classes, methods, and fields.
    • Non-access modifiers provide additional characteristics such as immutability, concurrency control, or indicate that something is platform-specific.
  • Keyword

    Java Keywords

    In Java, keywords or reserved words are predefined terms that are used by the language for specific internal processes or actions. As such, these keywords cannot be used as variable names or identifiers; doing so will result in a compile-time error.

    Example of Using Java Keywords as Variable Names

    // Java Program to Illustrate the Consequences of Using Keywords as Variable Names
    
    // Driver Class
    class Example {
        // Main Function
        public static void main(String[] args) {
            // Note "public" is a reserved word in Java
            String public = "Hello, Java!";
            System.out.println(public);
        }
    }

    Output:

    ./Example.java:6: error: illegal start of expression
            String public = "Hello, Java!";
            ^
    1 error
    Java Keywords List

    Java contains a set of keywords that are typically highlighted in different colors in an IDE or editor to distinguish them from other words. Here’s a table summarizing the keywords and their associated actions:

    KeywordUsage
    abstractSpecifies that a class or method will be implemented later in a subclass.
    assertIndicates that a condition is assumed to be true at a specific point in the program.
    booleanA data type that can hold true and false values.
    breakA control statement used to exit from loops or switch statements.
    byteA data type that can hold 8-bit data values.
    caseUsed in switch statements to define code blocks.
    catchHandles exceptions thrown by try statements.
    charA data type that can hold a single 16-bit Unicode character.
    classDeclares a new class.
    continueSkips the current iteration of a loop and proceeds to the next iteration.
    defaultSpecifies the default block of code in a switch statement.
    doBegins a do-while loop.
    doubleA data type for 64-bit floating-point numbers.
    elseSpecifies the alternative branch in an if statement.
    enumUsed to declare an enumerated type.
    extendsIndicates that a class is derived from another class or interface.
    finalIndicates that a variable holds a constant value or that a method cannot be overridden.
    finallyA block of code in a try-catch structure that will always execute.
    floatA data type for 32-bit floating-point numbers.
    forUsed to start a for loop.
    ifTests a condition and executes code based on the result.
    implementsSpecifies that a class implements an interface.
    importReferences other classes or packages.
    instanceofChecks whether an object is an instance of a specific class or implements an interface.
    intA data type that can hold a 32-bit signed integer.
    interfaceDeclares an interface.
    longA data type that can hold a 64-bit signed integer.
    nativeSpecifies that a method is implemented in platform-specific code.
    newCreates new objects.
    nullIndicates that a reference does not point to any object.
    packageDeclares a Java package.
    privateAn access specifier that restricts access to the class where it is declared.
    protectedAn access specifier that allows access to subclasses and classes in the same package.
    publicAn access specifier that makes a class, method, or variable accessible throughout the application.
    returnSends control and possibly a return value back from a called method.
    shortA data type that can hold a 16-bit signed integer.
    staticIndicates that a method or variable belongs to the class rather than an instance.
    strictfpEnsures floating-point calculations follow strict rules for precision.
    superRefers to the superclass of the current object.
    switchA statement that executes code based on a specified value.
    synchronizedIndicates that a method or block is synchronized for thread safety.
    thisRefers to the current object within a method or constructor.
    throwUsed to explicitly throw an exception.
    throwsSpecifies which exceptions a method can throw.
    transientIndicates that a variable is not part of an object’s persistent state.
    tryStarts a block of code that will be tested for exceptions.
    voidSpecifies that a method does not return a value.
    volatileIndicates that a variable may be changed unexpectedly, used in multithreading.
    whileStarts a while loop.
    sealedDeclares a class that restricts which classes can extend it.
    permitsUsed within a sealed class declaration to specify permitted subclasses.

    Important Notes on Java Keywords

    The keywords const and goto are reserved for potential future use but are not currently utilized in Java.

    • const: Reserved for future use.
    • goto: Reserved for future use.

    Important Keywords in Java

    Keywords are a reserved set of words in a programming language that are used for specific predefined actions.

    • abstract: This non-access modifier is used for classes and methods to achieve abstraction. For more information, see the abstract keyword in Java.
    • enum: This keyword is used to define an enumeration in Java.
    • instanceof: This keyword checks whether an object is an instance of a specified type (class, subclass, or interface).
    • private: This access modifier restricts visibility; anything declared as private is not accessible outside its class.
    • protected: Use this keyword to allow access to an element outside of its package, but only to classes that directly subclass your class.
    • public: Anything declared as public can be accessed from anywhere in the application. For more information on access modifiers, refer to Access Modifiers in Java.
    • static: This keyword is used to create members (blocks, methods, variables, nested classes) that can be accessed without a reference to a specific instance. For more details, refer to the static keyword in Java.
    • strictfp: This keyword restricts floating-point calculations, ensuring consistent results across different platforms. For more information, refer to the strictfp keyword in Java.
    • synchronized: This keyword can be applied to methods or blocks to achieve synchronization in Java. For more details, see Synchronized in Java.
    • transient: This variable modifier is used during serialization. When we don’t want to save the value of a particular variable in a file during serialization, we use the transient keyword. For more information, refer to the transient keyword in Java.
    • volatile: The volatile modifier indicates to the compiler that the variable can be modified unexpectedly by other parts of the program. For more details, see the volatile keyword in Java.

    Example Program Using Some Keywords

    // Java Program to Demonstrate the Use of Various Keywords
    
    // Abstract class definition
    abstract class Animal {
        abstract void sound(); // Abstract method
    }
    
    // Enum definition
    enum Color {
        RED, GREEN, BLUE
    }
    
    // Class implementing the abstract class
    class Dog extends Animal {
        void sound() {
            System.out.println("Bark");
        }
    }
    
    // Main class
    public class KeywordExample {
        // Static variable
        static int count = 0;
    
        // Synchronized method
        synchronized void increment() {
            count++;
        }
    
        public static void main(String[] args) {
            Dog dog = new Dog();
            dog.sound(); // Outputs: Bark
    
            Color myColor = Color.RED; // Using enum
            System.out.println("Color: " + myColor); // Outputs: Color: RED
    
            KeywordExample example = new KeywordExample();
            example.increment();
            System.out.println("Count: " + count); // Outputs: Count: 1
        }
    }

    Output:

    Bark
    Color: RED
    Count: 1

    Super Keyword in Java

    Characteristics of the super Keyword in Java

    In Java, the super keyword is utilized to refer to the parent class of a subclass. Here are some key characteristics:

    • Calling Superclass Constructors: When a subclass is instantiated, its constructor must invoke the constructor of its parent class using super().
    • Calling Superclass Methods: A subclass can invoke a method defined in its parent class using the super keyword, which is helpful when the subclass wants to execute the parent class’s implementation of that method as well.
    • Accessing Superclass Fields: A subclass can reference a field from its parent class using the super keyword. This is useful in cases where both the subclass and parent class have a field with the same name.
    • Placement in Constructor: The super() statement must be the first line in the constructor of the subclass when invoking a superclass constructor.
    • Static Context Restriction: The super keyword cannot be used in a static context, such as within static methods or static variable initializers.
    • Optional Use for Method Calls: While the super keyword can be used to call a parent class method, it is not necessary if the method is not overridden in the subclass. In such cases, calling the method directly will invoke the parent class’s version.

    Overall, the super keyword is a powerful tool for subclassing in Java, allowing subclasses to inherit and extend the functionality of their parent classes.

    Uses of the super Keyword in Java

    The super keyword is primarily used in the following contexts:

    1. Using super with Variables
    2. Using super with Methods
    3.Using super with Constructors

    1. Using super with Variables : This situation arises when both a derived class and its base class have identical data members, leading to potential ambiguity.

    Example:

    // Example of super keyword with variables
    
    // Base class Animal
    class Animal {
        String type = "Mammal";
    }
    
    // Subclass Dog extending Animal
    class Dog extends Animal {
        String type = "Canine";
    
        void display() {
            // Print type from base class (Animal)
            System.out.println("Animal Type: " + super.type);
        }
    }
    
    // Driver Program
    public class Test {
        public static void main(String[] args) {
            Dog dog = new Dog();
            dog.display();
        }
    }

    Output:

    Drawing a circle.
    Drawing a shape.

    In this example, both the base class and subclass have a member type. The super keyword allows us to access the type variable of the base class.

    2. Using super with Methods : This usage occurs when we need to call a method from the parent class. If both the parent and child classes have methods with the same name, the super keyword resolves ambiguity.

    Example:

    // Example of super keyword with methods
    
    // Superclass Shape
    class Shape {
        void draw() {
            System.out.println("Drawing a shape.");
        }
    }
    
    // Subclass Circle extending Shape
    class Circle extends Shape {
        void draw() {
            System.out.println("Drawing a circle.");
        }
    
        void display() {
            // Calls the current class draw() method
            draw();
            // Calls the parent class draw() method
            super.draw();
        }
    }
    
    // Driver Program
    public class Test {
        public static void main(String[] args) {
            Circle circle = new Circle();
            circle.display();
        }
    }

    In this example, when calling the draw() method, the current class’s implementation is executed, but the super keyword allows us to invoke the superclass’s method as well.

    3. Using super with Constructors : The super keyword can also be employed to access the constructor of the parent class. It can call both parameterized and non-parameterized constructors, depending on the situation.

    Example 1:

    // Example of super keyword with constructors
    
    // Superclass Animal
    class Animal {
        Animal() {
            System.out.println("Animal class Constructor");
        }
    }
    
    // Subclass Dog extending Animal
    class Dog extends Animal {
        Dog() {
            // Invoke the parent class constructor
            super();
            System.out.println("Dog class Constructor");
        }
    }
    
    // Driver Program
    public class Test {
        public static void main(String[] args) {
            Dog dog = new Dog();
        }
    }

    Output:

    Animal class Constructor
    Dog class Constructor
    Advantages of Using the super Keyword in Java

    The super keyword in Java offers several advantages in object-oriented programming:

    • Code Reusability: Using super allows subclasses to inherit functionality from their parent classes, promoting code reuse and reducing redundancy.
    • Supports Polymorphism: Subclasses can override methods and access fields from their parent classes using super, enabling polymorphism and allowing for more flexible and extensible code.
    • Access to Parent Class Behavior: Subclasses can utilize methods and fields defined in their parent classes through super, allowing them to leverage existing behavior without needing to reimplement it.
    • Customization of Behavior: By overriding methods and using super to invoke the parent implementation, subclasses can customize and extend the behavior of their parent classes.
    • Facilitates Abstraction and Encapsulation: The use of super promotes encapsulation and abstraction by allowing subclasses to focus on their own behavior while relying on the parent class to manage lower-level details.

    final Keyword in Java

    The final keyword in Java is a non-access modifier that can be applied to variables, methods, or classes. It is used to impose restrictions on the element to which it is applied.

    Key Uses of final in Java:

    1. Using super with Variables
    2. Using super with Methods
    3. Using super with Constructors

    By exploring these uses in detail, the Java programming course allows developers to understand how and when to apply the final keyword effectively.

    Characteristics of final in Java:

    1. Final Variables: When a variable is declared as final, its value cannot be changed once it is initialized. This makes it ideal for defining constants.
    2. Final Methods: Declaring a method as final prevents subclasses from modifying or overriding that method, ensuring its behavior is consistent.
    3. Final Classes: A final class cannot be extended, meaning no other class can inherit from it. This is useful for creating classes that are intended to be used as-is.
    4. Initialization of Final Variables: Final variables must be initialized when they are declared or in a constructor. If not, the program will not compile.
    5. Performance: The use of final can sometimes improve performance, as the compiler optimizes final variables or methods better since their behavior is predictable.
    6. Security: By making certain variables or methods final, you can prevent malicious code from altering critical parts of your program.

    Example of final Variable:

    The final keyword in Java is a non-access modifier that can be applied to variables, methods, or classes. It is used to impose restrictions on the element to which it is applied.

    Key Uses of final in Java:

    1. Variables : A variable declared with final cannot have its value changed after initialization.
    2. Methods : A method declared with final cannot be overridden by subclasses.
    3. Classes:  A class declared with final cannot be subclassed or extended.

    By exploring these uses in detail, the Java programming course allows developers to understand how and when to apply the final keyword effectively.

    Characteristics of final in Java:

    1. Final Variables: When a variable is declared as final, its value cannot be changed once it is initialized. This makes it ideal for defining constants.
    2. Final Methods: Declaring a method as final prevents subclasses from modifying or overriding that method, ensuring its behavior is consistent.
    3. Final Classes: A final class cannot be extended, meaning no other class can inherit from it. This is useful for creating classes that are intended to be used as-is.
    4. Initialization of Final Variables: Final variables must be initialized when they are declared or in a constructor. If not, the program will not compile.
    5. Performance: The use of final can sometimes improve performance, as the compiler optimizes final variables or methods better since their behavior is predictable.
    6. Security: By making certain variables or methods final, you can prevent malicious code from altering critical parts of your program.

    Example of final Variable:

    public class Example {
        public static void main(String[] args) {
            // Declaring a final variable
            final double CONSTANT = 3.14;
    
            // Printing the value
            System.out.println("Constant value: " + CONSTANT);
    
            // Attempting to change the value would cause a compile-time error
            // CONSTANT = 3.15;
        }
    }

    Output:

    Constant value: 3.14

    Different Ways to Use final Variable:

    1. Final Variable Initialization at Declaration:

    final int MAX_LIMIT = 100;

    2. Blank Final Variable:

    final int MAX_LIMIT; // Must be initialized later in the constructor

    3. Static Final Variable:

    static final double E = 2.718;

    4. Static Blank Final Variable Initialized in Static Block:

    static final int MAX_VALUE;
    static {
        MAX_VALUE = 999;
    }
    Initialization of Final Variables:

    Final variables must be initialized either at declaration or inside constructors. The Java compiler ensures that once a final variable is initialized, it cannot be reassigned.

    Example of Blank Final Variable:

    class Demo {
        final int THRESHOLD;
    
        // Constructor to initialize blank final variable
        public Demo(int value) {
            this.THRESHOLD = value;
        }
    
        public static void main(String[] args) {
            Demo demo = new Demo(10);
            System.out.println("Threshold: " + demo.THRESHOLD);
        }
    }

    Output:

    Threshold: 10
    Final Reference Variable (Non-Transitivity):

    In the case of reference variables declared as final, you can modify the internal state of the object, but the reference cannot be reassigned.

    Example of Final Reference Variable:

    class Example {
        public static void main(String[] args) {
            final StringBuilder message = new StringBuilder("Hello");
            System.out.println(message);
    
            // Modifying the internal state of the final object
            message.append(", World!");
            System.out.println(message);
    
            // Reassigning the reference would cause a compile-time error
            // message = new StringBuilder("Hi");
        }
    }

    Output:

    Hello
    Hello, World!
    Final Local Variable:

    A final variable inside a method is called a local final variable. It can be initialized once, and any attempt to reassign it will result in an error.

    class Example {
        public static void main(String[] args) {
            final int LIMIT;
            LIMIT = 100;  // Variable initialized
            System.out.println("Limit: " + LIMIT);
    
            // LIMIT = 200; // This line would cause a compile-time error
        }
    }

    Output:

    Limit: 100
    Final Classes:

    A class declared as final cannot be extended by any subclass. This is useful when you want to ensure the class’s functionality remains intact and is not modified by other developers.

    Example of Final Class:

    final class Car {
        void start() {
            System.out.println("Car is starting");
        }
    }
    
    // The following class would cause a compile-time error
    // class SportsCar extends Car { }
    
    public class Main {
        public static void main(String[] args) {
            Car myCar = new Car();
            myCar.start();
        }
    }

    Output:

    Car is starting
    Final Methods:

    When a method is declared as final, it cannot be overridden by any subclass.

    class Parent {
        final void show() {
            System.out.println("This is a final method.");
        }
    }
    
    class Child extends Parent {
        // The following method would cause a compile-time error
        // void show() {
        //     System.out.println("Trying to override.");
        // }
    }

    static Keyword in Java

    The static keyword in Java is primarily utilized for memory management. It allows variables or methods to be shared across all instances of a class. Users can apply the static keyword to variables, methods, blocks, and nested classes. Unlike instance members, static members belong to the class itself rather than any particular instance, making them ideal for defining constants or methods that should remain consistent across all objects of the class.

    Key Uses of static in Java:

    1. Blocks
    2. Variables
    3. Methods
    4. Classes

    Characteristics of the static Keyword:

    The static keyword plays a crucial role in memory management by enabling class-level variables and methods. For a comprehensive understanding of how to effectively use static, the Java Programming Course offers detailed explanations and practical examples. Here are some key characteristics of the static keyword in Java:

    • Shared Memory Allocation: Static variables and methods are allocated memory space only once during the program’s execution. This shared memory space is accessible by all instances of the class, making static members ideal for maintaining global state or shared functionality.
    • Accessible Without Object Instantiation: Static members can be accessed without creating an instance of the class. This makes them useful for utility functions and constants that need to be accessible throughout the program.
    • Associated with the Class, Not Objects: Static members are tied to the class itself, not to individual objects. Therefore, any changes to a static member are reflected across all instances of the class. Static members can be accessed using the class name rather than an object reference.
    • Cannot Access Non-Static Members: Static methods and variables cannot directly access non-static members of a class because they are not associated with any particular instance of the class.
    • Can Be Overloaded, but Not Overridden: Static methods can be overloaded (multiple methods with the same name but different parameters), but they cannot be overridden since they are linked to the class rather than any instance.
    • Early Access: Static members can be accessed before any objects of the class are created and without referencing any object. For example, in the Java program below, the static method displayMessage() is called without creating an object of the Utility class.

    Example Program Accessing Static Method Without Object Creation

    // Java program to demonstrate accessing a static method without creating an object
    
    class Utility {
        // Static method
        static void displayMessage() {
            System.out.println("Welcome to the Utility class!");
        }
    
        public static void main(String[] args) {
            // Calling the static method without creating an instance of Utility
            Utility.displayMessage();
        }
    }

    Output:

    Welcome to the Utility class!
    Static Blocks

    Static blocks are used for initializing static variables or executing code that needs to run once when the class is loaded. They are executed in the order they appear in the class.

    Example of Static Block Usage

    // Java program to demonstrate the use of static blocks
    
    class Configuration {
        // Static variables
        static String appName;
        static int version;
    
        // Static block
        static {
            System.out.println("Initializing Configuration...");
            appName = "MyApp";
            version = 1;
        }
    
        public static void main(String[] args) {
            System.out.println("Application Name: " + appName);
            System.out.println("Version: " + version);
        }
    }

    Output:

    Initializing Configuration...
    Application Name: MyApp
    Version: 1
    Static Variables

    Static variables, also known as class variables, are shared among all instances of a class. They are typically used to store common properties or constants.

    Important Points about Static Variables:
    • Class-Level Scope: Static variables are declared at the class level and are shared by all instances.
    • Initialization Order: Static blocks and static variables are executed in the order they appear in the program.

    Example Demonstrating Static Variable Initialization Order

    // Java program to demonstrate the initialization order of static blocks and variables
    
    class InitializationDemo {
        // Static variable initialized by a static method
        static int initialValue = initialize();
    
        // Static block
        static {
            System.out.println("Inside static block.");
        }
    
        // Static method
        static int initialize() {
            System.out.println("Initializing static variable.");
            return 50;
        }
    
        public static void main(String[] args) {
            System.out.println("Value of initialValue: " + initialValue);
            System.out.println("Inside main method.");
        }
    }

    Output:

    Initializing static variable.
    Inside static block.
    Value of initialValue: 50
    Inside main method.
    Static Methods

    Static methods belong to the class rather than any particular instance. The most common example of a static method is the main() method. Static methods have several restrictions:

    • They can only directly call other static methods.
    • They can only directly access static data.
    • They cannot refer to this or super keywords.

    Example Demonstrating Restrictions on Static Methods

    // Java program to demonstrate restrictions on static methods
    
    class Calculator {
        // Static variable
        static int total = 0;
    
        // Instance variable
        int count = 0;
    
        // Static method
        static void add(int value) {
            total += value;
            System.out.println("Total after addition: " + total);
    
            // The following lines would cause compilation errors
            // count += 1; // Error: non-static variable cannot be referenced from a static context
            // displayCount(); // Error: non-static method cannot be referenced from a static context
        }
    
        // Instance method
        void displayCount() {
            System.out.println("Count: " + count);
        }
    
        public static void main(String[] args) {
            Calculator.add(10);
            Calculator.add(20);
        }
    }

    Output:

    Total after addition: 10
    Total after addition: 30
    When to Use Static Variables and Methods
    • Static Variables: Use static variables for properties that are common to all instances of a class. For example, if all students share the same school name, the school name can be a static variable.
    • Static Methods: Use static methods for operations that do not require data from instances of the class. Utility or helper methods that perform tasks independently of object state are ideal candidates for static methods.

    Example Illustrating Static Variables and Methods

    // Java program to demonstrate the use of static variables and methods
    
    class School {
        String studentName;
        int studentId;
    
        // Static variable for school name
        static String schoolName;
    
        // Static counter to assign unique IDs
        static int idCounter = 1000;
    
        public School(String name) {
            this.studentName = name;
            this.studentId = generateId();
        }
    
        // Static method to generate unique IDs
        static int generateId() {
            return idCounter++;
        }
    
        // Static method to set the school name
        static void setSchoolName(String name) {
            schoolName = name;
        }
    
        // Instance method to display student information
        void displayInfo() {
            System.out.println("Student Name: " + studentName);
            System.out.println("Student ID: " + studentId);
            System.out.println("School Name: " + schoolName);
            System.out.println("--------------------------");
        }
    
        public static void main(String[] args) {
            // Setting the static school name without creating an instance
            School.setSchoolName("Greenwood High");
    
            // Creating student instances
            School student1 = new School("Emma");
            School student2 = new School("Liam");
            School student3 = new School("Olivia");
    
            // Displaying student information
            student1.displayInfo();
            student2.displayInfo();
            student3.displayInfo();
        }
    }

    Output:

    Student Name: Emma
    Student ID: 1000
    School Name: Greenwood High
    --------------------------
    Student Name: Liam
    Student ID: 1001
    School Name: Greenwood High
    --------------------------
    Student Name: Olivia
    Student ID: 1002
    School Name: Greenwood High
    --------------------------
    Static Classes (Nested Static Classes)

    A class can be declared as static only if it is a nested class. Top-level classes cannot be declared as static. Static nested classes do not require a reference to an instance of the outer class and cannot access non-static members of the outer class.

    Example of a Static Nested Class

    // Java program to demonstrate the use of static nested classes
    
    class OuterClass {
        private static String outerMessage = "Hello from OuterClass!";
    
        // Static nested class
        static class NestedStaticClass {
            void display() {
                System.out.println(outerMessage);
            }
        }
    
        public static void main(String[] args) {
            // Creating an instance of the static nested class without an instance of OuterClass
            OuterClass.NestedStaticClass nestedObj = new OuterClass.NestedStaticClass();
            nestedObj.display();
        }
    }

    Output:

    Hello from OuterClass!

    enum in Java

    What is an Enum in Java?

    In Java, Enum is a special data type used to define collections of constants. It allows you to represent a fixed set of predefined constants, such as the days of the week, the four seasons, etc. An enum is more than just a list of constants—it can contain methods, constructors, and variables, just like any other Java class.

    Enums are particularly useful when you know all possible values at compile time and want to prevent invalid values from being used.

    Key Properties of Enums
    • Enum Constants: Each enum constant is an object of the enum type.
    • Implicit Modifiers: Enum constants are publicstatic, and final.
    • Switch Compatibility: You can use enums with switch statements.
    • Constructor: An enum can contain a constructor that is invoked once for each constant.
    • Method Support: Enums can have methods like regular classes, including abstract methods that must be implemented by each enum constant.
    Enum Declaration in Java

    Enums can be declared both inside or outside a class, but not inside a method.

    1. Declaration Outside the Class

    // Enum declared outside the class
    enum Direction {
        NORTH,
        SOUTH,
        EAST,
        WEST
    }
    
    public class TestEnum {
        public static void main(String[] args) {
            Direction direction = Direction.NORTH;
            System.out.println("The direction is: " + direction);
        }
    }

    Output:

    The direction is: NORTH

    2. Declaration Inside the Class

    // Enum declared inside a class
    public class Weather {
        enum Season {
            SPRING,
            SUMMER,
            FALL,
            WINTER
        }
    
        public static void main(String[] args) {
            Season current = Season.WINTER;
            System.out.println("The current season is: " + current);
        }
    }

    Output:

    The current season is: WINTER
    Enum in Switch Statements

    Enums can be used in switch statements to handle different cases based on enum values.

    // Enum in a switch statement
    public class DaysOfWeek {
        enum Day {
            MONDAY,
            TUESDAY,
            WEDNESDAY,
            THURSDAY,
            FRIDAY,
            SATURDAY,
            SUNDAY
        }
    
        public static void main(String[] args) {
            Day today = Day.FRIDAY;
    
            switch (today) {
                case MONDAY:
                    System.out.println("It's the start of the work week.");
                    break;
                case FRIDAY:
                    System.out.println("Almost the weekend!");
                    break;
                case SATURDAY:
                case SUNDAY:
                    System.out.println("It's the weekend!");
                    break;
                default:
                    System.out.println("It's a regular workday.");
            }
        }
    }

    Output:

    Almost the weekend!
    Looping Through Enum Constants

    You can iterate over the constants in an enum using the values() method, which returns an array of all enum constants.

    // Looping through enum constants
    public class ColorExample {
        enum Color {
            RED, GREEN, BLUE, YELLOW
        }
    
        public static void main(String[] args) {
            for (Color color : Color.values()) {
                System.out.println("Color: " + color);
            }
        }
    }

    Output:

    Color: RED
    Color: GREEN
    Color: BLUE
    Color: YELLOW
    Enum with Constructor and Method

    Enums can contain constructors and methods, making them more powerful than just simple constants.

    // Enum with constructor and method
    public class CarTypeExample {
        enum CarType {
            SEDAN(4), SUV(6), TRUCK(8);
    
            private int seats;
    
            // Enum constructor
            CarType(int seats) {
                this.seats = seats;
            }
    
            public int getSeats() {
                return seats;
            }
        }
    
        public static void main(String[] args) {
            CarType myCar = CarType.SUV;
            System.out.println("My car type is: " + myCar + " with " + myCar.getSeats() + " seats.");
        }
    }

    Output:

    My car type is: SUV with 6 seats.
    Enum with Abstract Methods

    Enums can also contain abstract methods that each constant must implement.

    // Enum with abstract method
    public class PlanetExample {
        enum Planet {
            MERCURY {
                public String getOrbitalPeriod() {
                    return "88 days";
                }
            },
            EARTH {
                public String getOrbitalPeriod() {
                    return "365 days";
                }
            },
            MARS {
                public String getOrbitalPeriod() {
                    return "687 days";
                }
            };
    
            public abstract String getOrbitalPeriod();
        }
    
        public static void main(String[] args) {
            Planet planet = Planet.EARTH;
            System.out.println("The orbital period of " + planet + " is " + planet.getOrbitalPeriod());
        }
    }

    Output:

    // Using EnumSet to iterate over a specific range of enum values
    import java.util.EnumSet;
    
    public class DaysRangeExample {
        enum Day {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        }
    
        public static void main(String[] args) {
            EnumSet<Day> workdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
    
            for (Day day : workdays) {
                System.out.println("Workday: " + day);
            }
        }
    }

    Output:

    The orbital period of EARTH is 365 days

    transient keyword in Java

    The transient keyword in Java is used to indicate that a particular field should not be serialized when the object is written to a stream. During serialization, if a field is marked as transient, its value is not saved, and when the object is deserialized, that field is assigned its default value according to its data type.

    The transient keyword is especially useful for sensitive data that you don’t want to store, such as passwords or fields that can be recalculated at runtime, such as a person’s age or a timestamp.

    Usage of transient Keyword

    When an object is serialized, all its fields are saved unless they are marked with the transient modifier. If a field is marked as transient, it is skipped during the serialization process, and its value will be reset to the default when the object is deserialized.

    This is often used to protect sensitive data, such as passwords, or for fields that can be derived from other fields.

    Example of transient Keyword:

    // A simple class to demonstrate the use of the transient keyword
    import java.io.*;
    
    class Example implements Serializable {
        // Regular fields
        private String username;
        private String email;
    
        // Password field marked as transient for security
        private transient String password;
    
        // Age field marked as transient because it can be recalculated
        transient int age;
    
        // Constructor
        public Example(String username, String email, String password, int age) {
            this.username = username;
            this.email = email;
            this.password = password;
            this.age = age;
        }
    
        // Display user information
        public void displayInfo() {
            System.out.println("Username: " + username);
            System.out.println("Email: " + email);
            System.out.println("Password: " + password);
            System.out.println("Age: " + age);
        }
    }
    
    public class TestTransient {
        public static void main(String[] args) throws Exception {
            // Create an instance of the class
            Example user = new Example("JohnDoe", "john@example.com", "secretPassword", 30);
    
            // Serialization process
            FileOutputStream fileOut = new FileOutputStream("user_data.txt");
            ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
            objectOut.writeObject(user);
            objectOut.close();
            fileOut.close();
    
            // Deserialization process
            FileInputStream fileIn = new FileInputStream("user_data.txt");
            ObjectInputStream objectIn = new ObjectInputStream(fileIn);
            Example deserializedUser = (Example) objectIn.readObject();
            objectIn.close();
            fileIn.close();
    
            // Display the deserialized object's info
            System.out.println("After Deserialization:");
            deserializedUser.displayInfo();
        }
    }

    Output:

    Username: JohnDoe
    Email: john@example.com
    Password: null
    Age: 0

    In this example:

    • The password and age fields are marked as transient, so they are not serialized.
    • When deserialized, the password field becomes null, and the age field is set to 0 (the default value for integers).
    transient and static Fields

    The transient keyword has no effect on static fields because static fields are not serialized as part of the object state. Similarly, marking a static field as transient has no impact, as static variables belong to the class and not the instance.

    Example with static and final Fields:

    // A simple class to demonstrate transient with static and final variables
    import java.io.*;
    
    class TestStaticFinal implements Serializable {
        // Regular fields
        int x = 100, y = 200;
    
        // Transient field
        transient int z = 300;
    
        // Transient has no effect on static variables
        transient static int a = 400;
    
        // Transient has no effect on final variables
        transient final int b = 500;
    
        public static void main(String[] args) throws Exception {
            TestStaticFinal object = new TestStaticFinal();
    
            // Serialize the object
            FileOutputStream fos = new FileOutputStream("static_final.txt");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(object);
            oos.close();
            fos.close();
    
            // Deserialize the object
            FileInputStream fis = new FileInputStream("static_final.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            TestStaticFinal deserializedObject = (TestStaticFinal) ois.readObject();
            ois.close();
            fis.close();
    
            // Display the values after deserialization
            System.out.println("x = " + deserializedObject.x);
            System.out.println("y = " + deserializedObject.y);
            System.out.println("z = " + deserializedObject.z);  // Will be 0 (default value)
            System.out.println("a = " + TestStaticFinal.a);     // Will be 400
            System.out.println("b = " + deserializedObject.b);  // Will be 500
        }
    }

    Output:

    x = 100
    y = 200
    z = 0
    a = 400
    b = 500

    volatile Keyword

    The volatile keyword in Java is used to ensure that updates to a variable are immediately visible to all threads. This is essential in a multithreaded environment to prevent inconsistencies due to threads caching variable values locally, which can lead to stale data being used. Let’s consider an example to better understand its behavior.

    Problem Without volatile:

    When multiple threads are operating on the same variable, they may maintain a local copy of the variable in their own cache. Changes made by one thread may not be immediately visible to the other threads, leading to unpredictable results.

    For instance:

    class SharedResource {
        // Without volatile, changes made by one thread
        // may not reflect immediately in others.
        static int sharedValue = 10;
    }

    With volatile, the sharedValue is always read from the main memory and never from the thread’s cache, ensuring that all threads have the most up-to-date value.

    Difference Between volatile and synchronized:
    • Mutual Exclusion: The synchronized keyword ensures that only one thread can access a critical section of code at any time.
    • Visibility: Both volatile and synchronized ensure that changes made by one thread are visible to other threads.

    However, if you only need to ensure visibility and don’t require atomic operations (such as incrementing), volatile can be used to avoid the overhead of synchronization.

    Example Using volatile:

    // Java program to demonstrate the use of volatile keyword
    public class VolatileDemo {
        private static volatile int counter = 0;
    
        public static void main(String[] args) {
            new UpdateThread().start();
            new MonitorThread().start();
        }
    
        // Thread that monitors changes to the volatile variable
        static class MonitorThread extends Thread {
            @Override
            public void run() {
                int localCounter = counter;
                while (localCounter < 5) {
                    if (localCounter != counter) {
                        System.out.println("Detected change in counter: " + counter);
                        localCounter = counter;
                    }
                }
            }
        }
    
        // Thread that updates the volatile variable
        static class UpdateThread extends Thread {
            @Override
            public void run() {
                int localCounter = counter;
                while (counter < 5) {
                    System.out.println("Incrementing counter to " + (localCounter + 1));
                    counter = ++localCounter;
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    Output (With volatile):

    Incrementing counter to 1
    Detected change in counter: 1
    Incrementing counter to 2
    Detected change in counter: 2
    Incrementing counter to 3
    Detected change in counter: 3
    Incrementing counter to 4
    Detected change in counter: 4
    Incrementing counter to 5
    Detected change in counter: 5

    Output (Without volatile):

    Incrementing counter to 1
    Incrementing counter to 2
    Incrementing counter to 3
    Incrementing counter to 4
    Incrementing counter to 5

    final, finally and finalize in Java

    Here’s the modified version of the content with different examples, and the output remains as requested:

    final Keyword in Java

    In Java, final is a reserved keyword, meaning we cannot use it as an identifier (variable name, method name, etc.). It serves distinct purposes depending on where it is applied — whether to variables, methods, or classes.

    1. final with Variables

    When a variable is declared as final, its value cannot be modified once initialized. Any attempt to change its value will result in a compile-time error.

    class Example1 {
        public static void main(String[] args) {
            // Non-final variable
            int x = 10;
    
            // Final variable
            final int y = 20;
    
            // Modifying non-final variable: Allowed
            x++;
    
            // Modifying final variable: Gives a compile-time error
            y++;  // Error: Cannot assign a value to a final variable 'y'
        }
    }

    Here, attempting to modify the value of the final variable y will cause a compile-time error.

    2. final with Classes

    If a class is declared final, it cannot be subclassed. In other words, no class can extend a final class.

    final class SuperClass {
        public void display() {
            System.out.println("This is a final class.");
        }
    }
    
    // The following class will cause a compile-time error as `SuperClass` is final and cannot be extended
    class SubClass extends SuperClass {
        // Compile-time error: Cannot inherit from final 'SuperClass'
    }

    3. final with Methods

    When a method is declared as final, it cannot be overridden by subclasses.

    class ParentClass {
        final void show() {
            System.out.println("Final method in the parent class.");
        }
    }
    
    class ChildClass extends ParentClass {
        // The following method will cause a compile-time error
        void show() {
            // Compile-time error: Cannot override the final method from 'ParentClass'
            System.out.println("Trying to override the final method.");
        }
    }

    In this case, the subclass ChildClass cannot override the final method show() from ParentClass.

    Note: final with Classes and Methods

    If a class is declared final, all its methods are implicitly final by default. However, its variables are not.

    final class FinalClass {
        // Method is final by default
        void display() {
            System.out.println("Final class method.");
        }
    
        // Static variables can still be modified
        static int value = 50;
    
        public static void main(String[] args) {
            // Modifying the static variable
            value = 60;
            System.out.println("Value: " + value);  // Output: 60
        }
    }
    finally Keyword

    The finally keyword is associated with try and catch blocks. It ensures that a block of code will always be executed, regardless of whether an exception occurs or not. The finally block is generally used for cleanup operations like closing resources (e.g., file handles, database connections).

    Example: finally with Exception Handling

    class ExampleFinally {
        public static void main(String[] args) {
            try {
                System.out.println("Inside try block");
                throw new RuntimeException("Exception in try");
            } finally {
                System.out.println("Finally block always executes");
            }
        }
    }

    In this example, even though an exception is thrown, the finally block still executes.

    Cases Involving finally Block:

    Case 1: No Exception Occurs

    class NoException {
        public static void main(String[] args) {
            try {
                System.out.println("Inside try block");
                int result = 10 / 2;
            } finally {
                System.out.println("Finally block executed");
            }
        }
    }

    Output:

    Inside try block
    Finally block executed

    Here, no exception occurs, but the finally block still executes.

    Case 2: Exception Occurs and is Caught

    class CatchException {
        public static void main(String[] args) {
            try {
                System.out.println("Inside try block");
                int result = 10 / 0;
            } catch (ArithmeticException e) {
                System.out.println("Caught ArithmeticException");
            } finally {
                System.out.println("Finally block executed");
            }
        }
    }

    Output:

    Inside try block
    Caught ArithmeticException
    Finally block executed

    Case 3: Exception Occurs but No Catch Block

    class NoCatchBlock {
        public static void main(String[] args) {
            try {
                System.out.println("Inside try block");
                int result = 10 / 0;
            } finally {
                System.out.println("Finally block executed");
            }
        }
    }

    Output:

    Inside try block
    Finally block executed
    Exception in thread "main" java.lang.ArithmeticException: / by zero

    Case 4: System.exit(0) in try Block

    class ExitExample {
        public static void main(String[] args) {
            try {
                System.out.println("Inside try block");
                System.exit(0);
            } finally {
                System.out.println("Finally block not executed");
            }
        }
    }

    Output:

    Inside try block
    Finalize Method

    The finalize method is called by the garbage collector just before an object is destroyed. It is meant for resource cleanup before an object is deleted.

    Example: Garbage Collector Calling finalize

    class GarbageCollectorExample {
        public static void main(String[] args) {
            GarbageCollectorExample obj = new GarbageCollectorExample();
            obj = null;
            System.gc();
            System.out.println("Main method ends");
        }
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Finalize method called");
        }
    }

    Output:

    Main method ends
    Finalize method called
  • Wrapper Classes

    Wrapper Classes in Java

    The first question that typically comes to mind is, “When we already have primitive data types, why do we need wrapper classes in Java?” The reason lies in the additional functionality provided by wrapper classes that primitive data types lack. These features primarily include useful methods like valueOf()parseInt()toString(), and more.

    A wrapper class “wraps” around a primitive data type, giving it an object representation. These classes are final and immutable. Two key concepts related to wrapper classes are autoboxing and unboxing.

    1. Autoboxing is the automatic conversion of a primitive value into an instance of its corresponding wrapper class (e.g., converting an int to an Integer). The Java compiler applies autoboxing when:

    • A primitive value is passed as an argument to a method that expects an object of the corresponding wrapper class.
    • A primitive value is assigned to a variable of the corresponding wrapper class.

    2. Unboxing is the automatic conversion of an instance of a wrapper class to its corresponding primitive value (e.g., converting an Integer to an int). The Java compiler applies unboxing when:

    • An object of a wrapper class is passed as an argument to a method that expects a primitive type.
    • An object of a wrapper class is assigned to a variable of the corresponding primitive type.
    Features of Wrapper Classes

    Some of the notable benefits of wrapper classes include:

    1. They allow conversion between primitive data types and objects. This is useful in cases where arguments passed to methods need to be modified, as primitives are passed by value.
    2. Java classes, such as those in the java.util package, deal with objects rather than primitive types, making wrapper classes essential.
    3. Data structures in the Collection Framework (e.g., ArrayList, Vector) store only objects, not primitives.
    4. Wrapper classes enable object creation for synchronization in multithreading.
    They provide numerous utility methods. For instance, a float value can be converted to its integer equivalent using the provided method.

    Example 1: Autoboxing in Java

    The following code demonstrates autoboxing, where primitive types are automatically converted to their corresponding wrapper classes:

    import java.util.*;
    
    class WrapperDemo {
    
        public static void main(String[] args) {
            int x = 10;
            double y = 7.25;
            long z = 12345;
    
            // Autoboxing: converting primitives to objects
            Integer intObj = x;
            Double doubleObj = y;
            Long longObj = z;
    
            // Output the wrapped values
            System.out.println(intObj);
            System.out.println(doubleObj);
            System.out.println(longObj);
        }
    }

    Output:

    10
    7.25
    12345

    Example 2: Wrapper Class Utility Methods

    Here’s another example that illustrates the utility methods provided by wrapper classes:

    import java.io.*;
    
    class WrapperUtility {
    
        public static void main(String[] args) {
    
            // Converting a float to int using a wrapper class method
            Float floatValue = Float.valueOf(28.97f);
            int convertedValue = floatValue.intValue();
    
            // Output the converted value
            System.out.println(convertedValue);
    
            // Converting a binary string to an integer
            Integer binaryValue = Integer.valueOf("1101", 2);
    
            // Output the converted integer from binary
            System.out.println(binaryValue);
        }
    }

    Output:

    28
    13

    Character Class in Java

    Java provides the Character class within the java.lang package as a wrapper for the primitive char data type. This class encapsulates a single char value and offers numerous useful methods to manipulate characters. The Character class also supports Autoboxing, where a primitive char is automatically converted to a Character object when necessary.

    Creating a Character Object:

    Character myChar = new Character('b');

    This statement creates a Character object containing the character 'b'. The Character class constructor takes a single argument of type char.

    Autoboxing allows Java to automatically convert a char to a Character object if a method expects an object. Conversely, unboxing converts the object back to a primitive char.

    /** This is a documentation comment */
    Methods of the Character Class

    Here are some important methods provided by the Character class:

    1. boolean isLetter(char ch)

    This method checks whether a given character is a letter (A-Z or a-z).

    Syntax:

    boolean isLetter(char ch)

    Example:

    public class Demo {
        public static void main(String[] args) {
            System.out.println(Character.isWhitespace(' '));
            System.out.println(Character.isWhitespace('\t'));
            System.out.println(Character.isWhitespace('X'));
        }
    }

    Output:

    true
    true
    false

    2. boolean isUpperCase(char ch)

    This method checks whether the given character is uppercase.

    Syntax:

    boolean isUpperCase(char ch)

    Example:

    public class Demo {
        public static void main(String[] args) {
            System.out.println(Character.isUpperCase('K'));
            System.out.println(Character.isUpperCase('k'));
        }
    }

    Output:

    true
    false

    3. char toUpperCase(char ch)

    This method converts a lowercase character to its uppercase equivalent.

    Syntax:

    char toUpperCase(char ch)

    Example:

    public class Demo {
        public static void main(String[] args) {
            System.out.println(Character.toUpperCase('m'));
            System.out.println(Character.toUpperCase('M'));
        }
    }

    Output:

    M
    M

    4. char toLowerCase(char ch)

    This method converts an uppercase character to its lowercase equivalent.

    Syntax:

    char toLowerCase(char ch)

    Examples:

    public class Demo {
        public static void main(String[] args) {
            System.out.println(Character.toLowerCase('N'));
            System.out.println(Character.toLowerCase('n'));
        }
    }

    Output:

    n
    n
    Escape Sequences

    Java allows the use of escape sequences to represent special characters in strings. Here are some common escape sequences:

    Escape SequenceDescription
    \tInserts a tab
    \bInserts a backspace
    \nInserts a newline
    \rInserts a carriage return
    \fInserts a formfeed
    \'Inserts a single quote
    \"Inserts a double quote
    \\Inserts a backslash

    Example:

    public class EscapeDemo {
        public static void main(String[] args) {
            System.out.println("He said, \"Java is fun!\"");
            System.out.println("Line1\nLine2");
        }
    }

    Output:

    He said, "Java is fun!"
    Line1
    Line2

    Java.Lang.Byte class in Java

    The Byte class in Java is a wrapper class for the primitive byte type, which provides methods for dealing with byte values, such as converting them to string representations and vice versa. A Byte object can hold a single byte value.

    Constructors of Byte Class

    There are two primary constructors used to initialize a Byte object:

    1. Byte(byte b)
    Initializes a Byte object with the given byte value.

    public Byte(byte b)

    Parameters:b: The byte value to initialize the Byte object with.

    2. Byte(String s)
    Initializes a Byte object using the byte value from the provided string representation. The string is parsed as a decimal value by default.

    Syntax:

    public Byte(String s) throws NumberFormatException

    Parameter:value: string representing a byte value.

    Fields in Byte Class
    • static int BYTES : The number of bytes used to represent a byte value in two’s complement binary form.
    • static byte MAX_VALUE : The maximum value a byte can have, which is 2⁷ – 1 (i.e., 127).
    • static byte MIN_VALUE : The minimum value a byte can have, which is -2⁷ (i.e., -128).
    • static int SIZE : The number of bits used to represent a byte value (8 bits).
    • static Class<Byte> TYPE : The Class instance representing the primitive type byte.
    Methods in Byte Class

    1. toString()
    Returns the string representation of the byte value.

    Syntax:

    public String toString(byte value)

    2. valueOf()
    Returns a Byte object initialized with the provided byte value.

    Syntax:

    public static Byte valueOf(byte value)

    3. valueOf(String value, int radix)
    Parses the string into a byte value based on the given radix and returns a Byte object.

    Syntax:

    public static Byte valueOf(String value, int radix) throws NumberFormatException

    4. parseByte()
    Converts a string into a primitive byte value, with or without a specified radix.

    Syntax:

    public static byte parseByte(String value, int radix) throws NumberFormatException

    5. decode()
    Decodes a string into a Byte object. The string can be in decimal, hexadecimal, or octal format.

    Syntax:

    public static Byte decode(String value) throws NumberFormatException

    6. byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()
    These methods return the respective primitive values corresponding to the Byte object.

    7. hashCode()
    Returns the hash code for the Byte object.

    8. equals()
    Compares two Byte objects for equality.

    9. compareTo()
    Compares two Byte objects numerically.

    10. compare()
    Compares two primitive byte values.

    Java Program to Illustrate Byte Class Methods

    public class ByteExample {
        public static void main(String[] args) {
    
            byte num = 42;
            String numStr = "36";
    
            // Constructing two Byte objects
            Byte byteObj1 = new Byte(num);
            Byte byteObj2 = new Byte(numStr);
    
            // toString()
            System.out.println("toString(num) = " + Byte.toString(num));
    
            // valueOf()
            Byte byteVal1 = Byte.valueOf(num);
            System.out.println("valueOf(num) = " + byteVal1);
    
            byteVal1 = Byte.valueOf(numStr);
            System.out.println("valueOf(numStr) = " + byteVal1);
    
            byteVal1 = Byte.valueOf(numStr, 8);
            System.out.println("valueOf(numStr, 8) = " + byteVal1);
    
            // parseByte()
            byte primitiveByte = Byte.parseByte(numStr);
            System.out.println("parseByte(numStr) = " + primitiveByte);
    
            primitiveByte = Byte.parseByte(numStr, 8);
            System.out.println("parseByte(numStr, 8) = " + primitiveByte);
    
            // decode()
            String decString = "50";
            String octString = "040";
            String hexString = "0x2A";
    
            Byte decodedByte = Byte.decode(decString);
            System.out.println("decode(50) = " + decodedByte);
    
            decodedByte = Byte.decode(octString);
            System.out.println("decode(040) = " + decodedByte);
    
            decodedByte = Byte.decode(hexString);
            System.out.println("decode(0x2A) = " + decodedByte);
    
            // Various primitive type conversions
            System.out.println("byteValue() = " + byteObj1.byteValue());
            System.out.println("shortValue() = " + byteObj1.shortValue());
            System.out.println("intValue() = " + byteObj1.intValue());
            System.out.println("longValue() = " + byteObj1.longValue());
            System.out.println("floatValue() = " + byteObj1.floatValue());
            System.out.println("doubleValue() = " + byteObj1.doubleValue());
    
            // hashCode()
            int hash = byteObj1.hashCode();
            System.out.println("hashCode() = " + hash);
    
            // equals()
            boolean isEqual = byteObj1.equals(byteObj2);
            System.out.println("equals() = " + isEqual);
    
            // compare()
            int compareResult = Byte.compare(byteObj1, byteObj2);
            System.out.println("compare() = " + compareResult);
    
            // compareTo()
            int compareToResult = byteObj1.compareTo(byteObj2);
            System.out.println("compareTo() = " + compareToResult);
        }
    }

    Output:

    toString(num) = 42
    valueOf(num) = 42
    valueOf(numStr) = 36
    valueOf(numStr, 8) = 30
    parseByte(numStr) = 36
    parseByte(numStr, 8) = 30
    decode(50) = 50
    decode(040) = 32
    decode(0x2A) = 42
    byteValue() = 42
    shortValue() = 42
    intValue() = 42
    longValue() = 42
    floatValue() = 42.0
    doubleValue() = 42.0
    hashCode() = 42
    equals() = false
    compare() = 6
    compareTo() = 6

    Java.Lang.Short class in Java

    Short Class in Java

    The Short class is a wrapper for the primitive short data type. It provides various methods for handling short values, such as converting them to and from string representations. An instance of the Short class can store a single short value.

    Constructors in the Short Class

    The Short class has two main constructors:

    1. Short(short value)
    This constructor creates a Short object initialized with the provided short value.
    Syntax:

    public Short(short value)

    Parameters:value: The short value used for initialization.

    2. Short(String s)
    This constructor creates a Short object from a string representing a short value, with the default base (radix) of 10.
    Syntax:

    public Short(String s) throws NumberFormatException

    Parameters:s: A string representing the short value.

    Throws:NumberFormatException if the string does not represent a valid short value.

    Methods in the Short Class

    1. toString()
    This method converts a short value to its string representation.
    Syntax:

    public static String toString(short value)

    2. valueOf()
    This method returns a Short object initialized with the given short value.
    Syntax:

    public static Short valueOf(short value)

    3. parseShort()
    This method parses the string argument to return a primitive short value.
    Syntax:

    public static short parseShort(String s, int radix) throws NumberFormatException

    4. decode()
    This method decodes a string to return a Short object. It supports decimal, hexadecimal, and octal representations.
    Syntax:

    public static Short decode(String s) throws NumberFormatException

    5. byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()
    These methods return the corresponding primitive value from the Short object.

    6. hashCode()
    Returns the hash code for this Short object.
    Syntax:

    public int hashCode()

    7. equals()
    Checks if two Short objects are equal.
    Syntax:

    public boolean equals(Object obj)

    8. compareTo()
    Compares two Short objects.
    Syntax:

    public int compareTo(Short anotherShort)

    9. compare()
    Compares two primitive short values.
    Syntax:

    public static int compare(short x, short y)

    10. reverseBytes()
    This method reverses the order of the bytes in the given short value.
    Syntax:

    public static short reverseBytes(short value)

    Example:

    public class ShortExample {
        public static void main(String[] args) {
            // Short value and string representation
            short sValue = 90;
            String sString = "50";
    
            // Creating two Short objects
            Short first = new Short(sValue);
            Short second = new Short(sString);
    
            // toString()
            System.out.println("toString(sValue) = " + Short.toString(sValue));
    
            // valueOf()
            Short obj1 = Short.valueOf(sValue);
            System.out.println("valueOf(sValue) = " + obj1);
            obj1 = Short.valueOf(sString);
            System.out.println("valueOf(sString) = " + obj1);
            obj1 = Short.valueOf(sString, 7);
            System.out.println("valueOf(sString, 7) = " + obj1);
    
            // parseShort()
            short parsedValue = Short.parseShort(sString);
            System.out.println("parseShort(sString) = " + parsedValue);
            parsedValue = Short.parseShort(sString, 7);
            System.out.println("parseShort(sString, 7) = " + parsedValue);
    
            // decode()
            String decimalString = "40";
            String octalString = "006";
            String hexString = "0x10";
    
            Short decoded = Short.decode(decimalString);
            System.out.println("decode(40) = " + decoded);
            decoded = Short.decode(octalString);
            System.out.println("decode(006) = " + decoded);
            decoded = Short.decode(hexString);
            System.out.println("decode(0x10) = " + decoded);
    
            // Primitive value methods
            System.out.println("byteValue(first) = " + first.byteValue());
            System.out.println("shortValue(first) = " + first.shortValue());
            System.out.println("intValue(first) = " + first.intValue());
            System.out.println("longValue(first) = " + first.longValue());
            System.out.println("floatValue(first) = " + first.floatValue());
            System.out.println("doubleValue(first) = " + first.doubleValue());
    
            // Hash code
            int hash = first.hashCode();
            System.out.println("hashCode(first) = " + hash);
    
            // Equality check
            boolean isEqual = first.equals(second);
            System.out.println("first.equals(second) = " + isEqual);
    
            // Comparison
            int comparison = Short.compare(first, second);
            System.out.println("compare(first, second) = " + comparison);
    
            int compareTo = first.compareTo(second);
            System.out.println("first.compareTo(second) = " + compareTo);
    
            // Reverse bytes
            short toReverse = 60;
            System.out.println("Short.reverseBytes(toReverse) = " + Short.reverseBytes(toReverse));
        }
    }

    Output:

    toString(sValue) = 90
    valueOf(sValue) = 90
    valueOf(sString) = 50
    valueOf(sString, 7) = 35
    parseShort(sString) = 50
    parseShort(sString, 7) = 35
    decode(40) = 40
    decode(006) = 6
    decode(0x10) = 16
    byteValue(first) = 90
    shortValue(first) = 90
    intValue(first) = 90
    longValue(first) = 90
    floatValue(first) = 90.0
    doubleValue(first) = 90.0
    hashCode(first) = 90
    first.equals(second) = false
    compare(first, second) = 40
    first.compareTo(second) = 40
    Short.reverseBytes(toReverse) = 15360

    Java.Lang.Long class in Java

    Long Class in Java

    The Long class is a wrapper for the primitive type long that provides methods to handle long values more effectively. It can convert a long to a String representation and vice versa. A Long object holds a single long value, and there are two primary constructors to initialize this object:

    Long(long b): Initializes a Long object with the specified long value.

    public Long(long b)

    Parameter:s: String representation of the long value.

    Key Methods

    1. toString(): Converts a long value to a string representation.

    public String toString(long b)

    2. toHexString(): Converts a long value to its hexadecimal string representation.

    public String toHexString(long b)

    3. toOctalString(): Converts a long value to its octal string representation.

    public String toOctalString(long b)

    4. toBinaryString(): Converts a long value to its binary string representation.

    public String toBinaryString(long b)

    5. valueOf(): Converts a long or string to a Long object.

    public static Long valueOf(long b)
    public static Long valueOf(String val, long radix) throws NumberFormatException
    public static Long valueOf(String s) throws NumberFormatException

    6. parseLong(): Converts a string to a primitive long value.

    public static long parseLong(String val, int radix) throws NumberFormatException
    public static long parseLong(String val) throws NumberFormatException

    7. getLong(): Retrieves the Long object associated with a system property or returns null if it doesn’t exist. An overloaded method allows for a default value.

    public static Long getLong(String prop)
    public static Long getLong(String prop, long val)

    8. decode(): Decodes a string into a Long object (handles decimal, hex, and octal formats).

    public static Long decode(String s) throws NumberFormatException

    9. rotateLeft(): Rotates the bits of the given long value to the left by the specified distance.

    public static long rotateLeft(long val, int dist)

    Example:

    public class LongExample {
        public static void main(String[] args) {
            long num = 30;
            String strNum = "25";
    
            // Creating two Long objects
            Long obj1 = new Long(num);
            Long obj2 = new Long(strNum);
    
            // String conversion
            System.out.println("String representation: " + Long.toString(num));
    
            // Hexadecimal, Octal, and Binary conversion
            System.out.println("Hexadecimal: " + Long.toHexString(num));
            System.out.println("Octal: " + Long.toOctalString(num));
            System.out.println("Binary: " + Long.toBinaryString(num));
    
            // Using valueOf()
            Long val1 = Long.valueOf(num);
            System.out.println("valueOf(num): " + val1);
            Long val2 = Long.valueOf(strNum);
            System.out.println("valueOf(strNum): " + val2);
            Long val3 = Long.valueOf(strNum, 8);
            System.out.println("valueOf(strNum, 8): " + val3);
    
            // Using parseLong()
            long parsedValue1 = Long.parseLong(strNum);
            System.out.println("parseLong(strNum): " + parsedValue1);
            long parsedValue2 = Long.parseLong(strNum, 8);
            System.out.println("parseLong(strNum, 8): " + parsedValue2);
    
            // getLong() method examples
            Long propValue = Long.getLong("java.specification.version");
            System.out.println("System property value: " + propValue);
            System.out.println("Default value: " + Long.getLong("nonexistent", 5));
    
            // Decode
            System.out.println("Decoded (hex): " + Long.decode("0x1f"));
            System.out.println("Decoded (octal): " + Long.decode("007"));
    
            // Bit rotation
            System.out.println("rotateLeft(30, 2): " + Long.rotateLeft(num, 2));
            System.out.println("rotateRight(30, 2): " + Long.rotateRight(num, 2));
        }
    }

    Output:

    String representation: 30
    Hexadecimal: 1e
    Octal: 36
    Binary: 11110
    valueOf(num): 30
    valueOf(strNum): 25
    valueOf(strNum, 8): 21
    parseLong(strNum): 25
    parseLong(strNum, 8): 21
    System property value: 19
    Default value: 5
    Decoded (hex): 31
    Decoded (octal): 7
    rotateLeft(30, 2): 120
    rotateRight(30, 2): 7

    Java.Lang.Float class in Java

    The Float class in Java is a wrapper class for the primitive type float, and it provides several methods to work with float values, such as converting them to string representations and vice-versa. A Float object can hold a single float value. There are primarily two constructors to initialize a Float object:

    Float(float f): This creates a Float object initialized with the specified float value.

    Syntax:

    public Float(float f)

    Parameters:s: The string representation of a float value.

    Throws:NumberFormatException: If the provided string cannot be parsed as a float.

    Methods of the Float Class

    1. toString(): Returns the string representation of the float value.

    Synta

    public String toString(float f)

    Parameters: f: The float value for which the string representation is required.

    2. valueOf():Returns a Float object initialized with the given float value.

    Syntax:

    public static Float valueOf(String s) throws NumberFormatException

    3. parseFloat():Parses the string as a float and returns the primitive float value.

    Syntax:

    public static float parseFloat(String s) throws NumberFormatException

    4. byteValue():Returns the byte value of the Float object.

    Syntax:

    public byte byteValue()

    5. shortValue(): Returns the short value of the Float object.

    Syntax:

    public short shortValue()

    6. intValue(): Returns the int value of the Float object.

    Syntax:

    public int intValue()

    7. doubleValue():Returns the double value of the Float object.

    Syntax:

    Error: variable 'a' is already defined

    8. longValue(): Returns the long value of the Float object.

    Syntax:

    public double doubleValue()

    9. floatValue():Returns the float value of the Float object.

    Syntax:

    public float floatValue()

    10. floatValue(): Returns the float value of the Float object.

    Syntax:

    public float floatValue()

    11. isNaN(): Checks whether the float value is NaN (Not-a-Number).

    Syntax:

    public boolean isNaN()

    12. isInfinite(): Checks whether the float value is infinite.

    Syntax:

    public boolean isInfinite()

    13. toHexString():Returns the hexadecimal representation of the given float value.

    Syntax:

    public static String toHexString(float val)
  • Memory Allocation

    Java Memory Management

    Java handles memory management automatically, with the help of the Java Virtual Machine (JVM) and the Garbage Collector. But it’s essential for a programmer to understand how memory management works in Java, as it aids in writing efficient code and debugging potential memory issues. Knowing how to manage memory can also help improve performance and prevent memory leaks.

    Why Learn Java Memory Management?

    Even though Java automates memory management through the garbage collector, the programmer’s role isn’t eliminated. While developers don’t need to explicitly destroy objects like in languages such as C/C++, they must understand how Java memory management works. Mismanaging memory or not understanding what is managed by the JVM and what isn’t can lead to issues, such as objects not being eligible for garbage collection. In particular, understanding memory management enables writing high-performance programs that avoid memory crashes and helps debug memory issues effectively.

    Introduction to Java Memory Management

    Memory is a vital and limited resource in any programming language. Proper memory management ensures there are no memory leaks, improving the efficiency of programs. Unlike languages like C, where the programmer directly manages memory, Java delegates memory management to the JVM, which handles allocation and deallocation of memory. The Garbage Collector plays a significant role in managing memory automatically in Java.

    Key Concepts in Java Memory Management

    1. JVM Memory Structure
    2. Garbage Collection Process

    Java Memory Structure

    The JVM manages different runtime data areas, some of which are created when the JVM starts and some by threads used in a program. These memory areas have distinct purposes and are destroyed when the JVM or the respective threads exit.

    Key Components of JVM Memory:

    1. Heap : The heap is a shared runtime data area used for storing objects and array instances. It is created when the JVM starts. The size of the heap can be fixed or dynamic, depending on system configuration, and can be controlled by the programmer. For instance, when using the new keyword, the object is allocated space in the heap, while its reference is stored in the stack.

    Example:

    List<String> list = new ArrayList<>();

    In this case, the ArrayList object is created in the heap, and the reference list is stored in the stack.

    Output:

    Memory allocated for ArrayList in the heap.

    2. Method Area : The method area is a logical part of the heap and holds class structures, method data, and field data. It stores runtime constant pool information as well. Although it’s part of the heap, garbage collection in the method area is not guaranteed.

    3. JVM Stacks : Each thread in a Java program has its own stack, which stores data like local variables, method calls, and return values. The stack is created when a thread is instantiated and destroyed when the thread finishes.

    Example:

    public static void main(String[] args) {
        int x = 5;
        int y = calculate(x);
    }
    
    static int calculate(int val) {
        return val * 2;
    }

    In this example, the local variables x and y are stored in the stack. The method call to calculate is also stored on the stack.

    1. Native Method Stacks: These stacks support native methods (non-Java methods). Like JVM stacks, they are created for each thread and can be either dynamic or fixed.
    2. Program Counter (PC) Register : Each thread in the JVM has a program counter register that tracks the current method instruction being executed. For native methods, the value is undefined.

    How the Garbage Collector Works

    Java’s garbage collection is an automatic process that identifies and reclaims memory from objects that are no longer in use. It frees the programmer from manually managing memory deallocation. However, the garbage collection process can be costly, as it pauses other threads during execution. To improve performance, Java employs various garbage collection algorithms, a process referred to as “Garbage Collector Tuning.”

    Garbage Collection Algorithms:

    1. Generational Garbage Collection:
    Java uses a generational garbage collection approach, where objects are classified based on their lifespan (age). Objects that survive multiple garbage collection cycles are promoted to an older generation, while newly created objects are placed in a younger generation. This improves efficiency, as older objects are collected less frequently.

    Garbage Collection Example:

    public class GarbageCollectionDemo {
        public static void main(String[] args) {
            GarbageCollectionDemo demo = new GarbageCollectionDemo();
            demo = null; // Eligible for garbage collection
            System.gc(); // Requesting garbage collection
            System.out.println("Garbage collection triggered.");
        }
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Garbage collected!");
        }
    }

    Output:

    Garbage collection triggered.
    Garbage collected!

    Here, the object demo is made eligible for garbage collection by setting it to null. The System.gc() method requests the JVM to run the garbage collector, although it’s not guaranteed to happen immediately.

    Java Object Allocation on Heap

    In Java, all objects are dynamically allocated on the heap. This differs from languages like C++, where objects can be allocated on either the stack or the heap. When you use the new keyword in Java, the object is allocated on the heap, whereas in C++, objects can also be stack-allocated, unless they are declared globally or statically.

    When you declare a variable of a class type in Java, memory for the object is not allocated immediately. Only a reference is created. To allocate memory to an object, the new keyword must be used. This ensures that objects are always allocated on the heap.

    Creating a String Object in Java

    There are two ways to create a string in Java:

    1. By String Literal
    2. By using the new Keyword

    1. String Literal :  This is the most common way to create a string in Java, using double-quotes.

    Example:

    System.out.println("Hello");  // valid
    system.out.println("Hello");  // invalid

    In this case, every time a string literal is created, the JVM checks whether the string already exists in the string constant pool. If it does, a reference to the pooled instance is returned. If it doesn’t, a new string instance is created in the pool. Therefore, only one object will be created for both str1 and str2 if they have the same value.

    The JVM is not obligated to create new memory if the string already exists in the pool.

    2. Using the new Keyword : You can also create strings using the new keyword.

    Example:

    String str1 = new String("Hello");
    String str2 = new String("Hello");

    Here, both str1 and str2 are different objects. Even though the string content is the same, the JVM creates separate memory locations in the heap for each object when using the new keyword. The JVM will not check if the string already exists in memory; it always creates new memory for each object.

    The JVM is forced to allocate new memory for each string object created using new.

    Uninitialized Object Example

    If you attempt to use a reference to an object without initializing it, Java will throw a compilation error, as the object does not have memory allocated to it.

    Example with Error:

    class Demo {
        void display() {
            System.out.println("Demo::display() called");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Demo d;  // No memory allocated yet
            d.display();  // Error: d is not initialized
        }
    }

    Output:

    Error: variable d might not have been initialized

    How many types of memory areas are allocated by JVM?

    The Java Virtual Machine (JVM) is an abstract machine, essentially a software program that takes Java bytecode (compiled Java code) and converts it into machine-level code that the underlying system can understand, one instruction at a time.

    JVM acts as the runtime engine for Java applications and is responsible for invoking the main() method in Java programs. It is a core part of the Java Runtime Environment (JRE), which provides the necessary libraries and environment for Java code execution.

    Functions of the JVM

    JVM performs several key functions:

    1. Loading of code: It loads the bytecode into memory.
    2. Verification of code: It checks the bytecode for security issues or invalid operations.
    3. Execution of code: It executes the bytecode.
    4. Runtime environment: Provides a runtime environment for Java programs.

    ClassLoader

    ClassLoader is a crucial subsystem of the JVM that loads .class files into memory. It is responsible for:

    1. Loading: Loading the class into memory.
    2. Linking: Resolving symbolic references and ensuring class dependencies are loaded.
    Initialization: Preparing the class for use, initializing variables, etc.

    Types of Memory Areas Allocated by JVM

    JVM memory is divided into several parts that perform different functions. These are:

    1. Class (Method) Area : The Class Method Area is a memory region that stores class-related data, such as:

    • Class code
    • Static variables
    • Runtime constants
    • Method code (functions within classes)

    This area holds data related to class-level information, including constructors and field data.

    2. Heap : The Heap is where objects are dynamically created during the execution of a program. This memory area stores objects, including arrays (since arrays are objects in Java). The heap is where memory is allocated at runtime for class instances.

    3. Stack : Each thread in a Java program has its own stack, which is created when the thread starts. The stack holds data like:

    • Method call frames
    • Local variables
    • Partial results

    A new frame is created every time a method is called, and the frame is destroyed once the method call is completed.

    4. Program Counter (PC) Register

    Each thread has a Program Counter (PC) register. For non-native methods, it stores the address of the next instruction to execute. In native methods, the PC value is undefined. It also holds return addresses or native pointers in certain cases.

    5. Native Method Stack

    The Native Method Stack is used by threads that execute native (non-Java) code. These stacks, sometimes referred to as C stacks, store information about native methods written in other programming languages like C/C++. Each thread has its own native method stack, and it can be either fixed or dynamic in size.

    JVM Example: Code Execution

    Here’s an example demonstrating how the JVM manages memory:

    class Demo {
        static int x = 10; // Stored in the heap memory
        int y = 20; // Stored in the heap memory
    
        void display() {
            System.out.println("x: " + x + ", y: " + y);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Demo obj = new Demo();  // obj created in heap
            obj.display();  // Call method using obj
        }
    }

    Output:

    x: 10, y: 20

    Garbage Collection in Java

    Garbage Collection (GC) in Java is the process through which Java programs handle automatic memory management. Java programs are compiled into bytecode that runs on the Java Virtual Machine (JVM). As the program runs, objects are created on the heap memory, which is dedicated to the program’s use. Over time, some of these objects are no longer needed. The garbage collector identifies these unused objects and removes them, freeing up memory space.

    What is Garbage Collection?

    In languages like C and C++, developers are responsible for managing both the creation and destruction of objects. However, developers often neglect the destruction of objects that are no longer required, which can lead to memory shortages and eventual program crashes, resulting in OutOfMemoryErrors.

    In contrast, Java handles memory management automatically. The garbage collector in Java’s JVM frees up heap memory by destroying unreachable objects. This background process is the best example of a daemon thread, which continuously runs to manage memory.

    How Does Garbage Collection Work in Java?

    Java’s garbage collection is fully automatic. It inspects heap memory, identifying objects that are still in use and those that are not. In-use objects are referenced by the program, while unused objects are no longer referenced by any part of the program and can have their memory reclaimed.

    The garbage collector, part of the JVM, handles this process without the programmer needing to explicitly mark objects for deletion.

    Types of Garbage Collection in Java

    There are typically two types of garbage collection activities:

    1. Minor (Incremental) GC: This occurs when objects that are no longer referenced in the young generation of the heap are removed.
    2. Major (Full) GC: This occurs when objects that have survived multiple minor collections are promoted to the old generation of the heap and are later removed when they become unreachable.

    Key Concepts Related to Garbage Collection

    1. Unreachable Objects: An object is considered unreachable if no references to it exist within the program. Objects that are part of an “island of isolation” are also considered unreachable. For example:

    Integer i = new Integer(5);
    // 'i' references the new Integer object
    i = null;
    // The Integer object is now unreachable

    2. Eligibility for Garbage Collection: An object becomes eligible for garbage collection when it becomes unreachable, such as after nullifying the reference:

    Integer i = new Integer(5);
    i = null;  // Now, the object is eligible for GC.
    How to Make an Object Eligible for Garbage Collection

    While Java automatically handles garbage collection, programmers can help by making objects unreachable when they are no longer needed. Here are four ways to make an object eligible for GC:

    1. Nullifying the reference variable
    2. Re-assigning the reference variable
    3. Using objects created inside a method
    4. Islands of Isolation (when a group of objects reference each other but are no longer referenced elsewhere)

    Requesting JVM to Run the Garbage Collector

    Garbage collection doesn’t happen immediately when an object becomes eligible. The JVM will run the garbage collector at its discretion. However, we can request the JVM to perform garbage collection using these methods:

    1. System.gc(): Invokes the garbage collector explicitly.

    2. Runtime.getRuntime().gc(): The Runtime class allows an interface with the JVM, and calling its gc() method requests garbage collection.

    Finalization

    Before an object is destroyed, the garbage collector calls its finalize() method to allow cleanup activities (e.g., closing database connections). This method is defined in the Object class and can be overridden.

    protected void finalize() throws Throwable {
        // Cleanup code here
    }
    Important points about finalize():
    • The finalize() method is never called more than once for an object.
    • If the finalize() method throws an uncaught exception, it is ignored, and finalization terminates.
    • After the finalize() method is invoked, the garbage collector reclaims the object.
    Advantages of Garbage Collection
    • Memory efficiency: Garbage collection helps reclaim memory by removing unreferenced objects from heap memory.
    • Automation: Garbage collection happens automatically as part of the JVM, removing the need for manual intervention.

    Example:

    class Employee {
        private int ID;
        private String name;
        private int age;
        private static int nextId = 1;  // Common across all objects
    
        public Employee(String name, int age) {
            this.name = name;
            this.age = age;
            this.ID = nextId++;
        }
    
        public void show() {
            System.out.println("ID: " + ID + "\nName: " + name + "\nAge: " + age);
        }
    
        public void showNextId() {
            System.out.println("Next employee ID: " + nextId);
        }
    }
    
    public class Company {
        public static void main(String[] args) {
            Employee e1 = new Employee("Employee1", 30);
            Employee e2 = new Employee("Employee2", 25);
            Employee e3 = new Employee("Employee3", 40);
    
            e1.show();
            e2.show();
            e3.show();
            e1.showNextId();
            e2.showNextId();
            e3.showNextId();
    
            {  // Sub-block for interns
                Employee intern1 = new Employee("Intern1", 22);
                Employee intern2 = new Employee("Intern2", 24);
                intern1.show();
                intern2.show();
                intern1.showNextId();
                intern2.showNextId();
            }
    
            // X and Y are out of scope, but nextId has incremented
            e1.showNextId();  // Expected 4, but it will give 6 as output
        }
    }

    Output:

    ID: 1
    Name: Employee1
    Age: 30
    ID: 2
    Name: Employee2
    Age: 25
    ID: 3
    Name: Employee3
    Age: 40
    Next employee ID: 4
    Next employee ID: 4
    Next employee ID: 4
    ID: 4
    Name: Intern1
    Age: 22
    ID: 5
    Name: Intern2
    Age: 24
    Next employee ID: 6
    Next employee ID: 6
    Next employee ID: 6

    Types of JVM Garbage Collectors in Java with implementation details

    Garbage Collection: Garbage Collection (GC) is a key feature in Java that enables automatic memory management. GC is responsible for reclaiming memory used by objects that are no longer needed, making that memory available for future use. To achieve this, the garbage collector monitors objects in memory, identifies those that are still referenced, and deallocates the memory for objects that are no longer in use. One common approach used by garbage collectors is the Mark and Sweep algorithm, which marks objects that are still reachable and then sweeps away the unmarked ones to free up memory.

    Types of Garbage Collection in Java

    The Java Virtual Machine (JVM) provides several garbage collection strategies, each affecting the application’s throughput and the pause time experienced during collection. Throughput measures how fast the application runs, while pause time indicates the delay introduced during garbage collection.

    1. Serial Garbage Collector : The Serial Garbage Collector is the simplest form of GC, using a single thread to perform garbage collection. When this collector is in use, it stops all application threads during the collection process, known as stop-the-world behavior. Since it uses only one thread, it is not ideal for multi-threaded applications or environments where responsiveness is crucial. As a result, while it reduces complexity, it increases application pause time, negatively impacting throughput. This collector is suitable for smaller applications or single-threaded systems.

    Usage:
    To use the Serial Garbage Collector explicitly, execute your application with the following JVM argument:

    java -XX:+UseSerialGC -jar MyApplication.jar

    2. Parallel Garbage Collector : The Parallel Garbage Collector, also known as the Throughput Collector, is the default collector in Java 8. It improves upon the Serial Collector by using multiple threads to perform garbage collection, allowing for better throughput at the expense of longer pause times. Like the Serial Collector, it stops all application threads during the garbage collection process. However, it provides control over the number of threads the collector uses and allows you to specify maximum pause times.

    Usage:
    To run the Parallel Garbage Collector with a specified number of threads:

    java -XX:+UseParallelGC -XX:ParallelGCThreads=<num_of_threads> -jar MyApplication.jar

    To limit the maximum pause time for the GC, you can set the following parameter:

    java -XX:+UseParallelGC -XX:MaxGCPauseMillis=<max_pause_ms> -jar MyApplication.jar

    3. CMS Garbage Collector (Concurrent Mark-Sweep) : The Concurrent Mark-Sweep (CMS) Garbage Collector attempts to minimize application pauses by performing most of its work concurrently with the application. It scans memory for unreferenced objects and removes them without freezing the entire application, except in two specific scenarios:

    • When there are changes in heap memory during the garbage collection process.
    • When marking referenced objects in the old generation space.

    CMS typically uses more CPU than the Parallel Collector to achieve better application responsiveness. It is ideal for applications that can afford to allocate additional CPU resources for lower pause times. To enable the CMS Garbage Collector, use the following command:

    Usage:

    java -XX:+UseConcMarkSweepGC -jar MyApplication.jar

    4. G1 Garbage Collector (Garbage-First): Introduced in Java 7 and made the default in Java 9, the G1 Garbage Collector was designed for applications that require large heap sizes (greater than 4GB). Instead of treating the heap as a monolithic memory block, G1 divides it into equal-sized regions. During garbage collection, G1 focuses on the regions with the most garbage, collecting them first, hence the name Garbage-First. Additionally, G1 compacts memory during garbage collection, reducing fragmentation and enhancing performance. This garbage collector offers significant performance benefits for larger applications, especially those running on modern JVMs.

    Usage:

    If you’re using a Java version earlier than 9 and want to enable the G1 Garbage Collector, specify the following JVM argument:

    java -XX:+UseG1GC -jar MyApplication.jar

    5. Example with G1 Garbage Collector: Let’s update the example to demonstrate the use of the G1 Garbage Collector. In this scenario, we’ll manage memory for a simple Employee class.

    class Employee {
        private int id;
        private String name;
        private int age;
        private static int nextId = 1;
    
        public Employee(String name, int age) {
            this.name = name;
            this.age = age;
            this.id = nextId++;
        }
    
        public void display() {
            System.out.println("ID: " + id + "\nName: " + name + "\nAge: " + age);
        }
    
        public void displayNextId() {
            System.out.println("Next employee ID will be: " + nextId);
        }
    
        @Override
        protected void finalize() throws Throwable {
            --nextId;
            System.out.println("Finalize called for employee ID: " + id);
        }
    }
    
    public class TestEmployee {
        public static void main(String[] args) {
            Employee emp1 = new Employee("Alice", 30);
            Employee emp2 = new Employee("Bob", 25);
            Employee emp3 = new Employee("Charlie", 35);
    
            emp1.display();
            emp2.display();
            emp3.display();
    
            emp1.displayNextId();
            emp2.displayNextId();
            emp3.displayNextId();
    
            {
                // Temporary employees
                Employee tempEmp1 = new Employee("David", 28);
                Employee tempEmp2 = new Employee("Eva", 22);
    
                tempEmp1.display();
                tempEmp2.display();
    
                tempEmp1.displayNextId();
                tempEmp2.displayNextId();
    
                // Making temp employees eligible for GC
                tempEmp1 = null;
                tempEmp2 = null;
    
                // Requesting garbage collection
                System.gc();
            }
    
            emp1.displayNextId();  // After GC, nextId should be updated correctly
        }
    }

    Output:

    ID: 1
    Name: Alice
    Age: 30
    ID: 2
    Name: Bob
    Age: 25
    ID: 3
    Name: Charlie
    Age: 35
    Next employee ID will be: 4
    Next employee ID will be: 4
    Next employee ID will be: 4
    ID: 4
    Name: David
    Age: 28
    ID: 5
    Name: Eva
    Age: 22
    Next employee ID will be: 6
    Next employee ID will be: 6
    Finalize called for employee ID: 4
    Finalize called for employee ID: 5
    Next employee ID will be: 4

    Memory leaks in Java

    In C, programmers have full control over the allocation and deallocation of dynamically created objects. If a programmer neglects to free memory for unused objects, this results in memory leaks.

    In Java, automatic garbage collection helps to manage memory, but there can still be scenarios where objects remain uncollected because they are still referenced. If an application creates a large number of objects that are no longer in use but are still referenced, the garbage collector cannot reclaim their memory. These unnecessary objects are referred to as memory leaks. If the memory allocated exceeds the available limit, the program may terminate with an OutOfMemoryError. Therefore, it is crucial to ensure that objects no longer needed are made eligible for garbage collection. Tools can also help in detecting and managing memory leaks, such as:

    • HP OVO
    • HP JMeter
    • JProbe
    • IBM Tivoli

    Example of a Memory Leak in Java

    import java.util.ArrayList;
    
    public class MemoryLeakExample {
        public static void main(String[] args) {
            ArrayList<Object> list1 = new ArrayList<>(1000000);
            ArrayList<Object> list2 = new ArrayList<>(100000000);
            ArrayList<Object> list3 = new ArrayList<>(1000000);
            System.out.println("Memory Leak Example");
        }
    }

    Output:

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

    Java Virtual Machine (JVM) Stack Area

    In Java, when a new thread is created, the JVM assigns a separate stack for it. The memory for this stack does not need to be contiguous. The JVM performs two primary operations on this stack: it pushes and pops frames, and this stack can be referred to as the runtime stack. For each thread, every method invocation is stored in this runtime stack, which includes parameters, local variables, intermediate results, and other related data. After the method completes execution, the respective entry is removed from the stack. When all method calls finish, the stack becomes empty, and the JVM removes the empty stack just before terminating the thread. Each stack’s data is thread-specific, ensuring that the data within a thread’s stack is not accessible to other threads. Hence, local data in this stack is considered thread-safe. Each entry in the stack is known as a Stack Frame or Activation Record.

    Stack Frame Structure

    The stack frame consists of three key parts:

    1. Local Variable Array (LVA)
    2. Operand Stack (OS)
    3. Frame Data (FD)

    When the JVM invokes a Java method, it first checks the method’s class data to determine the required size of the local variable array and operand stack, measured in words. It then creates a stack frame of the appropriate size and pushes it onto the Java stack.

    1. Local Variable Array (LVA)The local variable array is organized as a zero-based array of slots where each slot stores a 4-byte word.

    • It stores all parameters and local variables of a method.
    • Values of types intfloat, and object references each occupy 1 slot (4 bytes).
    • double and long values occupy 2 consecutive slots (8 bytes total).
    • byteshort, and char values are converted to int before being stored.
    • Most JVM implementations allocate 1 slot for boolean values.

    The parameters of a method are placed into the local variable array in the order they are declared. For instance, consider a method in the Example class:

    class Example {
        public void bike(int i, long l, float f, double d, Object o, byte b) {
            // Method body
        }
    }

    The local variable array for the bike() method would store each parameter in order.

    2. Operand Stack (OS): The JVM uses the operand stack as a workspace for storing intermediate results from computations.

    • It is also structured as an array of words, similar to the local variable array, but the operand stack is accessed through specific instructions that push and pop values.
    • Certain instructions push values onto the operand stack, others perform operations on them, and some instructions pop the results back into the local variable array.

    Example of Operand Stack Usage:

    The following assembly instructions illustrate how the JVM might subtract two integers stored in local variables and store the result in another local variable:

    iload_0    // Push the value of local variable 0 to the operand stack
    iload_1    // Push the value of local variable 1 to the operand stack
    isub       // Subtract the two values on the operand stack
    istore_2   // Pop the result and store it in local variable 2

    3. Frame Data (FD): Frame data contains symbolic references and data related to method returns.

    • It includes a reference to the constant pool and handles normal method returns.
    • Additionally, it stores references to the exception table, which helps locate the correct catch block in case an exception is thrown during execution.

    In summary, the stack frame in Java is structured to efficiently manage method calls, local variables, intermediate operations, and exception handling, providing a safe and organized environment for method execution within each thread.