Java Shift Operators: Signed and Unsigned Bit Shifts
Java's shift operators (`<<`, `>>`, `>>>`) move bits left or right. Learn how signed and unsigned shifts behave with negative numbers, integer promotion, and shift distance masking.
Java's shift operators are <<, >>, and >>>. They move the bits of an integer value left or right by a specified number of positions. The result is a new integer value: none of the operators modify the original variable. Although simple in appearance, these operators have behavior that depends on the type of the left operand, the sign of the value, and the shift distance. Misunderstanding any of these leads to subtle bugs that are difficult to spot in code reviews.
The three operators cover two distinct kinds of shifts. << is the signed left shift. It moves bits toward the most significant position and fills the vacated low-order bits with zeros. >> is the signed right shift. It moves bits toward the least significant position and fills the vacated high-order bits with the sign bit, which preserves the sign of negative numbers. >>> is the unsigned right shift. It always fills the high-order vacated bits with zeros, regardless of the sign.
How the Left Shift Operator Works
The left shift operator << moves each bit of the left operand to the left by the number of positions specified by the right operand. The vacated bits on the right are set to zero. For example, the binary value 0000 1010 (10) shifted left by two positions becomes 0010 1000 (40). Each shift position effectively multiplies the value by two, assuming no bits are lost off the top of the type's range.
int a = 10; // binary 0000 1010 int b = a << 2; System.out.println(b); // 40
For positive values with a shift distance in the normal range for the type, a << n is equivalent to a * 2^n as long as the result fits within the range of the type. The equivalence falls apart when the shift pushes a 1 bit beyond the most significant bit of the type. That discarded bit is lost, and the remaining bits may produce a negative value or a value that no longer corresponds to simple multiplication.
A left shift works on int and long operands. A byte, short, or char left operand is promoted to int before the shift, so the result type is at least int. A left shift on a byte or short never produces a byte or short result.
Because the shift distance is masked, a << n uses only the lower five bits of n for an int, and the lower six bits of n for a long. That means shifting by 32 on an int is equivalent to shifting by zero. This behavior is surprising when a developer expects a full "clear everything" shift.
Signed Right Shift: Preserving the Sign
The signed right shift operator >> shifts bits to the right and fills the vacated leftmost bits with the sign bit. For a positive number, the sign bit is zero, so the vacated bits become zero. For a negative number, the sign bit is one, so the vacated bits become one. This keeps the sign of the value intact.
int negative = -16; // binary 1111 1111 1111 1111 1111 1111 1111 0000 int shifted = negative >> 2; System.out.println(shifted); // -4
In this example, -16 shifted right by two produces -4, which is the integer division of -16 by 4, rounding toward negative infinity. For positive values, >> n is equivalent to dividing by 2^n and rounding down. For negative odd values, the result is the floor of the division rather than truncation toward zero. The semantic difference matters when a developer expects -3 / 2 to be -1 but Java's signed shift gives -2.
The signed right shift is commonly used when the original sign must remain meaningful, such as when reconstructing a signed value from its high-order bits or implementing certain arithmetic loops. It is also the correct choice when you are extracting signed bit fields from an integer.
Unsigned Right Shift: Filling with Zeros
The unsigned right shift operator >>> shifts bits to the right and always fills the vacated leftmost bits with zero. It does not consider the sign bit. For a positive value, >> and >>> produce the same result. For a negative value, the two operators produce different results because >>> treats the negative number as a large unsigned bit pattern.
int negative = -1; // binary all ones int unsignedShifted = negative >>> 1; System.out.println(unsignedShifted); // 2147483647
The value -1 represented as an int is 0xFFFFFFFF. Shifting it right by one with >>> gives 0x7FFFFFFF, which is 2147483647, the maximum positive int. This behavior is useful when the bit pattern matters more than the signed interpretation, such as when implementing hash functions, compression code, or bit-level protocols.
There is no unsigned left shift operator. A left shift already introduces zeros in the low-order bits, so the signedness only affects how the high-order bits are interpreted. The lack of a dedicated operator means the existing << covers both signed and unsigned left shifts.
Shift Distance Masking and Type Promotion
When the left operand is an int, Java uses only the low five bits of the shift distance. For a long, it uses only the low six bits. This masking applies whether the distance is a literal or a runtime value. For an int, a distance of 32 therefore behaves as a distance of zero, and distances 32 through 63 behave as 0 through 31. For a long, a distance of 64 behaves as zero.
int value = 1; int distance = 32; int result = value << distance; System.out.println(result); // 1, not 0
A shift by 32 on an int is equivalent to a shift by zero because 32 masked by 31 is zero. This is a common source of off-by-one errors when a loop variable that can reach 32 is used directly as a shift distance.
Type promotion also matters. When the left operand is a byte, short, or char, it is promoted to int before the shift. The result is an int, and assigning it back to a byte or short requires an explicit cast. Ignoring the promotion leads to a compile-time error when the result is used where a smaller type is expected.
Practical Uses: Bit Masks and Flags
The most common production use of shift operators is constructing and extracting bit masks. Bit flags are often packed into a single integer for compact storage. Shifting is used to set, clear, or test individual bits.
int FLAG_A = 1 << 0; // 0b0001 int FLAG_B = 1 << 1; // 0b0010 int FLAG_C = 1 << 2; // 0b0100 int flags = 0; flags |= FLAG_A; // set FLAG_A flags |= FLAG_C; // set FLAG_C boolean hasB = (flags & FLAG_B) != 0; System.out.println(hasB); // false
A left shift by a constant creates a power-of-two mask. Combining masks with bitwise OR sets multiple flags in one integer. The bitwise AND test checks whether a particular flag is set. This pattern appears in permission systems, protocol headers, and configuration storage where several boolean values must be represented compactly.
Shifting is also used to extract a range of bits. For example, a color value packed as 32 bits with 8 bits each for alpha, red, green, and blue can be unpacked using a combination of >>> and &.
int argb = 0xFF336699; int alpha = (argb >>> 24) & 0xFF; int red = (argb >>> 16) & 0xFF; int green = (argb >>> 8) & 0xFF; int blue = argb & 0xFF;
The unsigned right shift ensures that the top bits are zeroed before the mask applies. If a signed right shift were used, a negative value would inject ones into the high bits and corrupt the red extraction. This is a concrete example where >>> is the correct choice and >> would be a bug.
Performance and Maintainability Considerations
Shift operators are primitive integer operations. On the JVM they map to bytecode instructions such as ishl, ishr, iushr, lshl, lshr, and lushr. They do not involve method calls, boxing, or heap allocation. The JIT compiler can also convert multiplication by a power of two into a shift, so substituting shifts for arithmetic rarely changes real-world speed. The practical benefit is more about expressing intent clearly than outperforming the compiler.
From a maintainability perspective, shifts are less readable than arithmetic operations for most developers. Using a * 2 communicates multiplication; a << 1 communicates bit movement. When the intent is bit-level manipulation, the shift is clearer. When the intent is numeric doubling, arithmetic is clearer. The choice should follow the meaning you want to convey, not a guess about performance.
Another aspect of maintainability is avoiding "magic numbers" in shift distances. A shift distance like 5 is meaningless without context. Naming the constant, such as int ORDER = 5; int SIZE = 1 << ORDER;, makes the code easier to read and reduces the chance of an off-by-one error when the value is later adjusted.
Common Mistakes with Shift Operators
One common mistake is using a signed right shift when an unsigned right shift was intended. This happens when a developer extracts a bit field from a value that could be negative in its signed interpretation. The result is an unexpected set of one bits on the left side of the extracted value. The fix is to use >>> when the bit pattern should be treated as unsigned.
Another mistake is assuming that a shift modifies the original variable. The expression value << 2 returns a new value; the variable value remains unchanged. Developers unfamiliar with immutable integer behavior often write value << 2 expecting it to update value. The correct statement is value = value << 2; or the compound assignment value <<= 2;.
A third mistake is failing to account for type promotion. Shifting a byte or short produces an int, so assigning the result back to a byte without a cast fails compilation. Casting the result back is explicit documentation that the higher-order bits are intentionally discarded.
Finally, the shift distance masking rule often surprises developers. Using a variable that can reach 32 on an int, or 64 on a long, without normalizing the distance produces a different result than a full shift might suggest. If the distance is not already constrained to the type width, normalize it explicitly or check it before shifting.
Shift Operators on Long Values
For long operands, the shift distance is masked with 63 rather than 31. The result type is long. The same sign-filling rules apply: >> preserves the sign bit, and >>> fills with zeros. A common place where long shifts appear is cryptographic code, hash functions, or custom serialization formats where values are 64-bit.
long bits = 0x8000000000000000L; long signed = bits >> 1; // 0xC000000000000000, negative long unsigned = bits >>> 1; // 0x4000000000000000, positive
The distinct behavior between >> and >>> on long is identical in principle to int. The only difference is the shift distance range. The mask is 63, so a shift by 64 becomes a shift by zero. This is a boundary that every developer working with long values should keep in mind.
Choosing the Right Shift Operator
Use << when you need to multiply by a power of two, build bit masks, or pack multiple fields into the high-order bits. Use >> when you want to divide by a power of two while preserving the sign, or when you need to extract a signed bit field. Use >>> when you need to treat the value as a purely unsigned bit pattern, such as in hashing, checksum, or protocol code.
The decision is not about speed but about correctness. The wrong shift operator produces values that are numerically different from what the algorithm expects. In bit-level code, a single incorrect shift can turn a valid hash into a constant, or a correct protocol header into garbage. The operator choice must follow the data's meaning, not a preference for shorter syntax.
A practical rule: if you are extracting a field that should never be negative, use >>>. If you are extracting a field that represents a signed quantity, use >>. For left shifts, the same value is produced regardless of signedness, so choose based on how the result will be interpreted downstream.
The Java shift operators are a small but deep part of the language. They reward attention to signedness, type promotion, and shift distance semantics. Used correctly, they enable compact and efficient bit-level logic. Used carelessly, they produce silent numerical errors that only surface under specific inputs. Keep the difference between >> and >>> in mind, and always verify the type and range of the shift distance before relying on the result.