toString(), String.valueOf(), and String Casting
- toString(): A method inherited from the
Objectclass, 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 fundamentalObjectclass 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 returnstrue.
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 overridehashCode()alongsideequals()can lead to inconsistent results in hash-based collections.
String, StringBuffer, and StringBuilder
- String: Immutable; any modification creates a new
Stringinstance, 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
Stringclass is marked asfinal, preventing inheritance and modification of its core behavior. - The underlying character array storing the string value is also
finaland 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 usesStringBuilder'sappend()method. However, in loops, the compiler doesn't reuse the sameStringBuilderinstance, leading to excessive object creation and performance issues. - For repeated concatenations (especially in loops), directly use
StringBuilder'sappend()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:
- A string constant "abc" is created in the string pool (if it doesn't already exist).
- A new
Stringinstance is created in the heap memory, which copies the value from the pool. The variables1references this heap object.
Object Count for new String("a") + new String("b")
This operation results in 6 objects:
- A
StringBuilderinstance (used internally for concatenation). - A heap
Stringinstance fornew String("a"). - The string pool constant "a" (if not already present).
- A heap
Stringinstance fornew String("b"). - The string pool constant "b" (if not already present).
- A new heap
Stringinstance for the final concatenated result "ab" (created viaStringBuilder.toString()).
The intern() Method
intern()is aStringmethod 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 toStringBuilderoperations (callingappend()followed bytoString()). - 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 repeatedStringBuildercreation 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
IOExceptionandSQLException. - Error: Indicates severe JVM-level issues that are usually unrecoverable (e.g., memory overflow, stack overflow). Errors like
OutOfMemoryErrorcause 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 includeIOExceptionandSQLException. - Unchecked Exceptions: Runtime exceptions that don't require explicit handling. They are usually caused by programming errors, such as
NullPointerExceptionorArrayIndexOutOfBoundsException.
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
tryblock. Multiplecatchblocks 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
finallyblock 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).
- If the JVM terminates abruptly (e.g., via
Replacing try-catch-finally with try-with-resources
- Introduced in Java 7, try-with-resources automatically closes resources that implement
AutoCloseableorCloseable. This eliminates the need for explicitfinallyblocks 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 withextends(upper bound) orsuper(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 likeIntegerinstead). This is because generics rely onObjectas 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 ofPerson(upper bound).<? super Manager>: Allows any supertype ofManager(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.
- The code only uses methods from
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
- Class Literal:
ClassName.class(e.g.,String.class). - Object's getClass():
object.getClass()returns the runtime class of the object. - Class.forName():
Class.forName("fully.qualified.ClassName")loads the class dynamically (throwsClassNotFoundExceptionif not found). - 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 usingConstructor.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.,
@Overridechecks 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
ServiceLoaderis a Java API that implements the SPI pattern, enabling dynamic loading of service providers.- Steps to Use:
- Define a Service Interface: Create an interface that service providers will implement.
- Register Providers: Create a file in
META-INF/services/named after the fully qualified interface name, containing the fully qualified names of implementation classes. - 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?
serialVersionUIDis 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
serialVersionUIDof the deserialized object doesn't match the local class's value, aInvalidClassExceptionis thrown.
Why Isn't static serialVersionUID Serialized?
- Although
serialVersionUIDis declaredstatic, 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
serialVersionUIDwith the one stored in the serialized data (if present) to ensure compatibility.
Excluding Fields from Serialization
- Use the
transientkeyword to mark fields that shouldn't be serialized. - Transient fields are set to their default values (
nullfor objects, 0 for primitives) during deserialization, so they need to be reinitialized manually if required. - Note:
transientcan only be applied to fields, not methods or classes.
The Unsafe Class
sun.misc.Unsafeis 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
Unsafedue 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
NullPointerExceptionoccurrences.