&, &&, |, || in Java and Kotlin

⏱ 7 min read • TECH

На булевих значеннях && і & дають однаковий результат, тому різницю легко проґавити, поки код не впаде з NullPointerException або не виконає побічний ефект, якого ніхто не чекав. Відрізняється не відповідь, а те, що встигає виконатися до неї.

У Java є дві пари операторів: && та || — логічні, зі скороченим обчисленням (short-circuit), і & та | — побітові, які на boolean теж працюють, але завжди обчислюють обидва операнди. Kotlin побітових & і | не має зовсім: ту саму роботу там виконують інфіксні функції and та or. Тому саме в Kotlin легко не помітити, що a and b на Boolean — це вже не a && b.

Таблиця відповідності

Скорочене обчислення є лише в && та || (в обох мовах); усе інше завжди обчислює обидві сторони.

ДіяJavaKotlinСкорочення
Логічне Іa && ba && bтак
Логічне АБОa \|\| ba \|\| bтак
Логічне І, обидва боки завждиa & ba and bні
Логічне АБО, обидва боки завждиa \| ba or bні
Виключне АБО (boolean)a ^ ba xor bні
Логічне НЕ!a!a—
Побітове І (цілі)x & yx and y—
Побітове АБО (цілі)x \| yx or y—
Побітове XOR (цілі)x ^ yx xor y—
Побітове НЕ~xx.inv()—
Зсув влівоx << nx shl n—
Зсув вправо зі знакомx >> nx shr n—
Зсув вправо без знакаx >>> nx ushr n—

Java: && та || в умовах

У звичайних умовах (if, while, тернарний оператор) потрібні && і ||: вони йдуть зліва направо і зупиняються, щойно результат уже відомий. Для a && b правий операнд пропускається, якщо a == false; для a || b — якщо a == true.

Отже, порядок операндів — це частина логіки, а не стиль. Спочатку те, що робить решту безпечною або дешевою:

// Захист від NullPointerException
if (user != null && user.isActive()) { ... }

// Перевірка меж перед доступом до елемента
if (i < arr.length && arr[i] > 0) { ... }

// Дешева перевірка перед дорогою
if (!items.isEmpty() && expensiveCheck(items)) { ... }

// Із || правий бік виконується лише коли лівий false
if (cache.containsKey(key) || loadFromDb(key) != null) { ... }

Якщо поміняти місцями першу пару, код зламається, і компілятор цього не помітить:

if (user.isActive() && user != null) { ... }   // NPE, коли user == null

Java: & та | на boolean і на числах

Оператори & та | роблять різні речі залежно від типу операндів: на boolean — логічну операцію без скорочення, на цілих числах (int, long, byte, short, char) — побітову.

На boolean вони доречні, коли обидві частини мають побічний ефект і обидві повинні виконатися. Типовий випадок — валідація, яка збирає всі помилки, а не зупиняється на першій:

int calls = 0;
boolean r1 = false && (++calls > 0);   // calls == 0, правий бік пропущено
boolean r2 = false & (++calls > 0);    // calls == 1, правий бік виконано

// обидві перевірки запишуть свої помилки у форму
boolean ok = validateName(form) & validateEmail(form);

// те саме для складених присвоєнь
boolean allOk = true;
for (Item it : items) {
    allOk &= check(it);     // check() викличеться для кожного елемента
}

Такий код варто коментувати: читач за звичкою очікує && і може «виправити» його, зламавши логіку.

На цілих числах & і | працюють з окремими бітами, що дає маски та прапорці:

final int READ = 0b001, WRITE = 0b010, EXEC = 0b100;

int perms = READ | WRITE;                  // 0b011, встановити прапорці
boolean canWrite = (perms & WRITE) != 0;   // перевірити біт
perms |= EXEC;                             // додати: 0b111
perms &= ~WRITE;                           // прибрати: 0b101
perms ^= READ;                             // перемкнути: 0b100

int lowByte  = value & 0xFF;               // молодший байт
int secondByte = (value >> 8) & 0xFF;      // другий байт

Kotlin: те саме, але словами

Логічні && та || у Kotlin працюють як у Java, а все, що в Java робили &, |, ^, ~ та зсуви, тут стало інфіксними функціями: and, or, xor, inv(), shl, shr, ushr. Та сама функція and на Boolean дає логічне І без скорочення, а на Int чи Long — побітове.

// Умови: як у Java, і smart cast після перевірки на null
if (user != null && user.isActive) { /* ... */ }

// Boolean: && скорочує, and — ні
var calls = 0
val a = false
val r1 = a && (++calls > 0)    // calls == 0
val r2 = a and (++calls > 0)   // calls == 1

// Валідація, яка збирає всі помилки
val ok = validateName(form) and validateEmail(form)

// Оператора &= немає, тож присвоєння пишемо повністю
var allOk = true
for (item in items) {
    allOk = allOk and check(item)
}

Прапорці та маски виглядають так само, як у Java, тільки без символів:

const val READ = 0b001
const val WRITE = 0b010
const val EXEC = 0b100

var perms = READ or WRITE                // встановити
val canWrite = (perms and WRITE) != 0    // перевірити
perms = perms or EXEC                    // додати
perms = perms and WRITE.inv()            // прибрати
perms = perms xor READ                   // перемкнути

val lowByte = value and 0xFF
val secondByte = (value shr 8) and 0xFF

Майже ті самі функції є й у беззнакових типів (UInt, ULong): and, or, xor, inv(), shl, shr. Окремого ushr там немає, бо shr у беззнакових типів і так заповнює нулями.

Типові пастки

Більшість помилок з цими операторами — не в самій логіці, а в тому, як компілятор розставляє дужки за вас.

  • Java, & слабший за ==. x & MASK != 0 читається як x & (MASK != 0), тобто int & boolean, і компілятор лається. Пишіть (x & MASK) != 0.
  • Kotlin, and сильніший за > і ==. a > 0 and b > 0 читається як a > (0 and b) > 0 і не компілюється. Пишіть (a > 0) and (b > 0) або просто a > 0 && b > 0.
  • && сильніший за || в обох мовах. a || b && c — це a || (b && c). Якщо сумніваєтесь, ставте дужки.
  • Метод із побічним ефектом справа від && чи || може не викликатися. У if (ready || init()) виклик init() пропускається, коли ready == true.
  • Kotlin, and на Boolean не скорочує. user != null and user.isActive не лише має проблему з пріоритетом, а й не захистив би від null. Для захисту потрібен &&.
  • Kotlin не розширює Int до Long сам. longValue and intMask не скомпілюється, потрібно longValue and intMask.toLong(). Java зробить це мовчки.

Ще дві пастки стосуються знака, і вони однакові в обох мовах:

byte b = (byte) 0xF0;          // -16
int wrong = b;                 // -16, знак розтягнувся
int right = b & 0xFF;          // 240

int n = -8;
int s1 = n >> 1;               // -4, знак зберігається
int s2 = n >>> 1;              // 2147483644, зліва нулі
val b: Byte = 0xF0.toByte()    // -16
val right = b.toInt() and 0xFF // 240

val n = -8
val s1 = n shr 1               // -4
val s2 = n ushr 1              // 2147483644

Що брати і коли

За замовчуванням беріть && і ||; &/| у Java та and/or у Kotlin залишайте для масок і для рідкісних випадків, коли виконатися мають обидві частини.

СитуаціяJavaKotlin
Звичайна умоваa && b, a \|\| ba && b, a \|\| b
Обидві частини мусять виконатисьa & b, a \| ba and b, a or b
Перевірити прапорець(x & F) != 0(x and F) != 0
Встановити прапорецьx \|= Fx = x or F
Прибрати прапорецьx &= ~Fx = x and F.inv()
Перемкнути прапорецьx ^= Fx = x xor F
Витягти другий байт(x >> 8) & 0xFF(x shr 8) and 0xFF
Беззнаковий зсувx >>> nx ushr n

Comments

© 2025 CodeByMe.de | Engineering