You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Think of the classpath as a roadmap that tells the Java Virtual Machine (JVM) where to look for the .class files (compiled Java code) and libraries (.jar files) that your program needs to run. It's essentially a list of directories and files that the JVM searches through to find the classes and resources your code references.
How is it Set?
Environment Variable: You can set the CLASSPATH environment variable to list the directories and JAR files you want to include.
Command Line: When running a Java program (java command), you can use the -cp or -classpath option to specify the classpath directly.
Manifest Files: JAR files can contain a manifest file that specifies the classpath for the classes inside the JAR.
Build Tools: Build tools like Maven or Gradle automatically manage the classpath for your project, making it easier to build and run your code.
java -cp /path/to/my/project:/path/to/library.jar MyMainClass This command tells the JVM to look for classes in /path/to/my/project and /path/to/library.jar when running MyMainClass.
Spring Boot simplifies classpath management through "starter" dependencies. When you include a starter in your project (e.g., spring-boot-starter-web), it automatically includes all the necessary libraries and their dependencies on the classpath. This makes it much easier to set up and run Spring Boot applications.
Object-Oriented Programming (OOP):
Classes and Object
Class: A blueprint or template for creating objects. It defines the structure (attributes/fields) and behavior (methods) that objects of that class will have.
Object: An instance of a class.
Dog myDog = new Dog(); // Object
Constructors: Special method. Constructors typically initialize the object's fields with default or provided values.
Nested Classes: Classes defined within another class. Two Types.
Static Nested Classes (Inner Classes)
Declaration: Declared within another class using the static keyword.
Independence: Act like regular top-level classes, except their name is scoped within the outer class (e.g., OuterClass.StaticNestedClass).
Object Creation: You can create instances of static nested classes without an instance of the outer class.
Access: Can access only static members (variables and methods) of the outer class.
Non-Static Nested Classes (Inner Classes)
- Declaration: Declared within another class without the static keyword.
- Dependency: Intimately tied to an instance of the outer class. An instance of the inner class must be associated with an instance of the outer class.
- Object Creation: You need an instance of the outer class to create an instance of the inner class.
- Access: Can access both static and non-static members of the outer class.
// Object CreationOuterClassouter = newOuterClass(); // First create an instance of the outer classOuterClass.InnerClassinner = outer.newInnerClass(); // Then create inner class instanceinner.display();
Static Members (Fields and Methods)
What they are: Members (fields or methods) that belong to the class itself, not to any particular object of the class.
Static Fields
Access: Accessed using the class name (e.g., MyClass.staticField).
Shared Among All Instances: A static field has a single copy that is shared by all instances of the class. Any changes to a static field are reflected in all objects.
Access: You can access static fields using the class name (e.g., ClassName.staticField) without needing to create an object of the class.
Initialization: Static fields are typically initialized when the class is first loaded into memory.
Use Cases: Static fields are often used for constants, counters to track the number of objects created, or to store information that's shared across all instances.
Static Methods
Access: Static methods can access and modify static fields, but they cannot access non-static (instance) fields or methods directly.
Control Visibility: Determine what parts of your code can access a class, field, or method.
public: Accessible from anywhere.
private: Accessible only within the same class.
protected: Accessible within the same package and subclasses.
default (no modifier): Accessible within the same package.
Reflection in Java
Reflection is a powerful feature in Java that allows you to inspect and manipulate classes, objects, fields, and methods at runtime. It's like having a toolkit that lets you explore and modify the structure and behavior of your code dynamically.
Use Cases: When you use @Autowired, Spring uses reflection to determine the types of dependencies a bean needs and to inject them automatically.
Use Cases: Spring Data JPA uses reflection to analyze your repository interfaces and dynamically generate implementations for common CRUD (Create, Read, Update, Delete) operations.
Use Cases: Actuator endpoints often use reflection to collect information about your application at runtime
Uses Cases: In Spring MVC the DispatcherServlet uses reflection to determine which controller method to call based on the incoming request's URL and HTTP method.
Use Cases: Testing frameworks like JUnit and Mockito frequently use reflection to manipulate objects under test (e.g., setting private fields, invoking private methods).
Abstract Classes
An abstract class is a class that cannot be instantiated (you can't create objects directly from it). It serves as a blueprint for other classes, providing a common structure and set of methods that subclasses must implement or override.
Abstract Methods: An abstract class can contain abstract methods, which are methods declared without an implementation (just a signature). Subclasses are required to provide concrete implementations for these methods.
Concrete Methods: Abstract classes can also contain concrete methods (methods with an implementation) that are inherited by subclasses.
Inheritance: Abstract classes are meant to be extended by other classes, which inherit their properties and methods.
Polymorphism: Abstract classes facilitate polymorphism, allowing you to treat objects of different subclasses interchangeably through a common superclass reference.
An interface in Java is a contract that defines a set of methods that a class must implement. It's a way to achieve abstraction and define a common behavior for a group of unrelated classes. Unlike classes, interfaces cannot be instantiated (you can't create objects directly from them). They only declare method signatures (names, parameters, and return types), but not their implementations.
Abstract Methods: Interfaces contain only abstract methods (methods without a body) by default.
Default Methods: Since Java 8, interfaces can have default methods with implementations.
Static Methods: Interfaces can also have static methods, which are associated with the interface itself rather than any particular object.
Multiple Inheritance: A class can implement multiple interfaces, providing a way to achieve a form of multiple inheritance in Java.
abstractclassAnimal {
publicabstractvoidmakeSound();
}
classDogextendsAnimal { // Error: Must implement all abstract methods// No implementation of makeSound()
}
Explanation: Concreteclassesextendingabstractclassesmustprovideimplementationsforallabstractmethodsinheritedfromtheabstractclass.
interfaceShape {
doublecalculateArea();
}
classCircleimplementsShape { // Error: Must implement all interface methods// No implementation of calculateArea()
}
Not OK (Ambiguity (Not a Direct Error))
interfaceA {
defaultvoidshow() { System.out.println("A's show method"); }
}
interfaceBextendsA {}
interfaceCextendsA {}
classDimplementsB, C {
// No implementation of show() leads to ambiguity
}
Extend vs Implement
Extends
Establishes an "is-a" relationship between classes. The subclass inherits the properties and methods of the superclass.
A class extends another class.
You can only extend one class at a time (no multiple inheritance in Java for classes).
The subclass can override methods from the superclass to provide its own implementation.
Implement
Establishes a "can-do" relationship between a class and an interface. The class agrees to provide implementations for all the methods defined in the interface.
Defines a contract that the class must fulfill.
You can use both extends and implements together. A class can extend one class and implement multiple interfaces.
abstractclassAnimal {
// ... some methods
}
interfacePet {
voidplay();
}
classDogextendsAnimalimplementsPet {
// ... must implement play() from Pet, inherits from Animal
}
Scenario
Use extends
Use implements
Creating a subclass with additional functionality
✅
❌
Defining a contract for a class to follow
❌
✅
Wanting to reuse existing code (inheritance)
✅
✅
Needing multiple inheritance-like behavior
❌
✅
Inheritence
Inheritance is a mechanism in which a new class (the child class or subclass) is created from an existing class (the parent class or superclass).
Parent Class (Superclass): The base class from which other classes inherit.
Child Class (Subclass): A class that inherits from a parent class.
"Is-A" Relationship: Inheritance represents an "is-a" relationship. A child class is a type of the parent class (e.g., a Car is a Vehicle).
Polymorphism: Inheritance allows objects of different classes to be treated as instances of a common superclass.
classParentClass {
// ... fields and methods ...
}
classChildClassextendsParentClass {
// ... additional fields and methods ...
}
Single Inheritance: A class inherits from only one parent class. Java supports only single inheritance directly.
Multilevel Inheritance: A class inherits from a parent class, which itself inherits from another parent class, forming a chain.
Hierarchical Inheritance: Multiple classes inherit from a single parent class.
Overriding: Child classes can override methods inherited from the parent class to provide their own implementations.
Use Case Example: Creating custom error handlers by extending ResponseEntityExceptionHandler and overriding methods to handle specific exceptions.
Super Keyword: The super keyword is used to refer to the parent class's members.
Constructors in Inheritance: Child class constructors implicitly or explicitly call the parent class constructor using super().
Use Case Example: You often create controllers by extending the @RestController or @Controller classes, inheriting their web request handling capabilities.
Object Class: The ultimate parent class of all classes in Java. It provides methods like equals(), hashCode(), and toString(), which you can override in your classes.
A final method within a class cannot be overridden by any subclasses. This helps maintain the integrity of a method's implementation.
classPaymentProcessor {
finalvoidprocessPayment(doubleamount) {
// Logic to process the paymentSystem.out.println("Processing payment of $" + amount);
}
}
classCreditCardProcessorextendsPaymentProcessor {
// Cannot override processPayment() because it's final
}
Constants
Constants are variables whose values are fixed and cannot be changed once they are assigned.
Java uses the following keywords to create constants:
final: This keyword indicates that the variable's value cannot be changed.
static: This keyword makes the constant accessible without creating an object of the class.
publicstaticfinaldatatypeCONSTANT_NAME = value;
// Accessing static constants directly using the class namedoublecircumference = 2 * ConstantsDemo.PI * 10; // Calculating circumference using the constant PI
Constants are typically declared within a class, often in a separate Constants class or as members of relevant classes where they are used. You can also declare them within interfaces.
Functional Programming
Functional programming (FP) is a programming paradigm. It emphasizes the use of pure functions that don't have side effects and always return the same output for a given input.
Pure Functions: These functions produce the same output given the same input, without causing any side effects (like changing external variables).
Immutability
Higher-Order Functions: Functions can take other functions as arguments and return functions as results.
Java, from version 8 onwards, has introduced features that enable a functional programming style:
Lambda Expressions: Concise way to represent functions. Comparator<String> comparator = (str1, str2) -> str1.compareTo(str2);
Functional Interfaces: Interfaces with a single abstract method (SAM). Examples: Predicate, Function, Consumer, Supplier.
Streams API
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
intsum = numbers.stream()
.filter(n -> n % 2 == 0) // Keep even numbers
.mapToInt(Integer::intValue) // Convert to ints
.sum(); // Calculate the sum
Other Common Programming Paradigms:
Imperative Programming: This is the most common style, focusing on describing how a program operates step-by-step. It emphasizes changing program state through statements like assignment and control flow (loops, conditionals). Languages like Java, C, and Python primarily follow this paradigm.
Object-Oriented Programming (OOP)
Procedural Programming: This is a subset of imperative programming that emphasizes breaking down tasks into procedures (functions) that operate on data. Languages like C and Pascal are strongly procedural.
Feature
Functional Programming
Imperative Programming (including OOP)
State Management
Favors immutability and pure functions
Relies heavily on mutable state
Side Effects
Minimizes or avoids side effects
Side effects are common and often essential
Focus
What to compute
How to compute
Building Blocks
Functions, higher-order functions
Objects, procedures, statements
Lambda Function
A concise way to represent anonymous functions.
(arguments) -> { body }
A lambda expression has three main parts:
Argument List: Zero or more parameters enclosed in parentheses.
Arrow Token: The -> separates the argument list from the body.
Body: The code to be executed when the lambda function is called.
Anonymous Class: An anonymous class is a class that is defined without a name and is usually used to implement an interface or extend another class on the fly. It provides a way to define a class inline, making your code more concise.
interfaceGreeting {
voidsayHello();
}
// Anonymous ClassGreetinggreeting = newGreeting() {
@OverridepublicvoidsayHello() {
System.out.println("Hello from an anonymous class!");
}
};
// Lambda Expressions: While anonymous classes are useful, they can become verbose, especially for simple implementations. Lambda expressions offer a more concise way to achieve the same functionality.Greetinggreeting = () -> System.out.println("Hello from a lambda expression!");
// Limitations of Lambda Expressions// - Single Abstract Method: Lambda expressions can only implement functional interfaces with a single abstract method (SAM).// - Scope Restrictions: Lambda expressions cannot access non-final local variables from the enclosing scope.
A functional interface is an interface that has exactly one abstract method (SAM - Single Abstract Method). This method is the core behavior that a lambda expression will implement. Although functional interfaces can have default and static methods, the presence of only one abstract method makes them compatible with lambda expressions.
Java provides several built-in functional interfaces in the java.util.function package
Interface Name
Description
Abstract Method
Example
Predicate<T>
Represents a predicate (boolean-valued function) of one argument.
boolean test(T t)
Predicate<Integer> isEven = n -> n % 2 == 0;
Function<T, R>
Represents a function that accepts one argument and produces a result.
Creating Custom Functional Interfaces: TriFunction takes three arguments and returns a result. The @FunctionalInterface annotation is not mandatory, but it's a good practice to include it as it ensures your interface adheres to the SAM contract.
@FunctionalInterfacepublicinterfaceTriFunction<T, U, V, R> {
Rapply(Tt, Uu, Vv);
}
Functional interfaces are designed to work seamlessly with lambda expressions.
Function<Integer, Integer> square = x -> x * x;
intresult = square.apply(5); // Output: 25
Method References
Method references provide an even more concise way to represent lambda expressions in certain cases. Here are a couple of examples:
// Static method referenceList<String> words = Arrays.asList("apple", "banana", "orange");
words.sort(String::compareToIgnoreCase); // Sorts the list (case-insensitive)// Instance method referenceFunction<String, Integer> strLength = String::length;
intlength = strLength.apply("Hello"); // Output: 5
Streams API
Introduced in Java 8, the Streams API provides a way to process collections of data in a declarative and functional manner. A stream is a sequence of elements that supports various aggregate operations. It allows you to express what you want to do with the data rather than how to do it, leading to more concise and expressive code.
Pipelines: Streams operate on data through a pipeline of operations, where the output of one operation becomes the input of the next.
// Creating StreamsList<String> words = Arrays.asList("apple", "banana", "orange");
Stream<String> wordStream = words.stream();
int[] numbers = {1, 2, 3, 4, 5};
IntStreamnumberStream = Arrays.stream(numbers);
Stream<String> lines = Files.lines(Paths.get("file.txt"));
// Infinite Streams: (Generates an infinite sequence of even numbers)Stream<Integer> infiniteStream = Stream.iterate(0, x -> x + 2);
Stream Operations
Intermediate Operations: These operations return a new stream and are lazily evaluated. Examples include filter, map, distinct, sorted, limit, and skip.
Terminal Operations: These operations produce a final result or side effect and consume the stream. Examples include forEach, toArray, reduce, collect, count, min, max, and anyMatch.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
intsum = numbers.stream()
.filter(x -> x % 2 == 0) // Filter even numbers
.map(x -> x * x) // Square each number
.reduce(0, Integer::sum); // Sum the resultsSystem.out.println(sum); // Output: 20
Parallel Streams: You can process streams in parallel using the parallelStream() method or by calling parallel() on an existing stream. This can potentially improve performance for operations that can be easily divided and executed concurrently.
intsum = numbers.parallelStream()
.filter(x -> x % 2 == 0)
.map(x -> x * x)
.reduce(0, Integer::sum);
map: Transforming elements in a stream (e.g., converting strings to integers).
filter: Selecting elements based on a condition.
sorted: Ordering elements.
distinct: Removing duplicates.
limit, skip: Restricting the number of elements in the stream.
flatMap: Flattening nested collections (e.g., turning a stream of lists into a stream of individual elements).
peek: Performing an action on each element without modifying it (useful for debugging).
How to Use apply() function
The apply() method takes an input and returns a result. It is used to apply a function to an argument and compute a result.
The apply() function is a method of functional interfaces, such as the function interface, that takes an argument of a specified type and returns a result of a specified type. It is the single abstract method of these interfaces, which is required for them to be used as functional interfaces.
Different functional interfaces have variations of apply() (or similar methods like test()) depending on their purpose.
Chaining functions using andThen() and compose() allows you to create complex behaviors from simpler ones.
Function composition is a technique in functional programming where you combine two or more functions to create a new function. The output of one function becomes the input to the next. It's like building a pipeline of functions, where each function processes the data and passes it on to the next stage.
// Define some functionsFunction<Integer, Integer> add5 = x -> x + 5;
Function<Integer, Integer> multiplyBy3 = x -> x * 3;
// Using `andThen` (add5 then multiplyBy3)Function<Integer, Integer> add5ThenMultiplyBy3 = add5.andThen(multiplyBy3);
intresult1 = add5ThenMultiplyBy3.apply(10); // Output: 45 (10 + 5) * 3// Using `compose` (multiplyBy3 then add5)Function<Integer, Integer> multiplyBy3ThenAdd5 = add5.compose(multiplyBy3);
intresult2 = multiplyBy3ThenAdd5.apply(10); // Output: 35 (10 * 3) + 5
----- ChainingMultipleFunctionsFunction<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> add5ThenMultiplyBy3ThenSquare =
add5.andThen(multiplyBy3).andThen(square);
intresult3 = add5ThenMultiplyBy3ThenSquare.apply(2); // Output: 121 ((2 + 5) * 3)^2
----- MethodReferencesandFunctionCompositionFunction<String, String> toUpperCase = String::toUpperCase;
Function<String, Integer> strLength = String::length;
Function<String, Integer> upperCaseThenLength = toUpperCase.andThen(strLength);
intlength = upperCaseThenLength.apply("hello"); // Output: 5
Usages:
Data Transformation: Clean, format, and transform data in a pipeline.
Validation: Create validation rules by composing predicates.
Business Logic: Break down complex business rules into smaller, composable functions.
Function Currying
Function currying is a technique in functional programming that involves transforming a function that takes multiple arguments into a sequence of functions, each taking a single argument. In simpler terms, instead of a function f(x, y), you create a chain of functions: f(x)(y).
Java doesn't have direct built-in support for currying like some functional languages do. However, you can achieve currying using nested lambda expressions or by creating custom functional interfaces.
Currying with Nested Lambda Expressions
// Non-curried functionBiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
// Curried versionFunction<Integer, Function<Integer, Integer>> addCurried = x -> y -> x + y;
// UsageFunction<Integer, Integer> add5 = addCurried.apply(5); // Fix the first argument to 5intresult = add5.apply(3); // Output: 8
Higher-Order Functions in Action
Higher-order functions are functions that operate on other functions. Let's see how we can leverage this in Java:
// Higher-Order Function: Takes a function as an argumentFunction<Integer, Integer> applyTwice(Function<Integer, Integer> func) {
returnx -> func.apply(func.apply(x));
}
// UsageFunction<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> squareTwice = applyTwice(square);
System.out.println(squareTwice.apply(3)); // Output: 81 (3 * 3 * 3 * 3)
Here, applyTwice is a higher-order function that takes a function as an argument and returns a new function that applies the original function twice.