Is Kotlin is the better Java? Mar 2017
Kotlin is around some time and I here want to show some practical experiences from converting one of my existing Java 8 projects to Kotlin. Is it worth the effort, what are the benefits?
As an example I take my stock prices charting application, which is based on JEE and Wildfly Swarm. Details of the old project you can find here.
The Java class to be converted is this one (full screen source):
@Singleton public class ChartBuilder { @Inject private DateTimeProvider dateTimeProvider; public ChartDataDTO createOne(Stock stock, int points, Optional<LocalDate> since, boolean percentages) { ChartDataDTO dto = new ChartDataDTO(stock.getName(), dateTimeProvider.now()); int dayPoints, instantPoints; if (stock.getInstantPrices().size() > 0) { dayPoints = (int) (points * 0.8); instantPoints = (int) (points * 0.2); } else { dayPoints = points; instantPoints = 0; } final LocalDateTime firstInstantPrice = stock.getInstantPrices(). stream().min(comparing(InstantPrice::getTime)). map(InstantPrice::getTime).orElse(LocalDateTime.MAX); Predicate<DayPrice> isAfterSinceAndBeforeInstantPrices = (DayPrice dp) -> { boolean isAfterSince = (!since.isPresent()) ? true : dp.getDay().isAfter(since.get()) || dp.getDay().isEqual(since.get()); return isAfterSince && dp.getDay().plus(1, ChronoUnit.DAYS).atStartOfDay().isBefore(firstInstantPrice); }; List<ChartItemDTO> dayItems = new ArrayList<>(); stock.getDayPrices().stream(). filter(isAfterSinceAndBeforeInstantPrices). sorted(comparing(DayPrice::getDay)). forEach(dp -> dayItems.add(new ChartItemDTO.Builder(). setDateTime(dp.getDay().atStartOfDay()). setMinLong(dp.getMin()). setMaxLong(dp.getMax()). setAverageLong(average(dp.getMin(), dp.getMax())). setInstantPrice(false). build()) ); aggregate(dayItems, dayPoints); List<ChartItemDTO> instantItems = new ArrayList<>(); stock.getInstantPrices().stream(). sorted(comparing(InstantPrice::getTime)). forEach(ip -> instantItems.add(new ChartItemDTO.Builder(). setDateTime(ip.getTime()). setMinLong(ip.getMin()). setMaxLong(ip.getMax()). setAverageLong(average(ip.getMin(), ip.getMax())). setInstantPrice(true). build()) ); aggregate(instantItems, instantPoints); dto.getItems().addAll(dayItems); dto.getItems().addAll(instantItems); if (percentages) transformIntoPercentages(dto.getItems()); return dto; } public ChartDataDTO createAggregated(List<Stock> stocks, List<Integer> stockWeights, int points, Optional<LocalDate> since) { if (stocks.size() < 2) throw new RuntimeException("at least 2 stocks need to be aggregated"); if (stocks.size() != stockWeights.size()) throw new RuntimeException("different amounts of stocks and stock weights"); Map<LocalDate, List<Double>> avgPercentsPerDate = avgPercentsPerDate(stocks, since); List<ChartItemDTO> aggregatedItemList = new ArrayList<>(); for (LocalDate date : avgPercentsPerDate.keySet()) { List<Double> avgPercents = avgPercentsPerDate.get(date); double aggregatedAvgPercent = 0; for (int i = 0; i < avgPercents.size(); i++) { double avg = avgPercents.get(i); aggregatedAvgPercent += avg * stockWeights.get(i) / 100d; } long aggregatedAvgPercentLong = round(aggregatedAvgPercent * 100d * 100d); ChartItemDTO itemDto = new ChartItemDTO.Builder(). setDateTime(date.atStartOfDay()). setAverageLong(aggregatedAvgPercentLong). setInstantPrice(false). build(); aggregatedItemList.add(itemDto); } aggregatedItemList.sort(comparing(ChartItemDTO::getDateTime)); aggregate(aggregatedItemList, points); ChartDataDTO dto = new ChartDataDTO("aggregated", dateTimeProvider.now()); dto.getItems().addAll(aggregatedItemList); return dto; } public AllInOneChartDto createAllInOne(List<Stock> stocks, int points, LocalDate since) { Map<LocalDate, List<Double>> avgPercentsPerDate = avgPercentsPerDate(stocks, Optional.of(since)); List<AllInOneChartItemDto> items = new ArrayList<>(); for (LocalDate date : avgPercentsPerDate.keySet()) { List<Double> avgs = avgPercentsPerDate.get(date); AllInOneChartItemDto item = new AllInOneChartItemDto(); item.setDateTime(date.atStartOfDay()); for (int i = 0; i < avgs.size(); i++) { double avg = (avgs.get(i) != null) ? avgs.get(i) : 0d; long avgLong = round(avg * 100 * 100); item.addAverageLong(StocksEnum.of(stocks.get(i).getSymbol()), avgLong); } items.add(item); } items.sort(comparing(AllInOneChartItemDto::getDateTime)); // TODO aggregation to max points, if needed AllInOneChartDto dto = new AllInOneChartDto(); dto.setItems(items); return dto; } private Map<LocalDate, List<Double>> avgPercentsPerDate(List<Stock> stocks, Optional<LocalDate> since) { // create several item lists after since date List<Map<LocalDate, Long>> avgPerDateList = new ArrayList<>(); for (Stock stock : stocks) { Map<LocalDate, Long> avgPerDate = new HashMap<>(); stock.getDayPrices().stream(). filter(dp -> !since.isPresent() || dp.getDay().isAfter(since.get())). forEach(dp -> avgPerDate.put(dp.getDay(), dp.getAverage()) ); avgPerDateList.add(avgPerDate); } // find intersection dates Set<LocalDate> intersectionDates = new HashSet<>(); avgPerDateList.get(0).keySet().stream().forEach( dayPrice -> intersectionDates.add(LocalDate.ofYearDay(dayPrice.getYear(), dayPrice.getDayOfYear()))); avgPerDateList.stream().skip(1).forEach(items -> intersectionDates.retainAll(items.keySet())); if (intersectionDates.size() == 0) throw new RuntimeException("no intersection found"); // find first averages, needed for calculating percentages LocalDate firstDate = intersectionDates.stream().min(LocalDate::compareTo).get(); List<Long> firstAverages = new ArrayList<>(); avgPerDateList.stream().forEach(avgPerDate -> firstAverages.add(avgPerDate.get(firstDate))); if (firstAverages.contains(null)) throw new RuntimeException("missing first average for percentage calculation"); Map<LocalDate, List<Double>> avgPercentsPerDate = new HashMap<>(); for (LocalDate date : intersectionDates) { List<Double> averages = new ArrayList<>(); for (int i = 0; i < avgPerDateList.size(); i++) { Long avg = avgPerDateList.get(i).get(date); if (avg == null) avg = 0L; long first = firstAverages.get(i); double avgPercent = 1d * (avg - first) / first; averages.add(avgPercent); } avgPercentsPerDate.put(date, averages); } return avgPercentsPerDate; } private void aggregate(List<ChartItemDTO> items, int points) { int index = 0; while (items.size() > points) { if (index + 1 < items.size()) { aggregate(items.get(index), items.get(index + 1)); items.remove(index + 1); index += 1; } else { index = 0; } } } private void aggregate(ChartItemDTO item2Update, ChartItemDTO item2Merge) { item2Update.setMaxLong(max(item2Update.getMaxLong(), item2Merge.getMaxLong())); item2Update.setMinLong(min(item2Update.getMinLong(), item2Merge.getMinLong())); item2Update.setAverageLong(average(item2Update.getAverageLong(), item2Merge.getAverageLong())); long diff = item2Update.getDateTime().until(item2Merge.getDateTime(), ChronoUnit.SECONDS); LocalDateTime medium = item2Update.getDateTime().plus(diff / 2, ChronoUnit.SECONDS); item2Update.setDateTime(medium); } private Long average(Long l1, Long l2) { if (l1 == null) return l2; if (l2 == null) return l1; return round((l1 + l2) / 2d); } private Long max(Long l1, Long l2) { if (l1 == null) return l2; if (l2 == null) return l1; return Math.max(l1, l2); } private Long min(Long l1, Long l2) { if (l1 == null) return l2; if (l2 == null) return l1; return Math.min(l1, l2); } private void transformIntoPercentages(List<ChartItemDTO> items) { if (items.isEmpty()) return; ChartItemDTO first = items.get(0); double firstAvg; if (first.getAverageLong() != null) { firstAvg = first.getAverageLong().doubleValue(); } else if (first.getMinLong() != null && first.getMaxLong() != null) { firstAvg = (first.getMinLong().doubleValue() + first.getMaxLong().doubleValue()) / 2d; } else { throw new RuntimeException("no first element for calculating percentages"); } items.stream().forEach(item -> { item.setMinLong(percent(firstAvg, item.getMinLong())); item.setMaxLong(percent(firstAvg, item.getMaxLong())); item.setAverageLong(percent(firstAvg, item.getAverageLong())); }); } private long percent(double first, Long value) { if (first == 0d || value == null || value == 0d) { return 0; } double percent = (value.doubleValue() - first) / first; long percentLong = round(percent * 100 * 100); return percentLong; } }
The probably most convenient approach is to start with IntelliJ's "Convert Java File to Kotlin file":
This provides the basic syntax translation fairly well. Some small compile problems remain to be solved and afterwards you have a Kotlin class looking like this
Obviously manual work is required to have Kotlin play it's advantages:
- Instead of java.util.Optional Kotlin has built in nullable AND not nullable types. Together with the concise syntax to work with nullable types I would rate this as one of the greatest advantages over Java. Null pointer exceptions can effectively be avoided, while not being forced to use Java's bloated Optional-Syntax. Java's Optional anyway only serves as a plaster since the Optional itself can be null.
- As another syntactical advantage one can recognize in the code below that if statements as well as function definitions can be expressions. No curly braces and return statements are needed.
- Unlike as with Java 8 one will NOT find any ...stream()... in the code below, which feels more natural. In general map reduce in Kotlin is more powerful. At least in Java I would not know how to work with an index when mapping, so previously with Java I used a good old for-loop instead.
- Kotlin pushes you in the direction of immutable state. While in daily work most Java developers do not use the final keyword extensively it is no burden to use the val keyword. Kotlin's List is immutable as well. As you might recognize my example would require more refactoring to get rid of all vars and MutableLists.
- While in my Java implementation I used a builder for ChartItemDTO with Kotlin there is no necessity for this. Instead you can use named parameters when calling the constructor.
- Accessing lists or maps with .get(i) can be replaced with [i] in Kotlin. Only a small thing of course, but a nice one.
- And last but not least modern languages do not require you to write getters and setters, or even let your IDE generate them for you, see the DTO class here.
The above Java class converted to Kotlin (full screen source):
@Singleton class ChartBuilder { @Inject lateinit private var dateTimeProvider: DateTimeProvider fun createOne(stock: Stock, points: Int, sinceParam: Optional<LocalDate>, percentages: Boolean): ChartDataDTO { val since: LocalDate? = sinceParam.orElse(null) val dto = ChartDataDTO(stock.name, dateTimeProvider.now()) val dayPoints = if (stock.instantPrices.isNotEmpty()) (points * 0.8).toInt() else points val instantPoints = if (stock.instantPrices.isNotEmpty()) (points * 0.2).toInt() else 0 val firstInstantPrice = stock.instantPrices.minBy { it.time }?.time ?: LocalDateTime.MAX val isAfterSinceAndBeforeInstantPrices = { dp: DayPrice -> val isAfterSince = since == null || dp.day.isAfter(since) || dp.day.isEqual(since) isAfterSince && dp.day.plus(1, ChronoUnit.DAYS).atStartOfDay().isBefore(firstInstantPrice) } val dayItems = stock.dayPrices.filter(isAfterSinceAndBeforeInstantPrices).sortedBy { it.day }.map { dp -> ChartItemDTO(dateTime = dp.day.atStartOfDay(), minLong = dp.min, maxLong = dp.max, averageLong = average(dp.min, dp.max), instantPrice = false) }.toMutableList() aggregate(dayItems, dayPoints) val instantItems = stock.instantPrices.sortedBy { it.time }.map { ip -> ChartItemDTO(dateTime = ip.time, minLong = ip.min, maxLong = ip.max, averageLong = average(ip.min, ip.max), instantPrice = true) }.toMutableList() aggregate(instantItems, instantPoints) dto.items.addAll(dayItems) dto.items.addAll(instantItems) if (percentages) transformToPercentages(dto.items) return dto } fun createAggregated(stocks: List<Stock>, stockWeights: List<Int>, points: Int, sinceParam: Optional<LocalDate>): ChartDataDTO { val since: LocalDate? = sinceParam.orElse(null) if (stocks.size < 2) throw RuntimeException("at least 2 stocks need to be aggregated") if (stocks.size != stockWeights.size) throw RuntimeException("different amounts of stocks and stock weights") val avgPercentsPerDate: Map<LocalDate, List<Double>> = avgPercentsPerDate(stocks, since) val aggregatedItemList = avgPercentsPerDate.keys.map { date -> val averagePercents = avgPercentsPerDate[date] ?: listOf() val aggregatedAvgPercent = averagePercents.mapIndexed { index, percent -> percent * stockWeights[index] / 100.0 }.sum() val aggregatedAvgPercentLong = round(aggregatedAvgPercent * 100.0 * 100.0) ChartItemDTO(dateTime = date.atStartOfDay(), averageLong = aggregatedAvgPercentLong, instantPrice = false) }.sortedBy { it.dateTime }.toMutableList() aggregate(aggregatedItemList, points) val dto = ChartDataDTO("aggregated", dateTimeProvider.now()) dto.items.addAll(aggregatedItemList) return dto } fun createAllInOne(stocks: List<Stock>, points: Int, since: LocalDate): AllInOneChartDto { val avgPercentsPerDate = avgPercentsPerDate(stocks, since) val items = avgPercentsPerDate.keys.map { date -> val averagePercents = avgPercentsPerDate[date] ?: listOf() val item = AllInOneChartItemDto() item.dateTime = date.atStartOfDay() for ((index, avg) in averagePercents.withIndex()) { val avgLong = round(avg * 100.0 * 100.0) val symbol = StocksEnum.of(stocks[index].symbol) item.addAverageLong(symbol, avgLong) } item }.sortedBy { it.dateTime }.toMutableList() // TODO aggregation to max points, if needed val dto = AllInOneChartDto() dto.items = items return dto } private fun avgPercentsPerDate(stocks: List<Stock>, since: LocalDate?): Map<LocalDate, List<Double>> { // create several item lists after since date val avgPerDateList = stocks.map { it.dayPrices. filter { dp -> since == null || dp.day.isAfter(since) }. map { dp -> dp.day to dp.average }.toMap() } // find intersection dates val intersectionDates = mutableSetOf<LocalDate>() avgPerDateList[0].keys.forEach { dayPrice -> intersectionDates.add(LocalDate.ofYearDay(dayPrice.year, dayPrice.dayOfYear)) } avgPerDateList.forEachIndexed { index, items -> if (index != 0) intersectionDates.retainAll(items.keys) } if (intersectionDates.size == 0) throw RuntimeException("no intersection found") // find first averages, needed for calculating percentages val firstDate = intersectionDates.min() val firstAverages = avgPerDateList.map { avgPerDate -> avgPerDate[firstDate] } if (firstAverages.contains(null)) throw RuntimeException("missing first average for percentage calculation") val avgPercentsPerDate = intersectionDates.map { date -> val averages = avgPerDateList.mapIndexed { index, avgPerDate -> val avg = avgPerDate[date] ?: 0 val first = firstAverages[index] ?: 0 val avgPercent = 1.0 * (avg - first) / first avgPercent } date to averages }.toMap() return avgPercentsPerDate } private fun aggregate(items: MutableList<ChartItemDTO>, pointsNeeded: Int): Unit { fun aggregate(updateItem: ChartItemDTO, mergeItem: ChartItemDTO): Unit { updateItem.maxLong = max(updateItem.maxLong, mergeItem.maxLong) updateItem.minLong = min(updateItem.minLong, mergeItem.minLong) updateItem.averageLong = average(updateItem.averageLong, mergeItem.averageLong) val diff = updateItem.dateTime.until(mergeItem.dateTime, ChronoUnit.SECONDS) val medium = updateItem.dateTime.plus(diff / 2, ChronoUnit.SECONDS) updateItem.dateTime = medium } var index = 0 while (items.size > pointsNeeded) { if (index + 1 < items.size) { aggregate(items[index], items[index + 1]) items.removeAt(index + 1) index += 1 } else { index = 0 } } } private fun transformToPercentages(items: List<ChartItemDTO>) { if (items.isEmpty()) return val first = items[0] val firstAvg: Double = if (first.averageLong != null) first.averageLong!!.toDouble() else if (first.minLong != null && first.maxLong != null) (first.minLong!! + first.maxLong!!) / 2.0 else throw RuntimeException("no first element for calculating percentages") items.forEach { item -> item.minLong = percent(firstAvg, item.minLong) item.maxLong = percent(firstAvg, item.maxLong) item.averageLong = percent(firstAvg, item.averageLong) } } } private fun percent(first: Double, value: Long?): Long = if (first == 0.0 || value == null || value == 0L) 0 else { val percent = (value.toDouble() - first) / first round(percent * 100.0 * 100.0) } private fun average(l1: Long?, l2: Long?): Long? = if (l1 == null) l2 else if (l2 == null) l1 else round((l1 + l2) / 2.0) private fun max(l1: Long?, l2: Long?): Long? = if (l1 == null) l2 else if (l2 == null) l1 else Math.max(l1, l2) private fun min(l1: Long?, l2: Long?): Long? = if (l1 == null) l2 else if (l2 == null) l1 else Math.min(l1, l2)
From my statements above you might already conclude that I think Kolin certainly has relevant advantages over Java 8. Of course it would be difficult to answer your manager’s question regarding the concrete savings in terms of money when using Kotlin. But somehow I believe software is getting better with better languages.
So maybe you can say the following to your manager. With Kotlin you need only 172 lines of code as opposed to 234 lines of code with Java, as shown in the example above. This is a saving of 25%, meaning everything can be implemented in 75% of the time with Kotlin.