In short: use bitwise operators in Java when the problem is genuinely binary. That means compact permissions and flags (hand-written bit masks, as in hardware registers; in everyday application code, prefer EnumSet, which stores the flags as bits for you, without any bitwise operators in your code), binary protocols and formats (parsing headers, RGB/ARGB colors with <<, >> and &), hashes and checksums (hashCode(), CRC, MurmurHash), and low-level algorithms where the bit operation is the algorithm (counting bits, detecting a power of two with (n & (n - 1)) == 0, or swapping without a temporary variable in specific embedded-systems contexts).

Avoid them when the only goal is to “optimize”. The compiler and the JIT already do strength reduction automatically, so replacing x * 2 with x << 1 by hand brings no real gain and only hurts readability. This is the kind of premature optimization that Knuth, and later Bloch in Effective Java, warn about: it is only worth it after measuring with a profiler, never by assumption.

Bitwise operators work on each individual bit of an integer instead of treating the number as a single value. Computers already store everything in binary, and these operators give programmers direct access to those ones and zeros.

Java has seven of them:

OperatorNameRuleExample (8 bits)
&AND1 only if both bits are 11100 & 1010 = 1000
|OR1 if at least one bit is 11100 | 1010 = 1110
^XOR1 if the bits are different1100 ^ 1010 = 0110
~NOT (complement)flips every bit~00001100 = 11110011
<<left shiftshifts left and fills with 0 (multiplies by 2ⁿ)0011 << 2 = 1100
>>signed right shiftshifts right and copies the sign bit-16 >> 2 = -4
>>>unsigned right shiftshifts right and fills with 0-16 >>> 2 = 1073741820

The Java Language Specification (§15.19) calls <<, >> and >>> the shift operators. The left operand is the value being shifted, and the right operand is the shift distance.

Where They Came From

The story has three stages: first the mathematics, then the hardware, and finally the programming languages.

1847–1854: George Boole invents the algebra of logic. Boole published “The Mathematical Analysis of Logic” in 1847 and “The Laws of Thought” in 1854, starting what we now call Boolean algebra (Britannica). He treated logical operators as pure mathematics. The modern form of the algebra, with union, intersection and complement, was completed later by Jevons, Peirce, Schröder and Huntington (Wikipedia).

1937: Claude Shannon brings logic into circuits. At 21, in his master’s thesis at MIT, Shannon realized that Boole’s true and false map onto the open and closed states of an electrical circuit (Christie’s). Switches in series compute AND, switches in parallel compute OR, and a normally closed relay performs NOT (salivity.github.io). Processing logic with electrical switches is the foundation of every modern digital computer (Wikipedia). From then on, processors came with native AND, OR, XOR and shift instructions that the hardware executes directly.

1960s–1970s: programming languages. CPL was designed to allow high-level programming without giving up control over individual bits (University of Michigan). Ken Thompson’s B already had the &, |, ^, << and >> operators (flash-gordon.me.uk). In BCPL and B, however, & and | changed meaning depending on context. Inside an if they acted as sequential logical tests; everywhere else they were bit operations (Bell Labs, BCPL manual). While creating C between 1971 and 1973, Dennis Ritchie took a suggestion from Alan Snyder and split the logical operators && and || from the bitwise & and | (Ritchie, “The Development of the C Language”). That is why Java, which inherited C’s syntax, has both pairs.

1995: Java adds >>>. Java has no unsigned integer types, so it defines two right shifts. >> is arithmetic and preserves the sign, while >>> is logical and fills with zeros (CERT/SEI). >>> also removes an ambiguity from C, where the result of >> on negative numbers depends on the compiler.

Why Use Them

  • Performance: these are the cheapest operations a processor can run, usually in a single cycle.
  • Memory savings: one int holds 32 flags and one long holds 64, where you would otherwise need 32 or 64 boolean values.
  • Matching binary formats: network protocols, file formats (PNG, ZIP), hardware and cryptography all define their data bit by bit.
  • Classic algorithms: hashing, Bloom filters, compression and random number generation all depend on these operators.

When to Use Them

SituationJava example
Flags and permissionsperms | WRITE, perms & ~WRITE, (perms & READ) != 0
Packing several values into one numberARGB color: (a << 24) | (r << 16) | (g << 8) | b
Reading unsigned bytesbyte & 0xFF in streams, sockets and files
Protocols and binary formatsTCP/IP headers, control bits
Hashing and table indexingh ^ (h >>> 16) and hash & (n - 1) in HashMap
Overflow-safe midpoint(low + high) >>> 1 in binary search
Fast checkseven/odd (n & 1) == 0; power of two (n & (n - 1)) == 0
Sets of integersBitSet, prime sieves, graphs and games (chess bitboards)
Cryptography and checksumsXOR, rotations (Integer.rotateLeft), CRC

When to Avoid Them

  • Business rules in APIs: as Joshua Bloch argues in Item 36 of Effective Java, prefer EnumSet over bit fields packed into an int. EnumSet is just as fast, and much more readable and type-safe.
  • Micro-optimizing without measuring: the JIT compiler already turns x * 8 into x << 3 when it pays off. Replace arithmetic with shifts only when it makes the code clearer or a profiler shows a real gain.
  • Tricks that confuse readers: swapping variables with XOR is a curiosity, not a good practice.
  • Known pitfalls: the shift distance is reduced modulo 32 (or 64 for long), byte and short are promoted to int before the operation, and & and | on boolean values do not short-circuit.

Hands-on Example: && vs & on Booleans

The last pitfall above is easier to see in code. This small project validates a user through an external service. The Usuario class holds a name and an active flag:

public class Usuario {

    private final String nome;
    private final boolean ativo;

    public Usuario(String nome, boolean ativo) {
        this.nome = nome;
        this.ativo = ativo;
    }

    public String getNome() { return nome; }

    public boolean isAtivo() { return ativo; }
}

ServicoExterno stands for any remote call, such as an API or a database. It returns the user as the service sees it:

public interface ServicoExterno {

    Usuario consultar(Usuario usuario);
}

Validador checks that the user is not null and, only then, asks the service:

public class Validador {

    public static boolean ehValido(Usuario usuario, ServicoExterno servico) {
        return usuario != null && servico.consultar(usuario).isAtivo();
    }
}

The && operator is short-circuit: when the left side is false, the right side never runs. With a null user, the service is not called and the method returns false. If you write & instead, Java evaluates both sides, so servico.consultar(null) runs and the .isAtivo() call on its result can throw a NullPointerException. Besides the exception, it also means a useless call to the external service.

A main method shows the safe version:

public class Main {

    public static void main(String[] args) {
        ServicoExterno servico = usuario -> usuario;

        Usuario usuarioAtivo = new Usuario("Hussyvel", true);
        Usuario usuarioNulo = null;

        System.out.println("Active user is valid? " + Validador.ehValido(usuarioAtivo, servico));
        System.out.println("Null user is valid? " + Validador.ehValido(usuarioNulo, servico));
    }
}

Testing It with JUnit 5

The tests below use lambdas as the service, so no mocking library is needed. The key test proves the short-circuit: with a null user, the service must never be called.

class ValidadorTest {

    private final ServicoExterno servicoEco = usuario -> usuario;

    @Test
    void deveRetornarTrueParaUsuarioAtivo() {
        assertTrue(Validador.ehValido(new Usuario("Ana", true), servicoEco));
    }

    @Test
    void deveRetornarFalseParaUsuarioInativo() {
        assertFalse(Validador.ehValido(new Usuario("Ana", false), servicoEco));
    }

    @Test
    void deveRetornarFalseParaUsuarioNulo() {
        assertFalse(Validador.ehValido(null, servicoEco));
    }

    @Test
    void naoDeveChamarServicoQuandoUsuarioForNulo() {
        AtomicInteger chamadas = new AtomicInteger();
        ServicoExterno servico = usuario -> {
            chamadas.incrementAndGet();
            return usuario;
        };

        Validador.ehValido(null, servico);

        assertEquals(0, chamadas.get());
    }

    @Test
    void deveUsarOResultadoDoServicoEmVezDoUsuarioOriginal() {
        Usuario original = new Usuario("Ana", true);
        ServicoExterno servico = usuario -> new Usuario("Ana", false);

        assertFalse(Validador.ehValido(original, servico));
    }

    @Test
    void devePropagarExcecaoDoServico() {
        ServicoExterno servico = usuario -> {
            throw new IllegalStateException("servico fora do ar");
        };

        assertThrows(IllegalStateException.class,
                () -> Validador.ehValido(new Usuario("Ana", true), servico));
    }
}

Change && to & in Validador and naoDeveChamarServicoQuandoUsuarioForNulo fails right away, because the service is called once. A good test suite catches this kind of one-character mistake.