How do I use the List.sort() method?

The List.sort() method was introduced in Java 8. This method sorts the elements of the list on the basis of the given Comparator. If no comparator is provided, it will use the natural ordering of the elements (only if the elements are Comparable).

Let’s take a look at an example where we sort a list of integers in ascending order:

package org.kodejava.util;

import java.util.ArrayList;
import java.util.List;

public class ListSortExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(3);
        numbers.add(1);
        numbers.add(4);
        numbers.add(1);
        numbers.add(5);

        // Use sort() to sort the numbers in ascending order
        numbers.sort(null);

        System.out.println(numbers); 
    }
}

Outputs:

[1, 1, 3, 4, 5]

You can also pass a Comparator to List.sort(). Here’s an example where we sort a list of strings by their length:

package org.kodejava.util;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ListSortOtherExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("rat");
        words.add("elephant");
        words.add("cat");
        words.add("mouse");

        // Comparator for comparing string lengths
        Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length();

        // Use sort() to sort the words by their length
        words.sort(lengthComparator);

        System.out.println(words);
    }
}

Outputs:

[rat, cat, mouse, elephant]

In this case, the Comparator is a lambda expression that computes the difference in length between two strings. The List.sort() method uses this Comparator to determine the ordering of the strings in the list.

What is a Functional Interface in Java?

A Functional Interface in Java is an interface that has exactly one abstract method. Apart from this abstract method, it can include default and static methods. Java 8 introduced the @FunctionalInterface annotation to ensure an interface follows the rules of Functional Interface. It’s optional but good practice to use this annotation.

Functional interfaces are extensively used in Java’s lambda expressions. The main purpose of a functional interface is to be used as Lambda Expressions or Method References.

Here’s a basic example of defining a functional interface:

@FunctionalInterface
interface GreetingService {
    void sayMessage(String message);
}

You could use it in conjunction with a lambda like this:

GreetingService greetService = message -> System.out.println("Hello " + message);
greetService.sayMessage("world");

In the code above, message -> System.out.println("Hello " + message) is a lambda expression that provides the implementation of the abstract method sayMessage(String message).

Java 8 has also defined several built-in functional interfaces. These built-in interfaces are packed in the java.util.function package. Some common ones include Predicate<T>, Function<T, R>, Supplier<T>, and Consumer<T>. Furthermore, BinaryOperator<T>, UnaryOperator<T>, BiFunction<T, U, R> are some other standard functional interfaces available.

Here are some examples of Java 8 built-in functional interfaces:

1. Predicate

Predicate<T> is a functional interface that takes a single input and returns a boolean value. It is located in java.util.function package.

Predicate<String> lengthCheck = s -> s.length() > 5;
System.out.println(lengthCheck.test("Hello"));  // Output: false

Predicate is often used when you need to pass some sort of condition or filter as a parameter. For example, you might be checking if the User inputs are valid:

Predicate<String> isValidEmail = email -> email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
System.out.println(isValidEmail.test("test@gmail.com"));  // Output: true
System.out.println(isValidEmail.test("testgmail.com"));   // Output: false

2. Function

Function<T, R> is an interface that accepts one argument and produces a result.

Function<String, Integer> parse = Integer::parseInt;
System.out.println(parse.apply("123"));  // Output: 123

A Function<T, R> can be useful when you need to convert from one type to another, such as transforming a list of String into a list of Integer.

Function<String, Integer> stringToInteger = Integer::parseInt;
List<String> strings = Arrays.asList("1", "2", "3");
List<Integer> integers = strings.stream()
                                .map(stringToInteger)
                                .collect(Collectors.toList());

3. Consumer

Consumer<T> is an interface that takes one argument and returns no results. It is meant for implementing side effects.

Consumer<String> printer = System.out::println;
printer.accept("Hello");  // Output: Hello

The Consumer<T> interface is often used in conjunction with Java streams or Optional, where you have a collection of objects, and you want to perform a certain action on each of the objects.

Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
List<String> names = Arrays.asList("Jon", "Sansa", "Arya", "Bran");
names.forEach(printUpperCase);

4. Supplier

Supplier<T> is an interface that does not take any argument, but it produces a result.

Supplier<LocalDate> current = LocalDate::now;
System.out.println(current.get());  // Output: [current date]

Suppose you have a class RandomService that produces random numbers and is supposed to be used by other classes in your system.

class RandomService {
    Supplier<Double> getRandomNumber = Math::random;
}

// Usage in another class
RandomService rs = new RandomService();
System.out.println(rs.getRandomNumber.get());

5. BinaryOperator and UnaryOperator

UnaryOperator<T> takes one argument and returns a result of the same type. BinaryOperator<T> takes two arguments and returns a result of the same type.

UnaryOperator<String> upperifier = String::toUpperCase;
System.out.println(upperifier.apply("hello"));  // Output: HELLO

BinaryOperator<String> concatenator = String::concat;
System.out.println(concatenator.apply("Hello ", "World"));  // Output: Hello World

You have already learned about a few key functional interfaces in Java and how to use them with lambda expressions. Now, I’ll introduce you to a few more advanced topics about functional interfaces:

1. Custom Functional Interface

If the built-in functional interfaces in Java do not satisfy your requirements, you can define your own functional interfaces. Here is an example of a custom functional interface:

@FunctionalInterface
interface CustomInterface {
    String concatenateStrings(String s1, String s2);
}

You can now use it like this:

CustomInterface ci = (s1, s2) -> s1 + s2;
System.out.println(ci.concatenateStrings("Hello", " World")); // Output: Hello World

2. Method References

In some cases, lambdas just call an existing method. In those cases, we can use method references to make the code clearer. Here are some examples

Consumer<String> printer = System.out::println; // same as s -> System.out.println(s)

Predicate<String> lengthCheck = String::isEmpty; // same as s -> s.isEmpty()

Supplier<LocalDate> current = LocalDate::now; // same as () -> LocalDate.now()

3. Chaining Functional Interface Calls (Compose and AndThen)

You can chain multiple calls of Function, Consumer, and Predicate using default methods they provide, such as compose, andThen.

Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> add1 = x -> x + 1;
Function<Integer, Integer> add1AndThenMultiplyBy2 = add1.andThen(multiplyBy2);

System.out.println(add1AndThenMultiplyBy2.apply(2)); // Output: 6

Remember that with compose, functions execute in reverse order.

Function<Integer, Integer> multiplyBy2ThenAdd1 = add1.compose(multiplyBy2);

System.out.println(multiplyBy2ThenAdd1.apply(2)); // Output: 5

Chaining calls this way leads to functional-style programming that can make your code more readable and maintainable by creating pipelines of transformations.

Real-world use of the functional interface is prevalent in Java library features such as Stream API, where they come together with lambda expressions to offer functional programming capabilities. They help contribute to writing clean, robust and concurrent code structures.

Overall, functional interfaces bring the power of functional programming to Java and are extensively used for implementing simple callback-style interfaces, or for defining “thin” data structures used in control statements, among other uses.

What are Method References in Java?

Method references in Java are a feature that was introduced in Java 8. They provide a way to refer to a method without actually executing it. They are often used in conjunction with Java’s functional programming features, such as Streams and Lambdas, where a method to be executed is often expected as a parameter.

The syntax for a method reference is the name of the class (or the name of an object), followed by :: and the method’s name. Here’s an example:

List<String> words = Arrays.asList("Hello", "Method", "References", "In", "Java");

// Let's use a method reference to print each word in the list
words.forEach(System.out::println);

In the above code, System.out::println is a method reference. The forEach method expects a lambda that takes a parameter and does something with it. Here, the println method of the System.out class is being referenced, and it will be used to print each word in the list.

There are four types of method references in Java:

  1. Static method reference: They refer to the static methods of a class. For example, ClassName::staticMethodName.
  2. Instance method reference of a particular object: They refer to the instance methods of a particular object. For example, in above code System.out::println.
  3. Instance method reference of an arbitrary object: They refer to the instance methods where the first parameter is the target of the method. For example, String::length.
  4. Constructor reference: They refer to the constructor of a class. For example, ClassName::new.

Let’s take a deeper look at the four kinds of method references with more elaborated examples.

1. Static method references:

Static method references can be used when the method to be invoked is a static method. For example:

package org.kodejava.basic;

import java.util.stream.Stream;

public class StaticMethodRef {
    public static void main(String[] args) {
        String[] array = {"Java", "Python", "Ruby", "JavaScript"};
        Stream.of(array).forEach(StaticMethodRef::printStr);
    }

    static void printStr(String str) {
        System.out.println("printStr method called with value: " + str);
    }
}

In this example, the printStr method is a static method, and we reference this method using StaticMethodRef::printStr.

2. Instance method reference of a particular object:

Instance method references can be used when the method to be invoked is an instance method. For example:

package org.kodejava.basic;

import java.util.stream.Stream;

public class InstanceMethodRef {
    public static void main(String[] args) {
        InstanceMethodRef instance = new InstanceMethodRef();
        String[] array = {"Java", "Python", "Ruby", "JavaScript"};
        Stream.of(array).forEach(instance::printInstanceStr);
    }

    void printInstanceStr(String str) {
        System.out.println("printInstanceStr method called with value: " + str);
    }
}

In this example, printInstanceStr is an instance method, and we create an instance of InstanceMethodRef and refer to an instance method instance::printInstanceStr.

3. Instance method reference of an arbitrary object:

We can do this when we have a collection of instances and want to invoke a method on them. For example:

package org.kodejava.basic;

import java.util.stream.Stream;

public class InstanceMethodReferenceArbitrary {
    public static void main(String[] args) {
        String[] array = {"Java", "Python", "Ruby", "JavaScript"};
        Stream.of(array).map(String::toUpperCase).forEach(System.out::println);
    }
}

In this example, String::toUpperCase invokes the toUpperCase method for every instance of the String in the Stream.

4. Constructor reference:

Constructor references are used for a constructor call. For example:

package org.kodejava.basic;

import java.util.stream.Stream;

class Student {
    String name;

    Student(String name) {
        this.name = name;
    }
}

public class ConstructorReference {
    public static void main(String[] args) {
        Stream.of("John", "Martin", "Don")
                .map(Student::new)
                .forEach(student -> System.out.println("Student name is: " + student.name));
    }
}

In the above example, Student::new creates a new instance of Student.