Skip to content

6.12 Final Tutorial

  • Why we can use lambda interface?

Because we need to implement functional interface and it only has one abstract method needed to be implement. Thus it's easy to know which method are you implementing - Can we use lambda expression to do recursion?

No you don't have reference to lambda - A const at the end of a member function (a function of a class), will only prohibit changes to the state of the object (making *this constant).

It will not affect the returned value, so function T& at(int i) const​ in List (and LinkedList), will not make the returned reference to be const. - The keywords inline​ and constexpr​ in C++ are suggestions to the compiler, both have several conditions for them to be considered, such as: if a function is recursive, a pointer of a function is used, the compiler noticed that inlining would be worse than just calling the function, the configuration of the compiler (flags and environment variables) are not enabling the required optimizations.

Generic, operator overloading of C++

#include <iostream>

class WrapperOfInt {
    private:
        int value;

    public:
        WrapperOfInt(): value(0) {}
        WrapperOfInt(int v): value(v) {}
        WrapperOfInt(const WrapperOfInt& other) {
            this->value = other.value;
        }
        ~WrapperOfInt() {}
        int getValue() const {
            return value;
        }

        friend WrapperOfInt operator+ (const WrapperOfInt& a, const WrapperOfInt& b) {
            return WrapperOfInt(a.value + b.value);
        }

        friend std::ostream& operator<< (std::ostream& os, const WrapperOfInt& w);

};

class Foo {
    private:
        std::string msg;
    public:
        Foo(): msg("no msg") {}
        Foo(std::string m): msg(m) {}
        Foo(const Foo& other) {
            this->msg = other.msg;
        }
        ~Foo() {}
        std::string getMessage() const {
            return msg;
        }
};

std::ostream& operator<< (std::ostream& os, const WrapperOfInt& w) {
    os << "WrapperOfInt(" << w.value << ")";
    return os;
}

template<typename T>
concept Addition = requires(T a, T b) {
    {a+ b} -> std :: same_as<T>;
};

template<Addition T>
T add(int size, T neutral, T values[]) {
    T result = neutral;
    for (int i = 0; i < size; i++) {
        result = result + values[i];
    }
    return result;
}

int main() {
    Foo foo1 = Foo("It's a foo!");
    Foo foo2 = Foo("Mamma Mia!");
    Foo foo3 = Foo("Course is almost over");
    Foo foos[] = {foo1, foo2, foo3};
    Foo result = add(3, Foo(""), foos);
    std::cout << "The sum of " << foo1 << ", " << foo2 << ", and " << foo3;
    std::cout << "\nis: " << add(3, Foo(""), foos) << std::endl;

    WrapperOfInt w1 = WrapperOfInt(39);
    WrapperOfInt w2 = WrapperOfInt(2);
    WrapperOfInt w3 = WrapperOfInt(1);
    WrapperOfInt values[] = {w1, w2, w3};
    std::cout << "The sum of " << w1 << ", " << w2 << ", and " << w3;
    std::cout << "\nis: " << add(3, WrapperOfInt(0), values) << std::endl;
    return 0;
}

Java lambda, anonymous class, stream example

Function.java

/**
 * Describes a Function that goes from a type {@code A} to a type {@code B}. This is
 * a functional interface, and it only has one abstract method.
 * @param A the type of the elements that this function takes
 * @param B the type of the elements that this function returns
 * @version 0.1
 */
@FunctionalInterface
public interface Function<A, B> {
    /**
     * Applies this function to its arguments.
     * @param source the argument for this function
     * @return the result of applying this function
     */
    public B apply(A source);
} 

FunctionExamples.java

import java.util.List;
import java.util.Arrays;

/**
 * Demonstrates different ways to implement the Function interface for various tasks.
 * Each task is implemented using: Classes, Anonymous Classes, Lambda expressions, and Method references.
 */
public class FunctionExamples {

    // ========== TASK 1: Absolute Value Function ==========

    // 1.1 Using Class
    static class AbsoluteValueFunction implements Function<Integer, Integer> {
        @Override
        public Integer apply(Integer source) {
            return Math.abs(source);
        }
    }

    // 1.2 Anonymous Class (defined in main method)
    // 1.3 Lambda Expression (defined in main method)
    // 1.4 Method Reference (defined in main method)

    // ========== TASK 2: Palindrome Check Function ==========

    // 2.1 Using Class
    static class PalindromeFunction implements Function<String, Boolean> {
        @Override
        public Boolean apply(String source) {
            return isPalindrome(source);
        }

        private boolean isPalindrome(String str) {
            if (str == null) return false;
            str = str.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
            int left = 0, right = str.length() - 1;
            while (left < right) {
                if (str.charAt(left) != str.charAt(right)) {
                    return false;
                }
                left++;
                right--;
            }
            return true;
        }
    }

    // ========== TASK 3: Addition of Two Integers ==========

    // Helper class to hold two integers
    static class IntegerPair {
        public final Integer first;
        public final Integer second;

        public IntegerPair(Integer first, Integer second) {
            this.first = first;
            this.second = second;
        }
    }

    // 3.1 Using Class
    static class AdditionFunction implements Function<IntegerPair, Integer> {
        @Override
        public Integer apply(IntegerPair source) {
            return source.first + source.second;
        }
    }

    // ========== TASK 4: Maximum of Three Integers ==========

    // Helper class to hold three integers
    static class IntegerTriple {
        public final Integer first;
        public final Integer second;
        public final Integer third;

        public IntegerTriple(Integer first, Integer second, Integer third) {
            this.first = first;
            this.second = second;
            this.third = third;
        }
    }

    // 4.1 Using Class
    static class MaximumFunction implements Function<IntegerTriple, Integer> {
        @Override
        public Integer apply(IntegerTriple source) {
            return Math.max(Math.max(source.first, source.second), source.third);
        }
    }

    // ========== TASK 5: List Element Access ==========

    // Helper class to hold list and index
    static class ListIndexPair {
        public final List<Integer> list;
        public final Integer index;

        public ListIndexPair(List<Integer> list, Integer index) {
            this.list = list;
            this.index = index;
        }
    }

    // 5.1 Using Class
    static class ListAccessFunction implements Function<ListIndexPair, Integer> {
        @Override
        public Integer apply(ListIndexPair source) {
            if (source.index >= 0 && source.index < source.list.size()) {
                return source.list.get(source.index);
            }
            throw new IndexOutOfBoundsException("Index out of bounds: " + source.index);
        }
    }

    // ========== AUXILIARY METHODS ==========

    public static boolean isPalindrome(String str) {
        if (str == null) return false;
        str = str.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
        int left = 0, right = str.length() - 1;
        while (left < right) {
            if (str.charAt(left) != str.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    public static Integer addTwo(IntegerPair pair) {
        return pair.first + pair.second;
    }

    public static Integer maxOfThree(IntegerTriple triple) {
        return Math.max(Math.max(triple.first, triple.second), triple.third);
    }

    public static Integer getListElement(ListIndexPair pair) {
        if (pair.index >= 0 && pair.index < pair.list.size()) {
            return pair.list.get(pair.index);
        }
        throw new IndexOutOfBoundsException("Index out of bounds: " + pair.index);
    }

    // ========== MAIN METHOD WITH DEMONSTRATIONS ==========

    public static void main(String[] args) {
        System.out.println("=== Function Interface Implementation Examples ===\n");

        // ========== TASK 1: Absolute Value ==========
        System.out.println("TASK 1: Absolute Value Function");

        // 1.1 Class
        Function<Integer, Integer> abs1 = new AbsoluteValueFunction();

        // 1.2 Anonymous Class
        Function<Integer, Integer> abs2 = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer source) {
                return Math.abs(source);
            }
        };

        // 1.3 Lambda Expression
        Function<Integer, Integer> abs3 = source -> Math.abs(source);

        // 1.4 Method Reference
        Function<Integer, Integer> abs4 = Math::abs;

        // Test all implementations
        Integer testValue1 = -15;
        System.out.println("Input: " + testValue1);
        System.out.println("Class: " + abs1.apply(testValue1));
        System.out.println("Anonymous: " + abs2.apply(testValue1));
        System.out.println("Lambda: " + abs3.apply(testValue1));
        System.out.println("Method Reference: " + abs4.apply(testValue1));
        System.out.println();

        // ========== TASK 2: Palindrome Check ==========
        System.out.println("TASK 2: Palindrome Check Function");

        // 2.1 Class
        Function<String, Boolean> palindrome1 = new PalindromeFunction();

        // 2.2 Anonymous Class
        Function<String, Boolean> palindrome2 = new Function<String, Boolean>() {
            @Override
            public Boolean apply(String source) {
                return isPalindrome(source);
            }
        };

        // 2.3 Lambda Expression
        Function<String, Boolean> palindrome3 = source -> isPalindrome(source);

        // 2.4 Method Reference
        Function<String, Boolean> palindrome4 = FunctionExamples::isPalindrome;

        // Test all implementations
        String testValue2 = "A man a plan a canal Panama";
        System.out.println("Input: \"" + testValue2 + "\"");
        System.out.println("Class: " + palindrome1.apply(testValue2));
        System.out.println("Anonymous: " + palindrome2.apply(testValue2));
        System.out.println("Lambda: " + palindrome3.apply(testValue2));
        System.out.println("Method Reference: " + palindrome4.apply(testValue2));
        System.out.println();

        // ========== TASK 3: Addition of Two Integers ==========
        System.out.println("TASK 3: Addition Function");

        // 3.1 Class
        Function<IntegerPair, Integer> add1 = new AdditionFunction();

        // 3.2 Anonymous Class
        Function<IntegerPair, Integer> add2 = new Function<IntegerPair, Integer>() {
            @Override
            public Integer apply(IntegerPair source) {
                return source.first + source.second;
            }
        };

        // 3.3 Lambda Expression
        Function<IntegerPair, Integer> add3 = pair -> pair.first + pair.second;

        // 3.4 Method Reference
        Function<IntegerPair, Integer> add4 = FunctionExamples::addTwo;

        // Test all implementations
        IntegerPair testValue3 = new IntegerPair(25, 17);
        System.out.println("Input: (" + testValue3.first + ", " + testValue3.second + ")");
        System.out.println("Class: " + add1.apply(testValue3));
        System.out.println("Anonymous: " + add2.apply(testValue3));
        System.out.println("Lambda: " + add3.apply(testValue3));
        System.out.println("Method Reference: " + add4.apply(testValue3));
        System.out.println();

        // ========== TASK 4: Maximum of Three Integers ==========
        System.out.println("TASK 4: Maximum Function");

        // 4.1 Class
        Function<IntegerTriple, Integer> max1 = new MaximumFunction();

        // 4.2 Anonymous Class
        Function<IntegerTriple, Integer> max2 = new Function<IntegerTriple, Integer>() {
            @Override
            public Integer apply(IntegerTriple source) {
                return Math.max(Math.max(source.first, source.second), source.third);
            }
        };

        // 4.3 Lambda Expression
        Function<IntegerTriple, Integer> max3 = triple -> 
            Math.max(Math.max(triple.first, triple.second), triple.third);

        // 4.4 Method Reference
        Function<IntegerTriple, Integer> max4 = FunctionExamples::maxOfThree;

        // Test all implementations
        IntegerTriple testValue4 = new IntegerTriple(12, 45, 23);
        System.out.println("Input: (" + testValue4.first + ", " + testValue4.second + ", " + testValue4.third + ")");
        System.out.println("Class: " + max1.apply(testValue4));
        System.out.println("Anonymous: " + max2.apply(testValue4));
        System.out.println("Lambda: " + max3.apply(testValue4));
        System.out.println("Method Reference: " + max4.apply(testValue4));
        System.out.println();

        // ========== TASK 5: List Element Access ==========
        System.out.println("TASK 5: List Element Access Function");

        // 5.1 Class
        Function<ListIndexPair, Integer> listAccess1 = new ListAccessFunction();

        // 5.2 Anonymous Class
        Function<ListIndexPair, Integer> listAccess2 = new Function<ListIndexPair, Integer>() {
            @Override
            public Integer apply(ListIndexPair source) {
                if (source.index >= 0 && source.index < source.list.size()) {
                    return source.list.get(source.index);
                }
                throw new IndexOutOfBoundsException("Index out of bounds: " + source.index);
            }
        };

        // 5.3 Lambda Expression
        Function<ListIndexPair, Integer> listAccess3 = pair -> {
            if (pair.index >= 0 && pair.index < pair.list.size()) {
                return pair.list.get(pair.index);
            }
            throw new IndexOutOfBoundsException("Index out of bounds: " + pair.index);
        };

        // 5.4 Method Reference
        Function<ListIndexPair, Integer> listAccess4 = FunctionExamples::getListElement;

        // Test all implementations
        List<Integer> testList = Arrays.asList(10, 20, 30, 40, 50);
        ListIndexPair testValue5 = new ListIndexPair(testList, 2);
        System.out.println("Input: List" + testList + ", Index: " + testValue5.index);
        System.out.println("Class: " + listAccess1.apply(testValue5));
        System.out.println("Anonymous: " + listAccess2.apply(testValue5));
        System.out.println("Lambda: " + listAccess3.apply(testValue5));
        System.out.println("Method Reference: " + listAccess4.apply(testValue5));
        System.out.println();

        System.out.println("=== All Function implementations completed successfully! ===");
    }
} 

FactorialDemo.java

/**
 * Demonstrates Task 2: Creating a variable and value for a Function that calculates factorial of an Integer
 * Shows different ways to implement factorial calculation using the Function interface
 */
public class FactorialDemo {

    public static void main(String[] args) {
        System.out.println("=== Task 2: Factorial Function Implementations ===\n");

        // Variable and value declarations for factorial Function

        // 1. Using Lambda Expression (most concise)
        Function<Integer, Integer> factorialLambda = n -> {
            if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers");
            if (n == 0 || n == 1) return 1;
            Integer result = 1;
            for (int i = 2; i <= n; i++) {
                result *= i;
            }
            return result;
        };

        // 2. Using Method Reference to helper method
        Function<Integer, Integer> factorialMethodRef = FactorialDemo::calculateFactorial;

        // 3. Using Anonymous Class
        Function<Integer, Integer> factorialAnonymous = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer source) {
                if (source < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers");
                if (source == 0 || source == 1) return 1;
                Integer result = 1;
                for (int i = 2; i <= source; i++) {
                    result *= i;
                }
                return result;
            }
        };

        // Testing the factorial functions
        Integer[] testValues = {0, 1, 5, 7};

        for (Integer testValue : testValues) {
            System.out.println("Factorial of " + testValue + ":");
            System.out.println("  Lambda Expression: " + factorialLambda.apply(testValue));
            System.out.println("  Method Reference: " + factorialMethodRef.apply(testValue));
            System.out.println("  Anonymous Class: " + factorialAnonymous.apply(testValue));
            System.out.println();
        }
    }

    // Helper method for method reference
    public static Integer calculateFactorial(Integer n) {
        if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers");
        if (n == 0 || n == 1) return 1;
        Integer result = 1;
        for (int i = 2; i <= n; i++) {
            result *= i;
        }
        return result;
    }
} 

Task 3: Can a Lambda expression be used? Justify your answer.

Answer: YES, a Lambda expression can be used.

Justification:

  1. Functional Interface Requirement: The Function<A, B>​ interface is annotated with @FunctionalInterface​, which means:

    • It has exactly one abstract method (apply​)
    • Lambda expressions can be used wherever functional interfaces are expected
  2. Single Abstract Method (SAM) : The Function interface has only one abstract method:

    public B apply(A source);
    

    This satisfies the requirement for lambda expression usage.

  3. Type Compatibility:

    • Lambda expressions can be automatically converted to functional interface types
    • The compiler can infer the parameter types from the Function's generic types
    • Function<Integer, Integer>​ means a function that takes an Integer and returns an Integer
  4. Syntax Advantages:

    • Concise: n -> factorial_logic​ instead of full anonymous class syntax
    • Readable: Focuses on the logic rather than boilerplate code
    • Type inference: Compiler automatically determines parameter types
  5. Example Proof:

    // This works perfectly:
    Function<Integer, Integer> factorial = n -> {
        // factorial calculation logic
    };
    

Conclusion: Lambda expressions are not only possible but are the preferred approach for implementing functional interfaces like Function due to their conciseness and readability.


Streams vs Collections: Key Mechanical Difference

Answer: Lazy Evaluation vs Eager Evaluation

Key Mechanical Difference:

Collections (Eager Evaluation):

  • Immediate Processing: Operations are executed immediately when called
  • Data Storage: Collections store all elements in memory
  • Multiple Iterations: Can be iterated multiple times
  • Concrete Data Structure: Physically holds data
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = numbers.stream()
    .map(x -> x * 2)
    .collect(Collectors.toList()); // Executes immediately, stores result

Streams (Lazy Evaluation):

  • Deferred Processing: Operations are not executed until a terminal operation is called
  • No Data Storage: Streams don't store elements; they process data on-demand
  • Single Use: Can only be consumed once
  • Pipeline of Operations: Describes what should be done, not when
Stream<Integer> numberStream = Arrays.asList(1, 2, 3, 4, 5).stream();
Stream<Integer> doubledStream = numberStream.map(x -> x * 2); // No execution yet!
// Only when we call a terminal operation like .collect() does processing happen

Additional Mechanical Differences:

  1. Memory Usage:

    • Collections: Store all data in memory
    • Streams: Process elements one-by-one, minimal memory footprint
  2. Iteration Model:

    • Collections: External iteration (explicit loops)
    • Streams: Internal iteration (framework controls the loop)
  3. Mutability:

    • Collections: Can be modified (add/remove elements)
    • Streams: Immutable pipeline of operations
  4. Performance:

    • Collections: Good for small datasets, random access
    • Streams: Better for large datasets, parallel processing, complex transformations

Example Demonstrating the Difference:

// Collection approach (eager)
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = new ArrayList<>();
for (Integer num : list) {
    if (num % 2 == 0) {
        result.add(num * 2);
    }
}

// Stream approach (lazy)
List<Integer> result = Arrays.asList(1, 2, 3, 4, 5)
    .stream()
    .filter(num -> num % 2 == 0)  // Not executed yet
    .map(num -> num * 2)          // Not executed yet
    .collect(Collectors.toList()); // NOW everything executes

The fundamental difference is that streams represent a recipe for computation while collections represent actual data storage.