Photo by Scott Graham on Unsplash
Table of Contents
Question 1. Is it possible to override static methods in Java?
In Java, static methods belong to the class rather than a particular instance of the class. This means that static methods are not part of the object’s instance, but rather part of the class itself. When you declare a static method in a subclass that has the same signature as a static method in the superclass, this is known as method hiding, not overriding.
Overriding in Java is a concept that applies to instance methods, where a subclass provides a specific implementation of a method that is already defined in its superclass. The version of the method that gets executed is determined at runtime, based on the object’s runtime type.
However, with static methods, the method that gets called is determined at compile time, based on the reference type. Thus, when you declare a static method with the same signature in a subclass, you are not overriding it, you are hiding the one in the superclass.The version of the static method to be called depends solely on the class type through which it is called, not the object’s runtime type.
Here’s a quick illustration:
class Superclass {
static void staticMethod() {
System.out.println("Static method in Superclass");
}
}
class Subclass extends Superclass {
static void staticMethod() {
System.out.println("Static method in Subclass");
}
}
public class Test {
public static void main(String[] args) {
Superclass obj = new Subclass();
obj.staticMethod(); // This will call the static method from Superclass, not Subclass
}
}
In this example, even though obj
is of runtime type Subclass
, the static method from Superclass
is called, demonstrating that static methods are not overridden.
Question 2. Explain the difference between final, finally, and finalize?
In Java, final
, finally
, and finalize
are three distinct keywords/concepts with different purposes:
final
- Usage: The
final
keyword can be used with variables, methods, and classes. - Purpose:
- Variables: When applied to a variable,
final
makes the variable a constant, meaning its value cannot be changed once it is initialized. - Methods: When applied to a method,
final
prevents the method from being overridden in any subclass. - Classes: When applied to a class,
final
prevents the class from being subclassed.
- Variables: When applied to a variable,
- Example:
- Variable:
final int MAX_VALUE = 5;
- Method:
public final void display() {}
- Class:
public final class MyFinalClass {}
- Variable:
finally
- Usage: The
finally
block is used in exception handling, paired with try-catch blocks. - Purpose: It defines a block of code that is always executed after the try block and any catch blocks, regardless of whether an exception was thrown or caught. It’s typically used for cleanup activities like closing file streams or database connections.
Example:
try {
// code that might throw an exception
} catch (ExceptionType name) {
// exception handler
} finally {
// code to be executed after try block ends, regardless of exception occurrence
}
finalize
- Usage: The
finalize()
method is a protected method of theObject
class. - Purpose: It is called by the garbage collector on an object when garbage collection determines that there are no more references to the object. It’s usually overridden to dispose of system resources or to perform cleanup activities before the object is garbage collected.
- Important Note: As of Java 9 and later, the
finalize()
method is deprecated due to its unpredictable behavior and performance issues. It’s recommended to use other mechanisms liketry-with-resources
orCleaner
for resource management and cleanup.
Example:
protected void finalize() throws Throwable {
// cleanup code, e.g., releasing resources super.finalize();
}
These three concepts are fundamental to Java programming, each serving its specific role in variable declaration, exception handling, and object lifecycle management.
Question 3. What is the difference between == and equals() in Java give Java example?
In Java, ==
and equals()
are used for comparison, but they serve different purposes and work in different ways.
==
Operator
- Usage: The
==
operator is used for reference comparison when dealing with objects and for value comparison when dealing with primitive types. - For Primitive Types: It compares the values for equality.
- For Object References: It compares the memory addresses or references of the objects to check if they refer to the same object in memory.
Example
int a = 10;
int b = 10;
System.out.println(a == b); // true, because the values are the same
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2); // false, because they are different objects in memory
equals()
Method
- Usage: The
equals()
method is intended for content comparison. It’s defined in theObject
class and can be overridden in derived classes to check if the current object is “equal to” the other object based on their contents. - Default Behavior: In the
Object
class, the default implementation ofequals()
uses the==
operator to compare memory addresses, so its behavior is identical to==
unless it’s overridden. - Custom Behavior: Many classes in Java, like
String
,Integer
, and other wrapper classes, overrideequals()
to provide content-based comparison. - Example:
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1.equals(str2)); // true, because the content of the objects is the same
// For custom objects, you might need to override equals()
class Person {
String name;
Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name.equals(person.name);
}
}
Person person1 = new Person("John");
Person person2 = new Person("John");
System.out.println(person1.equals(person2)); // true, if names are the same
In summary, ==
checks if two references point to the same object in memory (for objects) or if two primitive values are the same. In contrast, equals()
is used for checking if two objects have the same content, according to the logic defined in the equals()
method, which can be overridden.
Question 4. What distinguishes abstract classes from interfaces in object-oriented programming?
Abstract Classes are partially implemented classes that cannot be instantiated directly. They provide a template for other classes to extend, allowing for shared structures and behaviours.
Abstract classes can house both concrete (fully implemented) methods and abstract methods, which lack implementations and must be overridden by subclasses.
Interfaces are contracts that delineate a set of methods without detailing their implementations. Classes that implement an interface commit to implementing all its methods. Traditionally, interfaces were purely abstract, but modern OOP languages like Java now permit default and static methods within interfaces, thus enriching their utility.
Let’s dive deeper into the distinctions with practical examples:
Example 1: Abstract Class
Consider an abstract class Vehicle
that outlines shared characteristics and functionalities of vehicles:
public abstract class Vehicle {
protected String name;
public Vehicle(String name) {
this.name = name;
}
// Concrete method
public void display() {
System.out.println("Vehicle name: " + name);
}
// Abstract method
public abstract void accelerate();
}
A subclass Car
can extend Vehicle
, providing specific implementations for abstract methods:
public class Car extends Vehicle {
public Car(String name) {
super(name);
}
@Override
public void accelerate() {
System.out.println(name + " is accelerating.");
}
}
Example 2: Interface with Default and Static Methods
Now, let’s define an interface Gadget
with default and static methods:
public interface Gadget {
// Abstract method
int BATTERY_LIFE_HOURS = 24; // public, static, and final are implicit
void turnOn();
// Default method
default void reboot() {
System.out.println("Gadget is rebooting.");
}
// Static method
static void displayType() {
System.out.println("This is a Gadget.");
}
}
A class Smartphone
implements Gadget
, fulfilling the contract:
public class Smartphone implements Gadget {
@Override
public void turnOn() {
System.out.println("Smartphone is turning on.");
}
}
public class Main {
public static void main(String[] args) {
Smartphone myPhone = new Smartphone();
myPhone.turnOn(); // Outputs: Smartphone is turning on.
myPhone.reboot(); // Outputs: Gadget is rebooting.
Gadget.displayType(); // Outputs: This is a Gadget.
}
}
When to Use Each
- Use Abstract Classes when sharing code among closely related classes, leveraging both shared and unique behaviours. Abstract classes are ideal for situations where some methods should have a default implementation, and there’s a need for the class state.
- Use Interfaces to define a common behaviour that can be adopted by any class, regardless of its place in the class hierarchy. With the addition of default and static methods, interfaces offer greater flexibility, allowing for method implementations without forcing the class inheritance structure.
Please read more on Differene between Abstract Class Interface Here
https://codetechsummit.com/difference-between-abstract-class-and-interface/
Question 5: What is the distinction between String
, StringBuilder
, and StringBuffer
in Java?
In Java, String
, StringBuilder
, and StringBuffer
https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html are classes used for working with text data, but they differ significantly in their properties and use cases:
1. String
- Immutability: Once a
String
object is created, its content cannot be changed. If you try to modify it, a newString
instance is created instead. - Performance: Due to its immutable nature, operations like concatenation can be costly in performance-critical situations, as each operation results in creating a new
String
object. - Thread Safety: It is inherently thread-safe because its immutability means its state cannot be changed by multiple threads.
2. StringBuffer
- Mutability: Unlike
String
,StringBuffer
is mutable. Its content can be changed using methods likeappend()
,insert()
,delete()
, etc., without creating a new object each time. - Synchronization: It is thread-safe as most of its methods are synchronized, meaning multiple threads can use a
StringBuffer
object without corrupting its state. - Performance: While its thread safety is a benefit in multi-threaded contexts, the synchronization overhead makes it less performant compared to
StringBuilder
in single-threaded scenarios.
3. StringBuilder
- Mutability: Similar to
StringBuffer
,StringBuilder
is also mutable, allowing modifications without creating new objects. - Thread Safety: Unlike
StringBuffer
,StringBuilder
is not synchronized, which means it is not thread-safe. This lack of synchronization allows for better performance in single-threaded applications. - Performance: It offers better performance than
StringBuffer
in scenarios where thread safety is not a concern, due to the absence of synchronization overhead.
Summary
- Use
String
when you need immutability and thread safety without the need for modifying the text data. - Choose
StringBuffer
in multi-threaded scenarios where you need to modify text data safely across different threads. - Opt for
StringBuilder
in single-threaded or thread-confined scenarios where you need to modify text data without the overhead of synchronization.
Question 6: What is the Volatile Keyword?
The volatile
keyword in Java is used to indicate that a variable’s value will be modified by different threads. When a field is declared as volatile
, the Java Memory Model ensures that any thread that reads the field will see the most recent write by any thread.
The keyword addresses memory visibility issues, ensuring that changes made in one thread are immediately visible to other threads.
Please read more about volatile keyword in Java.
https://codetechsummit.com/volatile-keyword-in-java/
Question 7: Difference between a vector and array list collection?
In Java, both Vector and ArrayList are resizable-array implementations of the List interface, but they have some differences in terms of their behavior, thread-safety, and performance characteristics.
Vector:
- Vector is synchronized, which means it is thread-safe. Methods like
add()
,remove()
,get()
, etc., are synchronized, making Vector suitable for multi-threaded environments. - Vector is slower than ArrayList because of the overhead of synchronization.
- Vector has a legacy implementation and is considered to be a legacy class.
- Vector uses a synchronized
get()
andset()
method, which can cause performance issues when iterating over large data sets. - Vector allows null values to be inserted.
- In the case of Vector, the capacity is increased by doubling the current capacity when the number of elements exceeds the current capacity. The capacity of a newly created Vector is 10 by default.
Example:
import java.util.Vector;
public class VectorExample {
public static void main(String[] args) {
Vector<String> vector = new Vector<>();
vector.add("Apple");
vector.add("Banana");
vector.add("Cherry");
System.out.println("Vector elements: " + vector);
vector.remove(1);
System.out.println("Vector after removal: " + vector);
}
}
ArrayList:
- ArrayList is non-synchronized, which means it is not thread-safe. If multiple threads access an ArrayList concurrently, it must be manually synchronized.
- ArrayList is generally faster than Vector because it doesn’t have the overhead of synchronization.
- ArrayList is more widely used than Vector in modern Java programming.
- ArrayList allows null values to be inserted.
- In the case of ArrayList, the capacity is increased by roughly 50% when the number of elements exceeds the current capacity.
Example:
import java.util.ArrayList;
public class ArrayListExample {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(10);
arrayList.add(20);
arrayList.add(30);
System.out.println("ArrayList elements: " + arrayList);
arrayList.remove(1);
System.out.println("ArrayList after removal: " + arrayList);
}
}
In summary, Vector is synchronized and thread-safe, making it suitable for multi-threaded environments, but slower than ArrayList due to the overhead of synchronization.
ArrayList is non-synchronized and generally faster, but it is not thread-safe, so you need to manually synchronize it if you plan to use it in a multi-threaded environment. Modern Java programming tends to favor ArrayList over Vector due to its better performance and wider usage.
If you don’t need thread-safety, it is generally recommended to use ArrayList as it provides better performance. However, if you need thread-safety and don’t want to handle synchronization manually, Vector can be a suitable choice, but with a potential performance trade-off.
Question 8: Explain the synchronization keyword in java.
Synchronization in Java is a mechanism that allows controlling the access of multiple threads to shared resources or critical sections of code. It is used to prevent race conditions and ensure thread safety, where multiple threads try to access and modify the same shared resource concurrently, leading to unpredictable results.
In Java, synchronization is achieved using the synchronized
keyword, which can be applied to methods or code blocks. When a thread acquires a lock on a synchronized method or block, it gains exclusive access to the associated code, and other threads attempting to enter the same synchronized section are forced to wait until the lock is released.
Here’s an example that demonstrates synchronization in Java:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) {
Counter counter = new Counter();
// Thread 1
Thread threadA = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
// Thread 2
Thread threadB = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.decrement();
}
});
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, we have a Counter
class with three methods: increment()
, decrement()
, and getCount()
. All three methods are declared as synchronized
, which means that only one thread can execute these methods at a time on a given instance of the Counter
class.
In the main
method, we create two threads: threadA
and threadB
. threadA
increments the counter 10,000 times, while threadB
decrements the counter 10,000 times. Without synchronization, these threads could interleave their execution, leading to a race condition and an incorrect final count.
However, with the synchronized
keyword applied to the increment()
, decrement()
, and getCount()
methods, only one thread can access these methods at a time, ensuring that the counter is updated atomically and consistently.
After starting both threads and waiting for them to finish using the join()
method, we print the final count. Since the increment and decrement operations are synchronized, the final count should be 0, as the number of increments and decrements should be equal.
Output:
Final count: 0
By using synchronization, we have prevented race conditions and ensured that the shared resource (the count
variable) is accessed and modified in a thread-safe manner.
It’s important to note that synchronization comes with a performance cost due to the overhead of acquiring and releasing locks. Therefore, it should be used judiciously and only when necessary to protect shared resources from concurrent access by multiple threads. In cases where synchronization is not required, it’s better to avoid it for better performance.
Question 9: What is the importance or the significance of equals and hash code?
In Java, the equals()
and hashCode()
methods play crucial roles in object comparison and efficient storage and retrieval of objects in data structures, respectively. Their significance lies in the following aspects:
Equals Method (equals()
):
- The
equals()
method is used to determine whether two objects are logically equivalent or not. - It is essential for correctly implementing the contract of the
equals()
method, which states that ifx.equals(y)
returnstrue
, theny.equals(x)
must also returntrue
. - The default implementation of
equals()
in theObject
class compares the references (memory addresses) of the objects. However, in many cases, we need to override this method to compare the actual content or state of the objects. - Proper implementation of the
equals()
method is crucial for correct functioning of data structures likeHashSet
,HashMap
, and other collections that rely on object equality.
HashCode Method (hashCode()
):
- The
hashCode()
method is used to generate a unique integer value (hash code) for an object. - The hash code is typically used in hash-based data structures, such as
HashMap
,HashSet
, and hash tables, for efficient storage and retrieval of objects. - The contract of the
hashCode()
method states that ifx.equals(y)
returnstrue
, thenx.hashCode()
andy.hashCode()
must produce the same hash code value. - Proper implementation of the
hashCode()
method is essential for the efficient performance of hash-based data structures, as it allows for better distribution of objects within the underlying data structure, reducing collisions and improving lookup times.
The significance of these methods lies in the fact that they enable correct and efficient implementations of various data structures and algorithms in Java. Here are some specific scenarios where equals()
and hashCode()
are important:
- Comparing objects: The
equals()
method allows for meaningful comparison of objects based on their content or state, rather than just their memory addresses. - Unique object representation: The
hashCode()
method provides a way to generate a unique integer representation of an object, which is crucial for efficient storage and retrieval in hash-based data structures. - Collections and data structures: Proper implementation of
equals()
andhashCode()
is essential for the correct functioning of collections likeHashSet
,HashMap
, and other data structures that rely on object equality and hashing. - Caching and indexing: Many caching and indexing mechanisms rely on efficient object comparison and hashing, making
equals()
andhashCode()
crucial for their performance. - Database operations: When working with databases, the proper implementation of
equals()
andhashCode()
is important for efficient querying, indexing, and object retrieval.
It is generally recommended to override both equals()
and hashCode()
methods in a class if you need to define a custom notion of equality for objects of that class.
Additionally, whenever you override equals()
, you should also override hashCode()
to maintain the contract between the two methods, ensuring consistent behavior and efficient performance of data structures and algorithms that rely on them.