1. How Java sees Kotlin on the JVM
Today it makes sense to flip the arrow: not “Kotlin calls Java”, but “Java calls Kotlin”. We won’t write Java code as a separate practice, but we’ll figure out how Java will see Kotlin code, and we’ll learn how to make a Kotlin API significantly more convenient for a Java consumer using two small annotations.
Why even think about the Java representation
Imagine you wrote neat Kotlin code: file-level functions, constants, report formatting. And then a colleague (or you, six months later) comes along and says: “Can I call this from Java?”. And that’s where a small drama starts: Java doesn’t know about many Kotlin conveniences and sees your code through the lens of the JVM. That’s why it’s important to understand which names and shapes you get in bytecode, and where you can slightly “sweeten” interop.
In our console program (let it be a mini expense tracker) we already have many utility functions: command parsing, string formatting, report preparation. This is a great candidate for a “small API” that Java code could theoretically use as well (for example, another module in your company that hasn’t psychologically moved to Kotlin yet — and that’s fine).
Why Java sees a weird SomethingKt
Up to this point, we’ve been calmly writing functions “just in a file”:
fun formatMoney(amount: Int): String = "$amount ₣ "
From Kotlin’s point of view, this is natural: a file is a normal place for utility functions. But on the JVM, things are arranged so that functions must “live” in some class. So the compiler creates a technical wrapper class for you: a file facade (it’s also often called the “file class”).
If your file is named ReportUtils.kt, then Java will usually see something like ReportUtilsKt and static methods inside it. That is, the Java call (roughly) would look like ReportUtilsKt.formatMoney(10).
This is not “bad” and not a “mistake”. It’s simply the reality of the JVM: it needs somewhere to put static methods, so it puts them into a class whose name you sometimes didn’t even plan to “export to the public”.
2. A human name for the file class: @file:JvmName
When you build a library, a utilities module, or simply code that someone will use from outside, it suddenly becomes important how the name looks in Java. Naming the API entry point ReportUtilsKt is tolerable, but it slightly “smells like the compiler”. Kotlin lets you fix this with a single annotation: @file:JvmName("...").
The key idea: we control what the JVM file class will be named to the outside world. At the same time, Kotlin code inside the project usually doesn’t suffer at all: in Kotlin you still call functions by their usual names.
It’s also important where to write file annotations. By Kotlin code style conventions, file annotations are placed after the file comment (if there is one) and before package, and they are separated by an empty line so it’s visually obvious that the annotation applies to the file itself, not the package.
Example of an “exported API” file
Suppose we want to gather in one place the functions that build report strings. Let’s create a file ExpenseApi.kt:
@file:JvmName("ExpenseApi")
package app.expenses
fun formatMoney(amount: Int): String {
return "$amount ₣ "
}
fun formatExpenseLine(title: String, amount: Int): String {
return "$title: ${formatMoney(amount)}"
}
Note two things.
First, @file:JvmName("ExpenseApi") is at the very top and sets the JVM name of the file class. This exact placement style for file annotations is considered correct and expected.
Second, we didn’t create any classes by hand. We’re still in the “pre-OOP” part of the course: only functions and collections.
What this looks like at the idea level
If you imagine how Java will see this file, the idea is:
flowchart TD
A["ExpenseApi.kt top-level fun"] --> B["JVM class (facade)"]
B --> C["Java calls static methods"]
B["JVM class name = ExpenseApi"] --> C
Without @file:JvmName the JVM class would almost certainly be named ExpenseApiKt (because the file is ExpenseApi.kt). With the annotation it becomes simply ExpenseApi.
And yes, this is exactly the case where “we’re not writing Java”, but we do it so that a Java colleague won’t have to see the Kt suffix every day and remember what it even is.
3. When a property becomes a field: @JvmField
Why val is usually not a “field”
When you write:
val appName: String = "Budget CLI"
in Kotlin it looks like a “variable/constant”. But on the JVM a property usually turns into:
1) a field (where the value is stored),
2) a getter method getAppName() (and for var also a setter setAppName(...)).
That is, by default Java interacts with a property via methods, not via direct field access. This works well with encapsulation and with the fact that Kotlin properties can have custom getters/setters.
And here an interop nuance appears: if you want Java to see your value as a “simple field” (for example, as a constant or as a public flag), then access through getX() can feel unnecessarily verbose, and sometimes it also clashes with code style.
@JvmField and applicability boundaries
Sometimes you really want: “let this be a public field, without getters and setters”. For that there is the annotation @JvmField.
Its meaning: export a Kotlin property as a JVM field, so that Java can access it directly, without getX().
It’s important to immediately fix the boundary: @JvmField makes sense mostly for the sake of a Java consumer. In Kotlin code you still write API_VERSION, DEFAULT_CURRENCY, and for a Kotlin developer almost nothing changes. But for Java, the access form changes.
Example: API version and the app “banner”
Let’s add a file InteropConstants.kt to the package app.expenses:
package app.expenses
import kotlin.jvm.JvmField
@JvmField
val API_VERSION: Int = 1
val banner: String
get() = "Expense CLI (api=$API_VERSION)"
There are intentionally two different cases here.
API_VERSION is a stored value; it has a backing field (i.e., a real storage location). It can be exported as a field.
banner is a computed property: it is “assembled” by the getter every time. It does not have a separate storage field, which means there’s simply nothing to “export as a field”. This is an important idea: @JvmField works only where a real field actually exists.
How Java will see it
| Kotlin declaration | What is usually generated on the JVM | What you get with @JvmField |
|---|---|---|
|
a field + getX() | a public field X (no getter) |
|
a field + getX() + setX(...) | a public field X (no accessors) |
|
only getX() | not possible (no backing field) |
This is not a “spoken agreement”, but a direct consequence of how Kotlin maps properties onto the JVM.
4. Mini-embedding into a CLI project: a Java-friendly API without magic
Now there’s an important moment. We won’t turn our project into a world-class library; we won’t discuss Gradle publishing and other grown-up words. But we will make the code look neat and “export-ready”: a separate API file, a clear JVM name, and a couple of values that can be read as fields.
Let our project be a console expense tracker that stores expenses in a list MutableList<Triple<String, Int, String>>, containing (category, amount, comment).
File with API functions: @file:JvmName
Let’s create ExpenseInterop.kt:
@file:JvmName("ExpenseInterop")
package app.expenses
fun makeExpense(category: String, amount: Int, note: String): Triple<String, Int, String> {
return Triple(category, amount, note)
}
fun expenseAmount(expense: Triple<String, Int, String>): Int {
return expense.second
}
Yes, these are simple functions, and a Kotlin developer could just write expense.second everywhere. But the point of an “API file” is that you form a surface that other languages and modules can use.
And yes, we place the file annotation exactly at the top of the file, before package. This is not just “how it’s done”; it really affects readability and what the annotation applies to.
File with “public values”: @JvmField
Let’s create ExpenseConfig.kt:
package app.expenses
import kotlin.jvm.JvmField
@JvmField
val DEFAULT_CURRENCY: String = "RUB"
@JvmField
val API_VERSION: Int = 1
fun formatMoney(amount: Int): String {
return "$amount $DEFAULT_CURRENCY"
}
In Kotlin this will be convenient both as a “config” and as a simple way to keep shared values in one place.
If Java code ever wants to know the currency or the API version, it won’t have to call getDEFAULT_CURRENCY() (and then explain to the team lead why a constant looks like a getter). It will be able to access the field.
Verify that the app still works
Let’s update main (a simplified fragment — we’re showing the idea, not assembling the whole 200-line project):
package app
import app.expenses.API_VERSION
import app.expenses.formatMoney
import app.expenses.makeExpense
fun main() {
val expense = makeExpense("food", 350, "lunch")
println("API v$API_VERSION") // API v1
println("Amount: ${formatMoney(expense.second)}") // Amount: 350 RUB
}
Notice: for Kotlin code it doesn’t matter at all that @JvmField and @file:JvmName are there. Kotlin code reads as usual. But at the same time, the JVM representation becomes cleaner for an external consumer.
5. Typical mistakes
In this topic, mistakes are especially tricky: code can look “logical” in Kotlin but be inconvenient (or even surprising) for the Java world. So it’s better to learn to recognize the typical rakes by sound right away.
Mistake #1: put @file:JvmName in the wrong place — for example, after package.
File annotations apply to the entire file, so they must appear before package. If you put them lower, the compiler won’t “guess what you meant” — it will honestly say that you can’t do that. Keep the simple rule: file annotation at the top of the file, then an empty line, then package. This order is directly reflected in the general Kotlin code style conventions.
Mistake #2: expect Java to see a top-level function “just by name”, without a class.
On the JVM, functions don’t float in the air: they will be methods on some class. If you don’t control the file class name, Java will almost certainly see the Kt suffix. That’s normal, but if you are building an API, it’s better to give it a readable name via @file:JvmName right away so you don’t live with technical details in the public interface.
Mistake #3: try to put @JvmField on a computed property.
If a property has no backing field, then “exporting a field” is physically impossible: there’s no storage location, only computation logic. This is a common trap because in Kotlin val banner get() = ... looks like a “variable”, but on the JVM it’s a pure method. In that case, either keep the normal getter (and Java will call getBanner()), or make it a stored value.
Mistake #4: apply @JvmField “just in case”, without understanding why.
@JvmField is not decoration and not a “Kotlin accelerator”. It’s an interop design tool. If your module won’t be used from Java, then most of the time you don’t need to turn properties into fields: getters/setters are a normal and safe form. The annotation is needed when you are sure you want to give Java simple direct access.
Mistake #5: mix “everything” in one file, and then try to give it a nice name for Java.
If you want to export an API surface, it’s useful to keep a separate file (or several) for such functions/values. Then @file:JvmName("ExpenseApi") becomes meaningful: it’s really an “entry point”. But if the file mixes main, parsing, reports, and a random utility trimAndNormalize(), then even a perfect JVM name won’t save you — the API will look like an attic where “everything is kind of useful, but it’s scary to walk”.