Java Streams, introduced in Java 8, revolutionized how we handle collections by providing an elegant way to process data in a functional style. In this tutorial, we will explore how to use Streams to work with custom generic classes. You’ll learn how to build and manipulate these classes, applying Stream operations like map()
, filter()
, and reduce()
in practical scenarios.
What Are Custom Generic Classes?
A generic class in Java is a class that is written with type parameters, allowing it to handle different data types in a flexible and reusable manner. Custom generic classes are user-defined classes where you specify the type of objects they will store or process. This can be useful when creating data structures or utility classes that need to work with multiple data types.
Example: Creating a Custom Generic Class
Let’s start by creating a simple generic class. In this case, we’ll create a class called Box
that can hold any type of object.
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Here, <T>
is a type parameter, and it allows us to specify what type of object the Box
will contain. For example, you can create a Box<String>
to store a string, or a Box<Integer>
to store an integer.
Working with Streams
Once we have our custom generic class, we can use Java Streams to process objects of these classes in a functional and declarative way. Let’s explore how we can integrate Streams with our Box
class.
Stream Operations on Custom Generic Classes
To use a Stream with a custom generic class like Box
, we need to wrap it in a collection or some other container that can be processed by a Stream. For example, let’s say we have a list of Box
objects containing integers. We can create a Stream from this list and perform operations on the elements inside each Box
.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Box<Integer>> boxes = Arrays.asList(
new Box<>(1),
new Box<>(2),
new Box<>(3),
new Box<>(4)
);
List<Integer> squaredValues = boxes.stream()
.map(box -> box.getValue() * box.getValue())
.collect(Collectors.toList());
System.out.println(squaredValues); // Output: [1, 4, 9, 16]
}
}
In this example:
- We create a list of
Box<Integer>
objects. - We use the
stream()
method to convert the list into a Stream. - We apply the
map()
operation to square the value inside eachBox
. - Finally, we collect the results into a new list using
collect(Collectors.toList())
.
Filtering and Collecting Data from Custom Generic Classes
Streams also allow us to filter data based on certain conditions. For instance, if we want to only keep boxes containing values greater than 2, we can use the filter()
operation.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Box<Integer>> boxes = Arrays.asList(
new Box<>(1),
new Box<>(2),
new Box<>(3),
new Box<>(4)
);
List<Box<Integer>> filteredBoxes = boxes.stream()
.filter(box -> box.getValue() > 2)
.collect(Collectors.toList());
filteredBoxes.forEach(box -> System.out.println(box.getValue())); // Output: 3, 4
}
}
Here, we use filter()
to retain only those Box<Integer>
objects where the value is greater than 2.
Combining Custom Generic Classes with Complex Data Structures
In real-world applications, you’ll often be working with more complex structures, such as lists of custom generic objects or maps. Let’s explore an example where we have a Map
containing custom generic values and how to use Streams to process it.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Map<String, Box<Integer>> boxMap = new HashMap<>();
boxMap.put("a", new Box<>(1));
boxMap.put("b", new Box<>(2));
boxMap.put("c", new Box<>(3));
Map<String, Integer> squaredValuesMap = boxMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().getValue() * entry.getValue().getValue()
));
System.out.println(squaredValuesMap); // Output: {a=1, b=4, c=9}
}
}
In this case, we are working with a Map<String, Box<Integer>>
. Using the entrySet()
method, we create a Stream of entries, and then we use Collectors.toMap()
to create a new map where each value is squared.
Advanced Stream Operations on Custom Generic Classes
Beyond basic operations like map()
and filter()
, Streams in Java support a wide range of powerful operations, such as reduce()
, flatMap()
, and peek()
. These can be combined to create more complex data manipulations.
For instance, let’s consider an example where we want to reduce the values in a list of Box<Integer>
objects to a single sum.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Box<Integer>> boxes = Arrays.asList(
new Box<>(1),
new Box<>(2),
new Box<>(3),
new Box<>(4)
);
int sum = boxes.stream()
.mapToInt(box -> box.getValue())
.sum();
System.out.println(sum); // Output: 10
}
}
In this example, we use mapToInt()
to extract the integer values from the Box<Integer>
objects, and then we use sum()
to get the total sum of the values.
Conclusion
In this tutorial, we’ve demonstrated how to use Java Streams to process data stored in custom generic classes. By combining the flexibility of generics with the power of Streams, you can write clean, efficient, and functional code. Whether you’re filtering data, transforming values, or performing aggregations, Streams offer a powerful toolset for handling complex data structures.