Amol Pawar

Thread Constructors in java

Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency: Java Multithreading A Comprehensive Guide Part 2

Threads are the lifeblood of concurrent programming in Java, enabling multiple tasks to execute seemingly simultaneously, enhancing the performance of applications by utilizing the available processing power efficiently. Understanding thread constructors and methods is crucial for effectively creating, managing, and synchronizing threads in your applications. In this blog post, we’ll delve into the various constructors and methods provided by the Thread class in Java. Specially three fundamental methods: yield(), join(), and sleep(). Each of these methods serves a specific purpose in controlling thread execution, offering developers flexibility and control over concurrent processes.

Thread Constructors

Java
Thread t = new Thread();
Thread t = new Thread(Runnable r);
Thread t = new Thread(String name);
Thread t = new Thread(Runnable r, String name);
Thread t = new Thread(ThreadGroup g, String name);
Thread t = new Thread(ThreadGroup g, Runnable r);
Thread t = new Thread(ThreadGroup g, Runnable r, String name);
Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);

let’s break down each constructor of the Thread class:

  1. Thread t = new Thread();: This constructor creates a new thread object t but does not specify the task (runnable) for the thread to execute. In this case, the thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  2. Thread t = new Thread(Runnable r);: This constructor creates a new thread object t and associates it with the specified Runnable object r. When t.start() is called, the run() method of the Runnable object r will be executed by the new thread.
  3. Thread t = new Thread(String name);: This constructor creates a new thread object t with the specified name. The thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  4. Thread t = new Thread(Runnable r, String name);: This constructor creates a new thread object t associated with the specified Runnable object r and with the specified name.
  5. Thread t = new Thread(ThreadGroup g, String name);: This constructor creates a new thread object t within the specified ThreadGroup g and with the specified name.
  6. Thread t = new Thread(ThreadGroup g, Runnable r);: This constructor creates a new thread object t within the specified ThreadGroup g and associates it with the specified Runnable object r.
  7. Thread t = new Thread(ThreadGroup g, Runnable r, String name);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, and gives it the specified name.
  8. Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, gives it the specified name, and specifies the size of the thread’s stack.

Getting and Setting the Name of a Thread

Every thread in Java has a name, which may be a default name generated by the JVM or a customized name provided by the programmer. We can get and set the name of a thread using the following two methods of the Thread class:

  • public final String getName(): This method returns the name of the thread as a String.
  • public final void setName(String name): This method sets the name of the thread to the specified name.
Java
class MyThread extends Thread {
}

class Test {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());  // main
    MyThread t = new MyThread();
    System.out.println(t.getName()); // Thread-0
    Thread.currentThread().setName("softAai Apps");
    System.out.println(Thread.currentThread().getName()); // softAai Apps
  }
}

Here,

  • Thread.currentThread().getName() returns the name of the current thread, which is “main” by default.
  • t.getName() returns the name of the thread t, which is “Thread-0” by default since it’s created without a specific name.
  • Thread.currentThread().setName("softAai Apps") sets the name of the current thread to “softAai Apps”.
  • Thread.currentThread().getName() returns the updated name of the current thread, which is now “softAai Apps”.

These methods provide a way to manage and identify threads in Java programs.

Getting the Current Executing Thread Object

We can get the current executing thread object by using Thread.currentThread().

Java
class MyThread extends Thread {
  public void run() {
    System.out.println("run method executed by Thread: " + Thread.currentThread().getName());
  }
}

class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start()
    System.out.println("main method executed by Thread: " + Thread.currentThread().getName());
  }
}

Output:

Java
main method executed by Thread: main
run method executed by Thread: Thread-0
  • In the main() method, Thread.currentThread().getName() returns the name of the current thread, which is “main”. This is because the main() method is executed by the main thread.
  • In the run() method of MyThread, Thread.currentThread().getName() returns the name of the current thread, which is “Thread-0”. This is because the run() method is executed by the thread represented by the MyThread object t, which has the default name “Thread-0”.

Using Thread.currentThread() allows you to obtain information about the thread currently executing a particular block of code, which can be useful for debugging and logging purposes.

Thread Priority

Every thread in Java has some priority, which may be a default thread priority generated by the JVM or a customized priority provided by the programmer. The valid range of thread priorities is from 1 to 10, where 1 is the minimum thread priority and 10 is the maximum thread priority. The Thread class defines the following constants to represent some standard priorities:

  • Thread.MIN_PRIORITY –> 1
  • Thread.NORM_PRIORITY –> 5
  • Thread.MAX_PRIORITY –> 10

Identifying valid thread priorities:

  • 0: Invalid
  • 1: Valid
  • 10: Valid
  • Thread.LOW_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.HIGH_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.MIN_PRIORITY: Valid (constant defined in the Thread class)
  • Thread.NORM_PRIORITY: Valid (constant defined in the Thread class)

If two threads have the same priority, then we can’t expect the exact execution order; it depends on the thread scheduler.

The Thread class defines the following methods to get and set the priority of a thread:

  • public final int getPriority()
  • public final void setPriority(int p) (allowed values range: 1 to 10; otherwise, IllegalArgumentException is thrown)
Java
t.setPriority(7);  // valid
t.setPriority(17); // IllegalArgumentException

In this example, setting the priority to 7 is valid, but setting it to 17 would result in an IllegalArgumentException because it’s outside the valid range.

Default Thread Priority

The default thread priority for the main thread is 5, but for all other threads, the default thread priority will be inherited from the parent thread. This means that whatever priority the parent thread has, the same priority will be inherited by the child thread.

Java
class MyThread extends Thread {
}

class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getPriority());  // 5 (default priority for main thread)
        
        // Thread.currentThread().setPriority(15);  // Throws IllegalArgumentException
        // Thread.currentThread().setPriority(7);   // --> line 1 
        
        // line 1 allowed and sets 7 as priority, but it's redundant as the default priority is 5
        
        MyThread t = new MyThread();
        System.out.println(t.getPriority());  //initial output 7 if line 1 is not commented,  otherwise 5 (as inherited from the parent thread, which is the main thread)
    }
}

If we comment line 1, then the child thread priority will become 5, inherited from the parent thread.

MyThread t = new MyThread();
Here, the parent class is Thread class, and its parent thread is the Main Thread.
Parent thread and parent class are different entities.

Thread Priority Example

Let’s discuss the scenario of setting thread priorities and its implications:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("child thread");
    } 
  }
}

class ThreadPriorityDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    //t.setPriority(10);   --------->  line 1
    t.start();
   
    for(int i=0; i<10; i++) { 
      System.out.println("main thread");
    }
  }
}

If we comment out line 1, then both the child thread and the main thread will have the same priority, which is 5. Hence, we can’t expect the execution order and the exact output, as we don’t know which thread will get the first chance.

If we don’t comment out line 1, then the main thread has priority 5 and the child thread has priority 10. In this case, the child thread will get the chance first, followed by the main thread. The output will be:

Java
child thread
child thread
(child thread repeated 10 times)
main thread
(main thread repeated 10 times)

Note: Some platforms may not provide proper support for thread priorities. In such cases, even if we set high priority, we may not get the exact output. This is a system problem, and there’s nothing we can do about it.

Ways to Temporarily Prevent Thread Execution

We can prevent a thread’s execution temporarily by using the following methods:

  1. yield()
  2. join()
  3. sleep()

yield() Method

yield() causes the current executing thread to pause and gives a chance for waiting threads of the same priority. If there are no waiting threads or all waiting threads have a lower priority, then the same thread can continue its execution. If multiple threads are waiting with the same priority, we can’t predict which waiting thread will get the chance; it depends on the thread scheduler. When the thread that yielded will get a chance again also depends on the thread scheduler, and we can’t predict it precisely.

Complete prototype of yield():

Java
public static native void yield();

Impact of yield() method on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Child Thread");
      Thread.yield();  // Commented line 1
    }   
  }
}

class ThreadYieldDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    for(int i=0; i<10; i++) {
      System.out.println("main Thread");
    }
  }
}

In the above program, if we comment out line 1, then both threads will be executed simultaneously, and we can’t predict which thread will complete first. If we don’t comment out line 1, then the child thread will always call yield(), causing the main thread to get more chances to execute, increasing the likelihood of the main thread completing first.

Note: Some platforms may not provide proper support for yield().

join() Method

This method waits for a thread to complete its execution. When a thread calls join() on another thread, it will wait until the specified thread completes its execution before continuing its own execution. There are several overloaded versions of the join() method that allow you to specify a timeout duration for the waiting period.

  • Prototype of join() method:
Java
public final void join() throws InterruptedException
public final void join(long ms) throws InterruptedException
public final void join(long ms, int ns) throws InterruptedException

Note: Every join() method throws InterruptedException, which is a checked exception. Hence, we must handle it using either try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Usage of join() Method

For example, if a thread t1 wants to wait until another thread t2 completes, then t1 has to call t2.join(). When t1 executes t2.join(), it immediately enters a waiting state until t2 completes. Once t2 completes, t1 can continue its execution.

Example Scenario:

Venue Fixing activity (t1)
Wedding card printing (t2) —- t1.join() —-
Wedding card distribution (t3) —– t2.join() —-

The wedding card printing thread has to wait until the venue fixing thread, hence t2 needs to call t1.join(). Similarly, the wedding card distribution thread has to wait until the wedding card printing thread, hence t3 needs to call t2.join().

Impact of join() on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Seetha Thread");
      try {
        Thread.sleep(2000);
      } catch(InterruptedException e) {
        // Handle the exception
      }
    }
  }
}
 
class ThreadJoinDemo {
  public static void main(String[] args) throws InterruptedException {
    MyThread t = new MyThread();
    t.start();
  
    t.join();     // Commented line 1   (Main thread waits for the child thread to complete)
   
    for(int i=0; i<10; i++) {
      System.out.println("Rama Thread");
    }
  }
}

In the above program, if we comment out line 1, then both the main and child threads will execute simultaneously, and we can’t predict the exact output. If we don’t comment out line 1, then the main thread calls join() on the child thread object, causing the main thread to wait until the child thread completes. In this case, the output will be:

Java
Seetha Thread
Seetha Thread
.. (repeated 10 times)

Rama Thread
Rama Thread 
.. (repeated 10 times)

Waiting of Child Thread Until Completing Main Thread

Let’s discuss the scenario where the child thread waits until the completion of the main thread using the join() method:

Java
class MyThread extends Thread {
    static Thread mt; //this line holds the main thread object in the main method

    public void run() {
        try {
            mt.join(); // Child thread waits until the main thread completes
        } catch (InterruptedException e) {
            // Handle the exception
        }

        for (int i = 0; i < 10; i++) {
            System.out.println("child thread");
        }
    }
}

class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mt = Thread.currentThread();
        MyThread t = new MyThread();
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main thread");
            Thread.sleep(2000);
        }
    }
}

In the above example, the child thread calls join method on the main thread object, hence the child thread has to wait until the main thread completes. In this case, the output is:

Java
main thread
main thread 
.
.
10 times
.
child thread 
.
.
10 times 
.

Explanation

  • In this example, the child thread (MyThread) calls the join() method on the main thread object (mt). This causes the child thread to wait until the main thread completes its execution.
  • The main thread is identified by Thread.currentThread(), and its reference is stored in the mt static variable of the MyThread class.
  • After starting the child thread, the main thread continues its execution. It prints “main thread” ten times with a 2-second delay between each print statement.
  • Meanwhile, the child thread is in the waiting state due to the join() method. Once the main thread completes its execution, the child thread resumes and prints “child thread” ten times.
  • This ensures that the child thread waits for the main thread to finish before continuing its own execution.

Deadlock Situation in case of join() method

Let’s discuss two scenarios involving the join() method that can lead to program deadlock:

Case 1: Deadlock between Main Thread and Child Thread:

If the main thread calls the join method on the child thread object, and the child thread also calls join() on the main thread object, then both threads will wait indefinitely, resulting in a deadlock. This situation resembles a deadlock scenario where neither thread can proceed.

Java
class MyThread extends Thread {
    static Thread mainThread;

    public void run() {
        try {
            mainThread.join(); // Child thread waits for main thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Child thread finished.");
    }
}

class ThreadDeadlockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mainThread = Thread.currentThread(); // Save reference to main thread
        MyThread t = new MyThread();
        t.start();

        try {
            t.join(); // Main thread waits for child thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Main thread finished.");
    }
}

Case 2: Deadlock within the Same Thread:

If a thread calls the join method on itself, then the program will be stuck, similar to a deadlock situation. In this case, the thread has to wait indefinitely for itself to complete, which cannot happen.

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().join(); // Main thread waits for itself
    }
}

In the above example, the main thread is calling join() on itself, causing the program to get stuck indefinitely.

Note: In both cases, the program will be stuck indefinitely, unable to proceed further, due to deadlock situations. These scenarios illustrate the importance of using join() method with caution to avoid deadlocks.

sleep() Method

If a thread doesn’t need to perform any operation for a specific amount of time, then it should use the sleep method. This method causes the currently executing thread to sleep (temporarily suspend execution) for the specified duration.

Prototypes:

Java
public static native void sleep(long ms) throws InterruptedException
public static void sleep(long ms, int ns) throws InterruptedException

Note:
Every sleep() method throws InterruptedException, which is a checked exception. Therefore, whenever we use the sleep method, we must handle InterruptedException either by using try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Impact of sleep method on Thread life cycle

When a thread calls the sleep() method, it enters the TIMED_WAITING state for the specified duration. During this time, the thread does not consume CPU resources and is not eligible for execution. After the specified duration elapses, the thread transitions back to the RUNNABLE state and continues its execution.

Java
class SlideRotator {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Slide-" + i);
            Thread.sleep(5000); // Sleep for 5 seconds
        }
    }
}
  • In this example, the main method prints the slides from 1 to 10 and then sleeps for 5 seconds (Thread.sleep(5000)) before printing the next slide.
  • After printing each slide, the thread sleeps for 5 seconds before proceeding to print the next slide.
  • During the sleep period, the thread transitions to the TIMED_WAITING state, where it remains until the specified duration (5 seconds) elapses.
  • Once the sleep duration is over, the thread transitions back to the RUNNABLE state and continues its execution by printing the next slide.

So, in the above example, the thread pauses for 5000 milliseconds (5 seconds) after printing each slide. This simulates a slide rotation process where each slide is displayed for 5 seconds before moving to the next one.

interrupt() Method

A thread can interrupt a sleeping or waiting thread by using the interrupt() method of the Thread class. This method interrupts the execution of the target thread, causing it to throw an InterruptedException if the target thread is in a sleeping or waiting state.

Prototype:

Java
public void interrupt();

When a thread calls interrupt() on another thread, it sets the interrupt flag for that thread. If the target thread is in a blocking method such as sleep() or wait(), it will throw an InterruptedException immediately, or the next time it enters a blocking operation.

Java
class MyThread extends Thread {
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("I am lazy thread");
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            System.out.println("I got interrupted");
        }
    }
}

class ThreadInterruptedDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        t.interrupt(); // Commented line 1 (Interrupt the child thread)
        System.out.println("End of main thread");
    }
}

If we comment out line 1, then the main thread won’t interrupt the child thread. In this case, the child thread will execute the for loop 10 times.

If we don’t comment out line 1, then the main thread interrupts the child thread. In this case, the output is:

Java
End of main thread 
I am lazy thread
I got interrupted
  • In this example, the main thread interrupts the MyThread thread by calling the interrupt() method on it.
  • If line 1 is commented out, the main thread won’t interrupt the child thread. In this case, the child thread executes the for loop 10 times without interruption.
  • If line 1 is not commented out, the main thread interrupts the child thread. As a result, the child thread throws an InterruptedException, and the message “I got interrupted” is printed.
  • Regardless of whether the child thread is interrupted or not, the message “End of main thread” is always printed at the end of the main thread’s execution.

Note: Whenever we call the interrupt method, if a target thread is not in a sleeping state or waiting state, then there is no immediate impact of the interrupt call. The interrupt call will be queued until the target thread enters a sleeping or waiting state. If the target thread is in a sleeping or waiting state, then the interrupt call will immediately interrupt the target thread.

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<=10000; i++) {
      System.out.println("I am lazy thread-"+i);
    }
    System.out.println("I am entering into sleeping state");
    try {
      Thread.sleep(10000);
    } catch(InterruptedException e) {
      System.out.println("I got interrupted");
    }
  }
}

class ThreadSleepDemo1 {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    t.interrupt();  // Interrupt call is made immediately
    System.out.println("End of main thread");
  }
}

In the above example, the target thread t is interrupted immediately after the start method is called. However, since the target thread is not in a sleeping or waiting state yet, the interrupt call will not have any immediate effect. The interrupt call will be queued until the target thread enters the sleeping state. Therefore, in this case, the output will be:

Java
End of main thread
I am lazy thread-0
I am lazy thread-1
...
I am lazy thread-9999
I am entering into sleeping state
I got interrupted
  • In this example, the main thread starts a MyThread thread and immediately calls the interrupt() method on it.
  • However, since the MyThread thread is executing a loop and not in a sleeping or waiting state, the interrupt call has no immediate effect.
  • The interrupt call will be queued until the MyThread thread enters a sleeping or waiting state.
  • In this case, the MyThread thread eventually enters a sleeping state using Thread.sleep(10000), and the interrupt call immediately interrupts it, causing it to throw an InterruptedException.
  • If the MyThread thread never enters a sleeping or waiting state, the interrupt call would have no effect throughout its execution.

Quick Revision: yield(), join(), and sleep() methods

Let’s discuss yield(), join(), and sleep() methods in Java, along with their characteristics:

yield()

Purpose: If a thread wants to pause its execution to give a chance for the remaining threads of the same priority, then we should use yield().

  • Is it Overloaded? No,
  • Is it final? No
  • Does it throw InterruptedException? No
  • Is it native? Yes
  • Is it static? Yes

join()

Purpose: If a thread wants to wait until completing some other thread, then we should use join().

  • Is it Overloaded? Yes
  • Is it final? Yes
  • Does it throw InterruptedException? Yes
  • Is it native? No
  • Is it static? No

sleep()

Purpose: If a thread doesn’t want to perform any operation for a particular amount of time, then we should use the sleep method.

  • Is it Overloaded? Yes
  • Is it final? No
  • Does it throw InterruptedException? Yes
  • Is it native? sleep(long ms) is native, sleep(long ms, int ms) is non-native
  • Is it static? Yes

Part 3: Mastering Synchronization in Java Threads

Conclusion

Thread management is a crucial aspect of developing robust and efficient multithreaded Java applications. The yield(), join(), and sleep() methods provide valuable tools for controlling thread execution, synchronization, and timing.

By leveraging these methods effectively, developers can orchestrate concurrent activities, coordinate thread interactions, and manage resource contention, thereby enhancing the performance and reliability of their applications in a concurrent environment.

Understanding the nuances of these thread-related methods empowers developers to design and implement multithreaded applications that exhibit optimal concurrency behavior, scalability, and responsiveness, contributing to the development of high-performance Java software systems.

String Interning in Java

String Interning: A Memory-Saving Optimization for Efficient String Handling

In the realm of Java programming, efficient memory management is crucial for optimizing performance. One of the lesser-known yet powerful techniques employed by Java to conserve memory is string interning. String interning is a mechanism that allows multiple string objects with the same value to share the same memory location, thus reducing memory footprint and improving performance. In this blog post, we’ll explore the concept of string interning, its implementation in Java, its benefits, and when to use it.

What is String Interning?

String interning is the process of storing only one copy of each distinct string value in memory, rather than creating multiple copies. When a string is interned, it is added to a pool of strings called the “string intern pool” or “string constant pool.” Subsequent occurrences of the same string literal or identical string values are then referenced to the same string instance in the pool, rather than creating a new object. This ensures that memory is conserved by eliminating redundancy.

Interning of String object in Java

We can use the intern() method to get the corresponding SCP (String Constant Pool) object reference by using a heap object reference (or by using a heap object reference if we want to get the corresponding SCP object reference, then we should go for the intern method).

Java
String s1 = new String("softAai");
String s2 = s1.intern();
System.out.println(s1 == s2); // false
String s3 = "softAai";
System.out.println(s2 == s3); // true

In the heap area: s1 points to "softAai".

In the SCP area: s2 and s3 point to "softAai".

If the corresponding SCP object is not available, then the intern method itself will create the corresponding SCP object.

Java
String s1 = new String("softAai");
String s2 = s1.concat("Apps");
String s3 = s2.intern();
System.out.println(s2 == s3); // false
String s4 = "softAaiApps";
System.out.println(s3 == s4);

In the heap area:

s1 points to "softAai".

s2 points to "softAaiApps".

In the SCP area:

"softAai", "Apps" are stored individually,

Both s3, and s4 point to "softAaiApps".

Practical Proof of Where Objects Get Created: Heap Area or SCP Area

To illustrate the practical proof of where objects are created, let’s consider the following code:

Java
String s1 = new String("you cannot change me!");
String s2 = new String("you cannot change me!");

System.out.println(s1 == s2); // false

String s3 = "you cannot change me!";

System.out.println(s1 == s3); // false

String s4 = "you cannot change me!";
System.out.println(s3 == s4); // true

String s5 = "you cannot" + "change me!"; // ----> 1
System.out.println(s3 == s5); // true  

String s6 = "you cannot";
String s7 = s6 + "change me!"; // ----> 2

System.out.println(s3 == s7); // false

final String s8 = "you cannot";
String s9 = s8 + "change me!"; // -----> 3

System.out.println(s3 == s9);   // true
System.out.println(s6 == s8);    // true 

In the heap area:

  • s1 points to “you cannot change me!”
  • s2 points to “you cannot change me!”
  • s7 points to “you cannot change me!”

In the SCP area:

  • s3, s4, s5, s9 point to “you cannot change me!”
  • s6, s8 point to “you cannot”
  • “change me!” is stored individually
Note

In the above code, see lines marked as 1, 2, and 3,

Now, let’s analyze the lines marked as —->1, —->2, and —->3:

  1. This operation will be performed at compile time only because both arguments are compile-time constants.
  2. This operation will be performed at runtime only because at least one argument is a normal variable.
  3. This operation will be performed at compile time only because both arguments are compile-time constants.

Importance of String Constant Pool (SCP)

In our program, if a string object is required to be repeated, it is not recommended to create separate objects because it creates performance and memory problems. Instead of creating separate objects for every requirement, we have to create only one object, and we can reuse the same object for every requirement. This approach improves performance and memory utilization. This efficiency is possible because of SCP. Hence, the main advantages of SCP are improved memory utilization and performance.

However, the main problem with SCP is that if several references point to the same object, using one reference to change the content will affect the remaining references. To overcome this problem, Sun implemented string objects as immutable. This means that once we create a string object, we can’t perform any changes to the existing object. If we attempt to make any changes, a new object will be created. Hence, SCP is the primary reason for the immutability of string objects.

Conclusion

String interning is a powerful memory optimization technique in Java that allows for efficient storage and retrieval of string objects. By intelligently managing memory and reducing redundancy, string interning contributes to improved performance and resource utilization in Java applications. Understanding the principles and benefits of string interning empowers Java developers to leverage this technique effectively to optimize their applications for memory and performance efficiency.

Object Cloning in Java

Demystifying Object Cloning in Java: A Detailed Guide

Java, being an object-oriented programming language, allows developers to create and manipulate objects. In many cases, you might encounter a situation where you need to create a copy of an existing object. This process of creating an exact copy of an object is called object cloning. Understanding object cloning in Java is essential for various programming scenarios, such as creating copies of complex objects, implementing prototype patterns, and more. In this comprehensive guide, we’ll delve into the concept of object cloning in Java, explore different techniques, and discuss best practices.

What is Object Cloning?

The process of creating an exactly duplicate object is called cloning. The main purpose of cloning is to maintain a backup copy and preserve the state of the object. We can perform cloning by using the clone method of the Object class:

Java
protected native Object clone() throws CloneNotSupportedException

In Java, to enable cloning for objects, the corresponding class must implement the Cloneable interface. This interface, found in the java.lang package, doesn’t contain any methods; it serves solely as a marker interface. Attempting to clone an object that doesn’t implement Cloneable will result in a CloneNotSupportedException.

Java
class Test implements Cloneable {
    int i = 10;
    int j = 20;

    public static void main(String[] args) throws CloneNotSupportedException {
        Test t1 = new Test();
        Test t2 = (Test) t1.clone();
        t2.i = 888;
        t2.j = 999;
        System.out.println(t1.i + "......" + t1.j); //10...20
    }
}

Here, Test class implements the Cloneable interface, allowing its objects to be cloned using the clone() method.

What are different approaches to cloning objects in Java

Shallow cloning and deep cloning are two different approaches to cloning objects in Java.

Shallow Cloning

Shallow cloning refers to the process of creating a bitwise copy of an object. If the main object contains primitive variables, then exactly duplicate copies will be created in the cloned object. However, if the main object contains any reference variables, then corresponding objects won’t be created; instead, duplicate reference variables will be created, pointing to the old content object. The Object class’s clone method is meant for shallow cloning.

Java
class Cat {
  int j;
  Cat(int j) {
    this.j = j;
  }
}

class Dog implements Cloneable {
  Cat c;
  int i;
  Dog(Cat c, int i) {
    this.c = c;
    this.i = i;
  }

  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}

class ShallowCloning {
  public static void main(String[] args) throws CloneNotSupportedException {
    Cat c = new Cat(20);
    Dog d1 = new Dog(c, 10);
    System.out.println(d1.i + "..............." + d1.c.j);
    Dog d2 = (Dog)d1.clone();
    d2.i = 888;
    d2.c.j = 999;
    System.out.println(d1.i + "........." + d1.c.j); // Output: 10 .... 999
  }
}

In shallow cloning, if changes are made to the content object through the cloned object reference, those changes will be reflected in the main object. To overcome this problem, we should use deep cloning.

Deep Cloning

Deep cloning refers to the process of creating an exactly duplicate independent copy, including the content object. In deep cloning, if the main object contains any primitive variables, then duplicate values will be created in the cloned object. If the main object contains any reference variables, then corresponding cloned objects will be created in the cloned copy. By default, the Object class’s clone method is meant for shallow cloning, but we can implement deep cloning explicitly by overriding the clone method in our class.

Java
class Cat {
  int j;
  Cat(int j) { 
    this.j = j; 
  } 
} 

class Dog implements Cloneable {
  Cat c;
  int i;
   
  Dog(Cat c, int i) { 
    this.c = c;
    this.i = i;
  }

  public Object clone() throws CloneNotSupportedException {
    Cat c1 = new Cat(c.j);
    Dog d  = new Dog(c1, i);
    return d;
  } 
}

class DeepCloning {
  public static void main(String[] args) throws CloneNotSupportedException {
    Cat c = new Cat(20);
    Dog d1 = new Dog(c, 10);
    System.out.println(d1.i + "......." + d1.c.j);
    Dog d2 = (Dog) d1.clone();
    d2.i = 888;
    d2.c.j = 999;
    System.out.println(d1.i + "..........." + d1.c.j);
  }
}

When using a cloned object reference, if we perform any change to the contained object, those changes won’t be reflected in the main object.

Which cloning method is the best?

While shallow cloning creates a new object with copies of the original object’s fields, including references to the same content objects, deep cloning creates a completely independent duplicate copy, including new instances of all referenced objects. The choice between shallow and deep cloning depends on the specific requirements of the cloning operation and the desired behavior of the cloned objects.

If the object contains only primitive variables, then shallow cloning is the best choice. If the object contains reference variables, then deep cloning is the best choice.

Best Practices for Object Cloning

When implementing object cloning in Java, consider the following best practices:

  1. Override clone() method: Always override the clone() method to ensure proper cloning behavior.
  2. Implement Cloneable interface: If you choose to use the clone() method, implement the Cloneable interface to indicate that your class supports cloning.
  3. Handle CloneNotSupportedException: Handle the CloneNotSupportedException appropriately when cloning an object.
  4. Deep cloning when necessary: For complex objects containing reference types, implement deep cloning to ensure that all referenced objects are also cloned.
  5. Immutable classes: If possible, design your classes to be immutable to avoid the need for cloning.

Conclusion

Object cloning in Java is a powerful mechanism for creating copies of objects. By understanding the concepts of shallow and deep cloning and implementing the clone() method or Cloneable interface, you can effectively clone objects in your Java applications. However, it’s essential to be cautious while cloning objects, especially when dealing with complex data structures and mutable objects. Following best practices and considering the implications of cloning ensures that your Java applications perform as expected while maintaining the integrity of your data.

== Operator and .equals() Method in Java

Comprehensive Analysis: == Operator versus .equals() Methods in Java

In Java, developers often encounter situations where they need to compare objects for equality. However, understanding the nuances between using the == operator and the .equals() method is crucial for writing correct and efficient code. This article delves into the relationship between these two approaches in the java.lang package, providing insights into their differences, similarities, and best practices.

Understanding the == Operator

The == operator in Java is primarily used for comparing primitive data types and object references. When applied to primitive types such as int, double, or char, it compares their values directly. For example:

Java
int a = 5;
int b = 5;
boolean result = (a == b); // result will be true

However, when it comes to objects, the == operator behaves differently. It compares object references rather than their actual contents. Two object references are considered equal if they point to the same memory location, i.e., if they refer to the same object instance. For instance:

Java
String str1 = new String("softAai");
String str2 = new String("softAai");
boolean result = (str1 == str2); // result will be false

Here, str1 and str2 point to different memory locations, even though their contents are the same. Therefore, the == operator returns false.

Understanding the .equals() Method

In Java, the .equals() method is used to compare the contents of objects for equality. The java.lang.Object class, which is the root class for all Java classes, provides a default implementation of the .equals() method. This default implementation compares object references similar to the == operator.

However, many classes in Java override the .equals() method to provide custom equality comparison based on the contents of objects rather than their references. For example, the String class overrides the .equals() method to compare the actual string contents:

Java
String str1 = new String("Hello");
String str2 = new String("Hello");
boolean result = str1.equals(str2); // result will be true

In this case, the .equals() method compares the contents of str1 and str2, resulting in true since they contain the same string.

Relation between == Operator and .equals() Method

The relationship between the == operator and the .equals() method can be summarized as follows:

  • If two objects are determined to be equal by the == operator, it’s guaranteed that they will also be equal when compared using the .equals() method.
    • This means that if r1 == r2 evaluates to true, then r1.equals(r2) will always be true as well.
  • However, if two objects are not deemed equal by the == operator, we cannot make any assumptions about the outcome of the .equals() method. It may return either true or false.
    • In other words, if r1 == r2 evaluates to false, r1.equals(r2) may return true or false, and we cannot predict the exact result.
  • Similarly, if two objects are determined to be equal by the .equals() method, we cannot infer anything about the == operator. It may return true or false.
    • So, if r1.equals(r2) is true, then r1 == r2 may return true or false, and we cannot determine the exact outcome.
  • Conversely, if two objects are not equal according to the .equals() method, they will always not be equal when compared using the == operator.
    • Therefore, if r1.equals(r2) evaluates to false, then r1 == r2 will always be false as well.

Example

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");

System.out.println(s1 == s2);  //false
System.out.println(s1.equals(s2)); //true
System.out.println(sb1 == sb2);  //false
System.out.println(sb1.equals(sb2)); //false
// System.out.println(s1 == sb1);  //CE: incomparable types: java.lang.String and java.lang.StringBuffer
System.out.println(s1.equals(sb1)); //false

Note: To use the == operator, there must be some relation between argument types (either child to parent, parent to child, or the same type); otherwise, we will get a compilation error saying “incomparable types.” If there is no relation between argument types, then the .equals() method won’t raise any runtime or compile-time errors; it simply returns false.

Differences between the == operator and .equals() method

The differences between the == operator and .equals() method can be summarized as follows:

== operator

  • It is an operator applicable for both primitives and object types
  • In the case of object references, the == operator is meant for reference comparison, which entails comparing memory addresses.
  • We can’t override the == operator for content comparison
  • To use the == operator, there must be a relationship between the argument types (either child to parent, parent to child, or the same type); otherwise, we will receive a compile-time error indicating “incomparable types.”

.equals() method

  • It is a method applicable only for object types and not for primitives.
  • By default, the .equals() method present in the object class is also intended for reference comparison.
  • We can override the .equals() method for content comparison.
  • If there is no relation between argument types, then .equals() won’t raise any compile-time or runtime errors and simply returns false.

In general, use the == operator for reference comparison and .equals() for content comparison;

For any object reference “r,”

both “r == null” and “r.equals(null)” always return false.

Java
// Example:
Thread t = new Thread();
System.out.println(t == null); // false
System.out.println(t.equals(null)); // false

Also, one more thing,

Hashing-related data structures follow the following fundamental rules:

Two equivalent objects should be placed in the same bucket, but not all objects present in the same bucket need to be equal.

Contract between .equals() and hashCode()

If two objects are equal by the .equals() method, then their hashcodes must be equal; that is, two equivalent objects should have the same hashcode. Therefore, if “r1.equals(r2)” is true, then “r1.hashCode() == r2.hashCode()” is always true. The Object class’s .equals() and hashCode() methods follow these contracts; hence, whenever we override .equals(), it is compulsory to override hashCode() to satisfy these contracts. This ensures that two equivalent objects have the same hashcode.

If two objects are not equal by .equals(), then there are no restrictions on their hashcodes; they may be equal or not equal. If the hashcodes of two objects are equal, we cannot conclude anything about .equals(); it may return true or false. However, if the hashcodes of two objects are not equal, then these objects are always not equal by the .equals() method.

Note: To satisfy the contract between equals and hashCode methods, whenever we are overriding .equals(), it is compulsory to override the hashCode method; otherwise, we won’t get any compile-time or runtime error, but it is not a good programming practice.

In the String class, .equals() is overridden for content comparison, hence the hashCode method is also overridden to generate a hashcode based on content. For example:

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode()); // 95950491
System.out.println(s2.hashCode()); // 95950491

In the StringBuffer class, .equals() is not overridden for content comparison, and hence the hashCode method is also not overridden. For example:

Java
StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");
System.out.println(sb1.equals(sb2)); // false
System.out.println(sb1.hashCode()); // 19621457
System.out.println(sb2.hashCode()); // 4872882

It is highly recommended to override the hashCode() method. In all collection classes, in all wrapper classes, and in the String class, .equals() is overridden for content comparison; hence, it is highly recommended to override .equals() as well for content comparison.

Best Practices

To ensure correctness and efficiency in your Java code, consider the following best practices:

  1. Use the == operator for primitive types and object reference comparison.
  2. Use the .equals() method for comparing the contents of objects, especially when dealing with strings, arrays, and custom classes.
  3. Override the .equals() method in custom classes to provide meaningful content comparison.
  4. Be cautious when comparing objects of different types, as the behavior of == and .equals() may vary depending on the implementation.

Conclusion

Understanding the relationship between the == operator and the .equals() method is essential for writing robust and efficient Java code. By knowing when to use each approach and following best practices, developers can ensure correct object comparison and avoid common pitfalls. Whether comparing object references or their contents, choosing the appropriate method is key to achieving desired behavior in Java applications.

autoboxing in java

Mastering Autoboxing and Auto-Unboxing: A Comprehensive Guide for Java Developers

Java, being a statically-typed language, often requires explicit type conversions and careful handling of primitive types and their corresponding wrapper classes. However, certain features introduced in later versions of Java, particularly in the java.lang package, aim to simplify the process of working with primitive types and their object counterparts. In this article, we’ll delve into four such features: autoboxing, auto-unboxing, widening, and varargs methods.

Autoboxing and Auto-Unboxing

Autoboxing and auto-unboxing were introduced in Java 5 as a means to automatically convert between primitive types and their corresponding wrapper classes (Integer, Double, Boolean, etc.) without explicit intervention from the programmer.

Autoboxing

Autoboxing refers to the automatic conversion from a primitive type to its corresponding wrapper object by the compiler.

For example, when we write: Integer I = 10, the compiler converts the int to Integer automatically through autoboxing.

After compilation, the above line becomes: Integer I = Integer.valueOf(10);. Internally, autoboxing is always implemented using .valueOf().

So, Integer I = Integer.valueOf(10); is what the compiler executes at the time of autoboxing.

Auto-Unboxing

AutoUnboxing refers to the automatic conversion of a wrapper object to its corresponding primitive type by the compiler.

For example:

Java
Integer I = new Integer(10);
int i = I;  // Compiler converts Integer to int automatically by autounboxing

After compilation, the above line becomes:

Java
int i = I.intValue();

Internally, the autounboxing concept is implemented using xxxValue() methods.

The conversion can be summarized as follows:

  • Primitive value ———– Autoboxing[valueOf()] ——————-> Wrapper object
  • Wrapper object ————- AutoUnboxing[xxxValue()] —————-> Primitive value

Here’s an example demonstrating autoboxing and autounboxing in code:

Java
class Test {
    static Integer I = 10;   // AutoBoxing
    public static void main(String[] args) { 
        int i = I;  // AutoUnboxing
        m1(i);   
    } 

    public static void m1(Integer K) {  // AutoBoxing
        int m = K; // AutoUnboxing
        System.out.println(m);  // Output: 10 
    }
} 

Just because of autoboxing and autounboxing, we can use wrapper class objects and primitives interchangeably from Java 1.5 onwards.

Example 2:

Java
class Test {
    static Integer I = 0;
    public static void main(String[] args) { 
        int m = I;
        System.out.println(m);   // Output: 0
    }
}

However, if the wrapper object is null when performing autoboxing, it will result in a NullPointerException:

Java
class Test {
    static Integer I;
    public static void main(String[] args) {
        int m = I;  
        System.out.println(m); // NullPointerException
    }
}

To avoid NullPointerException in such cases, ensure that the wrapper object is not null before performing autounboxing.

Lets see one more example for another use case

Java
Integer X = 10;
Integer Y = X;

X++;

System.out.println(X);  // Output: 11
System.out.println(Y);  // Output: 10

System.out.println(X == Y); // Output: false

Explanation:

  1. Integer X = 10;: This line creates an Integer object X with the value 10.
  2. Integer Y = X;: Here, a reference to the same Integer object that X points to is assigned to Y.
  3. X++;: This increments the value of X by 1. Since Integer objects are immutable, a new Integer object with the value 11 is created, and X now points to this new object.
  4. System.out.println(X);: Prints the value of X, which is 11.
  5. System.out.println(Y);: Prints the value of Y, which still holds the original value of X, i.e., 10.
  6. System.out.println(X == Y);: Compares the references of X and Y, which are pointing to different objects after the increment operation, hence it returns false.

Note: All wrapper classes are immutable, meaning once we create a wrapper object, we cannot modify its value. If we attempt to perform any changes to it, a new object will be created instead. This is why a new Integer object with the value 11 is created when we increment X.

Few more examples:

Example 1

Java
Integer x = new Integer(10);  // x => 10
Integer y = new Integer(10);  // y => 10
System.out.println(x == y);   // Output: false

x and y are referencing different Integer objects, even though they hold the same value 10. Therefore, the comparison using == returns false.

Example 2

Java
Integer x = new Integer(10);  // x => 10
Integer y = 10;                // y => 10
System.out.println(x == y);    // Output: false

x refers to a new Integer object with the value 10, while y refers to an Integer object from the Integer cache. Though they hold the same value, they are different objects, hence false is printed.

Example 3

Java
Integer x = 10;   // x, y => 10
Integer y = 10;
System.out.println(x == y);   // Output: true

Both x and y refer to the same Integer object with the value 10. In this case, they are referencing the same cached object, so the comparison using == returns true.

Example 4

Java
Integer x = 100;   // x, y => 100
Integer y = 100;
System.out.println(x == y);   // Output: true

Similar to the previous case, x and y reference the same Integer object with the value 100, which is within the range of the Integer cache. Hence, true is printed.

Example 5

Java
Integer x = 1000;  // x => 1000
Integer y = 1000;  // y => 1000
System.out.println(x == y);   // Output: false

The values 1000 are outside the range of the Integer cache (-128 to 127), so new Integer objects are created for both x and y, resulting in false when compared using ==.

To facilitate autoboxing, Java internally maintains a buffer of wrapper objects upon the loading of wrapper classes. This buffer strategy optimizes memory usage by reusing existing objects. When an object is required, the JVM checks if it’s already present in the buffer. If so, the existing object is utilized; otherwise, a new object is created.

The buffer concept is available only within specific ranges for each wrapper type:

  • Byte: Always available.
  • Short: Ranges from -128 to 127.
  • Integer: Ranges from -128 to 127.
  • Long: Ranges from -128 to 127.
  • Character: Ranges from 0 to 127.
  • Boolean: Always available.

See below example usage which demonstrating buffer behavior

Java
Integer x = 127;
Integer y = 127;
System.out.println(x == y);  // Output: true

Integer x = 128;
Integer y = 128;
System.out.println(x == y);  // Output: false

Boolean x = false;
Boolean y = false;
System.out.println(x == y);  // Output: true

Double x = 10.0;
Double y = 10.0;
System.out.println(x == y);  // Output: false

Internally, autoboxing is implemented using valueOf(), so the buffer concept applies to it as well.

Examples using valueOf():

Java
Integer x = new Integer(10);
Integer y = new Integer(10);
System.out.println(x == y);  // Output: false

Integer x = 10;
Integer y = 10;
System.out.println(x == y);  // Output: true

Integer x = Integer.valueOf(10);
Integer y = Integer.valueOf(10);
System.out.println(x == y);  // Output: true

Integer x = 10;
Integer y = Integer.valueOf(10);
System.out.println(x == y);  // Output: true

Widening Methods

Let’s delve into overloading with respect to autoboxing, widening, and varargs, but befor that let’s see what is widening exactly.

Widening refers to the automatic conversion of a smaller data type to a larger data type in the hierarchy. In Java, the widening conversion follows the hierarchy: byte -> short -> char -> int -> long -> float -> double.

In the given example:

Java
class Test {
    public static void m1(Integer I) {
        System.out.println("Autoboxing");
    }

    public static void m1(long l) {
        System.out.println("Widening");
    }

    public static void main(String args[]) {
        int x = 10;
        m1(x);
    }
}

Explanation:

  • We have two overloaded methods m1: one accepts an Integer parameter, and the other accepts a long parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes widening to match the long parameter, as widening is preferred over autoboxing.
  • Therefore, the output will be “Widening”.

This happens because widening is applicable from Java 1.0 version and gets priority over autoboxing, which was introduced later in Java 1.5 version.

Varargs Methods

Varargs, short for variable-length arguments, allow methods to accept a variable number of arguments of a specified type. This feature simplifies the creation of methods that operate on a variable number of inputs.

Syntax

The syntax for declaring a varargs parameter is to use an ellipsis (...) after the parameter type.

Java
public void exampleMethod(String... strings) {
    // Method body
}
Java
public void printValues(String... values) {
    for (String value : values) {
        System.out.println(value);
    }
}

// Invoking the method
printValues("Hello", "World"); // Output: Hello\nWorld
printValues("Java", "is", "awesome"); // Output: Java\nis\nawesome

Now, Let’s see overloading with respect to varargs and widening.

Java
class Test {
    public static void m1(Int... x) {
        System.out.println("Varargs method");
    }

    public static void m1(long l) {
        System.out.println("Widening");
    }

    public static void main(String args[]) {
        int x = 10;
        m1(x);
    }
}
  • We have two overloaded methods m1: one accepts a varargs parameter of type Int, and the other accepts a long parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes widening to match the long parameter, as widening is preferred over varargs methods.
  • Therefore, the output will be “Widening” (as widening is from 1.0v to it gets first chance instead of Autoboxing).

Let’s see overloading with respect to varargs, autoboxing, and widening

Java
class Test {
    public static void m1(Int... x) {
        System.out.println("Varargs method");
    }

    public static void m1(Integer I) {
        System.out.println("Autoboxing");
    } 

    public static void main(String args[]) {
        int x = 10;
        m1(x);
    }
}
  • We have two overloaded methods m1: one accepts a varargs parameter of type Int, and the other accepts an Integer parameter.
  • In the main method, we have an int variable x.
  • When we call m1(x), the int value x undergoes autoboxing to match the Integer parameter, as autoboxing is preferred over varargs methods.
  • Therefore, the output will be “Autoboxing”.

This happens because, while resolving overloaded methods, the compiler gives priority to widening, then autoboxing, and finally varargs methods. Since autoboxing has a higher priority than varargs, it gets selected in this scenario.

Order Priority -> 1. Widening 2. Autoboxing 3. var-arg method

Compilation Error (CE)

In some cases compiler error occurs, Let’s see why we face a compilation error (CE).

Java
class Test {
  public static void m1(Long l) { 
    System.out.println("Long");
  } 
  
  public static void main (String args[]) {
    int x = 10;
    m1(x);
  }
}

Compilation Error: m1(java.lang.Long) in Test cannot be applied to (int)

Here, compiler error occurs because there’s no direct conversion path from int to Long involving both autoboxing and widening.

  1. In the m1 method, the parameter type is Long.
  2. In the main method, an int variable x is declared and initialized with the value 10.
  3. When calling m1(x), the compiler attempts to find a suitable method to match the argument x, which is of type int.
  4. In Java, there are no implicit conversions from int to Long that involve both autoboxing and widening. Although int can be widened to long and then autoboxed to Long, this kind of conversion sequence is not supported by the compiler.
  5. Therefore, the compiler reports a compilation error because there is no suitable overload of m1 that accepts an int argument directly or through a sequence of implicit conversions involving autoboxing and widening.
  6. To resolve the error, you can either change the type of x to Long or provide a suitable overload of m1 that accepts an int argument or its equivalent types.

This limitation arises because after widening, autoboxing is not implemented in Java. However, after autoboxing, widening is internally implemented. This means that widening followed by autoboxing is not supported directly, leading to the compilation error.

What will happen in case of Java Objects

Java
class Test {
    public static void m1(Object o) {
        System.out.println("Object version");
    }
  
    public static void main(String[] args) {
        int x = 10;
        m1(x);
    }
}

The output will be “Object version”. As we know autoboxing then widening is possible in java

Explanation:

  • The method m1(Object o) expects a parameter of type Object.
  • In the main method, we have an int variable x.
  • When m1(x) is called, autoboxing is applied to convert int to Integer.
  • Then, widening is applied to convert Integer to Object, as widening is applicable in Java.
  • Therefore, the method m1(Object o) is invoked with the argument Integer, and the output will be “Object Version”.

Additionally, the statements:

Java
Object o = 10;  // is perfectly valid
Number n = 10;  // is also perfectly valid

These statements are valid because of autoboxing. In Java, autoboxing allows automatic conversion between primitive types and their corresponding wrapper classes (e.g., int to Integer, double to Double, etc.). Since Integer is a subclass of Object, and Integer is a subclass of Number, the assignments Object o = 10; and Number n = 10; are perfectly valid.

Conclusion

Autoboxing, auto-unboxing, widening, and varargs methods are features introduced in Java to simplify the handling of primitive types and method parameterization. They enhance code readability, reduce boilerplate, and provide more flexibility in method invocation. However, it’s essential to understand their implications and usage patterns to leverage them effectively in Java programming.

Wrapper Classes in Java.lang Package

Wrapper Classes in Java.lang Package: Unveiling the Mystery through A Comprehensive Guide

In Java programming, the java.lang package plays a pivotal role, containing fundamental classes and interfaces that are automatically imported into every Java program. Among these classes, the wrapper classes stand out as essential components that bridge the gap between primitive data types and objects. In this article, we delve into the nuances of wrapper classes, exploring their significance, usage, and practical applications within the Java ecosystem.

What are Wrapper Classes?

In Java, data types can be classified into two categories: primitive types (such as int, double, boolean) and reference types (objects). While primitive types offer efficiency and simplicity, they lack the ability to participate in object-oriented paradigms directly. Wrapper classes address this limitation by providing an object representation for each primitive type, allowing them to be treated as objects.

Imagine a scenario where you need to store primitive data types (like int, char, boolean) within collections, pass them as arguments to methods expecting objects, or perform operations that leverage object-oriented features. This is where wrapper classes come to the rescue! They essentially “wrap” primitive data types into objects, bestowing upon them the power of object-oriented capabilities.

Wrapper classes’ main objective is to wrap primitives into object form so that we can handle primitives just like objects.

Several utility methods are required for primitives.

Almost all wrapper classes contain two constructors: one that can take corresponding primitives as arguments and another one that can take a string as an argument.

Example 1

Java
Integer I = new Integer(10);
Integer I = new Integer("10");

Example 2

Java
Double D = new Double(10.5);
Double D = new Double("10.5");

Note: If the string argument does not represent a number, then we will get a NumberFormatException. For example, Integer I = new Integer("ten"); will result in a NumberFormatException.

Java
Integer I = new Integer("ten"); // Resulting in a NumberFormatException

List of Wrapper Classes

Wrapper classes serve the purpose of encapsulating primitive data types into object form, facilitating the handling of primitives as objects. Each wrapper class provides constructors tailored to accept either the corresponding primitive type or a string representation.

Byte

Constructor Arguments: byte or String

Short

Constructor Arguments: short or String

Integer

Constructor Arguments: int or String

Long

Constructor Arguments: long or String

Float

Constructor Arguments: float, double, or String

Example Usage:

Java
Float f = new Float(10.5f);
Float f = new Float("10.5");
Float f = new Float(10.5);
Float f = new Float("10.5");

Note: Float class can contain three constructors with float, double, and string arguments.

Double

Constructor Arguments: double or String

Character

Constructor Arguments: char

Java
Character ch = new Character('a'); // Correct
Character ch = new Character("a"); // Incorrect

Note: Character class contains only one constructor which can take only one argument, that is, char and does not take a String constructor.

Boolean

Constructor Arguments: boolean or String

Boolean class contains two constructors which can take boolean and String arguments, that is, boolean and String constructor. but there are some twists, Let’s see it.

Case 1: When passing boolean primitives, only allowed values are true and false where case is important and content also very important.

Java
Boolean B = new Boolean(true);  // Correct
Boolean B = new Boolean(false); // Correct
Boolean B = new Boolean(True);  // Incorrect
Boolean B = new Boolean(amol); // Incorrect

Case 2: When passing a String as an argument, case and content both are not important. If the content is a case-insensitive String of “true”, then it is treated as true; otherwise, it is treated as false.

Java
Boolean B = new Boolean("true");  // true
Boolean B = new Boolean("True");  // true
Boolean B = new Boolean("TRUE");  // true 
Boolean B = new Boolean("malaika"); // false
Boolean B = new Boolean("mallika"); // false
Boolean B = new Boolean("jareena"); // false

Let’s see one more example,

Java
Boolean X = new Boolean("yes"); 
Boolean Y = new Boolean("no");  

System.out.println(X);  // Output: false
System.out.println(Y);  // Output: false
System.out.println(X.equals(Y)); // Output: true

Note: In all wrapper classes, toString method is overridden to return content directly, and .equals method is overridden for content comparison.

Utility Methods

1.valueOf()
2.xxxValue()
3.parseXxx()
4.toString()

Here’s the breakdown of the utility methods for wrapper classes:

1. valueOf()

Every wrapper class except the Character class contains a public static method valueOf(String s) which can take a String as an argument to create the corresponding wrapper class.

Java
public static wrapper valueOf(String s)

Example:

Java
Integer I = Integer.valueOf("10");
Double D = Double.valueOf("10.5");
Boolean B = Boolean.valueOf("softAai");
Form 2

Every integral type wrapper class (Byte, Short, Integer, Long) contains the following valueOf method to create a wrapper object for the specified radix string.

Java
public static wrapper valueOf(String s, int radix);

Note: Radix is the parameter that specifies the number system to be used. For example, binary = 2, octal = 8, hexadecimal = 16, etc. The allowed range of radix is 2 to 36.

Example:

Java
Integer I = Integer.valueOf("100", 2);
System.out.println(I); // Output: 4

Integer I = Integer.valueOf("101", 4);
System.out.println(I); // Output: 17
Form 3

Every wrapper class including the Character class contains a public static method valueOf(primitive p) to create a wrapper object for the given primitive.

Java
public static wrapper valueOf(primitive p);
Java
Integer I = Integer.valueOf(10);
Double D = Double.valueOf(10.5);
Character ch = Character.valueOf('a');

In short, valueOf() methods are used to create wrapper objects for given primitive or string values.

Primitive/String ——– valueOf() ———> Wrapper Object

2. xxxValue()

We can use xxxValue() methods to get the primitive value from a given wrapper object.

Every number type wrapper class (Integer, Byte, Short, Long, Float, Double) contains the following methods to get primitive values for a given wrapper object:

  1. byteValue():
    • Returns the byte primitive value of the wrapper object.
    • Method Signature: public byte byteValue()
  2. shortValue():
    • Returns the short primitive value of the wrapper object.
    • Method Signature: public short shortValue()
  3. intValue():
    • Returns the int primitive value of the wrapper object.
    • Method Signature: public int intValue()
  4. longValue():
    • Returns the long primitive value of the wrapper object.
    • Method Signature: public long longValue()
  5. floatValue():
    • Returns the float primitive value of the wrapper object.
    • Method Signature: public float floatValue()
  6. doubleValue():
    • Returns the double primitive value of the wrapper object.
    • Method Signature: public double doubleValue()
Java
Integer I = new Integer(130);
System.out.println(I.byteValue());    // Output: -126
System.out.println(I.shortValue());   // Output: 130
System.out.println(I.intValue());     // Output: 130
System.out.println(I.longValue());    // Output: 130
System.out.println(I.floatValue());   // Output: 130.0
System.out.println(I.doubleValue());  // Output: 130.0

These methods allow us to obtain the primitive values (byte, short, int, long, float, double) from the given wrapper object.

  • charValue():
    • The charValue() method is available in the Character class to get the char primitive value for a given character object.
    • Method Signature: public char charValue()
Java
Character ch = new Character('a');
char c = ch.charValue();
System.out.println(c); // Output: 'a'

This method allows us to obtain the char primitive value from the given character object.

  • booleanValue():
    • The booleanValue() method is available in the Boolean class to get the boolean primitive value for the given Boolean object.
    • Method Signature: public boolean booleanValue()
Java
Boolean B = Boolean.valueOf("softAai");
boolean b = B.booleanValue();
System.out.println(b); // Output: false

This method allows us to obtain the boolean primitive value from the given Boolean object.

In total, there are 38 (6*6 + 1 + 1) xxxValue() methods present.

Wrapper Object ———————— xxxValue() ———————-> Primitive Value

3. parseXxx()

We can use parseXxx() methods to convert a String to a primitive.

String ——————- parseXxx() ———————-> primitive

Form 1:

Every wrapper class except the Character class contains the following parseXxx() method to find the primitive for the given String object.

Java
public static primitive parseXxx(String s);
Java
int i = Integer.parseInt("10");
double d = Double.parseDouble("10.5");
boolean b = Boolean.parseBoolean("true");
Form 2:

Every integer wrapper class (Byte, Short, Integer, Long) contains the following parseXxx() method to convert a specified radix String to a primitive. The allowed range of radix is 2 to 36.

Java
public static primitive parseXxx(String s, int radix);
Java
int i = Integer.parseInt("1111", 2);
System.out.println(i); // Output: 15

4. toString()

We can use toString() to convert a wrapper object or primitive to a String.

Form 1

Every wrapper class contains the following toString() method to convert a wrapper object to a String type.

Java
public String toString();

It is an overriding version of the toString() method in the Object class. Whenever we try to print a Wrapper class reference, internally this toString() method will be called.

Java
Integer I = new Integer(10);
String s = I.toString();
System.out.println(s); // Output: "10"

System.out.println(I); // internally calls I.toString(), Output: "10"

Wrapper Object ——————- toString() ———————-> String

Form 2

Every wrapper class, including the Character class, contains the following static toString() method to convert a primitive value to a String type.

Java
public static String toString(primitive p)
Java
String s = Integer.toString(10);
String s = Boolean.toString(true);
String s = Character.toString('a');

This method is particularly useful when you need to convert a primitive value to a String without creating an object of the corresponding wrapper class.

Primitive Value ——————- toString() ———————-> String

Form 3

Integer and Long classes contain the following static toString() method to convert a primitive to a radix String.

Java
public static String toString(primitive p, int radix);

The allowed radix range is from 2 to 36.

Java
String s = Integer.toString(15, 2);
System.out.println(s);  // Output: "1111"

This method is particularly useful when you need to convert a primitive value to a String representation in a specific radix (base).

Form 4

Integer and Long wrapper classes contain the following toXxxString() methods to convert a primitive to a String in binary, octal, and hexadecimal form.

Java
public static String toBinaryString(primitive p);

public static String toOctalString(primitive p);

public static String toHexString(primitive p);
Java
String s = Integer.toBinaryString(10);
System.out.println(s); // Output: "1010"

String s = Integer.toOctalString(10);
System.out.println(s); // Output: "12"

String s = Integer.toHexString(10);
System.out.println(s); // Output: "a"

These methods allow you to obtain a string representation of the given primitive value in binary, octal, or hexadecimal form. They are particularly useful for formatting output or performing bitwise operations.

Wrapper Object / Primitive ———————- toString() —————————–> String

Conversions between String, WrapperObject, and Primitive

Wrapper Object to String:

  • Using toString(): Converts a wrapper object to a String.
Java
Wrapper Object ----------------- toString --------------------> String

Wrapper Object to Primitive Value:

  • Using XxxValue(): Extracts the primitive value from a wrapper object.
Java
Wrapper Object ------------------ XxxValues() ---------------------> Primitive Value

String to Primitive Value:

  • Using parseXxx(): Converts a String to a primitive value.
Java
String ------------------------- parseXxx() -------------------------> Primitive Value

String to Wrapper Object:

  • Using valueOf(): Converts a String to a wrapper object.
Java
String -------------------------- valueOf() -----------------------------> Wrapper Object

Primitive Value to Wrapper Object:

  • Using valueOf(): Converts a primitive value to a wrapper object.
Java
Primitive Value ---------------- valueOf() ----------------> Wrapper Object

Primitive Value to String:

Using toString(): Converts a primitive value to a String.

Java
Primitive Value ----------------- toString() --------------------> String

Here we saw various conversions between String, Wrapper Object, and Primitive Value in Java. Each step serves a specific purpose and facilitates flexible data manipulation within the Java ecosystem.

Partial Hierarchy of java.lang package

  • Object:
    • String, StringBuffer, StringBuilder, Number, Boolean, Character, Void, …
  • Number:
    • Byte, Short, Integer, Long, Float, Double
Java
Object ──┬─ String
         ├─ StringBuffer
         ├─ StringBuilder
         ├─ Number ──┬─ Byte
         │           ├─ Short
         │           ├─ Integer
         │           ├─ Long
         │           ├─ Float
         │           └─ Double
         ├─ Boolean
         ├─ Character
         └─ Void

Conclusions:

  1. Wrapper classes which are not child classes of Number are Boolean and Character.
  2. Wrapper classes which are not direct child classes of Object are Byte, Short, Integer, Long, Float, Double.
  3. Final wrapper classes are String, StringBuffer, and StringBuilder.
  4. In addition to the String class, all wrapper class objects are also immutable.
  5. Sometimes Void class is also considered a wrapper class.

Void class

  • It is a final class and is the direct child class of Object. It doesn’t contain any methods and contains only one variable, Void.TYPE.
  • In general, we can use the Void class in reflection to check whether any method return type is void or not.
Java
if(getMethod("m1").getReturnType() == Void.TYPE)

Void is the class representation of the void keyword in Java.

Conclusion

Wrapper classes in the java.lang package play a crucial role in Java programming, providing a bridge between primitive types and objects. They offer a range of features and utilities, including conversion, nullability, and seamless integration with collections and generics. While wrapper classes enhance the flexibility and expressiveness of Java code, developers should be mindful of their performance implications in certain contexts. By understanding the nuances of wrapper classes, developers can leverage them effectively to write robust and maintainable Java applications.

In conclusion, wrapper classes are integral components of the Java language, empowering developers to work with primitive types in an object-oriented manner. Their versatility and utility make them indispensable in various programming scenarios, enriching the Java ecosystem with enhanced expressiveness and functionality.

java.lang.Object Class in Java

Exploring the java.lang.Object Class in Java: A Comprehensive Guide

In the realm of Java programming, the java.lang.Object class holds a position of utmost importance. It serves as the root of the class hierarchy, making it a foundational element of the Java language. Understanding the Object class is crucial for every Java developer, as it forms the basis for many core concepts and functionalities within the language.

Understanding the java.lang Package

For any Java program, the most commonly required classes and interfaces are grouped together into a separate single package known as the java.lang package. We are not required to import the java.lang package because all classes and interfaces in this package are by default available to all Java classes.

Java Object Class (java.lang.Object)

The most commonly required methods for any Java class are grouped into a single class called the Object class. It is also the root class for any Java class. If any Java class does not extend any other Java class, then it is a direct child class of the Object class; otherwise, it is an indirect child class of the Object class. When it is an indirect child of the Object class, it is multi-level inheritance, not multiple inheritance.

For example, class A extends B i.e., A --> B --> Object. This is multilevel inheritance and not multiple inheritance.

Whether directly or indirectly, Java does not support multiple inheritance with respect to Java classes.

The Object class defines the following 11 methods:

  • public String toString()
  • public native int hashCode()
  • public boolean equals(Object o)
  • protected native Object clone() throws CloneNotSupportedException
  • protected void finalize() throws Throwable
  • public final Class getClass()
  • public final void wait() throws InterruptedException
  • public final native void wait(long ms) throws InterruptedException
  • public final native void wait(long ms, int ns) throws InterruptedException
  • public native final void notify()
  • public native final void notifyAll()

Note: The Object class contains a 12th method, but it is for internal purposes for the JVM: private static native void registerNatives.

toString(): This method is used to get the string representation of an object. If our class does not contain toString(), then the toString() method of the Object class will be executed. The Object class toString() method is: public String toString(){return getClass().getName() + "@" + Integer.toHexString(hashCode());}. For example, classname@hashcode-in-hexadecimal-form, i.e., Student@1888759.

Based on our requirement, we can override toString() in any Java class, and it is highly recommended. For example: public String toString(){return name + "..." + rollno;}.

hashCode(): Based on the object’s address, the JVM will generate a unique number called the hash code of that object. It doesn’t mean the object’s address is the hash code of that object. The JVM will generate a hash code for hashing-related data structures like HashTable, HashMap, HashSet to store objects into buckets based on their hash codes so that searching for that object becomes very efficient. As we know, the number one searching algorithm is hashing, and its time complexity is O(1).

Java
public native int hashCode(); 

We can override the hashCode() method of the Object class according to our requirement. Suppose we want to override hashCode() in the Student class; then, it will return the unique roll number of the student. This is the proper way to implement the hashCode method.

equals(): For example, obj1.equals(obj2).

If our class doesn’t contain equals(), then the equals() method of the Object class will be executed. The equals() method of the Object class is built for reference comparison, not for content comparison.

For example:

Java
Student s1 = new Student("amol", 101);   
Student s2 = new Student("softAai", 102); 
Student s3 = new Student("amol", 101); 
Student s4 = s1;

System.out.println(s1.equals(s2)); // false, as the Object class equals() is built for reference comparison
System.out.println(s1.equals(s3)); // false, even though the content is equal, the references are different
System.out.println(s1.equals(s4)); // true, as both references are pointing to the same object, meaning the references are equal

Now, based on our requirement, we can override the equals() method of the Object class for content comparison into our own class. But while doing this, we need to take care of the following things:

  • Determine whether we are comparing the whole content (student name, roll number) or only specific content (only roll number), and implement our equals() accordingly.
  • Ensure that if we pass a different object, it will not raise a ClassCastException, and if we pass null, it won’t raise a NullPointerException. In both cases, we need to handle them by returning false.

In the case of Strings

Java
String s1 = new String("softAai");
String s2 = new String("softAai");

System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

// In the case of StringBuffer:

StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");

System.out.println(sb1 == sb2); // false
System.out.println(sb1.equals(sb2)); // false

“==” is built for reference comparison. In the case of the String class, the equals() method is overridden for content comparison. Therefore, even though the objects are different, if the content is the same, equals() returns true. However, for StringBuffer, the equals() method is not overridden for content comparison, so if the objects are different, equals() returns false even if the content is the same. Its behavior is like the equals() method of the Object class, built for reference comparison.

getClass() is used to get the runtime class definition of an object so that we can access class-level properties like class name, declared methods, and constructors. The signature is: public final Class getClass(). We can utilize the Reflection API for this purpose from java.lang.reflection.*.

The finalize() method is called just before an Object is destroyed by the garbage collector to perform cleanup activities. Once the finalize method completes, the garbage collector automatically destroys that object.

Other methods like wait(), notify(), and notifyAll() are discussed in more detail in multithreading.

We can use these methods for inter-thread communication. The thread that is expecting an update is responsible for calling wait(), which immediately puts the thread into a waiting state. The thread responsible for performing the update can then call notify(). The waiting thread will receive that notification and continue its execution with those updates.

ay to create string objects from different sources such as literals, StringBuffer objects, char arrays, or byte arrays, offering flexibility in string manipulation and handling in Java.

Conclusion

Empower your Java programming journey by mastering the Object class and its myriad functionalities. With our comprehensive guide, you’ll gain a deeper understanding of Java’s foundational concepts and learn how to harness the full potential of the Object class for building robust and scalable applications. Start exploring today and elevate your Java programming skills to new heights!

string in java.lang Package

Unlocking the Power of the java.lang Package: A Comprehensive Guide on String, StringBuffer and StringBuilder

The java.lang package forms the core foundation of the Java programming language, encompassing fundamental classes and utilities essential for Java development. Within this package, crucial classes like String, StringBuffer, StringBuilder, and various primitive data types reside, offering robust functionalities for string manipulation, mathematical operations, and more. Understanding the nuances of these classes, including their differences, constructors, methods, and best practices, is vital for every Java developer. Moreover, delving into concepts such as immutability, synchronization, and method chaining within the java.lang package provides insights into efficient coding practices and performance optimization strategies.

java.lang package String Basic Concept in Java

In Java, strings are immutable. Once a string object is created, any attempt to modify it actually results in the creation of a new string object. This immutability means that the original object remains unchanged. Let’s dive into the distinction between immutable and mutable objects.

Consider the following snippet:

Java
String s = new String("softAai");
s.concat("Apps");
System.out.println(s); // Output: softAai
Java
s ---> softAai
       |
        ----> softAaiApps // Here, a new object will be created with those changes, but it doesn't hold any reference in this case. So, by default, it is eligible for garbage collection.

Here clearly shows that, once we create a string object, we can’t perform any changes to the existing object. If we try to perform any change, a new object will be created with those changes. This non-changeable behavior is known as the immutability of strings.

Mutable Objects: StringBuffer

Let’s examine another scenario involving a mutable object, StringBuffer:

Java
StringBuffer sb = new StringBuffer("softAai");
sb.append("Apps");
System.out.println(sb); // Output: softAaiApps
Java
sb ---> softAaiApps

Unlike strings, StringBuffer objects in Java are mutable. This means that modifications can be made directly to the existing object without creating new ones.

Here’s what happens:

Initially, sb references the StringBuffer object containing “softAai”.

Upon calling sb.append("Apps"), the content of sb is modified to “softAaiApps” in place.

As a result, when we print sb, it displays “softAaiApps”, reflecting the changes made directly to the original object.

This mutability allows for efficient manipulation of StringBuffer objects, as they can be altered without the overhead of creating new objects.

Understanding “==” Vs .equals() in Java Strings

Let’s explore the comparison between “==” and .equals() methods in Java strings:

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
System.out.println(s1 == s2);      // Output: false
System.out.println(s1.equals(s2)); // Output: true
Java
//Note: This not output

s1 --> softAai
s2 --> softAai

In the String class, .equals() is overridden for content comparison. Hence, even though the objects are different, if the content is the same, .equals() returns true.

Here’s what’s happening:

Initially, s1 and s2 both reference separate String objects with the content “softAai”.

When we use “==” to compare s1 and s2, it checks whether they reference the same object in memory. Since s1 and s2 are distinct objects, the result is false.

However, when we use .equals(), it compares the content of the strings. In the case of String objects, the .equals() method is overridden to compare the content of the strings rather than their references. Since the content of s1 and s2 is the same, .equals() returns true.

This distinction is important: “==” checks for reference equality, while .equals() checks for content equality. In the context of strings, .equals() is typically used to compare their actual values.

Exploring “==” vs. .equals() in StringBuffer

By the way, what will happen in StringBuffer? 🤔

Let’s analyze the behavior of “==” and .equals() methods when used with StringBuffer objects:

Java
StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");
System.out.println(sb1 == sb2);         // Output: false
System.out.println(sb1.equals(sb2));    // Output: false
Java
sb1 --> softAai
sb2 --> softAai

In Java, “==” compares references, while .equals() typically compares content, but in the case of StringBuffer, .equals() does not compare content; instead, it defers to the default implementation of the equals() method in the Object class, which checks for reference equality.

Here’s what’s happening:

Initially, sb1 and sb2 reference separate StringBuffer objects with the content “durga”.

When we use “==” to compare sb1 and sb2, it checks whether they reference the same object in memory. Since sb1 and sb2 are distinct objects, the result is false.

Similarly, when we use .equals(), it compares the references of sb1 and sb2, not their content. Since they are distinct objects, .equals() returns false.

In StringBuffer, .equals() doesn’t override the behavior inherited from the Object class, so it performs reference comparison rather than content comparison. Thus, even though the content of sb1 and sb2 is the same, .equals() returns false because they refer to different objects in memory.

Understanding the String Constant Pool (SCP)

The String Constant Pool (SCP) in Java is a special memory area within the Java Virtual Machine (JVM) that stores unique string literals (sequence of characters enclosed in double quotation marks (")). When you create a string using double quotes (e.g., "softAai"), Java checks if a string with the same value already exists in the SCP. If it does, the existing string reference is returned; otherwise, a new string is created and added to the SCP.

Here’s how the String Constant Pool works:

Java
String Constant Pool (SCP)
--------------------------
|                        |
--------------------------

Initially, the SCP is empty. When we create a string literal "softAai", it’s added to the SCP:

Java
String Constant Pool (SCP)
--------------------------
|     "softAai"          |
--------------------------

If you create another string literal with the same value "softAai", Java will reuse the existing string from the SCP instead of creating a new one:

Java
String Constant Pool (SCP)
--------------------------
|     "softAai"          |
--------------------------

This behavior helps to conserve memory by avoiding the creation of duplicate string objects with the same value. However, it’s important to note that string objects created using the new keyword (e.g., new String("softAai")) are not added to the SCP, even if they have the same value as existing literals.

Case1: String Created Using ‘new’ Keyword

Java
String s = new String("softAai");

In this case, two objects are created: one in the heap area and the other in the SCP area. The variable s always points to the object in the heap.

Here’s the breakdown:

In the heap area, a new String object is created with the content “softAai”, and s references this object.

Java
s ==> softAai

In the SCP area, another String object with the content “softAai” is created. However, this object isn’t pointed to by any reference; it exists solely in the SCP.

Java
String Constant Pool (SCP)
--------------------------
|     "softAai"          |
-------------------------- 

// not pointing to any reference

So,

In the heap area: s points to the String object containing “softAai”. In the SCP area: There’s a String object with the content “softAai”, but it’s not referenced by any variable.

Case2: String Created Using String Literal (“”)

Java
String s = "softAai";

In this case, only one object is created in the SCP area, and s is always pointing to that object.

In the heap area, nothing is created.

In the SCP area:

Java
String Constant Pool (SCP)
--------------------------
|     "softAai"          |
--------------------------



s ==> softAai  // here 's' points to the String object containing "softAai"

A String object with the content “softAai” is created. s directly references this object.

In the heap area: No new objects are created because string literals (here “softAai”) are directly stored in the SCP. Therefore, there’s no need for a separate object in the heap.

Understanding Object Creation in SCP and Heap

Object creation in the SCP is optional. First, it will check if any object is present in SCP with the required content. If an object is already present, the existing object will be reused. If an existing object is not available, then a new object will be created. However, this rule is applicable only for SCP and not for the heap.

GC is not allowed to access the SCP area. Hence, even though an object doesn’t contain a reference variable, it is not eligible for GC if it is present in the SCP area. All SCP objects will be destroyed at the time of JVM shutdown.

Let’s explore the nuances of object creation in the String Constant Pool (SCP) and the heap, along with garbage collection considerations:

Consider the following examples:

Example 1

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
String s3 = "softAai";
String s4 = "softAai"

In the heap area:

Java
s1 ==> softAai  
s2 ==> softAai

s1 and s2 refer to separate String objects with the content “softAai”.

In the SCP area:

Java
s3, s4 ==> softAai

s3 and s4 both refer to the same String object containing “softAai”

Here Total 3 objects created (2 in heap, 1 in SCP)

Note: Whenever we use the new operator, a new object will be created in the heap area. Hence, there may be existing two objects in the heap area but not in SCP. That means duplicate objects are possible in the heap area but not in SCP.

Java
String s1 = new String("softAai");
s1.concat("Apps");
String s2 = s1.concat("Blogs");
s1 = s1.concat("Jobs");

System.out.println(s1); // softAaiJobs
System.out.println(s2); // softAaiBlogs

In the heap area:

Java
s1 ==> softAai  // With the new operator, a new object will be created in the heap area and one in SCP.
     ==> softAaiApps  // Due to runtime change like .concat(), a new object will be created with those new changes.
s2 ==> softAaiBlogs
s1 ==> softAaiJobs  // As reference reassignment.
  • s1 initially refers to a String object with the content “softAai”.
  • After s1.concat("Apps"), a new object “softAaiApps” is created due to the immutable nature of strings.
  • s2 refers to a String object “softAaiBlogs” created by concatenating “softAai” with “Blogs”.
  • Finally, s1 is reassigned to a new object “softAaiJobs” after concatenating “Jobs”.

In the SCP area:

Java
==> softAai  // With the new operator, a new object will be created in the heap area and one in SCP, not holding any reference here.
==> Apps  // This is a string constant, thus created in SCP.
==> Blogs
==> Jobs

Objects “softAai”, “Apps”, “Blogs”, and “Jobs” are created due to string literals and stored in the SCP.

Total 8 objects are created (4 in heap and 4 in SCP).

Note: For every string constant, one object will be placed in the SCP area. If an object is required to be created due to some runtime operation, that object will be placed only in the heap area, not in SCP.

Understanding String Constructors

Let’s explore the various constructors available in the String class in Java:

String s = new String();
  • This constructor creates an empty string object with a size of zero.
  • Example: String s = "";
String s = new String(String literal);
  • This constructor creates a string object in the heap for the given string literal.
  • Example: String s = new String("softAai");
String s = new String(StringBuffer sb);
  • This constructor creates a string object for an equivalent StringBuffer object.
  • Example:
Java
StringBuffer sb = new StringBuffer("softAai");
String s = new String(sb);
String s = new String(char[] ch);
  • This constructor creates an equivalent string object for the given char array.
  • Example:
Java
char[] ch = {'a', 'b', 'c', 'd'};
String s = new String(ch);
System.out.println(s); // Output: abcd
String s = new String(byte[] b);
  • This constructor creates an equivalent string object for the given byte array.
  • Example:
Java
byte[] b = {100, 101, 102, 103};
String s = new String(b);
System.out.println(s); // Output: defg (as a-97, b-98, c-99, d-100, similarly for e, f, g)

Output – defg –> as ASCII Code a corresponds to 97, b corresponds to 98, c corresponds to 99, and d corresponds to 10.

Here, each constructor provides a way to create string objects from different sources such as literals, StringBuffer objects, char arrays, or byte arrays, offering flexibility in string manipulation and handling in Java.

Important Methods of the String Class

Let’s delve into some essential methods provided by the String class in Java:

public char charAt(int index);
  • This method returns the character at the specified index in the given string.
  • Example:
Java
String s = "softAai";
System.out.println(s.charAt(3)); // Output: t
// System.out.println(s.charAt(30)); // Throws StringIndexOutOfBoundsException
public String concat(String s);
  • This method concatenates the specified string to the end of the invoking string.
  • Overloaded + and += operators are also meant for string concatenation purposes only.
  • Example:
Java
String s = "softAai";
s = s.concat("Apps");
s = s + "Blogs";
s += "Jobs";
System.out.println(s); // Output: softAaiAppsBlogsJobs
public boolean equals(Object o);
  • This method performs content comparison where case is important.
  • This method is an overriding version of the equals() method in the Object class.
  • Example:
Java
String s = "amol";
System.out.println(s.equals("AMOL")); // Output: false
public boolean equalsIgnoreCase(String s);
  • This method performs content comparison where case is not important.
  • Example:
Java
String s = "amol";
System.out.println(s.equals("AMOL")); // Output: false
System.out.println(s.equalsIgnoreCase("AMOL")); // Output: true

Note: In general, we can use the equalsIgnoreCase method to validate usernames where case is not important, whereas we can use the equals method to validate passwords where case is important.

public String substring(int begin);
  • This method returns a substring starting from the specified begin index to the end of the original string.
  • Example:
Java
String s = "abcdefg";
System.out.println(s.substring(3)); // Output: defg
public String substring(int begin, int end);
  • This method returns a substring starting from the begin index to the end-1 index of the original string.
  • Example:
Java
String s = "abcdefg";
System.out.println(s.substring(2, 5)); // Output: cde
public int length();
  • This method returns the number of characters present in the string.
Java
String s = "softAai";
System.out.println(s.length()); // Output: 7

Note: The length variable is applicable for arrays but not for strings, whereas the length() method is applicable for string objects but not for arrays.

public String replace(char oldCh, char newCh);
  • This method replaces all occurrences of a specified character oldCh in the string with a new character newCh and returns the resulting string.
Java
String s = "ababa";
System.out.println(s.replace('a', 'b')); // Output: bbbbb
public String toLowerCase();
  • This method returns a new string with all characters converted to lowercase.
public String toUpperCase();
  • This method returns a new string with all characters converted to uppercase.
public String trim();
  • This method removes leading and trailing whitespace from the string, but not whitespace in the middle.
public int indexOf(char ch);
  • This method returns the index of the first occurrence of the specified character in the string.
public int lastIndexOf(char ch);
  • This method returns the index of the last occurrence of the specified character in the string.
Java
String s = "ababa";
System.out.println(s.indexOf('a')); // Output: 0
System.out.println(s.lastIndexOf('a')); // Output: 4

In this example, s.indexOf('a') returns 0 because ‘a’ first occurs at index 0 in the string “ababa”. Similarly, s.lastIndexOf('a') returns 4 because ‘a’ last occurs at index 4.

String Object Operations & Memory Allocation

Let’s see how string object operations affect memory allocation in Java:

Java
String s1 = new String("softaai");
String s2 = s1.toUpperCase();
String s3 = s1.toLowerCase();

System.out.println(s1 == s2); // Output: false
System.out.println(s1 == s3); // Output: true

Here,

In the heap area:

  • s1, s3 => softaai -> s1 and s3 both refer to the same String object with the content “softaai”.
  • s2 => SOFTAAI -> s2 refers to a new String object with the content “SOFTAAI”, as it is generated by the toUpperCase() method.

In the SCP area:

  • softaai -> Only string literal “softaai” exists

Note: Because of runtime operations, if there is a change in content, then a new object will be created in the heap. If there is no change, then the existing object will be used, and no new object will be created. Whether the object is present in the heap or SCP, the rule is the same for both.

Now, continuing with the example:

Java
String s4 = s2.toLowerCase();
String s5 = s4.toUpperCase();

In the heap area:

  • s4 => softaai -> s4 refers to a new String object with the content “softaai”, as it is generated by the toLowerCase() method./h2
  • s5 => SOFTAAI -> s5 refers to a new String object with the content “SOFTAAI”, as it is generated by the toUpperCase() method.

In short:

  • String objects in the heap are created dynamically based on operations performed on existing strings.
  • String objects in the SCP are reused if the content remains the same, but new objects are created if the content changes.

This example demonstrates how string operations can lead to the creation of new string objects in the heap, while the SCP is utilized for storing string literals and reusing existing strings when possible.

String Literal Operations & Memory Allocation

Now, Let’s understand how string literal operations impact memory allocation in Java:

Java
String s1 = "softaai";
String s2 = s1.toString();

System.out.println(s1 == s2); // Output: true

String s3 = s1.toLowerCase();
String s4 = s1.toUpperCase();
String s5 = s4.toLowerCase();

In the SCP area:

  • s1, s2, s3 => softaai -> s1, s2, and s3 all refer to the same String object with the content “softaai”.

Because of runtime operations, new objects will be created in the heap area:

In the heap area:

  • s4 => SOFTAAI -> s4 refers to a new String object with the content “SOFTAAI”, created by the toUpperCase() method.
  • s5 => softaai -> s5 refers to a new String object with the content “softaai”, created by the toLowerCase() method applied to s4.

In short:

  • String objects in the SCP are reused if their content remains the same, even when calling toString() on them.
  • However, runtime operations like toUpperCase() and toLowerCase() create new string objects in the heap, even if the content remains the same.
  • When comparing strings, using == checks for reference equality, which in this case returns true because s1 and s2 refer to the same string object.

How to Create Our Own Immutable Class

We can create our own immutable class in Java by following principles.

Once we create an object, we can’t perform any changes to that object. If we try to perform any change and there is a change in content, then a new object will be created. If there is no change in the content, then the existing object will be reused. This behavior is known as immutability.

Java
final public class Test {
    private final int i;

    public Test(int i) {
        this.i = i;
    }

    public Test modify(int i) {
        if (this.i == i) {
            return this; // If content remains the same, return existing object
        } else {
            return new Test(i); // If content changes, create a new object
        }
    }

    public int getValue() {
        return i;
    }
}


//Usage

Test t1 = new Test(10);
Test t2 = t1.modify(100);
Test t3 = t1.modify(10);

In the heap area:

  • t1, t3 => object(i=10) -> t1 and t3 reference the same object with the value 10.
  • t2 => object(i=100) -> t2 references a new object with the value 100, as it was modified.

So, once we create a Test object, we can’t perform any changes to the existing object. If we try to perform any change and there is a change in content, then a new object will be created. If there is no change in the content, then the existing object will be reused.

Final vs. Immutability

It’s important to differentiate between the concepts of final and immutability in Java:

final is applicable for variables but not for objects, whereas immutability is applicable for objects but not for variables. By declaring a reference variable as final, we won’t get any immutability nature. Even though the reference variable is final, we can perform any type of change on the corresponding objects, but we can’t perform reassignment for that variable. Hence, final and immutability are different concepts.

Let’s illustrate the difference with an example:

Java
final StringBuffer sb = new StringBuffer("softAai");
sb.append("Apps");
System.out.println(sb); // Output: softAaiApps

sb = new StringBuffer("Blogs"); // Compilation Error: cannot assign a value to final variable sb

Here,

  • We declare sb as a final variable, so we cannot reassign it to a new object after initialization. However, we can modify the state of the object it references, as StringBuffer objects are mutable.
  • Although sb is final, the StringBuffer object it references is not immutable. Therefore, appending “Apps” to the StringBuffer is allowed, but reassigning sb to a new StringBuffer object is not.

In conclusion:

  • Final variable => Valid
  • Immutable variable => Invalid
  • Final object => Invalid
  • Immutable object => Valid

StringBuffer

If content is fixed and won’t change frequently, then it is recommended to use String. If content is not fixed and keeps changing frequently, then it is not recommended to use String because for every change, a new object will be created, and in this case, performance-wise, String is not a good option. To overcome this problem, it is better to use StringBuffer. The main advantage of StringBuffer over String is that all required changes are performed on the existing object only.

Constructors

StringBuffer sb = new StringBuffer();

– Creates an empty string object with a default capacity of 16. When StringBuffer reaches its maximum capacity, then a new StringBuffer object will be created with a newCapacity = (currentCapacity + 1) * 2.

Java
StringBuffer sb = new StringBuffer();
System.out.println(sb.capacity()); // Output: 16
sb.append("abcdefghijklmnop"); 
System.out.println(sb.capacity()); // Output: 16
sb.append("q");
System.out.println(sb.capacity()); // Output: 34
StringBuffer sb = new StringBuffer(int initialCapacity);

– Creates an empty StringBuffer object with the specified initialCapacity.

StringBuffer sb = new StringBuffer(String s);

– Creates a StringBuffer object for the given String with a capacity = string.length() + 16.

Java
StringBuffer sb = new StringBuffer("softAai");
System.out.println(sb.capacity());  // Output: 21

In this example, sb is initialized with the String “softAai”, and its capacity is determined as the length of the String plus 16.

Important Methods of StringBuffer

public int length();
  • This method returns the number of characters present in the StringBuffer.
public int capacity();
  • This method returns the total capacity of the StringBuffer, i.e., how many characters it can accommodate.
public char charAt(int index);
  • This method returns the character located at the specified index in the StringBuffer.
Java
StringBuffer sb = new StringBuffer("softAai");
System.out.println(sb.charAt(3)); // Output: t
public void setCharAt(int index, char ch);
  • This method replaces the character located at the specified index with the provided character.
public StringBuffer append(…);

public StringBuffer append(int i)

public StringBuffer append(long l)

public StringBuffer append(char ch)

public StringBuffer append(boolean b)

  • Here, all above methods are overloaded.

  • These overloaded methods append various data types or strings to the end of the StringBuffer.
Java
StringBuffer sb = new StringBuffer();
sb.append("PI Value is : ");
sb.append(3.14);
sb.append(" It is exactly : ");
sb.append(true);
System.out.println(sb); // Output: PI Value is : 3.14 It is exactly : true
public StringBuffer insert(int index, ...);

public StringBuffer insert(int index, String s)

public StringBuffer insert(int index, int i)

public StringBuffer insert(int index, double d)

public StringBuffer insert(int index, char ch)

public StringBuffer insert(int index, boolean b)

Here, all methods are overloaded. In the append method, characters are added at the end, whereas in the insert method, characters are added at the specified index.

  • These overloaded methods insert various data types or strings at the specified index in the StringBuffer.
Java
StringBuffer sb = new StringBuffer("abcdefgh");
sb.insert(2, "xyz");
System.out.println(sb); // Output: abxyzcdefgh
public StringBuffer delete(int begin, int end);
  • This method deletes characters from the begin index to the end-1 index in the StringBuffer.
public StringBuffer deleteCharAt(int index);
  • This method deletes the character located at the specified index in the StringBuffer.
public StringBuffer reverse();
  • This method reverses the characters in the StringBuffer.
Java
StringBuffer sb = new StringBuffer("softAai");
System.out.println(sb.reverse()); // Output: iaaftos
public void setLength(int length);
  • This method sets the length of the StringBuffer to the specified length.
Java
StringBuffer sb = new StringBuffer("aiswaryaabhi");
sb.setLength(8);
System.out.println(sb); // Output: aiswarya
public void ensureCapacity(int capacity);
  • This method increases the capacity of the StringBuffer based on the provided capacity.
Java
StringBuffer sb = new StringBuffer();
System.out.println(sb.capacity()); // Output: 16
sb.ensureCapacity(1000);
System.out.println(sb.capacity()); // Output: 1000
public void trimToSize();
  • This method deallocates extra allocated memory to trim the StringBuffer’s capacity to its current size.
Java
StringBuffer sb = new StringBuffer(1000);
sb.append("abc");
sb.trimToSize();
System.out.println(sb.capacity()); // Output: 3

StringBuilder

Every method present in StringBuffer is synchronized, hence only one thread is allowed to operate on a StringBuffer object at a time, which may cause performance problems. To handle this issue, the concept of StringBuilder was introduced in version 1.5.

StringBuffer vs StringBuilder

StringBuilder

  • Non-Synchronized: Unlike StringBuffer, StringBuilder methods are not synchronized, allowing multiple threads to operate simultaneously on StringBuilder objects.
  • Not Thread-Safe: Because of its non-synchronized nature, StringBuilder is not inherently thread-safe.
  • Lower Thread Waiting Time: The absence of synchronization leads to lower thread waiting time, enhancing performance.
  • Introduced in Java 1.5: StringBuilder was introduced in Java version 1.5.

StringBuffer

  • Synchronized: StringBuffer methods are synchronized, meaning only one thread is allowed to operate on a StringBuffer object at a time, ensuring thread safety.
  • Thread-Safe: Due to its synchronized nature, StringBuffer is thread-safe.
  • Higher Thread Waiting Time: Synchronization can lead to higher thread waiting time, potentially impacting performance.
  • Introduced in Java 1.0: StringBuffer has been present since the initial release of Java.

It’s essential to note that everything, including methods and constructors, is the same in StringBuffer and StringBuilder, except for the synchronization aspect. StringBuffer has synchronized methods, while StringBuilder does not. This distinction provides developers with options based on their specific requirements: StringBuffer for thread-safe operations and StringBuilder for improved performance in scenarios where thread safety is not a concern.

String vs StringBuffer vs StringBuilder

  • If the content is fixed and won’t change frequently, then we use the String concept.
  • If the content is not fixed and keeps changing frequently, and we need thread safety, then we use the StringBuffer concept.
  • If the content is not fixed and keeps changing frequently, and we don’t need thread safety, then we use the StringBuilder concept.

Method Chaining

Method chaining allows consecutive method calls on an object, where each method call returns the object itself or another object of the same type, enabling a fluent and concise style of coding. This concept is widely used in classes like String, StringBuffer, and StringBuilder, where many methods return the same type as the object. call another method, which forms method chaining. In method chaining, all method calls are executed from left to right.

Java
StringBuilder sb = new StringBuilder();
sb.append("softAai").append("Apps").append("Blogs").insert(2,"xyz").reverse().delete(2,10);
System.out.println(sb);  //OUTPUT - sgolBsppAiaAtfzyxos
  • Initially, we create a StringBuilder object sb.
  • We then chain multiple methods together:
    • append("softAai"): The StringBuilder now contains “softAai”.
    • append("Apps"): “Apps” is appended to the StringBuilder, resulting in “softAaiApps”.
    • append("Blogs"): “Blogs” is appended, resulting in “softAaiAppsBlogs”.
    • insert(2,"xyz"): “xyz” is inserted at index 2, resulting in “soxyzftAaiAppsBlogs”.
    • reverse(): The content of the StringBuilder is reversed, resulting in “sgolBsppAiaAtfzyxos”.
    • delete(2,10): Characters from index 2 to index 9 (10 is exclusive) are deleted, resulting in “sgaAtfzyxos”.
  • Finally, this resulting content is printed.

Conclusion

In conclusion, the java.lang package serves as the cornerstone of Java programming, housing indispensable classes and utilities pivotal for everyday development tasks. Mastery of classes like String, StringBuffer, and StringBuilder, coupled with a deep understanding of immutability, synchronization, and method chaining, empowers developers to write efficient, concise, and robust Java code. By leveraging the capabilities offered by the java.lang package and adhering to best practices outlined herein, developers can enhance their productivity, optimize application performance, and craft software solutions that stand the test of time.

Java Interface

Mastering Java Interfaces: A Comprehensive Guide for Developers

Java interfaces are an essential aspect of the Java programming language. They provide a way to achieve abstraction, multiple inheritance, and polymorphism in Java. In this comprehensive guide, we will delve into the intricacies of Java interfaces, covering their definition, purpose, usage, best practices, and examples.

What is a Java Interface?

Any Service Requirement Specification (SRS) is considered an interface. For example, the JDBC API acts as a requirement specification to develop database drivers, and database vendors are responsible for implementing this JDBC API. Servlet API is an SRS, and web server vendors implement it.

From the client’s point of view, an interface is a set of services that are expected. From the service provider’s perspective, an interface is the set of services offered to the client. Hence, an interface is a contract between the client and the service provider. For instance, the Bank ATM GUI Screen serves as a contract between the bank and the customer, with services like withdrawal, mini-statement, and balance inquiry.

Inside an interface, every method is always abstract whether we declare it explicitly or not. Therefore, an interface is considered a 100% pure abstract class. Whenever we implement an interface, we must provide implementations for every method in the java interface. If we are unable to do so, we declare the implementing class as abstract since it doesn’t contain all method implementations. Additionally, we need to define methods as public so that the next level child class can provide implementations for those methods.

Defining a Java Interface

Java interfaces are defined using the interface keyword followed by the interface name. Here’s an example of a simple interface declaration:

Java
interface Animal {
    void eat();
    void sleep();
}

In this example, the Animal interface defines two abstract methods: eat() and sleep(). Any class that implements the Animal interface must provide implementations for these methods.

Implementing Java Interface

To implement an interface in Java, a class uses the implements keyword followed by the name of the interface. Here’s an example of a class implementing the Animal interface:

Java
class Dog implements Animal {
    public void eat() {
        System.out.println("Dog is eating");
    }
    
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

In this example, the Dog class implements the Animal interface by providing implementations for the eat() and sleep() methods.

When comparing “extends” and “implements”:

a) A class can extend only one class at a time, but an interface can extend any number of interfaces. For example:

Java
interface A {}
interface B {}
interface C extends A, B {}

b) A class can extend another class and implement any number of interfaces. For instance:

Java
class A extends B implements C, D, E {}

c) An interface never implements another interface or interfaces; it only extends interfaces. Therefore, the statement “interface A implements B” is incorrect.

Consider the following scenarios:

  1. X extends Y: Both X and Y can be either classes or interfaces.
  2. X extends Y, Z: X, Y, and Z should be interfaces.
  3. X implements Y, Z: X should be a class, and Y, Z should be interfaces.
  4. X extends Y implements Z: X and Y are classes, and Z is an interface.
  5. X implements Y extends Z: This would result in a compile error because “extends” must be specified before “implements.”

Java Interface Methods

Every method present in java interface is always public and abstract, whether declared explicitly or not. Hence, inside an interface, the following method declarations are equivalent:

Java
void m1();
public void m1();
abstract void m1();
public abstract void m1();
abstract public void m1();

In an interface Interf, the method declaration would be:

Java
interface Interf {
    public abstract void m1();
}

Here the use of public and abstract:

public: This modifier is used to make the method available to every implementation class.

abstract: This modifier signifies that the implementation class is responsible for providing the implementation.

Since every method in an interface is public and abstract, the following modifiers cannot be used in an interface:

private, protected, static, final, synchronized, strictfp, native.

Java Interface Variables

To define constant values at the requirement level, we use variables inside an interface. Every variable inside an interface is always public, static, and final, regardless of whether we explicitly declare these modifiers.

public: This modifier is used to make the variable available to every implementation class.

static: This modifier allows access to the variable without needing an existing object of the implementing class.

final: This modifier ensures that if one implementation class changes the value, all other implementation classes will be affected. To prevent this, every interface variable is declared as final.

Hence, the following declarations are equivalent inside an interface:

Java
int x = 10;
public int x = 10;
static int x = 10;
final int x = 10;
public static int x = 10;
public final int x = 10;
static final int x = 10;
public static final int x = 10;

As every interface variable is public, static, and final, we cannot declare interface variables with the following modifiers:

private, protected, transient, volatile.

It is mandatory to initialize interface variables at the time of declaration; otherwise, we will encounter a compilation error. For example:

Java
interface Interf {
    int x; // CE: = expected.
}

Inside a class implementing an interface, we can access interface variables, but we cannot modify them. Attempting to do so will result in a compilation error. For example:

Java
interface Interf {
    int x = 10;
}

class Test implements Interf {
    public static void main(String args[]) {
        x = 777; // CE: cannot assign a value to final variable x
        System.out.println(x);   
    }
}

However, if we declare int x = 777; instead of x = 777;, it is perfectly valid because it acts like a local variable and not an interface variable.

Java Interface Naming Conflicts

Method Naming Conflicts:

When dealing with method naming conflicts in interfaces, the resolution depends on various factors including method signature, return type, and argument types.

Same Signature, Same Return Type

If two interfaces contain methods with the same signature and return type, then in the implementing class, we have to provide implementation for only one method.

Java
interface Left {
    public void m1();
}

interface Right {
    public void m1();
}

class Test implements Left, Right {
    public void m1() {}
}
Same Name, Different Argument Types (Overloaded Methods)

If two interfaces contain methods with the same name but different argument types, then in the implementation class, we have to provide implementation for both methods, and these methods act as overloaded methods.

Java
interface Left {
    public void m1();
}
interface Right {
    public void m1(int i);
}

class Test implements Left, Right {
    public void m1() {
        // Implementation
    }

    public void m1(int i) {
        // Implementation
    }
}
Same Signature, Different Return Types

If two interfaces contain methods with the same method signature but different return types, then it is impossible to implement both interfaces simultaneously. However, in the case of general or covariant return types, it is possible.

Java
interface Left {
    public void m1();
}

interface Right {
    public int m1();
}

// It is impossible to implement both interfaces having the same method signature but different return types.
// In the case of general or covariant return types, it is possible.
class Test implements Left, Right {
    public void m1() {}   // Impossible
    
    public int m1() {}    // Impossible
}

When facing method naming conflicts in interface implementation, understanding the differences in method signatures, return types, and argument types helps determine the appropriate approach for implementation.

Java Interface Variable Naming Conflicts

When dealing with variable naming conflicts in java interfaces, if two interfaces contain variables with the same names, attempting to use the variable directly inside an implementing class can lead to a naming conflict error, resulting in a compilation error.

Java
interface Left {
    int x = 777;
}
interface Right {
    int x = 888;
}

class Test implements Left, Right {
    public static void main(String[] args) {
        System.out.println(x); // Compilation Error: reference to x is ambiguous

        // Solution: Resolve naming conflict using interface names
        System.out.println(Left.x);  // Output: 777
        System.out.println(Right.x); // Output: 888
    }
}

To resolve this issue, you can specify the interface name along with the variable name to uniquely identify which interface’s variable you are referring to. By doing so, you can access the variables without ambiguity and prevent compilation errors.

Marker Interface

Definition and Purpose

A marker interface, also known as an ability interface or tag interface, is an interface that does not contain any methods. Instead, it serves as a marker or tag to indicate that objects implementing the interface possess certain abilities or characteristics.

Examples of marker interfaces in Java include Serializable, Cloneable, RandomAccess, and SingleThreadModel. These interfaces indicate that objects implementing them have specific abilities or properties related to serialization, cloning, random access, or single-threaded access.

Java
interface Serializable {
}
Mechanism (Without having any methods how marker interface provide some ability?)

Even though marker interfaces do not declare any methods, they provide some ability to objects by relying on the internal mechanisms of the Java Virtual Machine (JVM). When an object implements a marker interface, the JVM recognizes this during runtime and provides the associated ability or behavior to that object.

Role of JVM (Why does the JVM provide this ability?)

The JVM is responsible for providing or enabling the abilities associated with marker interfaces. This is done to simplify programming and make Java language usage more straightforward for developers. By delegating certain responsibilities to the JVM, Java programs become more concise, easier to maintain, and less prone to errors.

Custom Marker Interfaces

Yes, we can create our own marker interface, but to do that, we would need to create our own JVM that supports all existing JVM features as well as any additional features we want to implement.

Adapter Class

An adapter class is a simple Java class that implements an java interface but provides empty implementations for all the methods in the interface.

Java
interface X {
    void m1();
    void m2();
    void m3();
    // ...
    void m1000();
}

abstract class AdapterX implements X {
    public void m1() {}
    public void m2() {}
    public void m3() {}
    // ...
    public void m1000() {}
}

When we implement an interface, we are required to provide implementations for every method in the interface, whether it’s required or not. This can increase the length of code unnecessarily.

Java
class Test implements X {
    public void m3() {
        // We only want to implement this method
    }

    // However, we need to implement all methods in the interface,
    // which increases the length of code unnecessarily
    public void m1() {}
    public void m2() {}
    public void m4() {}
    // ...
    public void m1000() {}
}

The problem with the above approach is that it increases the length of code and reduces readability. We can solve this problem by extending the Adapter class. This way, we only need to provide implementations for the required methods, and we are not responsible for implementing every method present in the interface, reducing the length of code.

Java
class Test extends AdapterX {
    public void m3() {
        // Provide implementation for only this method
    }
}

class Sample extends AdapterX {
    public void m7() {
        // Provide implementation for only this method
    }
}

class Demo extends AdapterX {
    public void m1000() {
        // Provide implementation for only this method
    }
}

The concept of marker interfaces and adapter classes simplifies the complexity of programming. They are powerful utilities for programmers, making their lives simpler.

Java Interface vs Abstract Class vs Concrete Class

Java Interface:

  • Definition: An interface in Java is a blueprint of a class that defines a set of abstract methods without providing any implementation.
  • Usage: Java Interfaces are used when we have a requirement specification without any knowledge of implementation details. For example, in Java servlets, the Servlet interface is used to define the methods that all servlets must implement.
  • Characteristics:
    • Contains only method declarations, without any implementation.
    • Provides a contract that concrete classes must adhere to.
    • Supports multiple inheritance.
    • Cannot contain constructors.

Abstract Class:

  • Definition: An abstract class in Java is a class that cannot be instantiated and may contain both abstract and concrete methods.
  • Usage: Abstract classes are used when we have some knowledge about implementation, but the implementation is not complete. They serve as a partial blueprint for concrete classes. For Example, GenericServlet and HttpServlet in Java servlets. These classes provide partial implementations of the Servlet interface, leaving some methods abstract for subclasses to implement.
  • Characteristics:
    • Can contain both abstract and concrete methods.
    • May include instance variables.
    • Cannot be instantiated directly, but can be subclassed.
    • Used to define common behavior for subclasses.

Concrete Class:

  • Definition: A concrete class in Java is a class that provides complete implementation for all its methods and can be instantiated directly.
  • Usage: Concrete classes are used when we have a complete understanding of implementation details and are ready to use them in the application.For Example, A custom servlet class (MyOwnServlet) that extends HttpServlet. This class provides complete implementations for all methods required by the servlet interface and can be directly instantiated for use in a web application.
  • Characteristics:
    • Provides complete implementation for all methods defined in its parent classes or interfaces.
    • Can be instantiated directly using the new keyword.
    • Can be subclassed further.
    • Typically used to create objects and perform specific tasks in the application.

Difference Between Interface and Abstract Class

Java Interface:

  1. If we lack knowledge about implementation and possess only the requirement specification, we opt for an interface.
  2. Inside an interface, every method is inherently public and abstract, whether explicitly declared or not. Hence, an interface is considered a 100% pure abstract class.
  3. Since every interface method is always public and abstract, we cannot declare them with the modifiers private, protected, final, static, synchronized, native, and strictfp.
  4. Every variable inside an interface is always public, static, and final, whether explicitly declared as such or not.
  5. As every interface variable is always public, static, and final, we cannot declare them with the modifiers private, protected, volatile, and transient.
  6. For interface variables, it is compulsory to perform initialization at the time of variable declaration; otherwise, a compilation error will occur.
  7. Inside an interface, we cannot declare static and instance blocks.
  8. Inside an interface, we cannot declare constructors.

Abstract class:

  1. If we discuss implementation but not completely (partial implementation), we should use an abstract class.
  2. Not every method inside an abstract class needs to be public and abstract; concrete methods are also allowed.
  3. There are no restrictions on the modifiers of methods in an abstract class.
  4. Not every variable inside an abstract class needs to be public, static, and final.
  5. There are no restrictions on the modifiers of variables in an abstract class.
  6. For variables in an abstract class, there’s no requirement to perform initialization at the time of declaration, so there are no restrictions on variable declaration.
  7. Inside an abstract class, we can declare static and instance blocks.
  8. Inside an abstract class, we can declare constructors.

Loopholes

As an abstract class cannot create an object and still contains a constructor, what is the need for a constructor in an abstract class?

The constructor in an abstract class will be executed whenever a child class object is created to perform initialization of child class objects only. Some properties inherited from the parent abstract class require initialization, necessitating the presence of an abstract class constructor. Its main advantage lies in writing less code and achieving code reusability. Although we cannot create an object directly or indirectly for an abstract class, we can still perform initialization of variables.

Anyway, we can’t create objects for abstract classes and interfaces. However, an abstract class can contain a constructor, whereas an interface does not. What is the reason for this?

The main purpose of a constructor is to perform initialization of instance variables. An abstract class can contain instance variables that are required for child objects, necessitating the presence of a constructor for the abstract class. However, every variable present inside an interface is always public static final, whether explicitly declared or not. Therefore, there is no chance of existing instance variables inside an interface, and the concept of a constructor is not required.

Whenever we create a child class object, the parent class object won’t be created; instead, the parent class constructor will be executed for the child object’s purpose only.

As an interface contains only abstract methods, and even in an abstract class, we can have only abstract methods, what is the difference between them? Can we replace an interface with an abstract class?

Yes, we can replace an interface with an abstract class, but it is not considered good programming practice. This is akin to recruiting an IAS officer for a sweeping activity. However, if everything is abstract, it is highly recommended to use an interface instead. Why?

Approach 1: Abstract Class
  1. Inheritance Constraint: While extending an abstract class, it’s not possible to extend any other class, limiting the inheritance concept.
  2. Object Creation Overhead: Object creation for classes extending an abstract class might be relatively more time-consuming due to the complexity of initialization logic.
Java
// Abstract class approach

abstract class X {
}

// 1) While extending an abstract class, we are unable to extend any other class, thus missing out on the inheritance concept.

class Test extends X {
}

// 2) In this case, object creation is costly.

Test t = new Test(); // Takes 2 minutes to create an object
Approach 2: Interface
  1. Inheritance Flexibility: Implementing an interface doesn’t restrict the ability to extend other classes, allowing more flexibility in inheritance.
  2. Object Creation Efficiency: Object creation for classes implementing an interface tends to be more efficient, as there is no overhead of initializing shared properties.
Java
// Interface approach

interface X {
}

// 1) While implementing an interface, we are able to extend some other class happily, thus not missing out on the inheritance concept.

class Test implements X {
}

// 2) In this case, object creation is not costly.

Test t = new Test(); // Takes 2 seconds to create an object

In short:

  • When using an abstract class, we cannot extend any other class while extending the abstract class itself, leading to a limitation in inheritance.
  • Object creation with abstract classes might be costly.
  • Conversely, with interfaces, we can happily implement them along with extending other classes, maintaining the inheritance concept intact.
  • Object creation with interfaces is typically not costly.

Conclusion

Java interfaces are a powerful feature of the Java programming language, allowing for abstraction, multiple inheritance, and polymorphism. By defining contracts that classes must adhere to, interfaces enable flexible and modular code design. Understanding how to define, implement, and use interfaces is essential for Java developers to write clean, maintainable, and extensible code.

In this guide, we’ve covered the basics of Java interfaces, including their definition, purpose, usage, best practices, and examples. With this knowledge, you should be well-equipped to leverage interfaces effectively in your Java projects.

Modifiers in java

Mastering Java Access Modifiers: A Comprehensive Guide for Enhanced Code Control

In the world of Java programming, understanding modifiers is crucial for writing efficient and secure code. Modifiers provide additional information about classes, methods, and variables, influencing their behavior and accessibility within the program. From controlling access levels to enforcing specific characteristics, modifiers play a vital role in shaping the structure and functionality of Java applications.

In this blog, we’ll delve into the various types of modifiers in Java, exploring their uses, syntax, and best practices. Whether you’re a beginner looking to grasp the basics or an experienced developer aiming to refine your skills, this comprehensive guide will serve as a valuable resource for mastering Java modifiers.

Class Level Modifiers

When writing our own class in Java, we need to provide some extra information to the JVM, such as whether the class is accessible everywhere, if child class creation is allowed, or whether object creation is possible or not, by using appropriate modifiers.

There are a total of 12 modifiers in Java:

  1. public
  2. private
  3. <default>
  4. protected
  5. final
  6. abstract
  7. static
  8. synchronized
  9. native
  10. strictfp
  11. transient
  12. volatile

Among them, there are 8 class-level modifiers:

For top-level classes, only five are allowed: public, <default>, final, abstract, and strictfp .

For inner classes, top-level modifiers along with private, protected, and static are permissible.

For Example:

Java
private class Test {
}

Results in a compilation error: “Modifier ‘private’ not allowed here.”

Java
public class Test {
  private class Test {
  }
}

Compiles without issues because private and static are allowed for inner classes.

Java
private class Test {
  // CE: modifier 'private' not allowed here.
}

public class Test {
  private class Test {
    // code compiler without any problem because 'private' and 'static' are allowed for inner classes.
  }

  static class Test {
    // code compiler without any problem because 'static' is allowed for inner classes.
  }
}

Similarly, this compiles fine since static is permitted for inner classes.

Access Specifiers and Modifiers in Java

In C and C++ languages, public, protected, <default>, and private are considered access specifiers, while all other modifiers are regarded as access modifiers. However, this distinction does not apply to Java, where all modifiers are considered as access modifiers.

  • public: Accessible everywhere within packages; even outside packages can access it.
  • <default>: Accessible everywhere within the current package; from outside the package, it cannot be accessed. Also known as package-level access.
  • protected: Enables access within the same package and subclasses, even if they are outside the package.
  • private: Restricts access to within the class where it’s declared, preventing access from other classes.
  • final modifiers: Applicable for classes, methods, and variables.
    • Final method: Method implementation is final, not allowed to change in a child class. Compiler Error: Child class cannot override overridden method declared as final in parent.Final class: If a class is final, it cannot be extended, meaning the whole implementation cannot be changed. Inheritance is not possible.Final variable: If a variable is final, its value cannot be changed, acting as a constant.
    Note: Every method in a final class is, by default, final implicitly, but not every variable in a final class needs to be final because non-final variables can be changed.

Advantages and Disadvantages of Final Modifiers:

Advantages of using final modifiers include enhanced security and ensuring a unique implementation of a class. However, there are disadvantages such as limitations on key Object-Oriented Programming (OOP) features like inheritance (due to final classes) and polymorphism (due to final methods). Thus, it’s generally not recommended to use the final keyword unless specific requirements necessitate its usage.

Before proceeding, a very silly but extremely important question regarding these concepts comes to my mind,

What are access specifiers and access modifiers in Java, and what is the difference between them?

In Java, access specifiers and access modifiers are used to control the visibility and accessibility of classes, methods, and variables within a program. These modifiers determine which parts of a program can access a particular member (class, method, or variable) and from where.

So,

Access Specifiers: Access specifiers define the scope of a class, method, or variable.

and,

Access Modifiers: Access modifiers are a subset of access specifiers that specifically modify the behavior of classes and members. Let’s take simple example, The final modifier can be applied to classes, methods, and variables. When applied to a class, it means that the class cannot be subclassed. When applied to a method, it means that the method cannot be overridden by subclasses. When applied to a variable, it means that the variable’s value cannot be changed once initialized.

Difference between Access Specifiers and Access Modifiers:

  • Access specifiers define the visibility of a class, method, or variable (e.g., public, protected, default, private).
  • Access modifiers are a subset of access specifiers and further modify the behavior of classes and members (e.g., final, abstract).

In short, accesAbstract Classs specifiers define where a class, method, or variable can be accessed from, while access modifiers modify the behavior of classes and members. However, it’s important to note that in Java, all of these are considered as access modifiers only.

Abstract Modifier in Java

In Java, the abstract modifier is applicable only to classes and methods, not to variables.

Abstract Method

Only declaration is available but no implementation is provided for that method. Hence, abstract methods end with a semicolon (;).

Example:

Java
public abstract int getNoOfWheels();
  • The child class is responsible for providing an implementation for the parent class’s abstract method.
  • If a class contains any abstract method, then it is compulsory for that class to be declared as abstract.
  • The main advantage of declaring abstract methods in a parent class is that every child class is compulsory to implement that abstract method.

It’s crucial to note that abstract methods are solely about declaration and lack implementation. Attempting to combine abstract with modifiers that imply implementation leads to a compilation error. These illegal combinations include final, native, synchronized, static, private, and strictfp. For instance:

Java
abstract final void m1()
// Results in a compilation error: "Illegal combination of modifiers 'abstract' and 'final'"

Means, abstract methods never deal with implementation. If any modifier pertains to implementation, then such modifier combinations with abstract are illegal. We get the same Compiler Error (CE).

Abstract Class

An abstract class is used for any Java class where object creation is not allowed due to partial implementation. Such types of classes are declared with abstract methods. Instantiation of an abstract class is not possible; we cannot create an object of an abstract class. If attempted, a Compiler Error (CE) is raised: “class is abstract, cannot be instantiated.”

If a class contains at least one abstract method, then it must be declared as abstract, otherwise, a Compiler Error will be encountered. Why? Because if a class contains at least one abstract method, its implementation is not complete, and it’s not recommended to create an object of such a class. To restrict object instantiation, it’s compulsory to declare the class as abstract. Even if a class contains zero abstract methods, we can still declare the class as abstract if we don’t want object creation of that class. For example, HttpServlet is abstract but doesn’t contain any abstract methods. Similarly, every adapter class is abstract but doesn’t contain any abstract methods.

When extending an abstract class, it’s necessary to implement each and every method present in the abstract class in the child class. If this is not possible, then the child class must be declared as abstract. In such cases, the next level child class is responsible for providing the implementation.

Abstract vs. Final

  • Abstract method: Compulsory to override in child class to provide implementation. Final method cannot be overridden; hence, the combination of final and abstract is illegal for methods.
  • For final classes, we cannot create a child class, whereas for abstract classes, we should create a child class to provide implementation. Hence, the combination of final and abstract is illegal for classes.
  • An abstract class can contain a final method, whereas a final class cannot contain an abstract method.

Example:

Java
abstract class Test {
    public final void m1() {
        // Implementation
    }
}

This is allowed, as an abstract class can contain final methods.

Java
final class Test {
    public abstract void m1(); // Not allowed
}

This is not allowed, as a final class cannot contain abstract methods.

It is highly recommended to use the abstract modifier in our day-to-day programming as it promotes several Object-Oriented Programming (OOP) features like polymorphism (method overriding) and inheritance (creating child classes). However, the final modifier suppresses OOP features, so it is not recommended to use it.

strictfp (strict floating point) Modifier

The strictfp modifier can be used with classes and methods but not for variables. Usually, floating-point results vary from platform to platform, and if we want platform-independent results, we use the strictfp modifier. A strictfp method needs to follow IEEE 754 standards to achieve platform-independent results. Since strictfp always pertains to implementation and abstract doesn’t, the combination of both modifiers is illegal.

strictfp at the Class Level

If every concrete method inside a class needs to follow IEEE 754 standards when using floating point, we declare that class as strictfp. However, if the class contains some abstract methods, then that class needs to be defined as abstract. Hence, the combination of strictfp and abstract is legal at the class level but illegal at the method level.

Member-Level Modifiers (Method or Variable Level Modifiers)

In Java, member-level modifiers are used to control access to methods or variables within classes. Here’s a breakdown of the commonly used member-level modifiers:

  1. public member: If a member is declared as public, it can be accessed from anywhere, but the corresponding class should be visible. Before checking member visibility, class visibility must be considered. This means that both the class and member need to be public for access from outside the package.
  2. default member: If a member is default, it can be accessed within the current package, but it cannot be accessed from outside the package. Hence, it is also known as package-level access.
  3. private member: If a member is private, it can be accessed only within the current class; outside the class, it cannot be accessed. Abstract methods are available to child classes for providing implementation, whereas private methods are not available to child classes for implementation. Therefore, the combination of abstract and private is illegal.
  4. protected member: If a member is protected, it can be accessed within the current package and also by child classes outside the package. It is one of the most misunderstood modifiers in Java. Protected access is equivalent to default access plus access for child classes.
    • We can access protected members within the current package anywhere using either parent reference or child reference. However, outside the package, we can access them only using child reference. If attempted to access using parent reference, a Compiler Error (CE) will be raised: “member has protected access in that class”.

Visibility rules for member-level modifiers

  • Within the same class: All access modifiers (private, <default>, protected, public) are allowed.
  • From child classes of the same package: <default>, protected, and public are allowed, but private is not.
  • From non-child classes of the same package: <default>, protected, and public are allowed, but private is not.
  • From child classes of outside packages: protected is accessible using child class references only, public is allowed, and others are not.
  • From non-child classes of outside packages: Only public access is allowed; others are not.

Final Variables

In Java, final variables are those whose values cannot be changed once they are assigned. Here’s a breakdown of final variables, let’s first focusing on final instance variables:

Final instance variable

The value of a variable that varies from object to object is called an instance variable or object-level variable. For every object, a separate instance variable copy is created. For instance variables, we don’t require explicit initialization because the JVM always provides default values. However, in the case of final instance variables, the JVM won’t provide a default value. We need to provide initialization explicitly whether it’s used or not; otherwise, we will encounter a Compiler Error (CE): “variable might not have been initialized”.

There are rules for initializing final instance variables:

Rule – Before constructor completion:

Case 1: At the time of declaration:

Java
class Test {
    final int x = 10;
}

Case 2: Inside an instance block:

Java
class Test {
    final int x;
    {
        x = 10;
    }
}

Case 3: Inside a constructor:

Java
class Test {
    final int x = 10;
    Test() {
        x = 10; // CE: Cannot assign value to final variable
    }
}

Note: If we try to initialize it in any method, we’ll get a CE: “cannot assign value to final variable”.

Final Static Variable

A final static variable is used when the value of the variable does not vary from object to object. Such a variable is called a static variable or class-level variable and is not recommended to be declared as an instance variable. It needs to be declared as static, and only a single copy exists at the class level, shared by every object.

In a static variable, the JVM always provides a default value at the time of initialization. We don’t need to do it explicitly. However, in the case of a final static variable, we must perform initialization explicitly; otherwise, we’ll encounter a Compiler Error (CE): “variable might not have been initialized”.

Rule: Before class loading completion, we need to perform initialization.

Case 1: At the time of declaration:

Java
class Test {
    final static int x = 10;
}

Case 2: Inside a static block:

Java
class Test {
    final static int x;
    static {
        x = 10;
    }
}

Note: We cannot initialize a final static variable inside a method; otherwise, we will get a CE: “cannot assign value to final variable”.

Final Local Variable

In Java, a final local variable is a variable declared within a method, block, or constructor, and its value remains constant once initialized. These variables are temporary and are stored on the stack, such variables are called local or temporary or stack or automatic variables.

Key points about final local variables:

  • JVM does not provide a default value for local variables. They must be explicitly initialized before use.
  • Even if a local variable is declared as final, initialization is only required if the variable is used. Unused final local variables do not need initialization.
  • The only applicable modifier for local variables is final. Attempting to use any other modifiers like public, private, protected, static, transient, or volatile will result in a compilation error.
  • Formal parameters of a method act as local variables within that method. They can also be declared as final, in which case, reassignment within the method is not allowed.

Example of declaring a final local variable:

Java
void myMethod() {
    final int x = 10; // Declaring and initializing a final local variable
    // Other code using variable x
}

Formal parameter example:

Java
void myMethod(final int param) {
    // param acts as a final local variable within this method
    // Reassignment of param is not allowed within this method
}

Note: The only applicable modifier for a local variable is final. If we try to use any other modifier like public, private, protected, static, transient, or volatile, we will encounter a Compiler Error (CE): “illegal start of expression”. If no modifier is declared, by default, it is <default>, but this rule applies only to instance and static variables, not for local variables.

Aso, formal parameters of a method simply act as local variables of that method. Formal parameters can be declared as final. If a formal parameter is declared as final, reassignment within the method is not permitted.

Static Modifiers

Static modifiers are applicable only for methods and variables and not for top-level classes. However, in static nested inner classes, it is applicable.

  • Instance Variable: A separate copy is created for every variable. In static variables, only one copy is created at the class level and shared by every object of the class.
  • Instance Variable vs. Static Variable: Instance variables can be accessed only from the instance area and cannot be accessed directly from the static area. However, static variables can be accessed from anywhere.

Consider the following declarations and let’s determine the correct combination:

I. int x = 10; 

II. static int x = 10; 

III. public void m1() { System.out.println(x); } 

IV. public static void m1() { System.out.println(x); }

  • Correct Combinations:
    • I and III: Correct. Instance variable x is accessed within an instance method. (Instance variable and instance method)
    • II and III: Correct. Static variable x is accessed within an instance method. (Static variable and instance method)
    • II and IV: Correct. Static variable x is accessed within an static method. (Static variable and static method)
  • Incorrect Combinations:
    • I and IV: Incorrect. Compiler Error (CE): “non-static variable x cannot be referenced from a static context.”
    • I and II: Incorrect. Duplicate variable x declaration within the same class.
    • III and IV: Incorrect. Duplicate method m1() declaration within the same class.

Regarding the main method overloading

If both public static void main(String[] args) and public static void main(int[] args) are present in the same class, and sop("String[]") is called, the output will be "String[]". This is because both methods are overloaded, and the class entry point is public static void main(String[] args).

Inheritance and Static Methods

The inheritance concept is applicable to static methods, including the main method. Hence, while executing a child class, if the child class doesn’t contain any main method, then the parent class’s main method will be executed.

Java
class P {
    public static void main(String[] args) {
        System.out.println("Parent main");
    }
}

class C extends P {
    public static void main(String[] args) {
        System.out.println("Child main");
    }
}

In this case, it is method hiding but not method overriding. So, when we run P.java, the output will be “Parent main”, and when we run C.java, the output will be “Child main”.

When to Use Static and Instance Methods:

  • Use instance methods when at least a single instance variable is being used.
  • Use static methods when no instance variable is being used, regardless of whether static variables are being used or not.

Abstract Static Combination:

The combination of abstract and static is illegal for methods because abstract means only declaration without implementation, while static means only implementation.

Synchronized Modifiers

In Java, the synchronized modifier is applicable only to methods and blocks but not to variables or classes.

Key points about the synchronized modifier:

  • It is used to prevent data inconsistency problems, such as race conditions, that may occur when multiple threads operate simultaneously on the same Java object.
  • When a method or block is declared as synchronized, only one thread is allowed to execute that method or block on the given object at a time, ensuring data consistency.
  • However, using synchronized can lead to increased waiting time for threads and performance issues. Therefore, it’s not recommended to use it unless there is a specific requirement to do so.

Combining synchronized with abstract is illegal for methods because:

  • abstract methods lack implementation and only provide a declaration.
  • synchronized methods require implementation to specify the synchronized behavior.
  • Combining these modifiers is contradictory, as abstract implies a lack of implementation while synchronized implies specific implementation details.

Therefore, synchronized abstract is not allowed for methods.

Native Modifiers

The native modifier is applicable only for methods and cannot be applied anywhere else.

Methods that are implemented in non-Java languages, mostly C and C++, are called native methods or foreign methods. Native methods are used when performance is critical. For example, the hashCode method in Java is implemented using native code.

The main objectives of using native methods are:

  1. To improve performance problems.
  2. To achieve machine-level communication.
  3. To utilize already existing legacy non-Java code.
Pseudo-code for using native methods in Java:

Load native libraries:

Java
static {
    System.loadLibrary("native_library_path");
}

Declare a native method:

Java
public native void m1();

Invoke a native method:

Java
n.m1();

For Java, native method implementations are already available in other old languages like C and C++. Therefore, we are not responsible for providing the implementation, and hence native methods end with a semicolon (;).

Java
public native void m1(); // CE: Native methods cannot have a body

The combination of native and abstract is illegal for methods because native methods already have implementation in old languages like C/C++, while abstract methods don’t have.

The combination of native and strictfp is also illegal for methods because strictfp follows IEEE 754 standards, but there is no guarantee that native methods, which are implemented in C/C++, adhere to IEEE 754 standards.

The main advantage of using native methods is improved performance, but the main disadvantage is that it introduces Java dependency as we completely depend on other language implementations.

Transient Modifiers

In Java, the transient modifier is applicable only to variables and is commonly used in the context of serialization.

Key points about the transient modifier:

  • It is used to indicate that a variable should not be serialized.
  • When an object is serialized, all of its non-transient instance variables are saved to a file or sent over a network. However, transient variables are excluded from this process.
  • Transient variables are useful when there are sensitive or unnecessary fields that should not be saved during serialization. For example, fields containing passwords or temporary data.
  • When an object is deserialized, transient variables will be initialized to their default values (e.g., null for objects, 0 for numeric types).
  • Using transient achieves better security by preventing certain data from being persisted or transmitted.
  • Transient variables are typically declared as private, though this modifier is not strictly necessary for serialization purposes.

Example usage:

Java
import java.io.Serializable;

class MyClass implements Serializable {
    private transient String sensitiveData; // This variable will not be serialized

    // Other non-transient variables and methods
}

In this example, the sensitiveData variable will not be included in the serialization process, ensuring that sensitive information is not persisted or transmitted.

Volatile Modifiers

The volatile modifier is applicable only for variables and not for anywhere else.

If the value of a variable keeps changing in multiple threads, there may be a chance of a data inconsistency problem. We can solve this problem using the volatile keyword.

When a variable is declared as volatile, JVM will create a separate copy of that variable for each thread. Any modification performed by any thread is on its own copy, ensuring that it will not affect the value seen by any other thread.

The combination of final and volatile is illegal for variables because final implies that the value never changes, while volatile implies that the value keeps on changing.

Advantage: Overcoming data inconsistency problems.

Disadvantage: Creating and maintaining separate copies of variables, leading to reduced performance and increased memory usage. Therefore, volatile is deprecated and not recommended for use.

Short Modifiers Summary:

Here’s a brief revision of the modifiers mentioned:

  • Local Variable: Only applicable modifier is final.
  • Constructor: Applicable modifiers are public, private, protected, and <default>.
  • Methods: Only applicable modifier is native.
  • Variables: Applicable modifiers are volatile and transient.
  • Class (not for interface): Only applicable modifier is final.
  • Class (not for enum): Applicable modifiers are final and abstract.

Conclusion

Java modifiers serve as powerful tools for developers to enhance the functionality, security, and performance of their applications. By understanding the nuances of each modifier type and applying them appropriately, programmers can write more robust and maintainable code.

From controlling access levels with keywords like public, private, protected, and <default>, to managing synchronization and serialization with volatile and transient, modifiers offer a wide range of capabilities to meet various programming needs.

As you continue your journey in Java development, remember to leverage modifiers effectively, striking the right balance between accessibility, security, and performance. With practice and a solid understanding of modifiers, you’ll be well-equipped to tackle complex programming challenges and build successful Java applications.

error: Content is protected !!