&, &&, |, || in Java and Kotlin
На булевих значеннях && і & дають однаковий результат, тому різницю легко проґавити, поки код не впаде з NullPointerException або не виконає побічний ефект, якого ніхто не чекав. Відрізняється не відповідь, а те, що встигає виконатися до неї.
У Java є дві пари операторів: && та || — логічні, зі скороченим обчисленням (short-circuit), і & та | — побітові, які на boolean теж працюють, але завжди обчислюють обидва операнди. Kotlin побітових & і | не має зовсім: ту саму роботу там виконують інфіксні функції and та or. Тому саме в Kotlin легко не помітити, що a and b на Boolean — це вже не a && b.
Таблиця відповідності
Скорочене обчислення є лише в && та || (в обох мовах); усе інше завжди обчислює обидві сторони.
| Дія | Java | Kotlin | Скорочення |
|---|---|---|---|
| Логічне І | a && b | a && b | так |
| Логічне АБО | a \|\| b | a \|\| b | так |
| Логічне І, обидва боки завжди | a & b | a and b | ні |
| Логічне АБО, обидва боки завжди | a \| b | a or b | ні |
| Виключне АБО (boolean) | a ^ b | a xor b | ні |
| Логічне НЕ | !a | !a | — |
| Побітове І (цілі) | x & y | x and y | — |
| Побітове АБО (цілі) | x \| y | x or y | — |
| Побітове XOR (цілі) | x ^ y | x xor y | — |
| Побітове НЕ | ~x | x.inv() | — |
| Зсув вліво | x << n | x shl n | — |
| Зсув вправо зі знаком | x >> n | x shr n | — |
| Зсув вправо без знака | x >>> n | x 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 залишайте для масок і для рідкісних випадків, коли виконатися мають обидві частини.
| Ситуація | Java | Kotlin |
|---|---|---|
| Звичайна умова | a && b, a \|\| b | a && b, a \|\| b |
| Обидві частини мусять виконатись | a & b, a \| b | a and b, a or b |
| Перевірити прапорець | (x & F) != 0 | (x and F) != 0 |
| Встановити прапорець | x \|= F | x = x or F |
| Прибрати прапорець | x &= ~F | x = x and F.inv() |
| Перемкнути прапорець | x ^= F | x = x xor F |
| Витягти другий байт | (x >> 8) & 0xFF | (x shr 8) and 0xFF |
| Беззнаковий зсув | x >>> n | x ushr n |