Introduction
This tip is an introduction of declarative programming style in Java 8.
We will view the STREAM in Java 8, introducing some concepts of declarative/functional style to process the collections in other sources.
An example at a glance: shapes.
Given a collection of shapes represented by the "Shape
Class", we want to collect all those that have an area less than 400, sorted.
Pre-Stream Solution
List < Shape > lowAreaShapes = new ArrayList < > ();
for (Shape s: shapeSet) {
if (s.getArea() < 400) lowAreaShapes.add(d);
}
Collections.sort(lowAreaShapes, new Comparator < Shape > () {
public int compare(Shape s1, Shape s2) {
return Double.compare(s1.getArea(), s2.getArea());
}
});
List < String > lowAreaShapesName = new ArrayList < > ();
for (Shape s: lowAreaShapes) {
lowAreaShapesName.add(d.getName());
}
Stream-Based Solution
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List < String > lowAreaShapesName=
menu.stream()
.filter(s -> s.getArea() < 400)
.sorted(comparing((Shape s) -> s.getArea())
.map((Shape s) -> s.getName())
.collect(toList());
Points of Interest
What are the Benefits?
The evident impact is the elegance of our source code:
- More declarative
- We want to specify what we want, not how to get it
- Complex computations can be obtained by combining several operations together in cascade in pipeline style
- More composable
- We have algebraic composition, no side effect (how we note from the image upon)
- Parallelizable
- We can use, in the code, "
.parallelStream()
", instead ".stream()
"
Example
For understanding the full power of streams, in general, we need a simple example.
We have the classes P2d
and V2d
which represent a point and a vector in a plane in the graphics viewport (that having extreme (0.0) as a corner the upper left and (w,h), w>0, h>0 as in the lower right corner), and the class BBox
representing a rectangular bounding box. We want to define, in the same package, the class "Shape
" that is characterized by the following methods:
package code.project.streams;
public interface Shape {
void move(V2d dv)
double getPerim()
bool isInside(BBox bbox)
bool contains(P2d p0)
}
Afterwards, define the classes that implement the interface upon:
Line
Rect
(that represent a Rectangle) Circle
Class Line
A line must have two points: "a
" (point extreme left) and "b
" (point extreme right) - for example. Not only: all methods of interfaces "shape
" must be implemented.
package code.project.streams;
public class Line implements Shape {
private P2d a, b;
public Line(int x0, int y0, int x1, int y1) {
a = new P2d(x0, y0);
b = new P2d(x1, y1);
}
@Override
public void move(V2d v) {
a = a.sum(v);
b = b.sum(v);
}
@Override
public double getPerim() {
return Math.abs(P2d.distance(a, b));
}
@Override
public boolean isInside(P2d p1, P2d p2) {
if ((Math.abs(a.getX()) <= Math.abs(p1.getX())) &&
(Math.abs(a.getY()) >= Math.abs(p1.getY())) &&
(Math.abs(b.getX()) <= Math.abs(p2.getX())) &&
(Math.abs(b.getY()) <= Math.abs(p2.getY())))
return true;
else
return false;
}
@Override
public boolean contains(P2d p) {
if (P2d.distance(a, p) + P2d.distance(b, p) == P2d.distance(a, b))
return true;
else
return false;
}
@Override
public String toString() {
return "Line - Point a(" + a.getX() +
"-" + a.getY() + ") Point b("
+ b.getX() + "-" + b.getY() + ")";
}
}
For the other class, you can download the complete source code.
Now, we want to define the "Utility
" class with the following methods, using appropriate expressions and Lambda Stream in their implementation:
moveShapes
public static void moveShapes(List<shape> listShape, V2d v) {
listShape.forEach(s -> s.move(v));
}
inBBox
public static List<shape> inBBox(List<shape> listShape, P2d p0, P2d p1) {
return listShape.stream()
.filter(s -> s.isInside(p0, p1))
.collect(toList());
}
maxPerim
public static OptionalDouble maxPerim(List<shape> listShape) {
return listShape.stream()
.mapToDouble(s -> s.getPerim())
.max();
}
shapeWithMaxPerim
public static Shape shapeWithMaxPerim(List<shape> listShape) {
return listShape.stream()
.max((p1, p2) -> Double.compare(p1.getPerim(),
p2.getPerim()))
.get();
}
contains
public static Boolean contains(List<shape> listShape, P2d p) {
return listShape.stream()
.filter(s -> s.contains(p))
.findFirst()
.isPresent();
}
getContaining
public static List<shape> getContaining(List<shape> listShape, P2d p) {
return listShape.stream()
.filter(s -> s.contains(p))
.collect(toList());
}
logAll
public static void logAll(List<shape> listShape) {
listShape.forEach(System.out::println);
}
There are many examples to understand the power that the lambda expression has in general: without the streams, we would have had to write many lines of code; in this manner we can focus on "what" and not on "who".
For example, the method inBBox
works in this manner:
Stages
- Creation of
stream
from a data source
- with the instruction
listShape.streams()
- Application of one or more intermediate data processing operations
- there returns a
stream
, so as to create pipelines - examples:
filter
, map
, limit
, ...
- Closing operation
- used to collect/sink elements from the
stream
, like a reducing. - examples:
collect
: Converts the stream in another form foreach
: Applies a lambda to every element of the stream???
Test the Streams
For testing the streams, we can call the static
method in this manner:
package code.project.streams;
import java.util.Arrays;
import java.util.List;
public class TestShapes {
public static void main(String args[]) {
final List< shape > listShape = Arrays.asList(new Line(0, 0, 0, 7),
new Line (0,0,0,8),
new Rect(1, 1, 3, 5)
);
Utils.logAll(listShape);
Utils.moveShapes(listShape, new V2d(1, 1));
Utils.logAll(listShape);
}
}
Conclusion
As we can see, we can manage all shapes with the streams/lambda expression.
A few lines of code are enough for managing the shapes and making the code better, through a declarative style and therefore more readable. For the complete code, you can download here.