Unlike newer languages like Kotlin and Scala, Java makes a very clear distinction between fields and methods. So if you've got a
public final Foo foo;
Then that's always pointing to an actual field in memory. It can never be a method, and it can never run arbitrary code. The call myInstance.foo
is always direct memory access, which is contrary to OOP principles. When I ask an instance for something, it should be the instance's decision how to handle that request, not mine.
In Kotlin, properties can have getters and setters, so even if we have something that looks like a field, it might call a method to access it. This allows us to future-proof our code in advance. We can implement an actual instance variable now,
val foo: Foo = myConcreteInstance()
and then, down the road, when foo
needs to go make a database query or something because we're scaling our application up, we can change the implementation
val foo: Foo
get() = loadFooLazily()
private fun loadFooLazily(): Foo {
// Some complicated code ...
}
Crucially, this affects nobody downstream. Everybody else downstream still writes myInstance.foo
and works with the new code. It's not a breaking change.
We can't do this in Java. myInstance.foo
is always a variable, not a method. So Effective Java suggests always accessing instance variables through a method in order to future-proof them. If we write
private final Foo foo;
public Foo getFoo() {
return foo;
}
Then we can change getFoo
in the future without breaking other code. It's a difference in philosophy: In Java, methods are the OOP construct while instance variables are strictly an implementation detail. In most newer languages, "instance variable" has been replaced with a high-level construct called a "property" which is intended to be used in an OOP way.