Java common questions
1. Public vs Private access modifiers
Access modifiers in Java control the visibility of classes, interfaces, variables, and methods.
Public
-
Definition: Members (variables, methods, constructors) or classes declared
public
are accessible from any other class, regardless of the package they are in. - Purpose: To expose an API (Application Programming Interface) for other parts of the application or external libraries to use.
Private
-
Definition: Members declared
private
are accessible only within the same class in which they are declared. They are not visible to subclasses or any other class in any package. - Purpose: Encapsulation – to hide the internal implementation details of a class and protect its internal state.
Key Differences
Feature | public |
private |
---|---|---|
Visibility | Everywhere (any class, any package) | Only within the same class |
Inheritance | Public members are inherited by subclasses | Private members are not inherited |
Use Case | Defining APIs, shared constants | Hiding implementation, internal state |
Example
// In package com.example
public class BankAccount {
public String accountNumber; // Public: accessible by anyone
private double balance; // Private: only accessible within BankAccount
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Public method to deposit money
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
System.out.println("Deposited: " + amount);
}
}
// Public method to get balance (controlled access)
public double getBalance() {
// We could add security checks here before returning
return this.balance;
}
// Private helper method, not accessible outside
private void logTransaction(String message) {
System.out.println("LOG: " + message + " for account " + accountNumber);
}
public void withdraw(double amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
logTransaction("Withdrawal of " + amount); // Using private method
System.out.println("Withdrew: " + amount);
} else {
System.out.println("Withdrawal failed.");
}
}
}
// In another class, possibly another package
public class Main {
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("12345", 1000.00);
System.out.println("Account Number: " + acc1.accountNumber); // OK, accountNumber is public
// System.out.println("Balance: " + acc1.balance); // ERROR! balance is private
acc1.deposit(500.00); // OK, deposit() is public
System.out.println("Current Balance: " + acc1.getBalance()); // OK, getBalance() is public
acc1.withdraw(200.00); // OK
// acc1.logTransaction("Manual log"); // ERROR! logTransaction() is private
}
}
2. Static methods vs Instance methods
Static methods
-
Definition: Belong to the class itself, not to any specific instance (object) of the class. They are called using the class name (
ClassName.methodName()
). - Memory: Only one copy of a static method exists in memory, shared among all instances of the class (and even accessible without any instance).
-
Access:
- Can access only static variables of the class.
- Cannot access instance variables or instance methods directly (because they don't operate on a specific instance).
- Cannot use
this
orsuper
keywords.
- Purpose: Utility functions, factory methods, operations that are not tied to the state of a specific object.
Instance methods
-
Definition: Belong to an instance (object) of the class. They are called on an object (
objectReference.methodName()
). - Memory: Each instance has its own conceptual "copy" or access to these methods, which operate on that instance's data.
-
Access:
- Can access both static and instance variables of the class.
- Can access both static and instance methods.
- Can use
this
(to refer to the current instance) andsuper
(to refer to the superclass instance).
- Purpose: To operate on or query the state (instance variables) of a specific object.
Key Differences
Feature | static method |
instance method |
---|---|---|
Association | Belongs to the class | Belongs to an object (instance) |
Invocation | ClassName.methodName() |
objectReference.methodName() |
this keyword |
Cannot use this |
Can use this to refer to the current instance |
Access to Members | Can access static members directly. Cannot access instance members directly. | Can access both static and instance members directly. |
State | Generally stateless or works with static state. | Operates on the state of a specific instance. |
Example
class Dog {
private String name; // Instance variable
private static int dogCount = 0; // Static variable
public Dog(String name) {
this.name = name;
dogCount++; // Increment static counter for each new dog
}
// Instance method
public void bark() {
System.out.println(name + " says Woof!"); // Accesses instance variable 'name'
}
// Instance method that can also access static members
public void displayInfo() {
System.out.println("My name is " + this.name + ". There are " + dogCount + " dogs in total.");
}
// Static method
public static int getDogCount() {
// System.out.println(name); // ERROR! Cannot access instance variable 'name' from a static context
// bark(); // ERROR! Cannot call instance method 'bark()' from a static context
return dogCount; // Accesses static variable 'dogCount'
}
// Static utility method
public static String getSpecies() {
return "Canis familiaris";
}
}
public class Main {
public static void main(String[] args) {
// Calling static method using class name
System.out.println("Species: " + Dog.getSpecies());
System.out.println("Initial dog count: " + Dog.getDogCount());
Dog dog1 = new Dog("Buddy");
Dog dog2 = new Dog("Lucy");
// Calling instance methods using object reference
dog1.bark(); // Buddy says Woof!
dog2.bark(); // Lucy says Woof!
dog1.displayInfo(); // My name is Buddy. There are 2 dogs in total.
// Calling static method (can also be called via an instance, but not recommended)
System.out.println("Current dog count: " + Dog.getDogCount()); // Prints 2
// System.out.println(dog1.getDogCount()); // Also works, but IDE might warn: "Static member accessed via instance reference"
}
}
3. Primitive data types vs Objects
Primitive data types
- Definition: Fundamental data types predefined by Java. They are not objects.
-
Types:
byte
,short
,int
,long
,float
,double
,char
,boolean
. - Storage: Store the actual binary value directly in the memory location where the variable is allocated (stack for local variables, or part of an object's memory on the heap).
-
Default Value: Have default values (e.g.,
0
for numeric types,false
forboolean
,\u0000
forchar
) if they are instance or static variables. Local primitive variables must be initialized. -
Behavior: Cannot have methods called on them. Operations are performed using operators (e.g.,
+
,-
,*
,/
). -
Nullability: Cannot be
null
.
Objects
-
Definition: Instances of classes (user-defined or built-in like
String
,ArrayList
). They represent more complex data structures and encapsulate data (fields) and behavior (methods). - Storage: Variables of object types store a reference (memory address) to the actual object, which resides on the heap.
-
Default Value: The default value for an object reference is
null
if it's an instance or static variable. Local object references must be initialized. - Behavior: Have methods that can be called on them to perform operations or query their state.
-
Nullability: Can be
null
, meaning the reference variable does not point to any object. This can lead toNullPointerException
if not handled.
Key Differences
Feature | Primitive Data Types | Objects |
---|---|---|
Nature | Basic, fundamental types | Instances of classes, complex data structures |
Storage | Stores actual value directly | Stores a reference (address) to the data on the heap |
Memory Allocation | Stack (for locals) or direct part of object | Heap (for the object itself) |
Methods | Cannot have methods | Have methods |
Nullability | Cannot be null |
Can be null |
Default Value | Specific to type (e.g., 0, false) | null (for object references) |
Examples | int , double , char , boolean |
String , ArrayList , custom class instances |
Example
public class PrimitiveVsObject {
int instancePrimitiveInt; // Default value 0
String instanceObjectString; // Default value null
public static void main(String[] args) {
// Primitive types
int localPrimitiveInt = 10; // Stores the value 10
double localPrimitiveDouble = 20.5; // Stores the value 20.5
boolean localPrimitiveBoolean = true; // Stores the value true
char localPrimitiveChar = 'A'; // Stores the character 'A'
// int uninitializedLocalPrimitive;
// System.out.println(uninitializedLocalPrimitive); // COMPILE ERROR: variable might not have been initialized
System.out.println("Primitive int: " + localPrimitiveInt);
// localPrimitiveInt.toString(); // ERROR! Primitives don't have methods
// Objects
String objString1 = "Hello"; // objString1 stores a reference to "Hello" object on the heap
String objString2 = new String("World"); // objString2 stores a reference to a new "World" object
// String uninitializedLocalObject;
// System.out.println(uninitializedLocalObject.length()); // COMPILE ERROR: variable might not have been initialized (and then NPE if it was null)
String nullString = null;
// System.out.println(nullString.length()); // RUNTIME ERROR: NullPointerException
System.out.println("Object String 1: " + objString1);
System.out.println("Length of String 1: " + objString1.length()); // Objects have methods
PrimitiveVsObject pvo = new PrimitiveVsObject();
System.out.println("Instance primitive int default: " + pvo.instancePrimitiveInt);
System.out.println("Instance object String default: " + pvo.instanceObjectString);
}
}
4. Wrapper classes (Integer, String) vs Primitive types (int, String)
This question mixes two concepts slightly. Integer
is a wrapper class for the primitive int
. String
is an object type, not a primitive, and not a wrapper for a primitive in the same way Integer
is. char
is the primitive for character data.
Let's address Integer
vs int
, and then briefly touch upon String
.
Wrapper Classes (e.g., Integer
) vs Primitive types (e.g., int
)
-
Primitive Type (
int
):- As described above: stores actual value, no methods, cannot be
null
, more efficient.
- As described above: stores actual value, no methods, cannot be
-
Wrapper Class (
Integer
):- An object that "wraps" or encapsulates a primitive value.
java.lang
package provides wrapper classes for all primitives:Byte
,Short
,Integer
,Long
,Float
,Double
,Character
,Boolean
. -
Purpose:
- To use primitive values in contexts where objects are required (e.g., Java Collections like
ArrayList<Integer>
, Generics). - To provide utility methods related to the primitive type (e.g.,
Integer.parseInt()
,Integer.MAX_VALUE
). - To allow primitive values to be
null
.
- To use primitive values in contexts where objects are required (e.g., Java Collections like
-
Behavior: Instances of wrapper classes are objects, so they are stored on the heap, can be
null
, and have methods.
- An object that "wraps" or encapsulates a primitive value.
String
-
String
is an Object Type: It's a class (java.lang.String
) representing a sequence of characters. It is not a primitive type. -
Immutable:
String
objects are immutable in Java. Once created, their value cannot be changed. -
Special Treatment: Java provides special support for strings, such as the string pool (for string literals) and the
+
operator for concatenation. -
Not a Wrapper for a Primitive: While it deals with character data, it doesn't "wrap" a single primitive
char
in the same wayInteger
wrapsint
. An array ofchar
(char[]
) is closer to the raw data thatString
manages.
Key Differences (int
vs Integer
)
Feature | int (Primitive) |
Integer (Wrapper Object) |
---|---|---|
Nature | Primitive data type | Object (instance of java.lang.Integer ) |
Storage | Actual value | Reference to an object on the heap |
null |
Cannot be null |
Can be null |
Methods | No methods | Has methods (e.g., intValue() , compareTo() ) |
Collections | Cannot be directly used in collections like ArrayList (before Java 5 without autoboxing) |
Can be used in collections (ArrayList<Integer> ) |
Performance | Generally faster, less memory overhead | Slower, more memory overhead |
Default Value | 0 (if instance/static variable) |
null (if instance/static variable) |
Example
import java.util.ArrayList;
import java.util.List;
public class WrapperVsPrimitive {
public static void main(String[] args) {
// int (primitive) vs Integer (wrapper object)
int primitiveInt = 100;
Integer wrapperInt = Integer.valueOf(100); // Explicit boxing (older way)
Integer autoBoxedInt = 100; // Autoboxing (Java 5+)
System.out.println("Primitive int: " + primitiveInt);
System.out.println("Wrapper Integer: " + wrapperInt);
System.out.println("Autoboxed Integer: " + autoBoxedInt);
// primitiveInt.compareTo(200); // ERROR: int has no methods
System.out.println("Compare wrapperInt to 200: " + wrapperInt.compareTo(200)); // -1
Integer nullInteger = null;
// int anotherPrimitive = nullInteger; // This would cause NullPointerException if unboxing is attempted
List<Integer> numberList = new ArrayList<>();
numberList.add(primitiveInt); // Autoboxing: int to Integer
numberList.add(wrapperInt);
// List<int> primitiveList = new ArrayList<>(); // ERROR: Generic types must be reference types
int unboxedInt = wrapperInt; // Auto-unboxing: Integer to int
// String (Object)
String strLiteral = "Java"; // String literal, often from string pool
String strObject = new String("Java"); // Explicitly creates a new String object on heap
char[] charArray = {'J', 'a', 'v', 'a'}; // Primitive char array
String strFromChars = new String(charArray);
System.out.println("String literal: " + strLiteral);
System.out.println("String object: " + strObject);
System.out.println("String from char array: " + strFromChars);
// strLiteral is an object, it has methods
System.out.println("Length of strLiteral: " + strLiteral.length());
}
}
5. Array vs List
Array
-
Definition: A fixed-size, ordered collection of elements of the same data type. The type can be primitive (e.g.,
int[]
) or object (e.g.,String[]
,MyObject[]
). - Size: Fixed at the time of creation. Cannot be changed dynamically.
-
Type Safety: Strong type checking at compile time. An
int[]
can only holdint
s. - Performance: Generally faster for element access (using index) if the index is known, due to direct memory access.
-
Features: Basic operations provided by language syntax (e.g.,
array[index]
,array.length
). No built-in methods for common data manipulations like add, remove (except by creating a new array). -
Generics: Cannot be used directly with generics in the way collections can (e.g., you can't have
Array<T>
).
List (Interface)
-
Definition: An interface (
java.util.List
) representing an ordered collection of elements (also known as a sequence). Allows duplicate elements. -
Implementations: Common implementations include
ArrayList
,LinkedList
,Vector
,Stack
. - Size: Dynamic. Lists can grow or shrink as elements are added or removed.
-
Type Safety: Uses generics for type safety (e.g.,
List<String>
,List<Integer>
). -
Performance: Varies by implementation.
ArrayList
is generally fast for random access, whileLinkedList
is faster for insertions/deletions in the middle. -
Features: Rich set of methods for adding, removing, searching, iterating, sorting, etc. (e.g.,
add()
,remove()
,get()
,size()
,isEmpty()
). - Generics: Designed to work with generics.
Key Differences
Feature | Array | List (Interface) |
---|---|---|
Size | Fixed, determined at creation | Dynamic, can change |
Type | Can hold primitives or objects | Can only hold objects (primitives are autoboxed) |
Mutability | Size is immutable | Size is mutable |
Methods | Limited (e.g., length property) |
Rich API (e.g., add , remove , size ) |
Performance | Fast direct access by index | Varies by implementation (ArrayList good for access, LinkedList for middle ops) |
Generics | Limited support | Full generic support |
Implementation | Language construct | Interface with multiple implementations |
Example
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ArrayVsListExample {
public static void main(String[] args) {
// --- Array ---
System.out.println("--- Array ---");
// Declaration and initialization
String[] stringArray = new String[3]; // Fixed size of 3
stringArray[0] = "Apple";
stringArray[1] = "Banana";
stringArray[2] = "Cherry";
// stringArray[3] = "Date"; // ArrayIndexOutOfBoundsException
System.out.println("Array size: " + stringArray.length);
System.out.println("Element at index 1: " + stringArray[1]);
// Iterating an array
for (String fruit : stringArray) {
System.out.println(fruit);
}
int[] intArray = {1, 2, 3, 4, 5}; // Array of primitives
System.out.println("Int Array: " + Arrays.toString(intArray));
// --- List ---
System.out.println("\n--- List (ArrayList implementation) ---");
// Declaration and initialization (using ArrayList)
List<String> stringList = new ArrayList<>(); // Dynamic size
stringList.add("Dog");
stringList.add("Cat");
stringList.add("Elephant");
System.out.println("List size: " + stringList.size());
System.out.println("Element at index 1: " + stringList.get(1));
stringList.add("Fish"); // Size dynamically increases
System.out.println("New list size: " + stringList.size());
System.out.println("List elements: " + stringList);
stringList.remove("Cat");
System.out.println("After removing Cat: " + stringList);
// List of Integers (uses wrapper class)
List<Integer> integerList = new ArrayList<>();
integerList.add(10); // Autoboxing: int to Integer
integerList.add(20);
System.out.println("Integer List: " + integerList);
}
}
6. Set vs List
Both Set
and List
are interfaces in the Java Collections Framework, extending Collection
.
List
- Definition: An ordered collection of elements. Allows duplicate elements.
- Order: Maintains the insertion order of elements (or can be sorted explicitly). Elements can be accessed by their integer index (position).
- Duplicates: Allows duplicate elements. You can add the same element multiple times.
-
Common Implementations:
ArrayList
,LinkedList
,Vector
. - Use Case: When the order of elements matters, and duplicates are acceptable. E.g., a list of steps in a recipe, a sequence of user actions.
Set
- Definition: A collection that contains no duplicate elements.
-
Order:
-
HashSet
: Makes no guarantees as to the iteration order of the set; it may even change over time. -
LinkedHashSet
: Maintains insertion order. -
TreeSet
: Stores elements in a sorted order (natural order or by aComparator
).
-
-
Duplicates: Does not allow duplicate elements. If you try to add an element that is already present (according to its
equals()
method), theadd()
method returnsfalse
and the set remains unchanged. -
Common Implementations:
HashSet
,LinkedHashSet
,TreeSet
. - Use Case: When you need to store unique items, and the primary concern is checking for the existence of an item. E.g., a collection of unique visitors to a website, distinct words in a document.
Key Differences
Feature | List | Set |
---|---|---|
Order | Ordered (maintains insertion order or sorted) | Generally unordered (HashSet ), or ordered (LinkedHashSet , TreeSet ) |
Duplicates | Allows duplicate elements | Does not allow duplicate elements |
Element Access | Positional access using get(index) |
No direct get(index) (except by iteration or converting to List) |
add() method |
Adds element, usually returns true |
Adds element if not present, returns true if added, false if already present |
Primary Use | Ordered sequences, allowing duplicates | Collections of unique items |
Implementations | ArrayList , LinkedList |
HashSet , LinkedHashSet , TreeSet |
Example
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class SetVsListExample {
public static void main(String[] args) {
// --- List Example (ArrayList) ---
System.out.println("--- List (ArrayList) ---");
List<String> fruitList = new ArrayList<>();
fruitList.add("Apple");
fruitList.add("Banana");
fruitList.add("Apple"); // Duplicate allowed
fruitList.add("Orange");
System.out.println("Fruit List: " + fruitList); // Order is maintained
System.out.println("Element at index 0: " + fruitList.get(0)); // Access by index
// --- Set Examples ---
System.out.println("\n--- Set (HashSet - unordered) ---");
Set<String> fruitHashSet = new HashSet<>();
System.out.println("Added Apple: " + fruitHashSet.add("Apple"));
System.out.println("Added Banana: " + fruitHashSet.add("Banana"));
System.out.println("Added Apple again: " + fruitHashSet.add("Apple")); // Duplicate not added, returns false
System.out.println("Added Orange: " + fruitHashSet.add("Orange"));
System.out.println("Fruit HashSet: " + fruitHashSet); // Order not guaranteed, no duplicates
// fruitHashSet.get(0); // COMPILE ERROR: No get(index) method
System.out.println("\n--- Set (LinkedHashSet - insertion order) ---");
Set<String> fruitLinkedHashSet = new LinkedHashSet<>();
fruitLinkedHashSet.add("Apple");
fruitLinkedHashSet.add("Banana");
fruitLinkedHashSet.add("Apple"); // Duplicate not added
fruitLinkedHashSet.add("Orange");
fruitLinkedHashSet.add("Grape");
System.out.println("Fruit LinkedHashSet: " + fruitLinkedHashSet); // Insertion order maintained, no duplicates
System.out.println("\n--- Set (TreeSet - sorted order) ---");
Set<String> fruitTreeSet = new TreeSet<>();
fruitTreeSet.add("Orange");
fruitTreeSet.add("Apple");
fruitTreeSet.add("Banana");
fruitTreeSet.add("Apple"); // Duplicate not added
fruitTreeSet.add("Grape");
System.out.println("Fruit TreeSet: " + fruitTreeSet); // Sorted order (natural string order), no duplicates
}
}
7. Comparable vs Comparator
Both are interfaces in Java used for sorting objects.
Comparable<T>
-
Definition: An interface (
java.lang.Comparable
) that a class can implement to define its "natural ordering." -
Method: It has a single method:
int compareTo(T o)
.- Returns a negative integer if
this
object is less thano
. - Returns zero if
this
object is equal too
. - Returns a positive integer if
this
object is greater thano
.
- Returns a negative integer if
-
Usage:
- Implemented by the class whose instances you want to sort.
- Used by sorting methods like
Collections.sort(List<T>)
andArrays.sort(T[])
by default if noComparator
is provided. - Data structures like
TreeSet
andTreeMap
use the natural ordering if noComparator
is specified.
- Modification: Requires modifying the source code of the class itself.
- Ordering: Provides a single way of sorting (the natural order).
Comparator<T>
-
Definition: An interface (
java.util.Comparator
) that can be implemented to define custom or multiple sorting orders for objects of a class. -
Method: It has a primary method:
int compare(T o1, T o2)
.- Returns a negative integer if
o1
is less thano2
. - Returns zero if
o1
is equal too2
. - Returns a positive integer if
o1
is greater thano2
. - (Also has other default/static methods like
thenComparing
,reversed
, etc., since Java 8).
- Returns a negative integer if
-
Usage:
- Implemented in a separate class, or as an anonymous class, or a lambda expression.
- Passed as an argument to sorting methods (e.g.,
Collections.sort(List<T>, Comparator<T>)
,Arrays.sort(T[], Comparator<T>)
). - Can be provided to constructors of
TreeSet
andTreeMap
to define a specific order.
- Modification: Does not require modifying the source code of the class being sorted.
- Ordering: Allows defining multiple, different sorting strategies for the same class.
Key Differences
Feature | Comparable<T> |
Comparator<T> |
---|---|---|
Package | java.lang |
java.util |
Method | compareTo(T o) |
compare(T o1, T o2) |
Implementation | Implemented by the class itself | Implemented in a separate class/lambda |
Purpose | Defines natural ordering of objects | Defines custom/alternative ordering of objects |
Modification | Requires changing the class being sorted | Does not require changing the class being sorted |
Flexibility | Provides one sorting logic (natural order) | Provides multiple sorting logics |
Usage | Collections.sort(list) uses it by default |
Passed as an argument to Collections.sort(list, comparator) |
Example
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// Class implementing Comparable for natural ordering (by age)
class Person implements Comparable<Person> {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
// Natural ordering: by age
return Integer.compare(this.age, other.age);
// If this.age < other.age, returns negative
// If this.age == other.age, returns zero
// If this.age > other.age, returns positive
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Comparator for sorting Person objects by name
class PersonNameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
}
public class ComparableComparatorExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
people.add(new Person("Diana", 25));
System.out.println("Original list: " + people);
// 1. Sorting using Comparable (natural order: by age)
Collections.sort(people); // Uses Person's compareTo() method
System.out.println("Sorted by age (Comparable): " + people);
// Reset list for next sort
people.clear();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
people.add(new Person("Diana", 25));
// 2. Sorting using Comparator (custom order: by name)
Collections.sort(people, new PersonNameComparator());
System.out.println("Sorted by name (Comparator): " + people);
// 3. Sorting using Comparator (lambda expression for reverse age order)
// No need to reset list, just re-sort
Comparator<Person> reverseAgeComparator = (p1, p2) -> Integer.compare(p2.age, p1.age);
// Or use Java 8 Comparator helpers:
// Comparator<Person> reverseAgeComparator = Comparator.comparingInt((Person p) -> p.age).reversed();
Collections.sort(people, reverseAgeComparator);
System.out.println("Sorted by age descending (Lambda Comparator): " + people);
// 4. Sorting by name then by age (if names are same)
Comparator<Person> nameThenAgeComparator = Comparator
.comparing((Person p) -> p.name)
.thenComparingInt(p -> p.age);
Collections.sort(people, nameThenAgeComparator);
System.out.println("Sorted by name then age: " + people);
}
}
8. Interface vs Abstract class
Both are mechanisms for abstraction in Java, but they have different characteristics and use cases.
Interface
- Definition: A blueprint of a class. It specifies a contract that implementing classes must adhere to.
-
Methods:
- Traditionally, interfaces could only have
public abstract
methods (method signatures without implementation) andpublic static final
constants. - Java 8 added
public default
methods (with implementation, can be overridden) andpublic static
methods (with implementation, cannot be overridden by implementing classes). - Java 9 added
private
andprivate static
methods (helper methods for default/static methods within the interface).
- Traditionally, interfaces could only have
-
Variables: Can only declare
public static final
variables (constants). Instance variables are not allowed. - Constructors: Cannot have constructors. Cannot be instantiated directly.
-
Implementation: A class
implements
an interface. A class can implement multiple interfaces (multiple inheritance of type). -
Access Modifiers: All members are implicitly
public
. Methods are implicitlyabstract
(unlessdefault
orstatic
). -
Purpose: To define a contract, achieve multiple inheritance of type, achieve loose coupling. Good for defining capabilities (e.g.,
Serializable
,Runnable
,Flyable
).
Abstract Class
- Definition: A class that cannot be instantiated directly and is meant to be subclassed. It can provide a partial implementation of a class.
-
Methods: Can have
abstract
methods (no implementation) and concrete methods (with implementation). Can also havestatic
andfinal
methods. -
Variables: Can have instance variables (non-static) and static variables. They can have any access modifier (
public
,protected
,private
, default). Can also befinal
. -
Constructors: Can have constructors. These are called when an instance of a concrete subclass is created (usually via
super()
call from subclass constructor). -
Implementation: A class
extends
an abstract class. A class can extend only one abstract class (or any class). - Access Modifiers: Members can have any access modifier.
- Purpose: To provide a common base class with some shared code and some parts that subclasses must implement. Good for "is-a" relationships where subclasses share common state and behavior.
Key Differences
Feature | Interface | Abstract Class |
---|---|---|
Multiple Inheritance | A class can implement multiple interfaces. | A class can extend only one abstract class. |
Methods | Abstract, default, static, private methods. | Abstract and concrete methods. Can be static, final. |
Variables | Only public static final constants. |
Instance variables, static variables, constants. Any access modifier. |
Constructors | No constructors. | Can have constructors. |
Implementation Detail | Focuses on "what" a class can do (contract). | Can provide some "how" (partial implementation). |
Keyword | interface , implements |
abstract class , extends |
State | Cannot have instance state (non-final variables). | Can have instance state (instance variables). |
Use Case | Defining capabilities, API contracts, mixins (with default methods). | Code reuse, common base for related classes. |
Access Modifiers | Implicitly public for members. |
Members can have any access modifier. |
Example
// --- Interface ---
interface Playable {
// public static final String TYPE = "Playable"; // Implicitly public static final
String TYPE = "Playable"; // Same as above
void play(); // Implicitly public abstract
default void displayType() { // Default method (Java 8+)
System.out.println("This is of type: " + TYPE);
}
static String getCategory() { // Static method (Java 8+)
return "Entertainment";
}
}
interface Recordable {
void record();
void stopRecording();
}
// --- Abstract Class ---
abstract class MediaDevice {
protected String deviceName; // Instance variable (can be protected, private, etc.)
private boolean powerOn; // Instance variable
public MediaDevice(String deviceName) { // Constructor
this.deviceName = deviceName;
this.powerOn = false;
System.out.println(deviceName + " device created.");
}
public void powerOn() { // Concrete method
this.powerOn = true;
System.out.println(deviceName + " powered ON.");
}
public void powerOff() { // Concrete method
this.powerOn = false;
System.out.println(deviceName + " powered OFF.");
}
public boolean isPoweredOn() {
return powerOn;
}
public abstract void showStatus(); // Abstract method - must be implemented by subclasses
}
// --- Concrete Class ---
// Implements multiple interfaces and extends one abstract class
class SmartSpeaker extends MediaDevice implements Playable, Recordable {
public SmartSpeaker(String name) {
super(name); // Call superclass constructor
}
@Override
public void play() {
if (isPoweredOn()) {
System.out.println(deviceName + " is playing music.");
} else {
System.out.println(deviceName + " is off. Cannot play.");
}
}
@Override
public void record() {
if (isPoweredOn()) {
System.out.println(deviceName + " is recording audio.");
} else {
System.out.println(deviceName + " is off. Cannot record.");
}
}
@Override
public void stopRecording() {
if (isPoweredOn()) {
System.out.println(deviceName + " stopped recording.");
}
}
@Override
public void showStatus() {
System.out.println("Status for " + deviceName + ": Power " + (isPoweredOn() ? "ON" : "OFF"));
}
}
public class InterfaceAbstractExample {
public static void main(String[] args) {
SmartSpeaker alexa = new SmartSpeaker("Alexa Echo");
alexa.powerOn();
alexa.showStatus();
alexa.play(); // From Playable interface
alexa.record(); // From Recordable interface
alexa.stopRecording();
alexa.displayType(); // Default method from Playable interface
System.out.println("Category: " + Playable.getCategory()); // Static method from Playable
alexa.powerOff();
alexa.showStatus();
alexa.play();
}
}
9. Final vs Static keyword
These keywords serve very different purposes in Java.
final
keyword
The final
keyword can be applied to variables, methods, and classes. Its meaning changes based on context.
-
final
variable:- Primitive: The value of the variable cannot be changed once assigned. It becomes a constant. Must be initialized at declaration or in the constructor (if it's an instance variable).
- Object Reference: The reference variable cannot be reassigned to point to a different object. However, the state of the object it points to can be changed (if the object itself is mutable).
-
final
method:- A final method cannot be overridden by subclasses.
- Used to prevent subclasses from altering critical behavior.
-
final
class:- A final class cannot be subclassed (extended).
- E.g.,
String
,Integer
are final classes. - Used for security or to ensure the immutability or specific behavior of the class cannot be compromised by subclassing.
static
keyword
The static
keyword indicates that a member (variable or method) or a nested class belongs to the class itself, rather than to instances of the class.
-
static
variable (class variable):- There is only one copy of the static variable shared among all instances of the class.
- It is initialized when the class is loaded.
- Can be accessed using
ClassName.variableName
.
-
static
method (class method):- Can be called without creating an instance of the class (
ClassName.methodName()
). - Can only access static variables and call other static methods directly.
- Cannot use
this
orsuper
.
- Can be called without creating an instance of the class (
-
static
block:- A block of code that is executed once when the class is loaded into memory. Used for static initialization.
-
static
nested class:- A nested class declared
static
. It does not have an implicit reference to an instance of the outer class. It can only access static members of the outer class directly. Can be instantiated without an instance of the outer class.
- A nested class declared
Key Differences
Feature | final |
static |
---|---|---|
Purpose | To restrict modification/extension/overriding. Creates constants. | To associate a member with the class rather than an instance. Shared memory. |
Applicable to | Variables, methods, classes. | Variables, methods, blocks, nested classes. |
Variables | Value cannot be changed (for primitives); reference cannot be changed (for objects). | Single copy shared by all instances. Belongs to the class. |
Methods | Cannot be overridden. | Belongs to the class, called via ClassName.method() . No this . |
Classes | Cannot be subclassed. | (For nested classes) Does not hold reference to outer class instance. |
Memory | Does not directly impact memory model like static (except making references unchangeable). | Static members are stored in a separate memory area (e.g., method area/metaspace). |
Example
class MyMath {
// final variable (constant)
public static final double PI = 3.14159; // 'static final' makes it a class constant
// instance variable (can be final)
private final int id; // Must be initialized in constructor or at declaration
private String instanceName;
// static variable
private static int instanceCount = 0;
public MyMath(int id, String name) {
this.id = id; // Initialize final instance variable
this.instanceName = name;
instanceCount++;
}
// final method (cannot be overridden)
public final void printID() {
System.out.println("ID: " + this.id);
}
// instance method
public void printName() {
System.out.println("Name: " + this.instanceName);
}
// static method
public static int getInstanceCount() {
// System.out.println(instanceName); // ERROR: Cannot access instance variable from static context
// printName(); // ERROR: Cannot call instance method from static context
return instanceCount;
}
// static block
static {
System.out.println("MyMath class loaded. Initial instance count: " + instanceCount);
// PI = 3.14; // Can initialize static final here if not at declaration
}
}
// final class (cannot be extended)
final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
// class TryingToExtend extends ImmutablePoint {} // ERROR: Cannot inherit from final ImmutablePoint
class AdvancedMath extends MyMath {
public AdvancedMath(int id, String name) {
super(id, name);
}
// @Override
// public final void printID() { // ERROR: Cannot override final method from MyMath
// System.out.println("Advanced ID: " + super.id); // 'id' is private, but can be accessed if it were protected
// }
@Override
public void printName() { // OK to override non-final method
System.out.print("Advanced Math - ");
super.printName();
}
}
public class FinalStaticExample {
public static void main(String[] args) {
System.out.println("Accessing static final constant: MyMath.PI = " + MyMath.PI);
// MyMath.PI = 3.0; // ERROR: Cannot assign a value to final variable PI
System.out.println("Initial instance count (static method): " + MyMath.getInstanceCount()); // 0
MyMath math1 = new MyMath(1, "MathObj1");
// math1.id = 2; // ERROR: Cannot assign a value to final variable id
math1.printID(); // Calls final method
math1.printName(); // Calls instance method
MyMath math2 = new MyMath(2, "MathObj2");
System.out.println("Instance count after 2 objects (static method): " + MyMath.getInstanceCount()); // 2
AdvancedMath advMath = new AdvancedMath(3, "AdvMathObj1");
advMath.printID(); // Calls inherited final method
advMath.printName(); // Calls overridden method in AdvancedMath
ImmutablePoint p = new ImmutablePoint(10, 20);
// p = new ImmutablePoint(30, 40); // This would be allowed if 'p' itself was not final.
// Here, the object p refers to is immutable.
final ImmutablePoint finalP = new ImmutablePoint(5,5);
// finalP = new ImmutablePoint(1,1); // ERROR: finalP is a final reference.
// The object finalP points to is also immutable due to its class design.
}
}
10. ==
vs equals()
method
These are used for comparison in Java, but they compare different things.
==
operator
-
Primitives: For primitive types (
int
,char
,boolean
, etc.),==
compares their actual values.-
int a = 5; int b = 5;
thena == b
istrue
.
-
-
Objects: For object reference types (
String
,ArrayList
, custom objects, etc.),==
compares their memory addresses (references). It checks if two reference variables point to the exact same object in memory.-
String s1 = new String("hello"); String s2 = new String("hello");
thens1 == s2
isfalse
becauses1
ands2
refer to two different objects in memory, even though their content is the same. -
String s3 = "hello"; String s4 = "hello";
thens3 == s4
istrue
due to String interning (string pool optimization for literals). -
MyObject o1 = new MyObject(); MyObject o2 = o1;
theno1 == o2
istrue
becauseo2
is assigned the same reference aso1
.
-
equals()
method
-
Definition: A method defined in the
java.lang.Object
class. Its default implementation inObject
class behaves exactly like==
for objects (i.e., it compares references).-
public boolean equals(Object obj)
-
-
Overriding: Classes like
String
,Integer
,Date
, and collection classes override theequals()
method to provide "logical" or "content" equality. They compare the actual content or state of the objects, not just their memory addresses.- For
String
,equals()
compares the sequence of characters. - For
Integer
,equals()
compares the wrapped integer value.
- For
-
Custom Classes: If you don't override
equals()
in your custom class, it will inherit theObject
class's implementation, which means it will perform a reference comparison (==
). It's a common practice to overrideequals()
(andhashCode()
) in custom classes if you need to define logical equality based on the object's attributes. -
Contract: When overriding
equals()
, you must also overridehashCode()
to maintain the general contract: ifa.equals(b)
is true, thena.hashCode() == b.hashCode()
must be true.
Key Differences
Feature | == Operator |
equals() Method |
---|---|---|
Type | Operator | Method |
Primitives | Compares values | Not applicable (cannot call methods on primitives) |
Objects (Default) | Compares references (memory addresses) | Compares references (default Object.equals() behavior) |
Objects (Overridden) | Still compares references | Compares content/state (if overridden appropriately, e.g., in String , Integer ) |
Usage | Value comparison for primitives, reference comparison for objects. | Content/logical comparison for objects (when properly overridden). |
Example
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// getters...
// Without overriding equals() and hashCode(), Point instances
// will be compared by reference using the default Object.equals().
@Override
public boolean equals(Object o) {
if (this == o) return true; // Same object reference
if (o == null || getClass() != o.getClass()) return false; // Null or different class
Point point = (Point) o;
return x == point.x && y == point.y; // Compare content
}
@Override
public int hashCode() {
// A simple hash code implementation
int result = x;
result = 31 * result + y;
return result;
// Or use Objects.hash(x, y) from Java 7+
}
}
public class EqualsVsDoubleEqual {
public static void main(String[] args) {
// --- Primitives ---
int a = 10;
int b = 10;
int c = 20;
System.out.println("Primitives:");
System.out.println("a == b: " + (a == b)); // true (10 == 10)
System.out.println("a == c: " + (a == c)); // false (10 == 20)
// a.equals(b); // COMPILE ERROR: equals() cannot be called on primitives
// --- Objects (String - special case with string pool) ---
String s1 = "hello"; // String literal, goes into string pool
String s2 = "hello"; // Another literal, reuses from string pool
String s3 = new String("hello"); // Explicitly creates a new object on the heap
String s4 = new String("hello"); // Explicitly creates another new object on the heap
System.out.println("\nStrings:");
System.out.println("s1 == s2: " + (s1 == s2)); // true (both point to the same object in string pool)
System.out.println("s1 == s3: " + (s1 == s3)); // false (s1 is in pool, s3 is a new heap object)
System.out.println("s3 == s4: " + (s3 == s4)); // false (s3 and s4 are distinct heap objects)
System.out.println("s1.equals(s2): " + s1.equals(s2)); // true (content is the same)
System.out.println("s1.equals(s3): " + s1.equals(s3)); // true (content is the same)
System.out.println("s3.equals(s4): " + s3.equals(s4)); // true (content is the same)
// --- Objects (Custom Class Point) ---
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = p1; // p3 now refers to the same object as p1
System.out.println("\nCustom Objects (Point):");
System.out.println("p1 == p2: " + (p1 == p2)); // false (p1 and p2 are different objects in memory)
System.out.println("p1 == p3: " + (p1 == p3)); // true (p1 and p3 refer to the same object)
// If Point did NOT override equals(), this would be false.
// Since Point DOES override equals() to compare x and y, this is true.
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true (content is the same due to overridden equals)
System.out.println("p1.equals(p3): " + p1.equals(p3)); // true (same object, content also same)
Object o = new Object();
Object o2 = new Object();
System.out.println("o.equals(o2) without override: " + o.equals(o2)); // false, default is ==
}
}
(The remaining answers will follow this detailed format. This is a lot of information, so I'll continue in subsequent responses if needed to avoid making one response excessively long.) Okay, continuing with the detailed explanations:
11. Autoboxing vs Unboxing
These are convenience features introduced in Java 5 that allow automatic conversion between primitive types and their corresponding wrapper classes.
Autoboxing
-
Definition: The automatic conversion that the Java compiler makes between a primitive type and its corresponding object wrapper class. For example, converting an
int
to anInteger
, adouble
to aDouble
, etc. -
When it happens:
- When a primitive value is passed as a parameter to a method that expects an object of the corresponding wrapper class.
- When a primitive value is assigned to a variable of the corresponding wrapper class.
- When a primitive is added to a collection that expects objects (e.g.,
ArrayList<Integer>
).
Unboxing
-
Definition: The automatic conversion that the Java compiler makes from a wrapper class object to its corresponding primitive type. For example, converting an
Integer
to anint
, aDouble
to adouble
, etc. -
When it happens:
- When an object of a wrapper class is passed as a parameter to a method that expects a value of the corresponding primitive type.
- When an object of a wrapper class is assigned to a variable of the corresponding primitive type.
- In arithmetic operations involving wrapper objects (the object is unboxed to a primitive before the operation).
-
Caution: Unboxing a
null
wrapper object will result in aNullPointerException
.
Key Differences
Feature | Autoboxing | Unboxing |
---|---|---|
Conversion | Primitive type → Wrapper class object | Wrapper class object → Primitive type |
Example | int i = 10; Integer obj = i; |
Integer obj = new Integer(10); int i = obj; |
Potential Issue | Less direct, slight performance overhead. | NullPointerException if wrapper object is null . |
Example
import java.util.ArrayList;
import java.util.List;
public class AutoboxingUnboxingExample {
public static void main(String[] args) {
// --- Autoboxing ---
// Primitive int to Integer object
int primitiveInt = 10;
Integer wrapperInt = primitiveInt; // Autoboxing: int -> Integer
// Under the hood, this is like: Integer wrapperInt = Integer.valueOf(primitiveInt);
System.out.println("Primitive int: " + primitiveInt);
System.out.println("Autoboxed Integer: " + wrapperInt);
List<Integer> integerList = new ArrayList<>();
integerList.add(20); // Autoboxing: int 20 -> Integer.valueOf(20)
integerList.add(primitiveInt); // Autoboxing
System.out.println("List of Integers: " + integerList);
takesIntegerObject(30); // Autoboxing: int 30 -> Integer.valueOf(30)
// --- Unboxing ---
Integer anotherWrapperInt = Integer.valueOf(50);
int anotherPrimitiveInt = anotherWrapperInt; // Unboxing: Integer -> int
// Under the hood, this is like: int anotherPrimitiveInt = anotherWrapperInt.intValue();
System.out.println("\nUnboxed primitive int: " + anotherPrimitiveInt);
int sum = anotherWrapperInt + 5; // Unboxing: anotherWrapperInt is unboxed to int before addition
System.out.println("Sum (unboxing in expression): " + sum);
int valFromList = integerList.get(0); // Unboxing: Element from list (Integer) to int
System.out.println("Value from list (unboxed): " + valFromList);
takesPrimitiveInt(anotherWrapperInt); // Unboxing: Integer -> int
// --- Potential NullPointerException with Unboxing ---
Integer nullInteger = null;
try {
// int problematicInt = nullInteger; // This would cause NullPointerException
if (nullInteger != null && nullInteger > 0) { // Guarded unboxing
System.out.println("This won't print");
}
// Or more directly, to show the error:
int npeInt = nullInteger.intValue(); // This is what unboxing effectively does
} catch (NullPointerException e) {
System.out.println("\nCaught NullPointerException during unboxing: " + e.getMessage());
}
}
public static void takesIntegerObject(Integer num) {
System.out.println("Method takesIntegerObject received: " + num);
}
public static void takesPrimitiveInt(int num) {
System.out.println("Method takesPrimitiveInt received: " + num);
}
}
12. Checked exceptions vs Unchecked exceptions
Java's exception handling mechanism categorizes exceptions into these two main types, which differ in how the compiler enforces their handling. Both are subclasses of java.lang.Throwable
. Error
is another subclass of Throwable
for severe system issues, usually not handled by applications.
Checked Exceptions
- Definition: Exceptions that the Java compiler checks at compile-time. They represent exceptional conditions that a well-written application should anticipate and recover from.
-
Subclasses of:
java.lang.Exception
(but notjava.lang.RuntimeException
or its subclasses). -
Handling:
- A method that might throw a checked exception must either:
- Handle it using a
try-catch
block. - Declare it in its
throws
clause, propagating the responsibility to the caller.
- Handle it using a
- A method that might throw a checked exception must either:
-
Examples:
IOException
,SQLException
,FileNotFoundException
,ClassNotFoundException
. - Purpose: To force programmers to deal with predictable, though exceptional, situations.
Unchecked Exceptions (Runtime Exceptions)
- Definition: Exceptions that the Java compiler does not check at compile-time. They usually indicate programming errors or unexpected conditions that are often unrecoverable or should have been prevented by better coding.
-
Subclasses of:
java.lang.RuntimeException
(which itself is a subclass ofjava.lang.Exception
).Error
and its subclasses are also unchecked, but typically not caught. -
Handling:
- The compiler does not require you to handle or declare them.
- You can still use
try-catch
blocks for them, but it's often a sign of fixing a symptom rather than the root cause (e.g., catchingNullPointerException
instead of ensuring an object is not null).
-
Examples:
NullPointerException
,ArrayIndexOutOfBoundsException
,IllegalArgumentException
,ArithmeticException
,ClassCastException
. - Purpose: To indicate bugs in the program logic or runtime issues that are generally not anticipated for recovery by the immediate calling code.
Key Differences
Feature | Checked Exceptions | Unchecked Exceptions (Runtime Exceptions) |
---|---|---|
Compiler Check | Checked at compile-time. | Not checked at compile-time. |
Handling | Must be handled (try-catch) or declared (throws). | Handling/declaring is optional. |
Parent Class | Subclass of Exception (excluding RuntimeException ). |
Subclass of RuntimeException . (Error is also unchecked). |
Nature | External conditions, often recoverable (e.g., file not found, network error). | Programming errors, internal logic flaws (e.g., null pointer, bad index). |
Examples | IOException , SQLException , FileNotFoundException |
NullPointerException , ArrayIndexOutOfBoundsException , ArithmeticException |
Example
import java.io.File;
import java.io.FileReader;
import java.io.IOException; // Checked exception
import java.io.FileNotFoundException; // Checked exception, subclass of IOException
public class CheckedUncheckedExceptions {
// Method that might throw a checked exception (IOException)
// It must declare it in the 'throws' clause or handle it.
public static void readFile(String fileName) throws FileNotFoundException, IOException {
File file = new File(fileName);
FileReader fr = new FileReader(file); // Might throw FileNotFoundException
// ... read file ... (further operations might throw IOException)
System.out.println("Successfully opened (but not read): " + fileName);
fr.close(); // Might throw IOException
}
// Method that might cause an unchecked exception
public static void processNumber(String numberStr, int divisor) {
if (numberStr == null) {
// This would lead to NullPointerException if not checked
System.out.println("Input string is null, cannot parse.");
return;
}
try {
int number = Integer.parseInt(numberStr); // Might throw NumberFormatException (unchecked)
int result = number / divisor; // Might throw ArithmeticException (unchecked if divisor is 0)
System.out.println("Result: " + result);
} catch (NumberFormatException e) {
System.err.println("Unchecked Exception: Invalid number format - " + e.getMessage());
} catch (ArithmeticException e) {
System.err.println("Unchecked Exception: Cannot divide by zero - " + e.getMessage());
}
// No need to declare NumberFormatException or ArithmeticException in throws clause.
}
public static void main(String[] args) {
// --- Handling Checked Exception ---
System.out.println("--- Checked Exception Example ---");
try {
readFile("existing_file.txt"); // Assume this file does not exist for demo
// To make it work, create an empty "existing_file.txt"
// readFile("non_existent_file.txt");
} catch (FileNotFoundException e) {
System.err.println("Checked Exception Caught: File not found - " + e.getMessage());
} catch (IOException e) {
System.err.println("Checked Exception Caught: IO Error - " + e.getMessage());
} finally {
System.out.println("File reading attempt finished.");
}
// --- Triggering Unchecked Exceptions ---
System.out.println("\n--- Unchecked Exception Example ---");
processNumber("100", 5); // Valid
processNumber("abc", 5); // NumberFormatException
processNumber("100", 0); // ArithmeticException
processNumber(null, 5); // Handled null case inside processNumber
// Example of an unhandled unchecked exception (if not caught inside method)
// String text = null;
// System.out.println(text.length()); // This would throw NullPointerException
}
}
// To run the "readFile" part without error, create an empty file named "existing_file.txt" in the same directory.
13. Thread vs Runnable
Both are used for creating and managing threads in Java for concurrent execution.
Thread
class (java.lang.Thread
)
- Definition: A class that represents a thread of execution.
-
Usage:
-
Extend
Thread
: Create a subclass ofThread
and override itsrun()
method to define the task the thread will execute. Then create an instance of this subclass and call itsstart()
method. -
Pass
Runnable
toThread
constructor: (More common and flexible) Create an instance ofThread
by passing aRunnable
object to its constructor.
-
Extend
-
State: A
Thread
object encapsulates the thread's state, priority, name, etc. -
Limitation: If you extend
Thread
, your class cannot extend any other class (Java doesn't support multiple class inheritance).
Runnable
interface (java.lang.Runnable
)
-
Definition: A functional interface with a single abstract method:
void run()
. -
Usage:
- Implement the
Runnable
interface in a class and provide the implementation for therun()
method. - Create an instance of this class (the
Runnable
object). - Create a
Thread
object, passing theRunnable
object to its constructor:Thread t = new Thread(myRunnable);
. - Call
t.start()
to begin execution.
- Implement the
-
Flexibility: Since it's an interface, your class can implement
Runnable
and still extend another class. This promotes better separation of concerns: the task (Runnable
) is separate from the execution mechanism (Thread
). -
Reusability: The same
Runnable
instance can potentially be executed by multiple threads if itsrun()
method is thread-safe or designed for it (though typically oneRunnable
is passed to oneThread
for a specific task).
Key Differences
Feature | Thread Class |
Runnable Interface |
---|---|---|
Type | Class | Interface |
Inheritance | Extending Thread means your class cannot extend other classes. |
Implementing Runnable allows your class to extend another class. |
Separation | Task logic is tightly coupled with the Thread object. |
Promotes separation of task logic from thread mechanism. |
Flexibility | Less flexible due to inheritance restriction. | More flexible. |
Object Sharing | Each thread is a new object with its own state. | Multiple threads can share the same Runnable object (if designed to be shareable/thread-safe). |
Primary Role | Represents the worker, the actual thread of execution. | Represents the task to be executed. |
Recommended Use | Generally, implementing Runnable is preferred over extending Thread . |
Preferred approach for defining a task. |
Example
// Approach 1: Extending Thread
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
super(name); // Set thread name via superclass constructor
this.threadName = name;
System.out.println("Creating " + threadName );
}
@Override
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", Count: " + i);
Thread.sleep(50); // Simulate work
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
}
// Approach 2: Implementing Runnable
class MyRunnable implements Runnable {
private String taskName;
private Thread thread; // Optional: to hold the thread that runs this runnable
public MyRunnable(String name) {
this.taskName = name;
System.out.println("Creating task " + taskName );
}
@Override
public void run() {
System.out.println("Running task " + taskName + " on thread " + Thread.currentThread().getName());
try {
for(int i = 4; i > 0; i--) {
System.out.println("Task: " + taskName + ", Count: " + i + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(70); // Simulate work
}
} catch (InterruptedException e) {
System.out.println("Task " + taskName + " interrupted.");
}
System.out.println("Task " + taskName + " exiting.");
}
public void start() { // Convenience method to create and start the thread
System.out.println("Starting task " + taskName);
if (thread == null) {
thread = new Thread(this, taskName + "-Thread"); // Pass runnable and a name for the thread
thread.start();
}
}
}
public class ThreadVsRunnableExample {
public static void main(String[] args) {
System.out.println("--- Extending Thread ---");
MyThread thread1 = new MyThread("Thread-A (extends Thread)");
MyThread thread2 = new MyThread("Thread-B (extends Thread)");
thread1.start(); // Calls run() method of MyThread
thread2.start(); // Calls run() method of MyThread
// Wait for threads to finish before proceeding (optional, for cleaner output)
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("--- Finished Extending Thread Example ---");
System.out.println("\n--- Implementing Runnable ---");
MyRunnable runnableTask1 = new MyRunnable("Task-X (implements Runnable)");
MyRunnable runnableTask2 = new MyRunnable("Task-Y (implements Runnable)");
// Manually creating Thread objects for Runnable tasks
Thread t3 = new Thread(runnableTask1, "Thread-C (for Task-X)");
Thread t4 = new Thread(runnableTask2, "Thread-D (for Task-Y)");
t3.start(); // Calls run() method of MyRunnable (runnableTask1)
t4.start(); // Calls run() method of MyRunnable (runnableTask2)
// Or using the convenience start method in MyRunnable
// MyRunnable runnableTask3 = new MyRunnable("Task-Z (implements Runnable)");
// runnableTask3.start();
try {
t3.join();
t4.join();
// if (runnableTask3.thread != null) runnableTask3.thread.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("--- Finished Implementing Runnable Example ---");
// Using Runnable with Lambda (Java 8+)
System.out.println("\n--- Runnable with Lambda ---");
Runnable lambdaTask = () -> {
System.out.println("Running lambda task on thread " + Thread.currentThread().getName());
try {
for(int i = 2; i > 0; i--) {
System.out.println("Lambda Task, Count: " + i + " (Thread: " + Thread.currentThread().getName() + ")");
Thread.sleep(60);
}
} catch (InterruptedException e) {
System.out.println("Lambda task interrupted.");
}
System.out.println("Lambda task exiting.");
};
Thread t5 = new Thread(lambdaTask, "Thread-E (for Lambda)");
t5.start();
try {
t5.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("--- Finished Runnable with Lambda Example ---");
}
}
Why Runnable
is generally preferred:
- Decoupling: It separates the task (what to do) from the worker (the thread that does it).
- Flexibility: Your task class can still extend another class if needed, as Java does not support multiple inheritance of classes.
-
Reusability: A
Runnable
can be submitted to anExecutorService
or other concurrency utilities more easily.
14. StringBuilder vs StringBuffer
Both StringBuilder
and StringBuffer
are mutable sequence of characters, used when you need to perform many modifications to strings (concatenation, insertion, deletion). The String
class in Java is immutable.
String
(briefly, for context)
- Immutable. Every modification creates a new
String
object. - Inefficient for frequent modifications (e.g., building a string in a loop).
StringBuffer
- Definition: A mutable sequence of characters.
-
Thread-Safety: Synchronized. Its methods (like
append()
,insert()
,delete()
) are synchronized, making it thread-safe. This means only one thread can access aStringBuffer
object's methods at a time. -
Performance: Due to synchronization, it's slower than
StringBuilder
in a single-threaded environment. - Introduced: Java 1.0.
StringBuilder
- Definition: A mutable sequence of characters.
- Thread-Safety: Not synchronized. Its methods are not synchronized, making it non-thread-safe.
-
Performance: Faster than
StringBuffer
in a single-threaded environment because it doesn't have the overhead of synchronization. -
Introduced: Java 5, as a non-synchronized alternative to
StringBuffer
.
Key Differences
Feature | StringBuffer |
StringBuilder |
---|---|---|
Mutability | Mutable | Mutable |
Thread-Safety | Synchronized (thread-safe) | Not synchronized (not thread-safe) |
Performance | Slower due to synchronization overhead | Faster (no synchronization overhead) |
Introduced | Java 1.0 | Java 5 |
Typical Use | Multi-threaded environments where shared mutable string is needed (rarely, as better concurrency utilities exist). | Single-threaded environments, or when synchronization is handled externally. Most common choice for string building. |
When to use which?
-
StringBuilder
: Use in most cases, especially in single-threaded applications or when theStringBuilder
instance is confined to a single thread (e.g., local variable within a method). This is the common recommendation. -
StringBuffer
: Use only if you need a mutable string that will be accessed and modified by multiple threads concurrently, and you need built-in synchronization. However, even in multi-threaded scenarios, it's often better to useStringBuilder
within synchronized blocks or use other concurrency utilities if complex coordination is needed. -
String
: Use for fixed string values or when immutability is desired. String concatenation with+
is often optimized by the compiler to useStringBuilder
under the hood for simple cases, but explicitStringBuilder
is better for loops.
Example
public class StringBuilderStringBufferExample {
public static final int ITERATIONS = 100000;
public static void main(String[] args) {
// --- String Concatenation (for comparison, less efficient in loops) ---
long startTime = System.nanoTime();
String str = "";
for (int i = 0; i < ITERATIONS / 10; i++) { // Reduced iterations for String due to slowness
str += "a"; // Creates new String object in each iteration
}
long endTime = System.nanoTime();
System.out.println("String concatenation time: " + (endTime - startTime) / 1_000_000.0 + " ms (for " + ITERATIONS/10 + " iterations)");
// --- StringBuffer ---
startTime = System.nanoTime();
StringBuffer sBuffer = new StringBuffer();
for (int i = 0; i < ITERATIONS; i++) {
sBuffer.append("a");
}
endTime = System.nanoTime();
System.out.println("StringBuffer append time: " + (endTime - startTime) / 1_000_000.0 + " ms");
// --- StringBuilder ---
startTime = System.nanoTime();
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < ITERATIONS; i++) {
sBuilder.append("a");
}
endTime = System.nanoTime();
System.out.println("StringBuilder append time: " + (endTime - startTime) / 1_000_000.0 + " ms");
// --- Functionality (same for both StringBuilder and StringBuffer) ---
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // "Hello World"
sb.insert(5, ", Java"); // "Hello, Java World"
sb.delete(5, 11); // "Hello World" (deletes ", Java")
sb.reverse(); // "dlroW olleH"
String finalString = sb.toString();
System.out.println("\nFinal string from StringBuilder: " + finalString);
// --- Thread Safety Demo (Conceptual) ---
// To truly demonstrate thread-safety issues with StringBuilder vs StringBuffer,
// you'd need multiple threads modifying the same instance.
// StringBuffer (thread-safe)
StringBuffer sharedBuffer = new StringBuffer();
Runnable taskBuffer = () -> {
for (int i = 0; i < 1000; i++) {
sharedBuffer.append("X");
}
};
// StringBuilder (not thread-safe)
StringBuilder sharedBuilder = new StringBuilder();
Runnable taskBuilder = () -> {
for (int i = 0; i < 1000; i++) {
sharedBuilder.append("Y"); // Potential race condition
}
};
Thread t1 = new Thread(taskBuffer);
Thread t2 = new Thread(taskBuffer);
Thread t3 = new Thread(taskBuilder);
Thread t4 = new Thread(taskBuilder);
t1.start(); t2.start();
t3.start(); t4.start();
try {
t1.join(); t2.join();
t3.join(); t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nShared StringBuffer length (expected 2000): " + sharedBuffer.length());
// For StringBuilder, the length might not be 2000 due to race conditions.
// This simple append might not always show it, more complex operations would.
System.out.println("Shared StringBuilder length (potentially < 2000): " + sharedBuilder.length());
// To make StringBuilder safe in a multi-threaded context, you would do:
// synchronized(sharedBuilder) { sharedBuilder.append("Y"); }
}
}
Output for the performance part will show StringBuilder
is generally faster than StringBuffer
, and both are much faster than repeated String
concatenation in a loop.
The thread-safety demo for length might consistently show 2000 for StringBuilder
in simple appends because append(char)
operations might be atomic enough on some JVMs/OSs for this trivial case. A more complex sequence of operations or more contention would more reliably show data corruption or incorrect lengths with StringBuilder
in a concurrent setting without external synchronization.
15. Synchronized methods vs Synchronized blocks
Both are mechanisms in Java to control access to shared resources by multiple threads, preventing race conditions and ensuring data consistency. They work by acquiring an intrinsic lock (also called a monitor lock) associated with an object.
Synchronized Methods
-
Definition: A method declared with the
synchronized
keyword. -
Locking:
-
Instance Method: When a synchronized instance method is called, the thread acquires the intrinsic lock for that specific instance (the
this
object) of the class. No other thread can execute any other synchronized instance method on the same object until the lock is released. -
Static Method: When a synchronized static method is called, the thread acquires the intrinsic lock for the
Class
object associated with that class. No other thread can execute any other synchronized static method in the same class until the lock is released.
-
Instance Method: When a synchronized instance method is called, the thread acquires the intrinsic lock for that specific instance (the
- Scope: The entire method is synchronized. The lock is acquired when the method is entered and released when the method exits (either normally or due to an unhandled exception).
- Granularity: Coarser-grained locking. If only a small part of the method needs synchronization, the entire method still gets locked, potentially reducing concurrency.
Synchronized Blocks
-
Definition: A block of code marked with the
synchronized
keyword, followed by an object reference in parentheses.synchronized (objectReference) { // code to be synchronized }
-
Locking: The thread acquires the intrinsic lock for the object specified in
objectReference
. No other thread can execute another synchronized block on the same objectReference until the lock is released. -
Object for Locking:
-
this
: Locks on the current instance (similar to a synchronized instance method, but for a specific block). -
MyClass.class
: Locks on the Class object (similar to a synchronized static method, but for a specific block). - Any other object: Can use a dedicated lock object (e.g.,
private final Object lock = new Object();
). This is often preferred for finer-grained control and to avoid unintended lock contention.
-
- Scope: Only the code within the block is synchronized.
- Granularity: Finer-grained locking. Allows you to synchronize only the critical sections of code that access shared resources, leaving other parts of the method non-synchronized, potentially improving concurrency.
Key Differences
Feature | Synchronized Method | Synchronized Block |
---|---|---|
Scope | Entire method body. | Specific block of code within a method. |
Lock Object | Implicit: this for instance methods, ClassName.class for static methods. |
Explicit: Specified object (objectReference) . |
Granularity | Coarser-grained. | Finer-grained, more flexible. |
Flexibility | Less flexible in choosing the lock object. | More flexible; can use different locks for different blocks even in the same method. |
Performance | Can lead to lower concurrency if the whole method doesn't need synchronization. | Can offer better concurrency by only locking critical sections. |
Readability | Can be simpler for methods where the entire logic needs to be atomic. | Can make code slightly more complex but offers more control. |
When to use which?
- Synchronized Method: Use when the entire logic of a method needs to be executed atomically with respect to other synchronized methods on the same object (or class for static methods). It's simpler to write.
-
Synchronized Block:
- Use when only a part of a method needs synchronization.
- Use when you need to synchronize on an object other than
this
orClassName.class
. - Use when different critical sections within the same class need to be protected by different locks to allow more concurrency (e.g., one block locks on
lock1
, another onlock2
). - Often preferred for better performance and finer control, especially in complex concurrent applications. Using private dedicated lock objects (
private final Object lock = new Object();
) is a good practice to avoid exposing locks and potential deadlocks if external code tries to lock on your public objects.
Example
class Counter {
private int count = 0;
private int anotherCount = 0;
// Lock object for finer-grained synchronization
private final Object lock1 = new Object();
private final Object lock2 = new Object();
// Synchronized instance method (locks on 'this' object)
public synchronized void incrementMethodSync() {
// Entire method is synchronized
System.out.println(Thread.currentThread().getName() + " acquired lock for incrementMethodSync on " + this);
count++;
try { Thread.sleep(10); } catch (InterruptedException e) {} // Simulate work
System.out.println(Thread.currentThread().getName() + " releasing lock for incrementMethodSync");
}
// Synchronized static method (locks on 'Counter.class' object)
private static int staticCount = 0;
public static synchronized void incrementStaticMethodSync() {
System.out.println(Thread.currentThread().getName() + " acquired lock for incrementStaticMethodSync on Counter.class");
staticCount++;
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " releasing lock for incrementStaticMethodSync");
}
// Method using synchronized block (locks on 'this' object)
public void incrementBlockThisSync() {
// Non-synchronized part
System.out.println(Thread.currentThread().getName() + " executing non-sync part before block (incrementBlockThisSync)");
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " acquired lock for block on " + this);
count++;
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " releasing lock for block on " + this);
}
// Non-synchronized part
System.out.println(Thread.currentThread().getName() + " executing non-sync part after block (incrementBlockThisSync)");
}
// Method using synchronized block with a dedicated lock object
public void incrementBlockDedicatedLock() {
System.out.println(Thread.currentThread().getName() + " executing non-sync part before block (incrementBlockDedicatedLock)");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " acquired lock1");
anotherCount++;
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " releasing lock1");
}
}
public void anotherOperationWithDifferentLock() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " acquired lock2 for another operation");
// Perform some other critical operation on a different resource
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " releasing lock2");
}
}
public int getCount() {
return count;
}
public int getAnotherCount() {
return anotherCount;
}
public static int getStaticCount() {
return staticCount;
}
}
public class SyncMethodVsBlockExample {
public static void main(String[] args) throws InterruptedException {
Counter c1 = new Counter();
Counter c2 = new Counter(); // Different instance
// Test synchronized instance method
// Threads will contend for lock on c1 if they call incrementMethodSync on c1
Thread t1 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementMethodSync(); }, "T1-MethodSync-c1");
Thread t2 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementMethodSync(); }, "T2-MethodSync-c1");
// This thread operates on a different object c2, so it won't contend with t1, t2 for c1's lock
Thread t_c2 = new Thread(() -> { for(int i=0; i<3; i++) c2.incrementMethodSync(); }, "T_MethodSync-c2");
// Test synchronized block on 'this'
Thread t3 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockThisSync(); }, "T3-BlockThisSync-c1");
// Test synchronized block on dedicated lock
// These two threads can run concurrently if they use different lock objects (lock1 vs lock2)
// or if they operate on different Counter instances.
// If incrementBlockDedicatedLock and anotherOperationWithDifferentLock are called on SAME c1 instance,
// they can run concurrently because they use different locks (lock1, lock2).
Thread t4 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockDedicatedLock(); }, "T4-BlockLock1-c1");
Thread t5 = new Thread(() -> { for(int i=0; i<3; i++) c1.anotherOperationWithDifferentLock(); }, "T5-BlockLock2-c1");
// Test static synchronized method
// Threads will contend for Counter.class lock
Thread t6 = new Thread(() -> { for(int i=0; i<3; i++) Counter.incrementStaticMethodSync(); }, "T6-StaticSync");
Thread t7 = new Thread(() -> { for(int i=0; i<3; i++) Counter.incrementStaticMethodSync(); }, "T7-StaticSync");
System.out.println("--- Starting Method Sync Demo (on c1) ---");
t1.start(); t2.start(); t_c2.start();
t1.join(); t2.join(); t_c2.join();
System.out.println("c1.count after method sync: " + c1.getCount()); // Expected: 6
System.out.println("c2.count after method sync: " + c2.getCount()); // Expected: 3
c1.count = 0; // Reset for next test
System.out.println("\n--- Starting Block 'this' Sync Demo (on c1) ---");
t3.start(); // Let's start another one to see contention
Thread t3_contend = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockThisSync(); }, "T3_Contend-BlockThisSync-c1");
t3_contend.start(); t3.join(); t3_contend.join();
System.out.println("c1.count after block 'this' sync: " + c1.getCount()); // Expected: 6
System.out.println("\n--- Starting Block Dedicated Lock Demo (on c1, different locks) ---");
// t4 and t5 use different locks on the same object c1, so they can run more concurrently
t4.start(); t5.start();
t4.join(); t5.join();
System.out.println("c1.anotherCount after block dedicated lock: " + c1.getAnotherCount()); // Expected: 3
System.out.println("\n--- Starting Static Method Sync Demo ---");
t6.start(); t7.start();
t6.join(); t7.join();
System.out.println("Counter.staticCount after static sync: " + Counter.getStaticCount()); // Expected: 6
}
}
I will continue with the next set of questions.