MTI TEK
  • Home
  • LLMs
  • Docker
  • Kubernetes
  • Java
  • All
Java | Functional Interfaces
  1. Functional Interfaces
  2. Using a Functional Interface
  3. Package "java.util.function"

  1. Functional Interfaces
    A functional interface is an interface that declares exactly one abstract method. This abstract method is referred to as the functional method of the functional interface.

    Functional interfaces can also inherit abstract methods from parent interfaces. If an interface inherits one abstract method and declares no additional abstract methods, it is still considered a functional interface.

    A functional interface can declare other non-abstract methods, but these must provide a default implementation using the default keyword.

    A functional interface can also define static methods.

    A functional interface can be annotated with the "@FunctionalInterface" annotation to make it explicit that it is intended to be a functional interface. The annotation is optional but recommended as it provides compile-time checking and clear documentation.

    Here are some functional interfaces from the java.util.function package:
    • Predicate<T>: Takes input of type T and returns boolean (boolean test(T t);).
    • Function<T,R>: Takes input of type T and returns result of type R (R apply(T t);).
    • Consumer<T>: Takes input of type T and returns void. It should performs side effects. (void accept(T t);).
    • Supplier<T>: Takes no input and returns result of type T (T get();).
    • UnaryOperator<T>: Takes input of type T and returns result of same type T (T apply(T t);).
    • BinaryOperator<T>: Takes two inputs of type T and returns result of type T (T apply(T t, T u);).

    Example: java.util.function.Predicate
    @FunctionalInterface
    public interface Predicate<T> {
        // This is the only abstract method in the intervace which makes it a functional interface
        boolean test(T t);
    
        // Default and Static methods don't break the functional interface contract
        default Predicate<T> and(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) && other.test(t);
        }
    
        default Predicate<T> negate() {
            return (t) -> !test(t);
        }
    
        default Predicate<T> or(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) || other.test(t);
        }
    
        static <T> Predicate<T> isEqual(Object targetRef) {
            return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object);
        }
    
        static <T> Predicate<T> not(Predicate<? super T> target) {
            Objects.requireNonNull(target);
            return (Predicate<T>) target.negate();
        }
    }
  2. Using a Functional Interface
    You can specify a functional interface type as the parameter type or return type of a method.

    For example, a method can be defined as follows:
    MyFunctionalInterface foo(MyFunctionalInterface myFunctionalInterface) { ... }

    To invoke a method that declares a parameter or return value of a functional interface type, you can use any of the following approaches:

    • A reference to a static method: MyType::myStaticMethod
      This approach is useful when you have existing static utility methods that match the functional interface signature.
      import java.util.function.UnaryOperator;
      
      /*
      // UnaryOperator extends Function<T,T>: it takes a parameter of type T and returns the same type T
      @FunctionalInterface
      public interface UnaryOperator<T> extends Function<T, T> {
      }
      
      @FunctionalInterface
      public interface Function<T, R> {
          R apply(T t);
      }
      */
      
      public class MainClass {
          // Static method that matches UnaryOperator<String>::apply signature: String -> String
          public static String toLowerCase(String value) {
              return value == null ? "" : value.toLowerCase();
          }
      
          public static String convert(String value, UnaryOperator<String> myStringConverter) {
              return myStringConverter.apply(value);
          }
      
          public static void main(String[] args) {
              final String convertedValue = MainClass.convert("tESt", MainClass::toLowerCase);
              System.out.println(convertedValue); // Output: test
          }
      }
    • A reference to an instance method: myInstance::myInstanceMethod
      This approach is useful when you need to use methods from a specific object instance.
      import java.util.function.UnaryOperator;
      
      public class MainClass {
          // Instance method that matches UnaryOperator<String>::apply signature: String -> String
          public String toUpperCase(String value) {
              return value == null ? "" : value.toUpperCase();
          }
      
          public static String convert(String value, UnaryOperator<String> myStringConverter) {
              return myStringConverter.apply(value);
          }
      
          public static void main(String[] args) {
              final MainClass mainClass = new MainClass();
              final String convertedValue = MainClass.convert("tESt", mainClass::toUpperCase);
              System.out.println(convertedValue); // Output: TEST
          }
      }
    • A reference to an instance method of an arbitrary object of a particular type: MyType::myInstanceMethod
      The method reference implies that the first parameter of the functional interface will be the object on which the method is called. Which means it accepts a parameter of type "MyType" as the first parameter:

      • In the example below, the reference "String::toUpperCase" implies that the functional interface is of type "Function<String, String>": it takes a String parameter and calls toUpperCase() on it.
        Equivalent to: s -> s.toUpperCase()

        We can use an assignment statement to illustrate this implication:
        Function<String, String> toUpperCaseFunction = String::toUpperCase;
      • On the other hand, the reference "MainClass::toUpperCase" implies that the functional interface is of type "Function<MainClass, String>": it takes a MainClass parameter and calls toUpperCase() on it.
        Equivalent to: mainClassInstance -> mainClassInstance.toUpperCase()

        We can use an assignment statement to illustrate this implication:
        Function<MainClass, String> toUpperCaseFunction = MainClass::toUpperCase;

      Example:
      import java.util.function.Function;
      import java.util.function.UnaryOperator;
      
      public class MainClass {
          private String value;
      
          public MainClass(String value) {
              this.value = value;
          }
      
          public String toUpperCase() {
              return value == null ? "" : value.toUpperCase();
          }
      
          // Accepts UnaryOperator<String>::apply signature: String -> String
          public static void convert(String value, UnaryOperator<String> myStringConverter) {
              System.out.println(myStringConverter.apply(value));
          }
      
          // Accepts Function<MainClass, String>::apply signature: MainClass -> String
          public static void convert(MainClass object, Function<MainClass, String> myFunction) {
              System.out.println(myFunction.apply(object));
          }
      
          public static void main(String[] args) {
              // First example: String::toUpperCase matches UnaryOperator<String>::apply
              // The String parameter becomes the object on which toUpperCase() is called
              MainClass.convert("hello", String::toUpperCase); // Output: HELLO
      
              // Second example: MainClass::toUpperCase matches Function<MainClass, String>::apply
              // The MainClass parameter becomes the object on which toUpperCase() is called
              MainClass.convert(new MainClass("hello"), MainClass::toUpperCase); // Output: HELLO
          }
      }
    • A reference to a constructor: MyType::new
      Constructor references are useful, for example, when you need to create objects based on functional interface calls.
      import java.util.function.Function;
      import java.util.function.Supplier;
      
      public class MainClass {
          private String value;
      
          public MainClass() {
              this.value = "default";
              System.out.println("Created with default value");
          }
      
          public MainClass(String value) {
              this.value = value;
              System.out.println("Created with value: " + value);
          }
      
          // Uses no-arg constructor: Supplier<MainClass>
          public static MainClass build(Supplier<MainClass> supplier) {
              return supplier.get(); // Equivalent to: () -> new MainClass()
          }
      
          // Uses constructor with parameter: Function<String, MainClass>
          public static MainClass build(String value, Function<String, MainClass> myFunction) {
              return myFunction.apply(value); // Equivalent to: t -> new MainClass(t)
          }
      
          public static void main(String[] args) {
              // Uses no-arg constructor
              MainClass instance1 = MainClass.build(MainClass::new); // Output: Created with default value
              MainClass instance2 = MainClass.build(() -> new MainClass()); // Output: Created with default value
      
              // Uses constructor with parameter
              MainClass instance3 = MainClass.build("test", MainClass::new); // Output: Created with value: test
              MainClass instance4 = MainClass.build("test", value -> new MainClass(value)); // Output: Created with value: test
          }
      }
      The compiler determines which constructor to use based on the functional interface's signature:
      • Supplier<T> with MyClass::new uses the no-argument constructor.
      • Function<T, R> with MyClass::new uses the constructor that takes a parameter of type R.
      • BiFunction<T, U, R> with MyClass::new would use a constructor taking two parameters of types U and R.

    • Using a lambda expression: (parameters -> statements)
      Lambda expressions are ideal for simple transformations or when no existing method matches your needs.
      import java.util.function.BinaryOperator;
      import java.util.function.Predicate;
      import java.util.function.UnaryOperator;
      
      public class MainClass {
          public static String convert(String value, UnaryOperator<String> myStringConverter) {
              return myStringConverter.apply(value);
          }
      
          public static String combine(String a, String b, BinaryOperator<String> combiner) {
              return combiner.apply(a, b);
          }
      
          public static boolean test(String value, Predicate<String> tester) {
              return tester.test(value);
          }
      
          public static void main(String[] args) {
              // Simple expression lambda
              final String convertedValue = MainClass.convert(" test ", t -> t == null ? "" : t.trim());
              System.out.println(convertedValue); // Output: test
      
              // Lambda with multiple parameters
              String combined = MainClass.combine("Hello", "Lambda", (a, b) -> a + " " + b);
              System.out.println(combined); // Output: Hello Lambda
      
              // Block syntax lambda
              final String convertedValue2 = MainClass.convert(" test ", t -> {
                  if (t == null) {
                      return "";
                  }
                  return t.trim();
              });
              System.out.println(convertedValue2); // Output: test
          }
      }
  3. Package "java.util.function"
    The Java library provides a list of functional interfaces located in the "java.util.function" package:

    • Predicate<T>: boolean test(T t)
      This interface defines the functional method "boolean test(T t)" which declares a single parameter ("t" of type "T") and returns a true/false result. Implementations typically apply some logic to the argument "t". Predicates support logical operations like and(), or(), and negate() for method chaining.
      Predicate<Integer> isOdd = t -> t % 2 != 0; // Output: true
      System.out.println(isOdd.test(1));
      
      Predicate<Integer> isEven = isOdd.negate();
      System.out.println(isEven.test(1)); // Output: false
      • IntPredicate: boolean test(int t)
        This interface is a primitive specialization of "Predicate", specifying the parameter type "t" as "int".

      • LongPredicate: boolean test(long t)
        This interface is a primitive specialization of "Predicate", specifying the parameter type "t" as "long".

      • DoublePredicate: boolean test(double t)
        This interface is a primitive specialization of "Predicate", specifying the parameter type "t" as "double".

    • BiPredicate<T, U>: boolean test(T t, U u)
      This interface defines the functional method "boolean test(T t, U u)" which declares two parameters ("t" of type "T", "u" of type "U") and returns a true/false result. Implementations typically apply logic to the arguments "t" and "u". Like Predicate, it supports logical operations for method chaining.
      BiPredicate<Object, Object> equals = (a, b) -> Objects.equals(a, b);
      System.out.println(equals.test(1, 2)); // Output: false
      
      BiPredicate<Object, Object> deepEquals = Objects::deepEquals;
      System.out.println(deepEquals.test(1, 2)); // Output: false
      
      BiPredicate<String, String> eitherNull = (a, b) -> a == null || b == null;
      System.out.println(eitherNull.test(null, "test")); // Output: true
      
      BiPredicate<String, String> bothNotNull = eitherNull.negate();
      System.out.println(bothNotNull.test(null, "test")); // Output: false

    • Consumer<T>: void accept(T t)
      This interface defines the functional method "void accept(T t)" which declares a single parameter ("t" of type "T") and does not return a value. Implementations typically consume the value of "t" and produce a side effect. Consumers support method chaining with andThen().
      Consumer<String> println = t -> System.out.println(t);
      println.accept("test"); // output: test
      • IntConsumer: void accept(int t)
        This interface is a primitive specialization of "Consumer", specifying the parameter type "t" as "int".

      • LongConsumer: void accept(long t)
        This interface is a primitive specialization of "Consumer", specifying the parameter type "t" as "long".

      • DoubleConsumer: void accept(double t)
        This interface is a primitive specialization of "Consumer", specifying the parameter type "t" as "double".

    • BiConsumer<T, U>: void accept(T t, U u)
      This interface defines the functional method "void accept(T t, U u)" with two parameters ("t" of type "T", "u" of type "U") and no return value. Implementations typically consume both arguments and produce side effects. Supports method chaining with andThen().
      BiConsumer<String, String> printlnIfElse = (a, b) -> System.out.println(a == null ? b : a);
      printlnIfElse.accept(null, "test"); // Output: test
      • ObjIntConsumer<T>: void accept(T t, int u)
        A primitive specialization of "BiConsumer" with parameter "u" typed as "int".

      • ObjLongConsumer<T>: void accept(T t, long u)
        A primitive specialization of "BiConsumer" with parameter "u" typed as "long".

      • ObjDoubleConsumer<T>: void accept(T t, double u)
        A primitive specialization of "BiConsumer" with parameter "u" typed as "double".

    • Supplier<T>: T get()
      This interface defines the functional method "T get()" which declares no parameters and returns a value of type "T".
      Supplier<Double> random = () -> Math.random();
      System.out.println(random.get()); // Output: 0.7086586779711673
      • BooleanSupplier: boolean getAsBoolean()
        A primitive specialization of "Supplier" returning a "boolean".

      • IntSupplier: int getAsInt()
        A primitive specialization of "Supplier" returning an "int".

      • LongSupplier: long getAsLong()
        A primitive specialization of "Supplier" returning a "long".

      • DoubleSupplier: double getAsDouble()
        A primitive specialization of "Supplier" returning a "double".

    • Function<T, R>: R apply(T t)
      This interface defines the functional method "R apply(T t)" which declares one parameter ("t" of type "T") and returns a value of type "R". Implementations typically apply some logic to the argument "t". Functions support method chaining with compose() and andThen().
      Function<Integer, String> valueOf = t -> String.valueOf(t);
      System.out.println(valueOf.apply(1)); // Output: 1
      
      Function<String, String> trim = s -> s.trim();
      System.out.println(trim.apply("       hello")); // Output: hello
      
      Function<String, String> toUpperCase = s -> s.toUpperCase();
      System.out.println(toUpperCase.apply("       hello")); // Output:HELLO
      
      Function<String, String> trimThenToUpperCase = trim.andThen(toUpperCase);
      System.out.println(trimThenToUpperCase.apply("       hello")); // Output: HELLO

      • IntFunction<R>: R apply(int t)
        A primitive specialization of "Function" with parameter type "int".

      • LongFunction<R>: R apply(long t)
        A primitive specialization of "Function" with parameter type "long".

      • DoubleFunction<R>: R apply(double t)
        A primitive specialization of "Function" with parameter type "double".



      • ToIntFunction<T>: int applyAsInt(T t)
        A specialization of "Function" with an "int" return type.

      • ToLongFunction<T>: long applyAsLong(T t)
        A specialization of "Function" with a "long" return type.

      • ToDoubleFunction<T>: double applyAsDouble(T t)
        A specialization of "Function" with a "double" return type.



      • IntToLongFunction: long applyAsLong(int t)
        A specialization converting from "int" to "long".

      • IntToDoubleFunction: double applyAsDouble(int t)
        A specialization converting from "int" to "double".

      • LongToIntFunction: int applyAsInt(long t)
        A specialization converting from "long" to "int".

      • LongToDoubleFunction: double applyAsDouble(long t)
        A specialization converting from "long" to "double".

      • DoubleToIntFunction: int applyAsInt(double t)
        A specialization converting from "double" to "int".

      • DoubleToLongFunction: long applyAsLong(double t)
        A specialization converting from "double" to "long".

    • BiFunction<T, U, R>: R apply(T t, U u)
      This interface defines the functional method "R apply(T t, U u)" with two parameters ("t" of type "T", "u" of type "U") and returns a value of type "R". Implementations typically apply logic to both arguments. Supports method chaining with andThen().
      BiFunction<Integer, Integer, String> valueOfMax = (a, b) -> "id-".concat(String.valueOf(Math.max(a, b)));
      System.out.println(valueOfMax.apply(1, 2)); // Output: id-2
      • ToIntBiFunction<T, U>: int applyAsInt(T t, U u)
        A specialization of "BiFunction" returning an "int".

      • ToLongBiFunction<T, U>: long applyAsLong(T t, U u)
        A specialization of "BiFunction" returning a "long".

      • ToDoubleBiFunction<T, U>: double applyAsDouble(T t, U u)
        A specialization of "BiFunction" returning a "double".

    • UnaryOperator<T> extends Function<T, T>: T apply(T t)
      This interface is a specialization of "Function" where both the parameter and return types are the same "T". Inherits method chaining capabilities from Function including compose() and andThen().
      UnaryOperator<Integer> abs = t -> Math.abs(t);
      System.out.println(abs.apply(-1)); // Output: 1
      • IntUnaryOperator: int applyAsInt(int t)
        A primitive specialization of "Function" for "int".

      • LongUnaryOperator: long applyAsLong(long t)
        A primitive specialization of "Function" for "long".

      • DoubleUnaryOperator: double applyAsDouble(double t)
        A primitive specialization of "Function" for "double".

    • BinaryOperator<T> extends BiFunction<T, T, T>: T apply(T t, T u)
      This interface is a specialization of "BiFunction" where both arguments and return types are the same "T". Inherits method chaining capabilities from BiFunction including andThen().
      BinaryOperator<Integer> add = (a, b) -> a + b;
      System.out.println(add.apply(3, 4)); // Output: 7
      • IntBinaryOperator: int applyAsInt(int t, int u)
        A primitive specialization of "BiFunction" for "int".

      • LongBinaryOperator: long applyAsLong(long t, long u)
        A primitive specialization of "BiFunction" for "long".

      • DoubleBinaryOperator: double applyAsDouble(double t, double u)
        A primitive specialization of "BiFunction" for "double".
© 2025 mtitek
About