Final. A Java Keyword.

Aaron Harris
7 min readFeb 16, 2020

--

Final. Go nuts or Don’t bother?

(PS — There is a TLDR at the end)

I’m not aiming to cover every possible debate on the topic, but these are the most common pros and cons I have encountered in my years.

True story: At some point when helping out another team, my code was rejected for lack of Final use. When I asked about the convention, I received: “Anywhere the compiler will allow it”. Wow.

Each of the following bullets are broken down further below for your enjoyment. Please criticize as necessary.

Use liberally:

  • It communicates intent.
  • It’s safe because you can’t modify it.
  • It allows you to reference variables from other threads.
  • It enables compiler optimizations.

Use minimally:

  • It confuses intent.
  • It’s unsafe because you can modify it.
  • It allows you to reference variables from other threads.
  • It clutters the code.

Why the disagreement?

How can it be that the reasons for “Final” look so similar to the reasons against? In a word: Ambiguity. Depending on how you were mentored as an engineer, you may draw certain conclusions about how helpful Final is, and others have not. It’s a belief, not unlike religion. Neither is incorrect depending on other supporting development environment variables.

Let’s break it down

By comparing each viewpoint, we can get a more realistic view on the pros and cons.

Communicating or Confusing Intent ?

If everyone on the team has a universally agreed upon standard for what the word Final means in any given situation and newcomers are on-boarded with this understanding, then Final can truly be a helpful form of documentation. However, how often is this true? It only takes one misunderstanding to convert communication to confusion and potential bugs. So why not rely on actual documentation to communicate this intent? Or better yet, the code itself should enforce intent by means that are not subject to interpretation such as “universal agreements”.

Conclusion: Use clear docs, or code enforcement instead.

Compiler Optimizations ?

Yes. In theory this is true. The compiler should be able to make certain optimizations about fields or local variables that are Final. It can consider primitives immutable, but it cannot consider non-primitives to be immutable, only the reference. The amount of gains we may see very seem debatable. I tried searching for experiments on this and found little or nothing. I wonder why that is. But, again, in theory, optimizations can be made. What about final constants and final class definitions? Certainly optimizations can be had there.

Conclusion: Sure… But until someone shows me concrete gains, we’re only guessing on the value brought by final. I strongly encourage you to try an experiment in a scenario that makes sense for your application. I expect you’ll see no real world performance gains.

Safe or Unsafe?

It’s [un]safe because you can modify it. Immutable data structures and immutable values are nice for locking things down and protecting state, especially for concurrent reasons or just simplifying things. How does Final help? A final class and final method can help to lock-in immutability by eliminating the possibility of overriding behavior that could misuse the data. Do it!

Fields?

  • Primitives are easily made immutable with Final. As final fields, the are guaranteed initialized at construction time which improves data consistency.
  • Non-Primitives are not made immutable with Final, but they still benefit from guaranteed initialization at construction time. Unfortunately this does not improve data consistency as the “how” it was initialized depends on the data structure and how well it enforces its contracts.

Parameters?

  • Final Primitives can be included in lambdas or anonymous inner classes because the reference is guaranteed not to change. This is necessary to enable the compiler to copy that reference to the inner class. All Java values are copied — not pass by reference (Java doesn’t move the data, it just copies the 32 bit reference to the data). Final Primitives also remind us that it’s a bad idea to change the value of parameters. I find no real value in either of these (subjective). If we look at a method as a contract between the consumer and maintainer of the method, then why would the consumer care that the maintainer shouldn’t touch the variables that were copied anyway? Primitives can’t be harmed and Objects can be whether they are final or not. Also, why would the consumer of this method care that the maintainer is using a Lambda? What’s better is, keep those details internalized. I would call general wasteful as it leads to misleading contract details.

Local Variables?

  • A local primitive or non-primitive that is final and not initialized upon declaration, must be initialized before exiting scope. This is a handy tool, use it. It forces future readers to understand the importance of this variable. However, if you go crazy and force every local variable to be final, you just hid that importance in a see of generalism. I would call general use unsafe as it hides important details.

Conclusion

  • Final Class and Method definition — Safe, Useful, Do it
  • Final Primitive (Field | Parameter | Local) — Safe, Sometimes Useful
  • Final Non-Primitive (Field | Parameter | Local) — Neither Safe nor Unsafe

Clutter or Value?

There’s little or no debate that liberal use of Final takes up space we’d rather not spend within our precious line width — another heated topic. In fact, more modern languages have baked this concept into their variable definition to avoid taking up the space. Let, Var, Val, etc. So let’s not argue about the space, but rather the effects of the space consumed by Final. Line-wrapping makes code harder to read. Technically that’s subjective but I think most would agree so let’s consider line width. Another consideration might be, the general number of words smashed together in an area where we’re trying to read logic such as a method definition or JavaDoc. What other considerations for clutter? Final doesn’t increase vertical spacing except from line-wrapping which we’ve already mentioned. Ok so let’s break it down.

Cases:

  • Class Definition: Final adds a slight width cost which may lead to wrapping but unlikely as it’s an O(1) cost. However, it compile-time restricts this class from being extended which is invaluable. Worth it
  • Field Definition: Final adds a slight width cost which may lead to wrapping but unlikely as it’s an O(1) cost. However, it compile-time restricts this field[-reference] from being overwritten and forces it to be initialized during construction which is a great pattern and allows use of @NonNull or similar mechanisms which are excellent non-confusing documentation that is also supported by most IDEs. Worth it
  • Method Definition: Final adds a slight width cost which may lead to wrapping but unlikely as it’s an O(1) cost. However, it compile-time restricts this method from being overridden by derived classes. In this case the optimizations are also obvious: final excludes override which allows the compiler to mark this method as non-virtual which saves one reference hop for each call. So in this case we have a real-world optimization and a compile-time support to tell developers how this method should be treated (do not override). Worth it
  • Method Parameter: (as a rule, when applied to all parameters) Final adds a significant width cost which will likely lead to wrapping as it’s an O(n) cost which increases reading difficulty and vertical space. The value added is that each parameter[-reference] cannot be overwritten. This could reduce accidental overwrites of parameters but most IDEs can utilize lint checks to catch this without final. In my experience it’s generally considered faux pas to mutate parameters without saying — is this your experience as well? No really, I’m asking. There’s another cost of what it means to be a final parameter, there is no universal consensus so it adds confusion. It can enable a parameter to be used in a lambda or another thread but that’s a poor reason to make all parameters final. On an individual basis it may be considered valuable documentation that this parameter will be used in a lambda or another thread, but that also reveals implementation that may be better encapsulated — debatable. When lint checks are available, Not Worth it
  • Local Variable: (as a rule, when applied to all variables) Final adds a slight cost horizontally, but contributes O(n) to the number of keywords in a logic area. This may or may not be an issue depending on what you’re accustomed to but it definitely won’t be received the same by everyone which increases confusion and time spent debating and rejecting code. The value added is that each variable cannot be overwritten, which means you know that this variable[-reference] is unchanged. However, this can also lead someone to falsely believe the state is unchanged and potentially increase confusion (read Safe or Unsafe? above). On an individual basis, some variables make sense as final. But this debate is about applying to all as a general rule. Not Worth it

Conclusion

A trend emerged from the break-down of pros and cons. The trend is that it’s Worth it to use final on an individual basis when it makes sense and the interpretation of its use is not subjective. It’s Not Worth it to apply final to everything as a general rule.

So I argue against Liberal use or Minimal use and instead propose, “Meaningful Use”.

Use final when its meaning is explicit and it serves a functional purpose supported by the compiler,IDE or Lint.

Avoid using final as a “general rule”, “universally agreed upon” meaning adopted by your team or

The End.

TLDR

Useful

  • Constants
  • Protect Classes & Methods from being extended
  • Member variables that just should not change
  • Immutability
  • Forcing initialization of variables within a method
  • Immutable primitives

Okay but unnecessary with modern IDEs

  • final method parameters to protect against overwriting
  • final variables declared in a method — except when forcing initialization

Confusing / Dangerous / Not-Useful

  • Method parameters for non-primitives — Prefer Immutable Objects
  • Final for the sake of final or religion — Causes useful cases of final to become shadowed from overuse of non-useful cases.
  • Use of final as some local-team documentation — Prefer real documentation or enforceable contracts.

--

--