Build CoreOrdered learning track

Conversions, Contexts & Casting Rules

Learn Java Data Types, Type Semantics, Object Model & Data Representation - Part 017

Conversion contexts, widening and narrowing conversions, boxing and unboxing interaction, casting rules, reference casts, unchecked conversion, string conversion, testing contexts, and production-grade API design guidance.

11 min read2101 words
PrevNext
Lesson 1734 lesson track0718 Build Core
#java#data-types#conversions#casting+7 more

Part 017 — Conversions, Contexts & Casting Rules

Target part ini: memahami aturan konversi Java sebagai sistem constraint, bukan sekadar “cast kalau error”. Engineer senior harus bisa membaca ekspresi Java dan langsung tahu: tipe compile-time-nya apa, target type-nya apa, context-nya apa, konversi apa yang diizinkan, apakah ada runtime check, apakah data bisa hilang, dan apakah API sedang menyembunyikan desain yang lemah.

1. Mental Model Utama

Setiap ekspresi Java punya tipe compile-time.

int x = 10;
long y = x;

Ekspresi x bertipe int. Variable y bertipe long. Karena ekspresi int muncul di assignment context dengan target type long, Java memperbolehkan widening primitive conversion.

Modelnya:

expression type -> target type -> context -> allowed conversion -> compile-time/runtime effect

Diagram:

Konversi di Java bukan berarti semua hal bisa berubah menjadi semua hal lain. Konversi selalu dibatasi oleh context.

Contoh:

long a = 10;       // OK: int literal assignment conversion to long
byte b = 10;       // OK: constant narrowing allowed in assignment context
byte c = (byte) a; // OK only because explicit cast is present

Salah satu skill penting adalah membedakan tiga pertanyaan:

  1. Apakah source type dan target type kompatibel?
  2. Apakah context ini mengizinkan conversion tersebut?
  3. Apakah conversion itu aman secara domain?

Java hanya menjawab 1 dan 2 secara formal. Engineer harus menjawab 3.

2. Kenapa Ini Penting di Engineering Nyata

Conversion bug biasanya tidak terlihat sebagai syntax problem. Ia muncul sebagai:

  • jumlah uang berubah karena double ke long;
  • ID 64-bit terpotong saat masuk sistem lama yang memakai int;
  • Boolean null meledak karena unboxing;
  • overload yang dipilih bukan method yang developer kira;
  • cast generic terlihat valid tetapi menghasilkan warning yang diabaikan;
  • JSON number masuk sebagai Integer, Long, Double, atau BigDecimal tergantung parser;
  • API publik menerima Object, lalu menunda type error ke runtime.

Dalam sistem enterprise, conversion adalah salah satu titik rawan boundary.

HTTP/JSON -> DTO -> domain model -> persistence -> event -> consumer

Di setiap panah ada potensi konversi.

3. Kategori Konversi Java

ConversionMakna PraktisRisk Utama
Identity conversionTipe tetap samaBiasanya tidak berisiko
Widening primitivePrimitive ke primitive yang lebih luasBisa kehilangan precision pada integral ke floating
Narrowing primitivePrimitive ke primitive yang lebih sempitBisa kehilangan range, precision, sign
Widening referenceSubtype ke supertypeKehilangan akses compile-time ke API subtype
Narrowing referenceSupertype/interface ke subtypeRuntime check, ClassCastException
BoxingPrimitive ke wrapperAllocation/identity/nullability model berubah
UnboxingWrapper ke primitiveNullPointerException jika wrapper null
Unchecked conversionRaw/parameterized generic boundaryHeap pollution, warning yang tidak boleh diabaikan
Capture conversionWildcard generic internal typingKompleksitas generic API
String conversionValue ke String dalam string contextDiagnostic vs serialization confusion
Forbidden conversionTidak diizinkanCompile-time error

Mental model sederhananya:

widening  = usually easier, often implicit
narrowing = dangerous, usually explicit
boxing    = primitive enters object world
unboxing  = object value extracted into primitive world
reference cast = static promise with possible runtime verification

4. Conversion Contexts

Konversi tidak terjadi di ruang hampa. Java memiliki beberapa context utama.

ContextContohKarakter
Assignment contextlong x = intValue;Binding expression ke variable
Strict invocation contextm(intValue) saat memilih method tanpa boxing/varargsMethod/constructor argument yang cocok melalui identity/widening
Loose invocation contextm(Integer.valueOf(1)) atau m(1) ke wrapperInvocation yang memperbolehkan boxing/unboxing setelah strict gagal
String context"x=" + valueMengubah operand ke String
Casting context(Target) exprExplicit conversion, paling luas tapi berisiko
Numeric contexta + b, -x, ~xNumeric promotion
Testing contextobj instanceof String sPattern compatibility/checking

Kunci: context menentukan konversi mana yang boleh terjadi secara implisit.

5. Assignment Conversion

Assignment conversion terjadi saat value expression dibind ke variable, field, array component, atau parameter-like target lain.

int i = 10;
long l = i;       // widening primitive
Object o = "abc"; // widening reference
Integer n = i;    // boxing

Assignment context cukup permisif, tetapi tetap tidak sembarang.

int i = 10;
Long x = i; // compile-time error

Kenapa?

int tidak otomatis menjadi long, lalu diboxing menjadi Long dalam assignment. Java tidak mengizinkan chain widening primitive -> boxing untuk menghasilkan wrapper berbeda.

Yang valid:

Long a = 10L;       // long literal -> boxing to Long
long b = 10;        // int literal -> widening to long
Number c = 10;      // int -> Integer -> Number? allowed by boxing then widening reference
Object d = 10;      // int -> Integer -> Object

5.1 Constant Narrowing in Assignment

Java memperbolehkan narrowing untuk constant expression tertentu jika nilainya representable.

byte b1 = 100;       // OK
short s1 = 1000;     // OK
char c1 = 65;        // OK

byte b2 = 128;       // compile-time error

Tapi ini hanya untuk constant expression.

int x = 100;
byte b = x;          // compile-time error
byte c = (byte) x;   // explicit cast

Jangan salah tafsir: byte b = 100; bukan bukti bahwa int bebas masuk ke byte. Itu special rule untuk constant expression.

6. Widening Primitive Conversion

Widening primitive conversion sering dianggap “safe”. Ini tidak selalu benar.

int i = 1_234_567_890;
float f = i;
System.out.println(i);
System.out.println((int) f);

int -> float adalah widening primitive conversion, tetapi bisa kehilangan precision.

Rule penting:

FromToCatatan
byteshort, int, long, float, doubleMagnitude preserved, floating biasanya cukup
shortint, long, float, doubleMirip byte
charint, long, float, doubleZero extension
intlong, float, doubleint -> float bisa kehilangan precision
longfloat, doubleBisa kehilangan precision
floatdoubleValue set lebih luas, tapi bukan decimal exactness

Guideline:

widening primitive preserves compile-time legality, not always business exactness

Untuk ID, counter, money minor unit, dan sequence number, jangan asal widen ke floating type.

7. Narrowing Primitive Conversion

Narrowing primitive conversion biasanya butuh explicit cast.

long l = 1_000_000_000_000L;
int i = (int) l;

Cast membuat compiler diam, bukan membuat data aman.

Contoh kehilangan data:

int x = 255;
byte b = (byte) x;
System.out.println(b); // -1

Floating ke integral lebih berbahaya:

System.out.println((int) 12.9);          // 12
System.out.println((int) -12.9);         // -12
System.out.println((int) Double.NaN);    // 0
System.out.println((int) Double.POSITIVE_INFINITY); // 2147483647

Prinsip:

cast is not validation
cast is not rounding policy
cast is not overflow protection

Jika conversion adalah business decision, buat policy eksplisit.

static int toIntExact(long value) {
    return Math.toIntExact(value);
}

Untuk floating rounding:

static long toMinorUnits(BigDecimal amount) {
    return amount.movePointRight(2)
            .setScale(0, RoundingMode.HALF_UP)
            .longValueExact();
}

8. Widening Reference Conversion

Widening reference conversion terjadi saat subtype diperlakukan sebagai supertype.

String s = "abc";
CharSequence cs = s;
Object o = s;

Tidak ada runtime check khusus karena compiler bisa membuktikan bahwa semua String adalah CharSequence dan Object.

Kelebihan:

  • API lebih abstrak;
  • coupling turun;
  • testability naik;
  • implementation bisa diganti.

Risiko:

  • kehilangan operasi spesifik subtype;
  • object tetap mutable jika subtype mutable;
  • abstraction terlalu luas bisa menyembunyikan invariant.

Contoh boundary yang baik:

void render(CharSequence text) {
    // can accept String, StringBuilder, etc.
}

Contoh boundary yang terlalu lebar:

void process(Object input) {
    // type error delayed to runtime
}

9. Narrowing Reference Conversion

Narrowing reference conversion adalah cast dari type yang lebih umum ke type yang lebih spesifik.

Object o = "abc";
String s = (String) o;

Ini runtime check. Jika gagal:

Object o = 123;
String s = (String) o; // ClassCastException

Narrowing reference conversion berarti developer membuat klaim:

I know more than the static type currently says.

Di codebase enterprise, klaim seperti ini harus jarang, lokal, dan mudah diaudit.

9.1 Prefer Pattern Matching for instanceof

Daripada:

if (input instanceof String) {
    String s = (String) input;
    return s.trim();
}

Gunakan:

if (input instanceof String s) {
    return s.trim();
}

Ini mengurangi double mention dan risiko cast salah.

9.2 Cast Tidak Memperbaiki Desain Boundary

Bad smell:

void handle(Object event) {
    if (event instanceof CaseClosedEvent e) {
        handleClosed(e);
    } else if (event instanceof CaseEscalatedEvent e) {
        handleEscalated(e);
    }
}

Lebih baik jika domain mengizinkan:

sealed interface CaseEvent permits CaseClosedEvent, CaseEscalatedEvent {}

record CaseClosedEvent(String caseId) implements CaseEvent {}
record CaseEscalatedEvent(String caseId, String reason) implements CaseEvent {}

Kemudian boundary bisa memakai polymorphism, visitor, pattern matching switch, atau dispatcher yang typed.

10. Boxing and Unboxing in Conversion Contexts

Boxing:

Integer x = 10;

Unboxing:

Integer x = 10;
int y = x;

NPE tersembunyi:

Integer maybeCount = null;
int count = maybeCount; // NullPointerException

Autoboxing tampak seperti syntactic sugar, tetapi ia mengubah model:

primitive value <-> nullable object reference

10.1 Boxing Followed by Widening Reference

Ini valid:

Object o = 10;   // int -> Integer -> Object
Number n = 10;   // int -> Integer -> Number

Tapi ini tidak valid:

Long l = 10;     // int cannot box to Long

10 bertipe int. Boxing-nya adalah Integer, bukan Long.

Yang valid:

Long l = 10L;

10.2 Unboxing Followed by Widening Primitive

Ini valid:

Integer i = 10;
long l = i; // Integer -> int -> long

Tetapi hati-hati dengan null:

Integer i = null;
long l = i; // NPE before widening

11. String Conversion

String conversion sering muncul dalam concatenation.

int count = 3;
String message = "count=" + count;

Aturannya nyaman, tetapi jangan samakan dengan serialization policy.

Instant now = Instant.now();
String audit = "at=" + now;

Untuk log diagnostic, ini biasanya cukup. Untuk API contract, gunakan formatter/serializer eksplisit.

Bad API contract:

String dueDate = "" + localDate;

Better:

String dueDate = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);

String conversion juga bisa memanggil toString() object. Ini berarti:

  • jangan masukkan PII ke toString();
  • jangan gunakan toString() untuk stable persistence format kecuali memang dikontrak;
  • jangan rely pada Object.toString() default untuk business semantics.

12. Unchecked Conversion and Generic Boundaries

Unchecked conversion biasanya muncul saat raw type bertemu parameterized type.

List raw = new ArrayList();
raw.add("abc");

List<Integer> numbers = raw; // unchecked warning
Integer n = numbers.get(0);  // ClassCastException later

Warning ini bukan noise. Ia menunjukkan compiler tidak bisa membuktikan type safety.

Guideline:

@SuppressWarnings("unchecked") boleh hanya di boundary kecil, dengan validasi/isolasi jelas.

Contoh lebih baik:

static List<String> stringsOnly(List<?> input) {
    List<String> result = new ArrayList<>();
    for (Object item : input) {
        if (!(item instanceof String s)) {
            throw new IllegalArgumentException("Expected only strings");
        }
        result.add(s);
    }
    return List.copyOf(result);
}

13. Capture Conversion

Capture conversion adalah mekanisme compiler untuk menangani wildcard.

void reverse(List<?> list) {
    reverseCaptured(list);
}

private <T> void reverseCaptured(List<T> list) {
    Collections.reverse(list);
}

Engineer tidak biasanya “memanggil” capture conversion secara sadar, tetapi harus paham gejalanya:

void broken(List<?> list) {
    Object first = list.get(0); // OK
    // list.set(0, first);      // compile-time error
}

List<?> artinya list dari some unknown type. Compiler tidak boleh menerima Object dimasukkan ke list tersebut karena element type aslinya bisa String, Integer, atau type lain.

14. Testing Contexts and Pattern Compatibility

Testing context muncul di pattern matching.

if (input instanceof String s) {
    System.out.println(s.length());
}

Ini bukan assignment biasa dan bukan cast biasa. Pattern harus compatible dengan expression yang diuji.

Pattern matching membuat narrowing lebih aman karena:

  • check dan binding berada dalam satu operasi;
  • variable pattern hanya hidup di scope yang aman;
  • compiler bisa melakukan flow-sensitive analysis.

Contoh:

static String describe(Object value) {
    if (value instanceof Integer i && i > 0) {
        return "positive int";
    }
    if (value instanceof String s && !s.isBlank()) {
        return "non-blank string";
    }
    return "other";
}

15. Casting Contexts

Casting context lebih luas daripada assignment/invocation context, tetapi bukan bebas risiko.

int x = (int) 10L;
String s = (String) object;
List<String> names = (List<String>) raw;

Tiga jenis risk:

CastCompile-TimeRuntime
Primitive narrowingDiizinkan eksplisitData bisa berubah tanpa exception
Reference narrowingDiizinkan jika plausibleBisa ClassCastException
Unchecked generic castWarningBisa gagal jauh setelah cast

15.1 Cast as Documentation

Kadang cast dipakai untuk memperjelas overload atau numeric intent.

double ratio = (double) completed / total;

Ini cast yang masuk akal karena menjelaskan numeric policy.

Bandingkan dengan:

int count = (int) repository.count();

Ini mencurigakan jika repository.count() mengembalikan long. Apakah count memang pasti muat dalam int? Jika iya, gunakan Math.toIntExact.

int count = Math.toIntExact(repository.count());

16. Conversion Matrix Praktis

SourceTargetBiasanya Implicit?Catatan
byteintYesWidening
intbyteNoCast atau constant narrowing
intlongYesSafe range-wise
longintNoGunakan Math.toIntExact jika harus safe
intfloatYesBisa kehilangan precision
longdoubleYesBisa kehilangan precision
doubleintNoRound toward zero, NaN jadi 0
intIntegerYesBoxing
IntegerintYesUnboxing, NPE jika null
intObjectYesBoxing lalu widening reference
intLongNoTidak ada int -> long -> Long otomatis
StringObjectYesWidening reference
ObjectStringNoCast/pattern check
ListList<String>WarningUnchecked conversion
StringintNoParsing bukan conversion bahasa

17. Parsing Bukan Conversion Bahasa

Ini penting untuk boundary eksternal.

int x = Integer.parseInt("123");
UUID id = UUID.fromString("...");
LocalDate date = LocalDate.parse("2026-06-30");

Parsing adalah library operation, bukan language conversion. Ia bisa gagal, punya policy, format, locale, timezone, dan error handling.

Jangan menyamakan:

conversion = compile-time language rule
parsing    = runtime interpretation of external representation

18. Serialization Boundary Bukan Conversion Context

JSON mapper, JDBC driver, message broker serializer, dan ORM punya aturan sendiri.

record Request(long caseId, BigDecimal amount, Instant submittedAt) {}

Ketika payload JSON masuk, Java language conversion tidak langsung bekerja. Yang bekerja adalah deserializer.

Risiko:

  • JSON number terlalu besar untuk long;
  • decimal dibaca sebagai double;
  • timestamp tanpa timezone dibaca sebagai local time;
  • enum string tidak dikenal;
  • absent field jadi default/null;
  • nullable wrapper di-unbox di domain layer.

Boundary harus punya validasi eksplisit.

record CreatePaymentRequest(
    String amount,
    String currency,
    String submittedAt
) {}

Kemudian parse ke domain type dengan policy yang jelas.

19. Failure Modes

19.1 Silent Truncation

long externalId = 9_000_000_000L;
int internalId = (int) externalId;

Bug: ID berubah, data link rusak, audit trail tidak cocok.

Mitigation:

int internalId = Math.toIntExact(externalId);

19.2 Unboxing Null

Boolean approved = dto.approved();
if (approved) {
    // NPE if approved is null
}

Mitigation:

if (Boolean.TRUE.equals(approved)) {
    // explicit three-state handling
}

Atau modelkan state:

enum ApprovalState {
    APPROVED, REJECTED, NOT_REVIEWED
}

19.3 Wrong Reference Cast

CaseEvent event = loadEvent();
CaseClosedEvent closed = (CaseClosedEvent) event;

Mitigation:

if (event instanceof CaseClosedEvent closed) {
    handle(closed);
} else {
    throw new IllegalStateException("Expected closed event but got " + event.getClass().getName());
}

19.4 Generic Heap Pollution

List raw = List.of("a");
List<Integer> ints = raw;
Integer first = ints.get(0);

Mitigation: isolate raw boundary and validate elements.

19.5 String Conversion as Contract

String exported = "" + amount;

Mitigation: use explicit formatter/serializer with tests.

20. Enterprise API Design Rules

Rule 1: Do Not Accept Object Unless You Are Building Infrastructure

Bad:

void submit(Object command) {}

Better:

void submit(CreateCaseCommand command) {}

If multiple command types are valid:

sealed interface CaseCommand permits CreateCaseCommand, EscalateCaseCommand {}

Rule 2: Use Explicit Conversion Methods at Boundaries

final class CaseId {
    private final long value;

    private CaseId(long value) {
        if (value <= 0) throw new IllegalArgumentException("caseId must be positive");
        this.value = value;
    }

    static CaseId of(long value) {
        return new CaseId(value);
    }

    long asLong() {
        return value;
    }
}

Rule 3: Prefer Exact Methods for Risky Numeric Narrowing

int size = Math.toIntExact(count);

Rule 4: Treat Warning as Design Feedback

Unchecked warnings should be rare and isolated.

@SuppressWarnings("unchecked")
private static <T> List<T> trustedCastList(Object value, Class<T> elementType) {
    if (!(value instanceof List<?> list)) {
        throw new IllegalArgumentException("Expected list");
    }

    for (Object element : list) {
        if (!elementType.isInstance(element)) {
            throw new IllegalArgumentException("Unexpected element type");
        }
    }

    return (List<T>) list;
}

Even here, the suppression is localized and defended.

Rule 5: Do Not Use Cast to Hide an Invariant Problem

Bad:

int age = (int) request.age();

Better:

Age age = Age.of(request.age());

Where Age.of validates range, unit, and domain meaning.

21. Conversion Review Checklist

Saat review code, tanyakan:

  • Apakah conversion ini implicit atau explicit?
  • Apakah conversion ini terjadi karena assignment, invocation, cast, numeric operator, string concat, atau pattern?
  • Apakah ada narrowing primitive?
  • Apakah ada conversion integral ke floating?
  • Apakah ada unboxing dari nullable wrapper?
  • Apakah ada cast reference dari Object, raw type, atau interface luas?
  • Apakah warning generic diabaikan?
  • Apakah toString() dipakai sebagai contract, bukan diagnostic?
  • Apakah parsing external string diperlakukan seperti conversion internal?
  • Apakah boundary punya validation dan error model?
  • Apakah conversion seharusnya menjadi method domain eksplisit?

22. Latihan 20 Menit

Latihan 1 — Prediksi Compile-Time vs Runtime

Tentukan mana yang compile, mana yang gagal compile, mana yang compile tapi runtime risk.

int i = 10;
long l = i;
float f = l;
Long boxedLong = i;
Object o = i;
Integer boxedInt = null;
int n = boxedInt;
String s = (String) o;

Expected reasoning:

long l = i;          OK, widening primitive
float f = l;         OK, widening primitive, precision risk
Long boxedLong = i;  compile-time error
Object o = i;        boxing + widening reference
int n = boxedInt;    compile OK, runtime NPE
String s = (String)o compile OK, runtime ClassCastException because o is Integer

Latihan 2 — Replace Cast with Domain Conversion

Refactor:

record PaymentRequest(long amountInCents) {}

int amount = (int) request.amountInCents();

Menjadi:

int amount = Math.toIntExact(request.amountInCents());

Lalu diskusikan: apakah int memang tipe domain yang tepat? Jika amount bisa besar, mungkin long atau BigDecimal lebih benar.

Latihan 3 — Isolate Unchecked Conversion

Ambil code ini:

List raw = load();
List<String> names = raw;

Ubah menjadi function boundary yang memvalidasi setiap element.

23. Mini Decision Tree

24. Ringkasan

Java conversion system adalah gabungan dari type safety, convenience, dan historical compatibility. Banyak konversi dibuat nyaman agar kode tidak terlalu verbose, tetapi kenyamanan itu bisa menyembunyikan risiko domain.

Pegangan utama:

implicit conversion answers: is this legal Java?
explicit domain conversion answers: is this correct for this business meaning?

Engineer top-tier tidak hanya bertanya “apakah compiler menerima ini?”, tetapi “apakah conversion ini mempertahankan invariant, precision, range, identity, nullability, dan contract lintas boundary?”

25. Referensi Resmi

  • Java Language Specification, Java SE 25 — Chapter 5: Conversions and Contexts.
  • Java Language Specification, Java SE 25 — Chapter 15: Expressions.
  • Java Platform API Documentation, Java SE 25 — java.lang, wrapper classes, and object contracts.
Lesson Recap

You just completed lesson 17 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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.