Java Data Types and Core Mechanisms
Java categorizes data types into two distinct groups: primitive types and reference types. Primitive types store their values directly on the stack, offering high performance for basic calculations. Reference types, such as classes, interfaces, and arrays, store a memory address on the stack that points to the actual object data residing in the heap.
Primitive Data Types
Java defines eight primitive data types to handle fundamental data values.
Integer Types
Integer types represent whole numbers without fractional parts. They vary in size to optimize memory usage for different scenarios.
| Type | Size (Bytes) | Range | Default Value |
|---|---|---|---|
| byte | 1 | -128 to 127 | 0 |
| short | 2 | -32,768 to 32,767 | 0 |
| int | 4 | -2^31 to 2^31-1 | 0 |
| long | 8 | -2^63 to 2^63-1 | 0L |
Floating-Point Types
These types handle numbers with fractional components, adhering to the IEEE 754 standard.
| Type | Size (Bytes) | Default Value |
|---|---|---|
| float | 4 | 0.0f |
| double | 8 | 0.0d |
Character and Boolean Types
- char: Occupies 2 bytes using UTF-16 encoding. Default value is '\u0000'.
- boolean: Represents logical values (
trueorfalse). The JVM typically compiles boolean variables tointtype (4 bytes), though in boolean arrays, they occupy 1 byte.
Wrapper Classes and Autoboxing
Each primitive type has a corresponding wrapper class in the java.lang package (e.g., Integer for int, Double for double). These classes allow primitives to be treated as objects, which is necessary for usage in Collections.
Autoboxing is the automatic conversion from a primitive to its corresponding wrapper class, while Unboxing is the reverse process.
Integer wrappedVal = 100; // Autoboxing: calls Integer.valueOf(100)
int rawVal = wrappedVal; // Unboxing: calls wrappedVal.intValue()
Excessive autoboxing/unboxing can degrade performance, so direct primitive usage is preferred for intensive calculations.
The Integer Cache
To optimize memory, Java maintains a cache of Integer instances for values between -128 and 127. When assigning a value within this range, the JVM returns a cached instance rather than creating a new object.
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true, same cached instance
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false, new instances created
String Handling
Strings in Java are immutable objects. Once created, their value cannot be modified. Operations that appear to modify a String actually create a new object. This immutability ensures thread safety and allows for String pooling.
String Creation and Memory
Creating a String via a literal (e.g., String s = "text") checks the String Constant Pool first. If the string exists, the reference is reused. If not, a new object is added to the pool.
Using the constructor (e.g., String s = new String("text")) creates a new object in the heap memory distinct from the pool, potentially resulting in two objects if the literal is not already in the pool.
String, StringBuilder, and StringBuffer
- String: Best for constants. Immutable and thread-safe.
- StringBuilder: Suitable for single-threaded environments where strings are modified frequently. Not thread-safe but faster.
- StringBuffer: Thread-safe version of StringBuilder, using synchronized methods. Use in multi-threaded contexts.
Object Class Methods
The java.lang.Object class is the root of the class hierarchy. Every class inherits methods like getClass(), hashCode(), equals(Object obj), clone(), toString(), and finalize(). The wait(), notify(), and notifyAll() methods are essential for thread synchronization and are located in Object because locks in Java are object-based.
Equality and Hashing
== vs equals()
The == operator compares memory addresses for reference types and values for primitives. The equals() method, by default, compares addresses but should be overridden to compare logical content.
Contract between equals() and hashCode()
When overriding equals(), one must also override hashCode(). The contract dictates that if two objects are equal via equals(), they must return the same hash code. This ensures objects behave correctly in hash-based collections like HashMap and HashSet.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return id == product.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
Common Type Behaviors and Pitfalls
Floating Point Precision
Java treats floating-point literals as double by default. Assigning a double to a float variable without a cast causes a compilation error.
float val = 3.4; // Compilation error
float val = 3.4f; // Correct syntax
Short Arithmetic
Numeric operations involving types smaller than int automatically promote the operands to int. Consequently, the result must be cast back explicitly if assigning to a smaller type.
short num = 10;
num = num + 1; // Error: result is int
num += 1; // OK: implicit cast performed by compound operator
Large Numbers
For numbers exceeding the 64-bit long limit, the java.math.BigInteger class provides arbitrary-precision integers. It handles overflow correctly but with lower performance than primitive types.
Operators and Utilities
Logical AND Operators
The && operator is a short-circuit logical AND; if the left operand is false, the right is not evaluated. The & operator evaluates both operands and can also function as a bitwise AND.
Deep vs. Shallow Copy
A shallow copy duplicates the object's fields, but for reference fields, only the address is copied. A deep copy creates new instances for referenced objects, resulting in a completely independent object graph.
Math.round() Behavior
The Math.round() method implements rounding by adding 0.5 to the value and then flooring it. For example, Math.round(11.5) returns 12, while Math.round(-11.5) returns -11.