Java
Java - object-oriented, high-level, general-purpose programming language
Overview
- Data Structures
- Fundamentals
- Garbage Collection (GC)
- Files
- Exceptions
- Generics
- Threads
Overview
Name | Structure | Methods | null | Duplicates | Synchronized | Ordered | Iterator | Enumeration |
---|---|---|---|---|---|---|---|---|
ArrayList | Dynamic Array | add, remove, get, set | ✅ | ✅ | ❌ | Insertion order | ✅ | ❌ |
Vector | Dynamic Array | add, remove, get, set | ✅ | ✅ | ✅ | Insertion order | ✅ | ✅ |
Stack | LIFO | push, pop, peek | ✅ | ✅ | ✅ | No order | ✅ | ✅ |
Queue | FIFO | enqueue, dequeue, peek | ✅ | ✅ | ❌ | No order | ✅ | ❌ |
List | Linear | add, remove, get, set | ✅ | ✅ | ❌ | Insertion order | ✅ | ❌ |
LinkedList | Doubly-Linked List | add, remove, get, set | ✅ | ✅ | ❌ | Insertion order | ✅ | ❌ |
Set | Collection | add, remove, contains | ✅ | ❌ | ❌ | No order | ✅ | ❌ |
HashSet | HashTable | add, remove, contains | ✅ | ❌ | ❌ | No order | ✅ | ❌ |
Map | key-value pair | put, remove, get, containsKey | 1 key and multiple values | only values | ❌ | No order | ✅ | ❌ |
HashMap | key-value pair | add, remove, contains | 1 key and multiple values | only values | ❌ | No order | ✅ | ❌ |
HashTable | key-value pair | put, remove, get, containsKey | ❌ | only values | ✅ | No order | ✅ | ✅ |
TreeSet | Red-black tree | add, remove, contains | ❌ | ❌ | ❌ | Natural order | ✅ | ❌ |
TreeMap | Red-black tree | put, remove, get, containsKey | only values | only values | ❌ | Natural order | ✅ | ❌ |
Collections
Collection Decision
Iterator vs Enumeration
Aspect | Iterator | Enumeration |
---|---|---|
Type | interface | class |
Namespace | java.util | java.util |
Common methods | hasNext / next / remove | hasMoreElements / nextElement |
Use Case | removes elements from collection during the iteration | used for passing through a collection, usually of unknown size |
Object
Superclass class for every class in Java
Methods
clone()
: creates a copy of an objectequals()
: comparesfinalize()
: called on GCgetClass()
: get runtime classhashCode()
: returns a hash code valuetoString()
: string representationnotify()
,notifyAll()
, andwait()
: These help in synchronizing the activities of the independently running threads in a program
Creation
Variant | Syntax |
---|---|
new keyword | new String() |
Class.forName |
|
newInstance() method of Constructor class |
|
clone() method | Creating an object using the clone method does not invoke any constructor
|
deserialization | When we serialize and then deserialize an object, JVM creates a separate object. In deserialization, JVM doesn't use any constructor to create the object |
Initialization
Action | Allocation | Example |
---|---|---|
Declaration | Reference created in stack |
|
Initialization | Allocates memory in heap with reference in stack |
|
Pass by Reference vs Value
- Java is Pass by Value: when you pass a parameter to a method, whether it's a primitive type or an object reference, you are always passing a copy of the value of that parameter
- Passing Object References: When you pass an object to a method, you're actually passing a copy of the reference to that object, not the object itself. This means you're still passing a value (the memory address where the object resides in the heap), but that value refers to the actual object in memory
Explanation
When you pass an object reference to a method, you're passing a copy of the reference to the object. This means changes made to the object's fields within the method will affect the original object because both the original reference and the method's copy of the reference point to the same object in memory.
However, it's important to understand that you cannot change the reference itself within the method. For example, you cannot make the original reference point to a different object by assigning a new object to it within the method.
class MyClass {
int value;
MyClass(int value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass(5);
System.out.println(obj.value); // Output: Before: 5
modifyObject(obj);
System.out.println(obj.value); // Output: After: 10
}
public static void modifyObject(MyClass obj) {
/*
Modifying the reference to the object.
This change will only affect the local copy of the reference within the method itself,
not the original reference passed from the calling method
*/
obj = new MyClass(10);
// Modifying the object's field will also affect the original object
obj.value = 10;
}
/*
Although `obj` is passed by value, meaning `modifyObject()` receives a copy of the reference to the `MyClass` object,
changes made to the object's field `value` within the `modifyObject()` method are reflected in the original object referenced by `obj`.
This is because both the original reference `obj` and the method's copy of the reference point to the same object in memory
*/
}
Feature | Passing by Value | Passing by Reference |
---|---|---|
Definition | Copies the actual value of an argument into the parameter | Copies the reference to the memory location of an object |
Effect on Original Data | Original data remains unchanged | Changes to the parameter affect the original data |
Memory Usage | Requires additional memory for copying values | Doesn't require additional memory for objects |
Example |
|
|
Static Block
- executed at the time of classloading
- can be executed before
main
method - can execute the program with static block and without
main
method
static { System.out.println("test"); }
Constructor
- is not inherited
- cannot be
final
- chaining constructors with
this()
- can be overloaded
- if no constructor is defined, the JVM creates a default no-arg constructor
public class Employee {
int id,age;
String name, address;
public Employee (int age) { this.age = age; }
public Employee(int id, int age) { this(age); this.id = id; }
}
- flow of creation: parent class -> child class
- flow of destruction: child class -> parent class
Class Casting
- cast can be performed only child -> parent
- to check if cast is possible, use
instanceof
class Parent { void display() { } }
class Child extends Parent { void show() { } }
interface MyInterface { void myMethod(); }
class AnotherChild extends Parent implements MyInterface { public void myMethod() { } }
public class Main {
public static void main(String[] args) {
Child child = new Child();
Parent parent1 = (Parent) child; // Casting Child to Parent
parent1.display(); // Accessing method of Parent class
AnotherChild anotherChild = new AnotherChild();
MyInterface myInterface = (MyInterface) anotherChild; // Casting AnotherChild to MyInterface
myInterface.myMethod(); // Accessing method of MyInterface
Parent parent2 = new Parent();
if(parent2 instanceof Child) { // false: Class parent to class child casting (not possible)
// Child child2 = (Child) parent2; // Compilation error: Parent cannot be cast to Child
}
Parent parent3 = new Parent();
if(parent3 instanceof MyInterface) { // Class parent to interface child casting (not possible)
// MyInterface myInterface2 = (MyInterface) parent3; // Compilation error: Parent cannot be cast to MyInterface
}
}
}
this
and super
keywords
this
- current class instance variable
- invoke current class method (implicitly)
this
can be passed as an argument in the method callthis
can be passed as an argument in the constructor callthis
can be used to return the current class instance from the methodthis()
- to invoke the current class constructor- can be used to refer static members (preferred via class name)
- can't assign value (compiler-time error)
super
- parent instance variable
- calls parent class method
super()
- to call parent constructor
Overriding vs Overloading vs Shadowing
Concept | Description | Polymorphism Type | Example |
---|---|---|---|
Overloading | Two or more methods in the same class have the exact same name, but different parameters | Compile Time (Static) |
|
Overriding | Child class redefines the same method as a parent class with the same name, argument list, and return type. May not limit the access of the method it overrides | Run-Time (Dynamic) |
|
Shadowing | Overriding static methods | ❌ |
|
String
Storage area | Mutability | Efficiency | Thread-safe | |
---|---|---|---|---|
String | String pool | immutable | slow | no |
new String | heap | immutable | slow | no |
StringBuilder | heap | mutable | fastest | single-thread |
StringBuffer | heap | mutable | fast | multi-thread |
String Pool
String str1 = "Hi";
String str2 = "Hi";
String str3 = new String("Hi");
String str4 = new String("Hi");
println(str1 == str2); // true (string pool)
println(str1 == str3); // false (string pool != heap object reference)
println(str1.equals(str2)); // true (checks by string value)
println(str1.equals(str3)); // true (checks by string value)
println(str3 == str4); // false (checks by object reference)
println(str3.equals(str4)); // true (checks by string value)
Serialization vs Deserialization
Aspect | Serialization | Deserialization |
---|---|---|
Definition | Process of converting an object into a byte stream for transmission over a network or storage in a file | Process of reconstructing an object from its serialized form, typically from a byte stream |
Purpose | Transferring objects between different applications or across a network | Reconstructing objects from their serialized form for use in an application |
Data Format | Serialized objects are typically stored in a specific format, such as JSON, XML, or binary | Data is read from serialized format and converted back into objects |
Operator precedence
Operator Type | Category | Precedence |
---|---|---|
Unary | postfix | expr++ expr-- |
prefix | ++expr --expr +expr -expr ~ ! | |
Arithmetic | multiplicative | * / % |
additive | + - | |
Shift | shift | << >> >>> |
Relational | comparison | < > <= >= instanceof |
equality | == != | |
Bitwise | bitwise AND | & |
bitwise exclusive OR | ^ | |
bitwise inclusive OR | | | |
Logical | logical AND | && |
logical OR | || | |
Ternary | ternary | ? : |
Assignment | assignment | = += -= *= /= %= &= ^= |= <<= >>= >>>= |
Main Method
static
- to reduce ambiguity- can be
final
- missing
static
in method - program compiles successfully but at runtime throws an errorNoSuchMethodError
- using static block (
static {}
) to execute code beforemain
method
Examples
public static void main(String[] args) throws Exception {} // with `throws`
public static final void main(String[] args) {} // with `final`
public static void main(String[] args) {} // array of strings
public static void main(String args[]) {} // array of strings
public static void main(String... args) {} // vararg
public static void main(String[] args, int additionalArg) {} // additional argument
public interface MyInterface { public static void main(String[] args) {} } // inside interface (Java8+)
Interface vs Abstract Class
Methods | Inheritance | Method Inheritance | Implement Interface | Variable | |
---|---|---|---|---|---|
Interface | all methods implicitly public abstract | implements multiple | class must implement all methods | no | default final |
Abstract Class | can contain default protected public non abstract methods | extends one | not all as an abstract sub-class | yes, w/o implementation | non final |
Default Values
class A {
static Object obj; // null
Object obj; // null
String str; // ""
boolean bool; // false
int i; // 0
float f; // 0.0
double d; // 0.0
byte b; // 0
char c; // '\u0000'
int[] arr; // []
void method() {
int x; // local variables within methods or blocks are not initialized
}
}
Access Modifiers
Class | Sub-Class | Package | Global | |
---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
protected | ✅ | ✅ | ❌ | ❌ |
default | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
Example
package com.example.myapplication;
class MyClass {
private int privateValue = 1;
protected int protectedValue = 2;
int defaultValue = 3;
public int publicValue = 4;
}
package com.example.myapplication;
class SubClass extends MyClass {
void sub() {
privateValue // compile time error
protectedValue // OK
super.protectedValue // OK
defaultValue // OK
publicValue // OK
}
}
package com.example.myapplication;
class PackageClass {
void package() {
privateValue // compile time error
protectedValue // compile time error
super.protectedValue // compile time error
defaultValue // OK
publicValue // OK
}
}
package org.example;
class GlobalClass {
void global() {
privateValue // compile time error
protectedValue // compile time error
defaultValue // compile time error
publicValue // OK
}
}
Non-Access Modifiers
class | method | variable | |
---|---|---|---|
final |
|
|
|
abstract |
|
| ❌ |
static |
|
|
|
synchronized | ❌ |
| ❌ |
native | ❌ |
| ❌ |
transient | ❌ | ❌ |
|
volatile | ❌ | ❌ |
|
strictfp |
|
| ❌ |
Variations & Combinations
Aspect | Possible Combinations |
---|---|
Class Declaration |
|
Class Fields |
|
Method & Interface Inputs |
|
Method Parameters |
|
Method Outputs |
|
Interface Declaration |
|
Interface Fields |
|
Interface Method Outputs |
|
Autoboxing vs Unboxing
- Autoboxing is the automatic conversion made by the Java compiler between the primitive types and their corresponding object wrapper classes
int i = 10;
Integer j = i;
- Unboxing when the conversion goes the other way
Integer i = 10;
int j = i;
- Explicit conversion
int i = 10;
Integer j = Integer.valueOf(i);
Integer x = 10;
int y = x.intValue();
int z = (int) x;
JVM Memory Allocation
Component | Description |
---|---|
class (method) | Stores per-class structures (runtime constant pool, field, method data, and the code for methods) |
heap | Runtime data area in which the memory is allocated to the objects |
stack |
|
program counter register | Contains the address of the JVM instruction currently being executed |
native method stack | Contains all the native methods used in the application |
JVM/JRE/JDK
Feature | JVM | JRE | JDK |
---|---|---|---|
Definition | Java Virtual Machine | Java Runtime Environment | Java Development Kit |
Components | Interpreter, Just-In-Time (JIT) Compiler | Implements the JVM, provides all class libraries, and other support files needed at runtime | Compiler, JRE, development tools (e.g., javac compiler, jar archiver) |
Usage | Executes Java bytecode, platform-independent | Runs Java applications, required for end-users | Developing, compiling, and debugging Java programs |
Inclusion | Part of JRE | Implements the JVM | JRE + Dev Tools |
Purpose | Acts as a run-time engine to run Java applications | Runtime environment in which Java bytecode can be executed, providing all the class libraries and other support files that JVM uses at runtime | Developing, compiling, and running Java applications |
JVM
JVM is a crucial component of the Java Runtime Environment (JRE) responsible for executing Java bytecode. It acts as an abstract computing machine, providing a runtime environment in which Java programs can run regardless of the underlying hardware and operating system.
Key components
- Class Loader: subsystem of the JVM is responsible for loading class files from the file system, network, or other sources and converting them into internal representation for execution
- Bytecode Verifier: before execution, the JVM performs bytecode verification to ensure that the loaded code does not violate any security constraints or Java language rules. This process helps in preventing harmful operations and ensuring type safety
- Memory Management: JVM manages memory allocation and deallocation for Java objects. It includes automatic garbage collection, which identifies and removes unreferenced objects to reclaim memory resources
- Just-In-Time (JIT) Compiler: JVM includes a JIT compiler that translates bytecode into native machine code at runtime for improved performance. It optimizes frequently executed code paths to enhance execution speed
- Execution Engine: Execution Engine interprets and executes Java bytecode instructions. It includes components such as the interpreter, which directly executes bytecode, and the JIT compiler, which compiles bytecode into native machine code for faster execution
- Runtime Data Areas: JVM maintains various runtime data areas such as the method area, heap, stack, PC (Program Counter) register, and native method stack. These areas store class metadata, objects, method invocation stacks, and other runtime information required for program execution
- Security Manager: JVM incorporates a Security Manager that enforces security policies to prevent untrusted code from performing harmful actions such as accessing system resources or executing dangerous operations
- Platform Independence: one of the key features of the JVM is its platform independence. Java programs compiled into bytecode can run on any system with a compatible JVM implementation, making Java a "write once, run anywhere" language
- Dynamic Linking and Loading: JVM supports dynamic linking and loading of classes and libraries at runtime, enabling the extension and modification of Java applications without recompilation
- Monitoring and Management: JVM provides tools and APIs for monitoring and managing its runtime behavior, including performance monitoring, memory profiling, and diagnostic capabilities
ClassLoader
- Classpath Resolution
- The class loading process begins with the JVM needing to load a class that is referenced by the application
- The class loader first checks its cache to see if the class has already been loaded. If found, it retrieves the class from the cache
- If the class is not cached, the class loader proceeds to locate the class file
- The classpath is a list of directories and JAR files where the class loader searches for class files
- The classpath can be set using the
-classpath
or-cp
option when invoking the JVM, or it can be defined by theCLASSPATH
environment variable
- Bootstrap Class Loader
- The bootstrap class loader is responsible for loading core Java classes provided by the JVM
- These classes are typically located in the
lib
directory of the Java installation - Examples of core classes include those in the
java.lang
package, such asObject
,String
, andInteger
- Extension Class Loader
- The extension class loader loads classes from JAR files located in the extensions directory (
$JAVA_HOME/lib/ext
) - This directory contains JAR files that extend the functionality of the JVM
- Classes loaded by the extension class loader are typically part of the Java Standard Extension mechanism
- The extension class loader loads classes from JAR files located in the extensions directory (
- Application Class Loader
- Also known as the system class loader, it loads classes from directories and JAR files specified by the classpath
- It is responsible for loading application-specific classes and 3rd-party libraries
- If a class is not found by the bootstrap or extension class loaders, the application class loader attempts to load it
- Custom Class Loaders
- Java allows developers to create custom class loaders to load classes from non-standard sources or to modify the default loading behavior
- Custom class loaders can be implemented by extending the
java.lang.ClassLoader
class - Developers can override methods such as
findClass()
to define their own class-loading logic
- Delegation Model
- Class loaders in Java follow a delegation model, where each class loader delegates the class-loading request to its parent class loader before attempting to load the class itself
- This ensures that classes are loaded only once and promotes code reuse
- If a class loader cannot find a class, it delegates the request to its parent class loader, and the process continues recursively until the bootstrap class loader is reached
- Resource Loading
- In addition to loading class files, class loaders can also load other types of resources, such as configuration files, images, and XML files
- Resources are located using the same mechanism as class files, by searching directories and JAR files specified in the
classpath
- Loading the Class File
- Once the class loader determines the location of the class file, it reads the binary data representing the class from the file system, network, or other sources
- The class loader then creates a
java.lang.Class
object, representing the loaded class, and stores it in the JVM's method area
- Linking the Class: after loading the class, the JVM performs linking which consists of 3 sub-stages
- Verification
- The JVM verifies the loaded class file to ensure it adheres to the rules of the Java language and the JVM specification
- Verification checks include verifying the format of the class file, examining the bytecode for correctness, and ensuring type safety and security
- Preparation
- During this stage, memory for class variables (static fields) is allocated within the method area
- Static fields are initialized with default values based on their data types (e.g.,
0
for numeric types,null
for objects) - However, the actual initialization of these variables to their proper values is deferred until later
- Resolution
- In this stage, symbolic references in the class file are resolved to direct references to objects or methods in memory
- This involves resolving symbolic references to other classes or interfaces, fields, and methods
- Resolution ensures that the class can interact with other classes and resources at runtime
- Verification
- Initialization
- The final stage involves the actual initialization of the class
- During initialization, the JVM executes any static initializer blocks (
static { }
) defined within the class - Static fields are initialized with explicit initial values specified in the code
- This stage ensures that the class is properly initialized before it is used in the application
JIT (Just-In-Time) Compiler
- used to improve the performance
- compiles parts of the bytecode that have similar functionality at the same time, and hence reduces the amount of time needed for compilation
- compiler - translates JVM instructions to specific CPU instructions
Java Compiler
- create file:
public class A {
public static void main(String[] args) {
System.out.println(8);
}
}
- save (empty name also possible):
.java
- compile:
javac .java
- execute:
java A
Packaging
- Modularity: Organizes related classes
- Namespace Management: Prevents naming conflicts
- Access Control: Defines visibility within packages
- Encapsulation: Hides implementation details
- Dependency Management: Manages component dependencies
- Code Organization: Organizes files for easy navigation
- Distribution & Versioning: Facilitates library distribution and version control
- Standardization: Promotes consistent development practices
Example
package com.example.myapplication;
public class MyClass {
public static void main(String[] args) {
System.out.println(8);
}
}
Garbage Collection (GC)
Process of automatically reclaiming memory occupied by objects that are no longer reachable by the application.
Objects are dynamically allocated memory from a region called the heap. The heap is managed by the JVM and is the storage area for all objects, including arrays.
GC Generations
- Young Generation
- Newly created objects are allocated in the Young Generation
- The Young Generation is further divided into Eden Space and Survivor Spaces (usually two)
- Old Generation (Tenured Generation)
- Objects that survive multiple garbage collection cycles in the Young Generation are promoted to the Old Generation
- The Old Generation holds objects with longer lifetimes
public class GarbageCollectionExample {
public static void main(String[] args) {
Object obj1 = new Object(); // Object 1 created in the Young Generation
Object obj2 = new Object(); // Object 2 created in the Young Generation
obj1 = null; // Object 1 becomes unreachable
System.gc(); // Request garbage collection
// After garbage collection, obj1 is collected from the Young Generation
Object obj3 = new Object(); // Object 3 created in the Young Generation
// Now, let's simulate a scenario where obj2 survives multiple garbage collections
for (int i = 0; i < 10000; i++) {
Object temp = new Object();
if (i % 100 == 0)
temp = obj2; // Reassigning obj2 to simulate its continued use
}
// After repeated garbage collections, obj2 is promoted to the Old Generation
Object obj4 = new Object(); // Object 4 created in the Young Generation
obj3 = null; // Object 3 becomes unreachable
obj4 = null; // Object 4 becomes unreachable
System.gc(); // Request garbage collection
// After garbage collection, obj3 and obj4 are collected from the Young Generation
// Meanwhile, obj2 remains in the Old Generation
} // obj2 is ready for garbage collection
}
GC Types
Aspect | Serial GC | Parallel GC | CMS GC | G1 GC | Shenandoah GC | ZGC | Epsilon GC |
---|---|---|---|---|---|---|---|
Concurrency | Single-threaded | Multi-threaded | Concurrent | Concurrent | Concurrent | Concurrent | No GC (no garbage collection) |
Pause Time | Can have longer pause times | Can have shorter pause times compared to Serial GC | Can have shorter pause times | Can have shorter and more predictable pause times | Can have shorter pause times | Can have shorter and more predictable pause times | No pauses (no garbage collection) |
Throughput | Lower throughput compared to Parallel GC | Higher throughput compared to Serial GC | Higher throughput compared to Serial and Parallel GC for concurrent mode | High throughput with incremental and parallel phases | High throughput with concurrent phases | High throughput with concurrent phases | Does not perform garbage collection |
Heap Size | Small to medium heap sizes | Medium to large heap sizes | Medium to large heap sizes | Large heap sizes | Medium to large heap sizes | Medium to very large heap sizes | N/A (no garbage collection) |
Garbage Collection Algorithm | Serial | Parallel | Concurrent Mark-Sweep | Garbage First | Concurrent | Concurrent | No GC (no garbage collection) |
JDK Version | All versions | All versions | Until Java 14 (deprecated in Java 9) | Java 7 onwards | Java 12 onwards | Java 11 onwards | Java 11 onwards |
Usability | Simple and lightweight | Easy to use | Requires tuning; deprecated in newer versions | Self-tuning; default in recent JDK versions | Requires specific JDK version | Requires specific JDK version | For performance testing and troubleshooting |
File I/O
Path path = Path.of("test.txt");
boolean isExists = Files.exists(path);
String content = Files.readString(path, Charsets.UTF_8);
/**
* CREATE - creates if not exists
* TRUNCATE_EXISTING - remove all content
* APPEND - adds to the file
* WRITE - write access
*/
Files.writeString(path, "some text", StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
Exception Tree
Structure
try {
// Code that may throw an exception
} catch (ExceptionType1 e1) {
// Handle ExceptionType1
} catch (ExceptionType2 e2) {
// Handle ExceptionType2
} finally {
// Optional: Code to be executed regardless of whether an exception occurred or not
// used to release resources like I/O buffers, database connections, etc
}
Try-With-Resources
// Specify the resources within the try-with-resources block
try (BufferedReader fileInput = new BufferedReader(new FileReader("file.txt"))) {
// Read from the file
String line;
while ((line = fileInput.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println(e.getMessage());
}
// No need to explicitly close the resources, it's automatically handled by try-with-resources
throws
public class ThrowsExample {
// Method that declares it may throw an IOException
public static void methodThatThrows() throws IOException { throw new IOException(); } // Simulating an IOException
// Method that calls methodThatThrows and handles the IOException
public static void callerMethod() {
try {
methodThatThrows(); // Call methodThatThrows
} catch (IOException e) {
System.err.println(e.getMessage()); // Handle the IOException
}
}
public static void main(String[] args) { callerMethod(); } // Call callerMethod
}
Order
When an exception occurs, the catch blocks are evaluated in the order they appear in the code. The first catch block that matches the type of the thrown exception or one of its superclasses will be executed. Therefore, catch blocks for more specific exceptions should come before catch blocks for more general exceptions to ensure that exceptions are handled in the most appropriate way. If no catch block is found that matches the exception type, the exception will propagate up the call stack
Exception Precedence
public class ExceptionOrderHandlingExample {
public static void indexOutOfBoundsException() {
var arr = {1, 2, 3};
int value = arr[3]; // Accessing an index that is out of bounds
}
public static void arithmeticException() { int result = 10 / 0; } // Attempting division by zero
public static void main(String[] args) {
try {
indexOutOfBoundsException(); // exception raised
arithmeticException(); // won't be executed
} catch (ArithmeticException e) {
System.err.println(e.getMessage()); // skipped
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println(e.getMessage()); // caught exception
} catch (Exception e) {
System.err.println(e.getMessage()); // didn't reach
}
}
}
Rules for Subclass Overriding Superclass Methods
class Superclass {
void doSomething() { } // Superclass method that doesn't declare any exception
}
class Subclass extends Superclass {
/* Subclass overridden method attempting to declare a checked exception
This will result in a compilation error because the superclass method doesn't declare it
but it can declare an unchecked exception
So, we will declare an unchecked exception instead
*/
@Override
void doSomething() throws RuntimeException { throw new RuntimeException(); }
}
public class Main {
public static void main(String[] args) {
Superclass obj = new Subclass(); // Upcasting
obj.doSomething(); // Calls overridden method in Subclass
}
}
Exception Declaration Flexibility in Subclass Methods
If a superclass method declares an exception, a subclass's overridden method can declare the same exception, a subclass exception, or no exception at all. However, it cannot declare a parent exception
class Superclass {
void doSomething() throws Exception { } // Superclass method that declares a checked exception
}
class Subclass extends Superclass {
@Override
void doSomething() throws Exception { } // Subclass overridden method can declare the same exception
}
class AnotherSubclass extends Superclass {
@Override
void doSomething() throws IllegalArgumentException { } // Subclass overridden method can declare a subclass exception
}
public class Main {
public static void main(String[] args) {
Superclass obj1 = new Subclass(); // Upcasting
Superclass obj2 = new AnotherSubclass(); // Upcasting
try {
obj1.doSomething(); // Calls overridden method in Subclass
obj2.doSomething(); // Calls overridden method in AnotherSubclass
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exception Propagation vs Chaining
Aspect | Exception Chaining | Exception Propagation |
---|---|---|
Definition | Exception chaining is the process of associating one exception with another, providing more context about the cause of the exception | Exception propagation refers to the process by which an exception is passed from one method to another if it is not caught and handled in the method where it occurs |
Triggering | Typically occurs when an exception is caught in one method and rethrown with additional context or information | Automatically occurs when an exception is thrown in a method and not caught, causing it to propagate up the call stack to the caller method |
Purpose | Provides more detailed information about the cause of an exception, allowing better understanding and debugging of the issue | Allows handling of exceptions at an appropriate level in the call stack, providing a mechanism to deal with errors at higher levels of abstraction |
Mechanism | The caught exception is wrapped with another exception that provides additional context, often using the constructor that accepts a String message and a Throwable cause | The exception is propagated up the call stack until it is caught and handled or until it reaches the top-level caller, typically using try-catch blocks |
Example |
|
|
Custom Exception
public class CustomException extends Exception {
public CustomException(String message) { super(message); }
}
Multi-Threaded Environment
public class MultiThreadedExceptionHandler {
public static void main(String[] args) {
// Create and start multiple threads
new Thread(new MyRunnable()).start();
new Thread(new MyRunnable()).start();
}
// Runnable class to be executed by threads
static class MyRunnable implements Runnable {
@Override
public void run() {
try {
// Simulate an exception
if (Math.random() < 0.5) {
throw new RuntimeException("Simulated Exception");
}
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
System.err.println(e.getMessage()); // when the thread is interrupted during sleep
} catch (RuntimeException e) {
System.err.println(e.getMessage()); // any other exceptions that occur during execution
}
}
}
}
Pros & Limitations
- Pros
- enable developers to create reusable, type-safe code, enhancing both the flexibility and safety of programs
- enabling operations on various object types
- aim to bolster compile-time type checks and prevent
ClassCastException
errors during runtime - enable reusable code components by allowing classes and methods to function with any data type
- Limitations
- don't directly support primitive types; instead, their wrapper classes must be used (e.g.,
Integer
forint
) - Type erasure, which occurs with generics, can result in unexpected behavior, particularly with reflection or low-level type manipulation
- don't directly support primitive types; instead, their wrapper classes must be used (e.g.,
Syntax
Generic Class Declaration
class ClassName<T> { }
Generic Method Declaration
<T, R> R methodName(T arg) { }
Generic Interface
interface Pair<T> {
void setFirst(T first);
T getFirst();
}
class StringPair implements Pair<Integer> {
private Integer first;
public void setFirst(Integer first) { this.first = first; }
public Integer getFirst() { return first; }
}
public class Main {
public static void main(String[] args) {
StringPair pair = new StringPair();
pair.setFirst(1);
System.out.println(pair.getFirst());
}
}
Bounded Type Parameters
public class Example<T extends Number> { } // restrict the types that can be used: T can only be a subclass of Number
Wildcards
List<?> list; // list of unknown (any) type
Context Switching
- technique where CPU time is shared across all running processes and processor allocation is switched in a time bound fashion
- to schedule a process to allocate the CPU, a running process is interrupted to a halt and its state is saved
- the process that has been waiting or saved earlier for its CPU turn is restored to gain its processing time from the CPU
- this gives an illusion that the CPU is executing multiple tasks, while in fact a part of the instruction is executed from multiple processes in a round robin fashion
Thread vs Process
Feature | Java Threads | Processes |
---|---|---|
Creation | Created within a Java Virtual Machine (JVM) | Spawned by the operating system |
Overhead | Lightweight | More heavyweight due to OS involvement |
Communication | Direct communication within the same JVM | Inter-process communication required |
Memory Sharing | Threads share memory space | Processes have separate memory spaces |
Resource Usage | Consumes less system resources | Consumes more system resources |
Context Switching | Faster context switching within a thread group | Slower context switching due to OS involvement |
Synchronization | Easier synchronization using built-in mechanisms | Requires explicit synchronization mechanisms |
Data Sharing | Shared memory model | Requires inter-process communication for data sharing |
Scalability | Easier to scale within a JVM | Scaling may involve more complexity due to inter-process communication |
Coordination | Easier coordination between threads | Inter-process coordination may be more complex |
Error Isolation | Errors in one thread can affect others within JVM | Errors are contained within the process boundary |
Deadlock Risks | Risk of deadlock within a thread group | Deadlock risks are typically more manageable |
Programming Difficulty | Typically easier to program and manage | May require more careful programming and management |
Performance Impact | Less overhead, typically better performance | More overhead, potentially lower performance |
Fault Tolerance | Less fault tolerant due to shared memory | More fault tolerant due to separate memory spaces |
Parallelism | Suitable for fine-grained parallelism | Suitable for coarse-grained parallelism |
Thread States
- NEW — a new thread instance that was not yet started via
Thread.start()
- RUNNABLE — running thread, enters when
Thread.start()
called - BLOCKED — running thread becomes blocked if it needs to enter a synchronized section but cannot do that due to another thread holding the monitor of this section
- WAITING — a thread enters this state if it waits for another thread to perform a particular action: Object.wait(), Thread.join()
- TIMED_WAITING — same as the above, but a thread enters this state after calling timed versions: Thread.sleep(), Object.wait()
- TERMINATED — a thread has completed the execution of its Runnable.run() method and terminated or exception happened
Concurrency Utilities
java.util.concurrent
package- collections:
ConcurrentHashMap
,ConcurrentLinkedQueue
- utilities:
CountDownLatch
,Semaphore
,CyclicBarrier
- atomic variables:
AtomicInteger
,AtomicBoolean
,AtomicLong
Runnable vs Callable
Aspect | Method | Return value or throw unchecked exceptions |
---|---|---|
Runnable | run() | ❌ |
Callable | call() | ✅ |
Thread Initialization
Extending the Thread
class
class MyThread extends Thread {
public void run() { /* Code to be executed by the thread*/ }
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
Implementing the Runnable
interface
class MyRunnable implements Runnable {
public void run() { /* Code to be executed by the thread */ }
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
Using lambda expressions with Runnable
public class Main {
public static void main(String[] args) {
Runnable myRunnable = () -> { /* Code to be executed by the thread */ };
Thread thread = new Thread(myRunnable);
thread.start();
}
}
Anonymous class with Runnable
public class Main {
public static void main(String[] args) {
Runnable myRunnable = new Runnable() {
public void run() { /* Code to be executed by the thread */ }
};
Thread thread = new Thread(myRunnable);
thread.start();
}
}
Using ExecutorService
and Callable
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callableTask = () -> {
// Code to be executed by the thread
return 8;
};
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(callableTask);
String result = future.get();
System.out.println(result);
executorService.shutdown();
}
}
Daemon Thread
- thread that does not prevent JVM from exiting
- used to carry out some supportive or service tasks for other threads
- they may be abandoned at any time
- not good for I/O operations due to not closing the resources
Thread daemon = new Thread(() -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();
Interrupt Flag (InterruptedException)
- internal Thread flag that is set when the thread is interrupted via
thread.interrupt()
- if a thread is inside of the methods that throw InterruptedException (wait, join, sleep), then it throws InterruptedException
- else nothing special happens. It is thread's responsibility to periodically check the interrupt status via
Thread.interrupted()
(clears the interrupt flag),isInterrupted()
(doesn't clear flag)
- else nothing special happens. It is thread's responsibility to periodically check the interrupt status via
Executor vs ExecutorService
Executor
- interface with a singleexecute
method. Interface that your task-executing code should depend onExecutorService
- extends theExecutor
interface with multiple methods for handling and checking the lifecycle of a concurrent task execution serviceThreadPoolExecutor
- for executing tasks using a pool of threadsScheduledThreadPoolExecutor
- allows to schedule task execution instead of running it immediately when a thread is available
Deadlock, Livelock, Starvation
- Deadlock - condition within a group of threads that cannot make progress because every thread in the group has to acquire some resource that is already acquired by another thread in the group
- when two threads need to lock both of two resources to progress, the first resource is already locked by one thread, and the second by another. These threads will never acquire a lock to both resources and thus will never progress
- situation in which two (or more) threads are each waiting on the other thread to free a resource that it has locked, while the thread itself has locked a resource the other thread is waiting on
- mutual exclusion
- resource that can be accessed only by one thread at any point in time
- optimistic locking
- resource holding
- while having locked one resource, the thread tries to acquire another lock on some other exclusive resource
- thread may release all its exclusive locks, when it does not succeed in obtaining all exclusive locks
- no preemption
- there is no mechanism, which frees the resource if one thread holds the lock for a specific period of time
- using a timeout for an exclusive lock frees the lock after a given amount of time
- circular wait
- during runtime a constellation occurs in which two (or more) threads are each waiting on the other thread to free a resource that it has locked
- when all exclusive locks are obtained by all threads in the same sequence, no circular wait occurs
- Livelock - case of multiple threads reacting to conditions, or events, generated by themselves
- An event occurs in one thread and has to be processed by another thread. During this processing, a new event occurs which has to be processed in the first thread, and so on. Such threads are alive and not blocked, but still, do not make any progress because they overwhelm each other with useless work
- Starvation - case of a thread unable to acquire resource because other thread (or threads) occupy it for too long or have higher priority
- thread cannot make progress and thus is unable to fulfill useful work
Lock Interface vs Synchronized Block
- there are two separate locks for reading and writing, which enables write high-performance data structures, like ConcurrentHashMap and conditional blocking
wait
vs sleep
action | usage | |
---|---|---|
wait | release the lock and must be called from the synchronized context | inter-thread communication |
sleep | pause the thread for some time and keep the lock | introduce pause on execution |
Share data
- shared objects
- Queue (shared data structures)
synchronized
keyword
Volatile vs Synchronized Method
volatile
- only synchronizes the value of one variable between the thread memory and the "main" memory
- forces all accesses (read or write) to the volatile variable to occur to main memory, effectively keeping the volatile variable out of CPU caches
synchronized
- synchronizes the value of all variables between the thread memory and the "main" memory and locks and releases a monitor to control the ownership between multiple threads
- prevents any other thread from obtaining the monitor (or lock) for the same object
Thread start()
vs run()
- when you call the
start()
method, it creates a new thread and executes code declared in therun()
- calling the
run()
method doesn't create any new threads and executes code on the same calling thread
Awake and Blocked Thread
- blocking can result in many ways
- IO - no way
wait()
,sleep()
,join()
- can interrupt the thread, and it will awake by throwingInterruptedException
Handle an unhandled exception in the thread
- use
UncaughtExceptionHandler
ThreadLocal
- As memory is shared between different threads,
ThreadLocal
provides a way to store and retrieve values for each thread separately - can provide different objects for each working thread
- to avoid synchronizing access to that object
SimpleDateFormat
(give each thread its own instance of the object)
- can be used to transport information throughout the application without the need to pass this from method to method
- to avoid synchronizing access to that object
Thread Safety
- can be used safely by any thread without additional synchronization, even when synchronization is not used to publish them
- not setter methods
- all fields should be
final
andprivate
- if a field is a mutable object create defensive copies of it for getter methods
- if a mutable object passed to the constructor must be assigned to a field create a defensive copy of it
- don't allow sub-classes to override methods
Thread Pool vs Group
Feature | Thread Pools | Thread Groups |
---|---|---|
Purpose | Manages and reuses a pool of worker threads. | Provides a way to organize and manipulate threads. |
Creation | Instantiated using ExecutorService or its subclasses like ThreadPoolExecutor. | Created directly using the ThreadGroup class. |
Management | Automatically manages the lifecycle of threads in the pool (creation, execution, termination). | Provides a way to organize threads hierarchically. |
Usage | Suitable for scenarios with a large number of short-lived tasks, where thread reuse is beneficial. | Useful for scenarios requiring hierarchical organization of threads, such as categorizing threads based on their functionality. |
Thread control | Limited control over individual threads in the pool. | Provides control over multiple threads as a group. |
Task submission | Tasks are submitted to the pool using execute() or submit() methods. | Threads are created independently and can be added to the thread group using the constructor specifying the thread group. |
Thread states | Threads in a pool can be in various states like RUNNABLE, WAITING, TIMED_WAITING, or TERMINATED. | Threads in a group can have different states as defined in the Thread.State enum, like NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, or TERMINATED. |
Dynamic resizing | Some thread pool implementations allow dynamic resizing based on workload. | Thread groups do not support dynamic resizing; once a thread is added to a group, it remains part of that group. |
Thread affinity | No direct control over which thread executes a particular task. | Threads in a group share certain characteristics and can be managed together. |
Hierarchy | Does not inherently support hierarchical organization of threads. | Supports hierarchical organization of threads, where a group can contain other groups and threads. |
Error handling | Exception handling within a task must be managed within the task itself. | Exception handling can be centralized within the thread group, allowing for a unified approach to error handling. |
Performance | Optimal for managing a large number of tasks with a limited number of threads, reducing overhead associated with thread creation and destruction. | Overhead associated with thread groups is generally minimal, but they may not be as efficient for managing individual tasks compared to thread pools. |
Flexibility | Provides flexibility in managing thread lifecycle and resource utilization through various configuration options. | Offers flexibility in organizing threads hierarchically, which can be useful for certain types of applications like monitoring or logging. |
Resource management | Provides better resource management by reusing threads, thus reducing overhead. | Primarily used for organizational purposes and does not directly impact resource management. |
Example |
|
|
Virtual Threads
Lightweight, highly efficient threads. Unlike traditional Java threads, which are mapped directly to native operating system threads, virtual threads are managed at the JVM level, allowing for more efficient use of system resources and greater scalability.
- Lightweight: Virtual threads have minimal memory overhead and quick creation/destruction, ideal for efficiently managing numerous threads
- Highly Scalable: Their lightweight nature enables virtual threads to scale to large numbers without excessive overhead, perfect for highly concurrent applications
- Simplified Concurrency: Virtual threads offer an intuitive and efficient model for concurrent task management, utilizing familiar Java concurrency constructs
- Interoperability: Seamlessly integrating with existing Java APIs and libraries, virtual threads ensure compatibility with frameworks like
CompletableFuture
and Stream API, without requiring significant codebase changes - Improved Performance: Managed at the JVM level, virtual threads optimize thread scheduling and resource allocation, reducing overhead in thread operations and enhancing application responsiveness and throughput
Create a Virtual Thread
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println(2);
});
System.out.println(1);
virtualThread.join();
System.out.println(3);
}
}
Synchronization with Virtual Threads
public class VirtualThreadSynchronizationExample {
static int sharedCounter = 0;
public static void main(String[] args) {
Thread virtualThread1 = Thread.startVirtualThread(() -> {
synchronized (VirtualThreadSynchronizationExample.class) {
for (int i = 0; i < 1000; i++) {
sharedCounter++;
}
}
});
Thread virtualThread2 = Thread.startVirtualThread(() -> {
synchronized (VirtualThreadSynchronizationExample.class) {
for (int i = 0; i < 1000; i++) {
sharedCounter++;
}
}
});
try {
virtualThread1.join();
virtualThread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sharedCounter);
}
}
CompletableFuture with Virtual Threads
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println(1);
});
future.join(); // Wait for the CompletableFuture to complete
System.out.println(2);
}
}
Practice
Practice
public static void main(String[] args) {
if (true)
break; // compile-time error: `break` is used inside switch and loops
}
public static void main(String[] args) {
System.out.println('j' + 'a' + 'v' + 'a'); // 106 + 97 + 118 + 97 = 418
}
public class Demo {
public static void main(String[] arr) {
Integer num1 = 100;
Integer num2 = 100;
Integer num3 = 500;
Integer num4 = 500;
if(num1==num2) { // num1 == num2
System.out.println("num1 == num2");
} else {
System.out.println("num1 != num2");
}
if(num3 == num4) { // num3 != num4
System.out.println("num3 == num4");
} else {
System.out.println("num3 != num4");
}
}
/*
Integer class has a caching range of -128 to 127. Whenever a number is between this range and autoboxing is used, it assigns the same reference
That's why for value 100, both num1 and num2 will have the same reference
For the value 500 (not in the range of -128 to 127), num3 and num4 will have different reference
*/
}
public static void main (String args[]) {
System.out.println(10 + 20 + "World"); // 30World
System.out.println("World" + 10 + 20); // World1020
System.out.println("World" + 10 * 20); // World200
}
class Test {
public static void main (String args[]) {
for(int i=0; 0; i++) // compile-time error: for loop demands a boolean value in the second part and we are providing an integer value, i.e., 0
{
System.out.println("Hello World");
}
}
}
class Test { int i; }
public class Main {
public static void main (String args[]) {
Test test = new Test();
System.out.println(test.i); // 0
}
}
class Test {
int test_a, test_b;
Test(int a, int b) {
test_a = a;
test_b = b;
}
public static void main (String args[]) {
Test test = new Test(); // compile-time error: no default constructor
System.out.println(test.test_a + test.test_b);
}
}
class OverloadingCalculation3 {
void sum(int a,long b){ System.out.println(1); }
void sum(long a,int b){ System.out.println(2); }
public static void main(String args[]) {
OverloadingCalculation3 obj = new OverloadingCalculation3();
obj.sum(20,20); // runtime error: ambiguity
}
}
class Base {
void method(int a) {
System.out.println(a);
}
void method(double d) {
System.out.println(d);
}
}
class Derived extends Base {
@Override
void method(double d) {
System.out.println(d);
}
}
public class Main {
public static void main(String[] args) {
new Derived().method(10); // Base class method called with integer a = 10
}
}
class Base {
public void baseMethod() {
System.out.println(1);
}
}
class Derived extends Base {
public void baseMethod() {
System.out.println(2);
}
}
public class Test {
public static void main (String args[]) {
Base b = new Derived();
b.baseMethod(); // Derived method called. Output: 2
}
}
abstract class Calculate {
abstract int multiply(int a, int b);
}
public class Main {
public static void main(String[] args) {
int result = new Calculate() {
@Override
int multiply(int a, int b) {
return a * b;
}
}.multiply(12,32);
System.out.println(result); // 384
}
}
class Calculation extends Exception {
public Calculation() {
System.out.println(1);
}
public void add(int a, int b) {
System.out.println(a+b);
}
}
public class Main {
public static void main(String []args) {
try {
throw new Calculation();
} catch(Calculation c) {
c.add(10,20); // Calculation class is instantiated. Output: 1, 30
}
}
}
public class Main {
void a() {
try {
System.out.println(1);
b(); // 2. method called
}catch(Exception e) {
System.out.println(2); // 8. Exception is caught
}
}
void b() throws Exception {
try{
System.out.println(3);
c(); // 3. method called
}catch(Exception e) { // 5. Exception is caught
throw new Exception(); // 6. throw exception
} finally {
System.out.println(4); // 7. finally block is called
}
}
void c() throws Exception {
throw new Exception(); // 4. throw exception
}
public static void main (String args[]) {
Main m = new Main();
m.a(); // 1. method called
}
}
public class Calculation {
int a;
public Calculation(int a) {
this.a = a;
}
public int add() {
a = a+10;
try {
a = a+10;
try {
a = a*10;
throw new Exception();
}catch(Exception e){
a = a - 10;
}
}catch(Exception e) {
a = a - 10;
}
return a;
}
public static void main (String args[])
{
Calculation c = new Calculation(10);
int result = c.add();
System.out.println("result = " + result); // result = 290
}
}
public class Test {
public static void main (String args[]) {
String s1 = "Hello";
String s2 = new String("Hello");
s2 = s2.intern();
System.out.println(s1 == s2); // true
/*
The intern method returns the String object reference from the string pool.
In this case:
s1 is created by using string literal
s2 is created by using the String pool
However, s2 is changed to the reference of s1, and the operator == returns true
*/
}
}
class Simple{
public Simple() { System.out.println(1); } // 1. Constructor is invoked
void message(){ System.out.println(2); } // 2. method is invoked
}
class Test1{
public static void main(String args[]) {
try {
Class c = Class.forName("Simple");
Simple s = (Simple) c.newInstance();
s.message(); // Output: 1, 2
} catch(Exception e) {
System.out.println(e);
}
}
}
public class Test {
Test(int a, int b) { }
Test(int a, float b) { }
public static void main (String args[]) {
byte a = 10;
byte b = 15;
Test test = new Test(a,b); // a = 10 b = 15
/*
Here, the data type of the variables a and b,
i.e., byte gets promoted to int,
and the first parameterized constructor with the 2 integer parameters is called
*/
}
}
public class Test{
Test() {
super(); // only 1 call can be made super() or this()
this(); // compile-time error: call to this must be first statement in constructor
System.out.println(1);
}
public static void main(String []args) {
Test t = new Test();
}
}
public class Animal {
void consume(int a) { System.out.println(a); }
static void consume(int a) { System.out.println(a); }
public static void main (String args[]) {
Animal a = new Animal();
a.consume(10); // compile-time error: method consume(int) is already defined in class Animal
Animal.consume(20); // compile-time error: non-static method consume(int) cannot be referenced from a static context
}
}
class Main {
public static void main(String args[]) {
final int i;
i = 20;
System.out.println(i); // 20
}
}
String s = new String("Welcome"); // 2 objects, one in string constant pool and other in non-pool (heap) is created
public static void main (String args[]) {
String a = new String("Hello");
String b = "Hello";
if(a == b) { System.out.println(1); } // false
if(a.equals(b)) { System.out.println(2); } // true
}