Warnings

John Hendrikx john.hendrikx at gmail.com
Sun Dec 4 16:22:19 UTC 2022


We've done a few fixes in JavaFX trying to reduce the amount of 
warnings, and there was some discussion as to the value of fixing 
certain warnings. Some of these are purely aesthetic (like using the 
diamond operator) but others can hide current or future programming 
problems.

I thought I'd post why I think fixing certain warnings is important for 
the continued health of a large code base.

- Unnecessary casts (on Objects)

Every cast in a code base is a potential place where a 
ClassCastException may lurk.  When a cast is unnecessary, it may seem 
harmless to leave it in.  However, as a code base grows or is 
refactored, a harmless cast hides a potential compilation problem which 
can become a runtime ClassCastException.  Java developers rely a lot on 
the compiler helping them when they do small refactorings to point out 
places where a type no longer matches after a change. Take the code below:

     private Number n;
     private Float f = (Float) n;  // warning unnecessary cast

Changing `n` to a more specific type (let's say `Double`) will not 
generate an error for `f`.  The only thing that happens is that the cast 
is no longer deemed unnecessary and will do an actual cast (resulting in 
CCE at runtime) -- as there is no error reported, the developer may 
easily miss this.

- Unnecessary casts (on primitives)

A cast on a primitive that is not currently required can become a place 
where precision is lost without warning when a refactoring occurs.

     private int x;
     private int y = (int) x;  // warning unnecessary cast

A refactoring that changes `x` to a type like `long` would go unnoticed 
by the compiler.  The version without the unnecessary cast would emit an 
error.

- Unnecessary declared checked exceptions

When a checked exception is declared that is never thrown from the 
underlying code, it is good to remove these to prevent them from 
propagating up the call chain.  Sometimes code bases have a checked 
exception in some deeply nested function that is not actually thrown, 
leading to perhaps top level public API to declare this exception. When 
the problem is discovered, the exception could be removed from dozens of 
places and could even affect public API (which now throws an exception 
that it can't actually throw but can't be removed as it is was already 
made public).

- Missing @Override annotations

Adding these everywhere makes extending super types and refactoring 
safer.  A sub type may declare a method that currently does not override 
any of its super types. If a super type is changed and a method is added 
that has a signature that is compatible with one that is already present 
in a sub type, the compiler will emit a warning that it is missing the 
override annotation alerting you of a potential problem (assuming the 
code base is relatively warning free, or it may get lost in the noise).  
Conversely, removing or changing a currently overriden method would 
result in errors if sub types declared their overriden methods with the 
override annotation, alerting you that this functionality was perhaps 
more broadly used than anticipated.  Without the annotation, the method 
in the sub type would simply change to a non-overriden version without 
any further warning.

- Avoiding indirect access to static methods and fields

Code that accesses static members via `this` or indirectly via a sub 
type that didn't declare it, runs the risk of that access being diverted 
to a new member if one is introduced with the same name without a 
compiler error:

        class A { static final int ONE = 1; }
        class B { }
        class C extends B {
             int one = ONE;  // unqualified access to A.ONE
        }

A "harmless" constant is introduced in B:

        class B { static final int ONE = 0x10000;  }  // 1 in 16-bit 
fixed point

No error occurs, and class C is broken.

A further reason to not allow indirect accesses to static members is 
that it may hide problems where an instance wasn't needed at all (if 
using `this.ONE` for example).

- Using raw types

With the introduction of generic types in Java 1.5, use of raw types 
should be considered something to be always avoided.  Using raw types 
will require casts in places that the compiler could have inferred or 
checked for you if you had used a generic type. Not using a generic type 
is basically risking a ClassCastException in those places (it was one of 
the most common exceptions in the pre-1.5 days).  Further more, the 
compiler can't help you during refactorings.  If a generic declared List 
is changed to contain a different type, there will be compiler errors in 
all the places you interact with the List.  Not so for a raw List; 
failing to fix all the casts will pay a runtime price.

- Unused fields / variables / parameters

All of these may point to a potential problem. It could be that the 
author mistyped something and meant to use one of the unused values; it 
could be that a parameter is genuinely unused and should be removed 
(triggering a potential cascade in callers that may be doing unnecessary 
actions to "provide" this unneeded information); it could be that a 
field is accessed by reflection (in which case that would have meritted 
a BIG warning comment and `SuppressWarnings`); whatever the case, it is 
confusing to future maintainers, may involve unnecessary memory use or 
unnecessary calculations (setting but never reading a value could 
involve a big calculation that may be completely unnecessary).

--John






More information about the openjfx-dev mailing list