skip to content

What is the difference between shr and ushr in Kotlin, and when does it actually matter?

level: middleimportance: must knowfreq 50%

answer

  1. shr fills with sign bit; ushr fills with zeros
  2. Differ only for negative numbers
  3. ushr for raw-bit / high-byte extraction
  4. (low+high) ushr 1 avoids overflow midpoint
  5. Shift count is modulo 32 (Int) / 64 (Long)

basics

~10 s

shr keeps the sign: shifting a negative number right fills the top bits with 1s. ushr ignores the sign and always fills the top bits with 0s. They only differ for negative numbers.

solid answer

~40 s

Both shift bits to the right. shr is an arithmetic (signed) right shift: it fills vacated high bits with copies of the sign bit, so a negative Int stays negative and the result equals floor-division by a power of two. ushr is a logical (unsigned) right shift: it always fills vacated high bits with 0, so the result is treated as an unsigned magnitude — a negative number becomes a large positive one. For non-negative values they're identical. ushr is essential when packing/unpacking unsigned fields, reading hash bytes, or extracting the high word of a value, e.g. (value ushr 24) and 0xFF to get the top byte without sign extension. The shift count is taken modulo 32 for Int and modulo 64 for Long, so 1 shl 32 == 1.

code

kotlin · 5 lines
kotlin
val argb = 0xFF8800CC.toInt()
val alpha = (argb ushr 24) and 0xFF // 255, no sign extension
val wrongWithShr = (argb shr 24) and 0xFF // also 255 here only because of the mask
println(-1 shr 1)   // -1
println(-1 ushr 1)  // 2147483647

go deeper

for a junior

States that ushr zero-fills and shr keeps the sign; differs only for negatives.

for a middle

Explains two's-complement sign extension and gives a concrete high-byte/midpoint use case.

for a senior

Knows shift-count-modulo semantics and reasons about overflow-safe midpoints and packed-field extraction.

for a principal

Weighs correctness/perf tradeoffs of bit tricks vs unsigned types and library helpers in real codecs.

## Two's-complement background Kotlin's `Int` is a 32-bit signed integer in **two's-complement** representation: the most-significant bit (bit 31) is the **sign bit** (1 = negative). Right-shifting must decide what to feed into the newly-vacated top bits. ## shr — arithmetic / signed right shift `a shr n` shifts right and fills the top `n` bits with **copies of the original sign bit**. This preserves sign and mathematically performs floor division by 2^n. ```kotlin println(-8 shr 1) // -4 (sign preserved) println( 8 shr 1) // 4 println(-1 shr 31) // -1 (all ones stays all ones) ``` ## ushr — logical / unsigned right shift `a ushr n` shifts right and fills the top `n` bits with **0**, ignoring sign. The bit pattern is treated as an unsigned magnitude. ```kotlin println(-8 ushr 1) // 2147483644 (top bit became 0) println(-1 ushr 28) // 15 println( 8 ushr 1) // 4 (same as shr for non-negatives) ``` ## When it matters They diverge **only for negative operands** (i.e. when the sign bit is set). Use `ushr` whenever you treat an `Int`/`Long` as a raw bit container rather than a signed number: - Extracting a high byte/field: `(rgba ushr 24) and 0xFF`. - Computing midpoints to avoid overflow: `(low + high) ushr 1`. - Reading bytes from a hash or packed format. Using `shr` there would sign-extend and corrupt the value. ## Shift-count semantics The shift distance is reduced **modulo the bit width**: 32 for `Int`, 64 for `Long`. So `1 shl 32 == 1` and `1 shl 33 == 2`. Only the low 5 bits (Int) / low 6 bits (Long) of the count are used. Negative counts therefore behave like their modulo-reduced positive equivalent, which is rarely what you want. ## Summary `shr` = keep sign (arithmetic); `ushr` = zero-fill (logical). Identical for non-negative inputs; for raw-bit work prefer `ushr`.

  • What does 1 shl 32 produce for an Int?
    1. The shift count is taken modulo 32, so shifting by 32 is the same as shifting by 0.
  • Why is (low + high) ushr 1 a safer binary-search midpoint than (low + high) / 2?
    If low+high overflows into a negative Int, ushr 1 still yields the correct unsigned average bit pattern, whereas signed division gives a wrong negative midpoint.

shr is like dividing while remembering you owe money (stays negative); ushr forgets the debt and reads the bits as a plain count.

saying these in an interview costs you the question

  • Saying shr and ushr are always identical
  • Claiming ushr exists for Float/Double
  • Not knowing shift count is modulo the bit width
  • Thinking ushr fills with the sign bit
  • Believing the difference shows up for positive numbers

context