Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hello and a warm welcome to the presentation on
effective Java with Groovy and Kotlin. I started
using Java on several projects and after that I went
on to use Groovy and Kotlin languages on few more JVM
projects. This journey had some interesting
lessons and I had some cool observations on how
program languages influences developers to adopt
good practices. And in this presentation I'm going to share them
with you. My name is Naresha and I help teams to
get better at applying technology to solve their business
problems. Let's get started by setting up a common ground.
You know, Java started as a platform independent technology.
Do you remember that famous acronym Vora which meant for
right ones run anywhere? Interestingly, Java was
the only language that you could use to run
or develop applications on top of JVM.
But later that situation changed and
we had more programming languages targeting
JVM. Like Groovy is such a language.
And later on we had Kotlin which is another language
which could run on JVM. Now making
the JVM a more promising place
for developers because they could choose
the language, what helps them better to create their
application and the information
or the key that JVM is. These most important
piece in the whole ecosystem has been endorsed
by people like James Gosling,
who has been attributed as the father of Java.
With that, let's move on to effective Java.
I'm sure you would consider effective Java
as one of the most influential books that
helped Java developers. When I read it like
15 years ago, it really changed the way
I wrote Java programs. Not only that,
it really changed some of my fundamental beliefs
on what is the right approach or right technique for
developing applications on JVM. A quick
look at groovy programming language one of
the problems faced by several developers
when they were developing on Java language was
that developer productivity developers weren't
that productive because they were spending a lot of time writing
a very mundane verbals code.
So Groovy was an attempt to solve
such problem. So here you could see what James
Strachen who created Groovy language said,
that the whole idea was to make developers
really productivity with the new language and at that time the approach
was dynamic language.
On a similar note, Kotlin is another JVM
language and jetbrains developed
this language to make sure that their development experience
on JVM is better. And they had certain
things in mind when they developed this languages.
And interestingly, it was at a later point
in time than Ruby, so more of like static typing
was the most widely preferred
choice at that point in time. With that, let's explore
a few wisdoms from effective Java and see how they're
applicable in languages like Groovy and Kotlin.
So here are a few icons that will help you
to navigate this. In case you feel lost, you will
always see these icons and that will help you to
see where you are. So we will take this approach. We will start with
these problem, right? When we have a problem, what do we do? Effective Java
comes to the rescue. So let's go and check what does effective Java say
in that context or how to solve that
particular problem. Then we know what is said in
effective Java is good for Java languages.
But when we take those ideas or when
we try to reuse these Java implementation in groovy or Kotlin,
we might end up with the certain problems. Those are the traps what
you need to be aware of. Then we will see for Groovy or
Kotlin what is the idiomatic solution. And then
finally we will see lessons what lessons
did we learn by going through these good examples?
Note here that effective Java was initially written
to improve the Java implementation. And when
languages like Groovy and Kotlin were developed, they were already aware
of effective Java practices. So they incorporated lot
of these ideas in the language itself.
But in a way, when we look
at how these effective Java suggestions
are used in Groovy and Kotlin, we get
another idea of how these languages, groovy and
Kotlin make developers life
easy and really encourage developers to embrace
good practices. So let's take few examples and
explore this since you would see good examples covering
three languages. Here I have the icons
of respective languages so that it becomes easy for you
to identify which language has been used in writing
that particular piece of code. So without further delay, let's get
started with the first item. In this case,
I have a class written in groovy, which is product
which has three attributes, namely sku,
description and price. So what I'm going to do is I'm creating
two instances of the product class with
the same set of attributes, and I'm trying
to check if these two objects are
equal. Please note that double
equals in groovy is like similar to
calling equals method in Java. So typically you would expect
them to be equal, but they are not
equals. On a very similar note, what I'm going to do
is create hash map with
the variable name as stock. Note the
native syntax of hash map in groovy
and I'm using book one as the key to store a value
100 in that and then I'm trying to retrieve the
value using boost two as these key.
Ideally, since they have the same value, we would expect to
retrieve the value 100, but that's not the case.
I'm sure you have had these surprise before and
you'd have rightly guessed it. The culprits are equals
and hash code. I mean the developers, we are the culprits. We were
supposed to override equals and hash code because
hash code is unique for each instance and
equals method by default would check
if they are pointing to the same object, same instance. But in
this case we had two instances, right? So it becomes our developer's
responsibility to override these methods.
So these are these related wisdoms
from effective Java book. Essentially what you need to
do is maintain that consistency between equals
and hash code methods. That's like whenever two objects
are equals, they should have the same hash
code, which means that when we
override equals and hash code method, they should
depend on the same set of attributes. You can't have equals
depending on few set of attributes and hash code depending on
another set of attributes. The typical approach without
or there are two approaches what you could take in
Java. Approach one, ask your ide to generate the
code approach. Two, use a library like
Apache commerce language which will provide you something like equals builder
and hash code builder. You would see an example of using
equals builder in action here. Essentially you need to supply
what are the fields that the equals method
would depend upon. I see two problems
here. One is like if you use IDE
to generate the code, let's say take this example
and now say I'm going to add one more attribute,
date of manufacturer after some time, say the
requirement changes and now I need to incorporate date of expiry.
Also, who is going to update equals and
hash code? Ide code generation works great for the first time.
Then if the developer forgets to update or to
regenerate that code, you will end up in
trouble, right? That's the problem with the IDE code generation.
On the other hand, what happens with the
approach, what we are using is like if you use equals and
hash code builder, there are duplicate representations,
right? Since we told in order to maintain the consistency between
equals and hash code, they should rely on the same set of attributes.
However, there were two different classes, one equals
builder and one hash code builder instead of a single
equals and has code builder. That's what groovy does. And you
would see that it
looks like an annotation at equals and hash code,
which is called as ast transformation in
groovy. Now what happens is compiler will
look at this instruction, that is the ast transformation equals
and hash code, which will automatically generate the equals
and hash code whenever you compile
your code. Now just by adding
that extra annotation we will rerun our
code. Now you would see book one equals book two,
and you would see that when we use the second
object as these key, we are getting the right value
and the code is working as expected.
Now however, a couple of things that you need to be
taking care. In the previous case the class,
what we looked is more like value object has against an
entity. But in the case of an entity what you
would do is like say you might have can autogenerated id.
So you have to be careful about the situation wherein
like object before persisting versus object
after persisting. So that's the reason why typically
relying upon an auto generated key can lead you
to trouble.
Instead of using that, you should typically rely upon
the business key, which in this case happens to be
sKu, so that there is always consistency even if
you perform a save in between. If you had question on how
do I instruct these groovy compiler to generate
equals and hash code methods based on subset
of fields? I'm sure the example also
tells you how to do that. Here I have used influences
in a very similar way. You have excludes options
as well. With that, let's move on to Kotlin
and recreate a very similar situation here.
You are very much familiar with this and what approach
Kotlin takes as against groovy is that
Kotlin uses more implicit approach.
That is like you use data class. By default you get
all the equals hash code methods generated
for you. Now just by adding making it
a data class rather than an ordinary class,
it works as expected because compiler generates
equals and hash code methods for you. And now
let's move on to the second problem of customizing.
In this case I wanted to have a field like Id
which gets changed later on when you're
saving the value. So now again you're back to the problem like
both they are not equals and you're
not retrieving the right value using the second object
as these key. So how do we solve these kind of problems?
Pretty simple. In Kotlin convention you
use the fields that are to be considered for
equals and hash code in your primary constructor,
and the remaining fields you declare
inside the body. That's the approach you have to
take.
With that, let's move on to see what lessons
did we learn. So groovy used ASD transformation,
you have to be explicit on what fields
to be used for equals and hash code. You say that
by default all fields are considered. If you just put the transformation
unlike that, Kotlin uses
more default convention like
okay, data classes will generate equals and
hash code with the fields to be considered are to
be part of the primary constructor. That's the more like
implicit approach of Kotlin versus slightly
more explicit approach of groovy.
And interesting point is, both these languages
save you, the developers, from committing
the mistake of violating dry principle, right? That's like
single point of representation for any knowledge which
you might commit in your Java code.
That's the real benefit you are going to get. And that's how these two
languages help you to embrace good practices.
So the good practice here is dry principle and you
have learned how groovy and Kotlin help you
to embrace good practices. With that, let's move on
to see these piece of code. Well, how does it look?
Very complex, right? Why does it look complex?
Because it has too many moving parts. I'm sure many times when
we see our code we get the same feeling represented
by these image. So let's take this code, piece of
code simple like Java code, wherein we
iterate through a list. What do you
think is the problem or the difference between the first
very conventional for loop versus the for
each loop? You would readily notice that the
second piece of code here has fewer moving parts compared
to the first, which makes the second code simpler and
less error prone. So what does effective
Java say? It says prefer for each loop to traditional
for loops. And let's move on
to Kotlin this time. And let's
use that for each loop. Very similar to that in Kotlin.
You could use this piece of code. And now
let's say we want to perform some sort of
map operation, multiply each by two.
So instead of using a typical loop, I'm using
map transformation here. That's why you have higher order
function map available which accepts closure
as the argument. Very similar. I can perform a
reduce operation using fold.
Now groovy also provides a very similar operation.
This is a regular like for each loop
and I could use map transformation.
The method is collect and very
similar to fold. You have inject in groovy.
So notice that in these languages,
instead of for each loop, we could go on to the next level.
So when it comes to interpreting favoring for
each loop against traditional for loops in these
languages, what we need to do is that we have to interpret
it in a slightly better way. That is like
what we need to do is we should favor internal iterators
to external iterators, wherein you could
make your code have even fewer moving
parts. And what helps these languages to
enable that is closures or
higher order functions. Since Java nowadays also
has support for lambda expressions.
This advice is very much applicable to Java also.
But in Java you'll have to use streams API
to take advantage of this, while these
languages have higher order functions supported right
in their collections. With that,
let's move on to these next item. If I ask you a
question, what would be a million dollar or
billion dollar effort spent in every
Java project, or money wasted in Java
presents writing redundant code? My favorite would be definitely
null check, right? A lot of null checks
people write, but still ending up with null pointer
exception in several places. So let's
see an example. Imagine I have
a method called get speakers, which accepts a conference
name, has arguments, and returns the list
of speakers who speak at the conference. What I do
is I go and return null directly
from this method. What is the implication of this?
Whenever a developers calls this method,
he has to make sure that he checks if the written value
is null, which is going to be very
counterproductive, and every call would require
a null check, which is not recommended.
So as per effective, Java says return empty
arrays are collections instead of nulls,
right? So you should never return nulls whenever the return
type is a collection. Let's call the same
piece of code in groovy. So imagine in groovy you
return null, and let's try to invoke
that method. On that return method, we'll perform subsequent
operations and see if we get null pointer
exception, which is the main problem, right? So what I do is I invoke
the collect method on the return value, which is
null, which is like calling null collect, right? These again in
the next line I'm calling null find
all. And interestingly, you would note that I'm not
going to get a null pointer exception, I'm going to get an
empty collection, empty list in this case.
Well, so now your friend is trying
to create trouble for you by returning null in
that method, but groovy is going to save you from all
the trouble that your friend is going
to cause to you. How did that happen?
Groovy, instead of using the null literal,
it replaced it with the null object. Null object.
What it does is whenever you invoke iterator
method on the null object, it returns an
empty list. In this case, instead of
ending up with these null pointer exception. If you invoke
any other method it will
throw a null pointer exception. So using null objects
for instead of null literals saved you
from a lot of trouble. Now in the case of Kotlin,
let's see how different approach Kotlin
takes. So you have these
method get speakers returning a list of speakers and I say return
null. What happens is that the code is not using to compile because by
default type is not null level.
If you think that these method can return null,
then you should explicitly say that by
suffixing the question mark in this case. In the second example
it will allow you to return null. The first example it will not
allow you to return null. So developer has to ask that
question to himself, hey, do you really want to return null
or not? Accordingly, the compiler will help
you with the extra safety checks. So note that in this
case groovy and Kotlin take quite
different approach for you.
Both were pretty much helpful to the developers,
but they were fundamentally different
in the approach they took. So what did we learn here?
Technique used was null object pattern in case of groovy and
type system itself, asking that question whether
it should allow null or not. Null was what
has been favored by Kotlin and essentially
helping you to write fewer boilerplate code and more
safety, which is the value what these
features deliver to the users of these languages.
And in fact, life is too short for null checks.
You would rather spend it wisely for solving
some real business problems. Well, what comes to
your mind when you hear the word side effect?
It's mutability, right? Interestingly, effective Java
says minimize mutability. So in the beginning when I said
effective Java fundamentally changed
a lot of my thoughts about how to approach programming in
JVM. Definitely minimizing mutability
is one of them. I was really surprised to see this in
the book, because before that I always used to think programming
in Java is pretty much about mutability.
However, reading this really changed
my thought process and under that point
the book talks about it gives list of
a checklist how to make a class immutable,
which is long enough as you would see.
Usually what happens when such a prescription is given to the developers
is that they happily ignore this, right? They don't
implement that. That was these challenge happened with
Java. So let's see how modern
JVM languages like Groovy and Kotlin overcome
this. Let's start with groovy in this case, as you would see,
I could go and take
the Java implementation and reuse it in groovy.
But again I'll have to write a lot of code. And there
is second challenge that the constructor has
so many fields here, and in order to invoke
that I have to remember the order of
these parameters, which is like the second challenge,
in addition to making fields final,
et cetera. So groovy provides can est transformation
which is called immutable here. So I apply that to
my class rectangle and
now all the fields are made final and
there is no setters, there is only getter and
the way in which I invoke the constructor.
Also you could use something like in the
named argument style, like length as
ten and breadth as five. With this you really don't have to remember
the order of parameters.
Let's see how this works under these hood in groovy.
So if you look at the generated code
by groovy compiler, you would see that both length and breadth
were made final and it generated
regular constructor as expected.
Note that it created constructor which takes a java util
map as the argument.
That's how my named argument style
works. So whenever, especially you say you provide the
named argument kind of syntax under the hood, it is converted
to map and map constructor
gets called. That's how groovy achieves
not remembering the argument order part. Let's see
what Kotlin does, which is very similar, but there is a slight
change there. Of course you will have to use, I mean, by making
it a data class you get immutability in
addition to equals and hash code, what we saw in the beginning.
But what happens is that again you would see the generate
making the field final having only getters,
no setters, and in this case you would see the regular constructs.
You won't see a map constructor with the map argument
here. But what happens is that whenever you invoke in
a named argument style in this I say length 20 and
breadth ten. Note that compiler
took care of rearranging and calling the
constructor in the right order. So that's the difference
between Groovy and Kotlin. In groovy. What happened
is it was map constructor, and in this
case compiler took care of rearranging or rather deciding
the order based on the named arguments what you providing
to it. So ac transformation
again is a predominant technique used in groovy as
you would see repeatedly. And syntactic sugar is that
of providing a named argument style
in turn results in a map argument. That's what
happens. That's a syntactic sugar and in
Kotlin you would see the compiler
doing the work and the area that
this really helps is the readability aspect.
Well it's just time for another question. What do you think is these golden
hammer with respect to Java the language?
Well most of you would have guessed it right,
it's inheritance, right people,
or I would say inheritance is the number one abused
feature in Java language. So effective Java says
favor composition over inheritance and
let's take an example. So imagine I
have a list of string here
which I want to be considered as like phone
numbers. So I want to perform all the operations that
are available in a list. In addition to that
I want it to have a few more operations
like say I want to check if it is like a
particular phone number belongs to indian. So what
I could do is how would I design these
phone numbers type, the first thought usually comes is
okay, array list has all the implementations of list
operations, why not I extend from array list and add
that additional one method which I need.
But if you look at it closely, right. The case
for using inheritance is typically substitutability which
is like Liskov substitution principle.
These consumer doesn't have to know the details without knowing
that it gets a common contract or interface
and they can invoke with these different types of
behavior getting executed. Not really to reuse
something is available in case class in the check
class. So typically the option is to use
composition in this case. But you would know that writing a composition
would involve typically writing lot of WordPress
code when it comes to Java. Let's take these case of groovy.
What you could do is you could use the delegate at
delegate ast transformation. Now I have
defined an instance variable called
phone numbers of type list which is marked as delegate
and whatever any other custom
methods. What I need, I can write it
inside the class. So now what happens is that even though my
class four numbers does not implement any
list methods on its own due to the delegate
instruction, compiler will make sure that it generates the necessary
code that is required to delegate into the realist
implementation. That's what happens in groovy.
And let's quickly check Kotlin.
Kotlin also provides a very similar feature.
So you would use the delegate like using that by
keyword. That's what we are using it here. And when
it comes to composition there is also a composition of
behavior, something like called as trait
in groovy which also is very much applicable.
But I'll not be going to the details of composing
through traits. So again groovy supports
this feature with the ASD transformation.
And essentially we need to
make sure that developer doesn't have to write, or the language has
to make sure that developers don't have to write a lot of code to
achieve some simple tasks. In that case they
might be discouraged, whereas if you simplify things for them,
they might be really encouraged towards embracing good
practices. Right, because the moment you say doing
right thing is easy and doing
the right thing is difficult and the wrong thing is easy, people might
choose these wrong way, which is why we should be careful.
And I'm happy that modern languages are really helping
developers embrace good practices. Now let me conclude by
summarizing the takeaways. Well, as you would
have noticed that some of the effective Java suggestions
are already baked into the language like Groovy and Kotlin,
making it easy for the developers because that's available right
out of the box. I understand that Java is also
slowly catching up something like records for
data classes. Kotlin what we saw,
those are coming up. I mean these are already there,
but not yet in the LTS version as such.
So that's one of these benefit what
you would see readily. And another aspect what
we saw is that compiler generated code is
much better than IDE generated because it's
my firm belief that any practice that assumes
that code does not change is fundamentally flawed
in software development, because we know change is the only
thing that is constant. If you use IDE to generate
the code, you may not get an opportunity, or the IDE doesn't
get an opportunity to update the code right, you will have to do it yourself.
Whereas whenever you make a change, obviously you're going to compile your
code. Compiler can regenerate the
code as per these changes you have made.
That's why I believe these
days it's much better to favor compiler generated
code than using the id generated code. And we
have saw several examples where copying
implementation from usage
of effective Java, like the Java code into groovy or Kotlin,
might not be the best practice. You may fall into traps.
So understanding the idiomatic approach in these languages
is really really important to be effective. And with
several examples I'm sure you have understood that programming
languages can really reduce these friction to implement
good practices by making developers really
productivity. And with this we saw that
Kotlin and groovy in several cases achieve the same value
with different set of approaches or different techniques,
which really helps a developer to understand that there could be really
multiple right solution. There's nothing like one perfect solution.
So always what fits better in your context.
You can go and pick one of such solutions which
is really thing to consider when you are designing
anything. And very important, the way we code
right, is not just influenced by the
language in which we write the code, but also depends on
our knowledge of other languages, which will really
open our mind towards new possibilities and new
design. So even if you're not using Kotlin
Groovy or any other language for your day to day work, knowing those languages
will definitely help you to come up with better designs.
And one thing what we saw is that while effective Java
in the beginning was very, very helpful for
Java developers to learn better ways of writing their
Java code. But then what happened later
is that these
effective Java ideas helped language designers to
develop languages in better ways. And today, as a
developer, when you use these languages,
I think an important question to ask is like can
we use effective Java as litmus test for
modern JVM languages? What do you think? With that, I hope I
managed to give you a few thoughts to ponder up on later and you
found them useful. I would like to thank the organizers of Conf
42 Java for providing me this opportunity. Thank you
one and all and happy coding.