© 2008 Наталия Македа
Все материалы блога защищены авторским правом. Любая перепечатка или использование материалов этого блога в коммерческих целях возможна лишь с письменного согласия автора. При некоммерческом использовании ссылка на блог обязательна.

среда, 28 мая 2008 г.

2.5 Операторы сравнения (Выпуск 8)

Операторы сравнения, <, >, <=, >=, ==, !=, возвращают результат типа boolean, то есть true или false. Эти операторы обычно используются в условных конструкциях (например, if () или циклы). Существует три типа сравнения:

  1. порядковое (ordinal) тестирует относительные значения числовых операндов.
  2. объектно-ориентированное определяет тип объекта во время исполнения программы.
  3. операторы равенства тестируют, одинаковы ли два значения, в том числе и нечисленные.

Рассмотрим их все подробнее.

Порядковые операторы (<, >, <=, >=)

К порядковым операторам относятся < (меньше), > (больше), <= (меньше либо равно), >= (больше либо равно). Они применяются ко всем численным типам и типу char и возвращают результат boolean. Например, если у нас имеются следующие объявления:

int p = 9;
int q = 65;
int r = -12;
float f = 9.0F;
char c = ‘A’;

то следующие тесты возвратят true:

p < q
f < q
f <= c
c > r
c >= q

Заметьте, что, когда эти операторы используются, применяется арифметическое распространение. Например, будет ошибкой присвоение значение 9.0F типа float переменной c типа char. Но к этой паре может быть применено сравнение! Для этого Java распространяет меньший тип к большему типу. То есть значение 'A' типа char (представляемое значением 65 в Unicode) распространяется до float 65.0F. Сравнение затем выполняется на результирующей паре значений float.

Порядковые сравнения не могут быть применены к объектным типам!

Оператор instanceof

Оператор instanceof тестирует класс объекта в момент исполнения программы (runtime). Левый операнд - любое выражение объектного типа (переменная или массив). Правый операнд - имя класса, интерфейса или массивный тип. Однако, левым операндом не может быть объект типа java.lang.Class, а правым операндом не может быть String.

Фрагмент кода ниже показывает пример использования оператора instanceof. Предположим, что существует класс Person с подклассом Parent:

1. public class Classroom {
2. private Hashtable inTheRoom = new Hashtable();
3. public void enterRoom(Person p) {
4. inTheRoom.put(p.getName(), p);
5. }
6. public Person getParent(String name) {
7. Object p = inTheRoom.get(name);
8. if (p instanceof Parent) {
9. return (Parent)p;
10. }
11. else {
12. return null;
13. }
14. }
15. }

Метод getParent() в строках 6-14 проверяет, содержит ли Hashtable родителя со специфическим именем. Для этого в Hashtable сначал ищется элемент со специфическим именем родителя. А потом проверяется, является ли этот элемент объектом типа Parent. Оператор instanceof проверяет, совпадает ли класс левого операнда с названием класса, заданного правым операндом или является его подклассом.

Правый операнд также может быть и названием интерфейса. И в этом случае оператор определяет, имплементирует ли левый аргумент данный интерфейс.

Опертор также используется для тестирования, является ли объект массивом. Поскольку массивы в Java являются объектами, то такое тестирование логично. Но тест проводится в два шага: (1) является ли объект массивом и (2) является ли тип элементов массива (под)классом правого аргумента. Это отражает идею, что массив, скажем, объектов типа Button (кнопки) является массивом объектов типа Component (компоненты), поскольку Button - это Component. Тест для такого массива будет выглядеть так:

if (x instanceof Component[])

Заметьте, что невозможно провести тестирование для "любого массива с элементами любого типа". То есть следующая строка недопустима:

if (x instanceof [])

Недопустимо также тестирование на массив с элементами типа Object:

if (x instanceof Object [])

поскольку элементы массива могут быть примитивного типа и тест, попросту, не сработает.

А как же протестировать, является ли объект массивом без привязки к типу его элементов? Допустим myObject это массив. Тогда следующая строка возвратит true:

myObject.getClass().isArray()

Если левый операнд равен null, то instanceof возвратит false. Никокого исключения не возникнет.

Операторы равенства (== и !=)

Операторы == и != тестируют, соответственно, равенство или неравенство. Для примитивных типов концепция равенства или неравенства достаточно тривиальна. Как и в случае с операторами порядка, операнды распространяются до наибольшего. Например, значение 10.0 типа float равно значению 10 типа byte. Для значений объектного типа, сравниваемая величина - это ссылка на объект, то есть адрес памяти.

Никогда не используйте операторы равенства для сравнения объектных типов (например строк)! Потому что они возвращают true только если адрес памяти совпадает, а не поля объектов совпадают. Для сравнения объектов используется метод equals().

Метод equals() должен был определён в классе сравниваемых объектов. Чтобы быть уверенными в этом, проверьте документацию. В документации должно быть указано, что equals() определён в классе или переопределяет (overrides) метод equals() своего супер-класса. Если документация ничего по этому поводу не говорит, то метод equals() будет, скорее всего, работать неправильно. Метод equals() принимает аргумент типа Object. Но в реальности вы должны передавать ему аргумент того же типа, что и объект, для которого он вызывается. Например, если вы вызываете x.equals(y), то y instanceof x должно возвращать true. Если же это не так, то equals() вернёт false.
Если вы определяете equals() в вашем собственном классе, то вы должны соблюдать три правила:
  • Аргумент для equals() должен быть объект типа Object. Не поддавайтесь искушению вставить аргумент того же типа, что и класс. Иначе вы перегрузите (overload) метод, а не переопределите (override) его. В итоге функциональность других частей кода, зависящих от equals() будет неправильна. Например, итерации по HashMap, то есть containsKey() и get(), будут неправильны.
  • equals() должен быть коммутативным оператором. То есть результат x.equals(y) должен быть равен результату y.equals(x).
  • Если вы лпределяете equals() в своём классе, то вы должны определить и hashCode(), который возвращает одно и то же значение для объектов, сравниваемых методом equals(). Это, опять таки, нужно для правильного функционирования итераций по контейнерам. Минимально допустимое поведение hashCode() - это return 1. Конечно, при такой имплементации потеряются все преимущества "настоящего" хеширования и HashMap будет просто связным списком. Но, по крайней мере, функциональность будет корректной.


Читать далее!

четверг, 15 мая 2008 г.

2.4. Операторы сдвига <<, >> и >>>(Выпуск 7)

В Java есть операторы сдвига. Операторы << и >> позаимствованы из С/C++. Кроме того, Java обладает своим новым оператором сдвига >>>.

Операторы сдвига присущи системам, которые могут выравнивать биты, прочтённые из IO портов или зартсываемые в IO порты. Это также быстрое умножение или деление на степень двойки. Преимущество операторов сдвига в Java - это независимость от платформы. Поэтому вы можете использовать их не беспокоясь ни о чём.

Основы сдвига

Сдвиг - это, по сути, простейшая операция: мы берём последовательность битов и двигаем её влево или вправо. Больше всего конфуза вызывает оператор >>>. Но о нём мы поговорим чуть позже.

Операторы сдвига могут применяться лишь к целым числам, то есть к типам int или long. Следующая таблица иллюстрирует базовый механизм сдвига.

Таблица 1: Идея сдвига
Исходные данные

192

Бинарное представление00000000000000000000000011000000
Сдвиг влево на 1 бит

0

0000000000000000000000011000000?
Сдвиг вправо на бит?00000000000000000000000011000000
Сдвиг влево на 4 бита00000000000000000000000011000000????
Исходные данные

-192

Бинарное представление11111111111111111111111101000000
Сдвиг влево на 1 бит

1

1111111111111111111111101000000?
Сдвиг вправо на бит?11111111111111111111111101000000

Таблица показывает фундаментальную идею сдвига: перемещение битов относительно их позиций. Это как очередь в магазине: как только один человек совершил покупку и отшёл, вся очередь сдвинулась и позиции всех участников очереди изменились.

Однако, глядя на таблицу, возникают три вопроса вопроса:

  1. Что происходит, если мы сдвигаем влево и при этом часть бинарной записи выходит за границу слева, а часть - остаётся пустой справа?
  2. Что происходит, когда справа - выход за границы, а слева - пустое место?
  3. Какое истинное значение принимает знак "?"?.

Ответим на часть этих вопросов. Биты, вышедшие за границы, просто теряются. Мы о них забываем.

В некоторых языках, типа ассемблер, есть операция ротации, когда при сдвиге вышедшие за границы биты не теряются, но ставятся на освободившееся место (вместо вопросиков). Однако языки высокого уровня, типа Java, не имеют в своём арсенале такой операции.

Сдвиг отрицательных чисел

Ответ на вопрос о значении символов "?" в приведенной выше таблице требует отдельного рассмотрения.

В случае сдвига влево << и беззнакового сдвига вправо >>> новые биты просто устанавливаются в ноль. В случае сдвига вправо со знаком >> новые биты принимают значение старшего (самого левого) бита перед сдвигом. Следующая таблица демонстрирует это:

Таблица 2: Сдвиг положительных и отрицательных чисел
Исходные данные

192

Бинарное представление00000000000000000000000011000000
Сдвиг вправо на 1 бит 00000000000000000000000001100000
Сдвиг вправо на 7 бит00000000000000000000000000000001
Исходные данные

-192

Бинарное представление11111111111111111111111101000000
Сдвиг вправо на 1 бит 11111111111111111111111110100000
Сдвиг вправо на 7 бит11111111111111111111111111111110

Заметьте: в том, случае, где старший бит был 0 перед сдвигом, новые биты стали тоже 0. Там где старший бит перед сдвигом был 1, новые биты тоже заполнились 1.

Это правило может показаться странным на первый взгляд. Но оно имеет под собой очень серьёзное обоснование. Если мы сдвигаем бинарное число влево на одну позицию, то в десятичной записи мы умножаем его на два. Если мы сдвигаем влево на n позиций, то умножение происходит на 2n, то есть на 2, 4, 8, 16 и т.д.

Сдвиг вправо даёт деление на степени двойки. При этом, добавление слева нулей на появившиеся биты на самом деле даёт деление на степени двойки лишь в случае положительных чисел. Но для отрицательных чисел всё совсем по другому!

Как известно, старший бит отрицательных чисел равен единице, 1. Для того, чтобы сохранить старшинство единицы при сдвиге, то есть сохранить отрицательный знак результата деления отрицательного числа на положительное (степень двойки), нам нужно подставлять единицы на освободившиеся места.

Если мы посмотрим на Таблицу 2, то заметим, что 192, сдвинутое на 1 бит вправо - это 192/2=96, а сдвинутое на 7 битов вправо - это 192/27=192/128=1 по законам целочисленной арифметики. С другой стороны, -192 сдвинутое на 1 бит вправо - это 192/2=-96 и т.д.

Есть, однако пример, когда реультат сдвига вправо отличается от результата целочисленного деления на 2. Это случай, когда аргумент = -1. При целочисленном делении мы имеем: -1/2=0. Но результат сдвига вправо нам даёт -1. Это можно трактовать так: целочисленное деление округляет к нулю, а сдвиг округляет к -1.

Таким образом, сдвиг вправо имеет две ипостаси: одна (>>>) просто сдвигает битовый паттерн "в лоб", а другая (>>) сохраняет эквивалентность с операцией деления на 2.

Зачем же Java потребовался беззнаковый сдвиг вправо (сдвиг "в лоб"), когда ни в С, ни в С++ его не существует? Ответ прост, потому что в С и С++ сдвиг всегда беззнаковый. То есть >>>> в Java - это и есть сдвиг вправо в C и C++. Но, поскольку в Java все численные типы со знаком (за исключением char), то и результаты сдвигов должны иметь знаки.

Сокращение (reduction) правого операнда

На самом деле у операторов сдвига есть правый операнд - число позиций, на которое нужно произвести сдвиг. Для корректного сдвига это число должно быть меньше, чем количество битов в результате сдвига. Если число типа int (long), то сдвиг не может быть сделан более, чем на 32 (64) бита.

Оператор же сдвига не делает никаких проверок данного условия и допускает операнды, его нарушающие. При этом правый операнд сокращается по модулю от нужного количества битов. Например, если вы захотите сдвинуть целое число на 33 бита, то сдвиг произойдёт на 33%32=1 бит. В результатае такого сдвига мы легко можем получить аномальные результаты, то есть результаты, которых мы не ожидали. Например, при сдвиге на 33 бита мы ожидаем получить 0 или -1 (в знаковой арифметике). Но это не так.

Почему Java сокращает правый операнд оператора сдвига или грустная история о заснувшем процессоре

Одной из главной причин введения сокращения было то, что процессоры сами сокращают подобным образом правый операнд оператора сдвига. Почему?

Несколько лет назад был создан мощнейший процессор с длинными регистрами и операциями ротации и сдвигам на любое количество битов. Именно потому, что регистры были длинными, корректное выполнение этих операций требовало несколько минут.

Основным применением данных процессоров был контроль систем реального времени. В данных системах самый быстрый ответ на внешнее событие должно занимать не более задержки на прерывание (interrupt latency). Отдельные инскрукции таких процессоров были неделимы. Поэтому выполнение длинных операций (сдвига на несколько бит и ротации) нарушало эффективную работу процессора.

Следующая версия процессора имплементировала эти операции уже по-другому: размер правого операнда сократился. Задержка на прерывание восстанавилась. И многие процессоры переняли данную практику.

Арифметическое распространение (promotion) операндов

Апифметическое распространение операндов происходит перед применением оперции сдвига и гарантирует, что операнды по крайней мере типа int. Это явление имеет особый эффект на беззнаковый сдвиг вправо, когда сдвигаемое число меньше, чем int: мы получаем не тот результат, который ожидали.

Следующая таблица показывает пример аномалии:

Таблица 3: Арифметическое распространение для беззнакового сдвига вправо, когда операнд меньше, чем int
Исходные данные (-64 в десятичной записи)

11000000

Распространение до int11111111111111111111111111000000
Сдвиг вправо на 4 битa 00001111111111111111111111111100
Сокращение до байта11111100
Ожидаемый результат был00001100


Читать далее!

суббота, 3 мая 2008 г.

Сложение и вычитание, NaN (Выпуск 6)

Сложение и вычитание (+ и -)

За сложение и вычитание отвечают соответсвенно операторы + и -. Они применимы для операндов любых численных типов. Помимо этого, операция + применяется и для операндов типа String. Когда один из операндов типа String, то результат будет объектом типа String.

Особенности оператора +

В Java не допускается определять перегрузку операторов (то есть перепрограммирование операторов в зависимости от типа), как в С/С++. Но сам язык Java перегружает операторы автоматически. И это в общем-то не ново, поскольку многие языки программирования, которые поддерживают множественные арифметические типы, определяют автоматическую перегрузку арифметических операторов для примитивных типов. Java, помимо этого, имеет перегрузку оператора + для строкового типа (String). И результат действия оператора + в данном случае - это конкатенация, сцепление строк. Если один из операндов не строковый, то к нему будет предварительно применено приведение типа к строке.

Перегрузка (overloading) - это термин, упоминаемый в том случае, когда одна и та же операция (оператор) используется для операндов (аргументов) различного типа. При этом поведение операции (оператора) определяется типом операндов, к котором она должна быть применена. Например, метод println() может применяться как для операндов строкового, так и целочисленного типа. Эти два использования, на самом деле, относятся к совершенно различным методам. Просто было использовано одно и то же имя операции. Аналогично сложение (+) используется, как для целочисленных, так и для дробных типов. Но код, реализующий это сложение совершенно различный.

Вы все знаем, что произойдёт при сложении двух знацений численного типа: операнды будут суммированы. Конечно, может случится и переполнение, если мы суммироем слишком большие числа. Но в общем и целом, поведение арифметических операторов не изобилует сюрпризами.

Если переполнение происходит при арифметическом сложении или вычитании, никаких сообщений об ошибках или исключений мы не получаем. Дело обстоит совсем по-другому, если мы конкатенируем строки. Если оба оператора строки, то результат - это "скленная" строка. Если один из операторов строка, а второй - число, то число будет приведено к типу String.

Приведение численных типов к строке

Для объектных типов приобразование к типу String происходит посредством вызова метода toString(). Этот метод определён в классе java.lang.Object, который является прародителем всех остальных классов. Поэтому все объекты наследуют от Object метод toString(). Однако, этот метод выдаёт некое зашифрованное значение объекта. Например, если мы рассмотрим такой код:

1. public class Test {
2. private static class MyClass {
3. private String name;
4. private int age;
5. public MyClass() {
6. name = "Natalia Macheda";
7. age = 28;
8. }
9.
10. }
11. public static void main (String[] args) {
12. MyClass myObj = new MyClass();
13. System.out.println(myObj.toString());
14. }
15.}

То результат, выводимый 13-й строкой будет такого вида: Test$MyClass@c17164. Здесь мы видим имена классов объекта (разделённые символом $) и какой-то идентификатор после символа @. Идентификатор - это, как правило, ссылка на объект.

Это не очень удобно для понимания, но может быть использовано, например, для отладки программы. С другой строны, для предания читаемости результату операции toString() необходимо перегрузить эту операцию. Например, если в 9-й строке вставить код:

9.       public String toString(){
9'. return (name + " " + age);
9''. }

то результат будет Natalia Macheda 28. Заметьте, что втрой аргумент - целочисленного типа. Как же произошло его преобразование в строку?

Преобразование чесленных аргументов к строчному типу неявно использует метод toString() классов-оболочек (wrapper). Например, значение типа int конвертируется вызовом статической функции Integer.toString().

Резюмируем сведения о сложении. Сложение двух числовых значений примтивных типов даёт результат:

  • примитивного численного типа;
  • по крайней мере самого длинного типа среди типов операндов, участвующих в сложении;
  • вычисленный приведением обоих операндов к типу результата и их дальнейшим суммированием.
  • Результат может переполнить тип и точность вычисления может потеряться.

Если один из операндов - НЕпримитивного типа:

  • второй операнд должен быть строкового типа. Иначе операция нелегальна;
  • операнд НЕстрокового типа приводится к типу String и результат - сцепление двух строк.

Приведение операнда объектного типа к строке производится путём вызова метода toString().

Приведение операнда примитивного типа к строке производится неявно путём вызова статичского метода класса-оболочки.

Если вы хотите держать под контролем форматирование результата приведения типа, обратитесь к коду пакета java.text.

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

Арифметические ошибки

Мы ожидаем от арифметических операций результата, который выдаёт знаяение с математическим смыслом. Но, как уже было сказано выше, результат может не соответсвовать нашим ожиданиям в виду ограниченности ресурсов компьютера. В частности:

  • Деление на ноль (в том числе и для операции %) выдаёт ArithmeticException;
  • Никаких других исключений для арифметических операций не выдаётся, но результат может быть арифметически неправильным ввиду переполнения.
  • Дробные типы представляют абстрактные значения при помощи следующих значений: IEEE 754 бесконечности, минус бесконечности и Не-числа NaN (Not a Number). Именные константы, представляющие эти значения, объявлены в классах Float и Double.

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

Сравнение с НЕ-числом

Два НЕ-числа NaN определены в пакете java.lang. Это Float.NaN и Double.NaN. Следующие сравнения всегда дают false, даже если x равен NaN:

x < Float.NaN
x <= Float.NaN
x == Float.NaN
x > Float.NaN
x >= Float.NaN

В тестах Float.NaN != Float.NaN и Doube.NaN != Double.NaN результат будет true.

Если же вы хотите проверить, что число (не) является NaN, то нужно использовать статические методы Float.isNaN(float) или Double.isNaN(double), определённые в пакете java.lang.


Читать далее!