EXPLICIT_FINALIZE_CALL
Summary
- Rule ID:
EXPLICIT_FINALIZE_CALL - Name: Explicit finalize call
- Description: Detects direct virtual calls to
finalize()on object instances, which bypass GC lifecycle management and indicate broken resource cleanup. - Annotation policy:
@Suppress-style suppression is unsupported. Annotation-driven semantics support JSpecify only; this rule has no annotation-driven semantics.
Motivation
Java's Object.finalize() method was designed to be called by the GC during object collection, not by application code. Explicitly calling obj.finalize() is almost always a mistake:
- It does not trigger GC collection of
objor its resources. - It can run the finalizer twice (once explicitly, once by GC), which is unsafe for resources like file handles or sockets.
Object.finalize()is deprecated since Java 9 and the entire finalizer mechanism is being removed; explicit calls make migration harder.- The correct resource management approach is
AutoCloseablewith try-with-resources, orjava.lang.ref.Cleaner.
This mistake is hard to notice in code review because obj.finalize() is syntactically valid and compiles without warnings in older Java versions.
What it detects
- Any bytecode-level
invokevirtualcall where method name isfinalizeand descriptor is()V, in any analysis target class. - This includes
this.finalize(),field.finalize(), andlocalVar.finalize().
What it does NOT detect
super.finalize()calls insidefinalize()overrides — those useinvokespecialand are a legitimate pattern for finalize override chains.- Overriding the
finalize()method itself — only call sites are flagged. invokespecialcalls tofinalize()outside afinalizeoverride (a rare edge case not worth the complexity to detect).- Classes that are classpath-only (not analysis targets).
- Any suppression behavior via
@Suppressor@SuppressWarnings.
Examples (TP/TN/Edge)
TP: explicit call on a local variable (reported)
package com.example;
public class ClassA {
public void methodOne() throws Throwable {
Object varOne = new Object();
varOne.finalize();
}
}
ClassA.methodOne.
TP: explicit call on this (reported)
package com.example;
public class ClassB {
public void methodTwo() throws Throwable {
this.finalize();
}
}
ClassB.methodTwo.
TN: super.finalize() inside a finalize override (not reported)
package com.example;
public class ClassC {
@Override
protected void finalize() throws Throwable {
super.finalize();
}
}
super.finalize() uses invokespecial, not invokevirtual.
TN: finalize() override with no super call (not reported)
package com.example;
public class ClassD {
@Override
protected void finalize() throws Throwable {
// cleanup
}
}
TN: unrelated method calls (not reported)
No finding: nofinalize() call present.
Edge: classpath-only class with explicit finalize call (not reported)
A class compiled as a classpath dependency and not included in the analysis target is not reported, even if it contains an explicit finalize() call.
Output
- Report one finding per
invokevirtual finalize ()Vcall site. - Message must be actionable:
Explicit call to finalize() in <class>.<method><descriptor>; use AutoCloseable with try-with-resources or java.lang.ref.Cleaner for deterministic resource cleanup. - Location is reported at the method level using method logical location and, where available, the source line of the call instruction.
Performance considerations
- Analysis is bounded by the total number of call sites across all analysis target methods.
- No inter-method or inter-class analysis is performed.
- No CFG traversal is required; a simple scan of
method.callssuffices. - Traversal order must be deterministic: process classes in iteration order, methods in iteration order, calls in offset order (as provided by the IR).
Acceptance criteria
- Reports when any analysis target method contains an
invokevirtual finalize ()Vcall. - Does not report
invokespecial finalize ()Vcalls (i.e.,super.finalize()patterns). - Does not report the
finalize()method declaration itself. - Does not report classpath-only classes.
- Covers TP, TN, and edge cases in tests.
- Produces deterministic finding order and count across repeated runs.
- Keeps
@Suppress-style suppression unsupported and does not add non-JSpecify annotation semantics.