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,...
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
IntegerI = newInteger(10);inti = I; // Compiler converts Integer to int automatically by autounboxing
After compilation, the above line becomes:
Java
inti = 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:
Integer X = 10;: This line creates an Integer object X with the value 10.
Integer Y = X;: Here, a reference to the same Integer object that X points to is assigned to Y.
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.
System.out.println(X);: Prints the value of X, which is 11.
System.out.println(Y);: Prints the value of Y, which still holds the original value of X, i.e., 10.
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
Integerx = newInteger(10); // x => 10Integery = newInteger(10); // y => 10System.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
Integerx = newInteger(10); // x => 10Integery = 10; // y => 10System.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.
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.
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
Integerx = 1000; // x => 1000Integery = 1000; // y => 1000System.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
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.
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.
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).
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.
In the m1 method, the parameter type is Long.
In the main method, an int variable x is declared and initialized with the value 10.
When calling m1(x), the compiler attempts to find a suitable method to match the argument x, which is of type int.
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.
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.
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.
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
Objecto = 10; // is perfectly validNumbern = 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.
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.
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
IntegerI = newInteger("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.
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.
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.
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.
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
publicstatic 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.
Every wrapper class including the Character class contains a public static method valueOf(primitive p) to create a wrapper object for the given primitive.
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:
byteValue():
Returns the byte primitive value of the wrapper object.
Method Signature: public byte byteValue()
shortValue():
Returns the short primitive value of the wrapper object.
Method Signature: public short shortValue()
intValue():
Returns the int primitive value of the wrapper object.
Method Signature: public int intValue()
longValue():
Returns the long primitive value of the wrapper object.
Method Signature: public long longValue()
floatValue():
Returns the float primitive value of the wrapper object.
Method Signature: public float floatValue()
doubleValue():
Returns the double primitive value of the wrapper object.
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
publicstatic primitive parseXxx(String s);
Java
inti = Integer.parseInt("10");doubled = Double.parseDouble("10.5");booleanb = 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
publicstatic primitive parseXxx(String s, int radix);
Java
inti = 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
publicStringtoString();
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.
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
publicstaticStringtoString(primitive p, int radix);
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.
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.
Using valueOf(): Converts a primitive value to a wrapper object.
Java
Primitive Value ---------------- valueOf() ----------------> WrapperObject
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.
Object ──┬─ String ├─ StringBuffer ├─ StringBuilder ├─ Number ──┬─ Byte │ ├─ Short │ ├─ Integer │ ├─ Long │ ├─ Float │ └─ Double ├─ Boolean ├─ Character └─ Void
Conclusions:
Wrapper classes which are not child classes of Number are Boolean and Character.
Wrapper classes which are not direct child classes of Object are Byte, Short, Integer, Long, Float, Double.
Final wrapper classes are String, StringBuffer, and StringBuilder.
In addition to the String class, all wrapper class objects are also immutable.
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.
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 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 packageString 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.
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:
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:
//Note: This not outputs1 --> softAais2 --> 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:
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.
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
Strings = newString("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
StringConstantPool (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
Strings = "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
StringConstantPool (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:
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.
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 ==> softAaiBlogss1 ==> 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.
This constructor creates an equivalent string object for the given byte array.
Example:
Java
byte[] b = {100, 101, 102, 103};Strings = newString(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.
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.
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.
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:
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.
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:
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
finalpublicclassTest {privatefinalinti;publicTest(inti) {this.i = i; }publicTestmodify(inti) {if (this.i == i) {returnthis; // If content remains the same, return existing object } else {returnnewTest(i); // If content changes, create a new object } }publicintgetValue() {return i; }}//UsageTestt1 = newTest(10);Testt2 = t1.modify(100);Testt3 = 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
finalStringBuffersb = newStringBuffer("softAai");sb.append("Apps");System.out.println(sb); // Output: softAaiAppssb = newStringBuffer("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.
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
StringBuffersb = newStringBuffer("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
StringBuffersb = newStringBuffer();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.
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.
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 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...
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....
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
classA {// Class A implementation}classB {// Class B implementation}classC {// 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
classA {publicstaticvoidmain(Stringargs[]) {System.out.println("A class main"); }}classB {publicstaticvoidmain(Stringargs[]) {System.out.println("B class main"); }}classC {publicstaticvoidmain(Stringargs[]) {System.out.println("C class main"); }}classD {}
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:
Explicit Import: Imports a specific class or interface -> import java.util.ArrayList;
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:
Explicit class import take precedence
Classes present in the current working directory (default package) are considered next.
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.
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:
Current class static members
Explicit static import
Implicit static import.
Also,
There are two types of static import:
Explicit static import
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 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...
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...