📜 ⬆️ ⬇️

Kotlin aftertaste, part 2

In the last part, I talked about the Kotlin pitfalls, in this one I’ll tell you what it outweighs.

I was asked several times, but what is so about Kotlin, which could lead to a transition from Java, what is its trick? Yes, Kotlin introduced Null-safety and a large amount of syntactic sugar, covering some of the weak points of Java. But this does not become a reason for the transition. What could be? New features and new philosophy.



New opportunities


1. Null safety


This is stated first. This really makes the code more secure. Yes, there is a problem when calling java. It comes down to two options:
')
  1. Calling third-party libraries. It is treated either by explicitly declaring the type of variable to which the result is assigned, or by writing extension, which one often wants to do to straighten the call chain (an example will be at the end).
  2. Receiving a message from the outside world (REST, MQ, etc.). Here comes Spring Validate.

2. Korutiny


I myself have not used it yet, but apparently, this can greatly change the approach to multi-threaded programming. UPD I corrected myself and wrote an article about korutinah .

3. Compiling JavaScript


Unfortunately, I have not tried it either. At the time of the start of my project, it was only in beta, and I was not familiar with angular. Now itching to try - a single module with DTO for the server and client side.

New philosophy


Kotlin's philosophy is Concise , Safe , Interoperable , Tool-friendly (from the official website and reports).

1. Interoperable


Java compatibility is truly 100% round-trip. No matter what I did, everything worked fine.

2. Tool-friendly


I haven’t tried Eclipse, but Intellij is fine and continues to improve.

3. Safe


In my pragmatic view, this is the most important thing. Concise, Interoperable, Tool-friendly are the minimum conditions for the survival of languages ​​on a JVM, otherwise they will lose Java.

Null-safety + mutability


For example, a local variable is a list of strings. In java, the compiler knows only two options:

List<String> list1; final List<String> list2; 

The second option is found in isolated cases. Usually, this is not the 8th Java, and this list is needed in an anonymous class.

But Kotlin:

 val list1: List<String>? val list2: List<String?>? val list3: List<String> val list4: List<String?> val list5: MutableList<String>? val list6: MutableList<String?>? val list7: MutableList<String> val list8: MutableList<String?> var list9: List<String>? var list10: List<String?>? var list11: List<String> var list12: List<String?> var list13: MutableList<String>? var list14: MutableList<String?>? var list15: MutableList<String> var list16: MutableList<String?> 

What does this give? Each type has its own guarantees and a set of operations allowed on it. So, + = null can only be called on var list12: List <String?>, And add (null) on val list8: MutableList <String?> And var list16: MutableList <String?>.

With each ad to write the full type will be expensive. Therefore, there is a type inference:

 val test = Random().nextBoolean() val list1 = if (test) null else listOf("") val list2 = if (test) null else listOf(null, "") val list3 = listOf("") val list4 = listOf(null, "") val list5 = if (test) null else mutableListOf("") val list6 = if (test) null else mutableListOf(null, "") val list7 = mutableListOf("") val list8 = mutableListOf(null, "") var list9 = list2?.filterNotNull() var list10 = list2 var list11 = list2?.filterNotNull() ?: emptyList() var list12 = list2 ?: emptyList() var list13 = list2?.filterNotNull()?.toMutableList() var list14 = list2?.toMutableList() var list15 = list2?.filterNotNull()?.toMutableList() ?: mutableListOf() var list16 = list2?.toMutableList() ?: mutableListOf() 

When you write code, do not want to declare the extra field as nullable, so that you don’t write later? and?:, and most operations lead to immutable collections. As a result, the code declares the narrowest states, which gives more stringent contracts and reduces the complexity of the program.

Safe support for other language features


  1. Naturally, you want to move away from the constructor without parameters and setters for the fields, since the constructor without parameters will create the non-compiled state of the object - null user login, for example.
  2. Language encourages the absence of local variables - there are no intermediate states.
  3. Cheaper DTO - data class . So we can weaken control over states when we transfer object in UI, without weakening contracts of model.
  4. Cheaper method overloading - the default settings - there is no temptation to write a bunch of setters.

    Example
     data class Schedule( val delay: Int, val delayTimeUnit: TimeUnit = TimeUnit.SECONDS, val rate: Int? = null, val rateTimeUnit: TimeUnit = TimeUnit.SECONDS, val run: () -> Unit ) fun usage() { Schedule(1) { println("Delay for second") } Schedule(100, TimeUnit.MILLISECONDS) { println("Delay for 100 milliseconds") } Schedule(1, rate = 1) { println("Delay for second, repeat every second") } } 


What may appear


  1. Inline classes / Value classes. Allows you to make wrapper classes around primitives, while compiling without this class. It will be possible, for example, to make two types of lines: login and email, which will not be cast to each other. Heard about it on jpoint.
  2. Truly immutable data. Support at the level of syntax object variability. The immutable object cannot contain references to non-immutable and somehow change them. Took the third place in voting for new language features.

4. Concise (examples from my project, almost as is)


There is no limit "one file - one class"


Working with spring data looks like this for me (all in one file):
 @Repository interface PayerRepository : CrudRepository<Payer, Int> { fun findByApprenticeId(id: Int): List<Payer> } @Repository interface AttendanceRepository : CrudRepository<LessonAttendance, LessonAttendance.ID> { fun findByDateBetween(from: Date, to: Date): List<LessonAttendance> } fun AttendanceRepository.byMonth(month: Date): List<LessonAttendance> { val from = month.truncateToMonth() val to = month.addMonths(1).subtractDays(1) return findByDateBetween(from, to) } // 10  inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T { return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName) } 


Extensions.


We straighten the call to DateUtils
Kotlin

 fun isJournalBlocked(date: Date, forMonth: Date) = forMonth <= date.subtractMonths(1).subtractDays(10) //   20  fun Date.subtractMonths(amount: Int): Date = DateUtils.addMonths(this, -amount) //   8  fun Date.subtractDays(amount: Int): Date = DateUtils.addDays(this, -amount) 

Java

 public boolean isJournalBlocked(Date date, Date forMonth) { return date.compareTo(DateUtils.addDays(DateUtils.addMonths(forMonth, -1), -1)) <= 0; } 


It was necessary to store the sequence of changes of some parameters, for this I wrote the History interface for storing such a parameter and the extension SortedMap <Date, out History> to align the content after the changes:

Implementation
 interface History<out T> { val begin: Date var end: Date? fun historyOf(): T fun containsMonth(date: Date): Boolean { val month = date.truncateToMonth() return begin <= month && (end == null || month < end) } } fun <T> SortedMap<Date, out History<T>>.fix() { removeRepeatedNeighbors() val navigableMap = TreeMap<Date, History<T>>(this) values.forEach { it.end = navigableMap.higherEntry(it.begin)?.value?.begin } } private fun <T> SortedMap<Date, out History<T>>.removeRepeatedNeighbors() { var previousHistory: T? = null for (history in values.toList()) { if (history.historyOf() == previousHistory) { remove(history.begin) } else { previousHistory = history.historyOf() } } } //usage: fun setGroup(from: Date, group: ClassGroup) { val history = GroupHistory( this, group, from.truncateToMonth(), null ) groupsHistory[history.begin] = history groupsHistory.fix() this.group = groupsHistory.getValue(groupsHistory.lastKey()).group } 


Collection operations


Example 1
Kotlin

 val apprentices: List<ApprenticeDTO> = apprenticeRepository.findAll() .map(::ApprenticeDTO) .sortedWith(compareBy({ it.lastName }, { it.firstName })) 

Java

 List<ApprenticeDTO> apprentices = StreamSupport.stream( apprenticeRepository.findAll().spliterator(), false ).map(ApprenticeDTO::new) .sorted(Comparator.comparing(ApprenticeDTO::getLastName) .thenComparing(Comparator.comparing(ApprenticeDTO::getFirstName))) .collect(Collectors.toList()); 


Example 2
Kotlin

 val attendances: Map<Pair<Date, Int>, Int> attendances = attendanceRepository .byMonth(month) .groupBy { it.date to it.group.id } .mapValues { it.value.count() } .toMap() 

Java

 Map<Pair<Date, Integer>, Integer> attendances = attendanceRepository .byMonth(month) .stream() .collect(Collectors.groupingBy((it) -> new Pair<>(it.getDate(), it.getGroup().getId()))) .entrySet() .stream() .map(entry -> new Pair<>(entry.getKey(), entry.getValue().size())) .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); 


There are filterNot (more often you can use method reference), separate first and firstOrNull, etc ... And if something is missing, you add your extension (for example, I added a sum to the BigDecimal sheet).

Lazy


When working with jsf, this is just a salvation. Jsf often jerks the same field (and I go to the base for it), and in the case of a table with sorting, it expects to return exactly the same object as it did last time. And most importantly, lazy is very easy to remove / insert.

Smart cast + sealed class


Example
Kotlin

 fun rentForGroup(month: Date, group: ClassGroup): Int { val hall = group.hall val hallRent = hall.rent(month) return when (hallRent) { is Monthly -> hallRent.priceForMonth() / hall.groups(month).size is PercentOfRevenue -> hallRent.priceForMonth(creditForGroup(month, group)) is Hourly -> hallRent.priceForLessons(group.monthLessons(month)) } } 

Java

 public int rentForGroup(Date month, ClassGroup group) { Hall hall = group.getHall(); Rent hallRent = hall.rent(month); if (hallRent instanceof Monthly) { return ((Monthly) hallRent).priceForMonth() / hall.groups(month).size(); } else if (hallRent instanceof PercentOfRevenue) { return ((PercentOfRevenue) hallRent).priceForMonth(creditForGroup(month, group)); } else if (hallRent instanceof Hourly) { return ((Hourly) hallRent).priceForLessons(group.monthLessons(month)); } else { throw new UnsupportedOperationException(); } } 


Inline functions


In Java, this is simply impossible to do (unless you add an explicit parameter with a class).
 inline fun <reified E : Throwable> assertFail(expression: () -> Unit) { try { expression() Assert.fail("expression must fail with ${E::class.qualifiedName}") } catch (e: Throwable) { if (e !is E) { throw e } } } @Test fun greenTest() { assertFail<ArrayIndexOutOfBoundsException> { arrayOf(1, 2)[3] } } 

 inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T { return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName) } 


String literals.


The difference is small, but the chances of being mistaken are much less, and you can copy-paste from Ineta without a headache.

regexp
Kotlin

 val email = """^([_A-Za-z0-9-+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,}))?$""" 

Java

 String email = "^([_A-Za-z0-9-+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,}))?$" 


In addition, very nice templates .

Aftertaste


I have only one project at Kotlin, not counting small crafts. So I can safely say one thing: Kotlin, Spring and the DDD elements support each other perfectly. If you write on Kotlin as Java, you will feel only syntactic sugar (which is already nice), but if you abandon the classic bins, into which anyone can insert anything (which means there are practically no restrictions on states), then Kotlin will flourish.

UPD
Kotlin aftertaste, part 1
Aftertaste from Kotlin, part 3. Korutiny - we divide the processor time

Source: https://habr.com/ru/post/337002/


All Articles