Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 выражения, инструкции и блоки».
Предыдущая статья — «Переменные в Java 8».
Операции позволяют выполнить арифметические, логические и битовые действия с переменными и литералами. В этой статье я попробую описать каждую операцию с таким уровнем подробностей, с которым его учил я.
Содержание
Преобразование примитивных типов
— Расширяющее преобразование примитивов
— Сужающее преобразование примитивов
— Отличие постфиксного и префиксного инкремента и декремента
Присвоение с выполнением другой операции
Операция присваивания
Операция «=» позволяет присвоить значение переменной:
1 2 3 4 5 6 7 |
int x = 3; long l1 = 10_000_000_000L; float f1 = 1.3f; double weight = 81.34; byte b1 = 100; short sh1 = -10000; char ch1 = 60000; |
КОНСТАНТНЫЕ значения до int можно присвоить без приведения типа к переменным меньшего размера (например short в byte), если значение помещается в эту переменную.
Вы можете присвоить переменной, имеющей больший тип, значение меньшего типа, например переменной типа double можно присвоить значение int, но не наоборот (но можно использовать приведение типа, если очень нужно).
Примеры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
double d1 = 2; // Это можно int x = 2.3; // так нельзя. Будет ошибка компиляции. byte b1 = 100; //Это можно, так как литерал 100 гарантировано // поместится в byte. byte b2 = 10000; //Нельзя. Ошибка компиляции. int n = 100; byte b3 = n; //А вот так тоже нельзя, так как // переменная n имеет тип int. |
Операция присваивания возвращает значение, которое присвоила, поэтому можно присваивать значение сразу нескольким переменным по цепочке:
1 2 3 |
int x; int y; int z = x = y = 10; // y, x и z будет присвоено 10. |
Преобразование примитивных типов
Есть два типа преобразования примитивных типов:
- расширяющее преобразование (Widening Primitive Conversion)
- сужающее преобразование (Narrowing Primitive Conversion)
Расширяющее преобразование примитивов
Следующие преобразования называются расширяющими преобразованиями примитивов:
- byte -> short, int, long, float, double
- short -> int, long, float, double
- char -> int, long, float, double
- int -> long, float, double
- long -> float, double
- float -> double
Расширяющее преобразование не приводит к потере информации в следующих случаях:
- из целого типа в другой целый тип
- из byte, short, char в тип с плавающей точкой
- из int в double
- из float в double в выражении с strictfp (это такой особый режим вычислений с плавающей точкой, возможно, распишу позже).
Расширяющее преобразование из float в double в обычном режиме (без strictfp ) может привести к потере точности.
Расширяющее преобразование int во float или из long во float, или из long в double может привести к потере точности, то есть результат может потерять несколько наименее значимых бит информации, что приведёт к получению округлённого значения.
Примеры расширяющего преобразования примитивов:
1 2 3 4 5 6 7 8 |
byte b1 = 100; short sh1 = b1; // Расширяющее преобразование byte->short char ch1 = 100; int i1 = sh1; // Расширяющее преобразование short->int int i2 = ch1; // Расширяющее преобразование char->int long l1 = i1; // Расширяющее преобразование int->long float f1 = l1; // Расширяющее преобразование long->float double d1 = f1; // Расширяющее преобразование float->double |
Несмотря на возможность потери точности расширяющее преобразование примитивов никогда не приводит к ошибкам во время выполнения.
Сужающее преобразование примитивов
Следующие преобразования называются сужающими преобразованиями примитивов:
- short -> byte, char
- char -> byte , short
- int -> byte , short , char
- long -> byte , short , char , int
- float -> byte , short , char , int , long
- double -> byte , short , char , int , long , float
Сужающее преобразование примитивов может привести к потере точности и даже к получению совсем другого числа из-за выхода за границу размерности.
Преобразование double во float может привести к потере точности и получению значения -0.0f или +0.0f вместо очень маленького значения double, а также к -Infinity и +Infinity вместо очень большого значения double. NaN из double преобразуется в NaN из float. Infinity в double преобразуется в Infinity в float того же знака.
При целочисленном сужающем преобразовании примитивов значения старших бит просто теряются, поэтому результат операции может запросто оказаться даже другого знака, чем исходное число.
Несмотря на возможность потери точности и даже получения совсем другого числа сужающее преобразование никогда не приводит к ошибке во время выполнения.
При сужающем преобразовании нужно явно приводить к нужному типу, указав его в скобках, иначе возникнет ошибка компиляции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
double d1 = 2.3; double dPositiveInfinity = Double.POSITIVE_INFINITY; double dNegativeInfinity = Double.NEGATIVE_INFINITY; double dPlusZero = +0.0; double dMinusZero = -0.0; double dNaN = Double.NaN; float fPositiveInfinity = (float) dPositiveInfinity; // +Infinity сужающее преобразование float fNegativeInfinity = (float) dNegativeInfinity; // -Infinity сужающее преобразование float fPlusZero = (float) dPlusZero; //0.0f сужающее преобразование float fMinusZero = (float) dMinusZero; //-0.0f сужающее преобразование float fNaN = (float) dNaN; // NaN сужающее преобразование float f1 = (float) d1; // сужающее преобразование int n = (int) d1; // 2 // сужающее преобразование. Дробная часть отбрасывается System.out.println(fPositiveInfinity); System.out.println(fNegativeInfinity); System.out.println(fPlusZero); System.out.println(fMinusZero); System.out.println(fNaN); System.out.println(f1); System.out.println(n); |
Арифметические операции
Арифметические операции позволяют выполнять сложение (операция «+»), вычитание (операция «-»), умножение (операция «*»), деление (операция «/») и взятие остатка (операция «%»). Эти операции имеют такие же приоритеты, что и в обычной математике, которую изучают в школе, то есть умножение и деление выполняется перед сложением и вычитанием.
1 2 3 4 5 6 |
double x1 = 1.1 + 2.3; // 3.4 double x2 = 1.1 - 0.1; // 1.0 double x3 = 1.1 * 2 + 1; // 3.2 double x4 = 6 / 2.0; // 3.0 int x5 = 12 + 3; // 15 int x6 = 13 % 5; // 3 |
При выполнении арифметических операций операнды всегда преобразуются как минимум в int (например при умножении двух переменных типа byte оба значения сначала преобразуются в int, и результат выражения будет int).
При выполнении арифметической операции над операндами разных типов результат операции будет иметь наибольший тип, Что можно описать следующими правилами:
- Если один из операндов имеет тип double, то результат выражения имеет тип double, иначе смотри пункт 2.
- Если один из операндов имеет тип float, то результат выражения имеет тип float, иначе смотри пункт 3.
- Если один из операндов имеет тип long, то результат выражения имеет тип long, иначе результат выражения имеет тип int.
(например, при сложении int и long результат будет иметь тип long, а при сложении long и float результат будет иметь тип float, а при сложении float и double результат будет иметь тип double).
Если результат операции с целочисленными данными выходит за диапазон, то старшие биты отбрасываются, и результирующее значение будет совершенно неверным. При попытке деления на 0 возникает исключение java.lang.ArithmeticException / zero.
При выполнении операций с плавающей точкой при выходе за верхнюю или нижнюю границу диапазона получается +Infinity ( Double.POSITIVE_INFINITY и Float.POSITIVE_INFINITY) и -Infinity ( Double.NEGATIVE_INFINITY и Float.NEGATIVE_INFINITY ) соответственно, а при получении слишком маленького числа, которое не может быть нормально сохранено в этом типе данных получается -0.0 или +0.0.
При выполнении операций с плавающей точкой результат NaN ( Double.NaN и Float.NaN) получается в следующих случаях:
- Когда один из операндов
NaN
- В неопределённых результатах:
- Деления 0/0, ∞/∞, ∞/−∞, −∞/∞, −∞/−∞
- Умножения 0×∞ and 0×−∞
- Степень 1∞
- сложения ∞ + (−∞), (−∞) + ∞ и эквивалентные вычитания.
- Операции с комплексными результатами:
- Квадратный корень из отрицательного числа
- Логарифм отрицательного числа
- Тангенс 90 градусов и ему подобных (или π/2 радиан)
- Обратный синус и косинус от числа меньше −1 и больше +1.
Унарные операции
Унарными называются операции, которые имеют только один операнд. Унарные операции бывают префиксные и постфиксные.
Постфиксные унарные операции ставятся после операнда:
- Инкремент (увеличение на 1) ++
- Декремент (уменьшение на 1) --
Примеры:
1 2 3 4 |
int x = 3; short y = 100; x++; // после выполнения x становится равным 4. y--; // после выполнения y становится равным 99. |
Префиксные унарные операции ставятся перед операндом:
- Унарный плюс (обозначает положительные числа, хотя числа положительными будут и без него) +
- Унарный минус (обозначает отрицательные числа) -
- Логическое НЕ (инвертирует значение логического типа, превращая true в false и наоборот) !
- Префиксный инкремент (увеличивает значение на 1) ++
- Префиксный декремент (уменьшает значение на 1) --
Примеры:
1 2 3 4 5 6 7 8 |
int x1 = +10; // положительная десятка int x2 = -x1; // -10 boolean b1 = true boolean b2 = !b1; // false ++x1; // теперь x1 равен 11. --x2; // теперь x2 равен -11 |
Отличие постфиксного и префиксного инкремента и декремента
С виду может показаться, что префиксный и постфиксные инкремент и декремент одинаковы, но это не так. Их отличие в том, что префиксный инкремент и декремент возвращают значение, которое получилось после операции увеличения и уменьшения соответственно, а постфиксный инкремент и декремент возвращают исходное значение, которое было до увеличения или уменьшения.
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Main { public static void main(String[] args) { int x1 = 100; int x2 = 145; int y1 = ++x1; int y2 = --x2; // Вывод для префиксных операций System.out.println("\nPrefix ++, -- test"); System.out.println("x1=" + x1 + "; y1=" + y1); System.out.println("x2=" + x2 + "; y2=" + y2); // Возвращаем исходные значения x1 = 100; x2 = 145; int z1 = x1--; int z2 = x2++; // Вывод для постфиксных операций System.out.println("\nPostfix ++, -- test"); System.out.println("x1=" + x1 + "; z1=" + z1); System.out.println("x2=" + x2 + "; z2=" + z2); } } |
Не помню, описывал ли я это, но две косые черты // означают комментарий. Компилятор игнорирует любой текст, находящийся правее //, что позволяет записать какое-нибудь пояснение для будущего читателя программы. Строки System.out.println выводят текст в консоль.
Этот пример выводит в консоль следующее:
1 2 3 4 5 6 7 |
Prefix ++, -- test x1=101; y1=101 x2=144; y2=144 Postfix ++, -- test x1=99; z1=100 x2=146; z2=145 |
Как видно из примера y1 и y2 стали равны значениям x1 и x2, которые получились после осуществления операций инкремента и декремента соответственно, а z1 и z2 стали равны значениям x1 и x2, которые были до операций инкремента и декремента.
Операции сравнения
Операции сравнения позволяют проверить, больше ли один операнд другого, либо что один операнд равен другому и т. д.
Вот список операций сравнения в Java:
- == равенство (обратите внимание, что нужно использовать два символа равно для сравнения, а не один)
- != неравенство
- > больше
- >= больше или равно
- < меньше
- <= меньше или равно
Все операции сравнения возвращают логическое значение boolean, что означает, что результат операции сравнения можно присвоить переменной этого типа и использовать в любом месте, где требуется значение типа boolean.
Пример:
1 2 3 4 5 6 7 8 9 |
class Main { public static void main(String[] args) { int x = 3; double d = 3.1; System.out.println(x == d); // false System.out.println(x > d); // false System.out.println(x < d); // true } } |
При сравнении используются следующие правила:
- Если один из операндов NaN, то результат false.
- -Infinity меньше +Infinity
- -0.0 с плавающей точкой равен +0.0 с плавающей точкой
- При сравнении примитивов разных типов значение меньшего типа преобразуется в больший тип.
Логические И и ИЛИ
Логическое И && и Логическое ИЛИ || ведут себя вполне ожидаемо для логического И или логического ИЛИ:
1 2 3 4 5 6 7 8 9 |
boolean b1 = true && true; //true boolean b2 = true && false; //false boolean b3 = true || false; // true boolean b4 = false || false; //false System.out.println(b1); System.out.println(b2); System.out.println(b3); System.out.println(b4); |
Логическое И && вычисляет свой правый операнд только в том случае, если левый равен true. Если левый операнд равен false, то сразу возвращается false. Логическое ИЛИ || вычисляет правый операнд только в том случае, если левый равен false. Если левый операнд равен true, то сразу возвращается true. Эти два правила сокращения вычислений позволяют сразу откинуть последующие вычисления, если результат всего выражения уже известен. Это можно использовать для проверки на null перед проверкой результата какого-либо метода объекта (будет описано в дальнейшем):
1 2 3 |
if (obj != null && obj.method1()) { // obj.method1() будет вызывать только // если проверка obj!= null вернула true. } |
Операция instanceof
Классы я пока здесь не описал, но эту операцию невозможно объяснить без них. Операция instanceof проверяет, является ли объект экземпляром класса или экземпляром дочернего класса или экземпляром класса, реализующего интерфейс.
1 |
obj1 instanceof A |
Возвращается true, если obj1 не null и является экземпляром класса A или экземпляром дочернего класса A или экземпляром класса, реализующего интерфейс A.
1 2 3 4 |
Object obj1 = new String("test1"); if (obj1 instanceof String) { System.out.println("YES"); } |
Если левый операнд равен null, то результатом будет false. Код ниже выведет “NO”:
1 2 3 4 5 6 |
Object obj1 = null; if (obj1 instanceof String) { System.out.println("YES"); } else { System.out.println("NO"); } |
Тернарная операция
Операция «?:» называется тернарной, потому что он принимает три операнда.
1 |
<выражение_boolean> ? <выражение1> : <выражение2> |
Тернарная операция вычисляет <выражение_boolean>, если оно равно true, то вычисляет и возвращает <выражение1>, а если false, то <выражение2> .
1 2 3 4 5 6 7 8 |
class Main { public static void main(String[] args) { int x = 3 > 2 ? 5 : -3; // 5 String str1 = 3 == 2 ? "YES": "NO"; //"NO" System.out.println(x); System.out.println(str1); } } |
Битовые операции
Битовые операции в Java используются редко, но знать их нужно. Работают они так же, как и в Javascript.
Битовые операции в Java:
- Битовый сдвиг влево <<
- Битовый знаковый сдвиг вправо >>
- Беззнаковый битовый сдвиг вправо >>>. Он отличается от >> тем, что ставит 0 в самую левую позицию, а >> ставит то, что было в знаковом бите.
- Инвертация бит ~ меняет 0 на 1 и 1 на 0 во всех битах.
- Битовый & применяет побитовую операцию И
- Битовый | применяет побитовую операцию ИЛИ
- Битовый ^ применяет XOR (исключающее или)
Эти операнды работают так же, как и из аналоги в других языках программирования. Вряд ли имеет смысл их особенно сильно рассматривать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Main { public static void main(String[] args) { int n1 = 4; // 100 в двоичной системе System.out.println("n1 >> 1 = " + (n1 >> 1)); //2 или 10 // в двоичной системе. System.out.println("n1 << 1 = " + (n1 << 1)); ;// 8 или 100 // в двоичной системе. System.out.println("0b101 & 0b100 = " + (0b101 & 0b100)); // 4 (0b100) System.out.println("0b001 | 0b100 = " + (0b001 | 0b100)); // 5 (0b101) System.out.println("0b1110 ^ 0b1011 = " + (0b1110 ^ 0b1011)); //5 (0b101); System.out.println("-2 >> 1 = " + (-2 >> 1)); // -1 (единица со знака // сдвинется вправо, так что знак не поменяется) System.out.println("-2 >>> 1= " + (-2 >>> 1)); // 2147483647 (сменит // знак, так как левый бит заполнится нулём). System.out.println("~1 = " + ~1) ; // -2 (0b000...001 // превратится в 0b1111..10) } } |
Присвоение с выполнением другой операции
Операции += (сложение с присвоением), -= (вычитание с присвоением), *= (умножение с присвоением), /= (деление с присвоением), %= (взятие остатка с присвоением), &= (битовый И с присвоением), ^= (битовое исключающее ИЛИ с присвоением), |= (битовое ИЛИ с присвоением), <<= (сдвиг влево с присвоением), >>= (знаковый сдвиг вправо с присвоением), >>>= (беззнаковый сдвиг вправо с присвоением) позволяют сразу выполнить операции и присвоить результат другой переменной.
Они работают так:
1 |
E1 compop E2 |
эквивалентно
1 |
E1 = (T) E1 op E2; |
, где T — это тип переменной E1.
То есть int x1 += x2 эквивалентно int x1 = (int) x1 + x2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Main { public static void main(String[] args) { int x1 = 100; byte x2 = 100; int x3 = 100; x1 += 300; // эквивалентно x1 = (int) x1 + 300; x2 += 300; // эквивалентно x2 = (byte) x2 + 300; x3 += 300.1; // эквивалентно x3 = (int) x3 + 300.1; System.out.println("x1=" + x1); // 400 System.out.println("x2=" + x2); // -112 System.out.println("x3=" + x3); // 400 } } |
Приоритеты операций
Все операции вычисляются слева направо (сначала вычисляется левый операнд, затем правый и затем сама операций, кроме операции присваивания. Операция присваивания вычисляется справа налево.
Вычисления производятся в соответствии с таблицей приоритетов операций, приведённой ниже. Операция, находящаяся выше в таблице, имеет более высокий приоритет, чем операция, находящаяся ниже, и вычиляется раньше. Операции, находящиеся на одной строке, имеют одинаковый приоритет. Если в одном выражении находится несколько разных операций, то сначала вычисляется результат операции с наивысшим приоритетом. Можно использовать скобки для указания того, что сначала нужно вычислить эту часть выражения.
Пример 1:
1 |
int z = 200 * (3 + 4); |
Последовательность вычисляения такая:
- 3+4 = 7
- 200 * 7 = 1 400
- z = 1 400
Пример 2:
1 2 3 |
int x; int y; int z = x = y = 10000 + 20000 >> 1 + 3 * 2; |
Последовательность вычисления такая:
- 10 000 + 20 000 = 30 000 (Присвоение вычисляется справа налево, поэтому сначала смотрится y = 10000 + 20000 >> 1 + 3 * 2 , и вычисляется правая часть. В правой части ( 10000 + 20000 >> 1 + 3 * 2 ) вычисление идёт слева направо, и берётся 10000 + 20000 (выбирается среди 10000 + 20000 , 20000 >> 1 , 1 + 3 и 3 * 2 ), которое вычисляется перед сдвигом, так как у сложения приоритет выше.)
- 3 * 2 = 6 (В выражении 30000 >> 1 + 3 * 2 вычисление идёт слева направо, и выбирается умножение(среди 30000 >> 1 , 1 + 3 и 3 * 2 ), так как у него приоритет выше, что означает, что сложение будет выполнено раньше.)
- 1 + 6 = 7 (В выражении 30000 >> 1 + 6 вычисление идёт слева направо и сложение вычисляется раньше сдвига, так как приоритет у сложения выше.)
- 30 000 >> 7 = 234 (0b00…111010100110000 сдвигаем на 7 бит вправо и получаем 0b00…0011101010)
- y = 234
- x = 234
- z = 234
Таблица приоритетов операций
Группа операций | Приоритет |
---|---|
Группировка | ( ... ) |
Доступ к члену | ... . ... |
постфиксные | expr++ expr-- |
унарные | ++expr --expr +expr -expr ~ ! |
мультипликативные | * / % |
аддитивные | + - |
сдвиги | << >> >>> |
сравнения | < > <= >= instanceof |
равенства | == != |
бинарный И | & |
бинарный исключающее ИЛИ | ^ |
бинарный ИЛИ | | |
логический И | && |
логический ИЛИ | || |
тернарный | ? : |
лямбда | -> |
присваивания | = += -= *= /= %= &= ^= |= <<= >>= >>>= |
Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 выражения, инструкции и блоки».
Предыдущая статья — «Переменные в Java 8».
Приоритет операции «*» выше приоритета операции «+». Следовательно в выражении «int z = x = y = 10000 + 20000 >> 1 + 3 * 2;» сначала вычисляется часть «3*2», а уже потом «10000 + 20000». А в статье написано наоборот.
Вычисления выполняются слева направо. Часть выражения 10000 + 20000 — это самая первая полная операция, которая находится при проходе слева направо.
Вероятно, в описание instanceof можно добавить его поведение с null, а то встречал проверки на null перед его использованием.
Дополнил, спасибо.
Популярность этой статьи растёт. Она опасна?
То есть int x1 += x3 эквивалентно int x1 = (int) x1 + x2.
Ошибка. Должно быть так:
То есть int x1 += x3 эквивалентно int x1 = (int) x1 + x3.
Поправил, спасибо. Сделал x1 += x2 эквивалентно int x1 = (int) x1 + x2
Здравствуйте!
А почему в Java преобразования «short -> char» и «char -> short» являются сужающими?
Оба примитива занимают по 16 бит. Оба целочисленные. Где тут можно потерять точность?
Число преобразованное по цепочке «short -> char -> short» останется тем же числом.
Символ, преобразованный по цепочке «char -> short -> char» останется тем же символом.
При всем при этом требуется обязательно явно указывать приведение. Не пойму логику, которой руководствовались регламентируя данное правило.
Думаю, что в случае short -> char и char -> short преобразование сужающее, потому что short может принимать диапазон от -32 768 до +32 767 (включительно), а char может хранить только положительные значения от 0 до 65 535 (включительно). При конвертации отрицательных значений из short или слишком больших положительных из char получим совсем не те данные, что хотели бы. Поэтому оно сужающее. Пример: