JavaRush /Courses /Kotlin SELF /Kotlin on the JVM: how to call Java APIs

Kotlin on the JVM: how to call Java APIs

Kotlin SELF
Level 29, Lesson 0
Available

1. Kotlin/JVM and the Java standard library: what is actually happening

When you write Kotlin code for a “regular” console application, you are almost certainly writing Kotlin/JVM: your code is compiled into bytecode and runs on the JVM (Java Virtual Machine). This means that next to your code there is always a JDK (Java Development Kit) with the standard java.* packages: strings, collections, system functions, UUIDs, and a lot more. Essentially, Kotlin does not “replace” the Java world—it lives alongside it in a very pragmatic way and calmly uses ready-made tools from the JDK.

So that the scale does not feel scary, remember a simple idea: for Kotlin, the Java API is just “another library,” only very large and already installed almost everywhere there is a JVM.

Java APIs in Kotlin look “normal”: dots, parentheses, and no rituals

If you expect that to call Java code you need to open a portal to hell, say a spell, and sacrifice a couple of parentheses—you will be pleasantly disappointed. In Kotlin, calling Java APIs looks as ordinary as it gets: create an object via a “type call” and call methods via the dot.

It is useful to build intuition here: a Java class is just a type that you can import, instantiate via a constructor, and use like any other object. A Java method is just a method: obj.method(...). And a Java “static method” is just a call through the class name: Type.staticMethod(...). Yes, it is that boring—and that is the whole point.

2. Packages and import: how Kotlin finds Java classes

When a project is small, you want to write “short and without extra words.” But the compiler is not telepathic: if you use the class UUID, it needs to understand which UUID (from which package) you mean. So we have two main ways to refer to a Java type: via import or via the fully qualified name.

Recall the idea of an address: a package is like a “street,” a class is a “house.” If you did not write import, you can “walk there” by the full address: java.util.UUID. And if you did write import, then you can speak in short: UUID. This is the same principle you have already used with your own functions and packages; it is just that now the packages start with java..

Approach #1: fully qualified name

A fully qualified name is convenient when you are making a small example, trying something in a sandbox, or simply do not want to clutter the file with imports yet. This helps beginners in particular: you see the entire path and more quickly connect “class → package” in your head.

fun main() {
    val id = java.util.UUID.randomUUID().toString()
    println(id) // for example: 6c6f3a7a-0a51-4b4e-9a61-1dbd7c2f0fb2
}

Here java.util.UUID is a Java class from the JDK. The randomUUID() method is “static” (called via the class name), and toString() turns the UUID into a string.

Approach #2: import

import makes code cleaner and shorter, especially when the class is used many times. In normal projects this is the primary approach: you import the types you need and work calmly with short names.

import java.util.UUID

fun main() {
    val id = UUID.randomUUID().toString()
    println(id) // for example: 1c0d6e2a-9d0d-4e1a-9e84-63aaf1c30a24
}

If IntelliJ IDEA highlights UUID in red, it usually means one of two things: either you forgot import, or you misspelled the name.

3. Constructors and Java static methods: basic practice

When you see a Java class, you do not need to think “this is different.” In Kotlin, creating an object via a constructor looks like calling a function: Type(...). This applies to Kotlin classes (later in the course) and to Java classes (already now). The good news: the syntax is the same.

The only important thing to remember is that many Java objects are mutable by style (modifiable): you create an object and then “add more” data into it via methods like add, append, put, and so on. Kotlin can do this too (for example, MutableList); today it is just especially noticeable.

Example: StringBuilder() as a Java class

You may have already seen StringBuilder as a tool for building long strings. On the JVM it is a class from the Java world, and Kotlin works with it without any issues.

fun main() {
    val sb = StringBuilder()
    sb.append("Kotlin ")
    sb.append("and Java are friends.")
    println(sb.toString()) // Kotlin and Java are friends.
}

Notice the style: append modifies the sb object “in place.” This is normal and expected for Java APIs.

Java static methods: call through the type name

Java has the concept of static: the method belongs not to a specific object, but to the class itself. Kotlin generally does not force you to think about static as a separate entity: you simply call the method via the type name—and that is it.

Why does this matter? Because many utilities in the JDK are built exactly like this: System.getenv(...), System.getProperty(...), UUID.randomUUID(), etc. This is a very common pattern in real applications: “give me system information,” “generate an identifier,” “take the current settings.”

Example: System.getProperty(...) — learn something about the environment

System is a Java class that helps you interact with the outside world (operating system, JVM properties, environment). For a CLI program, this is useful at least for a friendly “hello, user.”

fun main() {
    val user = System.getProperty("user.name")
    println("Hello, $user!") // Hello, alex!
}

There are lots of properties. Two commonly used ones: "user.name" and "user.dir" (the process working directory). We are not talking about files today, but the directory as a string can already be useful for diagnostics.

Mini table: “static” vs “regular” method

To stop your brain from getting confused, it helps to keep this table in mind:

What we do What it looks like Example
Call a method via the class
Type.method()
UUID.randomUUID()
Create an object
Type(...)
StringBuilder()
Call a method via an object
obj.method()
sb.append("...")

This scheme is simple, but it covers 80% of “why does this not compile?!” at the beginning.

4. Java collections: ArrayList and “mutation is the norm”

By this point you are already confidently using Kotlin collections (List, MutableList, Map), and that is great. But on the JVM there is also a huge layer of code and libraries that use Java collections: ArrayList, HashMap, HashSet, and so on. Kotlin can work with them directly. Moreover, on the JVM this is so common that sometimes you will not even notice where Kotlin ends and Java begins.

The key practical difference for a beginner: Java collections are designed to be mostly mutable, and that is considered normal. So add will mutate the list rather than return a new one.

Example: create an ArrayList and add elements

import java.util.ArrayList

fun main() {
    val xs = ArrayList<Int>()
    xs.add(10)
    xs.add(20)
    println(xs.size) // 2
}

Compare this with the Kotlin style listOf(10, 20): there the list is read-only (by reference), while here it is “live” and mutable.

Why touch Java collections at all if Kotlin collections exist?

In this lecture we will not dive into compatibility nuances (that is a separate big topic). But the practical reason is simple: a huge number of APIs in the JVM world historically accept and return Java collections. And if you want to use libraries confidently, you need to at least not be afraid of ArrayList.

5. Mini project: “Text Analyzer” gets more grown-up

Up to now we have been writing fairly self-contained programs: enter text → count → print a report. Today we will take a small but very “real” step: we will add service information to the report—information you typically want to see in logs and reports. For example: a run identifier, the user name, and the working directory. All of this will be provided by Java APIs from the JDK—for free, with no dependencies and no magic.

To tie the examples into one whole, we will assume we already have a simplified text analyzer (in the style of the lectures about pipelines): tokenization, frequency counting, printing top-N.

Generate a run identifier via UUID.randomUUID()

In long scenarios it is useful to be able to say: “This report belongs to a specific program run.” Especially if a user sends you a screenshot or a piece of a log. A UUID is just a unique string that is easy to generate and almost impossible to accidentally repeat.

Let us make a function newRunId() that returns a string. Inside, we use the Java class UUID and its static method.

import java.util.UUID

fun newRunId(): String {
    return UUID.randomUUID().toString()
}

fun main() {
    println(newRunId()) // for example: 9a9dbb6c-2c92-40bb-9c5c-2ee7d4c8d77b
}

We are not discussing “why it is designed this way” yet—what matters is that you saw that a Java static method in Kotlin is called completely naturally.

Get environment info via System.getProperty(...)

When a program runs on different machines, sometimes you want to understand: “Where am I even running?”. This is especially relevant when a user says “it does not work for me” and you say “it works for me.” Simple system properties help you at least start the conversation with something concrete: which user, which launch directory.

Let us make a small function that collects a couple of values.

fun runtimeInfo(): String {
    val user = System.getProperty("user.name")
    val dir = System.getProperty("user.dir")
    return "user=$user, dir=$dir"
}

fun main() {
    println(runtimeInfo()) // user=alex, dir=/Users/alex/projects/analyzer
}

There is still nothing “Kotlin-specific” here: these are normal Java API calls and normal Kotlin strings.

Build a report header via StringBuilder

When you print a report to the console, you quickly want to format multi-line text nicely: a title, metadata, then the analysis result. String concatenation with + works, but for large text it is simpler and clearer to build everything in a StringBuilder and return the final string. This is a classic approach on the JVM (and yes, it is a Java class).

Let us make a function buildHeader(...) that returns a multi-line header.

fun buildHeader(runId: String, info: String): String {
    val sb = StringBuilder()
    sb.appendLine("=== Text Analyzer Report ===")
    sb.appendLine("runId: $runId")
    sb.appendLine("env:   $info")
    return sb.toString()
}

The appendLine method appends a line and a newline. If you do not have it (depending on your environment), you can replace it with append("...\n"); the idea is the same.

Where import is not needed—and why that is normal

Beginners are often confused that we import some Java things, but not others. For example, we did not import System. This is not because “we felt like it,” but because on the JVM some packages are imported automatically (in particular, java.lang). But at the practical level the rule is simple: if the compiler cannot see a name, add import (or temporarily write the fully qualified class name) and move on.

For practice, you can rewrite System.getProperty as java.lang.System.getProperty—it will work the same way.

Use Java ArrayList as a “line buffer” before final output

Sometimes it is convenient to build a report not as one string, but as a list of lines: add a line—then join. This is the “buffering” approach, and it is extremely common in older Java code. We will do it on purpose so you can feel the Java API style: create a mutable structure, fill it, then transform it.

import java.util.ArrayList

fun buildLines(): ArrayList<String> {
    val lines = ArrayList<String>()
    lines.add("line 1")
    lines.add("line 2")
    return lines
}

fun main() {
    println(buildLines().size) // 2
}

The point is not that this is “better than Kotlin.” The point is that this is often how it is in APIs you will run into, and you need to feel confident.

Put it all together: a mini main for our analyzer

Now let us glue the ideas into one small scenario: generate a runId, get runtimeInfo, print the header, then read a line of text and print the length (as the simplest “analysis stub”). We will not rewrite tokenization and frequencies—they are already there from previous days.

import java.util.UUID

fun main() {
    val runId = UUID.randomUUID().toString()
    val info = runtimeInfo()
    println(buildHeader(runId, info))

    val text = readln()
    println("chars: ${text.length}") // chars: 12
}

If you are thinking “wait, runtimeInfo() and buildHeader() were somewhere above, but they are not here”—that is correct: this is one project; the functions are in the same file or imported from another. We keep evolving the application gradually, just like we did before.

6. Diagram: Kotlin → JVM → JDK

After the first encounters with Java APIs, it can feel like Kotlin “suddenly became Java.” In reality, it just stands on the shoulders of the JVM and uses the JDK as the standard toolkit. It is convenient to visualize this as a simple block diagram: you write Kotlin, it gets compiled, and at runtime the JDK classes are right there, and you can call them freely.

flowchart TD
    A[Kotlin code] --> B[Kotlin compiler]
    B --> C[JVM bytecode]
    C --> D[JVM]
    D --> E[JDK / java.* classes]
    D --> F[Your application: text analyzer]
    E --> F

This diagram matters psychologically: you do not “add Java separately.” You are already inside the JVM world—you just started consciously taking useful things from it.

7. Typical mistakes when calling Java APIs from Kotlin

Mistake #1: forgot import and decided “Kotlin does not have that class.”
Most often the class exists; the compiler just does not know where to get it from. If you see Unresolved reference, try writing the fully qualified name first (java.util.UUID), then press Alt+Enter in the IDE and add import. This is the fastest way to stop guessing and start moving forward.

Mistake #2: mixed up calling a static method vs an instance method.
If you write UUID.toString() and expect a string, the compiler will not be happy: toString() is an instance method, not a class method. The correct chain is: first get an object (for example, UUID.randomUUID()), then call its method (.toString()).

Mistake #3: expect Java collections to “behave like a read-only List.”
If you created an ArrayList and passed it somewhere, other code can modify it—and that is not a bug, it is the Java style. In practice, this means you need to be more mindful of mutability with Java objects: if you need a snapshot of the data, you typically make a copy (for now, it is enough to understand the issue).

Mistake #4: try to “fix” the problem by copy-pasting fully qualified names everywhere.
Fully qualified names are convenient for a one-off example, but they turn real code into a wall of text. If you use a class more than once, it is better to just add import. The code will become shorter, and you will start seeing the program logic rather than house addresses on java.util street.

Mistake #5: think that Java APIs are “not for Kotlin,” and avoid them even when they simplify life.
Paradoxically, a beginner sometimes refuses to touch System or UUID on principle, because “that is Java.” On the JVM, it is not “foreign,” it is the standard library of the runtime environment. If a task is easier to solve via the JDK—use the JDK and do not torture yourself (and the future reader of your code).

1
Task
Kotlin SELF, level 29, lesson 0
Locked
Run identifier
Run identifier
1
Task
Kotlin SELF, level 29, lesson 0
Locked
Environment profile
Environment profile
1
Task
Kotlin SELF, level 29, lesson 0
Locked
Chat business card
Chat business card
1
Task
Kotlin SELF, level 29, lesson 0
Locked
List cleanup
List cleanup
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION