Java Data Types: A Comprehensive Systems Perspective
1. Foundational Data Types and First Principles
To understand Java's data types, we must begin at the foundation of computational theory: what is a type system? At the hardware level, raw memory bytes are typeless. The interpretation of these bits only occurs when they are loaded into CPU registers. Modern CPUs have dedicated arithmetic units—the ALU (Arithmetic Logic Unit) for integers and the FPU (Floating Point Unit) for decimals—each with distinct instructions.
The 3-Step Typeless Memory Sequence:
- Store: Java writes
11000010to RAM address0x4A. (Is it the integer -62? The character 'Â'?) - Load: The CPU loads the byte from RAM into a register. It is still just typeless bits.
- Execute: The JVM instructs the CPU to execute
FADD(floating-point add) orADD(integer add) on that register. The instruction defines the type!
A data type is an abstraction—a semantic contract enforced by the compiler—that dictates which instructions should be applied to these raw bits.
In computing, languages can be categorized across two primary axes:
- Static vs. Dynamic Typing: When are types checked? (Compile-time vs. Run-time)
- Strong vs. Weak Typing: How strictly are type conversions enforced?
Java is a statically, strongly typed language.
- Statically Typed: The compiler verifies type safety before the code executes (unlike Python or JavaScript, where variables bind to types at runtime).
- Strongly Typed: Implicit conversions that risk data loss are strictly forbidden (unlike C, which implicitly allows casting pointers to arbitrary integers).
Cross-Language Comparison
# Python (Dynamically Typed)
x = 10 # x is an int
x = "Hello" # Valid: x is now a str
// C (Weakly Typed)
int x = 65;
char *p = (char*)&x; // Valid: Reinterprets integer bits as char pointer
// Java (Statically, Strongly Typed)
int x = 10;
x = "Hello"; // Compiler Error: incompatible types
The Syntax Sandbox
Java requires you to declare the type of data a variable will hold. Modern Java allows underscores to make large numbers readable, and prefix notations for different bases:
int standard = 1000000;
int readable = 1_000_000; // Exact same value, easier to read
int binaryLiteral = 0b1010; // Binary (Base 2) for 10
int hexLiteral = 0xFF; // Hex (Base 16) for 255
Variable Scope and Initialization
A variable's "scope" defines where it lives and dies, usually bounded by curly braces {}.
Crucial Rule: Local variables (inside a method) do not get default values. You must initialize them before use, or the code will not compile.
Why no default values for stack variables? It is a mechanical performance optimization. When a method is called, the JVM allocates a "stack frame" by simply moving the stack pointer. The memory in that new frame contains leftover "garbage" bits from previously executed methods. Zeroing out this memory takes CPU cycles. To maximize method invocation speed, Java leaves the garbage bits intact and forces you (the compiler) to prove you've overwritten them before reading.
Stack Memory Allocation (Moving the Pointer)
Before Call: After Method Call:
| | | |
| | | [Garbage Bits] | <- New Stack Frame
| | ====> | [Garbage Bits] | (Not Zeroed!)
|===================| |===================|
| Previous Frame | | Previous Frame |
| | | |
public void doMath() {
int x;
// System.out.println(x); // ERROR: variable x might not have been initialized
x = 5;
System.out.println(x); // Works!
}
Pass-by-Value Semantics
When passing primitive types to methods, Java creates a strict copy of the value.
public void tryToChange(int a) {
a = 99; // This only changes the local copy
}
int myNum = 10;
tryToChange(myNum);
// myNum is still 10 here!
Behind the Syntax
While the syntax sandbox above feels straightforward, every variable declaration fundamentally dictates how the Java Virtual Machine allocates and aligns memory. Let's look past the beginner syntax and bridge the gap to JVM memory allocation.
2. The JVM Memory Model: Stack vs. Heap
Before diving into specific data types, we must understand where and how they are stored in the Java Virtual Machine (JVM). Java partitions memory into several areas, primarily the Thread Stack and the Heap.
- The Stack: Every thread in Java has its own JVM Stack. Each time a method is invoked, a "Stack Frame" is created to store local variables and partial results. Primitive data types (when declared as local variables) are stored directly on the stack frame.
- The Heap: The Heap is the runtime data area from which memory for all class instances and arrays is allocated. Reference types (and primitives that are part of an object/array) reside here.
Thread 1 Stack Heap Memory
┌───────────────────────┐ ┌─────────────────────────┐
│ Method Frame 3 │ │ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ int a = 42; │ │ │ │ Object: String │ │
│ │ boolean b = true; │ │ │ │ value: "Hello" │ │
│ │ String str ───────┼─┼─────────┼─►└───────────────────┘ │
│ └───────────────────┘ │ │ │
└───────────────────────┘ └─────────────────────────┘
Crucial Distinction: A primitive variable directly holds its value in the memory address allocated for it. A reference variable holds a 64-bit (or 32-bit with Compressed Oops) pointer/memory address that points to an object on the heap.
The Virtual Machine as Universal Translator (Endianness)
At the hardware level, different CPUs store multi-byte data types differently. x86 architectures use Little-Endian (least significant byte first), while others use Big-Endian.
The JVM acts as a "Universal Translator." The Java specification strictly defines that all data types are represented and transmitted in Big-Endian order across the JVM, regardless of the underlying hardware. When the JVM interacts with the physical CPU, it transparently translates its internal Big-Endian representation into the CPU's native endianness. This is why a compiled .class file runs identically on an ARM chip and an Intel x86 chip without modifying the bytecode.
3. The 8 Primitive Data Types: A Deep Dive
Java features exactly 8 primitive data types, inherited primarily from C/C++ but with strictly defined sizes to guarantee platform independence (the "Write Once, Run Anywhere" philosophy).
| Type | Memory Size | Min Value | Max Value | Default |
| :--- | :--- | :--- | :--- | :--- |
| byte | 8 bits (1 byte) | -128 | 127 | 0 |
| short | 16 bits (2 bytes)| -32,768 | 32,767 | 0 |
| int | 32 bits (4 bytes)| -2^31 | 2^31 - 1 | 0 |
| long | 64 bits (8 bytes)| -2^63 | 2^63 - 1 | 0L |
| float | 32 bits | IEEE 754 | IEEE 754 | 0.0f |
| double| 64 bits | IEEE 754 | IEEE 754 | 0.0d |
| char | 16 bits | \u0000 (0) | \uffff (65,535) | \u0000|
| boolean| JVM dependent | N/A | N/A | false |
3.1 Integer Types (Two's Complement)
Java represents all integer types (byte, short, int, long) as signed Two's Complement integers. Unlike C, Java does not have unsigned primitives (though Java 8 introduced utility methods like Integer.compareUnsigned).
Two's Complement Mathematics
To find the representation of a negative number:
- Write out the positive number in binary.
- Invert all bits (One's Complement).
- Add 1.
Example: Representing -5 in an 8-bit byte
5 in binary (8-bit) : 0000 0101
Invert bits : 1111 1010
Add 1 : 1111 1011 => This is -5
Integer Overflow and Underflow
Because types have strict hardware-enforced boundaries, operations exceeding these boundaries "wrap around."
int max = Integer.MAX_VALUE; // 01111111 11111111 11111111 11111111 (2,147,483,647)
int overflow = max + 1; // 10000000 00000000 00000000 00000000 (-2,147,483,648)
Proof of Complexity: Checking for overflow on every operation would incur an penalty per addition, slowing down tight mathematical loops. Java prioritizes execution speed over mathematical correctness here, deferring to the developer to use Math.addExact() when safety is paramount.
Production Incident Post-Mortem: 64-bit Tearing on 32-bit JVMs
The Incident: A high-frequency trading application on a legacy 32-bit JVM occasionally read wildly incorrect values from a shared long variable representing the total traded volume.
The Root Cause: On a 32-bit JVM, writing a 64-bit long or double requires two separate 32-bit memory operations. If Thread A wrote the first 32 bits, and a context switch occurred before it could write the second 32 bits, Thread B could read a corrupted, half-written value (a "torn read").
The Fix: Marking the variable with the volatile keyword. In Java, volatile enforces that reads and writes to 64-bit variables are fully atomic, preventing this mechanical tearing.
// Vulnerable to tearing on 32-bit systems
private long totalVolume = 0;
// The Fix: Atomic read/writes guaranteed
private volatile long atomicVolume = 0;
3.2 Floating-Point Types (IEEE 754)
Floating-point numbers in Java (float, double) conform to the IEEE 754 standard. This is arguably the most misunderstood aspect of type systems.
A 32-bit float is partitioned in memory as follows:
- Sign bit: 1 bit
- Exponent: 8 bits
- Mantissa (Fraction): 23 bits
Value =
The Precision Loss Dilemma
Many rational numbers cannot be represented precisely in binary floating-point. For instance, 0.1 in binary is an infinitely repeating fraction: 0.00011001100110011...
Execution Trace:
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println(c); // Prints 0.30000000000000004
Resolution: For precise monetary calculations, ALWAYS use java.math.BigDecimal or scale integers.
Edge Cases: Infinity and NaN
The IEEE 754 standard defines special memory states for undefined mathematics.
double inf = 1.0 / 0.0; // Positive Infinity (Exponent all 1s, Mantissa 0)
double negInf = -1.0 / 0.0; // Negative Infinity
double nan = 0.0 / 0.0; // NaN: Not a Number (Exponent all 1s, Mantissa != 0)
System.out.println(nan == nan); // FALSE! NaN is never equal to itself.
3.3 Character Type (UTF-16)
A Java char is an unsigned 16-bit integer representing a UTF-16 code unit.
Why 16 bits? When Java was created in 1995, the Unicode standard had fewer than 65,536 characters, making 16 bits sufficient to represent every symbol on Earth. However, Unicode later expanded.
The Surrogate Pair Problem:
Characters outside the Basic Multilingual Plane (BMP), like emojis (e.g., 🚀 - U+1F680), require 32 bits (two 16-bit chars).
char c = 'A'; // 16 bits, U+0041
String rocket = "🚀"; // Contains TWO chars (Surrogate pair)
System.out.println(rocket.length()); // Prints 2, not 1!
3.4 Boolean Type JVM Implementation
The boolean type represents true or false. However, the JVM specification has a fascinating quirk regarding its memory layout.
The JVM does not have dedicated bytecode instructions for operating on booleans. Instead:
- Local
booleanvariables are mapped to 32-bitints on the stack (1for true,0for false). boolean[]arrays are mapped tobyte[]arrays, using 1 byte per boolean. Space Complexity impact: A boolean array takes 8x more memory than theoretically necessary (1 bit), trading memory efficiency for CPU byte-addressable speed.
4. Wrapper Classes, Autoboxing, and Memory Overhead
Java primitives are not Objects. They do not inherit from java.lang.Object. To bridge this gap (especially for Generics and Collections), Java provides Wrapper Classes (Integer, Double, etc.).
Autoboxing and Unboxing
Introduced in Java 5, the compiler automatically inserts code to convert between primitives and wrappers.
Source Code:
Integer x = 5; // Autoboxing
int y = x + 10; // Unboxing
Decompiled Bytecode Execution Trace (javap -c):
0: iconst_5
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1 // Store object ref in x
5: aload_1
6: invokevirtual #3 // Method java/lang/Integer.intValue:()I
9: bipush 10
11: iadd
12: istore_2 // Store int in y
The Wrapper Object Layout & Overhead
Using a Wrapper class incurs a massive memory penalty. Let's analyze an Integer object using JOL (Java Object Layout) in a 64-bit JVM with Compressed Oops enabled:
- Mark Word (Header): 8 bytes (used for synchronization, GC state, hashcode)
- Klass Pointer: 4 bytes (pointer to the class definition)
- Primitive
intpayload: 4 bytes - Total size: 16 bytes.
Proof: An array of 1,000,000 ints takes ~4MB. An ArrayList<Integer> containing 1,000,000 elements takes ~4MB (array backing) + 16MB (objects) = ~20MB. An space complexity increase by a constant factor of 5!
The Integer Cache Pool (Flyweight Pattern)
To mitigate object creation overhead, Java heavily caches Integer objects between -128 and 127.
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // TRUE (Same object reference from cache)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // FALSE (Outside cache, two distinct objects allocated on heap)
Note: The upper bound of the cache can be dynamically adjusted via JVM flag: -XX:AutoBoxCacheMax=<size>.
Case Study: Project Valhalla and L1 Cache Locality
The massive memory overhead and cache misses caused by Wrapper classes led to Project Valhalla, an ongoing effort to introduce Inline Types (Value Objects) to Java.
When you iterate over an ArrayList<Integer>, the CPU loads pointers from the array, then chases those pointers across the heap to find the actual Integer objects. This scattered memory access destroys L1 cache locality, forcing the CPU to constantly wait for slow RAM fetches.
Project Valhalla aims to allow custom data structures that act like objects but are stored as flat memory arrays (like primitives), completely eliminating the object header overhead and pointer chasing.
5. Type Casting: Widening and Narrowing
Java strictly manages conversions between primitives to prevent inadvertent data loss.
Implicit Widening (Upcasting)
Conversions where data cannot be lost happen automatically.
byte → short → int → long → float → double
int i = 100;
long l = i; // Implicit, safe
Explicit Narrowing (Downcasting)
Requires an explicit cast (type). This truncates bits, fundamentally altering the data representation.
int large = 130;
byte b = (byte) large;
// 130 in binary 32-bit: 00000000 00000000 00000000 10000010
// Truncated to 8-bit : 10000010
// Two's complement of 10000010 is -126.
System.out.println(b); // Prints -126
6. Edge Cases and Hardened Interview Questions
Question 1: Multi-level casting interpretation What does the following snippet print?
System.out.println( (int) (char) (byte) -1 );
Execution Trace & Proof:
(byte) -1is explicitly0xFF.(char)is an unsigned 16-bit type. Casting negativebytetocharfirst widens it toint(sign extension:0xFFFFFFFF), then narrows it to 16 bits, giving0xFFFF(65,535).(int)widenscharwithout sign extension (sincecharis unsigned).0xFFFFbecomes0x0000FFFF.- Answer:
65535.
Question 2: Floating point precision looping Why does this code run forever?
for (float f = 16777216f; f < 16777220f; f++) {
System.out.println(f);
}
Proof: 16777216 is . A float only has 23 bits of mantissa. At , the implicit leading bit (1) and 23 bits of mantissa are exhausted. Adding 1 to 16777216f yields 16777216f because the next representable IEEE 754 float is 16777218f. f++ essentially does nothing (absorptive addition). Infinite loop!
Question 3: Compound Assignment Magic
Why does A compile, but B fails?
// Snippet A
short s = 10;
s += 5;
// Snippet B
short s = 10;
s = s + 5;
Explanation: Java's + operator automatically promotes operands smaller than int to int before evaluating. In Snippet B, s + 5 evaluates to an int. Assigning an int to a short requires an explicit cast. However, compound operators (+=) automatically include an implicit cast. Snippet A is compiled equivalently to s = (short)(s + 5).
Question 4: Guided Exercise - Branchless Bitwise Absolute Value
Using if (x < 0) x = -x; introduces a CPU branch, which can cause costly pipeline flushes if unpredictable. Can you compute Math.abs(x) without branching?
The Solution:
int x = -10;
int mask = x >> 31;
int abs = (x + mask) ^ mask;
How it works:
x >> 31smears the sign bit across all 32 bits. Ifxis positive,maskis0. Ifxis negative,maskis-1(all1s in binary).- If positive:
(x + 0) ^ 0remainsx. - If negative:
(x + -1) ^ -1. Adding-1is equivalent to subtracting 1. XORing with-1flips all bits. Flipping bits and subtracting 1 is exactly the reverse of Two's Complement (invert and add 1), thus computing the absolute value branchlessly!
Summary of Best Practices
- Never use
floatordoublefor currency: UseBigDecimal. - Be cautious of Autoboxing in loops: It creates extreme GC pressure.
- Use
longfor IDs/Timestamps: The year 2038 problem impacts 32-bitints. - Remember
==tests Reference Identity, not Value Equality for Wrapper objects.
Projects
Applying your knowledge of Java data types is critical to mastering the memory model and system-level implications. Below is a detailed project designed to push your understanding of primitive types, boxing, casting, and memory constraints.
Project 1: The Custom Big Integer Engine
Objective: Build a Java application that handles arithmetic operations for numbers far exceeding the maximum value of a 64-bit long. You will build this without using java.math.BigDecimal or java.math.BigInteger.
Step 1: Define a class LargeNumber that internally represents a giant integer using an array of byte or int primitives. Each element in the array will represent a single digit or a block of digits. Consider the memory implications of byte[] versus int[] in the JVM.
Step 2: Implement the addition logic. You must manually handle the carry-over from one array index to the next, exactly mimicking how the CPU handles integer arithmetic, but in base-10 or base-256.
Step 3: Implement subtraction and multiplication. For multiplication, you will need to understand the nested loops required for multi-digit arithmetic, being incredibly careful about integer overflow when multiplying two int components.
Step 4: Add a memory stress test. Initialize one million LargeNumber instances. Profile your application using VisualVM or similar tools to compare the heap memory consumed by your custom implementation versus the standard BigInteger. Reflect on the object header overhead and array length metadata that Java attaches to every object.
Assignments
These assignments will help you test edge cases, casting behaviors, and performance bottlenecks associated with Java's type system.
Assignment 1: The Float Precision Trap
Write a Java program that simulates a bank ledger. Start with a balance of $10,000.00. Use the float data type to deduct $0.10 exactly 100,000 times.
Deliverable 1: Print the final balance. Explain in a comment exactly why the final result is not precisely zero, referencing the IEEE 754 mantissa and exponent layout.
Deliverable 2: Rewrite the same logic using int, representing the balance entirely in cents (e.g., 1000000 cents). Perform the exact same deductions and print the final result. Write a short paragraph comparing the execution speed and accuracy of the integer approach versus float.
Assignment 2: Unmasking Autoboxing Performance
Create a program that calculates the sum of all integers from 1 to 10,000,000.
Deliverable 1: Implement the loop using Integer as the accumulator variable (e.g., Integer sum = 0;). Time the execution using System.nanoTime().
Deliverable 2: Implement the exact same loop using a primitive int for the accumulator. Time the execution.
Deliverable 3: Write a detailed analysis of the performance difference. Calculate approximately how many unnecessary objects were allocated on the heap during the first loop and explain the strain this places on the Garbage Collector.
Debugging Guide
When working with Java data types, bugs often manifest silently due to overflow or precision loss rather than throwing exceptions. Here is a guide to debugging common data type issues.
Common Bug: Silent Integer Overflow
Symptom: A calculation involving large numbers suddenly yields a massive negative number. For example, multiplying 1,000,000 by 3,000 results in -1294967296.
Fix: This occurs because the maximum value of a 32-bit int is 2,147,483,647. When a calculation exceeds this, it wraps around into negative values. To fix this, you must cast at least one of the operands to a long before the operation takes place. Changing the result type is not enough. Fix: long result = 1000000L * 3000;
Common Bug: The Wrapper Equality Trap
Symptom: You have two wrapper objects, Integer a = 150; and Integer b = 150;, but if (a == b) evaluates to false. However, if they are 100, it evaluates to true.
Fix: The == operator compares object references, not the underlying primitive values. Java caches Integer objects between -128 and 127. Values outside this range are allocated as distinct objects on the heap. Always use .equals() when comparing wrapper classes: if (a.equals(b)).
Common Bug: Floating Point Equality
Symptom: A loop condition like while (myFloat != 1.0f) never terminates, even when myFloat conceptually equals 1.
Fix: Floating-point math is imprecise. Repeated additions (like 0.1f ten times) will rarely equal exactly 1.0f. Instead of strict equality, use an epsilon value to check if the difference is within an acceptable tolerance: Math.abs(myFloat - 1.0f) < 0.0001f.
Testing Strategy
Testing data types, especially numerical operations and casting boundaries, requires a deliberate approach to edge cases. Since types dictate the physical boundaries of data, your tests must target these specific thresholds.
1. Boundary Value Analysis
Always test the absolute limits of your data types. If a method accepts an int parameter, your test suite must explicitly invoke that method with Integer.MAX_VALUE, Integer.MIN_VALUE, 0, -1, and 1. Many algorithms work perfectly for small positive numbers but crash spectacularly or enter infinite loops when subjected to MIN_VALUE due to unexpected two's complement behavior (e.g., Math.abs(Integer.MIN_VALUE) returns a negative number!).
2. Type Casting Verification
If your application performs explicit narrowing casts (e.g., downcasting a long timestamp to an int), you must write unit tests that verify behavior both before and after the truncation threshold. Provide a test case that passes a long within the int bounds and verifies the cast is lossless. Then, provide a test case that passes a long exceeding 2^31-1 and strictly assert the expected (often truncated and negative) outcome, ensuring downstream systems handle this graceful degradation correctly.
3. Precision Loss Assertions
For any financial or scientific calculations, tests should explicitly verify that precision is maintained. If using double or float, use JUnit's overloaded assertEquals(expected, actual, delta) to explicitly document the acceptable margin of error. If exact precision is required, your tests should assert that the implementation utilizes BigDecimal rather than primitives, potentially using reflection to inspect the method signatures or field types in your domain models.
FAQs
Q: Why doesn't Java support unsigned integers like C and C++?
A: Java's creator, James Gosling, intentionally omitted unsigned types to simplify the language. In C, mixing signed and unsigned integers often leads to nasty, hard-to-find bugs due to implicit conversion rules. By making all standard integers signed, Java eliminates this entire class of bugs. If you truly need unsigned behavior, Java 8 introduced utility methods in the Integer and Long classes (like Integer.parseUnsignedInt and Integer.divideUnsigned) to interpret the bits as unsigned.
Q: If boolean only needs 1 bit, why does it consume an entire byte (or more) in memory? A: This is a hardware and JVM architectural limitation. Modern CPUs are "byte-addressable," meaning the smallest unit of memory they can fetch from RAM is a single byte (8 bits). Trying to address a single bit would require fetching the whole byte and applying expensive bitwise masking operations. The JVM sacrifices memory density for execution speed by aligning booleans to byte boundaries.
Q: What is the difference between null and 0 when initializing variables?
A: 0 is an actual numeric value that exists in memory for primitive types (like int, double). null is the absence of a value, meaning a reference variable does not point to any object on the heap. Primitives can never be null. Only Reference Types (including Wrapper classes like Integer) can be null. If an Integer object is null and you attempt to automatically unbox it into an int, the JVM will throw a NullPointerException.
Revision Notes / Cheat Sheet
Use this cheat sheet to quickly review the memory layout and fundamental constraints of Java's primitive data types before an exam or interview.
| Data Type | Size in Memory | Fundamental Use Case | Critical Edge Case / Trap |
| :--- | :--- | :--- | :--- |
| byte | 8 bits (1 byte) | Raw file I/O, network streams, low-level binary data. | Narrowing an int > 127 to byte wraps to negative. |
| short | 16 bits (2 bytes) | Rarely used. Legacy 16-bit hardware compatibility. | Promotes to int during arithmetic operations automatically. |
| int | 32 bits (4 bytes) | Default type for whole numbers and loop counters. | Max value is ~2.14 billion. Overflow is silent. |
| long | 64 bits (8 bytes) | Database IDs, timestamps (milliseconds since epoch). | Must suffix with L (e.g., 100L), otherwise evaluated as int. |
| float | 32 bits (4 bytes) | Graphics programming, saving memory in massive arrays. | Must suffix with f. Suffers from IEEE 754 precision loss. |
| double | 64 bits (8 bytes) | Default type for decimal math. Standard scientific type. | Never use for exact currency calculations. Use BigDecimal. |
| char | 16 bits (2 bytes) | Representing a single UTF-16 code unit. | Emojis and obscure symbols take TWO char primitives. |
| boolean| 8+ bits (JVM spec) | Conditional logic, state flags. | Represented as int (32-bit) on the JVM stack. |
Visualization: Type Conversions
Below is a visualization illustrating how narrowing and widening conversions flow through Java's primitive hierarchy.
graph TD
B[byte: 8-bit] -->|Widening: Safe| S[short: 16-bit]
S -->|Widening: Safe| I[int: 32-bit]
C[char: 16-bit unsigned] -->|Widening: Safe| I
I -->|Widening: Safe| L[long: 64-bit]
I -.->|Widening: Precision Loss Possible| F[float: 32-bit IEEE 754]
L -.->|Widening: Precision Loss Possible| F
L -.->|Widening: Precision Loss Possible| D[double: 64-bit IEEE 754]
F -->|Widening: Safe| D
D ==>|Narrowing: Explicit Cast Required| I
L ==>|Narrowing: Explicit Cast Required| I
I ==>|Narrowing: Explicit Cast Required| B
style B fill:#e1f5fe,stroke:#01579b
style I fill:#b3e5fc,stroke:#01579b
style L fill:#81d4fa,stroke:#01579b
style F fill:#ffecb3,stroke:#ff6f00
style D fill:#ffe082,stroke:#ff6f00