Java Streams: A Comprehensive Guide


Java Streams, introduced in Java 8, have revolutionized the way developers work with data collections. They provide a concise and expressive way to perform operations on sequences of data, making code more readable and maintainable. In this detailed tutorial, we’ll explore Java Streams from the ground up, covering everything from the basics to advanced techniques.

Table of Contents

  1. Introduction to Java Streams
  2. Creating Streams
    • 2.1. From Collections
    • 2.2. From Arrays
    • 2.3. Stream.of
    • 2.4. Stream.builder
  3. Intermediate Operations
    • 3.1. Filter
    • 3.2. Map
    • 3.3. FlatMap
    • 3.4. Sorted
    • 3.5. Peek
  4. Terminal Operations
    • 4.1. forEach
    • 4.2. toArray
    • 4.3. collect
    • 4.4. reduce
    • 4.5. min and max
    • 4.6. count
  5. Parallel Streams
  6. Stream API Best Practices
  7. Advanced Stream Techniques
    • 7.1. Custom Collectors
    • 7.2. Stream of Streams
    • 7.3. Grouping and Partitioning
  8. Real-World Examples
    • 8.1. Filtering Data
    • 8.2. Mapping Data
    • 8.3. Aggregating Data
  9. Performance Considerations
  10. Conclusion

1. Introduction to Java Streams

Java Streams are a powerful addition to the Java programming language, designed to simplify the manipulation of collections and arrays. They allow you to perform operations like filtering, mapping, and reducing in a more functional and declarative way.

Key characteristics of Java Streams:

  • Sequence of Data: Streams are a sequence of elements, whether from collections, arrays, or other sources.
  • Functional Style: Operations on streams are expressed as functions, promoting a functional programming paradigm.
  • Lazy Evaluation: Streams are evaluated on demand, making them efficient for large datasets.
  • Parallel Processing: Streams can easily be processed in parallel to leverage multi-core processors.

2. Creating Streams

2.1. From Collections

You can create a stream from a collection using the stream() method:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<String> nameStream = names.stream();

2.2. From Arrays

Arrays can be converted into streams using Arrays.stream():

String[] colors = { "Red", "Green", "Blue" };
Stream<String> colorStream = Arrays.stream(colors);

2.3. Stream.of

To create a stream from individual elements, use Stream.of():

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

2.4. Stream.builder

For dynamic stream creation, employ a Stream.Builder:

Stream.Builder<String> builder = Stream.builder();
builder.accept("One");
builder.accept("Two");
Stream<String> customStream = builder.build();

3. Intermediate Operations

Intermediate operations are used to transform or filter data within a stream.

3.1. Filter

The filter operation allows you to select elements that meet a specific condition:

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> evenNumbers = numbers.filter(n -> n % 2 == 0);

3.2. Map

map transforms elements by applying a function to each element:

Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
Stream<Integer> nameLengths = names.map(String::length);

3.3. FlatMap

flatMap is used to flatten nested streams into a single stream:

Stream<List<Integer>> nestedStream = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4));
Stream<Integer> flattenedStream = nestedStream.flatMap(Collection::stream);

3.4. Sorted

You can sort elements using the sorted operation:

Stream<String> names = Stream.of("Charlie", "Alice", "Bob");
Stream<String> sortedNames = names.sorted();

3.5. Peek

peek allows you to perform an action on each element without modifying the stream:

Stream<Integer> numbers = Stream.of(1, 2, 3);
Stream<Integer> peekedNumbers = numbers.peek(System.out::println);

4. Terminal Operations

Terminal operations produce a result or a side-effect and trigger the execution of the stream.

4.1. forEach

The forEach operation performs an action on each element:

Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
names.forEach(System.out::println);

4.2. toArray

toArray converts a stream into an array:

Stream<Integer> numbers = Stream.of(1, 2, 3);
Integer[] numArray = numbers.toArray(Integer[]::new);

4.3. collect

The collect operation accumulates elements into a collection:

Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
List<String> nameList = names.collect(Collectors.toList());

4.4. reduce

reduce combines the elements of a stream into a single result:

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.reduce(Integer::sum);

4.5. min and max

You can find the minimum and maximum elements using min and max:

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> min = numbers.min(Integer::compareTo);
Optional<Integer> max = numbers.max(Integer::compareTo);

4.6. count

count returns the number of elements in the stream:

Stream<String> names = Stream.of("Alice", "Bob", "Charlie");
long count = names.count();

5. Parallel Streams

Java Streams can be easily parallelized to take advantage of multi-core processors. You can convert a sequential stream to a parallel stream using the parallel method:

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

Be cautious when using parallel streams, as improper usage can lead to performance issues and race conditions.

6. Stream API Best Practices

To write clean and efficient code with Java Streams, follow these best practices:

  • Keep Streams Stateless: Avoid modifying variables from outside the lambda expressions used in stream operations.
  • Choose Appropriate Data Structures: Use the right data structure for your needs to optimize stream performance.
  • Lazy Evaluation: Use intermediate operations to filter and transform data before calling terminal operations to minimize unnecessary work.
  • Avoid Side Effects: Keep terminal operations clean and avoid side effects for better code maintainability.

7. Advanced Stream Techniques

7.1. Custom Collectors

You can create custom collectors to perform advanced data aggregations:

List<Person> people = ...;
Map<Gender, List<Person>> peopleByGender = people.stream()
    .collect(Collectors.groupingBy(Person::getGender));

7.2. Stream of Streams

Streams can be nested, allowing for more complex data processing:

Stream<List<Integer>> listOfLists = ...;
Stream<Integer> flattenedStream = listOfLists.flatMap(List::stream);

7.3. Grouping and Partitioning

The groupingBy and partitioningBy collectors enable advanced data grouping:

Map<Gender, List<Person>> peopleByGender = people.stream()
    .collect(Collectors.groupingBy(Person::getGender));

8. Real-World Examples

Let’s explore some real-world scenarios where Java Streams shine:

8.1. Filtering Data

Filtering a list of products by price and category:

List<Product> filteredProducts = products.stream()
    .filter(p -> p.getPrice() < 50 && p.getCategory().equals("Electronics"))
    .collect(Collectors.toList());

8.2. Mapping Data

Calculating the average salary of employees in a department:

double averageSalary = employees.stream()
    .filter(e -> e.getDepartment().equals("HR"))
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0.0);

8.3. Aggregating Data

Finding the most popular tags among a list of articles:

Map<String, Long> tagCounts = articles.stream()
    .flatMap(article -> article.getTags().stream())
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

9. Performance Considerations

While Streams offer convenience, improper use can impact performance. Be mindful of:

  • Stream Size: Large data sets may lead to excessive memory usage.
  • Parallel Streams: Use with caution; not all tasks benefit from parallelism.
  • Statelessness: Ensure lambda expressions used in stream operations are stateless.
  • Avoiding Excessive Intermediate Operations: Minimize unnecessary filtering and mapping.

10. Conclusion

Java Streams are a versatile and powerful tool for working with data in a functional and declarative manner. By mastering the concepts, operations, and best practices outlined in this tutorial, you’ll be well-equipped to write clean, efficient, and expressive code that makes the most of Java’s stream processing capabilities.

Happy coding!

Avoid nested loops using Collection Framework in Java


High performance is essential for any software implemented in any programming language. And, loops plays major role in this regard. This post explains how to avoid the loops using Java’s Collection framework.

Below are the two Java programs to understand how the performance could be increased using the Collection framework.

Using nested loops

package in.javatutorials;

/**
* Finds out the Duplicates is String Array using Nested Loops.
*/
public class UsingNesteadLoops {
  private static String[] strArray = { "Cat", "Dog", "Tiger",     "Lion", "Lion" };

  public static void main(String[] args) {
   isThereDuplicateUsingLoops(strArray);
  }

  /**
   * Iterates the String array and finds out the duplicates 
   */
   public static void isThereDuplicateUsingLoops(String[]     strArray) {

   boolean duplicateFound = false;
   int loopCounter = 0;

   for (int i = 0; i < strArray.length; i++) {
   String str = strArray[i];
   int countDuplicate = 0;

   for (int j = 0; j < strArray.length; j++) {
      String str2 = strArray[j];
      if (str.equalsIgnoreCase(str2)) {
         countDuplicate++;
      }
      if (countDuplicate > 1) {
         duplicateFound = true;
         System.out.println("Duplicates Found for " + str);
      }
      loopCounter++;
   }// end of inner nested for loop

   if (duplicateFound) {
    break;
   }
}// end of outer for loop

System.out.println("Looped " + loopCounter + " times to find the result");
}

}

If we run the above program, it will be looped 20 times to find out the duplicates in the string array which has the length of 5. Number of loops increases exponentially depending on size of array, hence the performance takes a hit. These are not acceptable to use in applications which require high performance.

Without using nested loops

package in.javatutorials;

import java.util.HashSet;
import java.util.Set;

/**
* Finds out the Duplicates is String Array using Collection.
*/
public class AvoidNesteadLoopsUsingCollections {

private static String[] strArray = { "Cat", "Dog", "Tiger", "Lion", "Lion" };

public static void main(String[] args) {
 isThereDuplicateUsingSet(strArray);
}

/**
* Iterates the String array and finds out the duplicates
*/
public static void isThereDuplicateUsingSet(String[] strArray) {
  boolean duplicateFound = false;
  int loopCounter = 0;
  Set setValues = new HashSet();

  for (int i = 0; i < strArray.length; i++) {
    String str = strArray[i];

    if(setValues.contains(str)){
        duplicateFound = true;
        System.out.println("Duplicates Found for " + str);
    }
    setValues.add(str);
    loopCounter++;

    if (duplicateFound) {
       break;
    }
   }// end of for loop

   System.out.println("Looped " + loopCounter + " times to find the result");
 }

}
  • Above approach takes only 5 loops to identify the duplicates in the same array.
  • It is more readable , easier to maintain and performs better.
  • If you have an array with 1000 items, then nested loops will loop through 999000 times and utilizing a collection will loop through only 1000 times.

Other Useful links:

Convert Array to Vector in Java


package com.test;

import java.util.Arrays;
import java.util.Vector;

public class ArrayTest {

public static void main(String[] args) {
ArrayTest arrayTest = new ArrayTest();
arrayTest.arrayToVector();
}
public void arrayToVector() {

String[] arrayToConvert = { “jhony”, “peter”, “don” };

Vector<String> convertedVector = new Vector<String> Arrays.asList(arrayToConvert));

System.out.println(“11111 ” + convertedVector.get(0));

System.out.println(“3” + convertedVector.size());
}
}

How to convert an array to comma separated string in java


package com.test;

public class ArrayTest {

public static void main(String[] args) {
ArrayTest arrayTest = new ArrayTest();
arrayTest.arrayJoin();
}

public void arrayJoin() {

String[] arrayToJoin = { “John”, “Peter”, “Tom”, “Scott” };

StringBuilder joinedString = new StringBuilder();
String seperator = “,”;
//System.out.println(“Converting to String “+arrayToJoin.toString());;
for (String value : arrayToJoin) {
joinedString.append(value).append(seperator);
}

System.out.println(“Final Result …….. ” + joinedString);
}
}

How to Copy an Array into Another Array?


Contents of one array can be copied into another array by using the arraycopy() method of the System class in Java.

  • arraycopy() method accepts source array, source length, destination array and destination length.
  • Following program shows the use of arraycopy() method to copy the contents of one array to another.
package com.Test;

public class ArrayCopyTest{
 public static void main(String[] args){
    int[] src = new int[] {1, 2, 3, 4, 5};
    int[] dest = new int[src.length];
    System.arraycopy(src, 0, dest, 0, src.length);
    for (int i = 0; i < dest.length; i++){
       System.out.println(dest[i]);
    }
  }
}

How to check duplicated value in array


package com.test;

import java.util.Set;
import java.util.HashSet;

public class CheckDuplicate
{
	public static void main(String args[])
	{
		String [] sValue = new String[]{"a","b","c","d","","","e","a"};

		if(checkDuplicated_withNormal(sValue))
			System.out.println("Check Normal : Value duplicated! \n");
		if(checkDuplicated_withSet(sValue))
			System.out.println("Check Set : Value duplicated! \n");

	}

	//check duplicated value
	private static boolean checkDuplicated_withNormal(String[] sValueTemp)
	{
		for (int i = 0; i < sValueTemp.length; i++) {
			String sValueToCheck = sValueTemp[i];
			if(sValueToCheck==null || sValueToCheck.equals(""))continue; //empty ignore
			for (int j = 0; j < sValueTemp.length; j++) {
					if(i==j)continue; //same line ignore
					String sValueToCompare = sValueTemp[j];
					if (sValueToCheck.equals(sValueToCompare)){
							return true;
					}
			}

		}
		return false;

	}

	//check duplicated value
	private static boolean checkDuplicated_withSet(String[] sValueTemp)
	{
		Set<String> sValueSet = new HashSet<String>();
		for(String tempValueSet : sValueTemp)
		{
			if (sValueSet.contains(tempValueSet))
				return true;
			else
				if(!tempValueSet.equals(""))
					sValueSet.add(tempValueSet);
		}
		return fals