What I miss in Java after working with Kotlin / Scala
Recently, I often hear that Java has become an obsolete language in which it is difficult to build large supported applications. In general, I do not agree with this point of view. In my opinion, the language is still suitable for writing fast and well-organized applications. However, I admit, it also happens that when you write code everyday, you sometimes think: “how well it would be solved with this thing from another language”. In this article, I wanted to share my pain and experience. We will look at some Java problems and how they could be resolved in Kotlin / Scala. If you have a similar feeling or are just wondering what other languages can offer, I ask you under cat.
Sometimes it happens that it is necessary to expand an existing class without changing its internal content. That is, after creating the class, we supplement it with other classes. Consider a small example. Suppose we have a class that is a point in two-dimensional space. In different places in our code, we need to serialize it in both Json and XML.
It looks quite voluminous, right? Is it possible to solve this problem more elegantly with the help of language tools? Scala and Kotlin nod positively. This is achieved using the method extension mechanism. Let's see how it looks.
It looks much better. Sometimes this is really not enough with plentiful mapping and other transformations.
Now everyone is talking about asynchronous computing and the prohibitions on locking in execution threads. Let's imagine the following problem: we have several sources of numbers, where the first just returns the number, the second - returns the answer after calculating the first. As a result, we must return a string with two numbers.
Let's try to solve the problem in Java first
In this example, our number is wrapped in Optional to control the result. In addition, all actions are performed inside CompletableFuture for convenient work with threads. The main action takes place in the thenApplyAsync method. In this method, we get Optional as an argument. Next, flatMap is called to control the context. If the received Optional returned as Optional.empty, then we will not go to the second service.
The total that we received? Using CompletableFuture and Optional features with flatMap and map, we were able to solve the problem. Although, in my opinion, the solution does not look the most elegant way: before you understand what’s the matter, you need to read the code. And what would happen with two or more data sources?
Could language somehow help us to solve the problem. And again, turn to Scala. Here's how you can solve it with Scala tools.
It looks familiar. And this is no coincidence. It uses the scala.concurrent library, which is primarily a wrapper over java.concurrent. Well, what else can Scala help us with? The fact is that chains of the form flatMap, ..., map can be represented as a sequence in for.
It’s gotten better, but let's try changing our code again. Connect the cats library.
Now it’s not so important what OptionT means. I just want to show how simple and short this operation can be.
But what about Kotlin? Let's try to do something similar on coroutines.
This code has its own peculiarities. First, it uses the Kotlin mechanism of corutin. Tasks inside async are performed in a special thread pool (not ForkJoin) with a work stealing mechanism. Secondly, this code requires a special context, from which keywords like async and withContext are taken.
If you liked Scala Future, but you write on Kotlin, then you can pay attention to similar Scala wrappers. Type such.
To show the problem above in more detail, let's try to expand the previous example: we turn to the most popular Java programming tools - Reactor , on Scala - fs2 .
Consider line-by-line reading of 3 files in a stream and try to find matches there.
Here is the easiest way to do this with Reactor in Java.
Not the most optimal way, but indicative. It is not difficult to guess that with more logic and access to third-party resources, the complexity of the code will grow. Let's see the for-comprehension syntax sugar alternative.
It seems that there are not many changes, but it looks much better.
Let's go ahead and see how else we can improve our code. I want to warn that the next part may not be immediately understandable. I want to show the possibilities, and leave the implementation method out of the brackets for now. A detailed explanation requires at least a separate article. If there is a desire / comments, I will follow in the comments to answer questions and write the second part with a more detailed description :)
So, imagine a world in which we can set business logic regardless of technical effects that may arise during development. For example, we can make each subsequent request to a DBMS or a third-party service be performed in a separate thread. In unit tests, we need to make a stupid mok in which nothing happens. Etc.
Perhaps some people thought about the BPM engine, but today it’s not about him. It turns out that this problem can be solved with the help of some patterns of functional programming and language support. In one place we can describe the logic like this.
Here F [_] (read as "ef with a hole") means a type over a type (sometimes it is called a species in Russian literature). It can be List, Set, Option, Future, etc. All that is a container of a different type.
Next, we just change the context of the code execution. For example, for the prod environment we can do something like this.
Our business logic now does not depend on which frameworks, http-clients and servers we used. At any time, we can change the context, and the tool will change.
This is achieved by features such as higherKind and implicit. Let's consider the first, and for this we will return to Java.
How many ways to return the result? Enough. We can subtract, add, swap and much more. Now imagine that we have been given clear requirements. We need to add the first number to the second. How many ways can we do this?if you try hard and refine yourself a lot ... in general, only one.
But what if the call to this method is hidden, and we want to test in a single-threaded environment? Or what if we want to change the implementation of the class by removing / replacing CompletableFuture. Unfortunately, in Java we are powerless and have to change the method API. Take a look at the alternative in Scala.
We create traits (the closest analogue is the interface in Java) without specifying the container type of our integer value.
Further we can simply create various implementations if necessary.
In addition, there is such an interesting thing as Implicit. It allows you to create the context of our environment and implicitly select the implementation of the trait based on it.
Simplified implicit before val - adding a variable to the current environment, and implicit as an argument to a function means taking the variable from the environment. This is somewhat reminiscent of an implicit closure.
In aggregate, it turns out that we can create a combat and test environment rather concisely without using third-party libraries.
In general, these are not all my pains. There are more. I think that each developer has their own. For myself, I realized that the main thing is to understand what is really necessary for the benefit of the project. For example, in my opinion, if we have a rest service that acts as a kind of adapter with a bunch of mapping and simple logic, then all the functionality above is not very useful. Spring Boot + Java / Kotlin is perfect for such tasks. There are other cases with a large number of integrations and aggregation of some information. For such tasks, in my opinion, the last option looks very good. In general, it's cool if you can choose a tool based on a task.
Useful resources:
Extending existing classes
Sometimes it happens that it is necessary to expand an existing class without changing its internal content. That is, after creating the class, we supplement it with other classes. Consider a small example. Suppose we have a class that is a point in two-dimensional space. In different places in our code, we need to serialize it in both Json and XML.
Let's see how it can look in Java using the Visitor pattern
More about the pattern and its use
public class DotDemo {
public static class Dot {
private final int x;
private final int y;
public Dot(int x, int y) {
this.x = x;
this.y = y;
}
public String accept(Visitor visitor) {
return visitor.visit(this);
}
public int getX() { return x; }
public int getY() { return y; }
}
public interface Visitor {
String visit(Dot dot);
}
public static class JsonVisitor implements Visitor {
@Override
public String visit(Dot dot) {
return String
.format("" +
"{" +
"\"x\"=%d, " +
"\"y\"=%d " +
"}",
dot.getX(), dot.getY());
}
}
public static class XMLVisitor implements Visitor {
@Override
public String visit(Dot dot) {
return "" + "\n" +
" " + dot.getX() + " " + "\n" +
" " + dot.getY() + " " + "\n" +
" ";
}
}
public static void main(String[] args) {
Dot dot = new Dot(1, 2);
System.out.println("-------- JSON -----------");
System.out.println(dot.accept(new JsonVisitor()));
System.out.println("-------- XML ------------");
System.out.println(dot.accept(new XMLVisitor()));
}
}
More about the pattern and its use
It looks quite voluminous, right? Is it possible to solve this problem more elegantly with the help of language tools? Scala and Kotlin nod positively. This is achieved using the method extension mechanism. Let's see how it looks.
Extensions in Kotlin
data class Dot (val x: Int, val y: Int)
// неявно получаем ссылку на объект
fun Dot.convertToJson(): String =
"{\"x\"=$x, \"y\"=$y}"
fun Dot.convertToXml(): String =
"""$x $y """
fun main() {
val dot = Dot(1, 2)
println("-------- JSON -----------")
println(dot.convertToJson())
println("-------- XML -----------")
println(dot.convertToXml())
}
Extensions in Scala
object DotDemo extends App {
// val is default
case class Dot(x: Int, y: Int)
implicit class DotConverters(dot: Dot) {
def convertToJson(): String =
s"""{"x"=${dot.x}, "y"=${dot.y}}"""
def convertToXml(): String =
s"""${dot.x} ${dot.y} """
}
val dot = Dot(1, 2)
println("-------- JSON -----------")
println(dot.convertToJson())
println("-------- XML -----------")
println(dot.convertToXml())
}
It looks much better. Sometimes this is really not enough with plentiful mapping and other transformations.
Multi-threaded computing chain
Now everyone is talking about asynchronous computing and the prohibitions on locking in execution threads. Let's imagine the following problem: we have several sources of numbers, where the first just returns the number, the second - returns the answer after calculating the first. As a result, we must return a string with two numbers.
Schematically, this can be represented as follows
Let's try to solve the problem in Java first
Java example
private static CompletableFuture> calcResultOfTwoServices (
Supplier> getResultFromFirstService,
Function> getResultFromSecondService
) {
return CompletableFuture
.supplyAsync(getResultFromFirstService)
.thenApplyAsync(firstResultOptional ->
firstResultOptional.flatMap(first ->
getResultFromSecondService.apply(first).map(second ->
first + " " + second
)
)
);
}
In this example, our number is wrapped in Optional to control the result. In addition, all actions are performed inside CompletableFuture for convenient work with threads. The main action takes place in the thenApplyAsync method. In this method, we get Optional as an argument. Next, flatMap is called to control the context. If the received Optional returned as Optional.empty, then we will not go to the second service.
The total that we received? Using CompletableFuture and Optional features with flatMap and map, we were able to solve the problem. Although, in my opinion, the solution does not look the most elegant way: before you understand what’s the matter, you need to read the code. And what would happen with two or more data sources?
Could language somehow help us to solve the problem. And again, turn to Scala. Here's how you can solve it with Scala tools.
Scala example
def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int],
getResultFromSecondService: Int => Option[Int]) =
Future {
getResultFromFirstService()
}.flatMap { firsResultOption =>
Future { firsResultOption.flatMap(first =>
getResultFromSecondService(first).map(second =>
s"$first $second"
)
)}
}
It looks familiar. And this is no coincidence. It uses the scala.concurrent library, which is primarily a wrapper over java.concurrent. Well, what else can Scala help us with? The fact is that chains of the form flatMap, ..., map can be represented as a sequence in for.
Second version example on Scala
def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int],
getResultFromSecondService: Int => Option[Int]) =
Future {
getResultFromFirstService()
}.flatMap { firstResultOption =>
Future {
for {
first <- firstResultOption
second <- getResultFromSecondService(first)
} yield s"$first $second"
}
}
It’s gotten better, but let's try changing our code again. Connect the cats library.
Third version of Scala example
import cats.instances.future._
def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int],
getResultFromSecondService: Int => Option[Int]): Future[Option[String]] =
(for {
first <- OptionT(Future { getResultFromFirstService() })
second <- OptionT(Future { getResultFromSecondService(first) })
} yield s"$first $second").value
Now it’s not so important what OptionT means. I just want to show how simple and short this operation can be.
But what about Kotlin? Let's try to do something similar on coroutines.
Kotlin example
val result = async {
withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first ->
withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second ->
"$first $second"
}
}
}
This code has its own peculiarities. First, it uses the Kotlin mechanism of corutin. Tasks inside async are performed in a special thread pool (not ForkJoin) with a work stealing mechanism. Secondly, this code requires a special context, from which keywords like async and withContext are taken.
If you liked Scala Future, but you write on Kotlin, then you can pay attention to similar Scala wrappers. Type such.
Work with streams
To show the problem above in more detail, let's try to expand the previous example: we turn to the most popular Java programming tools - Reactor , on Scala - fs2 .
Consider line-by-line reading of 3 files in a stream and try to find matches there.
Here is the easiest way to do this with Reactor in Java.
Reactor example in Java
private static Flux glueFiles(String filename1, String filename2, String filename3) {
return getLinesOfFile(filename1).flatMap(lineFromFirstFile ->
getLinesOfFile(filename2)
.filter(line -> line.equals(lineFromFirstFile))
.flatMap(lineFromSecondFile ->
getLinesOfFile(filename3)
.filter(line -> line.equals(lineFromSecondFile))
.map(lineFromThirdFile ->
lineFromThirdFile
)
)
);
}
Not the most optimal way, but indicative. It is not difficult to guess that with more logic and access to third-party resources, the complexity of the code will grow. Let's see the for-comprehension syntax sugar alternative.
Example from fs2 on Scala
def findUniqueLines(filename1: String, filename2: String, filename3: String): Stream[IO, String] =
for {
lineFromFirstFile <- readFile(filename1)
lineFromSecondFile <- readFile(filename2).filter(_.equals(lineFromFirstFile))
result <- readFile(filename3).filter(_.equals(lineFromSecondFile))
} yield result
It seems that there are not many changes, but it looks much better.
Separating business logic with higherKind and implicit
Let's go ahead and see how else we can improve our code. I want to warn that the next part may not be immediately understandable. I want to show the possibilities, and leave the implementation method out of the brackets for now. A detailed explanation requires at least a separate article. If there is a desire / comments, I will follow in the comments to answer questions and write the second part with a more detailed description :)
So, imagine a world in which we can set business logic regardless of technical effects that may arise during development. For example, we can make each subsequent request to a DBMS or a third-party service be performed in a separate thread. In unit tests, we need to make a stupid mok in which nothing happens. Etc.
Perhaps some people thought about the BPM engine, but today it’s not about him. It turns out that this problem can be solved with the help of some patterns of functional programming and language support. In one place we can describe the logic like this.
In one place, we can describe the logic like this
def makeCatHappy[F[_]: Monad: CatClinicClient](): F[Unit] =
for {
catId <- CatClinicClient[F].getHungryCat
memberId <- CatClinicClient[F].getFreeMember
_ <- CatClinicClient[F].feedCatByFreeMember(catId, memberId)
} yield ()
Here F [_] (read as "ef with a hole") means a type over a type (sometimes it is called a species in Russian literature). It can be List, Set, Option, Future, etc. All that is a container of a different type.
Next, we just change the context of the code execution. For example, for the prod environment we can do something like this.
What might the combat code look like?
class RealCatClinicClient extends CatClinicClient[Future] {
override def getHungryCat: Future[Int] = Future {
Thread.sleep(1000) // doing some calls to db (waiting 1 second)
40
}
override def getFreeMember: Future[Int] = Future {
Thread.sleep(1000) // doing some calls to db (waiting 1 second)
2
}
override def feedCatByFreeMember(catId: Int, memberId: Int): Future[Unit] = Future {
Thread.sleep(1000) // happy cat (waiting 1 second)
println("so testy!") // Don't do like that. It is just for debug
}
}
What the test code might look like
class MockCatClinicClient extends CatClinicClient[Id] {
override def getHungryCat: Id[Int] = 40
override def getFreeMember: Id[Int] = 2
override def feedCatByFreeMember(catId: Int, memberId: Int): Id[Unit] = {
println("so testy!") // Don't do like that. It is just for debug
}
}
Our business logic now does not depend on which frameworks, http-clients and servers we used. At any time, we can change the context, and the tool will change.
This is achieved by features such as higherKind and implicit. Let's consider the first, and for this we will return to Java.
Let's look at the code
public class Calcer {
private CompletableFuture getCalc(int x, int y) {
}
}
How many ways to return the result? Enough. We can subtract, add, swap and much more. Now imagine that we have been given clear requirements. We need to add the first number to the second. How many ways can we do this?
There he is
public class Calcer {
private CompletableFuture getCalc(int x, int y) {
return CompletableFuture.supplyAsync(() -> x + y);
}
}
But what if the call to this method is hidden, and we want to test in a single-threaded environment? Or what if we want to change the implementation of the class by removing / replacing CompletableFuture. Unfortunately, in Java we are powerless and have to change the method API. Take a look at the alternative in Scala.
Consider trait
trait Calcer[F[_]] {
def getCulc(x: Int, y: Int): F[Int]
}
We create traits (the closest analogue is the interface in Java) without specifying the container type of our integer value.
Further we can simply create various implementations if necessary.
Like so
val futureCalcer: Calcer[Future] = (x, y) => Future {x + y}
val optionCalcer: Calcer[Option] = (x, y) => Option(x + y)
In addition, there is such an interesting thing as Implicit. It allows you to create the context of our environment and implicitly select the implementation of the trait based on it.
Like so
def userCalcer[F[_]](implicit calcer: Calcer[F]): F[Int] = calcer.getCulc(1, 2)
def doItInFutureContext(): Unit = {
implicit val futureCalcer: Calcer[Future] = (x, y) => Future {x + y}
println(userCalcer)
}
doItInFutureContext()
def doItInOptionContext(): Unit = {
implicit val optionCalcer: Calcer[Option] = (x, y) => Option(x + y)
println(userCalcer)
}
doItInOptionContext()
Simplified implicit before val - adding a variable to the current environment, and implicit as an argument to a function means taking the variable from the environment. This is somewhat reminiscent of an implicit closure.
In aggregate, it turns out that we can create a combat and test environment rather concisely without using third-party libraries.
But what about kotlin
Actually in a similar way we can do in kotlin:
Here we also set the execution context of our code, but unlike Scala, we explicitly flag this.
Thanks to Beholder for the example.
interface Calculator {
fun eval(x: Int, y: Int): T
}
object FutureCalculator : Calculator> {
override fun eval(x: Int, y: Int) = CompletableFuture.supplyAsync { x + y }
}
object OptionalCalculator : Calculator> {
override fun eval(x: Int, y: Int) = Optional.of(x + y)
}
fun Calculator.useCalculator(y: Int) = eval(1, y)
fun main() {
with (FutureCalculator) {
println(useCalculator(2))
}
with (OptionalCalculator) {
println(useCalculator(2))
}
}
Here we also set the execution context of our code, but unlike Scala, we explicitly flag this.
Thanks to Beholder for the example.
Conclusion
In general, these are not all my pains. There are more. I think that each developer has their own. For myself, I realized that the main thing is to understand what is really necessary for the benefit of the project. For example, in my opinion, if we have a rest service that acts as a kind of adapter with a bunch of mapping and simple logic, then all the functionality above is not very useful. Spring Boot + Java / Kotlin is perfect for such tasks. There are other cases with a large number of integrations and aggregation of some information. For such tasks, in my opinion, the last option looks very good. In general, it's cool if you can choose a tool based on a task.
Useful resources: