Core Java Fundamentals & Essential Interview Questions

toString(), String.valueOf(), and String Casting

  • toString(): A method inherited from the Object class, used to convert an object to its string representation.
  • String.valueOf(): Can convert both objects and primitive data types to strings. If the input is an object, it internally invokes the object's toString() method; for primitives, it directly converts the value to a string.
  • (String) Casting: Explicit type casting that requires prior type checking with instanceof. This approach is generally not recommended due to potential ClassCastExceptions.

The hashCode() Method

  • hashCode() is a fundamental Object class method that returns an object's hash code, an integer primarily used for efficient object lookups in hash-based collections.

Object Equality with hashCode() and equals()

  • If two objects have different hash code values, they are guaranteed to be unequal.
  • Equal hash codes do not guarantee object equality (this is known as a hash collision).
  • Two objects are considered equal only if their hash codes match and the equals() method returns true.

Why Override hashCode() When Overriding equals()?

  • When checking for an object's existence (e.g., in hash collections), the JVM first compares hash codes.
  • If hash codes differ, the JVM immediately treats the objects as unequal without invoking equals(). Failing to override hashCode() alongside equals() can lead to inconsistent results in hash-based collections.

String, StringBuffer, and StringBuilder

  • String: Immutable; any modification creates a new String instance, leading to potential performance overhead for frequent changes.
  • StringBuffer: A mutable character sequence designed for thread-safe operations. It uses synchronization to ensure correctness in multi-threaded environments.
  • StringBuilder: A mutable character sequence with the same core functionality as StringBuffer, but optimized for single-threaded scenarios (non-thread-safe, resulting in better performance).

Why Is String Immutable?

  • The String class is marked as final, preventing inheritance and modification of its core behavior.
  • The underlying character array storing the string value is also final and has no exposed methods to modify its contents directly.
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** Internal storage for character data */
    private final char value[];
}

Benefits of String Immutability

  • Security: Immutable strings are safe to use as constants (e.g., passwords, configuration values) since their values can't be altered accidentally or maliciously.
  • Thread Safety: Immutable objects can be safely shared across multiple threads without synchronization.
  • Compiler Optimizations: The JVM can optimize string usage (e.g., via string pooling and constant folding) to reduce memory footprint and improve performance.

Key String Characteristics

  • Immutability: Once created, a string's value cannot be changed.
  • Comparison: Use equals() to compare string content, and the == operator to compare object references.
  • String Pool: The JVM maintains a string pool (a cache of string constants). When creating a new string, the JVM first checks the pool; if the string exists, it returns a reference to the pooled instance instead of creating a new one.

String Concatenation: + Operator vs StringBuilder

  • The + operator for strings is syntactic sugar that internally uses StringBuilder's append() method. However, in loops, the compiler doesn't reuse the same StringBuilder instance, leading to excessive object creation and performance issues.
  • For repeated concatenations (especially in loops), directly use StringBuilder's append() method for optimal performance.

equals() in Object vs String

  • Object's equals(): Compares object references (checks if both variables point to the same memory location).
  • String's equals(): Overridden to compare the actual character content of the strings, not just their references.

Object Creation with new String("abc")

This statement creates two objects:

  1. A string constant "abc" is created in the string pool (if it doesn't already exist).
  2. A new String instance is created in the heap memory, which copies the value from the pool. The variable s1 references this heap object.

Object Count for new String("a") + new String("b")

This operation results in 6 objects:

  1. A StringBuilder instance (used internally for concatenation).
  2. A heap String instance for new String("a").
  3. The string pool constant "a" (if not already present).
  4. A heap String instance for new String("b").
  5. The string pool constant "b" (if not already present).
  6. A new heap String instance for the final concatenated result "ab" (created via StringBuilder.toString()).

The intern() Method

  • intern() is a String method that adds the string to the string pool (if not already present) and returns a reference to the pooled instance.
// Creates "Java" in the string pool and assigns reference to s1
String s1 = "Java";
// Returns reference to the existing pooled "Java"
String s2 = s1.intern();
// Creates a new heap instance of "Java"
String s3 = new String("Java");
// Returns reference to the pooled "Java" instead of the heap instance
String s4 = s3.intern();

System.out.println(s1 == s2); // true (same pool reference)
System.out.println(s3 == s4); // false (heap vs pool)
System.out.println(s1 == s4); // true (same pool reference)
// Creates heap instance of "a" and pool constant "a" (if missing)
String s = new String("a");
// Pool already has "a", so no change
s.intern();
// Assigns reference to pool "a"
String s2 = "a";
System.out.println(s == s2); // false (heap vs pool)
// Concatenates to create heap "ab"; pool has no "ab" initially
String s3 = new String("a") + new String("b");
// Adds "ab" reference to pool (points to heap instance)
s3 = s3.intern();
// Assigns reference to the pooled "ab" (which points to heap s3)
String s4 = "ab";
System.out.println(s3 == s4); // true (same reference)

What Happens During String Concatenation with +?

  • At runtime, string concatenation using + is converted to StringBuilder operations (calling append() followed by toString()).
  • The compiler optimizes constant string concatenation via constant folding (e.g., "str" + "ing" becomes "string" at compile time). However, concatenation of non-constant strings (e.g., variables) can't be optimized and leads to repeated StringBuilder creation in loops.
String str1 = "str";
String str2 = "ing";
// Compiler optimizes to "string" (pool instance)
String str3 = "str" + "ing";
// Runtime creates new StringBuilder, appends str1 and str2, then converts to string (heap instance)
String str4 = str1 + str2;
// Pool instance of "string"
String str5 = "string";

System.out.println(str3 == str4); // false (pool vs heap)
System.out.println(str3 == str5); // true (same pool instance)
System.out.println(str4 == str5); // false (heap vs pool)

Exception vs Error

  • Exception: Represents runtime issues that can typically be handled by the application (e.g., invalid input, network failures). Examples include IOException and SQLException.
  • Error: Indicates severe JVM-level issues that are usually unrecoverable (e.g., memory overflow, stack overflow). Errors like OutOfMemoryError cause the JVM to terminate the program.

Checked vs Unchecked Exceptions

  • Checked Exceptions: Must be explicitly handled (caught or declared via throws) at compile time. Examples include IOException and SQLException.
  • Unchecked Exceptions: Runtime exceptions that don't require explicit handling. They are usually caused by programming errors, such as NullPointerException or ArrayIndexOutOfBoundsException.

Common Throwable Class Methods

  • getMessage(): Returns a detailed message describing the exception/error.
  • printStackTrace(): Prints the stack trace (sequence of method calls leading to the error) to the console.
  • getCause(): Retrieves the underlying cause of the exception (if any).

Using try-catch-finally

  • try: Contains code that may throw an exception.
  • catch: Catches and handles specific exceptions thrown in the try block. Multiple catch blocks can handle different exception types.
  • finally: Contains cleanup code that executes regardless of whether an exception is thrown. It's typically used to close resources like file streams or database connections.

Does finally Always Execute?

  • No. The finally block executes in most cases, but there are exceptions:
    • If the JVM terminates abruptly (e.g., via System.exit(0)).
    • If a fatal error occurs that crashes the JVM (e.g., a stack overflow).

Replacing try-catch-finally with try-with-resources

  • Introduced in Java 7, try-with-resources automatically closes resources that implement AutoCloseable or Closeable. This eliminates the need for explicit finally blocks for cleanup.
  • Example structure:
try (FileReader reader = new FileReader("file.txt")) {
    // Use the reader
} catch (IOException e) {
    // Handle exception
}
// Reader is automatically closed here

What Are Generics and Their Purpose?

  • Generics enable parameterized types, allowing classes, interfaces, and methods to operate on objects of specified types without casting.
  • Key benefits:
    • Compile-Time Type Safety: Catches type mismatches at compile time, reducing runtime errors.
    • Code Reusability: Enables writing generic code that works with multiple data types.

Ways to Use Generics

  • Generic Classes: Classes that take type parameters (e.g., ArrayList<E>).
  • Generic Interfaces: Interfaces with type parameters (e.g., List<E>).
  • Generic Methods: Methods that define their own type parameters, independent of the class's type.
  • Wildcards: Use ? to represent an unknown type, often with extends (upper bound) or super (lower bound) to restrict the type range.

Generic Type Erasure

  • Type erasure is a compiler process that removes generic type information during compilation, converting generic code to raw types.
  • This ensures backward compatibility with pre-generics Java code (versions before Java 5).

Bridge Methods in Generics

  • Bridge methods are synthetic methods generated by the compiler to handle polymorphism in generic classes and interfaces.
  • They ensure that the correct method is called when a generic subclass overrides a method from a raw type or another generic type. For example, a bridge method might cast parameters to the appropriate generic type before calling the original method.

Limitations of Generics

  • No Primitive Type Parameters: Generics can't use primitive types (e.g., int), only reference types (use wrapper classes like Integer instead). This is because generics rely on Object as the base type.
  • No Generic Arrays: Arrays require a concrete type at runtime, which isn't available due to type erasure.
  • No Instantiation of Generic Types: You can't create an instance of a generic type (e.g., new T()) because the actual type is unknown at runtime.

Wildcards in Generics

  • Wildcards (?) represent an unknown type. They are often used with bounds to restrict the allowed types:
    • <? extends Person>: Allows any subtype of Person (upper bound).
    • <? super Manager>: Allows any supertype of Manager (lower bound).

Wildcards vs Generic Type Parameters (T)

  • Wildcards (?): Represent an unknown type, used for flexibility in method parameters where the exact type isn't needed. They can be bounded but can't be used to create new instances.
  • Type Parameters (T): Represent a specific type that's defined when the class or method is used. They can be used for creating instances, returning values, and enforcing type consistency.

Unbounded Wildcards (<?>)

  • An unbounded wildcard represents any type. It's useful in scenarios where:
    • The code only uses methods from Object (e.g., printing elements of any collection).
    • A method needs to accept a generic collection without modifying its elements.

List<?> vs Raw List

  • Raw List: A list without generic type information. It's unsafe because it allows adding any object type, leading to runtime casting errors.
  • List<?>: A list of unknown type. It's read-only (you can't add elements except null) and safe for operations that don't depend on the specific type.
  • Example:
// Accepts any list type
List<?> unknownList = new ArrayList<String>();
// unknownList.add("test"); // Compile error (can't add non-null elements)

What Is Reflection?

  • Reflection is a Java API that allows programs to inspect and manipulate classes, methods, fields, and constructors at runtime, even if their types are unknown at compile time.

Common Reflection Use Cases

  • Implementing annotation processors.
  • Creating dynamic proxies (e.g., in Spring AOP).
  • Manipulating JavaBeans (e.g., setting/getting fields dynamically).
  • Building debugging tools and IDE plugins.
  • Parsing configuration files to instantiate classes dynamically.
  • Implementing dependency injection frameworks.

Pros and Cons of Reflection

  • Pros:
    • Flexibility: Enables dynamic code execution and adaptation to runtime conditions.
    • Framework Development: Essential for building frameworks that need to work with arbitrary classes (e.g., Spring, Hibernate).
  • Cons:
    • Performance Overhead: Reflective operations are slower than direct method calls due to runtime type resolution.
    • Security Risks: Can bypass access modifiers (e.g., accessing private fields), potentially violating encapsulation.
    • Reduced Readability: Reflective code is often harder to read and maintain than direct code.

Ways to Obtain Class Objects

  1. Class Literal: ClassName.class (e.g., String.class).
  2. Object's getClass(): object.getClass() returns the runtime class of the object.
  3. Class.forName(): Class.forName("fully.qualified.ClassName") loads the class dynamically (throws ClassNotFoundException if not found).
  4. ClassLoader: classLoader.loadClass("fully.qualified.ClassName") loads the class using the specified class loader.

Basic Reflection Operations

  • Get Class Object: Use any of the methods listed above.
  • Retrieve Fields: Class.getDeclaredFields() returns all fields (including private ones) of the class.
  • Retrieve Methods: Class.getDeclaredMethods() returns all methods (including private ones).
  • Instantiate Objects: Class.newInstance() (deprecated in Java 9) or using Constructor.newInstance() to create instances.
  • Invoke Methods: Use Method.invoke(object, args) to call a method dynamically.
  • Set Fields: Use Field.set(object, value) to set the value of a field (even private ones, if accessibility is enabled).

What Are Annotations?

  • Annotations are metadata that add descriptive information to code elements (classes, methods, fields, parameters). They can be processed at compile time or runtime to trigger specific behavior.

Common Annotations

  • @Override: Marks a method as overriding a superclass method (compiler checks for correctness).
  • @Deprecated: Indicates that a method or class is outdated and should no longer be used.
  • @SuppressWarnings: Suppresses specific compiler warnings (e.g., unchecked cast warnings).
  • @Autowired: Automatically injects dependencies in Spring.
  • @Component: Marks a class as a Spring-managed component.
  • @RestController: Marks a class as a RESTful web service controller.
  • @RequestMapping: Maps HTTP requests to controller methods (specifies URL and HTTP method).
  • @Transactional: Marks a method or class to be managed by Spring's transaction system.
  • @NotNull: Ensures a parameter or field is not null (used in validation frameworks).
  • Framework-specific annotations: Spring's @Service, @Repository, JUnit's @Test, @BeforeEach.

Annotation Parsing Approaches

  • Compile-Time Processing: The compiler scans annotations during compilation and takes action (e.g., @Override checks for valid overrides). This is often done using annotation processors.
  • Runtime Processing: Using reflection to read annotations at runtime and execute custom logic (e.g., Spring uses this to inject dependencies based on @Autowired).

API vs SPI

  • API (Application Programming Interface): A set of interfaces and methods that allow developers to use a library or framework's functionality. It defines how to call the functionality.
  • SPI (Service Provider Interface): A design pattern that allows third-party developers to provide implementations for a set of interfaces. It defines how to extend or plugin to a system (e.g., JDBC drivers use SPI to allow different database implementations).

Using ServiceLoader for SPI

  • ServiceLoader is a Java API that implements the SPI pattern, enabling dynamic loading of service providers.
  • Steps to Use:
  1. Define a Service Interface: Create an interface that service providers will implement.
  2. Register Providers: Create a file in META-INF/services/ named after the fully qualified interface name, containing the fully qualified names of implementation classes.
  3. Load Providers: Use ServiceLoader.load(ServiceInterface.class) to load all registered providers and iterate over them.

Example configuration file (META-INF/services/com.example.MyService):

com.example.impl.MyServiceImpl
com.example.impl.AnotherServiceImpl

Example code:

import java.util.ServiceLoader;

public class ServiceLoaderDemo {
    public static void main(String[] args) {
        ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
        for (MyService service : loader) {
            service.performAction();
        }
    }
}
interface MyService {
    void performAction();
}
class MyServiceImpl implements MyService {
    @Override
    public void performAction() {
        System.out.println("First service implementation");
    }
}
class AnotherServiceImpl implements MyService {
    @Override
    public void performAction() {
        System.out.println("Second service implementation");
    }
}

Serialization and Deserialization

  • Serialization: Converts an object's state into a byte stream for storage (e.g., in a file) or network transmission.
  • Deserialization: Reconstructs the object from the byte stream back into an in-memory instance.
  • This process is commonly used for distributed systems, caching, and persistence.

Common Serialization Protocols

  • JDK Serialization: Built-in Java serialization (requires implementing Serializable).
  • JSON: A human-readable text format widely used in web services (e.g., Jackson, Gson libraries).
  • XML: A markup language used for data exchange (e.g., JAXB).

JDK Serialization

  • To make a class serializable in Java, it must implement the marker interface java.io.Serializable (no methods to override).

What Is serialVersionUID?

  • serialVersionUID is a unique identifier used during serialization and deserialization to verify that the sender and receiver of a serialized object have compatible class definitions.
  • If the serialVersionUID of the deserialized object doesn't match the local class's value, a InvalidClassException is thrown.

Why Isn't static serialVersionUID Serialized?

  • Although serialVersionUID is declared static, it is not part of the object's serialized state. It's a class-level constant used only for version checking during deserialization.
  • The JVM compares the local class's serialVersionUID with the one stored in the serialized data (if present) to ensure compatibility.

Excluding Fields from Serialization

  • Use the transient keyword to mark fields that shouldn't be serialized.
  • Transient fields are set to their default values (null for objects, 0 for primitives) during deserialization, so they need to be reinitialized manually if required.
  • Note: transient can only be applied to fields, not methods or classes.

The Unsafe Class

  • sun.misc.Unsafe is a low-level utility class that provides direct access to memory and other unsafe operations (e.g., atomic operations, memory allocation).
  • It's not part of the public Java API and is not recommended for general use. Modern Java versions restrict access to Unsafe due to potential security and stability risks.

Java Syntactic Sugar

  • Syntactic sugar refers to language features that make code more readable and concise without adding new functionality. These features are translated to lower-level code during compilation.
  • Examples include enhanced for loops, try-with-resources, lambda expressions, and string concatenation with +.

Java 8 Key New Features

  • Default and Static Methods in Interfaces: Allows interfaces to have method implementations (default methods) and static methods, enabling backward compatibility when extending interfaces.
  • Lambda Expressions: Anonymous functions that enable functional programming, simplifying the creation of single-method interfaces (functional interfaces).
  • Stream API: Provides a functional approach to process collections, enabling parallel execution and concise data manipulation.
  • Optional Class: A container object that may or may not contain a non-null value, designed to reduce NullPointerException occurrences.

Tags: Java Basics Core Java Interview Questions String Handling in Java Java Generics Java Reflection

Posted on Mon, 11 May 2026 02:46:07 +0000 by carsale