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":

convert to kotlin

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:

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.