Skip to content

Instantly share code, notes, and snippets.

@1UC1F3R616
Last active June 22, 2024 21:34
Show Gist options
  • Save 1UC1F3R616/5603d6f6b34bc11e3a4a009825b8dc65 to your computer and use it in GitHub Desktop.
Save 1UC1F3R616/5603d6f6b34bc11e3a4a009825b8dc65 to your computer and use it in GitHub Desktop.
A Guide to Java: Crystal Clear Level M (K>L>M) | Not for Learning from Beginning | Maintaining some concepts only

Classpath

  • 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.
    1. 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.
class OuterClass {
    static int outerStaticVar = 10;

    static class StaticNestedClass {
        void display() {
            System.out.println("Outer static var: " + outerStaticVar); // Accessing outer static member
        }
    }

    public static void main(String[] args) {
        OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
        nested.display(); 
    }
}
// Object Creation
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass(); // Create directly
nested.display();
  1. 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.
class OuterClass {
    int outerVar = 5;

    class InnerClass {
        void display() {
            System.out.println("Outer var: " + outerVar); // Accessing outer instance member
        }
    }

    public void outerMethod() {
        InnerClass inner = new InnerClass();
        inner.display();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.outerMethod();
    }
}
// Object Creation
OuterClass outer = new OuterClass(); // First create an instance of the outer class
OuterClass.InnerClass inner = outer.new InnerClass(); // Then create inner class instance
inner.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.
class MathUtils {
    public static final double PI = 3.14159; // Static constant field

    public static int countInstances = 0; // Static field to count instances

    public static double square(double x) {
        countInstances++; // Increment instance counter
        return x * x;
    }
}

// Usage:
double result = MathUtils.square(5.0);
System.out.println("PI: " + MathUtils.PI);
System.out.println("Instances created: " + MathUtils.countInstances);

Access Modifiers (public, private, protected, default)

  • 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.
abstract class Animal {
    // Abstract method
    public abstract void makeSound(); 

    // Concrete method
    public void sleep() {
        System.out.println("Zzz...");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

Interface

  • 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.
interface Shape {
    double calculateArea();
    double calculatePerimeter();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}

// ... similar implementations for Rectangle, Triangle, etc.
  • Not OK
abstract class Animal {
    public abstract void makeSound();
}

class Dog extends Animal {  // Error: Must implement all abstract methods
    // No implementation of makeSound()
}

Explanation: Concrete classes extending abstract classes must provide implementations for all abstract methods inherited from the abstract class.
  • Not OK
abstract class Animal {
    // ...
}

class Dog extends Animal {
    // ...
}

class Cat extends Animal, Dog {  // Error: Multiple inheritance not allowed
    // ...
}
Explanation: Java does not support multiple inheritance of classes, only multiple inheritance of interfaces.
  • Not OK
interface Shape {
    double calculateArea();
}

class Circle implements Shape {  // Error: Must implement all interface methods
    // No implementation of calculateArea()
}
  • Not OK (Ambiguity (Not a Direct Error))
interface A { 
    default void show() { System.out.println("A's show method"); }
}

interface B extends A {}

interface C extends A {}

class D implements B, 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.
abstract class Animal { 
    // ... some methods 
}

interface Pet {
    void play();
}

class Dog extends Animal implements Pet {
    // ... 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.
class ParentClass {
    // ... fields and methods ...
}

class ChildClass extends ParentClass {
    // ... 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.
class Vehicle { // Parent Class
    private String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    public void start() {
        System.out.println(brand + " vehicle started.");
    }
}

class Car extends Vehicle { // Child Class
    private int numDoors;

    public Car(String brand, int numDoors) {
        super(brand); // Call the parent's constructor
        this.numDoors = numDoors;
    }
    
    public void honk() {
        System.out.println("Honk!");
    }
}
  • 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.
  • Upcasting and Downcasting
class Vehicle {
    public void start() {
        System.out.println("Vehicle started.");
    }
}

class Car extends Vehicle {
    public void honk() {
        System.out.println("Car honked.");
    }
}

class Motorcycle extends Vehicle {
    public void revEngine() {
        System.out.println("Motorcycle engine revved.");
    }
}

Car myCar = new Car();  
Vehicle myVehicle = myCar; // Upcasting

myVehicle.start(); // Output: Vehicle started.
Vehicle anotherVehicle = new Motorcycle();
Motorcycle myMotorcycle = (Motorcycle) anotherVehicle; // Downcasting

myMotorcycle.revEngine(); // Output: Motorcycle engine revved.
  • The instanceof Operator: Used to check if an object is an instance of a specific class or interface.

Polymorphism

  • Method Overloading (Compile Time) and Method Overriding (Run Time).
class Animal {
    public void makeSound() {
        System.out.println("Generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

// ...

Animal[] animals = {new Dog(), new Cat()};
for (Animal animal : animals) {
    animal.makeSound(); // Polymorphic behavior
}

Encapsulation

  • Encapsulation is the bundling of data (attributes or fields) and the operations that act on that data (methods) into a single unit, known as a class.

Extend vs Implement

Feature extends (Inheritance) implements (Interface Implementation)
Relationship "Is-a" relationship (subclass is a superclass) "Can-do" relationship (class provides the behavior of an interface)
Purpose Inheriting properties and behavior from a parent class Implementing the contract defined by an interface
What it applies to Classes (extending other classes) and interfaces (extending other interfaces) Classes (implementing interfaces)
Number allowed Only one class can be extended Multiple interfaces can be implemented
Method requirement Subclass may or may not override parent's methods Class must implement all methods declared in the interface
Example class Dog extends Animal class Dog implements Mammal, Pet

Final

  • A final class cannot be inherited. This is used to prevent subclassing when you want to guarantee a class's behavior won't be altered or extended.
final class MathConstants {
    public static final double PI = 3.14159;

    // ... other mathematical constants
}
  • A final method within a class cannot be overridden by any subclasses. This helps maintain the integrity of a method's implementation.
class PaymentProcessor {
    final void processPayment(double amount) {
        // Logic to process the payment
        System.out.println("Processing payment of $" + amount);
    }
}

class CreditCardProcessor extends PaymentProcessor {
    // 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.
public static final datatype CONSTANT_NAME = value;
// Accessing static constants directly using the class name
double circumference = 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);
int sum = 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.
Runnable r = () -> System.out.println("Hello, Lambda!");
r.run(); // Output: Hello, Lambda!


Function<Integer, Integer> doubleIt = x -> x * 2;
int result = doubleIt.apply(5); // Output: 10


BiConsumer<String, String> concatStrings = (s1, s2) -> System.out.println(s1 + s2);
concatStrings.accept("Hello", " World!"); // Output: Hello World!
  • 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.
interface Greeting {
    void sayHello();
}

// Anonymous Class
Greeting greeting = new Greeting() {
    @Override
    public void sayHello() {
        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.

Greeting greeting = () -> 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.
  • Great Documentation on them all here

Functional Interfaces

  • 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. R apply(T t) Function<String, Integer> strLength = String::length;
Consumer<T> Represents an operation that accepts a single input argument and returns no result. void accept(T t) Consumer<String> print = System.out::println;
Supplier<T> Represents a supplier of results. T get() Supplier<Double> random = Math::random;
BiFunction<T, U, R> Represents a function that accepts two arguments and produces a result. R apply(T t, U u) BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
BiConsumer<T, U> Represents an operation that accepts two input arguments and returns no result. void accept(T t, U u) BiConsumer<String, String> printBoth = (s1, s2) -> System.out.println(s1 + s2);
  • 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.
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}
  • Functional interfaces are designed to work seamlessly with lambda expressions.
Function<Integer, Integer> square = x -> x * x;
int result = 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 reference
List<String> words = Arrays.asList("apple", "banana", "orange");
words.sort(String::compareToIgnoreCase); // Sorts the list (case-insensitive)

// Instance method reference
Function<String, Integer> strLength = String::length;
int length = 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 Streams
List<String> words = Arrays.asList("apple", "banana", "orange");
Stream<String> wordStream = words.stream();

int[] numbers = {1, 2, 3, 4, 5};
IntStream numberStream = 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);
int sum = numbers.stream()
                .filter(x -> x % 2 == 0) // Filter even numbers
                .map(x -> x * x)        // Square each number
                .reduce(0, Integer::sum); // Sum the results
System.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.
int sum = numbers.parallelStream()
                .filter(x -> x % 2 == 0)
                .map(x -> x * x)
                .reduce(0, Integer::sum); 
List<Integer> scores = List.of(85, 92, 78, 64, 98, 81, 73);

forEach:

Purpose: Performs an action for each element in the stream.

scores.stream().forEach(System.out::println); 

85
92
78
64
98
81
73

------

toArray:

Purpose: Collects the stream elements into an array.

Integer[] scoreArray = scores.stream().toArray(Integer[]::new); 
System.out.println(Arrays.toString(scoreArray));

[85, 92, 78, 64, 98, 81, 73]

------

reduce:

Purpose: Combines the stream elements into a single value using a binary operator (like addition, multiplication, etc.).

int totalScore = scores.stream().reduce(0, Integer::sum); 
System.out.println(totalScore); 

571

------

collect:

Purpose: Accumulates the stream elements into a collection or other data structure (e.g., a list, set, or map).

Set<Integer> uniqueScores = scores.stream().collect(Collectors.toSet());
System.out.println(uniqueScores);

[98, 64, 73, 78, 81, 85, 92]

------

count:

Purpose: Returns the number of elements in the stream.

long numScores = scores.stream().count();
System.out.println(numScores);

7

------

min, max:

Purpose: Finds the minimum and maximum values in the stream (using Optional to handle cases where the stream might be empty).

OptionalInt minScore = scores.stream().mapToInt(Integer::intValue).min();
OptionalInt maxScore = scores.stream().mapToInt(Integer::intValue).max();
System.out.println(minScore.getAsInt());
System.out.println(maxScore.getAsInt());

64
98

------

nyMatch, allMatch, noneMatch:

Purpose: Check whether any, all, or none of the elements in the stream match a given predicate.

boolean hasPassingScore = scores.stream().anyMatch(score -> score >= 70);
boolean allPassingScores = scores.stream().allMatch(score -> score >= 70);
boolean noFailingScores = scores.stream().noneMatch(score -> score < 70);

System.out.println(hasPassingScore); 
System.out.println(allPassingScores); 
System.out.println(noFailingScores); 


true
false
false

More Intermediate Stream Operations:

  • 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<T, R>

Purpose: Transforms an input of type T into an output of type R.

Function<Integer, Integer> square = x -> x * x; 
int result = square.apply(5); // Output: 25

------

BiFunction<T, U, R>

Purpose: Accepts two inputs of types T and U, and produces a result of type R.

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
int sum = add.apply(3, 4); // Output: 7

------

Predicate<T>

Purpose: Tests an input of type T and returns a boolean (true or false). While Predicate uses the test() method instead of apply(), the concept is very similar.

Predicate<Integer> isEven = n -> n % 2 == 0;
boolean result = isEven.test(6); // Output: true

-----
Using apply() with Method References: You can also use method references with apply() for even more concise code:

Function<String, Integer> strLength = String::length;
int length = strLength.apply("Hello"); // Output: 5

----

Chaining Functions with andThen() and compose(): Functional interfaces like Function also have methods like andThen() and compose() that allow you to chain multiple functions together. This enables you to create more complex behaviors by composing simpler functions.

Function<Integer, Integer> timesTwo = x -> x * 2;
Function<Integer, Integer> plusThree = x -> x + 3;

Function<Integer, Integer> combined = timesTwo.andThen(plusThree); // First timesTwo, then plusThree
int result = combined.apply(5); // Output: 13 
public class FileReaderPipeline {

    static Function<String, String> trim = String::trim;
    static Function<String, String> toUpperCase = String::toUpperCase;
    static Function<String, String> replaceSpecialCharacters = 
    	str -> str.replaceAll("[^\\p{Alpha}]+", "");

    static Function<String, String> pipeline = 
                                trim
                                  .andThen(toUpperCase)
                                  .andThen(replaceSpecialCharacters);

 public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new 	FileReader("example.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String result = pipeline.apply(line);
                System.out.println(result);
            }
        }
    }
}

Function Composition with andThen and compose

  • 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 functions
Function<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);
int result1 = add5ThenMultiplyBy3.apply(10); // Output: 45  (10 + 5) * 3

// Using `compose` (multiplyBy3 then add5)
Function<Integer, Integer> multiplyBy3ThenAdd5 = add5.compose(multiplyBy3);
int result2 = multiplyBy3ThenAdd5.apply(10); // Output: 35  (10 * 3) + 5

----- Chaining Multiple Functions

Function<Integer, Integer> square = x -> x * x;

Function<Integer, Integer> add5ThenMultiplyBy3ThenSquare = 
    add5.andThen(multiplyBy3).andThen(square);

int result3 = add5ThenMultiplyBy3ThenSquare.apply(2); // Output: 121  ((2 + 5) * 3)^2

----- Method References and Function Composition

Function<String, String> toUpperCase = String::toUpperCase;
Function<String, Integer> strLength = String::length;

Function<String, Integer> upperCaseThenLength = toUpperCase.andThen(strLength);
int length = 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 function
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;

// Curried version
Function<Integer, Function<Integer, Integer>> addCurried = x -> y -> x + y;

// Usage
Function<Integer, Integer> add5 = addCurried.apply(5); // Fix the first argument to 5
int result = 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 argument
Function<Integer, Integer> applyTwice(Function<Integer, Integer> func) {
    return x -> func.apply(func.apply(x)); 
}

// Usage
Function<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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment