Amol Pawar

== 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,...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
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...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
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...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
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....

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Java Class Structure and Import Statements

Java Class Structure and Import Statements: A Comprehensive Guide for Developers

Java programming offers a flexible structure for organizing classes and utilizing import statements efficiently. Let’s delve into the various aspects of class organization, naming conventions, and import statements in Java.

Java Class Organization and Naming Conventions

Any Java class can contain any number of Java classes, and we can give any name to it, whether it is the containing class name or any other user-defined name. For example:

Java
class A {
  // Class A implementation
}

class B {
  // Class B implementation
}

class C {
  // Class C implementation
}

Case 1: Here we can take any name like A.java, B.java, C.java, or amol.java, so there are no restrictions.

Case 2: If we declare a class with the public modifier, then the class name should match that of the file. For instance, in the above example, if public class B is declared, then the class name should be B.java; otherwise, we will get a compilation error: “class B is public, should be declared file name as B.java.”

Case 3: If a class contains multiple public classes, then at most one public class name should match the file name. For example, if public class B and public class C are present in the above code, then the file name should be C.java. Otherwise, we will get a compilation error: “class C is public, declare file name as C.java.”

Execution Behavior

In Java, a single source file can accommodate multiple classes, each with its main method. For example:

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

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

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

class D {
}

When running Java programs with multiple classes, the behavior varies based on the class invoked:

If the above class is named “amol.java,” it compiles with that name, but different source files are generated for each class name, such as A.java, B.java, C.java, and D.java. However, when we run with different class names, we get different output:

java A –> output: A class main

java B –> output: B class main

java C –> output: C class main

java D –> Runtime Error: NoSuchMethodError exception

java amol –> Runtime Error: NoClassDefFoundError

It is highly recommended to have only one class per source file and name the program the same as the class name. This practice improves readability and maintainability. This practice also aids in avoiding issues like the one encountered with java D. Additionally, importing statements in the class should be written so that there is no need to write the fully qualified name.

Import Statements

Import statements in Java facilitate the use of classes and packages within the code. There are two types of import statements:

  1. Explicit Import: Imports a specific class or interface -> import java.util.ArrayList;
  2. Implicit Import: Imports all classes within a package -> import java.util.*;

Resolving Class Names

While resolving class names, the compiler always gives precedence in the following manner:

  1. Explicit class import take precedence
  2. Classes present in the current working directory (default package) are considered next.
  3. Implicit class import are checked last.

We don’t require import statements for the java.lang package and the default package (CWD – current working directory).

Static Import Statements

Import statements are a concept at the compiler level and have no effect on execution time.

A key difference between the #include statement in C and the import statement in Java is their behavior:

  • The #include statement in C is a static include type, loading all required classes at the beginning, which can lead to unnecessary memory usage and reduced performance.
  • The import statement in Java is a dynamic include type, loading classes on demand or “on the fly,” reducing memory usage and improving performance.

According to Sun Microsystems, static import statements reduce code length and enhance code readability. However, some programmers argue that it decreases readability and increases complexity.

In Java, without static imports, we can use class names with static methods like Math.sqrt() and Math.random(). With static imports, we can directly use methods without the class name, by using the import static statement. For instance, import static java.lang.Math.sqrt; allows us to use sqrt(10) directly without the class name.

Java
import static java.lang.Math.sqrt;

public class Example {
  public static void main(String[] args) {
    System.out.println(sqrt(10));
  }
}

Static import statements provide a way to access static members of a class directly without qualifying the class name. This can enhance code readability but may increase complexity.

System.out.println()

The System.out.println() method in Java can be dissected as follows:

  • System: This is a class present in the java.lang package.
  • out: It’s a static variable within the System class, of the type PrintStream.
  • println(): This is a method present in the PrintStream class.

While resolving static members, in the case of static statements, the compiler will always consider the following order: 

  1. Current class static members 
  2. Explicit static import 
  3. Implicit static import. 

Also,

There are two types of static import:

  1. Explicit static import
  2. Implicit static import.

Java Package

In Java, a package is a group of interrelated classes and interfaces bundled into a single unit. The structure of a Java source file typically includes:

  • Package statement: This appears at most once and defines the package of the file’s contents.
  • Import statements: Any number of import statements can be included to import classes from other packages.
  • Class, interface, or enum declarations: There can be any number of these declarations within the source file.

Conclusion

Understanding Java class structure and import statements is crucial for writing efficient and maintainable code. By organizing classes logically and utilizing import statements effectively, developers can improve code readability and performance in Java applications.

sorting algorithms in Java

A Comprehensive Guide to Sorting Algorithms in Java

Sorting algorithms are fundamental tools in computer science used to organize data efficiently. Sorting algorithms are techniques used to rearrange elements in a list or array in a specific order. The order could be ascending or descending based on certain criteria, such as numerical value or alphabetical order. Sorting algorithms play a crucial role in...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Graphs in java

Mastering Graphs in Java: A Comprehensive Guide for Developers

Graphs are fundamental data structures in computer science and are widely used to model complex relationships between entities. They are versatile and can represent a variety of real-world scenarios such as social networks, transportation networks, and computer networks. In this blog post, we’ll delve into some essential graph algorithms implemented in Java, covering Depth-First Search...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
error: Content is protected !!