Kotlin through the eyes of a Java developer
Hello, Habr! Today I want to tell you about my experience of interaction with the Kotlin language.
Let me introduce myself – I am a Java developer, I work at a large bank, I create (and support existing) microservices.
A small remark: I'm not going to become an Android developerneither now nor in the future, so when I became interested in a new language, I did not take into account the arguments about the various conveniences of mobile development on it, and was guided only by the convenience of the language as a whole for the backend.
So, why did I decide to learn Kotlin? Well, first of all, everyone's been buzzing about it, saying it's about reducing the amount of code, conciseness, readability and sugar.
Secondly, Kotlin is fully compatible with Java. For understanding, below is a screenshot of what the contents of one of the intellij packages of the git4idea plugin looks like:
(Java and Kotlin classes are mixed together, but the code remains readable (in places))
Yes, projects can be migrated from Java to Kotlin gradually, which is a killer feature in my opinion.
So I thought, why not.
Here are the things I've gotten used to in my six months of pet-designing in Kotlin, and which I personally find convenient to use:
Classes and methods closed for extension. Everything is final by default, unlike Java, where, on the contrary, any class is open for extension. Kotlin adheres to the principle described in Effective Java by J. Bloch, which sounds like this:
“classes and methods should be private and not be extended or overridden unless we have a compelling reason to extend or override them. And when we decide that our class should be open for extension, we should document the consequences of overriding any method.” Source
In order to make a class in kotlin extensible, we must use the open keyword, which does not have the transitive property – this means that when Class2 inherits from open class Class1, we cannot inherit Class3 from Class2 without adding the open keyword to it.
Null-safe. The ability to allow/disallow (by default) assignment of null to a variable at the compile level is a plus.
var a: String = "abc" a = null // compile error var b: String? = "xyz" b = null // no problem
Getters and setters by default. Any field is not directly retrieved, moreover, under the hood it is private (by default) and can only be accessed by a getter. When you try to get a field of your object, you actually call the get method
val isEmpty: Boolean
is equivalent to the following Java code:
private final Boolean isEmpty; public Boolean isEmpty() { return isEmpty; }
Well, and:
var someProperty: String = "defaultValue"
also equivalent to:
var someProperty: String = "defaultValue" get() = field set(value) { field = value }
If desired, you can, of course, restrict access to getters or setters:
var isEmpty: Boolean = true private set
Examples commodified
One-line methods. I'll be brief: when declaring a method like:
fun doSomething(): String{ return “doing something” }
Kotlin allows you to remove the explicit return type declaration, curly braces, and the return keyword
It starts to look like this:
fun doSomething() = “doing something”
At first it was unusual (especially when such declarations come after the class field initialization block), but then I got used to it
Simplified large nesting when comparing strings and any other complex types in If – guys sewn up equals V == (starts working if you override the equals method explicitly, or if you mark the class with the data keyword.)
I'll add that if you really need to compare references to objects, Kotlin has a separate operator for this: triple equals ===
Java:
if (str1.equals(str2.equals(errString)? "default":str2)){ //... }
Kotlin:
if (str1 == if (str2 == errString) "default" else str2) { //... }
Comparison of classes:
Java:
@EqualsAndHashCode @AllArgsConstructor public class MyClass { private String name; private int value; } public static void main(String[] args) { MyClass first = new MyClass("name", 5); MyClass second = new MyClass("name", 5); System.out.println(first.equals(second));//true System.out.println(first == second);//false }
Kotlin:
data class MyClassK(var name: String?, var value: Int) fun main() { val first = MyClassK("name",10) val second = MyClassK("name",10) println(first == second)//true println(first === second)//false }
Named arguments. When combined with default arguments, named arguments eliminate the need to use Builders (Builder pattern)
Instead of the following Java code:
@Builder //lombok annotation public class MyClass { private String name; private int value; private double rating; } MyClass myClass = MyClass.builder() .name("MyJavaObj") .value(10) .rating(4.99f) .build();
In kotlin we can do this:
class MyClassK(name: String? = null, value: Int = 0, rating:Double = 0.0) fun main() { var myClassK = MyClassK(value = 10, name = "MyKotlinObj", rating = 4.99) }
Operator Overloading
Kotlin has a predefined set of operators that can be overloaded to improve readability:
data class Vec(val x: Float, val y: Float) { operator fun plus(v: Vec) = Vec(x + v.x, y + v.y) } val v = Vec(2f, 3f) + Vec(4f, 1f)
example borrowed from here
Extension Functions
In short: we can supplement existing classes with our functionality without inheritance, and treat this functionality as native throughout the rest of the program.
fun String.format() = this.replace(' ', '_') val str = "hello world" val formatted = str.format()
or, for example:
val result = str.removeSuffix(".txt")
(In Java I would put this in a static method of some StringUtils class)
Convenient, isn't it? Especially since the IDE suggests our function among other pop-ups.
Working with NPE in a call chain. If we want to extract some small part from a complex, composite object, but there is no guarantee that null will not be waiting for us on this path, we have to do the following:
Java:
try{ Status status = journal.getLog().getRow(0).getStatus(); if(status == null) throw new NullPointerException(“null status detected in log”); } catch(NullPointerException e){ status == Status.ERROR; //logger.error(“Journal is not correct”); }
Kotlin:
var status: Status? = journal?.log?.row?.status status = status ?: Status.ERROR
or
var status: Status? = journal?.log?.row?.status if(status.isNull()){ status = Status.ERROR logger.error(“Journal is not correct”) }
I didn't touch on coroutines here, because I'm not familiar enough with them, and in general I don't have much experience working with multithreading, so I can express my expert opinion. But in general, I watched the report and I advise you to do the same. Maybe I'll get around to it, study the topic and write something about it.
How wonderful it is that now I don't have to worry about NPEs in the call chain. In Java, as you can see, you have to wrap the unsafe call chain in a cumbersome try-cath, but damn, sometimes I want null to be simply assigned to a variable when null is returned somewhere in the chain. This is exactly the functionality that kotlin gives me.
Lyrical conclusion:
In general, I am in deep thought about the language: I have been programming in Java for a very long time and it is somehow more familiar to me, or something.
(When I see files with functions in a project, separate from classes, I periodically twitch.)
There are many nuances and changes, no matter how you look at it, although there is compatibility.
I recently listened to several podcasts with guests – JetBrains developers and product managers, the guys were so enthusiastic about the language, its prospects and horizons, like Java, only better, Kotlin Multiplatform, etc.
But it still remains a fact that Kotlin didn't go to the backend en masse, but occupied the mobile niche, although maybe I don't know something. Maybe behind my back everyone in production has already secretly rewritten their toad projects in Kotlin, and they didn't tell me.
And is there such a thing as a main language? Maybe we shouldn't treat it that way, but stick to the principle of “a tool for every task”. But then for which backend tasks is kotlin, and for which java? Or is it “pumped-up java”?
I would really like to hear other people's experience of interacting with these languages in the comments, preferably from guys who work on the backend in the bloody enterprise, and not mobile developers, although I am glad to hear from everyone.
Have you had any experience migrating large projects from one language to another? If yes, which one? If no, why not?
Maybe you personally changed your main language? Why? Sugar and lack of semicolons? Or more global reasons?
(It occurred to me that syntactic sugar doesn't seem like a killer feature of any language, because when you're used to writing like that, and others are used to reading like that, it ceases to be a problem)
Sources:
https://habr.com/ru/companies/otus/articles/532270/ – Equality testing in Kotlin
https://youtu.be/rB5Q3y73FTo?si=piObKnscuv1S9vtg – Roman Elizarov. Coroutines in kotlin
https://habr.com/ru/companies/vk/articles/329294/ – kotlin features overview from VK
https://stackoverflow.com/questions/37906607/getters-and-setters-in-kotlin – getters and setters. Thread on Stack Overflow
https://www.baeldung.com/kotlin/open-keyword – keyword open
https://github.com/amitshekhariitbhu/from-java-to-kotlin – cool repo-training in Java vs Kotlin format
https://radioprog.ru/category/183 – design patterns, without the fluff