Логические операторы. Логические операторы "и" и "или" ( & и | ) можно использовать в двух вариантах

Логические операторы "и" и "или" ( & и | ) можно использовать в двух вариантах. Это связано с тем, что, как легко убедиться, для каждого оператора возможны случаи, когда значение первого операнда сразу определяет значение всего логического выражения. Если вторым операндом является значение некоторой функции, то появляется выбор – вызывать ее или нет, причем это решение может сказаться как на скорости, так и на функциональности программы.

Первый вариант операторов ( &, | ) всегда вычисляет оба операнда, второй же – ( &&, || ) не будет продолжать вычисления, если значение выражения уже очевидно. Например:

int x=1;(x>0) | calculate(x) // в таком выражении // произойдет вызов // calculate(x>0) || calculate(x) // а в этом - нет

Логический оператор отрицания "не" записывается как ! и, конечно, имеет только один вариант использования. Этот оператор меняет булевское значение на противоположное.

int x=1;x>0 // выражение истинно!(x>0) // выражение ложно

Оператор с условием ?: состоит из трех частей – условия и двух выражений. Сначала вычисляется условие (булевское выражение), а на основании результата значение всего оператора определяется первым выражением в случае получения истины и вторым – если условие ложно. Например, так можно вычислить модуль числа x:

x>0 ? x : -x

Битовые операции

Прежде чем переходить к битовым операциям, необходимо уточнить, каким именно образом целые числа представляются в двоичном виде. Конечно, для неотрицательных величин это практически очевидно:

0 01 12 103 114 1005 101

и так далее. Однако как представляются отрицательные числа? Во-первых, вводят понятие знакового бита. Первый бит начинает отвечать за знак, а именно 0 означает положительное число, 1 – отрицательное. Но не следует думать, что остальные биты остаются неизменными. Например, если рассмотреть 8-битовое представление:

-1 10000001 // это НЕВЕРНО!-2 10000010 // это НЕВЕРНО!-3 10000011 // это НЕВЕРНО!

Такой подход неверен! В частности, мы получаем сразу два представления нуля – 00000000 и 100000000, что нерационально. Правильный алгоритм можно представить себе так. Чтобы получить значение -1, надо из 0 вычесть 1:

00000000- 00000001------------- 11111111

Итак, -1 в двоичном виде представляется как 11111111. Продолжаем применять тот же алгоритм (вычитаем 1):

0 00000000-1 11111111-2 11111110-3 11111101

и так далее до значения 10000000, которое представляет собой наибольшее по модулю отрицательное число. Для 8-битового представления наибольшее положительное число 01111111 (=127), а наименьшее отрицательное 10000000 (=-128). Поскольку всего 8 бит определяет 28=256 значений, причем одно из них отводится для нуля, то становится ясно, почему наибольшие по модулю положительные и отрицательные значения различаются на единицу, а не совпадают.

Как известно, битовые операции "и", "или", "исключающее или" принимают два аргумента и выполняют логическое действие попарно над соответствующими битами аргументов. При этом используются те же обозначения, что и для логических операторов, но, конечно, только в первом (одиночном) варианте. Например, вычислим выражение 5&6:

00000101& 00000110------------- 00000100 // число 5 в двоичном виде // число 6 в двоичном виде //проделали операцию "и" попарно над битами // в каждой позиции

То есть выражение 5&6 равно 4.

Исключение составляет лишь оператор "не" или "NOT", который для побитовых операций записывается как ~ (для логических было !). Этот оператор меняет каждый бит в числе на противоположный. Например, ~(-1)=0. Можно легко установить общее правило для получения битового представления отрицательных чисел:

Если n – целое положительное число, то -n в битовом представлении равняется ~(n-1).

Наконец, осталось рассмотреть лишь операторы побитового сдвига. В Java есть один оператор сдвига влево и два варианта сдвига вправо. Такое различие связано с наличием знакового бита.

При сдвиге влево оператором << все биты числа смещаются на указанное количество позиций влево, причем освободившиеся справа позиции заполняются нулями. Эта операция аналогична умножению на 2n и действует вполне предсказуемо, как при положительных, так и при отрицательных аргументах.

Рассмотрим примеры применения операторов сдвига для значений типа int, т.е. 32-битных чисел. Пусть положительным аргументом будет число 20, а отрицательным -21.

// Сдвиг влево для положительного числа 2020 << 00 = 00000000000000000000000000010100 = 2020 << 01 = 00000000000000000000000000101000 = 4020 << 02 = 00000000000000000000000001010000 = 8020 << 03 = 00000000000000000000000010100000 = 16020 << 04 = 00000000000000000000000101000000 = 320...20 << 25 = 00101000000000000000000000000000 = 67108864020 << 26 = 01010000000000000000000000000000 = 134217728020 << 27 = 10100000000000000000000000000000 = -161061273620 << 28 = 01000000000000000000000000000000 = 107374182420 << 29 = 10000000000000000000000000000000 = -214748364820 << 30 = 00000000000000000000000000000000 = 020 << 31 = 00000000000000000000000000000000 = 0// Сдвиг влево для отрицательного числа -21-21 << 00 = 11111111111111111111111111101011 = -21-21 << 01 = 11111111111111111111111111010110 = -42-21 << 02 = 11111111111111111111111110101100 = -84-21 << 03 = 11111111111111111111111101011000 = -168-21 << 04 = 11111111111111111111111010110000 = -336-21 << 05 = 11111111111111111111110101100000 = -672...-21 << 25 = 11010110000000000000000000000000 = -704643072-21 << 26 = 10101100000000000000000000000000 = -1409286144-21 << 27 = 01011000000000000000000000000000 = 1476395008-21 << 28 = 10110000000000000000000000000000 = -1342177280-21 << 29 = 01100000000000000000000000000000 = 1610612736-21 << 30 = 11000000000000000000000000000000 = -1073741824-21 << 31 = 10000000000000000000000000000000 = -2147483648

Как видно из примера, неожиданности возникают тогда, когда значащие биты начинают занимать первую позицию и влиять на знак результата.

При сдвиге вправо все биты аргумента смещаются на указанное количество позиций, соответственно, вправо. Однако встает вопрос – каким значением заполнять освобождающиеся позиции слева, в том числе и отвечающую за знак. Есть два варианта. Оператор >> использует для заполнения этих позиций значение знакового бита, то есть результат всегда имеет тот же знак, что и начальное значение. Второй оператор >>> заполняет их нулями, то есть результат всегда положительный.

// Сдвиг вправо для положительного числа 20// Оператор >>20 >> 00 = 00000000000000000000000000010100 = 2020 >> 01 = 00000000000000000000000000001010 = 1020 >> 02 = 00000000000000000000000000000101 = 520 >> 03 = 00000000000000000000000000000010 = 220 >> 04 = 00000000000000000000000000000001 = 120 >> 05 = 00000000000000000000000000000000 = 0// Оператор >>>20 >>> 00 = 00000000000000000000000000010100 = 2020 >>> 01 = 00000000000000000000000000001010 = 1020 >>> 02 = 00000000000000000000000000000101 = 520 >>> 03 = 00000000000000000000000000000010 = 220 >>> 04 = 00000000000000000000000000000001 = 120 >>> 05 = 00000000000000000000000000000000 = 0

Очевидно, что для положительного аргумента операторы >> и >>> работают совершенно одинаково. Дальнейший сдвиг на большее количество позиций будет также давать нулевой результат.

// Сдвиг вправо для отрицательного числа -21// Оператор >>-21 >> 00 = 11111111111111111111111111101011 = -21-21 >> 01 = 11111111111111111111111111110101 = -11-21 >> 02 = 11111111111111111111111111111010 = -6-21 >> 03 = 11111111111111111111111111111101 = -3-21 >> 04 = 11111111111111111111111111111110 = -2-21 >> 05 = 11111111111111111111111111111111 = -1// Оператор >>>-21 >>> 00 = 11111111111111111111111111101011 = -21-21 >>> 01 = 01111111111111111111111111110101 = 2147483637-21 >>> 02 = 00111111111111111111111111111010 = 1073741818-21 >>> 03 = 00011111111111111111111111111101 = 536870909-21 >>> 04 = 00001111111111111111111111111110 = 268435454-21 >>> 05 = 00000111111111111111111111111111 = 134217727...-21 >>> 24 = 00000000000000000000000011111111 = 255-21 >>> 25 = 00000000000000000000000001111111 = 127-21 >>> 26 = 00000000000000000000000000111111 = 63-21 >>> 27 = 00000000000000000000000000011111 = 31-21 >>> 28 = 00000000000000000000000000001111 = 15-21 >>> 29 = 00000000000000000000000000000111 = 7-21 >>> 30 = 00000000000000000000000000000011 = 3-21 >>> 31 = 00000000000000000000000000000001 = 1

Как видно из примеров, эти операции аналогичны делению на 2n. Причем, если для положительных аргументов с ростом n результат закономерно стремится к 0, то для отрицательных предельным значением является -1.

Заключение

В этой лекции были рассмотрены основы лексического анализа программ Java. Для их записи применяется универсальная кодировка Unicode, позволяющая использовать любой язык помимо традиционного английского. Еще раз напомним, что использование Unicode возможно и необходимо в следующих конструкциях:

  • комментарии;
  • идентификаторы ;
  • символьные и строковые литералы.

Остальные же ( пробелы, ключевые слова, числовые, булевские и null- литералы, разделители и операторы) легко записываются с применением лишь ASCII -символов. В то же время любой Unicode -символ также можно задать в виде специальной последовательности ASCII -символов.

Во время анализа компилятор выделяет из текста программы < пробелы > (были рассмотрены все символы, которые рассматриваются как пробелы ) и комментарии, которые полностью удаляются из кода (были рассмотрены все виды комментариев, в частности комментарий разработчика). Пробелы и все виды комментариев служат для разбиения текста программы на лексемы. Были рассмотрены все виды лексем, в том числе все виды литералов.

В дополнении были рассмотрены особенности применения различных операторов.

Лекция 4. Типы данных

Введение

Java является строго типизированным языком. Это означает, что любая переменная и любое выражение имеют известный тип еще на момент компиляции. Такое строгое правило позволяет выявлять многие ошибки уже во время компиляции. Компилятор, найдя ошибку, указывает точное место (строку) и причину ее возникновения, а динамические "баги" (от английского bugs) необходимо сначала выявить с помощью тестирования (что может потребовать значительных усилий), а затем найти место в коде, которое их породило. Поэтому четкое понимание модели типов данных в Java очень помогает в написании качественных программ.

Все типы данных разделяются на две группы. Первую составляют 8 простых, или примитивных (от английского primitive), типов данных. Они подразделяются на три подгруппы:

  • целочисленные
    • byte
    • short
    • int
    • long
    • char (также является целочисленным типом)
  • дробные
    • float
    • double
  • булевые
    • boolean

Вторую группу составляют объектные, или ссылочные (от английского reference), типы данных. Это все классы, интерфейсы и массивы. В стандартных библиотеках первых версий Java находилось несколько сот классов и интерфейсов, сейчас их уже тысячи. Кроме стандартных, написаны многие и многие классы и интерфейсы, составляющие любую Java-программу.

Иллюстрировать логику работы с типами данных проще всего на примере переменных.

Переменные

Переменные используются в программе для хранения данных. Любая переменная имеет три базовых характеристики:

  • имя;
  • тип;
  • значение.

Имя уникально идентифицирует переменную и позволяет обращаться к ней в программе. Тип описывает, какие величины может хранить переменная. Значение – текущая величина, хранящаяся в переменной на данный момент.

Работа с переменной всегда начинается с ее объявления (declaration). Конечно, оно должно включать в себя имя объявляемой переменной. Как было сказано, в Java любая переменная имеет строгий тип, который также задается при объявлении и никогда не меняется. Значение может быть указано сразу (это называется инициализацией), а в большинстве случаев задание начальной величины можно и отложить.

Некоторые примеры объявления переменных примитивного типа int с инициализаторами и без таковых:

int a;

int b = 0, c = 3+2;

int d = b+c;

int e = a = 5;

Из примеров видно, что инициализатором может быть не только константа, но и арифметическое выражение. Иногда это выражение может быть вычислено во время компиляции (такое как 3+2 ), тогда компилятор сразу записывает результат. Иногда это действие откладывается на момент выполнения программы (например, b+c ). В последнем случае нескольким переменным присваивается одно и то же значение, однако объявляется лишь первая из них (в данном примере е ), остальные уже должны существовать.

Резюмируем: объявление переменных и возможная инициализация при объявлении описываются следующим образом. Сначала указывается тип переменной, затем ее имя и, если необходимо, инициализатор, который может быть константой или выражением, вычисляемым во время компиляции или исполнения программы. В частности, можно пользоваться уже объявленными переменными. Далее можно поставить запятую и объявить новую переменную точно такого же типа.

После объявления переменная может применяться в различных выражениях, в которых будет браться ее текущее значение. Также в любой момент можно изменить значение, используя оператор присваивания, примерно так же, как это делалось в инициализаторах.

Кроме того, при объявлении переменной может быть использовано ключевое слово final. Его указывают перед типом переменной, и тогда ее необходимо сразу инициализировать и уже больше никогда не менять ее значение. Таким образом, final -переменные становятся чем-то вроде констант, но на самом деле некоторые инициализаторы могут вычисляться только во время исполнения программы, генерируя различные значения.

Простейший пример объявления final -переменной:

final double pi=3.1415;








Дата добавления: 2016-03-22; просмотров: 1116;


Поиск по сайту:

При помощи поиска вы сможете найти нужную вам информацию.

Поделитесь с друзьями:

Если вам перенёс пользу информационный материал, или помог в учебе – поделитесь этим сайтом с друзьями и знакомыми.
helpiks.org - Хелпикс.Орг - 2014-2024 год. Материал сайта представляется для ознакомительного и учебного использования. | Поддержка
Генерация страницы за: 0.01 сек.