CodeGym /Java Course /Module 2. Java Core /Working with streams, part 1

Working with streams, part 1

Module 2. Java Core
Level 6 , Lesson 3
Available

1. List of methods of the Stream class

The Stream class was created to make it easy to construct chains of data streams. To achieve this, the Stream<T> class has methods that return new Stream objects.

Each of these data streams does one simple action, but if you combine them into chains and add interesting lambda functions, then you have a powerful mechanism for generating the output you want. Soon you will see for yourself.

Here are the methods of the Stream class (only the most basic ones):

Methods Description
Stream<T> of()
Creates a stream from a set of objects
Stream<T> generate()
Generates a stream according to the specified rule
Stream<T> concat()
Concatenates two streams
Stream<T> filter()
Filters the data, only passing along data that matches the specified rule
Stream<T> distinct()
Removes duplicates. Does not pass along data that has already been encountered
Stream<T> sorted()
Sorts the data
Stream<T> peek()
Performs an action on each element in the stream
Stream<T> limit(n)
Returns a stream that is truncated so that it is no longer than the specified limit
Stream<T> skip(n)
Skips the first n elements
Stream<R> map()
Converts data from one type to another
Stream<R> flatMap()
Converts data from one type to another
boolean anyMatch()
Checks whether there is at least one element in the stream that matches the specified rule
boolean allMatch()
Checks whether all the elements in the stream match the specified rule
boolean noneMatch()
Checks whether none of the elements in the stream match the specified rule
Optional<T> findFirst()
Returns the first element found that matches the rule
Optional<T> findAny()
Returns any element in the stream that matches the rule
Optional<T> min()
Searches for the minimum element in the data stream
Optional<T> max()
Returns the maximum element in the data stream
long count()
Returns the number of elements in the data stream
R collect()
Reads all the data from the stream and returns it as a collection

2. Intermediate and terminal operations by the Stream class

As you can see, not all methods in the table above return a Stream. This is related to the fact that the methods of the Stream class can be divided into intermediate (also known as non-terminal) methods and terminal methods.

Intermediate methods

Intermediate methods return an object that implements the Stream interface, and they can be chained together.

Terminal methods

Terminal methods return a value other than a Stream.

Method call pipeline

Thus, you can build a stream pipeline consisting of any number of intermediate methods and a single terminal method call at the end. This approach lets you implement rather complex logic, while increasing code readability.

Method call pipeline

The data inside a data stream does not change at all. A chain of intermediate methods is a slick (declarative) way of specifying a data processing pipeline that will be executed after the terminal method is called.

In other words, if the terminal method is not called, then the data in the data stream is not processed in any way. Only after the terminal method is called does the data begin to be processed according to the rules specified in the stream pipeline.

stream()
  .intemediateOperation1()
  .intemediateOperation2()
  ...
  .intemediateOperationN()
  .terminalOperation();
General appearance of a pipeline

Comparison of intermediate and terminal methods:

intermediate terminal
Return type Stream not a Stream
Can be combined with multiple methods of the same type to form a pipeline yes no
Number of methods in a single pipeline any no more than one
Produces the end result no yes
Starts processing the data in the stream no yes

Let's look at an example.

Suppose we have a club for animal lovers. Tomorrow the club celebrates Ginger Cat Day. The club has pet owners, each of which has a list of pets. They are not limited to cats.

Task: you need to identify all the names of all the ginger cats in order to create personalized greeting cards for them for tomorrow's "professional holiday". The greeting cards should be sorted by the cat's age, from oldest to youngest.

First, we provide some classes to help solving this task:

public enum Color {
   WHITE,
   BLACK,
   DARK_GREY,
   LIGHT_GREY,
   FOXY,
   GREEN,
   YELLOW,
   BLUE,
   MAGENTA
}
public abstract class Animal {
   private String name;
   private Color color;
   private int age;

   public Animal(String name, Color color, int age) {
      this.name = name;
      this.color = color;
      this.age = age;
   }

   public String getName() {
      return name;
   }

   public Color getColor() {
      return color;
   }

   public int getAge() {
      return age;
   }
}
public class Cat extends Animal {
   public Cat(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Dog extends Animal {
   public Dog(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Parrot extends Animal {
   public Parrot(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Pig extends Animal {
   public Pig(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Snake extends Animal {
   public Snake(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Owner {
   private String name;
   private List<Animal> pets = new ArrayList<>();

   public Owner(String name) {
      this.name = name;
   }

   public List<Animal> getPets() {
      return pets;
   }
}

Now let's look at the Selector class, where the selection will be made according to the specified criteria:

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

public class Selector {
   private static List<Owner> owners;

   private static void initData() {
      final Owner owner1 = new Owner("Ronan Turner");
      owner1.getPets().addAll(List.of(
            new Cat("Baron", Color.BLACK, 3),
            new Cat("Sultan", Color.DARK_GREY, 4),
            new Dog("Elsa", Color.WHITE, 0)
      ));

      final Owner owner2 = new Owner("Scarlet Murray");
      owner2.getPets().addAll(List.of(
            new Cat("Ginger", Color.FOXY, 7),
            new Cat("Oscar", Color.FOXY, 5),
            new Parrot("Admiral", Color.BLUE, 3)
      ));

      final Owner owner3 = new Owner("Felicity Mason");
      owner3.getPets().addAll(List.of(
            new Dog("Arnold", Color.FOXY, 3),
            new Pig("Vacuum Cleaner", Color.LIGHT_GREY, 8)
      ));

      final Owner owner4 = new Owner("Mitchell Stone");
      owner4.getPets().addAll(List.of(
            new Snake("Mr. Boa", Color.DARK_GREY, 2)
      ));

      final Owner owner5 = new Owner("Jonathan Snyder");
      owner5.getPets().addAll(List.of(
            new Cat("Fisher", Color.BLACK, 16),
            new Cat("Zorro", Color.FOXY, 14),
            new Cat("Margo", Color.WHITE, 3),
            new Cat("Brawler", Color.DARK_GREY, 1)
      ));

      owners = List.of(owner1, owner2, owner3, owner4, owner5);
   }
}

What remains is to add code to the main method. Currently, we first call the initData() method, which populates the list of pet owners in the club. Then we select the names of ginger cats sorted by their age in descending order.

First, let's look code that does not use streams to solve this task:

public static void main(String[] args) {
   initData();

   List<String> findNames = new ArrayList<>();
   List<Cat> findCats = new ArrayList<>();
   for (Owner owner : owners) {
      for (Animal pet : owner.getPets()) {
         if (Cat.class.equals(pet.getClass()) && Color.FOXY == pet.getColor()) {
            findCats.add((Cat) pet);
         }
      }
   }

   Collections.sort(findCats, new Comparator<Cat>() {
      public int compare(Cat o1, Cat o2) {
         return o2.getAge() - o1.getAge();
      }
   });

   for (Cat cat : findCats) {
      findNames.add(cat.getName());
   }

   findNames.forEach(System.out::println);
}

Now let's look at an alternative:

public static void main(String[] args) {
   initData();

   final List<String> findNames = owners.stream()
           .flatMap(owner -> owner.getPets().stream())
           .filter(pet -> Cat.class.equals(pet.getClass()))
           .filter(cat -> Color.FOXY == cat.getColor())
           .sorted((o1, o2) -> o2.getAge() - o1.getAge())
           .map(Animal::getName)
           .collect(Collectors.toList());

   findNames.forEach(System.out::println);
}

As you can see, the code is much more compact. In addition, each line of the stream pipeline is a single action, so they can be read like sentences in English:

.flatMap(owner -> owner.getPets().stream())
Move from a Stream<Owner> to a Stream<Pet>
.filter(pet -> Cat.class.equals(pet.getClass()))
Retain only cats in the data stream
.filter(cat -> Color.FOXY == cat.getColor())
Retain only ginger cats in the data stream
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
Sort by age in descending order
.map(Animal::getName)
Get the names
.collect(Collectors.toList())
Put the result into a list

3. Creating streams

The Stream class has three methods that we haven't covered yet. The purpose of these three methods is to create new threads.

Stream<T>.of(T obj) method

The of() method creates a stream that consists of a single element. This is usually needed when, say, a function takes a Stream<T> object as an argument, but you only have a T object. Then you can easily and simply use the of() method to get a stream that consists of a single element.

Example:

Stream<Integer> stream = Stream.of(1);

Stream<T> Stream.of(T obj1, T obj2, T obj3, ...) method

The of() method creates a stream that consists of passed elements. Any number of elements is allowed. Example:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

Stream<T> Stream.generate(Supplier<T> obj) method

The generate() method lets you set a rule that will be used to generate the next element of the stream when it is requested. For example, you can give out a random number each time.

Example:

Stream<Double> s = Stream.generate(Math::random);

Stream<T> Stream.concat(Stream<T> a, Stream<T> b) method

The concat() method concatenates the two passed streams into one. When the data is read, it is read first from the first stream, and then from the second. Example:

Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = Stream.of(10, 11, 12, 13, 14);
Stream<Integer> result = Stream.concat(stream1, stream2);

4. Filtering data

Another 6 methods create new data streams, letting you combine streams into chains (or pipelines) of varying complexity.

Stream<T> filter(Predicate<T>) method

This method returns a new data stream that filters the source data stream according to the passed rule. The method must be called on an object whose type is Stream<T>.

You can specify the filtering rule using a lambda function, which the compiler will then convert to a Predicate<T> object.

Examples:

Chained streams Explanation
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x < 3));

Retain only numbers less than three
Stream<Integer> stream = Stream.of(1, -2, 3, -4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x > 0));

Retain only numbers greater than zero

Stream<T> sorted(Comparator<T>) method

This method returns a new data stream that sorts the data in the source stream. You pass in a comparator, which sets the rules for comparing two elements of the data stream.

Stream<T> distinct() method

This method returns a new data stream that contains only the unique elements in the source data stream. All duplicate data is discarded. Example:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.distinct(); // 1, 2, 3, 4, 5

Stream<T> peek(Consumer<T>) method

This method returns a new data stream, though the data in it is the same as in the source stream. But when the next element is requested from the stream, the function you passed to the peek() method gets called with it.

If you pass the function System.out::println to the peek() method, then all the objects will be displayed when they will pass through the stream.

Stream<T> limit(int n) method

This method returns a new data stream that contains only first n elements in the source data stream. All other data is discarded. Example:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.limit(3); // 1, 2, 3

Stream<T> skip(int n) method

This method returns a new data stream that contains all the same elements as the source stream, but skips (ignores) the first n elements. Example:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.skip(3); // 4, 5, 2, 2, 2, 3, 4

4
Task
Java Core, level 6, lesson 3
Locked
My first thread
The grand moment arrived at last! Brace yourself: you have to create your very own thread. Create a public static TestThread class, a thread with the Runnable interface.
4
Task
Java Core, level 6, lesson 3
Locked
My second thread
Let's continue to unravel threads. This time, we need to create a public static TestThread class that inherits the Thread class. Then we'll create a static block inside TestThread, which will display "This is the static block inside TestThread". And the run method should display "This is the run method".
9
Task
Java Core, level 6, lesson 3
Locked
A list and some threads
Let's multiply threads again and again. In the main method, add five threads to the static list. Each thread must be a new Thread object that works with its own SpecialThread object. The SpecialThread class's run method should display "This is the run method inside SpecialThread".
4
Task
Java Core, level 6, lesson 3
Locked
Displaying a stack trace
Do you still remember about the stack trace and the fact that the currently running method is at the top of the stack? Let's recall this by completing a task: you need to create a "task" (a public static class called SpecialThread that implements the Runnable interface). SpecialThread should display its own stack trace.
9
Task
Java Core, level 6, lesson 3
Locked
Let's talk music
Even robotic programmers yearn for art! Today we're talking about bowed string instruments. We have a Violin class. You need to change it to make it a task for a thread. To do this, use the MusicalInstrument interface. And then you can "play" it and display how long you've been playing.
Comments (5)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Ranganathan Kasiganesan Level 75, Chennai, India Expert
29 March 2024
Though I know lambdas and streams thanks to CodeGym for providing excellent content. Kudos to the team.
m-troja Level 21, Warsaw, Poland
7 January 2024
crazy lesson
Abhishek Tripathi Level 72, Rewa, India Expert
5 October 2023
before going through please complete the lambda expression in deep and also complete the different java.util.Functions package's interfaces and use.
Dillon Morgan Level 21, London, United Kingdom
26 December 2023
Agreed. Understanding functional interfaces is the key to deciphering this lesson. Specifically, those used in conjunction with the Stream <T> API.
Krzysiek Nowak Level 33, Poland, Poland
18 April 2023
This topic is kinda tough