Java Core Flashcards
Can you explain the difference between Thread.sleep(), wait(), and join() in Java? When would you use each one?
-
Thread.sleep(ms)
pauses the current thread for a fixed time. It does not release any locks and can be called from any context. -
wait()
is used for inter-thread communication. A thread calling wait() must own the object’s monitor (i.e., be in a synchronized block), and it releases the lock until another thread calls notify() or notifyAll() on the same object. -
join()
is important when you need to make sure that one thread completes before another can proceed.
Thread t2 = new SampleThread(1); t2.start(); LOGGER.info("Invoking join"); t2.join(); // Waits for t2 to finish LOGGER.info("Returned from join");
Can you explain the differences between heap and stack memory in Java, and how the garbage collector works with them?
Stack memory is used for storing method call frames, including local variables and function parameters. It’s thread-safe because each thread gets its own stack. Memory is allocated when a method is invoked and deallocated when it returns.
Heap memory is used to allocate objects and is shared among all threads. It is divided into:
- Young Generation, which includes Eden and Survivor spaces, where most new objects are allocated.
- Old Generation, which stores objects that have survived multiple garbage collection cycles in the Young Gen.
- (Java 7 and earlier): PermGen, for class metadata (removed in Java 8).
- (Java 8 and later): Metaspace, native memory used for class metadata.
The Garbage Collector (GC) automatically reclaims memory by removing objects that are no longer reachable. It typically uses a generational approach:
- Minor GC collects in the Young Gen.
- Major (or Full) GC collects in the Old Gen.
- GC algorithms include mark-and-sweep, copy collection, and compaction.
Can you explain the four main pillars of Object-Oriented Programming (OOP) and give a Java-specific example of each?
Encapsulation – Bundling data and behavior together and restricting access. For example:
public class Person { private String name; // hidden data public String getName() { return name; } public void setName(String name) { this.name = name; } }
Abstraction – Hiding internal implementation details and exposing only functionality. Achieved using abstract classes and interfaces.
interface Vehicle { void move(); // no implementation }
Inheritance – One class acquiring the properties of another.
class Animal {} class Dog extends Animal {}
Polymorphism – One interface, many implementations.
Compile-time: method overloading.
Runtime: method overriding.
Animal a = new Dog(); // Runtime polymorphism
What is the JIT (Just-In-Time) compiler in Java, and how does it improve performance?
The JIT (Just-In-Time) compiler is a part of the Java Virtual Machine (JVM) that improves the performance of Java applications at runtime.
How it works:
- Java code is first compiled into bytecode (.class files), which is platform-independent.
- The bytecode is interpreted by the JVM interpreter, but interpreting is slower than executing native machine code.
- JIT compiles bytecode into native machine code at runtime, enabling faster execution.
- It caches the compiled native code so that future calls to the same method can skip interpretation.
- JIT identifies “hot spots” — methods or code blocks that are executed frequently — and optimizes them.
Can you explain the difference between method overloading and method overriding in Java? What are the rules and use cases for each?
Method Overloading means having multiple methods in the same class with the same name but different parameter lists (number, type, or order). The return type can vary, but overloading cannot be done based on return type alone.
void print(String s) {} void print(int i) {}
Method Overriding happens when a subclass provides a specific implementation of a method that is already defined in its superclass or interface. The method signature must match exactly.
class Animal { void makeSound() {} } class Lion extends Animal { @Override void makeSound() { System.out.println("Roaring"); } }
Can you explain the difference between final, finally, and finalize in Java?
final:
A final variable cannot be reassigned once it is initialized.
A final method cannot be overridden by subclasses.
A final class cannot be extended.
finally:
A block that is always executed after a try or catch, regardless of whether an exception was thrown.
Typically used for cleanup actions, such as releasing resources.
finalize():
A method defined in Object class.
Called by the GC before an object is collected.
Used to perform cleanup, but it’s deprecated since Java 9 and removed in Java 18.
It is not reliable, and developers are encouraged to use try-with-resources or explicit cleanup methods instead.
What does the static keyword mean in Java? Can static methods be overridden or overloaded?
The static keyword means the member belongs to the class rather than instances.
- Static variables are shared across all instances.
- Static methods can be called without creating an instance of the class.
Static Methods Overriden?
- Static methods cannot be overridden. Instead, they are subject to method hiding. If a subclass defines a s tatic method with the same signature, the superclass method is hidden, not overridden. There’s no runtime polymorphism.
Static Methods Overloaded?
- Static methods can be overloaded, just like instance methods — as long as the parameter list is different.
What is a ClassLoader in Java?
A ClassLoader is part of the JVM that loads classes into memory dynamically at runtime when they are referenced in code.
Can you explain the difference between a shallow copy and a deep copy in Java, with examples?
A shallow copy of an object creates a new instance but copies the references to objects inside it, not the actual nested objects themselves. So, both the original and the clone share the same nested objects.
Shallow copy is usually created via Object.clone():
Person copy = (Person) original.clone(); // shares address reference
A deep copy creates a new object along with new instances of all referenced objects, ensuring complete independence between original and copied objects.
Deep copy requires manual copy or using serialization:
Person deepCopy = new Person(); deepCopy.address = new Address(original.address.city); // new address object
Why are strings immutable in Java? What are the benefits of this design choice?
Strings are immutable in Java for several important reasons:
Security – Strings are used in class loaders, network connections, and file paths. If mutable, someone could alter these after validation, leading to vulnerabilities.
Hash-based collections – Strings are commonly used as keys in maps. If a string key were mutable, its hash code might change after insertion, making it unfindable.
Thread-safety – Immutable objects can be safely shared between threads without synchronization.
String Pooling – Java maintains a pool of string literals. Immutability allows reuse without risk of shared references being modified.
Performance – Immutable strings can be cached, reused, and optimized aggressively by the JVM.
What does it mean when we say an object is immutable in Java? How can you make a class immutable?
In Java, an immutable object is one whose state cannot change after it’s created. Instead of modifying fields, you create a new object when you want to represent a different state.
To create an immutable class:
- Mark the class as final (to prevent subclassing).
- Declare all fields as private and final.
- Initialize fields only once, via constructor.
- Provide only getters (no setters).
- For mutable objects, return defensive copies.
public final class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } }
What is the String Pool in Java, and how does it relate to memory optimization?
The String Pool in Java is a special memory region within the heap where string literals are stored to optimize memory usage.
When you write a string like “hello” in code, the JVM checks if “hello” already exists in the pool.
- If yes, it reuses the reference.
- If not, it adds it to the pool.
String a = "hello"; String b = "hello"; // both refer to the same object
If you use new String(“hello”), it creates a new object in heap memory, even if “hello” already exists in the pool.
- You can call .intern() on it to store it in the pool manually.
This sharing is safe only because strings are immutable — multiple references to the same literal can’t affect each other.
What is a Singleton class in Java? What are the common ways to implement it, and what problems can arise with multithreading?
A Singleton is a design pattern that restricts a class to a single instance and provides a global point of access to it.
Implementation:
- private constructor
- private static instance attribute
- public static getInstance method
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Multithreading issues:
If two threads enter getInstance() simultaneously and instance is null, both may create separate instances.
Solutions:
- Synchronized method
- Double-check locking (DCL) with volatile
- Static Inner Class
- Enum Singleton
Why is volatile necessary in double-checked locking for singleton pattern?
- The DCL pattern works by minimizing synchronization while still being thread-safe.
- Without volatile, instruction reordering can cause the instance reference to be visible before the object is fully constructed.
- Thread A may assign memory to instance before finishing the constructor.
- Thread B sees non-null instance and starts using it — undefined behavior.
- volatile prevents this reordering and ensures:
– Writes to instance are visible to all threads
– Initialization happens-before access
Is double-checked locking still considered a best practice in Java?
- It wasn’t safe until Java 1.5 due to a broken memory model.
- Post Java 1.5+, DCL with volatile is safe and widely used.
- However, many modern alternatives are preferred:
– Static inner class holder pattern (lazy, thread-safe, no synchronization).
– Enum singleton (serialization and reflection safe). - DCL adds complexity and is error-prone — still valid but often less preferred unless performance is critical.
Can you explain the differences between String, StringBuilder, and StringBuffer in Java? When would you use each one?
String:
- Immutable and final — once created, cannot be changed.
- All operations (e.g., concatenation) return a new String.
- Thread-safe by nature due to immutability.
- Used for constants, keys, and values that don’t change.
StringBuilder:
- Mutable and not thread-safe.
- Offers better performance for string manipulation in single-threaded environments.
- Example: when building a string inside a loop.
StringBuffer:
- Mutable and thread-safe.
- All methods are synchronized, making it safe for multi-threaded string manipulations.
- Slightly slower due to synchronization overhead.
What are the key differences between abstract classes and interfaces in Java? When would you choose one over the other?
An interface defines a contract — it declares what methods a class must implement.
- A class can implement multiple interfaces.
- Starting from Java 8, interfaces can have:
– default methods (with a body)
– static methods
- From Java 9, they can even have private methods.
- Interfaces cannot have constructors or instance state (other than constants).
An abstract class can contain both abstract and concrete methods.
- Can have instance variables, constructors, and any access modifiers.
- You can only extend one class (abstract or not).
Use an interface when:
You want to define capabilities (e.g., Serializable, Comparable)
You need multiple inheritance of type.
Use an abstract class when:
You want to share code between related classes.
You want to enforce a base class structure with partial implementation.
How do you create a thread in Java?
Threads in Java can be created in two common ways:
- By extending the Thread class and overriding the run() method.
- By implementing the Runnable interface and passing it to a Thread object.
Extending Thread:
- Inherits all methods from Thread.
- Not flexible due to Java’s single inheritance.
Implementing Runnable:
- More flexible; allows the class to extend another class.
- Encourages separation of thread logic and thread control.
*Recommended approach: *prefer Runnable for better design and flexibility.
What is the difference between throw and throws in Java? Also, explain the difference between checked and unchecked exceptions.
throw:
- Used to explicitly throw an exception inside a method or block.
- Syntax: throw new SomeException();
throws:
- Declares exceptions a method might throw (used in method signature).
- Used primarily for checked exceptions to inform the caller.
Checked Exceptions:
- Subclasses of Exception, but not RuntimeException.
- Must be either caught or declared using throws.
- Examples: IOException, SQLException.
Unchecked Exceptions:
- Subclasses of RuntimeException.
- Do not require explicit handling.
- Usually represent programming errors: NullPointerException, IllegalStateException
.
Java is said to be ‘pass by value’. What does that mean, and how does it behave when we pass objects to methods?
Java is strictly pass-by-value. That means when you pass a variable to a method, a copy of the value is passed.
For primitive types, the actual value is copied. Any changes made inside the method have no effect on the original variable.
For objects, what gets copied is the reference to the object. So:
- If you change object fields, the original object is modified.
- If you reassign the object, it only changes the local copy of the reference—not the original.
This often leads to confusion, but Java does not support pass-by-reference.
Can you explain the difference between “is-a” and “has-a” relationships in Object-Oriented Programming, with Java examples?
“is-a” relationship:
- Represents inheritance (extends or implements in Java).
- A subclass is a type of the superclass.
Example: Dog extends Animal → Dog is-an Animal.
“has-a” relationship:
- Represents composition (a class contains an instance of another class).
- Used for creating complex types by combining objects.
Example: Car has-an Engine (Car contains Engine as a member).
Composition is preferred over inheritance because:
- It allows loose coupling.
- More flexible, easier to change.
- Follows “favor composition over inheritance” design principle.
What are serialization and deserialization in Java? Why would you use them?
Serialization:
The process of converting a Java object into a byte stream.
Allows object persistence (save to disk, send over network, etc.).
Deserialization:
The reverse: converting a byte stream back into a Java object.
Use Cases:
Storing objects to files or databases.
Sending objects through message queues, web services, APIs.
Working with formats like JSON, XML, or binary.
Mechanism in Java:
Java’s built-in mechanism via the Serializable interface.
Fields that are not serializable (like threads or sockets) will throw exceptions unless marked transient.
What is object cloning in Java? How does it differ from creating a new object manually, and what are the limitations?
Cloning is done via the clone()
method in Java, which belongs to the Object class.
To use it, a class must:
- Implement the Cloneable interface.
- Override the clone() method (because the default implementation is protected).
Default clone() does a shallow copy:
- Primitive fields are copied.
- Object references are copied as-is, not cloned — both objects share the same nested references.
Deep copy requires manually cloning nested objects.
Limitations:
- Cloneable is considered poorly designed (e.g., doesn’t define clone() itself).
- Not recommended by many experts — alternatives like copy constructors or factory methods are often preferred.
- Can be error-prone and hard to maintain.
What is the ‘try-with-resources’ statement in Java, and how does it improve resource management over traditional try-finally blocks?
The try-with-resources
statement in Java, introduced in Java 7, is used to automatically manage and close resources like files, streams, sockets, and database connections.
It works with any object that implements the AutoCloseable interface (or its subtype Closeable), and ensures that .close() is called automatically at the end of the try block — even if an exception is thrown.
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) { String line = reader.readLine(); System.out.println(line); } catch (IOException e) { e.printStackTrace(); }
Why is it important to override both equals() and hashCode() methods together when using objects in collections like HashMap or HashSet?
In Java, when using objects in hash-based collections like HashMap or HashSet, it’s critical to override both equals()
and hashCode()
together.
The contract is: if two objects are considered equal using equals(), then they must return the same hash code.
These collections use:
- ` hashCode() to determine which bucket to place or search in.
-
equals()` to determine if two keys or elements are actually the same.
If you violate this contract:
- You may not be able to find objects you inserted (contains() fails).
- You may accidentally store duplicates.
That’s why always overriding both methods consistently is essential for correct behavior in hash-based collections.
Can two unequal objects have the same hash code? Why?
Yes, two unequal objects can have the same hash code — this is called a hash collision.
Java allows this because:
- hashCode()
is not guaranteed to be unique — the output is a 32-bit int, so collisions are inevitable.
- In hash-based structures like HashMap or HashSet:
– hashCode()
determines the bucket.
– equals()
is then used to resolve collisions and check actual equality.
As long as equals() correctly identifies distinct objects, having the same hashCode() is acceptable.