Why are Wrapper Classes Needed? #
📌 What?
- Represents wrappers around primitive data types (
int
,double
, etc.) - Implemented in
java.lang
package (e.g.,Integer
,Double
)
📌 Why?
- Allows primitives to be used as objects (e.g., in collections like
ArrayList
).- Works with collections and other generic classes (
List<Integer>
,Optional<Double>
).
- Works with collections and other generic classes (
- Provides utility methods (e.g.,
Integer.parseInt()
,Double.valueOf()
). - Supports autoboxing and unboxing, making conversions easier.
import java.util.ArrayList;
import java.util.List;
public class WrapperExample {
public static void main(String[] args) {
// Cannot use List<int>
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // Autoboxing (int → Integer)
numbers.add(20);
// Unboxing (Integer → int)
int sum = numbers.get(0) + numbers.get(1);
System.out.println("Sum: " + sum);
}
}
📌 Wrapper Classes in Java
Primitive Type | Wrapper Class |
---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
📌 Example Methods in Wrapper Classes
Wrapper Class | Method | Description | Example |
---|---|---|---|
Integer |
valueOf (String s) |
Converts a string to an Integer object |
Integer num = Integer. valueOf("100"); |
Integer |
parseInt (String s) |
Converts a string to an int primitive |
int num = Integer. parseInt("100"); |
Integer |
compare (int x, int y) |
Compares two int values |
Integer. compare(10, 20); // returns -1 |
Integer |
toBinaryString (int n) |
Converts an int to a binary string |
Integer. toBinaryString(5); // "101" |
Double |
valueOf (String s) |
Converts a string to a Double object |
Double num = Double. valueOf("3.14"); |
Double |
parseDouble (String s) |
Converts a string to a double primitive |
double num = Double. parseDouble("3.14"); |
Double |
isNaN (double d) |
Checks if the value is NaN (Not a Number) | Double. isNaN(0.0 / 0.0); // true |
Boolean |
valueOf (String s) |
Converts a string to a Boolean object |
Boolean flag = Boolean. valueOf("true"); |
Boolean |
parseBoolean (String s) |
Converts a string to a boolean primitive |
boolean flag = Boolean. parseBoolean("true"); |
Character |
isDigit (char ch) |
Checks if a character is a digit | Character. isDigit('5'); // true |
Character |
toUpperCase (char ch) |
Converts a character to uppercase | Character. toUpperCase('a'); // 'A' |
Character |
toLowerCase (char ch) |
Converts a character to lowercase | Character. toLowerCase('A'); // 'a' |
📌 Things to Remember
- Autoboxing: Converts a primitive into a wrapper object automatically.
Integer num = 10; // int → Integer
- Unboxing: Converts a wrapper object back into a primitive automatically.
int value = num; // Integer → int
- Immutable: Wrapper class objects cannot be modified after creation.
Best Practices in Using Wrapper Classes #
📌 1. Creating Wrapper Objects
Use valueOf()
instead of new
(Efficient Memory Usage)
✅ Preferred:
// Uses cached objects
Integer num = Integer.valueOf(10);
❌ Avoid:
// Unnecessary object creation
Integer num = new Integer(10);
📌 2. Avoid Autoboxing in Loops (Performance Issue)
Autoboxing creates unnecessary objects, leading to performance issues.
✅ Preferred:
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // Uses int (fast)
}
//Even Faster
int sumFunctional = IntStream.range(0, 1000).sum();
❌ Avoid:
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i; // Autoboxing occurs (slow)
}
📌 3. Use parseXxx()
for String to Primitive Conversion
Avoids unnecessary object creation
✅ Preferred:
int num = Integer.parseInt("123");
❌ Avoid:
// Deprecated and inefficient
Integer num = new Integer("123");
How does Java optimize memory usage with
Integer.valueOf()
? #
📌 What?
Integer.valueOf(int)
caches commonly used integer values.- Instead of creating a new object, it reuses an existing one from the cache when possible.
📌 Why?
- Reduces memory usage by avoiding unnecessary object creation.
- Improves performance since cached objects are returned instead of allocating new memory.
📌 How?
- Java maintains a cache of
Integer
objects for values from -128 to 127. - When
Integer.valueOf(n)
is called within this range, it returns a cached object instead of creating a new one. - If the number is outside the range, a new object is created.
📌 Example: Cached vs. Non-Cached Integers
public class IntegerCacheExample {
public static void main(String[] args) {
Integer a = Integer.valueOf(100); // Cached
Integer b = Integer.valueOf(100); // Cached
System.out.println(a == b); // true (Same cached object)
Integer x = Integer.valueOf(200); // Not Cached
Integer y = Integer.valueOf(200); // Not Cached
System.out.println(x == y); // false (Different objects)
}
}
📌 Where is this cache defined?
- Java uses an internal cache inside the
Integer
class. - The cache is implemented in
IntegerCache
(an inner class ofInteger
).
Source Code (from Integer
class)
private static class IntegerCache {
static final Integer cache[];
static {
// Cache range: -128 to 127
cache = new Integer[-(-128) + 127 + 1];
for (int i = 0; i < cache.length; i++)
cache[i] = new Integer(i - 128);
}
}
This ensures efficient memory usage for frequently used integers.
Cached Values for Wrapper Classes
Wrapper Class | Cached Values |
---|---|
Byte |
All values (-128 to 127) |
Short |
-128 to 127 |
Integer |
-128 to 127 |
Long |
-128 to 127 |
Character |
0 to 127 (ASCII characters) |
Boolean |
true and false |
Float |
❌ No caching |
Double |
❌ No caching |
Why Are Wrapper Classes in Java Immutable? #
📌 What is Immutability?
- What? Immutability means an object cannot be modified after it is created.
- Why? Helps with thread safety, caching, and predictable behavior.
- How? Any modification creates a new object instead of changing the existing one.
Example of an Immutable Class in Java:
- Final class prevents subclassing.
- Private final fields prevent changes after initialization.
- No setter methods ensure immutability.
final class ImmutableExample {
private final int value;
ImmutableExample(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
📌 Wrapper Classes are Immutable
- What? Wrapper classes (
Integer
,Double
,Boolean
, etc.) are immutable in Java. - Why?
- Caching Mechanism – Frequently used values (
Integer.valueOf(10)
) are reused, reducing memory usage. - Thread Safety – Threads cannot modify shared values.
- Caching Mechanism – Frequently used values (
Example of Wrapper Class Immutability:
public class WrapperImmutableExample {
public static void main(String[] args) {
Integer x = 10;
// Creates a new Integer object,
//old one is discarded
x = x + 1;
System.out.println(x); // Output: 11
}
}
String vs StringBuffer vs StringBuilder #
📌 Using String
for Immutable Data
- What? Used for storing fixed values like constants, configuration keys, or error messages.
- Example:
public class StringExample { public static void main(String[] args) { String greeting = "Hello"; greeting += " World"; // Creates a new String object System.out.println(greeting); // Output: Hello World } }
- When to Use String class?
- ✅ Fixed values that do not change
- ❌ Avoid for frequent modifications (creates multiple objects)
📌 Using StringBuffer
for Thread-Safe Modifications
- What? A mutable string alternative that is thread-safe (synchronized).
- Why? Ensures safe string modification in multi-threaded environments.
- Example:
public class StringBufferExample { public static void main(String[] args) { StringBuffer buffer = new StringBuffer("Thread-safe"); buffer.append(" operations"); buffer.insert(0, "Ensuring "); buffer.reverse(); System.out.println(buffer); } }
- When to Use?
- ✅ Multi-threaded modifications
- ❌ Avoid in single-threaded cases (slower than
StringBuilder
)
📌 Using StringBuilder
for Fast String Modifications
- What? A mutable string alternative optimized for performance.
- Why? Faster than
StringBuffer
because it is not synchronized. - Example:
public class StringBuilderExample { public static void main(String[] args) { StringBuilder builder = new StringBuilder("Efficient"); builder.append(" string"); builder.append(" operations"); builder.delete(0, 4); // Removes "Effe" System.out.println(builder); } }
- When to Use?
- ✅ Frequent modifications in single-threaded programs
- ❌ Avoid in multi-threaded environments (not thread-safe)
📌 Avoiding Performance Issues with String Concatenation in Loops
❌ Inefficient Approach Using String
: Creates 1000+ unnecessary String objects, impacting performance.
public class StringLoopExample {
public static void main(String[] args) {
String result = "";
for (int i = 0; i < 1000; i++) {
// Creates a new String object in every iteration
result += i;
}
System.out.println(result.length());
}
}
✅ Optimized Approach Using StringBuilder
: Uses a single StringBuilder
object, significantly improving performance.
public class StringBuilderLoopExample {
public static void main(String[] args) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
result.append(i);
}
System.out.println(result.length());
}
}
📌 Comparison
Factor | String | StringBuffer | StringBuilder |
---|---|---|---|
Immutable Data | ✅ YES | ❌ NO | ❌ NOT |
Thread Safety | ✅ Thread-Safe | ✅ Thread-Safe | ❌ Not Safe |
Performance (Loops) | ❌ Slowest | ❌ Slower (due to synchronization) | ✅ Fast |
Frequent Changes | ❌ Creates new objects | ✅ Thread-Safe | ✅ Best for performance |
Why Are String Classes in Java Immutable? #
- What? The
String
class in Java is immutable, meaning its value cannot be changed after creation. - Why?
- Security – Strings are used in class loading, networking, and security keys. If mutable, an attacker could change
"password123"
to"password456"
. - Thread Safety – Since a
String
object cannot change, it can be shared safely across threads. - String Pooling Optimization – Multiple references to the same string reuse the same object, saving memory.
- Security – Strings are used in class loading, networking, and security keys. If mutable, an attacker could change
Example of String Immutability:
public class StringImmutableExample {
public static void main(String[] args) {
String s = "Hello";
s.concat(" World"); // Creates a new object
//does NOT modify `s`
System.out.println(s); // Output: Hello
}
}
- Why does
"Hello"
remain unchanged? →s.concat(" World")
creates a new String, buts
still points to"Hello"
.
How do Text Blocks Help? #
Example Without Text Blocks:
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30\n" +
"}";
Same Example With Text Blocks:
String json = """
{
"name": "John",
"age": 30
}
""";
//json ==> "{\n \"name\": \"John\",\n \"age\": 30\n}\n"
Summary:
- Java New Feature: Introduced in JDK 15
- Less Escape Sequences: No more
\n
or\"
for multi-line strings. - More Readable: Code looks like actual text formatting.
- Auto Alignment: Handles indentation automatically.
- Better for JSON, HTML, SQL: Keeps structure intact.
What is a String Pool? #
- What? A special memory area inside the heap where string literals are stored only once.
- Why? Saves memory by reusing the same
String
object instead of creating duplicates. - How? When a new string is created using double quotes (
""
), it is automatically added to the pool.- If the same string already exists, Java reuses the existing reference instead of creating a new object.
Example of String Pool Optimization:
Both s1
and s2
refer to the same "Java"
object in the pool, improving memory efficiency.
public class StringPoolExample {
public static void main(String[] args) {
String s1 = "Java"; // Stored in String Pool
String s2 = "Java"; // Reuses the same object
System.out.println(s1 == s2);
// Output: true (same reference)
}
}
Heap Memory
- When using
new String("Hello")
, a new object is always created in the heap memory, even if the same value exists in the pool. - This results in two objects: one in the heap and one in the pool (if not already present).
Example:
public class StringMemoryExample {
public static void main(String[] args) {
// Stored in String Constant Pool
String s1 = "Hello";
// Reuses the same object
String s2 = "Hello";
// Creates a new object in Heap
String s3 = new String("Hello");
// true (same reference)
System.out.println(s1 == s2);
// false (different objects)
System.out.println(s1 == s3);
}
}
🎯 Best Practice: Always prefer string literals (String str = "Hello";
) to save memory and improve performance.
How Does
intern()
Work? #
- What? Forces a string to be stored in the String Pool, even if it was created using
new
. - Why? Helps reduce duplicate objects and optimize memory.
Example of intern()
Method:
s1
is created outside the pool, buts1.intern()
moves it into the pool.s2
ands3
now share the same reference, saving memory.
public class StringInternExample {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = s1.intern(); // Moves to String Pool
String s3 = "Hello";
System.out.println(s2 == s3); // Output: true
}
}
What are the things to be careful about when comparing Strings? #
-
Using
==
instead of.equals()
==
compares references, not values.- Always use
.equals()
to check content equality.
String s1 = "Hello"; String s2 = new String("Hello"); System.out.println(s1 == s2); // false (different objects) System.out.println(s1.equals(s2)); // true (same content)
-
Case Sensitivity
equals()
is case-sensitive. UseequalsIgnoreCase()
for case-insensitive checks.
System.out.println( "hello".equalsIgnoreCase("HELLO")); // true
-
String Pool Behavior
- String literals are stored in the String Constant Pool and can be shared.
- Using
new String()
creates a new object, even if the same value exists.
String a = "Java"; String b = "Java"; // true (same object in pool) System.out.println(a == b); String c = new String("Java"); // false (different objects) System.out.println(a == c);
What Are the Best Practices with Conditionals? #
✅ Use else if
Instead of Multiple if
Statements
if (age < 18) {
System.out.println("Minor");
} else if (age < 60) {
System.out.println("Adult");
} else {
System.out.println("Senior");
}
✅ Avoid Complex Conditions
Hard to Read (Complex Condition in if
)
if ((age > 18 && hasLicense)
|| (age > 16 && hasPermit && !hasViolations)) {
System.out.println("Allowed to drive");
}
Improved Readability (Using Meaningful Variables)
boolean isAdultWithLicense = age > 18 && hasLicense;
boolean isTeenWithPermit = age > 16
&& hasPermit && !hasViolations;
if (isAdultWithLicense || isTeenWithPermit) {
System.out.println("Allowed to drive");
}
✅ Prefer Ternary Operator for Simple Cases
// ❌ Verbose
String result;
if (x > 10) result = "High"; else result = "Low";
// ✅ Use ternary
String result = (x > 10) ? "High" : "Low";
✅ Use switch
Instead of Multiple if-else
When Applicable
String result = switch (day) {
//Add other days :)
case "Monday", "Tuesday" -> "Workday";
default -> "Weekend";
};
System.out.println(result);
What Are the Best Practices with Loops? #
✅ Use Enhanced for
Loop or Functional Programming for Collections (Faster & Readable)
// ❌ Slower: Uses index-based access repeatedly
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// ✅ Faster: Uses iterator internally
for (String item : list) {
System.out.println(item);
}
// ✅ ALTERNATIVE: Functional Programming
list.forEach(System.out::println);
✅ Avoid Expensive Operations Inside Loops
// ❌ Inefficient: Calls `list.size()` in each iteration
for (int i = 0; i < list.size(); i++) {
process(list.get(i));
}
// ✅ Efficient: Store size in a variable
int size = list.size();
for (int i = 0; i < size; i++) {
process(list.get(i));
}
// ✅ ALTERNATIVE: Functional Programming
list.forEach(this::process);
✅ Use StringBuilder
Instead of String
for Concatenation
// ❌ Inefficient: Creates new String objects in each iteration
String result = "";
for (String word : words) {
result += word; // New object each time
}
// ✅ Efficient: Uses a single `StringBuilder` object
StringBuilder result = new StringBuilder();
for (String word : words) {
result.append(word);
}
✅ Use break
and continue
Wisely
// ❌ Inefficient: Loops through all elements
// even after finding the match
for (int num : numbers) {
if (num == target) {
System.out.println("Found!");
}
}
// ✅ Efficient: Stops loop once found
for (int num : numbers) {
if (num == target) {
System.out.println("Found!");
break; // Exits loop early
}
}
✅ Use Parallel Streams for Large Data Sets (Java 8+)
// ✅ Parallel processing for large lists
list.parallelStream().forEach(System.out::println);
✅ Choose the Right Loop Type
Scenario | Best Loop Type |
---|---|
Fixed number of iterations | for loop |
Loop until condition is met | while loop |
Iterating over a collection | Enhanced for loop |
Processing large datasets | parallelStream() |
What Are the Best Practices with Using Arrays? #
✅ Use Arrays.toString()
to Print Arrays
int[] myArray = {1, 2, 3, 4};
// ❌ Prints something like [I@1b6d3586
System.out.println(myArray);
// ✅ Prints: [1, 2, 3, 4]
System.out.println(Arrays.toString(myArray));
✅ Use Enhanced for
Loop or Functional Programming instead of Traditional for
Loop
for (int num : myArray) {
System.out.println(num);
}
Arrays.stream(myArray).forEach(System.out::println);
✅ Use Arrays.equals()
to Compare Arrays
if (Arrays.equals(array1, array2)) {
System.out.println("Arrays are equal");
}
//If you compare arrays using ==,
//it checks only the memory references, NOT the actual contents.
✅ Use Arrays.deepEquals()
for Multidimensional Arrays
if (Arrays.deepEquals(array1, array2)) {
System.out.println("2D Arrays are equal");
}
✅ Use Arrays.copyOf()
for Cloning
int[] original = {1, 2, 3, 4, 5};
int[] copy = Arrays.copyOf(original, original.length);
//[1, 2, 3]
int[] partialCopy = Arrays.copyOf(original, 3);
//[1, 2, 3, 4, 5, 0, 0]
int[] expanded = Arrays.copyOf(original, 7);
//Creates a new array → No modification of the original.
//Simple way to resize arrays
✅ Use System.arraycopy()
for Fast Copying of values (Faster than looping for large arrays)
//dest is an existing array
System.arraycopy(src, 0, dest, 0, src.length);
//Can copy from any index
//Faster (native implementation)
✅ Use List
Instead of Arrays When Possible
//Allows Adding/Removing Elements
List<String> list = new ArrayList<>(Arrays.asList(array));
✅ Use Streams for Advanced Array Operations
int sum = Arrays.stream(myArray).sum();
How does Local Variable Type Inference Help? (Java 10) #
Example:
// ArrayList<String> numbers = new ArrayList<>();
// No need to repeat ArrayList<String>
var numbers = new ArrayList<String>();
// Inferred as HashMap<Integer, String>
var map = new HashMap<Integer, String>();
Advantages
- Less Code: Reduces redundancy in variable declarations.
- Improves Readability: Focus on logic, not type details.
- Flexible: Works with generics, loops, and complex types.
- Compiler Safety: Still enforces strong typing at compile-time.
How do Records improve the conciseness of Java code? #
- Records eliminate boilerplate code in Java Beans by automatically generating:
- Constructor
- Getters (accessors)
toString()
,equals()
, andhashCode()
- Makes code more readable and maintainable.
Before Records (Verbose Java Class)
public class Person {
private final String name;
private final String email;
private final String phoneNumber;
public Person(
String name, String email,
String phoneNumber) {
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getPhoneNumber() {
return phoneNumber;
}
@Override
public boolean equals(Object obj) {
/* Implementation */
}
@Override
public int hashCode() {
/* Implementation */
}
@Override
public String toString() {
/* Implementation */
}
}
With Records (Concise and Readable)
public record Person(
String name, String email,
String phoneNumber) {}
//You Can Write Code like this!
Person person = new Person(
"John Doe",
"[email protected]",
"1234567890");
// Auto-generated toString()
System.out.println(person);
// No need for getName()
System.out.println(person.name());
// No need for getEmail()
System.out.println(person.email());
What are the best practices with Records? #
📌 Use Records for Immutable Data
- Best suited for DTOs (Data Transfer Objects) and configuration objects .
public record UserDTO(String username, String role) {}
📌 Add Custom Validation in Compact Constructors
- Use a compact constructor to enforce field constraints. (runs automatically during object creation, validating name. If name is null or blank, it throws an exception.)
public record Person(String name, String email) { public Person { if (name == null || name.isBlank()) { throw new IllegalArgumentException( "Name cannot be empty"); } } }
📌 Use Methods for Additional Functionality
- Records can contain instance methods for derived values.
public record Rectangle(int width, int height) { public int area() { return width * height; } }
Rectangle rect = new Rectangle(5, 10); System.out.println(rect.area()); // Output: 50
📌 Use Static Fields and Methods When Needed
- Records do NOT allow instance variables, but you can use static fields.
public record Car(String model, int year) { static String manufacturer = "Tesla"; public static String getManufacturer() { return manufacturer; } }
What are the things that you should be careful about when using Records? #
📌 Records Are Immutable
-
Fields cannot be changed after object creation.
Person p = new Person("Alice", "[email protected]"); // p.name = "Bob"; // Compilation Error
-
If a class needs mutable state or complex behavior, use a regular class instead.
public class BankAccount { private double balance; public void deposit(double amount) { balance += amount; } public void withdraw(double amount) { balance -= amount; } }
📌 Cannot Add Instance Variables
- Fields must be declared in the record header.
public record Product(String name, double price) { // private int discount; // ❌ ERROR - Cannot add instance variables }
📌 Inheritance is Not Supported
- Records extend
java.lang.Record
. - Records cannot extend other classes (but can implement interfaces).
public record Employee( String name, int id) /* extends Person */ {} // ❌ ERROR
📌 When to Use Records?
- Data Models (e.g.,
UserDTO
) - Configurations (e.g.,
AppSettings
) - Lightweight Domain Objects (e.g.,
Rectangle
,Person
)
📌 When NOT to Use Records?
- Mutable Objects (e.g.,
BankAccount
) - Complex Business Logic
- Inheritance-Based Hierarchies
📌 Good to Know: Simple Record Pattern Matching
record Course(int id, String name) {}
public static void processCourse(Object obj) {
if (obj instanceof Course(int id, String name)) {
System.out.println(
"Course ID: " + id + ", Name: " + name);
}
}
📌 Good to Know: Nested Record Pattern Matching
record Person(String name, int age) {}
record Address(String street, String city) {}
record Contact(Person person, Address address) {}
public static void processContact(Object obj) {
if (obj instanceof Contact(
Person(var name, var age),
Address(var street, var city))) {
System.out.println(name + " lives in " + city);
}
}
Summary:
- Records introduced in JDK 16
- Advanced Pattern Matching in JDK 21