Streams are quite fun - in short: it’s a different approach to writing loops. Suppose you have a List of Strings and you want to filter out the String starting with an ’s' and put them in a new List.

List<String> sStrings = new ArrayList<>();
for(int i = 0; i < strings.size(); i++){
    if(strings.get(i).startsWith('s')){
      //do something with strings starting with s
      sStrings.add(strings.get(i));
    }
}

Although still readable, there is a lot of redundant code: the for loop itself with the counter and condition, the if statement. With streams, it looks more readable.

List<String> sStrings = strings.stream()
  .filter(s -> s.startsWith('s'))
  collect(Collectors.toList());

Inside the filter method, you have a function - this has the same meaning as in mathematics: you have some inputs, the functions does it magic and something different comes out. In this case, the function takes a String and outputs true when the String starts with an ’s'.

This filter generates a stream on its own, in this case a stream of Strings all starting with an ’s'. You can collect the results, in this case a List.

Besides filter you also have map, suppose you want the length of all Strings starting with an ’s'. You can just add a map after the filter statement. map also takes a function, in this case a String as input, and it outputs a number, the length of the strings. You can string filter and map calls as much as you want. There is a second map, turning an in into an Integer. It is not mandatory as such with autoboxing available, but it shows you can have as much maps as you want.

List<Integer> stringLengths = strings.stream()
  .filter(s -> s.startsWith('s'))
  .map(s -> s.length())
  .map(i -> new Integer(i))
  collect(Collectors.toList());