Every Java value has either a primitive or reference type. Primitives directly represent Boolean, integer, floating-point, or UTF-16 code-unit values; a reference points to an object or array, or is null.
Widening can still lose precision and narrowing can truncate or wrap. Wrapper types add null-unboxing and reference-equality hazards.
Choose types from the value range and missing-value semantics, validate conversions at boundaries, and express domain constraints with BigDecimal, Math.*Exact, and value equality.
What it is and why it exists
A data type defines what a value can represent and which operations accept it. Java is statically typed: before code runs, the compiler checks whether assignments, method calls, and expressions can be combined under the language rules. A variable’s type does not change when it later holds a different value; var merely asks the compiler to infer one fixed local-variable type from the initializer.
Java divides types into primitive types and reference types . The eight primitive types are built-in value categories; classes, interfaces, arrays, and type variables are reference types. A primitive variable cannot hold null, while a reference variable holds a reference to an object or array, or the null reference.
You encounter this distinction at almost every API boundary. Arithmetic expressions apply numeric promotion, generic collections require wrapper types, database or deserialized data may produce null, and text processing must distinguish a char from a complete Unicode code point. Knowing the rules lets you ask whether a compiler-approved conversion also preserves the business meaning.
The eight primitive types
Java specifies the widths and ranges of its integral types, so those semantics do not change with the processor platform. A boolean has only the values true and false; the Java Language Specification does not prescribe its storage size in an object layout.
| Type | Category | Width | Values or meaning |
|---|---|---|---|
byte | Signed integer | 8 | −128 through 127 |
short | Signed integer | 16 | −32,768 through 32,767 |
int | Signed integer | 32 | −2³¹ through 2³¹−1 |
long | Signed integer | 64 | −2⁶³ through 2⁶³−1 |
char | Unsigned integer | 16 | One UTF-16 code unit, 0 through 65,535 |
float | IEEE 754 binary floating point | 32 | Finite values, signed zeros, infinities, and NaN |
double | IEEE 754 binary floating point | 64 | Finite values, signed zeros, infinities, and NaN |
boolean | Boolean | Unspecified | true or false |
int is usually the default integer choice, and an integer literal also has type int by default. Integer literals outside that range normally need an L suffix to become long. Floating-point literals default to double; assigning one to float requires an F suffix or a cast. Suffixes select literal types, but they do not prevent runtime arithmetic from overflowing or rounding.
Reference types and objects
A reference value is not the object’s contents. Assigning an array, string, or ordinary object reference to another variable makes both variables point to the same object. If the object is mutable, a change through one reference is visible through the other. Java passes every argument by value; for a reference type, the copied value is the reference itself.
An array is an object even when its elements are primitives. An int[] has int elements, whereas an Integer[] has reference elements and each element may be null. Generic type arguments cannot be primitive types, so List<int> is illegal and a general-purpose integer collection uses List<Integer>.
Defaults belong only to certain storage locations
Instance fields, static fields, and elements of a newly created array receive default values. Numeric primitives default to zero, char to \u0000, boolean to false, and reference types to null. These defaults give objects and arrays a defined state immediately after construction.
Local variables have no default value. The compiler performs definite-assignment analysis, and reading a local that is not certainly assigned on that path is a compile-time error. This rule stops a method from treating uninitialized stack state as a valid business value.
Select types from a contract
A type choice starts with the permitted value set, not the smallest representation that fits today’s sample. Use int for ordinary bounded counts and long when the documented range requires it. If overflow is an error rather than modular arithmetic, the contract also needs validation or exact operations; choosing a wider type alone only moves the boundary.
Absence is a separate decision from numeric range. A primitive says every instance has a value, while a wrapper can encode absence with null but requires every consumer to handle that state. A sentinel such as -1 is safe only when the domain excludes it and the API documents its meaning.
Some data that looks numeric is not a quantity. Account numbers, postal codes, and externally assigned identifiers may need leading zeros, formatting, or more digits than arithmetic types provide. Represent them as text or a domain type unless arithmetic operations are part of their contract.
Use byte[] for encoded bytes and char or String for UTF-16 text, but do not convert between them by casting. Text-to-byte conversion requires an explicit character encoding such as UTF-8. That boundary belongs in an encoder or decoder API where malformed-input behavior can be chosen deliberately.
How it works
Declarations, assignments, and calls
A variable declaration establishes a static type, and an assignment expression must produce that type through an assignment conversion. Method invocation uses method-invocation conversions to determine which overloads apply. A cast unlocks additional conversions, but it only asks Java to change a representation or check a reference type according to fixed rules; it does not prove that the result fits the business range.
A reference assignment can widen, such as assigning an ArrayList<String> reference to a List<String> variable. A downcast is explicit and checks at runtime whether the object belongs to the target type; failure throws ClassCastException. Pattern matching for instanceof combines the test with a safe local binding.
Conversion phases in overloaded calls
Overload resolution considers conversions in phases. Fixed-arity candidates that need only identity, widening primitive, or widening reference conversions are considered before candidates that require boxing or unboxing. Variable-arity candidates are considered only after the fixed-arity phases fail.
This ordering means adding a wrapper overload can change which generated call is selected, even though each method would compile by itself. A primitive widening overload may win before a boxing overload, while an existing wrapper reference may widen to Object in an earlier phase than it could unbox for a primitive parameter. Read the declared argument type as well as its runtime value when checking a call.
The null literal can be passed to reference parameters but not primitive parameters. If unrelated reference overloads are equally specific, a call with bare null can become ambiguous at compile time. Avoid overload families whose only distinction is unrelated wrapper types; a domain-specific method name usually expresses the contract more clearly.
Widening and narrowing primitive conversions
A widening primitive conversion sends a value to a target type allowed by the language. byte, short, and char can widen to int, and integral types can continue through the allowed wider integral or floating-point types. Widening an integer to a wider integer preserves the value, but int to float, long to float, and long to double can lose precision because the target has too few significant bits.
A narrowing conversion requires an explicit cast except for a few constant-assignment rules. Narrowing an integer discards high bits and can change the sign. Converting floating point to an integer first rounds toward zero, then applies the specified results for NaN and out-of-range values. An ordinary numeric cast does not throw merely because the source is outside the target range.
The common widening paths are below. An arrow implied by the table means a widening conversion exists, not that every step preserves numeric precision.
| Source | Permitted widening targets |
|---|---|
byte | short, int, long, float, double |
short | int, long, float, double |
char | int, long, float, double |
int | long, float, double |
long | float, double |
float | double |
boolean does not participate in numeric conversion. Java neither accepts 0 and 1 as Boolean values nor permits casts between Boolean and integral types. This keeps the intent of condition expressions explicit.
Numeric promotion and intermediate results
Unary and binary numeric operations perform numeric promotion . byte, short, and char are promoted to int for most arithmetic, so even the type of two added byte values is int. Mixed numeric operands use binary numeric promotion to select a common type.
A wider result variable cannot rescue an intermediate result that already overflowed. In long total = intPrice * intQuantity, multiplication finishes as int before the result is widened to long. Widening one operand before multiplication, or using Math.multiplyExact, changes the range of the multiplication itself or turns overflow into an exception.
Compound assignment also includes implicit narrowing. count += delta is roughly equivalent to count = (T) (count + delta), where T is the left-hand type, except that the left side is evaluated only once. A byte += expression can therefore compile and still wrap silently.
Boxing, unboxing, and null
Autoboxing converts a primitive value to the corresponding wrapper reference, such as int to Integer. Unboxing performs the reverse conversion. The compiler inserts these conversions when generic collections, assignments, calls, or expressions require them.
A wrapper object has reference identity, but most application code cares about the wrapped value. == on two Integer references compares identity unless the expression first triggers unboxing. The specification guarantees identity for some boxed small constants, which can make broken code appear correct with small test data. Use equals or Objects.equals to compare wrapper values.
Unboxing a null wrapper reference throws NullPointerException. The unboxing can be hidden inside arithmetic, a relational comparison, a conditional expression, a method argument, or an enhanced for loop. An API must state whether a missing value is allowed, then validate it, supply a domain-meaningful default, or preserve the reference form before unboxing.
Floating point, decimals, and text
float and double use binary floating-point representations. Many decimal fractions have no finite binary representation, so the result of 0.1 + 0.2 is not the same double as 0.3. These types suit measurements, scientific calculations, and values with an accepted error model, but they usually do not suit money that requires exact decimal values and a fixed rounding policy.
BigDecimal represents a decimal with an arbitrary-precision integer and a scale. Use the string constructor when the input is external decimal text; new BigDecimal(0.1) faithfully captures the already inexact double. Division may need an explicit precision or rounding mode. equals also compares scale, while compareTo compares numeric order.
A char represents one UTF-16 code unit, not necessarily one complete user-visible character. A Unicode code point outside the Basic Multilingual Plane uses a surrogate pair, so a string’s length() can differ from its code-point count. Use codePoints(), codePointAt, or code-point offset APIs when you need to traverse complete code points.
Arrays preserve runtime component types
An array object remembers its component type at runtime. Java arrays are covariant, so a String[] can be assigned to an Object[] variable, but the underlying object is still a string array. Storing a non-string through that broader reference compiles and then throws ArrayStoreException.
Generic collections make a different tradeoff. List<String> is not a subtype of List<Object>, so the analogous unsafe write is rejected at compile time. Generic type arguments are erased in ordinary Java generics, while an array’s component-type check remains reified at runtime.
Primitive arrays also avoid per-element boxing semantics. An int[] element always has an int value and begins at zero; an Integer[] element is a reference and begins at null. Pick between them from the API’s need for generic interoperability and missing values, then measure performance only if it matters.
Examples
Primitives and references
public class PrimitiveValues {
public static void main(String[] args) {
byte warehouseZone = 12;
int unitsInStock = 2_400;
long orderId = 3_000_000_000L;
double packageWeightKg = 1.75;
char currencySymbol = '€';
boolean paid = true;
int[] dailyOrders = {18, 21, 16};
int[] sameOrders = dailyOrders; // Copies the reference, not the array elements.
sameOrders[0] = 20;
System.out.println("zone=" + warehouseZone + ", stock=" + unitsInStock);
System.out.println("order=" + orderId + ", weight=" + packageWeightKg);
System.out.println("currencyUnit=" + (int) currencySymbol + ", paid=" + paid);
System.out.println("firstDay=" + dailyOrders[0]);
}
}zone=12, stock=2400
order=3000000000, weight=1.75
currencyUnit=8364, paid=true
firstDay=20The literal suffixes give orderId and packageWeightKg the required types. The last line demonstrates that array variables hold references: changing the element through sameOrders makes the same change visible through dailyOrders.
Conversions and overflow checks
public class NumericConversions {
public static void main(String[] args) {
int exactInteger = 16_777_217;
float roundedFloat = exactInteger; // Widens the type but loses one unit of precision.
int shipmentCode = 130;
byte narrowedCode = (byte) shipmentCode; // Keeps only the low eight bits.
byte packedCount = 12;
packedCount += 120; // Includes an implicit narrowing conversion.
System.out.printf("int=%d float=%.0f%n", exactInteger, roundedFloat);
System.out.println("narrowed=" + narrowedCode + ", compound=" + packedCount);
try {
Math.toIntExact(3_000_000_000L);
} catch (ArithmeticException error) {
System.out.println("overflow detected");
}
}
}int=16777217 float=16777216
narrowed=-126, compound=-124
overflow detectedThe first line shows that widening does not imply exact conversion. The two byte results come from retaining low bits rather than throwing. Math.toIntExact fits a boundary that must reject an out-of-range value.
Wrappers and missing values
import java.util.List;
import java.util.Objects;
public class BoxingValues {
public static void main(String[] args) {
List<Integer> quantities = List.of(2, 3, 4); // Adding elements requires boxing.
int total = 0;
for (int quantity : quantities) { // Reading each element requires unboxing.
total = Math.addExact(total, quantity);
}
Integer expected = 9;
Integer actual = Integer.valueOf(total);
Integer missing = null;
System.out.println("total=" + total);
System.out.println("matches=" + Objects.equals(expected, actual));
System.out.println("fallback=" + Objects.requireNonNullElse(missing, 0));
}
}total=9
matches=true
fallback=0The collection’s element type must be Integer, while the accumulator can remain an int. Objects.equals handles value comparison and null references. A default is appropriate only when the domain genuinely interprets absence as zero.
Exact decimals and Unicode code points
import java.math.BigDecimal;
import java.math.RoundingMode;
public class DecimalAndText {
public static void main(String[] args) {
BigDecimal unitPrice = new BigDecimal("19.95");
BigDecimal quantity = BigDecimal.valueOf(3);
BigDecimal taxRate = new BigDecimal("0.075");
BigDecimal subtotal = unitPrice.multiply(quantity);
BigDecimal tax = subtotal.multiply(taxRate)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal oneDecimal = new BigDecimal("1.0");
BigDecimal twoDecimals = new BigDecimal("1.00");
String vehicle = "🚗";
System.out.println("subtotal=" + subtotal + ", tax=" + tax);
System.out.println("numericEqual=" + (oneDecimal.compareTo(twoDecimals) == 0));
System.out.println("equals=" + oneDecimal.equals(twoDecimals));
System.out.println("units=" + vehicle.length()
+ ", codePoints=" + vehicle.codePointCount(0, vehicle.length()));
}
}subtotal=59.85, tax=4.49
numericEqual=true
equals=false
units=2, codePoints=1The money enters BigDecimal from decimal strings, and the rounding rule appears only at the boundary where tax needs two fractional digits. The other lines expose BigDecimal’s scale semantics and the difference between UTF-16 code-unit and Unicode code-point counts.
Pitfalls
Conversion boundaries in detail
Constant-expression narrowing assignments
The integer literal 127 has type int, yet byte limit = 127 compiles. Assignment conversion may narrow an int constant expression to byte, short, or char when its value is representable in the target. This is an exception proved safe at compile time, not a general conversion between runtime integer variables.
If you first write int limitValue = 127 and then assign limitValue to a byte, a cast is required because the expression no longer meets the relevant constant-expression rule. A final int limitValue = 127 can remain a constant variable when it satisfies the constant-variable rules. In generated-code reviews, distinguish a compile-time proof from runtime data.
Defined floating-to-integral results
When floating point narrows to an integer, Java does not use a caller-selected rounding mode. A finite value first rounds toward zero. A value outside the target integer range becomes the corresponding minimum or maximum, and NaN becomes zero. Narrowing farther to byte or short then discards high bits.
That determinism is not business safety. Casting an unvalidated double to an order quantity can quietly turn NaN into 0 or positive infinity into the largest integer. If those inputs must be rejected, check Double.isFinite and the explicit range before converting.
The minimum boxing-identity guarantee
For certain compile-time constants, the specification requires repeated boxing to produce ==-identical references. These include Boolean values, char from \u0000 through \u007f, and integral values from −128 through 127. An implementation may cache more values, so identity outside the minimum range is not a portable assumption.
This is why wrapper == defects often escape unit tests. If every test value is inside the minimum cache range, identity comparison happens to match the expected value comparison. Express value semantics with equals, Objects.equals, or an intentional comparison after unboxing.
BigDecimal value and representation
A BigDecimal value contains an unscaled integer and a scale, so 1.0 and 1.00 have the same numeric ordering but different representations. compareTo returns zero for numeric equality; equals is true only when both value and scale agree. The distinction matters when BigDecimal is a HashMap key because hash equality follows equals.
The calculation policy must also define division and rounding boundaries. A division with no exact terminating result throws ArithmeticException when no rounding context is supplied. Do not let generated code pick HALF_UP or two decimal places arbitrarily; the currency, tax rule, or domain protocol decides the scale and rounding mode.
Runtime checks for reference casts
A reference cast does not transform an object into an instance of another class. It changes the static view after a runtime compatibility check. Casting an Object that actually refers to a Long into Integer fails even when both wrappers hold numerically comparable values.
Use instanceof when several runtime variants are an expected part of the input contract. Use polymorphism or a sealed hierarchy when the variants belong to the domain model. Catching ClassCastException after speculative casts usually hides a boundary that should have been checked or typed explicitly.
null behaves differently: casting null to a reference type succeeds and still yields null. A successful cast therefore proves type compatibility only for a non-null object; it does not prove presence. Keep the null check visible when the following operation dereferences the result.
Reflection and deserialization APIs often expose values as Object, which invites generated casts based on example payloads. Verify the producer’s declared schema and all supported numeric wrapper classes before casting. Converting through the Number API may still narrow or round, so it also needs a range policy.
Signed zero, infinity, and NaN
Floating-point types include positive zero, negative zero, positive and negative infinity, and NaN. Ordinary == treats the two zeros as equal, while reciprocals can reveal their signs. NaN compares unequal to every value, including itself, so test it with Float.isNaN or Double.isNaN.
Division by floating-point zero produces an infinity or NaN according to IEEE 754 semantics; it does not throw the integer ArithmeticException used for division by integer zero. Generated validation that merely catches ArithmeticException therefore does not reject non-finite floating-point results. Use isFinite when an API accepts only ordinary finite measurements.
Relational operators do not provide a useful total ordering when NaN is present. Sorting and ordered containers should use the library comparison contract, such as Double.compare or Double.compareTo, and the application should decide whether non-finite values are valid at all. Do not improvise an ordering with subtraction and a cast.
BigDecimal has no NaN or infinity values. That makes invalid decimal input fail rather than propagate as a special numeric value, but it does not remove the need for bounds, scale, and rounding rules. Exact representation and valid business input are separate guarantees.
Type rules versus runtime layout
The language specification fixes value ranges and conversion semantics, but it does not promise that a local variable or wrapper object occupies a particular number of bytes on a given JVM. Object headers, reference widths, alignment, and JIT optimization are implementation details. Without a measurement on the target JVM, do not derive whole-object sizes or performance multiples from primitive widths.
Likewise, autoboxing’s source-level semantics do not guarantee that every operation allocates a distinct observable object. The compiler and JVM may eliminate allocations when observable behavior is unchanged. Measure performance-sensitive code with an appropriate benchmark, but never make correctness depend on whether that optimization occurs.
When a wire format or file layout requires an exact width, use the format’s byte-level contract rather than an assumed JVM object layout. ByteBuffer and explicit encoders make byte order, width, and failure handling visible.
When an API only needs value semantics, expose primitives or immutable domain values instead of wrapper identity. This keeps implementation caching and allocation decisions outside the contract.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug