Lincoln's Notes
5 Scala Idioms for Java Developers
These are some Scala idioms that a Java Programmer can add to their toolbox
1. Scala's Option<> class: Programming without null
Consider the following Problem Statement: Calculate the Future Value(FV) using interest rate(r), present value(PV), and time in years(n)
The formula for this is
FV = PV ( 1 + r ) ** n
In Java, we could create a method/function like this:
class InterestRates {
Double calculateFutureValue(Integer presentValue,
Double interestRate,
Integer n) {
return presentValue * Math.pow(1 + interestRate, n);
}
}
A programmer has 2 considerations for null
parameter values:
-
null
parameter value can be used to signify "special"/"Out of Band" value. For e.g., we could rewrite thecalculateFutureValue
function, so that wheninterestRate
isnull
, the current market interest rate is used OR -
null
can be treated as an unacceptable parameter value. It indicates that the parameter is essential to compute the value of the function. If the parameter isnull
, then the function will return null.
Conversely, when function is invoked, null
parameter values can be used either :
- to pass a "special" value. or
- to indicate that the parameter value is not known
Whatever the intention of a null
parameter value may be, the programmer needs to detect and handle null
values in a special way.
Else, we will end up with a NPE.
This results in multiple null
checks(if
statements) spread over our function definition.
Consider the function below:
class InterestRates {
Double calculateTaxPayable(Integer presentValue, Double interestRate, Integer n, Double taxRate) {
if(presentValue != null && interestRate != null && n != null && taxRate != null) {
Double futureValue = calculateFutureValue(presentValue, interestRate, n);
if(futureValue != null)
Double taxPayable = futureValue * taxRate;
return taxPayable;
}
return null;
}
}
First, the function verifies that none of its parameters are nulls.
Then it invokes calculateFutureValue
Then again, it has to check if the return value is null.
What if we could avoid all this null
checking?
In Scala, we use the the Option
monad to handle null/unknown values. A similar class called Optional
is available in Java.
With Optional
, we can avoid null
checks and also combine(or chain) the parameters without having to worry about null
values.
Let us see how we can rewrite the same method using Optional
:
class InterestRates {
Optional<Double> calculateFutureValue(Optional<Integer> presentValue,
Optional<Double> interestRate,
Optional<Integer> n) {
return presentValue.flatMap(pv ->
interestRate.flatMap(r ->
n.flatMap( nv -> Optional.of(pv * Math.pow(1 + r, nv)))));
}
}
The caller of the function can invoke this function with
Optional<Double> fv = calculateFutureValue(Optional.of(1), Optional.of(2.0), Optional.of(3));
If some parameter values are not available(or unknown or null), then we need to use a special type of Optional
Optional<Double> fv = futureValue(Optional.empty(), Optional.of(2.0), Optional.of(3))
This eliminates all the if(x == null)
checks in our code and make it more readable.
However, a caller could invoke the function with null
which will still result in a NullPointerException
.
futureValue(null, null, null)
So what is the problem that is being solved with Optional
?
Without Optional
, if we do not know the value of a parameter, we use null
.
Then we are forced to use null
checks in the function body.
With Optional
, if we do not know the value of a parameter, we us Optional.empty
. And we get to skip all null
checks
in function body.
If one of the parameters is Optionsl.empty
, the chain of flatMaps
will also return Optional.empty
without
we having to put in any null checks or if
conditions.
The return value of the function invocation is an Optional<Double>
.
This can either be passed to another function or the actual Double
value can be extracted
using get
or orElse
class InterestRates {
Optional<Double> calculateTaxPayable(Optional<Integer> presentValue,
Optional<Double> interestRate,
Optional<Integer> n,
Optional<Double> taxRate) {
// no need to `null` check the parameters
Optional<Double> futureValue = presentValue.flatMap(pv ->
interestRate.flatmap(i ->
n.flatMap(nv-> taxRate.flatMap(t -> ))));
// ... no need to `null` check the return values
Optional<Double> tax = futureValue.flatMap(fv -> taxRate.flatMap(t -> Optional.of(fv * t)));
return tax;
}
// ...or...
// non functional style...
Double calculateTaxPayable_NON_FUNCTIONAL_STYLE(Optional<Integer> presentValue,
Optional<Double> interestRate,
Optional<Integer> n,
Double taxRate) {
Optional<Double> futureValue = presentValue.flatMap(pv ->
interestRate.flatmap(i ->
n.flatMap(nv-> taxRate.flatMap(t -> ))));
Optional<Double> tax = futureValue.getOrElse(0) * taxRate ;
return tax;
}
}
We have eliminated null
checking, facilitated function chaining and also and made our code more readable.
There are some drawbacks to consider:
1. Every Optional<>
is a additional object that adds to the memory cost
2. We can only return success Optional<Value>
or failure Optional.empty
.
It would have been great if we could return additional info for the failure(see scala.util.Either/Left/Right)
2. Scala's case classes:Quick Data(DTO) Classes
In Java, when we define POJOs, we are forced to define a large amount of boilerplate code. This includes:
- getters and setters
- hashCode() and equals()
- toString()
- factories and/or builders
Although this code is autogenerated by most IDEs, it would still be nice to avoid this manual step.
Also, when we add or remove a field from an existing auto-generated class, we have to make sure not to break the autogenerated methods.
Scala has "case" classes. These classes help eliminate a lot of boilerplate code. When we define a "case" class, we get the all the above methods for free. The Scala syntax is quite straightforward:
case class MyScalaPojo(var name:String, var age:Int, var address:Array[String]);
val myPojo = MyScalaPojo("John", 33, Array("House No 10", "Thailand"))
myPojo.name = "James";
Java 14 has record classes which provide similar functionality. In Java versions lower than 14, we can achieve similar economy of code using the Lombok Library.
To add Lombok to our project, we can add the following dependency to the pom file.
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version> <!-- check for later versions -->
<scope>provided</scope>
</dependency>
</dependencies>
Also, do not forget to add the Lombok Plugin for your IDE.
Let us see how boilerplate code is reduced with Lombok.
Lombok introduces multiple annotations that automate a lot of the boilerplate code.
- @Getter and @Setter will auto generate getters and setters for all fields
- @Getter and @Setter can also be applied at field level to restrict getters and setters to specific fields
- @EqualsAndHashCode and @ToString will generate the synonymous methods.
- @ToString.Exclude will exclude a field from toString. Same with @EqualsAndHashCode.Exclude
- @Builder will generate Factory methods
- @Singleton will allow creation of a Singleton
import lombok.*;
@Getter @Setter
@EqualsAndHashCode
@ToString(includeFieldNames = true)
@Builder(toBuilder = true)
class Data {
@Getter @Setter // this is redundant, since we have used @Getter and @Setter at class level
private String name;
@Getter @Setter @ToString.Exclude @EqualsAndHashCode.Exclude
private String[] address;
}
We can then invoke the getters
, setters
and other methods as usual:
public class Main {
public static void main(String[] args) {
Data data = Data.builder().
name("John"). // auto generated
address(new String[]{"New Delhi", "India"}).//auto generated
build(); // auto generated Builder
data.setName("Jane"); // auto generated Setter
data.setAddress(new String[]{"Mumbai", "India"}); //auto generated Setter
System.out.println(data.toString()); //auto generated toString
}
}
Lombok also provides shortcuts that combine multiple annotations:
- @lombok.Data will generate Getters, Setters, toString, and hashCode
- @lombok.Value will generate all above methods except Setters
Using the lombok.Data
annotation:
@lombok.Data
class Data {
private String name;
private String[] address;
}
Using the lombok.Value
annotation
@lombok.Value // @ToString, @EqualsAndHashCode, @Getter
class V {
private String name;
private String[] address;
}
3. Scala's var and val: Less Boilerplate code with Type Inference
In Scala, we do not have to define the type
of a variable. Scala is able to infer the type.
var name = "John Doe"
var age = 33
We can rely on the smartness of the compiler to make our code more readable. Developers can still specify the type if they want though.
var name:String = "John Doe"
var age:Int = 33
name = "John James Doe"
age = 44
Thanks to Lombok, we can now pretend that the Java Compiler is as smart as Scala.
import lombok.*;
var javaName = "John";
var javaAge = 33;
var javaObject = new StringBuilder("")
Lombok also adds the "val" "keyword"
to Java. This makes variables immutable.
val javaName = "John";
javaName = "James";// Not allowed
Lombok uses Annotation Processing to make them available in Java.
Note that var
has been introduced as a keywork in Java 10.
4. Scala's predefined functional Interfaces: Refactor and Reuse Lambdas
While programming with streams, we frequently use .map
and .filter
to modify collection streams.
Consider this code snippet:
firstIntegerList.map(i -> i + 1).filter(i -> i%2 == 0)
secondIntegerList.map(i -> i + 1).filter(i -> i%2 == 0)
We are duplicating the lambda function code in filter
and map
invocations.
We could factor out the code using one of the predefined functional interfaces like so
Function<Integer, Integer> incrementer = i -> i + 1;
Predicate<Integer> evenChecker = i -> i % 2 == 0;
Then we can use the same function objects for both the streams
firstIntegerList.map(incrementer).filter(evenChecker);
secondIntegerList.map(incrementer).filter(evenChecker);
In Java, we have predefined Functional Interfaces defined in java.util.function
Interface | Description |
---|---|
Supplier | A function that takes in nothing and produces data |
Consumer | A function that takes in data and producing nothing |
Function | A function that takes in data and produces data |
Predicate | A function that takes in data and returns a boolean |
5. Scala's collection classes: Quickly creating collections
In Scala, we can quickly create a List with val newList = List(1, 2, 3)
The Java equivalent would be
List<Integer> newList = ArrayList();
newList.add(1);
newList.add(2);
newList.add(3);
Google's Guava library allows us to use Scala-like syntax to create collections. To add Guava to your project, add the following to your pom.xml.
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version> <!-- Check for later versions -->
</dependency>
Then, use the Factory methods in Guava classes to create collections:
List<Integer> newList = ImmutableList.of(1, 2, 3)
Map<Integer, String> newMap = ImmutalbeMap.of(1,"hello", 2, "bye")
Java programmers can use these 5 tips to add some Scala flavour to their code.