Java8: Lambda Expression with Shape Class





5.00/5 (2 votes)
Implement the Lambda expression to handle the class Shape in Java8: the power of declaration programming.
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()
"
- We can use, in the code, "
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;
/**
* Interface that represent a shape in the graphic viewport (0,0)->(w,h)
*/
public interface Shape {
void move(V2d dv) //move the shape of a certain vector, passed as parameter
double getPerim() //compute the perimeter
bool isInside(BBox bbox) //checking if the figure falls within the bounding box specified
bool contains(P2d p0) //checks if the point belongs or not to the figure
}
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;
//The method design a simply line that it composed by
//two coordinated: (x0,y0) - extreme left - and (x1,y1) - extreme right
public Line(int x0, int y0, int x1, int y1) {
a = new P2d(x0, y0);
b = new P2d(x1, y1);
}
//To move the figure simply use the vector passed as a parameter
@Override
public void move(V2d v) {
a = a.sum(v);
b = b.sum(v);
}
//To return the perimeter is sufficient to calculate
//the distance between the point "a" and "b"
@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())) && // left extreme X
(Math.abs(a.getY()) >= Math.abs(p1.getY())) && // left extreme Y
(Math.abs(b.getX()) <= Math.abs(p2.getX())) && // right extreme X
(Math.abs(b.getY()) <= Math.abs(p2.getY()))) // right extreme Y
return true;
else
return false;
}
@Override
public boolean contains(P2d p) {
// if (distance(A, C) + distance(B, C) == distance(A, B))
// return true; // C is on the line.
// return false; // C is not on the line.
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
//given a list of shape and a vector v, moves each shape public static void moveShapes(List<shape> listShape, V2d v) { listShape.forEach(s -> s.move(v)); }
inBBox
//given a list of shapes and a bounding box, //computes the list of shapes contained in the bounding box p0 p1 public static List<shape> inBBox(List<shape> listShape, P2d p0, P2d p1) { return listShape.stream() .filter(s -> s.isInside(p0, p1)) .collect(toList()); }
maxPerim
//given a list of shapes, identifies the max perimeter public static OptionalDouble maxPerim(List<shape> listShape) { return listShape.stream() .mapToDouble(s -> s.getPerim()) .max(); }
shapeWithMaxPerim
//given a list of shapes, determines the shape with the larger perimeter public static Shape shapeWithMaxPerim(List<shape> listShape) { return listShape.stream() .max((p1, p2) -> Double.compare(p1.getPerim(), p2.getPerim())) .get(); }
contains
//given a list of shapes and a point, verify if there is a shape that contains the point public static Boolean contains(List<shape> listShape, P2d p) { return listShape.stream() .filter(s -> s.contains(p)) .findFirst() .isPresent(); }
getContaining
//given a list of shapes and a point p, computes the list of shapes that contain the point public static List<shape> getContaining(List<shape> listShape, P2d p) { return listShape.stream() .filter(s -> s.contains(p)) .collect(toList()); }
logAll
//given a list of shapes, print the log 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()
- with the instruction
- Application of one or more intermediate data processing operations
- there returns a
stream
, so as to create pipelines - examples:
filter
,map
,limit
, ...
- there returns a
- Closing operation
- used to collect/sink elements from the
stream
, like a reducing. - examples:
collect
: Converts the stream in another formforeach
: Applies a lambda to every element of the stream???
- used to collect/sink elements from the
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.