How do I use visibility modifiers (private, internal, protected, public) in Kotlin classes?

In Kotlin, visibility modifiers control where classes, properties, functions, and constructors can be accessed from.

The main visibility modifiers are:

  • public
  • private
  • protected
  • internal

If you do not specify a visibility modifier, Kotlin uses public by default.


1. public

public means the declaration can be accessed from anywhere.

class User {
    public val name: String = "Alice"

    public fun sayHello() {
        println("Hello, $name")
    }
}

Because public is the default, this is equivalent:

class User {
    val name: String = "Alice"

    fun sayHello() {
        println("Hello, $name")
    }
}

Use public when you want something to be part of the normal external API of a class.


2. private

Inside a class, private means the member can only be accessed inside that same class.

class BankAccount {
    private var balance: Double = 0.0

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun getBalance(): Double {
        return balance
    }
}

Usage:

fun main() {
    val account = BankAccount()

    account.deposit(100.0)

    println(account.getBalance())

    // Error:
    // println(account.balance)
}

Here, balance is hidden from outside code. The class controls access through deposit() and getBalance().

Private class members

class UserService {
    private fun validateName(name: String): Boolean {
        return name.isNotBlank()
    }

    fun createUser(name: String) {
        if (validateName(name)) {
            println("User created: $name")
        }
    }
}

validateName() is an implementation detail and cannot be called from outside UserService.


3. protected

protected means the member is visible inside:

  1. The class where it is declared
  2. Its subclasses

It is not visible to general outside code.

open class Animal {
    protected val name: String = "Animal"

    protected fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    fun bark() {
        println(name)
        makeSound()
        println("Woof!")
    }
}

Usage:

fun main() {
    val dog = Dog()

    dog.bark()

    // Error:
    // println(dog.name)

    // Error:
    // dog.makeSound()
}

Dog can access name and makeSound() because it inherits from Animal, but outside code cannot.

Important note about protected

In Kotlin, protected is mainly for class inheritance. Unlike Java, Kotlin does not make protected members visible to the whole package.


4. internal

internal means the declaration is visible within the same module.

A module is usually something like:

  • a Gradle source set
  • a Maven project
  • an IntelliJ IDEA module
  • a set of files compiled together
internal class InternalLogger {
    fun log(message: String) {
        println("LOG: $message")
    }
}

Code in the same module can use it:

fun main() {
    val logger = InternalLogger()
    logger.log("Application started")
}

But code from another module cannot access InternalLogger.

Use internal when you want something available across your project/module but not exposed as a public API to other modules.


Visibility in class members

You can apply visibility modifiers to properties and functions:

class Profile {
    public val username: String = "guest"
    private val passwordHash: String = "abc123"
    internal val accountId: Int = 42
    protected val role: String = "user"

    public fun showUsername() {
        println(username)
    }

    private fun checkPassword() {
        println(passwordHash)
    }
}

However, protected only makes sense in classes that may be inherited from:

open class BaseController {
    protected fun authorize() {
        println("Checking permissions")
    }
}

class UserController : BaseController() {
    fun getUser() {
        authorize()
        println("Returning user")
    }
}

Visibility for constructors

You can also control constructor visibility.

Private constructor

Useful when you want to restrict object creation.

class DatabaseConnection private constructor() {
    companion object {
        fun create(): DatabaseConnection {
            return DatabaseConnection()
        }
    }
}

Usage:

fun main() {
    val connection = DatabaseConnection.create()

    // Error:
    // val direct = DatabaseConnection()
}

Visibility for property setters

A common Kotlin pattern is to expose a property for reading but restrict writing.

class Counter {
    var count: Int = 0
        private set

    fun increment() {
        count++
    }
}

Usage:

fun main() {
    val counter = Counter()

    counter.increment()
    println(counter.count)

    // Error:
    // counter.count = 10
}

Here:

  • count can be read from outside the class
  • count can only be changed inside the class

You can also use protected set or internal set:

open class Document {
    var status: String = "draft"
        protected set
}

Subclasses can change status, but outside code cannot.


Top-level visibility

Kotlin also allows functions, properties, and classes outside classes.

private fun helper() {
    println("Only visible in this file")
}

internal fun moduleHelper() {
    println("Visible in the same module")
}

public fun publicApi() {
    println("Visible everywhere")
}

For top-level declarations:

Modifier Meaning
public Visible everywhere
private Visible only in the same file
internal Visible in the same module
protected Not allowed at top level

Quick comparison

Modifier Class member visibility
public Visible everywhere
private Visible only inside the declaring class
protected Visible inside the declaring class and subclasses
internal Visible inside the same module

For top-level declarations:

Modifier Top-level visibility
public Visible everywhere
private Visible only in the same file
internal Visible inside the same module
protected Not allowed

Practical example

open class Account(
    public val owner: String,
    private var balance: Double
) {
    internal val accountNumber: String = "ACC-123"

    protected fun canWithdraw(amount: Double): Boolean {
        return amount <= balance
    }

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun currentBalance(): Double {
        return balance
    }
}

class SavingsAccount(owner: String, balance: Double) : Account(owner, balance) {
    fun withdraw(amount: Double) {
        if (canWithdraw(amount)) {
            println("Withdrawal allowed")
        } else {
            println("Insufficient funds")
        }
    }
}

Usage:

fun main() {
    val account = SavingsAccount("Alice", 500.0)

    println(account.owner)
    println(account.currentBalance())

    account.deposit(100.0)
    account.withdraw(50.0)

    // Error: private
    // println(account.balance)

    // Error: protected
    // account.canWithdraw(100.0)

    // Accessible only in the same module:
    // println(account.accountNumber)
}

Rule of thumb

Use the most restrictive visibility that still works:

  • Use private for implementation details.
  • Use protected for behavior intended only for subclasses.
  • Use internal for code shared inside a module but hidden from external modules.
  • Use public for APIs that other code is expected to use.

In most Kotlin classes, you will commonly use private for internal state and leave only the necessary functions or properties public.

How do I implement interfaces and use multiple inheritance in Kotlin?

In Kotlin, interfaces define a contract that classes can implement. Kotlin supports multiple inheritance of interfaces, but not multiple inheritance of classes.

1. Defining an interface

An interface can declare:

  • abstract properties
  • abstract functions
  • functions with default implementations
interface Drivable {
    val maxSpeed: Int

    fun drive()

    fun stop() {
        println("Stopping the vehicle")
    }
}

Here:

  • maxSpeed must be implemented by any class using the interface.
  • drive() has no body, so it must be implemented.
  • stop() has a default implementation, so overriding it is optional.

2. Implementing an interface

Use : after the class name to implement an interface.

class Car : Drivable {
    override val maxSpeed: Int = 180

    override fun drive() {
        println("The car is driving at up to $maxSpeed km/h")
    }
}

Usage:

fun main() {
    val car = Car()

    car.drive()
    car.stop()
}

Output:

The car is driving at up to 180 km/h
Stopping the vehicle

3. Implementing multiple interfaces

A class can implement more than one interface by separating them with commas.

interface Flyable {
    fun fly() {
        println("Flying")
    }
}

interface Swimmable {
    fun swim() {
        println("Swimming")
    }
}

class Duck : Flyable, Swimmable

Usage:

fun main() {
    val duck = Duck()

    duck.fly()
    duck.swim()
}

4. Handling conflicting default implementations

If two interfaces provide a function with the same signature, the implementing class must override it.

interface Printer {
    fun print() {
        println("Printing from Printer")
    }
}

interface Scanner {
    fun print() {
        println("Printing from Scanner")
    }
}

class AllInOneMachine : Printer, Scanner {
    override fun print() {
        super<Printer>.print()
        super<Scanner>.print()
        println("Printing from AllInOneMachine")
    }
}

Usage:

fun main() {
    val machine = AllInOneMachine()

    machine.print()
}

Output:

Printing from Printer
Printing from Scanner
Printing from AllInOneMachine

The syntax:

super<Printer>.print()

means “call the print() implementation from the Printer interface.”

5. Interfaces with properties

Interfaces can declare properties, but they do not store state directly like classes do.

interface Identifiable {
    val id: String
}

class User(
    override val id: String,
    val name: String
) : Identifiable

Usage:

fun main() {
    val user = User("u123", "Alice")

    println(user.id)
    println(user.name)
}

6. Kotlin and multiple inheritance

Kotlin allows:

class Duck : Flyable, Swimmable

But Kotlin does not allow multiple class inheritance:

open class Animal
open class Machine

// Not allowed in Kotlin
class RobotDog : Animal(), Machine()

Instead, Kotlin allows:

  • one superclass
  • multiple interfaces
open class Animal {
    fun eat() {
        println("Eating")
    }
}

interface Runnable {
    fun run()
}

interface Trainable {
    fun train()
}

class Dog : Animal(), Runnable, Trainable {
    override fun run() {
        println("Dog is running")
    }

    override fun train() {
        println("Dog is training")
    }
}

Key syntax

class ClassName : InterfaceName
class ClassName : InterfaceOne, InterfaceTwo
class ClassName : SuperClass(), InterfaceOne, InterfaceTwo

Summary

  • Use interface to define behavior.
  • Use : to implement interfaces.
  • Use override to implement interface members.
  • Kotlin supports multiple interface inheritance.
  • Kotlin does not support inheriting from multiple classes.
  • If interfaces have conflicting default methods, override the method and choose which parent implementation to call using super<InterfaceName>.

How do I use class inheritance with open and override in Kotlin?

In Kotlin, classes and members are final by default, so you must explicitly mark them as open if you want them to be inherited or overridden.

Basic class inheritance

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Bark")
    }
}

Usage:

fun main() {
    val dog = Dog()
    dog.makeSound()
}

Output:

Bark

Key rules

1. Use open on a class to allow inheritance

open class Animal

Without open, this is not allowed:

class Dog : Animal()

because Animal would be final by default.

2. Use open on functions or properties to allow overriding

open fun makeSound() {
    println("Some sound")
}

If a function is not marked open, subclasses cannot override it.

3. Use override in the subclass

override fun makeSound() {
    println("Bark")
}

Kotlin requires override so it is clear that you are replacing behavior from the parent class.

Inheriting from a class with a constructor

If the parent class has a constructor, the subclass must call it:

open class Animal(val name: String) {
    open fun introduce() {
        println("I am $name")
    }
}

class Dog(name: String) : Animal(name) {
    override fun introduce() {
        println("I am a dog named $name")
    }
}

Usage:

fun main() {
    val dog = Dog("Buddy")
    dog.introduce()
}

Output:

I am a dog named Buddy

Overriding properties

Properties can also be open and override:

open class Animal {
    open val sound: String = "Some sound"
}

class Dog : Animal() {
    override val sound: String = "Bark"
}

Usage:

fun main() {
    val dog = Dog()
    println(dog.sound)
}

Output:

Bark

Calling the parent implementation with super

You can call the superclass version using super:

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        super.makeSound()
        println("Bark")
    }
}

Output:

Some sound
Bark

Preventing further overriding

An overridden member is open by default. If you want to prevent subclasses from overriding it again, mark it as final:

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

open class Dog : Animal() {
    final override fun makeSound() {
        println("Bark")
    }
}

Now subclasses of Dog cannot override makeSound().

Summary

open class Parent {
    open fun method() {
        println("Parent method")
    }
}

class Child : Parent() {
    override fun method() {
        println("Child method")
    }
}
  • open class means the class can be inherited.
  • open fun or open val means the member can be overridden.
  • override means the subclass is replacing a parent member.
  • Use super.method() to call the parent version.

How do I use the init block to run setup logic in Kotlin classes?

In Kotlin, an init block runs setup logic when a class instance is created.

It is commonly used to validate constructor arguments, initialize derived properties, or perform other construction-time setup.

class User(val name: String, val age: Int) {

    init {
        require(name.isNotBlank()) {
            "Name must not be blank"
        }

        require(age >= 0) {
            "Age must be non-negative"
        }

        println("Created user: $name")
    }
}

Usage:

val user = User("Alice", 30)

When User("Alice", 30) is called, the init block runs automatically.

Key points

  • init blocks are part of the class initialization process.
  • They run after primary constructor parameters are available.
  • A class can have multiple init blocks.
  • Multiple init blocks run in the order they appear in the class body.
  • They are especially useful with primary constructors.

Example with multiple init blocks:

class Rectangle(val width: Int, val height: Int) {

    val area: Int

    init {
        require(width > 0) {
            "Width must be positive"
        }
    }

    init {
        require(height > 0) {
            "Height must be positive"
        }

        area = width * height
    }
}

You can also combine init blocks with property initializers:

class Product(val price: Double) {

    val tax = price * 0.2

    init {
        require(price >= 0) {
            "Price cannot be negative"
        }
    }
}

Initialization happens in the order the declarations appear in the class body, so property initializers and init blocks are executed top to bottom.

How do I override the toString(), equals(), and hashCode() methods in Kotlin?

In Kotlin, you usually don’t manually override toString(), equals(), and hashCode() if your class is just a value/data holder. Use a data class instead.

data class User(
    val id: Int,
    val name: String
)

Kotlin automatically generates:

toString()
equals()
hashCode()
copy()
componentN()

Example:

val a = User(1, "Alice")
val b = User(1, "Alice")

println(a)        // User(id=1, name=Alice)
println(a == b)   // true
println(a.hashCode() == b.hashCode()) // true

Manual override

If you need custom behavior, override them explicitly:

class User(
    val id: Int,
    val name: String
) {
    override fun toString(): String {
        return "User(id=$id, name=$name)"
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is User) return false

        return id == other.id && name == other.name
    }

    override fun hashCode(): Int {
        var result = id
        result = 31 * result + name.hashCode()
        return result
    }
}

Important notes

Use == for equality in Kotlin:

a == b

This calls equals() internally.

Use === for reference equality:

a === b

This checks whether both variables point to the exact same object.

Custom equality example

Sometimes you only want equality based on one property, such as id:

class User(
    val id: Int,
    val name: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is User) return false

        return id == other.id
    }

    override fun hashCode(): Int {
        return id
    }

    override fun toString(): String {
        return "User(id=$id, name=$name)"
    }
}

The key rule is: if you override equals(), you should also override hashCode() consistently. Objects that are equal must have the same hash code.

How do I use data classes in Kotlin to store structured data?

In Kotlin, data classes are designed to store structured data with minimal boilerplate.

A data class automatically provides useful functions such as:

  • toString()
  • equals()
  • hashCode()
  • copy()
  • component functions for destructuring, like component1(), component2()

Basic example

data class User(
    val id: Int,
    val name: String,
    val email: String
)

You can create and use it like this:

fun main() {
    val user = User(
        id = 1,
        name = "Alice",
        email = "[email protected]"
    )

    println(user)
}

Output:

User(id=1, name=Alice, [email protected])

Accessing properties

println(user.name)
println(user.email)

Because the properties are declared in the primary constructor, they are available directly.

Comparing data objects

Data classes compare values, not object references:

val user1 = User(1, "Alice", "[email protected]")
val user2 = User(1, "Alice", "[email protected]")

println(user1 == user2) // true

Copying with changes

Use copy() to create a modified copy:

val updatedUser = user.copy(email = "[email protected]")

println(updatedUser)

The original object is unchanged.

Destructuring

Data classes support destructuring declarations:

val (id, name, email) = user

println(id)
println(name)
println(email)

Mutable vs immutable properties

Prefer val for immutable data:

data class Product(
    val id: Long,
    val name: String,
    val price: Double
)

Use var only if the property needs to change:

data class MutableUser(
    var name: String,
    var age: Int
)

Example with nested structured data

data class Address(
    val street: String,
    val city: String,
    val postalCode: String
)

data class Customer(
    val id: Int,
    val name: String,
    val address: Address
)

fun main() {
    val customer = Customer(
        id = 100,
        name = "Maria",
        address = Address(
            street = "Main Street",
            city = "Berlin",
            postalCode = "10115"
        )
    )

    println(customer.address.city)
}

Important rules

A Kotlin data class must:

  • Have at least one parameter in the primary constructor
  • Mark primary constructor parameters with val or var
  • Not be abstract, open, sealed, or inner

Example:

data class Book(
    val title: String,
    val author: String,
    val year: Int
)

Use data classes when you mainly need a class to hold data rather than define complex behavior.

How do I use val and var inside Kotlin classes?

In Kotlin classes, val and var are used to declare properties.

  • val means read-only after initialization
  • var means mutable / can be reassigned

Basic example

class User {
    val id: Int = 1
    var name: String = "Alice"
}

Usage:

fun main() {
    val user = User()

    println(user.id)      // 1
    println(user.name)    // Alice

    user.name = "Bob"     // OK: name is var

    // user.id = 2        // Error: id is val
}

val inside a class

Use val when the property should not be reassigned after it gets a value.

class Product {
    val sku: String = "ABC-123"
}

You can read it:

val product = Product()
println(product.sku)

But you cannot assign a new value:

// product.sku = "XYZ-999" // Not allowed

var inside a class

Use var when the property can change.

class Counter {
    var count: Int = 0

    fun increment() {
        count++
    }
}

Usage:

fun main() {
    val counter = Counter()

    counter.increment()
    counter.increment()

    println(counter.count) // 2
}

Declaring properties in the constructor

A common Kotlin style is to put properties directly in the class constructor.

class User(
    val id: Int,
    var name: String
)

This creates a class with:

  • a read-only id
  • a mutable name

Usage:

fun main() {
    val user = User(1, "Alice")

    println(user.id)
    println(user.name)

    user.name = "Bob" // OK

    // user.id = 2    // Error
}

Important distinction

If you write this:

val user = User(1, "Alice")

The variable user itself cannot point to another User, because it is a val.

But if the object has var properties, those properties can still change:

val user = User(1, "Alice")

user.name = "Bob" // OK, because name is var

// user = User(2, "Charlie") // Error, because user is val

So:

val user

means the reference cannot be reassigned.

var name

inside the class means the property can be changed.

Rule of thumb

Use val by default, and only use var when the value really needs to change.

class Person(
    val birthYear: Int,
    var displayName: String
)

Here, birthYear probably should not change, but displayName might.

How do I define constructors and initialize class properties in Kotlin?

In Kotlin, you usually define constructors and initialize properties directly in the class header using a primary constructor.

1. Primary constructor

The most common style is:

class Person(val name: String, var age: Int)

This defines:

  • a class named Person
  • a read-only property name
  • a mutable property age
  • a constructor that requires both values

Usage:

fun main() {
    val person = Person("Alice", 30)

    println(person.name)
    println(person.age)

    person.age = 31
    println(person.age)
}

Here, val name: String and var age: Int are both constructor parameters and class properties.


2. Constructor parameters without properties

If you omit val or var, the parameter is only available during initialization:

class Person(name: String) {
    val uppercaseName = name.uppercase()
}

Usage:

fun main() {
    val person = Person("Alice")

    println(person.uppercaseName)
}

In this example, name is not a property. You cannot access person.name unless you declare it with val or var.


3. Initialize properties in the class body

You can initialize properties using constructor values:

class Rectangle(val width: Int, val height: Int) {
    val area: Int = width * height
}

Usage:

fun main() {
    val rectangle = Rectangle(5, 4)

    println(rectangle.area)
}

Output:

20

4. Use an init block

If you need validation or setup logic, use an init block:

class User(val username: String, val age: Int) {
    init {
        require(username.isNotBlank()) {
            "Username must not be blank"
        }

        require(age >= 0) {
            "Age must not be negative"
        }
    }
}

The init block runs when an object is created:

fun main() {
    val user = User("kotlinFan", 25)

    println(user.username)
}

5. Default constructor values

You can give constructor parameters default values:

class Product(
    val name: String,
    val price: Double = 0.0,
    val inStock: Boolean = true
)

Usage:

fun main() {
    val freeSample = Product("Sticker")
    val laptop = Product("Laptop", 999.99, false)

    println(freeSample.price)
    println(laptop.inStock)
}

6. Named arguments

Named arguments make constructor calls clearer:

class Book(
    val title: String,
    val author: String,
    val pages: Int
)

fun main() {
    val book = Book(
        title = "Kotlin Basics",
        author = "JetBrains",
        pages = 250
    )

    println(book.title)
}

7. Secondary constructors

Kotlin also supports secondary constructors, but they are less common:

class Car {
    val brand: String
    val year: Int

    constructor(brand: String, year: Int) {
        this.brand = brand
        this.year = year
    }
}

Usage:

fun main() {
    val car = Car("Toyota", 2024)

    println(car.brand)
    println(car.year)
}

However, this is usually better written with a primary constructor:

class Car(val brand: String, val year: Int)

8. Primary and secondary constructors together

If a class has a primary constructor, secondary constructors must delegate to it using this(...):

class Employee(val name: String, val role: String) {
    constructor(name: String) : this(name, "Employee")
}

Usage:

fun main() {
    val employee = Employee("Sam")
    val manager = Employee("Dana", "Manager")

    println(employee.role)
    println(manager.role)
}

9. Late initialization with lateinit

For mutable non-null properties initialized later, use lateinit var:

class Session {
    lateinit var token: String

    fun start(token: String) {
        this.token = token
    }
}

Usage:

fun main() {
    val session = Session()

    session.start("abc123")

    println(session.token)
}

Use lateinit carefully. Accessing it before initialization causes an exception.


10. Custom getters and setters

You can customize property access:

class Temperature(celsius: Double) {
    var celsius: Double = celsius
        set(value) {
            require(value >= -273.15) {
                "Temperature cannot be below absolute zero"
            }
            field = value
        }

    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32
}

Usage:

fun main() {
    val temperature = Temperature(25.0)

    println(temperature.fahrenheit)

    temperature.celsius = 30.0
    println(temperature.fahrenheit)
}

Quick summary

class Person(
    val name: String,
    var age: Int = 0
) {
    init {
        require(name.isNotBlank()) {
            "Name cannot be blank"
        }
    }

    val isAdult: Boolean
        get() = age >= 18
}

This example shows:

  • val name: read-only property initialized from constructor
  • var age: mutable property with a default value
  • init: validation logic
  • isAdult: computed property

In most Kotlin code, prefer a primary constructor with val or var properties unless you specifically need more complex construction logic.

How do I create a class and an object in Kotlin?

In Kotlin, you create a class with the class keyword, and you create an object instance by calling the class constructor.

class Person {
    var name: String = "Unknown"
    var age: Int = 0
}

fun main() {
    val person = Person()

    person.name = "Alice"
    person.age = 25

    println("${person.name} is ${person.age} years old")
}

Output:

Alice is 25 years old

Class with a constructor

A more common Kotlin style is to define properties directly in the constructor:

class Person(
    val name: String,
    var age: Int
)

fun main() {
    val person = Person("Alice", 25)

    println("${person.name} is ${person.age} years old")
}

Here:

  • class Person(...) defines a class.
  • val name is a read-only property.
  • var age is a mutable property.
  • Person("Alice", 25) creates an object of the class.

Kotlin object keyword

Kotlin also has the object keyword, which creates a singleton object:

object AppConfig {
    val appName = "My Kotlin App"
    val version = "1.0"
}

fun main() {
    println(AppConfig.appName)
    println(AppConfig.version)
}

Unlike a class, you do not create instances of an object. There is only one instance, and you access it directly by name.

Java Class File Format Versions

A compiled Java .class file starts with a fixed header (0xCAFEBABE), followed by a pair of numbers: minor_version and major_version. The pair (commonly written as major.minor, e.g., 52.0) identifies which Java platform level the bytecode targets. The JVM uses this to decide whether it can load the class. If the class was compiled for a newer platform than the JVM supports, you’ll get UnsupportedClassVersionError.

Why It Matters:

  • Backward compatibility: Newer JVMs can generally run older class files, but not the other way around.
  • Build reproducibility: Ensuring all modules target the same release avoids subtle runtime issues.
  • Tooling alignment: IDEs, build tools, containers, and CI images must agree on the target level to prevent version skew.

Quick mapping highlights:

  • Java 8 → 52.0
  • Java 11 → 55.0
  • Java 17 (LTS) → 61.0
  • Java 21 (LTS) → 65.0
  • Java 22 → 66.0, 23 → 67.0, 24 → 68.0, 25 → 69.0, 26 → 70.0, 27 → 71.0, 28 → 72.0
JDK Version Class File Format Version
1.0 45.0
1.1 45.3
1.2 46.0
1.3 47.0
1.4 48.0
5 49.0
6 50.0
7 51.0
8 52.0
9 53.0
10 54.0
11 55.0
12 56.0
13 57.0
14 58.0
15 59.0
16 60.0
17 61.0
18 62.0
19 63.0
20 64.0
21 65.0
22 66.0
23 67.0
24 68.0
25 69.0
26 70.0
27 71.0
28 72.0

Note:

  • Early JDK branding used 1.x (e.g., 1.5, 1.6) but these correspond to modern names 5, 6, etc. The table above reflects the modern naming for 5+.
  • There was no official 1.9 brand; Java 9 is simply 9 → 53.0 (already shown above).

How to check a class file’s version

  • Using javap (JDK tool):
    javap -v path/to/Some.class | find "major"
    

    Look for a line like major version: NN (e.g., 52 for Java 8). For modern compilers, minor is typically 0.

  • Reading the header directly (forensics style):

    1. Confirm magic bytes: CA FE BA BE.
    2. Next 2 bytes: minor_version.
    3. Next 2 bytes: major_version (e.g., 0x003D = 61 → Java 17).

How to compile for a specific Java level

  • Recommended (single flag):
    javac --release 21 -d out $(find src -name "*.java")
    

    --release consistently sets language features, APIs, and the class file version.

  • Legacy approach (not preferred, can mismatch APIs):

    javac -source 1.8 -target 1.8 -bootclasspath "%JAVA8_HOME%\\jre\\lib\\rt.jar" -extdirs ""
    
  • Maven (maven-compiler-plugin):
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
      <configuration>
          <release>21</release>
      </configuration>
    </plugin>
    
  • Gradle (Groovy DSL):
    java {
      toolchain {
          languageVersion = JavaLanguageVersion.of(21)
      }
    }
    // Or explicitly set the target bytecode
    tasks.withType(JavaCompile).configureEach {
      options.release = 21
    }
    

Common failure and how to fix

  • Symptom:
    • java.lang.UnsupportedClassVersionError: … has been compiled by a more recent version of the Java Runtime.
  • Causes:
    • Running on an older JRE/JDK than the class file requires.
    • Mixed toolchains or inconsistent --release/target levels in a multi-module build.
  • Fixes:
    • Upgrade the runtime to meet the class file’s major.minor level; or
    • Recompile with an older target using --release <level> that matches your deployment runtime; and
    • Standardize toolchains via Maven/Gradle toolchains and CI images to avoid skew.

Tips and caveats

  • Prefer --release over -source/-target because it also validates against platform APIs for that release.
  • Preview features do not change the class file version; they require --enable-preview at compile and run time, but the mapping still follows the JDK’s version.
  • When publishing libraries, choose the lowest --release that matches your supported runtime matrix to maximize compatibility; consider multi-release JARs if you need newer APIs while keeping a baseline.