Thursday, October 10, 2024

Java Stream API: A Beginner's Crash Course

java,stream api,functional programming,consumer,predicate,programming,software development,technology, software engineering
Java 8 introduced a powerful new feature called the Stream API, which marked a significant evolution in the language. Developers can use the Stream API to perform data processing operations on collections concisely and expressively. In this blog post, we'll look at the Stream API, its core concepts, and how it simplifies data processing in Java applications.

What is the Stream API?

In Java 8, a Stream is a collection of elements processed concurrently or sequentially. CPUs have become more powerful and complex due to recent advancements in hardware development, with multiple cores that can process data in parallel. The Stream API was introduced to support parallel data processing in Java, without the boiler code of defining and synchronizing threads. Unlike traditional collections, streams do not store data; they serve as a pipeline for transforming and aggregating data from a source such as a List, Set, or array. Streams enable developers to perform declarative and functional operations such as filtering, mapping, sorting, and reducing.

java,stream api,functional programming,consumer,predicate,programming,software development,technology
The Stream interface defines many operations, which can be divided into two categories: intermediate operations and terminal operations. Stream operations that can be connected are referred to as intermediate operations, while those that close a stream are known as terminal operations.

java,stream api,functional programming,consumer,predicate,programming,software development,technology

We can divide the operations into two groups:
  • Intermediate Operations: filter() and map() return another stream as a return type and can be combined to form a pipeline.
  • Terminal Operationscollect() executes the pipeline, returns the result, and closes it.

Creating Streams

Before optimizing our code with streams, let's look at how to create them. To create a stream, we need a source. That source could be anything: a collection (list, set, or map), an array, or I/O resources that are used as input.
A stream does not change its source, so multiple stream instances can be created from the same source and used for different purposes.


Create Streams from Collections

In the code snippet below, we convert a list of integers into a stream by calling the stream() method. After creating a stream, we use the forEach() method to print the stream's values as well as the name of the execution thread on which this code is running.
public class StreamFromCollection {
    public static void main(String[] args) {
        List<Integer> geometrySeries = List.of(1, 3, 9, 27, 81, 243);
        geometrySeries.stream()
                .forEach(s -> System.out.println(Thread.currentThread().getName() + ": " + s));
    }
}
The previous code generates a stream of integer elements. The most commonly used method is forEach(), which iterates over the stream's elements and prints their values. Because the output of the forEach() method is not a stream, it is also referred to as a terminal function (operation).


Create Streams from Arrays

We can also create streams from arrays. Consider the following piece of code.
public class StreamFromArrays {
    public static void main(String[] args) {
        Integer<> squareSeries = {1, 4, 9, 16, 25};
        Arrays.stream(squareSeries)
                .forEach(s -> System.out.println(Thread.currentThread().getName() + ": " + s));
    }
}
The static method stream(int[] array) from the utility class Arrays creates a stream of primitives.

By using the parallel() method, we can also parallelise the stream that is created from arrays.
public class StreamFromArrays {
    public static void main(String[] args) {
        Integer<> squareSeries = {1, 4, 9, 16, 25};
        Arrays.stream(squareSeries).parallel()
                .forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i));
    }
}

In the previous example, we saw how to pass an array object to the stream() method. The stream() method also allows us to pass the start and end indexes along with the array object. This will create a subarray of the original array object.
public class StreamFromArrays {
    public static void main(String[] args) {
        Integer<> squareSeries = {1, 4, 9, 16, 25, 36, 49};
        Arrays.stream(squareSeries, 3, squareSeries.length - 1)
                .forEach(i -> System.out.println(Thread.currentThread().getName() + ": " + i));
    }
}

Create Finite Stream

You can also create streams without a source, such as collections or arrays, using Stream.generate() and Stream.builder(). The code below shows how the builder() method can be used to generate a finite stream of Integer values. 
public class StreamFinite {
    public static void main(String[] args) {
        Stream<Integer> integerStream = Stream.<Integer>builder()
                .add(1).add(2).add(9).add(16).add(25)
                .build();
        integerStream.takeWhile(s -> s > 0)
                .forEach(s -> System.out.println(s + " "));
    }
}
Because the builder() method is generic, it is required to specify the type that will be used to create the stream's elements.

The generate() method can be used to create a stream. This method takes an instance of Supplier<T> as a parameter.
public class StreamFinite {
    public static void main(String[] args) {
        Stream<BigDecimal> randomDecimal = Stream.generate(
                new Supplier<BigDecimal>() {
                    @Override
                    public BigDecimal get() {
                        Random random = new Random();
                        return BigDecimal.valueOf(random.nextDouble() * 100)
                                .setScale(2, RoundingMode.UP);
                    }
                }
        ).limit(10);
        randomDecimal.forEach(s -> System.out.println(s + " "));
    }
}
To keep the stream finite, we used the limit() method to control the number of elements generated.

Product class

Here's a simple Product class with a few properties. We will be using this class throughout the tutorial.

Product List

We've written a static method that returns a list of Products. This product list is used later in the tutorial.

Intermediate Operation: map() And Terminal Operation: collect()

The map() method is used to perform intermediate operations. Intermediate operations process elements are in the stream and send the result as a stream to the next function in the pipeline. It takes an argument of type Function<T, R>, essentially a reference to a function, and then applies this function to each element of the stream and returns a stream.

A stream of objects can be transformed in a variety of ways using the map() method. For instance, we can extract a particular attribute or field from each item in a stream or change an object stream from one type to another.

👉 Consider this example. Here, we will find the square of each element in the stream and print the resulting stream.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamMapExample {
    public static void main(String args[]) {
      List<Integer> result = Arrays.asList(2, 3, 4, 5, 6, 7, 8)
				.stream()
				.map(n-> n*n)
				.collect(Collectors.toList());
      System.out.println(result);
    }
}
java,stream api,functional programming,consumer,predicate,programming,software development,technology
The map() method in the preceding program takes the stream values one by one and executes the logic that we specified. The collector then gathers the resultant elements in a list and returns it to the caller method.


Convert lowercase letters to uppercase

Let's look at another example of the Java stream's map() method, this time converting all lowercase letters to uppercase letters. The code for this is shown below.
import java.util.stream.Stream;
import java.util.function.Function;

public class StreamMapExample {
    public static void main(String args[]) {
        Function<String, String> uppercaseFunction = String::toUpperCase;
	Stream.of("Seoul", "Istanbul", "Chicago", "Barcelona", "Sydney")
		.map(uppercaseFunction)
		.forEach(System.out::println);
    }
}


Number of characters in each word

In this example, we want to know how many characters are in each word. We can accomplish this task by calling the map() method. This method will take each word and return its length.
public class StreamMapCollect {
    public static void main(String[] args) {
        List<String> countries = List.of("Barbados", "Japan", "New Zealand");
        List<Integer> nameLength = countries
                .stream()
                .map(String::length)
                .collect(Collectors.toList());
        System.out.println(nameLength);
    }
}
java,stream api,functional programming,consumer,predicate,programming,software development,technology

Fetch values from a list of objects

In the next example, we will extract a list of brand names from the Product list.
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamMapCollect {
	public static void main(String[] args) {
		List<String> brandList = ProductList.getProductList()
				.stream().map(Product::getBrand)
				.collect(Collectors.toList());
		System.out.println(brandList);

		Function<String, String> uppercaseFunction = String::toUpperCase;
		brandList = ProductList.getProductList()
				.stream().map(obj -> uppercaseFunction.apply(obj.getBrand()))
				.collect(Collectors.toList());
		System.out.println(brandList);
	}
}
The map() method returns a String stream. Using the collect() method, all of these elements are added to a List<String>. This method adds elements to a Collection instance as they are processed.

Fetch distinct values from a list of objects

We can find distinct brand names in this manner:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamMapCollect {
	public static void main(String[] args) {
		List<String> distinctBrands = ProductList.getProductList()
			.stream().map(Product::getBrand).distinct()
			.collect(Collectors.toList());
		System.out.println(distinctBrands);
	}
}
The distinct() method takes a stream and returns a stream containing all of the stream's different elements.

👉 We can find distinctive brand names in another way.
public class StreamMapCollect {
	public static void main(String[] args) {
		Set<String> brandSet = ProductList.getProductList()
				.stream()
                .map(Product::getBrand).collect(Collectors.toSet());
		System.out.println(brandSet);
	}
}
So we're collecting the processed elements in a Set<String> with the collect() method. We will get a collection of distinct elements because Set does not permit duplicate elements.


Create a list of Object using Stream

👉 We can extract elements from the stream and create another list of objects using the map() method.
public class StreamMapCollect {
	public static void main(String[] args) {
		Set<ProductDetails> productSet = ProductList.getProductList()
			.stream().map(obj -> {
				return new ProductDetails(obj.getTitle(), 
                		obj.getDescription(), 
                    		obj.getPrice());
			}).collect(Collectors.toSet());
		System.out.println(productSet);
	}
}

class ProductDetails {
	private String name;
	private String description;
	private double price;

	public ProductDetails() {
	}

	public ProductDetails(String name, String description, double price) {
		this.name = name;
		this.description = description;
		this.price = price;
	}

	public String getName() {
		return name;
	}

	public String getDescription() {
		return description;
	}

	public double getPrice() {
		return price;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public void setPrice(double price) {
		this.price = price;
	}

	@Override
	public String toString() {
		return "ProductDetails [name=" + name + ", description=" + description + ", price=" + price + "]";
	}
}
We made a ProductDetails class with a few properties. We extract a few properties from ProductList and use them to create ProductDetails objects with the map(..) method.


Intermediate Operation: filter()

The Stream API's filter() method chooses elements from a collection based on a condition. It takes a predicate (a functional interface that returns a boolean) as input and returns a new stream containing only elements that satisfy the condition. Here’s the syntax:
Stream<T> filter(Predicate<? super T> predicate)
Let's look at a basic example. Assume we have a list of integers and want to exclude the even numbers.
public class StreamFilterCollect {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(19, 24, 32, 47, 52, 67, 71, 85);

        // Predicate to filter out even numbers
        Predicate<Integer> predicate = (x) -> x % 2 != 0;

        List<Integer> oddNumbers = numbers
                .stream()
                .filter(predicate) 
                .collect(Collectors.toList());

        System.out.println("Odd Numbers: " + oddNumbers);
    }
}
java,stream api,functional programming,consumer,predicate,programming,software development,technology


Filtering Objects

You can also filter a list of objects based on specific attributes. Our data source will be ProductList.
public class StreamFilterExample {
    public static void main(String[] args) {
        List<Product> products = ProductList.getProductList()
                .stream()
                .filter(e -> e.getRating() > 4.5)
                .collect(Collectors.toList());
        System.out.println("Products :: " + products);
    }
}
We've filtered the products based on their ratings. In this case, predicate is mentioned in the filter() method.

We can use the && or || operators in the filter() method to apply multiple conditions.
public class StreamFilterExample {
    public static void main(String[] args) {
        List<Product> products = ProductList.getProductList()
                .stream()
                .filter(e -> e.getRating() > 4.5
                        && e.getDiscountPercentage() > 10)
                .collect(Collectors.toList());
        System.out.println("Products :: " + products);
    }
}
Products are filtered by their ratings and discounts.


Combining filter() with map()

To extract Product titles based on ratings and discounts, we first filter the stream and then use map() to extract the title.
public class StreamFilterExample {
    public static void main(String[] args) {
        List<String> productTitles = ProductList.getProductList()
                .stream()
                .filter(e -> e.getRating() > 4.5
                        && e.getDiscountPercentage() > 10)
//                .map(e -> e.getTitle())
                .map(Product::getTitle)
                .collect(Collectors.toList());
        System.out.println("Product titles :: " + productTitles);
    }
}


Intermediate Operation: findAny()

Use findAny() to find any element in the current stream. It yields an Optional<T> instance. Here's a basic example.
public class StreamFindAnyDemo {
    public static void main(String[] args) {
        Optional<Integer> anyNumber = Arrays.asList(8, 57, 19, 14, 51, 33, 38)
                .stream()
                .filter(x -> x % 2 != 0)
                .findAny();
        anyNumber.ifPresent(n -> System.out.println(n));
    }
}
java,stream api,functional programming,consumer,predicate,programming,software development,technology

👉 We can also select a product from our Product List.
public class StreamExample {
    public static void main(String[] args) {
        Optional<Product> productAny = ProductList.getProductList()
                .stream()
                .findAny();
        productAny.ifPresent(c -> System.out.println(c));
    }
}

👉 We can use findAny() alongside other intermediate operations. In the example below, it is combined with filter().
public class StreamExample {
    public static void main(String[] args) {        
        Optional<Product> productFilterAny = ProductList.getProductList()
                .stream()
                .filter(e -> e.getPrice() > 890)
                .findAny();
        productFilterAny.ifPresent(c -> System.out.println(c));
    }
}


Intermediate Operation: anyMatch()

We want to know if a specific element is in the stream, we can use anyMatch(). It accepts Predicate<T> arguments and returns a true or false boolean.

Here's a simple code snippet for checking whether or not a specific word is in the list.
public class StreamAnyMatchExample {
    public static void main(String[] args) {
        Boolean isAnimalPresent = List.of("Fern", "Zebra", "Peace Lily", "Snake Plant")
                .stream()
                .anyMatch(e -> e.equals("Zebra"));
        if (isAnimalPresent) {
            System.out.println("List contains animal name.");
        } else {
            System.out.println("We have a list of plant names.");
        }
    }
}
java,stream api,functional programming,consumer,predicate,programming,software development,technology

👉 The code snippet below checks to see if there is a Product in the stream with a price greater than a specified amount.
public class StreamAnyMatchExample {
    public static void main(String[] args) {
	// Is there any Product of price greater than 1000/- in the Product list.
        Predicate<Product> predicate = (p) -> p.getPrice() > 1000;
        Boolean anyMatch = ProductList.getProductList()
                .stream()
                .anyMatch(predicate);
        System.out.println("Product of price 1000/- is preset? " + anyMatch);
    }
}

👉 We can combine the anyMatch() and filter() methods. We've got a list of brand names. We want to know if these brand names are present in our existing stream.
public class StreamAnyMatchExample {
    public static void main(String[] args) {
        List<String> brandNames = List.of(new String[]{"Apple", "Boat"});

        List<Product> products = ProductList.getProductStream()
                .filter(e -> brandNames.stream().anyMatch(a -> a.equals(e.getBrand())))
                .collect(Collectors.toList());
        System.out.println("Products :: " + products);
    }
}


Conclusion

The Stream API is an effective tool for modern Java development. Understanding its key concepts and operations will help you write more concise, readable, and efficient code. Whether you're working with collections, arrays, or custom data sources, the Stream API gives you a flexible and declarative way to process data.

Happy coding!!! 😊
in

Popular posts