Streams

Source

  • Array, Collection, Generator function, I/O channel

Object Streams

  • Collections
// native support
List<String> list = Arrays.asList("a", "b");
Stream<String> stream = list.stream();
  • Object Arrays
Person[] peopleArray = { 
    new Person("John", "Boston", 21), 
    new Person("Mary", "Boston", 29) 
};
 
// Both are same
Stream<Person> stream1 = Stream.of(peopleArray);
Stream<Person> stream2 = Arrays.stream(peopleArray);
 
// Warning!!!!
// Never pass a primitive array (int[]) to Stream.of(). 
// It creates a Stream<int[]> with a size of 1
Stream<int[]> badStream = Stream.of(arr);
  • From Direct Values
Stream<String> words = Stream.of("hello", "world");

Primitive Streams

  • Only 3 Specialized Primitive Streams
    • IntStream
      • used for byte, short, char, int
    • LongStream
      • used for long
    • DoubleStream
      • used for float, double
  • From Primitive Arrays
int[] arr = {4, 3, 7};
IntStream stream = Arrays.stream(arr);
  • From Direct Values
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
  • Numeric Ranges
// range: [0, 10) = [0, 9]
IntStream zeroToNine = IntStream.range(0, 10);      
// range: [0, 10] 
IntStream zeroToTen  = IntStream.rangeClosed(0, 10);

Boxing and Unboxing

  • stream.boxed()
    • IntStream —> Stream<Integer>
  • stream.mapToInt()
    • Stream<Integer> —> IntStream
int[] nums = {4, 3, 7, 8, 1};
 
// Boxing: IntStream -> Stream<Integer> -> List<Integer>
List<Integer> numbers = Arrays.stream(nums).boxed().toList();
 
// Unboxing: List<Integer> -> Stream<Integer> -> IntStream -> int[]
int[] finalNums = numbers.stream().mapToInt(n -> n).toArray();

Intermediate vs Terminal Operators

  • https://stackoverflow.com/questions/47688418/what-is-the-difference-between-intermediate-and-terminal-operations
  • Intermediate:
    • filter(Predicate)
    • map(Function)
      • mapToInt(ToIntFunction)
      • mapToLong(ToLongFunction)
      • mapToDouble(ToDoubleFunction)
    • flatMap(Function)
      • flatMapToInt(ToIntFunction)
      • flatMapToLong(ToLongFunction)
      • flatMapToDouble(ToDoubleFunction)
    • sorted() same as sorted(Comparator.naturalOrder())
      • sorted(Comparator)
    • peek(Consumer)
    • distinct()
    • limit(long n)
    • skip(long n)
  • Terminal:
    • collect(Collector)
    • toList() — Returns unmodifiable list
      • Not same as collect(Collectors.toUnmodifiableList())
      • For modifiable list: collect(Collectors.toList())
    • toArray()
    • forEach(Consumer)
    • forEachOrdered(Consumer)
    • reduce(BinaryOperator)
    • min(Comparator)
    • max(Comparator)
    • count()
    • Short Circuiting:
      • Can stop processing elements early if match found
      • anyMatch(Predicate)
      • allMatch(Predicate)
      • noneMatch(Predicate)
      • findFirst() — Returns Optional
      • findAny() — Returns Optional
  • Not supported by Primitive streams
    • collect(Collector) —> Must call .boxed() first
    • toList() —> Must call .boxed().toList()
    • sorted(Comparator) —> Only supports no-argument .sorted()
    • min(Comparator) / max(Comparator) —> Overloaded to accept NO arguments
  • Primitive-Only Terminal Operators
    • sum()
    • average()
    • summaryStatistics()
      • getAverage()
      • getCount()
      • getSum()
      • getMin()
      • getMax()
    • asDoubleStream()
    • asLongStream()

Collectors

  • These are static factory methods of the Collectors interface
  • It is consumed by collect(Collector) stream terminal method and as a downstreamCollector in another Collector argument

Grouping and Partitioning

  • Grouping:
    • Returns: Map<Key, List<>>
    • groupingBy(keyMapper)
    • groupingBy(keyMapper, downstreamCollector)
    • groupingBy(keyMapper, mapFactory, downstreamCollector)
      • mapFactory specifies custom Map type like TreeMap::new
  • Partitioning
    • Returns: Map<Boolean, List<>>
    • Splits exclusively by true/false
    • partitioningBy(Predicate)
    • partitioningBy(Predicate, downstreamCollector)

Data Structure Collections

  • toList()
  • toSet()
  • toMap(keyMapper, valueMapper)
  • toMap(keyMapper, valueMapper, mergeFunction)
    • Crucial for handling duplicate key conflicts
  • toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
    • mapFactory specifies custom Map type like TreeMap::new
  • toUnmodifiableList() (Java 10+)

Aggregations

  • joining(delimiter) — String case
  • reducing(T, BinaryOperator)
  • collectingAndThen(downstreamCollector, finisherFunction)
  • counting()
  • minBy(Comparator)
  • maxBy(Comparator)

Mathematical Summaries

  • Sum
    • summingInt(mapper)
    • summingLong(mapper)
    • summingDouble(mapper)
  • Average
    • averagingInt(mapper)
    • averagingLong(mapper)
    • averagingDouble(mapper)
  • Summary Statistics
    • summarizingInt(mapper)
    • summarizingLong(mapper)
    • summarizingDouble(mapper)
    • Returns a SummaryStatistics object containing methods
      • getAverage()
      • getCount()
      • getSum()
      • getMin()
      • getMax()

Examples

Sum and avg. of all the integers

int[] nums = {4, 3, 7, 8, 1};
System.out.println(Arrays.stream(nums).sum()); // IntStream.count()
 
List<Integer> numbers = Arrays.stream(nums) // IntStream to List
        .boxed()
        .toList();
 
// Using mapToInt()
numbers.stream()
        .mapToInt(n -> n).sum();
 
// Using Collectors.summingInt()
numbers.stream()  
        .collect(Collectors.summingInt(n -> n));
 
// Avg
numbers.stream()
    .mapToDouble(Integer::doubleValue) // !important
    .average()
    .orElse(0.0);

String based

// Uppercase all strings
var names = Arrays.asList("Sachin", "manisH", "Suresh");
final List<String> uppercaseNames = names.stream()
    .map(String::toUpperCase)
    .toList();
 
// count of strings with length > 5
List<String> list = new ArrayList<>(List.of("he", "hello", "kiamotors", "helloworld", "hd"));
long count = list.stream().filter(e -> e.length() > 5).count();

Data Setup

@Getter
@Setter
@AllArgsConstructor
class Employee {
    private int id;
    private String name;
    private int age;
    private String departNames;
    private String address;
    private double salary;
    private String gender;
}
 
List<Employee> employees = Arrays.asList(
    new Employee(1, "Abraham", 29, "IT", "Mumbai", 20000, "Male"),
    new Employee(2, "Mary", 27, "Sales", "Chennai", 25000, "Female"),
    new Employee(3, "Joe", 28, "IT", "Chennai", 22000, "Male"),
    new Employee(4, "John", 29, "Sales", "Gurgaon", 29000, "Male"),
    new Employee(5, "Liza", 25, "Sales", "Bangalore", 32000, "Female"),
    new Employee(6, "Peter", 27, "Admin", "Mumbai", 31500, "Male"),
    new Employee(7, "Harry", 30, "Research", "Kochi", 21000, "Male")
);

Max age of employees

employees.stream()  
        .mapToInt(Employee::getAge)  
        .max().orElse(-1);

Count of employee in each department

employees.stream().collect(
    Collectors.groupingBy(Employee::getDepartment, Collectors.counting())
);

Second Lowest/Highest Salary

var secondLowestSalaryEmployee = employees.stream()
        .sorted(Comparator.comparing(Employee::getSalary))
        .skip(1)
        .findFirst().orElse(null);
 
var secondHighestSalaryEmployee = employees.stream()
        .sorted(Comparator.comparing(Employee::getSalary).reversed())
        .skip(1)
        .findFirst().orElse(null);

Department with maximum number of employees

employees.stream()
                .collect(Collectors.groupingBy(
                                Employee::getDepartment,
                                Collectors.counting()))
                .entrySet().stream()
                .max(Comparator.comparingLong(Map.Entry::getValue))
                .map(Map.Entry::getKey).orElse(null);

Average age of each gender

employees.stream()  
        .collect(Collectors.groupingBy(  
                Employee::getGender,  
                Collectors.averagingInt(Employee::getAge)  
        ));

Find first non-repeating character using streams

String str = "karnataka";
List<String> characters = Arrays.asList(str.split(""));
Map<String, Long> freq = characters.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
String result = characters.stream()
        .filter(c -> freq.get(c).equals(1L)) // compare with Long else will fail
        .findFirst().orElse(null);
 
System.out.println(freq);
System.out.println(result);
  • Optimized using LinkedHashMap
    • It remembers the insertion order
String result = characters.stream()
        .collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()))
        .entrySet().stream()
        .filter(entry -> entry.getValue() == 1L)
        .map(entry -> entry.getKey())
        .findFirst().orElse(null);
 
System.out.println(result);