Numeric Promotion, Overload Resolution & Surprising Expressions
Learn Java Data Types, Type Semantics, Object Model & Data Representation - Part 018
Numeric promotion, unary and binary numeric contexts, compound assignment, integer division, char arithmetic, shift operators, conditional expressions, overload resolution phases, most-specific method selection, and surprising Java expressions.
Part 018 — Numeric Promotion, Overload Resolution & Surprising Expressions
Target part ini: memahami bagaimana Java menentukan tipe hasil ekspresi numerik dan method overload yang dipanggil. Banyak bug Java tidak muncul karena developer tidak tahu operator
+, tetapi karena ia salah menebak tipe hasilbyte + byte,int / int,char + int,long * int, conditional expression, compound assignment, atau overload antara primitive, wrapper, varargs, dan reference type.
1. Mental Model Utama
Java arithmetic tidak selalu menghasilkan tipe operand yang terlihat di source code.
byte a = 1;
byte b = 2;
// byte c = a + b; // compile-time error
int c = a + b; // result is int
Kenapa? Karena binary numeric promotion.
Rule praktis:
small integral types do arithmetic as int
Tipe byte, short, dan char sering dipromosikan ke int sebelum operasi.
Diagram:
2. Numeric Context
Numeric promotion terjadi dalam numeric context, misalnya:
- unary operators:
+x,-x,~x; - binary arithmetic:
a + b,a - b,a * b,a / b,a % b; - comparison tertentu;
- shift operators dengan aturan khusus;
- conditional expression dengan numeric operands;
- compound assignment secara implisit melakukan operasi dan narrowing ke tipe kiri.
Numeric promotion menjawab:
what type should this numeric operation use?
Bukan:
is this operation domain-safe?
3. Unary Numeric Promotion
Unary numeric promotion berlaku pada operator seperti unary plus, unary minus, dan bitwise complement.
byte b = 1;
int x = +b;
int y = -b;
int z = ~b;
b dipromosikan ke int.
Contoh surprise:
byte b = 1;
// byte c = -b; // compile-time error
byte c = (byte) -b;
Walaupun nilai -1 muat di byte, ekspresi -b bertipe int karena b bukan constant expression bertipe compile-time constant yang bisa langsung dinarrow ke byte tanpa cast.
Untuk literal:
byte x = -1; // OK: constant expression representable
4. Binary Numeric Promotion
Binary numeric promotion berlaku untuk banyak operator dua operand.
Rule umum:
- Jika salah satu operand
double, operand lain dikonversi kedouble, hasildouble. - Jika tidak, jika salah satu operand
float, operand lain dikonversi kefloat, hasilfloat. - Jika tidak, jika salah satu operand
long, operand lain dikonversi kelong, hasillong. - Jika tidak, kedua operand dikonversi ke
int, hasilint.
Contoh:
int a = 10;
long b = 20L;
var r1 = a + b; // long
float f = 1.5f;
double d = 2.5;
var r2 = f + d; // double
short s1 = 1;
short s2 = 2;
var r3 = s1 + s2; // int
Gunakan var dengan hati-hati dalam materi numeric. var tidak membuat tipe “fleksibel”; ia mengunci tipe hasil inference.
var total = 0; // int
Jika aggregator harus long:
var total = 0L; // long
5. byte, short, dan char Arithmetic
Small integral types dipakai untuk storage, protocol, dan interop. Tetapi arithmetic-nya umumnya int.
byte a = 100;
byte b = 20;
// byte c = a + b; // compile-time error
int c = a + b;
Ini desain yang mencegah silent overflow di ekspresi kecil, tetapi tetap bisa mengejutkan.
Untuk char:
char ch = 'A';
int code = ch + 1; // 66
char next = (char) (ch + 1);
char bukan “character abstraction lengkap”. Ia adalah 16-bit unsigned code unit. Arithmetic char adalah numeric operation, bukan Unicode-aware text operation.
6. Integer Division Trap
int completed = 1;
int total = 2;
double ratio = completed / total;
System.out.println(ratio); // 0.0
Kenapa? completed / total dievaluasi sebagai int / int, hasil int yaitu 0, lalu baru widening ke double.
Yang benar:
double ratio = (double) completed / total;
Atau:
double ratio = completed / (double) total;
Guideline:
cast before division, not after division
Salah:
double ratio = (double) (completed / total); // too late
7. Overflow Before Widening
int a = 1_500_000_000;
int b = 2;
long result = a * b;
System.out.println(result);
a * b adalah int * int, hasil int, bisa overflow sebelum assignment ke long.
Yang benar:
long result = (long) a * b;
Atau:
long result = Math.multiplyExact((long) a, b);
Rule:
target type of assignment does not control arithmetic type unless operand is converted before operation
Ini bug umum di quota, billing, pagination, byte-size calculation, SLA duration, dan rate limiting.
8. Compound Assignment Surprise
byte b = 1;
b = (byte) (b + 1); // explicit
b += 1; // compile OK
b += 1 terlihat seperti arithmetic byte, tetapi secara konsep mirip:
b = (byte) (b + 1);
Kiri dievaluasi sekali, operasi terjadi dengan numeric promotion, lalu hasil di-cast kembali ke tipe kiri.
Konsekuensi:
byte b = 127;
b += 1;
System.out.println(b); // -128
Compound assignment bisa menyembunyikan narrowing.
Guideline:
avoid compound assignment when overflow/narrowing matters
Untuk counter domain-critical, lebih eksplisit:
int next = Math.addExact(current, delta);
9. Constant Expressions and Narrowing
Java memperlakukan compile-time constant expression secara khusus.
byte a = 1 + 2; // OK
Tapi:
int x = 1;
int y = 2;
// byte b = x + y; // compile-time error
Jika nilai constant expression representable dalam target type, assignment narrowing bisa terjadi.
final int x = 1;
final int y = 2;
byte b = x + y; // OK because x and y are constant variables
Namun hati-hati dengan public constants. Inlining compile-time constants bisa menjadi binary compatibility trap, sudah dibahas di Part 008.
10. Shift Operators
Shift operator punya aturan khusus.
byte b = 1;
int x = b << 2; // left operand promoted to int
Right operand tidak menentukan tipe hasil. Untuk shift:
- left operand dipromosikan;
- hasil bertipe tipe promoted left operand;
- right operand menentukan jarak shift;
- untuk
int, shift distance efektif memakai 5 bit bawah; - untuk
long, shift distance efektif memakai 6 bit bawah.
Contoh:
int x = 1 << 31;
int y = 1 << 32; // same as 1 << 0
long z = 1L << 32;
1 << 32 bukan 2^32. Karena 1 adalah int, shift distance 32 efektif menjadi 0 untuk int.
Yang benar untuk 2^32:
long value = 1L << 32;
Guideline:
use L suffix intentionally in bitmask code
11. String Concatenation and Numeric +
Operator + punya dua wajah:
- numeric addition;
- string concatenation.
System.out.println(1 + 2 + "x"); // "3x"
System.out.println("x" + 1 + 2); // "x12"
Evaluasi kiri ke kanan.
Dalam logging dan message building, ini biasanya harmless. Dalam protocol string atau key generation, buat format eksplisit.
String key = userId + ":" + caseId;
Jika userId atau caseId bisa mengandung delimiter, ini bukan sekadar issue string concat; ini issue encoding contract.
12. Conditional Expression Numeric Surprise
Ternary operator bisa memengaruhi tipe hasil.
boolean flag = true;
var x = flag ? 1 : 2L; // long
var y = flag ? 1 : 2.0; // double
Dengan wrappers dan null, lebih berbahaya:
Integer a = null;
int b = 1;
// var x = flag ? a : b; // may unbox a depending on type rules and runtime path
Guideline:
do not mix primitive, wrapper, and null casually in conditional expressions
Lebih jelas:
Integer result = flag ? a : Integer.valueOf(b);
Atau modelkan absence secara eksplisit.
13. Overload Resolution Mental Model
Overload resolution memilih method pada compile-time berdasarkan static types argument, bukan runtime class argument.
void print(Object o) { System.out.println("Object"); }
void print(String s) { System.out.println("String"); }
Object value = "abc";
print(value); // Object
Walaupun runtime object adalah String, overload yang dipilih adalah print(Object) karena variable value bertipe compile-time Object.
Diagram:
14. Overload Phases: Strict, Loose, Varargs
14.1 Widening Beats Boxing
static void m(long x) {
System.out.println("long");
}
static void m(Integer x) {
System.out.println("Integer");
}
m(10); // long
int -> long works in strict invocation. Boxing is considered in a later phase, so m(long) wins.
14.2 Boxing Beats Varargs
static void m(Integer x) {
System.out.println("Integer");
}
static void m(int... x) {
System.out.println("varargs");
}
m(10); // Integer
Fixed arity with boxing is considered before varargs.
14.3 Widening Reference After Boxing
static void m(Object x) {
System.out.println("Object");
}
m(10); // Object, via int -> Integer -> Object
Boxing can be followed by widening reference in invocation contexts.
14.4 No Widening Primitive Then Boxing to Different Wrapper
static void m(Long x) {}
m(10); // compile-time error
int does not become long and then Long for this call.
Use:
m(10L);
15. Most Specific Method
Jika beberapa method applicable, compiler memilih yang paling specific.
static void m(CharSequence x) {
System.out.println("CharSequence");
}
static void m(String x) {
System.out.println("String");
}
m("abc"); // String
Namun dengan null, bisa ambigu.
static void m(Integer x) {}
static void m(String x) {}
// m(null); // ambiguous
Karena null bisa menjadi Integer atau String, dan tidak ada yang lebih specific dari yang lain.
Jika harus, cast eksplisit:
m((String) null);
Tetapi lebih baik hindari overload yang membuat null ambigu di API publik.
16. Overloading vs Overriding
Overloading dipilih compile-time. Overriding dipilih runtime setelah signature dipilih.
class Parent {
void handle(Object x) {
System.out.println("Parent Object");
}
}
class Child extends Parent {
void handle(Object x) {
System.out.println("Child Object");
}
void handle(String x) {
System.out.println("Child String");
}
}
Parent p = new Child();
Object value = "abc";
p.handle(value); // Child Object
Kenapa bukan Child String?
- Compile-time type receiver adalah
Parent. - Candidate method pada
Parentadalahhandle(Object). - Signature terpilih:
handle(Object). - Runtime dispatch memilih override
Child.handle(Object).
Overload Child.handle(String) tidak ikut karena tidak terlihat dari compile-time receiver type Parent.
17. Varargs and Boxing Pitfalls
static void m(Integer... values) {
System.out.println("Integer...");
}
static void m(int... values) {
System.out.println("int...");
}
// m(); // ambiguous
Varargs overload dengan primitive dan wrapper bisa membingungkan.
Contoh lain:
static void log(Object... values) {}
static void log(String message, Object... values) {}
log(null); // can be surprising/ambiguous depending overload set
Guideline:
avoid overload sets that differ only by primitive/wrapper/varargs shape in public APIs
18. Surprising Expression Catalog
18.1 byte + byte is int
byte a = 1;
byte b = 2;
int c = a + b;
18.2 char + char is int
char a = 'A';
char b = 1;
int c = a + b; // 66
18.3 int / int is int
double x = 1 / 2; // 0.0
18.4 Overflow Before Assignment
long x = 2_000_000_000 * 2; // overflow as int first
Use:
long x = 2_000_000_000L * 2;
18.5 Compound Assignment Narrows
short s = 32767;
s += 1; // -32768
18.6 Floating Equality Is Usually Wrong
double x = 0.1 + 0.2;
System.out.println(x == 0.3); // false
This is numeric representation, not Java being random.
18.7 Math.abs(Integer.MIN_VALUE)
int x = Math.abs(Integer.MIN_VALUE);
System.out.println(x); // still negative
Because positive counterpart cannot be represented in int.
Mitigate with wider type or exact domain checks.
18.8 Long Comparison by ==
Long a = 1000L;
Long b = 1000L;
System.out.println(a == b); // false usually
This is wrapper identity, not numeric equality.
Use:
Objects.equals(a, b)
or unbox safely if null impossible.
19. API Design: Avoid Ambiguous Overload Sets
Bad public API:
void setTimeout(int millis) {}
void setTimeout(long millis) {}
void setTimeout(Duration timeout) {}
This can be usable, but it invites unit ambiguity and overload confusion.
Better enterprise API:
void setTimeout(Duration timeout) {}
If primitive overload exists for performance/internal reasons, keep it private or very carefully named.
private void setTimeoutMillis(long millis) {}
Bad:
void publish(String topic, Object payload) {}
void publish(String topic, byte[] payload) {}
void publish(String topic, String payload) {}
Potential confusion: string payload vs serialized bytes vs domain event object.
Better:
void publishText(String topic, String payload) {}
void publishBytes(String topic, byte[] payload) {}
void publishEvent(String topic, DomainEvent event) {}
Names can be better than overloads when semantics differ.
20. Numeric Domain Rules
20.1 Counters
Use long for counters that can grow beyond local memory assumptions.
long totalRows = repository.count();
Convert to int only at APIs that require int, with exact conversion:
int pageSize = Math.toIntExact(validatedPageSize);
20.2 Money
Do not use binary floating point for money.
BigDecimal amount = new BigDecimal("10.25");
Or use minor units if domain supports it:
long cents = 1025;
20.3 Percentages and Rates
Distinguish:
record Percentage(BigDecimal value) {}
record RatePerSecond(BigDecimal value) {}
record Ratio(BigDecimal numerator, BigDecimal denominator) {}
A double can be useful for telemetry and approximation. It should not silently become financial truth.
20.4 Byte Sizes
Use suffixes intentionally.
long oneGiB = 1L << 30;
Avoid:
int broken = 1024 * 1024 * 1024 * 4; // overflow
21. Testing Strategy for Numeric Expressions
21.1 Boundary Tests
Test around:
0;1;-1;- max/min primitive values;
- values just above/below cast target range;
- large powers of two;
- decimal fractions like
0.1; NaN,Infinity,-0.0if floating point accepted.
21.2 Property Tests
For conversion functions:
int safe = toIntExact(value);
Properties:
- accepts all values within int range;
- rejects values outside int range;
- never silently wraps.
21.3 Golden Tests for API Boundaries
For JSON/DB/event boundary:
- exact large integer round-trip;
- decimal scale preserved;
- timestamp precision preserved;
- enum code not ordinal;
- null wrapper not unboxed accidentally.
22. Refactoring Patterns
22.1 Replace Numeric Primitive with Domain Type
Before:
void approve(long caseId, int priority, double riskScore) {}
After:
void approve(CaseId caseId, Priority priority, RiskScore riskScore) {}
22.2 Replace Overload with Named Method
Before:
void search(String query) {}
void search(int caseId) {}
void search(UUID id) {}
After:
void searchByText(String query) {}
void searchByNumericCaseId(long caseId) {}
void searchByExternalId(UUID id) {}
22.3 Replace Cast with Exact Conversion
Before:
int count = (int) total;
After:
int count = Math.toIntExact(total);
22.4 Replace Integer Division with Ratio Method
Before:
double completionRate = completed / total;
After:
double completionRate = ratio(completed, total);
static double ratio(long completed, long total) {
if (total == 0) {
throw new IllegalArgumentException("total must not be zero");
}
return (double) completed / total;
}
23. Review Checklist
Saat melihat ekspresi numerik atau overload:
- Apakah
byte,short, ataucharterlibat dalam arithmetic? - Apakah
int / inttidak sengaja menghasilkan integer division? - Apakah overflow terjadi sebelum assignment ke
long? - Apakah ada compound assignment yang menyembunyikan narrowing?
- Apakah literal butuh suffix
L,f, ataud? - Apakah
varmengunci tipe yang lebih kecil dari niat desain? - Apakah conditional expression mencampur primitive, wrapper, dan null?
- Apakah overload berbeda hanya pada primitive vs wrapper?
- Apakah overload berbeda hanya pada
intvslongtapi domain sebenarnya unit berbeda? - Apakah
nullmembuat overload ambigu? - Apakah runtime class disangka memengaruhi overload?
- Apakah method name lebih jelas daripada overload?
24. Latihan 20 Menit
Latihan 1 — Prediksi Tipe Hasil
Tentukan tipe hasil:
byte b = 1;
short s = 2;
char c = 'A';
int i = 3;
long l = 4L;
float f = 5.0f;
double d = 6.0;
var r1 = b + s;
var r2 = c + i;
var r3 = i + l;
var r4 = l + f;
var r5 = f + d;
Expected:
r1 -> int
r2 -> int
r3 -> long
r4 -> float
r5 -> double
Latihan 2 — Cari Overflow
int users = 1_500_000_000;
int sessionsPerUser = 3;
long sessions = users * sessionsPerUser;
Fix:
long sessions = (long) users * sessionsPerUser;
Untuk domain critical:
long sessions = Math.multiplyExact((long) users, sessionsPerUser);
Latihan 3 — Overload Prediction
static void m(long x) { System.out.println("long"); }
static void m(Integer x) { System.out.println("Integer"); }
static void m(Object x) { System.out.println("Object"); }
m(1);
Expected: long, because widening primitive in strict invocation beats boxing.
Latihan 4 — Runtime Dispatch
class A {
void f(Object x) { System.out.println("A Object"); }
}
class B extends A {
void f(Object x) { System.out.println("B Object"); }
void f(String x) { System.out.println("B String"); }
}
A a = new B();
Object x = "hello";
a.f(x);
Expected: B Object.
25. Mermaid Summary
Key idea:
assignment target does not retroactively change operation type
runtime class does not retroactively change overload selection
26. Ringkasan
Numeric promotion dan overload resolution adalah dua sistem “tersembunyi” yang banyak bekerja di balik ekspresi Java.
Pegangan utama:
small integral arithmetic becomes int
target assignment type does not control arithmetic unless operands are converted first
widening beats boxing
boxing beats varargs
overloading is compile-time
overriding is runtime
Kedewasaan engineering terlihat dari kemampuan menghindari ekspresi yang “pintar tapi rapuh”. Dalam code enterprise, clarity biasanya menang atas cleverness.
27. Referensi Resmi
- Java Language Specification, Java SE 25 — Chapter 5: Conversions and Contexts.
- Java Language Specification, Java SE 25 — Chapter 15: Expressions.
- Java Language Specification, Java SE 25 — Method invocation, overload resolution, numeric contexts, and expression typing.
You just completed lesson 18 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.