24 апр. 2015 г.

Операторы и выражения в Java. Часть 2 – описание таблицы приоритетов операторов

Здесь мы подробно рассмотрим таблицу приоритетов операторов приведенную в прошлом посте.

Приоритет

В колонке P таблицы приоритетов операторов определен приоритет (precedence) для каждого оператора. Приоритет определяет порядок выполнения операторов. Рассмотрим следующее выражение:

a + b * c

Оператор умножения имеет более высокий приоритет, чем оператор сложения, поэтому a прибавляется к произведению b и c. Приоритет операторов можно считать показателем степени связанности операторов с их операндами. Чем больше число, тем сильнее они связаны.

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

(a + b) * c

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

// Приведение класса, объединенное с доступом к члену
(( Integer) o).intValue();

// Присваивание в совокупности со сравнением
while(( line = in.readLine()) != null) { ... }

// Побитовые операторы в совокупности со сравнением
if (( flags & (PUBLIC | PROTECTED)) != 0) { ... }

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

Ассоциативность

Если выражение включает несколько операторов с одинаковым приоритетом, то порядком выполнения операций управляет ассоциативность операторов. Большинство операторов ассоциативны слева направо, то есть операции выполняются слева направо. Однако операторы присваивания и унарные операторы обратно ассоциативны (справа налево). В колонке А таблицы приоритетов операторов определена ассоциативность для каждого оператора или группы операторов. Значение L означает ассоциативность слева направо, а R означает обратную ассоциативность.

Аддитивные операторы ассоциативны слева направо, то есть выражение a+b-c вычисляется слева направо: (a+b)-c. Унарные операторы и операторы присваивания вычисляются справа налево. Рассмотрим более сложное выражение:

a = b += c = ~d

Оно вычисляется следующим образом:

a = ( b += ( c = (~d)))

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

Количество и типы операндов

В четвертой колонке таблицы приоритетов операторов определены возможные типы и количество операндов для каждого оператора. Некоторые операторы работают только с одним операндом; они называются унарными операторами. Например, оператор «унарный минус» меняет знак отдельного числа.

-n // Оператор «унарный минус»

Однако большинство операторов являются бинарными; они работают с двумя операндами. Оператор (минус) может выступать в обеих формах:

a-b // Оператор вычитания является бинарным

В Java также определен один тернарный (ternary) оператор (работает с тремя операндами), часто называемый условным оператором. Он похож на условный оператор if, но стоит внутри выражения. Эти три операнда разделяются знаком вопроса и двоеточием; второй и третий операнды должны относиться к одному типу.

x > y ? x : y // Тернарное выражение, возвращает большее число из x и y.

Тернарный оператор мы рассмотрим поподробнее чуть позже.

Кроме определенного количества операндов, каждый оператор ожидает конкретные типы операндов. В четвертой колонке таблицы перечислены типы операндов. Некоторые коды из этой колонки необходимо объяснить подробнее:

  • число
    Целое число, значение с плавающей точкой или символ (то есть любой примитивный тип, кроме boolean).
  • целое число
    Значения: byte, short, int, long или char (значения long не допускаются в операторе доступа к массиву []).
  • ссылка
    Объект или массив.
  • переменная
    Переменная или любая другая величина (например, элемент массива), которой
    можно присвоить значение.

Возвращаемый тип

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

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

Побочные эффекты

GrabliКаждый оператор вычисляет значение на основе одного или нескольких операндов. Однако некоторые операторы, в дополнение к основному вычислению, приводят к побочным эффектам. Если выражение подразумевает побочные эффекты, то при его вычислении состояние Java программы изменяется настолько, что повторное вычисление выражения может привести к результату, отличному от первого. Например, оператор инкремента ++ имеет побочный эффект приращения переменной. Выражение ++a увеличивает значение переменной a и возвращает новое, увеличенное значение. При следующем вычислении этого выражения получится уже другое значение. Различные операторы присваивания также имеют побочные эффекты. Например, выражение a*=2 можно записать в виде a=a*2. Значением выражения является значение a, умноженное на 2, но выражение имеет побочный эффект сохранения нового значения в a. Оператор вызова метода ()  имеет побочные эффекты, если побочные эффекты есть у вызываемого метода. Некоторые методы, например Math.sqrt(), просто вычисляют и возвращают значение без каких-либо побочных эффектов. Однако обычно методы всетаки имеют побочные эффекты. И наконец, оператор new имеет существенный побочный эффект, выраженный в создании нового объекта.

Порядок вычислений

Вычисляя выражение, интерпретатор Java выполняет различные операции в последовательности, заданной круглыми скобоками, а также приоритетом и ассоциативностью операторов. Однако перед началом операции интерпретатор вычисляет операнды оператора (исключение составляют операторы &&, ||  и ?:, которые не всегда вычисляют все свои операнды). Интерпретатор всегда вычисляет операнды слева направо. Это имеет значение, если какойлибо из операндов является выражением с побочными эффектами. В качестве примера рассмотрим следующий код:

int a = 2;
int v = ++a + ++a * ++a;

Хотя умножение выполняется перед сложением, первыми вычисляются операнды оператора ++. Таким образом, выражение равно 3+4*5, или 23.

Хотя большинство операторов мы уже рассмотрели совместно с примитивными типами данных, но там был акцент именно на примитивных типах данных и операторы мы использовали чтобы понять что это за типы :-). Теперь же рассмотрим операторы чуть подробнее, так как там тоже могут быть грабельки.

Арифметические операторы

Поскольку большинство программ работает, в первую очередь, с числами, наиболее часто используются операторы, выполняющие арифметические операции. Арифметические операторы можно задавать с целыми числами, числами с плавающей точкой и даже с символами (то есть их можно применять с любым примитивным типом данных, кроме boolean). Арифметические операции с плавающей точкой применяются, если один из операндов является числом с плавающей точкой; в противном случае задействуется целочисленная арифметика. Это важно, так как арифметика для чисел с плавающей точкой отличается от целочисленной арифметики – например, по способу деления и способу обработки переполнения и округления. Существуют следующие арифметические операторы:

Сложение (+)

GrabliОператор + складывает два числа. Как вы уже возможно замечали, оператор + можно применять для сцепления строк. Если один из операндов сложения является строкой, то другой операнд также преобразуется в строку. Если вы хотите сочетать сложение со сцеплением, убедитесь в наличии круглых скобок. Например:

System.out.println("Total: " + 3 + 4); // Отображает "Total: 34", не 7!

 

Вычитание (−)

Если оператор является бинарным, то он вычитает второй операнд из первого. Например, 7−3 равняется 4. Оператор «» может выполнять унарную инверсию.

Умножение (*)

Оператор * умножает два числа. Например, 7*3 равняется 21.

Деление (/)

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

7/3       // Равно 2
7/3.0f   // Равно 2.333333f
7/0       // Генерируется ArithmeticException
7/0.0    // Равно плюс бесконечности
0.0/0.0 // Равно NaN

Деление по модулю (%)

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

Оператор % вычисляет первый операнд по модулю второго (то есть он возвращает целый остаток от деления первого операнда на второй). Например, 7%3 дает 1. Знак результата совпадает со знаком первого операнда. Хотя оператор взятия по модулю обычно используют с целыми числами, он также подходит для значений с плавающей точкой. Например, 4.3%2.1 равняется 0.1. Вычисление значений по модулю нуль для целых чисел приводит к ArithmeticException. Для значений с плавающей точкой любое значение по модулю 0.0 и бесконечность по любому модулю дают NaN.

Унарный минус (−)

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

Оператор сцепления строк

Кроме сложения чисел, оператор + и родственный оператор += могут сцеплять, или соединять, строки. Если один из складываемых операндов является строкой, оператор преобразовывает другой операнд в строку. Например:

// Отображает "Quotient: 2.3333333"
System.out.println("Quotient: " + 7/3.0f);

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

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

Операторы инкремента и декремента

Оператор ++ увеличивает на единицу операнд, который является переменной, элементом массива или полем объекта. Поведение данного оператора зависит от его положения относительно операнда. Если оператор находится перед операндом, то он называется оператором префиксной формы инкремента (preincrement). Он увеличивает значение операнда на единицу и возвращает вычисленное значение. Если же оператор находится после операнда, то он называется оператором постфиксной формы инкремента (postincrement). Такой оператор увеличивает значение операнда на единицу, но возвращает значение операнда до увеличения.

Например, в следующем коде обе величины i и j  получают значение 2:

i = 1;
j = ++i;

Однако в следующем примере i получает значение 2, a j  – значение 1:

i = 1;
j = i++;

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

Выражения x++ и x-- эквивалентны выражениям x=x+1 и x=x-1  соответственно; x вычисляется один раз, за исключением случаев использования операторов инкремента и декремента. Если x является выражением с побочными эффектами, это существенно меняет дело. Например, следующие два выражения не идентичны:

a[ i++]++;  // Увеличивает элемент массива
a
[ i++] = a[ i++] + 1; // Прибавляет единицу к одному элементу массива, а сохраняет его в другом

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

Операторы сравнения и булевы операторы мы подробно рассмотрели с примитивным типом boolean тут. А побитовые операторы и операторы побитового сдвига рассмотрели с целочисленными типами тут.

Операторы присваивания

Операторы присваивания сохраняют, или присваивают, значение какой-либо переменной. Левый операнд должен быть локальной переменной, элементом массива или полем объекта. Справа может находиться любое значение типа, совместимого с переменной. В отличие от всех остальных операторов, операторы присваивания обратно ассоциативны, то есть присваивания в a=b=c выполняются справа налево: a=(b=c) .

Условный оператор

Условный оператор ?:  является тернарным (три операнда) оператором, унаследованным из языка С. Он позволяет внедрять условие в само выражение. Его можно представить как версию оператора if/else. Знак вопроса (?) разделяет первый и второй операнды условного оператора, а двоеточие (:) – второй и третий. Первый операнд должен иметь boolean значение. Второй и третий операнды могут быть любого типа, но они должны приводиться к одному и тому же типу. Вначале условный оператор вычисляет первый операнд. Если он истинен, оператор вычисляет второй операнд, а результат представляет как значение выражения. В противном случае, если первый операнд ложен, условный оператор вычисляет и возвращает третий операнд. Условный оператор никогда не вычисляет оба операнда (второй и третий), поэтому следует осторожно использовать выражения с побочными эффектами. Ниже приведены примеры оператора:

int max = (x > y) ? x : y;
String name = (name != null) ? name : "unknown";

Примечание: приоритет оператора ?  ниже, чем всех других операторов, кроме операторов присваивания. Таким образом, операнды этого оператора обычно не нужно заключать в круглые скобки. Однако большинству программистов легче читать условные выражения, когда первый операнд заключен в круглые скобки – особенно если учесть, что и в условном операторе if условное выражение всегда заключено в круглые скобки.

Оператор instanceof

Этот оператор более подробно рассмотрим уже при изучении классов и объектов, а пока мотаем на ус. Хотя что-то возможно кому-то и не будет понятно.

Для оператора instanceof левым операндом должно быть значение объекта или массива, а правым операндом – имя ссылочного типа. Он возвращает true, если объект или массив является экземпляром указанного типа, иначе возвращается false. Оператор instanceof всегда возвращает false, если левый операнд является null. Если выражение instanceof равно true, можно без риска присваивать левый операнд переменной типа правого операнда.

Оператор instanceof можно использовать только с типами и значениями массивов и объектов, а не с примитивными типами и значениями.

Ниже приведены примеры использования instanceof (не все из них можно скомпилировать, это просто примеры для понимания):

"string" instanceof String // Истинно: все строки являются экземплярами String
"" instanceof Object       // Истинно: строки также являются экземплярами Object
null instanceof String     // Ложно: null не является чьим>либо экземпляром

Object o = new int[] {1,2,3};
o
instanceof int[]    // Истинно: значение массива является int>массивом
o instanceof byte[] // Ложно: значение массива не является byte>массивом
o instanceof Object // Истинно: все массивы являются экземплярами Object

// Используйте instanceof, чтобы убедиться в безопасности приведения объекта
if ( object instanceof Point) {
Point p = (Point) object;
}

Специальные операторы

В Java определены 5 языковых конструкций, которые иногда считаются операторами, а иногда – просто частью основного синтаксиса языка. Эти операторы перечислены в таблице приоритетов операторов, чтобы показать их приоритет по отношению к другим операторам. Далее эти языковые конструкции будут описаны более подробно. Здесь они изложены вкратце (так как многое мы еще пока не изучили), чтобы вы могли распознать их в примерах кода:

Доступ к члену объекта (.)

Объектом (object) называется множество данных и методы, работающие с этими данными, а поля данных и методы объекта являются его членами (members). Оператор точка (.) обеспечивает доступ к этим членам. Если о – это выражение, представляющее собой объектную ссылку, а f является именем поля объекта, то о.f равно значению данного поля. Если m – имя метода, то o.m относится к этому методу и позволяет вызывать его при помощи оператора (), описанного ниже.

Доступ к элементу массива ([])

Массивом (array) называется нумерованный список значений. Можно обратиться к каждому элементу массива через его номер, или индекс. Оператор [] позволяет обращаться к отдельным элементам массива. Если b является массивом, а i – выражением с типом int, то выражение b[i]  ссылается на один из элементов b. В отличие от других операторов, работающих с целыми значениями, данный оператор ограничивает типы значения индекса до int и более узких.

Вызов метода (())

Метод (method) представляет собой именованный фрагмент Java кода, который можно запускать, или вызывать, сопровождая имя метода несколькими выражениями, заключенными в круглые скобки и разделенными запятыми (выражений может и не быть). Значения этих выражений являются аргументами метода. Метод обрабатывает аргументы и может возвращать значение, которое становится значением выражения вызова метода. Если o.m является методом, не ожидающим аргументов, то его можно вызвать при помощи o.m(). Например, если метод требует наличия трех аргументов, то его можно вызвать выражением o.m(x,y,z). Перед вызовом метода интерпретатор Java вычисляет каждый передаваемый методу аргумент. Эти выражения обязательно вычисляются слева направо, что имеет значение только в случае побочных эффектов какоголибо из аргументов.

Создание объекта (new)

В Java объекты (и массивы) создаются при помощи оператора new, за которым следует тип создаваемого объекта и заключенный в круглые скобки список аргументов, передаваемый конструктору объектов. Конструктором называется особыйметод, который инициализирует вновь созданный объект. Таким образом, синтаксис создания объекта похож на синтаксис вызова метода. Например:

new ArrayList();
new Point(1,2);

Преобразование или приведение типа (())

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

(byte) 28 // Целый литерал приводится к типу byte
(int) (x + 3.14f) // Значение суммы с плавающей точкой приводится к целому
(String) h.get(k) // Базовый объект приводится к более узкому типу

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

Ну и на последок небольшой примерчик, чтобы не было скучно:

O0001

Немного объясню что делает данная программа.

До строки 15 все просто, даже объяснять не надо.

В 15 строке проверяется если есть аргументы командной строки, то строковой переменной isArgs задается значение переменной sYes, если нет аргументов, то переменной sNo.

Далее в 20 строке, опять же, проверяется наличие аргументов в командной строке, и если они есть то выводится на печать первый аргумент переданный в командной строке при запуске программы.

 

Ну и теперь пример работы этой программы в консоли:

O0002

Стрелочками указаны два запуска данной прогарммы: без аргументов в командной строке и с ними.

Результат, как говорится, на лицо.

23 апр. 2015 г.

Операторы и выражения в Java. Часть 1 - Таблица приоритетов выполнения операторов Java

Достаточно большую часть операторов Java мы уже рассмотрели вместе с примитивными типами Java, но есть некоторые вещи которые мы еще не разбирали или касались лишь слегка. Теперь постараемся устранить эти пробелы.

У операторов Java есть приоритет выполнения, а так же понятие ассоциативности связанное с приоритетом.

В таблице ниже приведены доступные в Java операторы. Колонки P и А определяют приоритет и ассоциативность для каждой группы родственных операторов соответственно.

Таблица приоритетов выполнения операторов Java

OJ04

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

Переменные и константы в Java

Мы уже и использовали переменные в наших примерах, поскольку без них было бы очень затруднительно объяснить примитивные типы данных в Java, но не акцентировали на этом внимания, так как это не большая и не сложная тема. О константах в Java мы вообще пока не говорили. И вот настало время! Ну погнали!

Переменная — основной компонент хранения данных в Java-программе. Переменная определяется комбинацией идентификатора, типа и необязательного начального значения. Кроме того, все переменные имеют область определения, которая задает их видимость для других объектов и время существования.

В Java все переменные должны быть объявлены до их использования. Объявить переменную можно в любом месте программы. Основная форма объявления переменных выглядит следующим образом:

тип идентификатор [=значение][, идентификатор [=значение] ...] ;

  • тип — это один из элементарных типов Java либо имя класса или интерфейса.
  • идентификатор — это имя переменной

Переменной можно присвоить начальное значение (инициализировать ее), указывая знак равенства и значение. Следует помнить, что выражение инициализации должно возвращать значение того же (или совместимого) типа, который указан для переменной. Для объявления более одной переменной указанного типа можно использовать список с разделителями-запятыми.

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

int a, b, c; // объявление трех переменных типа int: a, b и c
int d = 3, e, f = 5;// объявление еще трех переменных типа int с инициализацией d и f
byte z = 22; // инициализация переменной z
double pi = 3.14159;// объявление приблизительного значения переменной pi
char x = 'x'; // присваивание значения 'x' переменной x

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

int vacationDays;
System.out.println(vacationDays); // ОШИБКА! Переменная не инициализирована

Как правильно именовать переменные мы уже обсуждали.

Динамическая инициализация переменных

Java допускает динамическую инициализацию переменных посредством любого выражения, допустимого в момент объявления переменной. Например:

double a = 3.0, b = 4.0;
// динамическая инициализация переменной c
double c = Math.sqrt(a * a + b * b);
System.out.println("Гипотенуза равна " + c);

Область действия и время существования переменных

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

Использование блоков кода

Java позволяет группировать один и более оператора в блоки кода, называемые также кодовыми блоками. Это выполняется путем помещения операторов в фигурные скобки. Сразу после создания блок кода становится логическим модулем, который можно использовать в тех же местах, что и отдельный оператор. Например, блок может служить в качестве цели для операторов if и for. Рассмотрим следующий оператор if:

if (x < y) { // начало блока
x = y;
y =
0;
} // конец блока

В этом примере, если x меньше y, программа выполнит оба оператора, расположенные внутри блока. Таким образом, оба оператора внутри блока образуют логический модуль, и выполнение одного оператора невозможно без одновременного выполнения и второго. Основная идея этого подхода состоит в том, что во всех случаях, когда требуется логически связать два или более оператора, это делается посредством создания блока.

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

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

Области действия могут быть вложенными. Например, при каждом создании блока кода мы создаем новую, вложенную область действия. В этих случаях внешняя область действия заключает в себя внутреннюю область. Это означает, что объекты, объявленные во внешней области, будут видны коду, определенному во внутренней области. Тем не менее, обратное не верно. Объекты, которые объявлены во внутренней области действия, не будут видны за ее пределами. Это может пояснить следующий пример:

V00001

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

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

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

V00002Хотя блоки могут быть вложенными, во внутреннем блоке нельзя объявлять переменные с тем же именем, что и во внешней области.

Код приведенный на примере слева, выдаст ошибку компиляции.

Такие трюки возможные в C и C++ в Java не проходят.

 

Константы в Java

В Java для обозначения констант служит ключевое слово final. Ключевое слово final означает, что присвоить данной переменной какое-нибудь значение можно лишь один раз, после чего изменить его уже нельзя. Рекомендуется использовать для именования констант прописные буквы, хоть это и не является обязательным, но такой стиль способствует удобочитаемости кода. Пример объявления константы:

final double CM_PER_INCH = 2.54;

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

Преобразование примитивных типов в Java - Практика

Теперь немного практики чтобы закрепить предыдущую тему. Сперва простая программа неявного преорбазования примитивных типов Java.

С0004Тут все предельно просто. Единственное что стоит отметить, что я задал значение переменной i типа int в двоичной системе счисления, чтобы было явно видно, что количество значимых бит больше чем 23.
Тут же приведу и вывод этой программы:
С0005

Как  можно заметить, есть небольшая потеря точности при преобразовании целочисленного значения int к значению типа float.

Так же обратите внимание что значение переменной типа char спокойно преобразовалось в другие, более емкие, типы.

Теперь перейдем к примерам явного преобразования.

С0006[4][2]Тут все тоже достаточно просто. Единственное что может надо пояснить почему число 454.874 было преобразовано в –58. Казалось бы удивительный результат и почти магический, но магии тут ни какой нет, тут одна матчасть.

Достаточно просто посмотреть на двоичное представление числа 454 и отбросить все старшие биты, оставив только младшие восемь.
В результате в восьми младших битах получаем число 11000110, которое для типа int соответствует числу 198, а для типа byte числу –58, поскольку первый бит для него знаковый, поэтому данное число является отрицательным.

Если чуть более глубоко капнуть, то тоже самое получается если разделить число 454 по модулю на диапазон целевого типа, который в данном случае byte и его диапазон покрывает 256 значений.

454 % 256 = 198

Число 198 можно представить восемью битами, про это мы уже говорили выше. И то что для int 198, то для byte –58. Вот такая арифметика.

Далее у нас идет пример когда значение float слишком большое для значения int, в данном случае значение int получает свое максимальное или минимальное значение (с учетом знака).

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


Ну и затем совсем простой пример с классом Byte, который является оберткой для типа byte.

Теперь посмотрим на вывод этой программы:

21 апр. 2015 г.

Преобразование примитивных типов в Java

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

В Java возможны преобразования между целыми значениями и значениями с плавающей точкой. Кроме того, можно преобразовывать значения целых типов и типов с плавающей точкой в значения типа char и наоборот, поскольку каждый символ соответствует цифре в кодировке Unicode. Фактически тип boolean является единственным примитивным типом в Java, который нельзя преобразовать в другой примитивный тип. Кроме того, любой другой примитивный тип нельзя преобразовать в boolean.

Преобразование типов в Java бывает двух видов: неявное и явное.

Неявное преобразование типов выполняется в случае если выполняются условия:

  1. Оба типа совместимы
  2. Длина целевого типа больше или равна длине исходного типа

JavaBasics_ImplicitTypeCastingPrimitives

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

Так же существуют два типа преобразований:

  1. Расширяющее преобразование (widening conversion)
  2. Сужающее преобразование (narrowing conversion)

Расширяющее преобразование (widening conversion) происходит, если значение одного типа преобразовывается в более широкий тип, с большим диапазоном допустимых значений. Java выполняет расширяющие преобразования автоматически, например, если вы присвоили литерал типа int переменной типа double или значение пепременной типа char переменной типа int. Неявное преобразование всегда имеет расширяющий тип.

GrabliНо у тут могут быть свои небольшие грабельки. Например если преобразуется значение int в значение типа float. И у значения int в двоичном представлении больше чем 23 значащих бита, то возможна потеря точности, так как у типа float под целую часть отведено 23 бита. Все младшие биты значения int, которые не поместятся в 23 бита мантиссы float, будут отброшены, поэтому хотя порядок числа сохраниться, но точность будет утеряна. То же самое справедливо для преобразования типа long в тип double.

Расширяющее преобразование типов Java можно изобразить еще так:

JavaBasics_ImplicitTypeCastingPrimitivesDetailed

Сплошные линии обозначают преобразования, выполняемые без потери данных. Штриховые линии говорят о том, что при преобразовании может произойти потеря точности.

Стоит немного пояснить почему, к примеру тип byte не преобразуется автоматически (не явно) в тип char, хотя тип byte имеет ширину 8 бит, а char 16, тоже самое касается и преобразования типа short в char. Это происходит потому, что byte и short знаковые типы данных, а char без знаковый. Поэтому в данном случае требуется использовать явное приведение типов, поскольку компилятору надо явно указать что вы знаете чего хотите и как будет обрабатываться знаковый бит типов byte и short при преобразовании к типу char.

Поведение величины типа char в большинстве случаев совпадает с поведением величины целого типа, следовательно, значение типа char можно использовать везде, где требуются значения int или long. Однако напомним, что тип char не имеет знака, поэтому он ведет себя отлично от типа short, несмотря на то что диапазон обоих типов равен 16 бит.

short s = ( short) 0xffff; // Данные биты представляют число –1
char c = '\uffff'; // Те же биты представляют символ юникода
int i1 = s; // Преобразование типа short в int дает –1
int i2 = c; // Преобразование char в int дает 65535

Сужающее преобразование (narrowing conversion) происходит, если значение преобразуется в значение типа, диапазон которого не шире изначального. Сужающие преобразования не всегда безопасны: например, преобразование целого значения 13 в byte имеет смысл, а преобразование 13000 в byte неразумно, поскольку byte может хранить только числа от −128 до 127. Поскольку во время сужающего преобразования могут быть потеряны данные, Java компилятор возражает против любого такого преобразования, даже если преобразуемое значение укладывается в более узкий диапазон указанного типа:

int i = 13;
byte b = i; // Компилятор не разрешит это выражение

Единственное исключение из правила – присвоение целого литерала (значения типа int) переменной byte или short, если литерал соответствует диапазону переменной.

Сужающее преобразование это всегда явное преобразование типов.

Явное преобразование примитивных типов

Оператором явного преобразования типов или точнее говоря приведения типов являются круглые скобки, внутри которых указан тип, к которому происходит преобразование – (type). Например:

int i = 13;
byte b = (byte) i; // Принудительное преобразование int в byte
i = (int) 13.456; // Принудительное преобразование литерала типа double в int 13

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

При приведении более емкого целого типа к менее емкому старшие биты просто отбрасываются. По существу это равнозначно операции деления по модулю приводимого значения на диапазон целевого типа (например для типа byte это 256).

Слишком большое дробное число при приведении к целому превращается в MAX_VALUE или MIN_VALUE.

Слишком большой double при приведении к float превращается в Float.POSITIVE_INFINITY или Float.NEGATIVE_INFINITY.

Таблица представленная ниже представляет собой сетку, где для каждого примитивного типа указаны типы, в которые их можно преобразовать, и способ преобразования. Буква N в таблице означает невозможность преобразования. Буква Y означает расширяющее преобразование, которое выполняется автоматически. Буква С означает сужающее преобразование, требующее явного приведения. Наконец, Y* означает автоматическое расширяющее преобразование, в процессе которого значение может потерять некоторые из наименее значимых разрядов. Это может произойти при преобразовании int или long во float или double. Типы с плавающей точкой имеют больший диапазон, чем целые типы, поэтому int или long можно представить посредством float или double. Однако типы с плавающей точкой являются приближенными числами и не всегда могут содержать так много значащих разрядов в мантиссе, как целые типы.

JavaBasics_ImplicitTypeCastingPrimitivesDetailedTable

Автоматическое расширение типов в выражениях

Так же стоит еще раз упомянуть об автоматическом повышении (расширении) типов в выражениях. Мы с этим уже сталкивались когда рассматривали целочисленные типы данных и операции над ними, но все же стоит и тут напомнить, чтобы усвоилось еще лучше и к тому же это имеет непосредственное отношение к данной теме. В примере ниже знак @ означает любой допустимый оператор, например +, , *, / и т.п.

С0001

То есть, все целочисленные литералы в выражениях, а так же типы byte, short и char расширяются до int. Если, как описано выше, в выражении не присутствуют другие, более большие типы данных (long, float или double). Поэтому приведенный выше пример вызовет ошибку компиляции, так как переменная c имеет тип byte, а выражение b+1, в результате автоматического повышения имеет тип int.

Неявное приведение типов в выражениях совмещенного присваивания

Хоть данный раздел и относится к неявному преобразованию (приведению) типов, его объяснение мы привели тут, поскольку в данном случае так же работает и автоматическое расширение типов в выражениях, а затем уже неявное приведение типов. Вот такой кордебалет. Пример ниже я думаю все разъяснит. Так же как и в предыдущем объяснении знак @ означает любой допустимый оператор, например +, , *, / и т.п.

С0002

Это стоит пояснить на простом примере:

byte b2 = 50;
b2 = b2 * 2; // не скомпилируется
b2 *= 2; //скомпилируется, хотя и равнозначна b2 = b2 * 2

Вторя строка, приведенная в примере не скомпилируется из-за автоматического расширения типов в выражениях, так как выражение b2*2 имеет тип int, так как происходит автоматическое расширение типа (целочисленные литералы в выражении всегда int). Третья же строка спокойно скомпилируется, так как в ней сработает неявное приведение типов в совмещенном выражении присваивания.

Boxing/unboxing – преобразование примитивных типов в объекты обертки

Boxing и unboxin – это тоже достаточно большая тема, но она достаточно простая.

По существу boxing и unboxing это преобразование примитивных типов в объекты обертки и обратно.

Для объектов оберток примитивных типов применимо все что было сказано выше.

Об классах обертках упоминалось в таблицах, при разборе каждого из примитивных типов. Но тогда это было лишь упоминание в таблице.

Так вот, для каждого примитивного типа есть его старший брат, и он совсем не примитивный, а является настоящим классом, с полями и методами. И для каждой такой парочки возможно автоматическое преобразование.

С0003

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

Приведу простой пример:

int i3;
byte b2=3;
Byte myB;
myB=b2;
myB++;
b2=myB;
i3=myB;

Если пока не понятно зачем это нужно, то это не страшно, просто завяжите узелок на память.

19 апр. 2015 г.

Примитивные вещественные типы - знакомство с граблями

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

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

Чтобы сразу стало понятно о чём пойдет речь, разберем простой пример. Как вы уже должны знать, вещественные литералы в Java, по умолчанию имеют тип double. Теперь допустим у нас есть вот такая строка:

double d = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;

То есть мы 10 раз складываем число 0.1. Можно предположить что в результате получится 1, но как бы не так. Мы получим число очень близкое к единице но не единицу, например это может быть число:

0.9999999999999999

Если же мы этот же эксперимент проделаем для типа float:

float f = 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f;

то там будет другой результат:

1.0000001

Это объясняется разностью в точности представления вещественных чисел в типах double и float в двоичном формате (вспоминаем, что компьютер работает только с нулями и единицами :) ).

Более подробно и популярно почему так происходит можно почитать тут и тут. Чистая теори о представлении вещественных чисел тут.

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

Еще одними грабельками при вычислениях с вещественными числами может быть ситуация когда возможно подобрать такое положительное число Х при котором будет верно сравнение a+x == x, то есть прибавление какого-то числа к переменной не меняет ее значения.

Чтобы стало все понятнее приведу пример:

Вывод программы:

FP0002

FP0003Приведу еще один интересный пример слева и его вывод ниже:

FP0004Как видим, в первом случае dd и ff не равны друг другу. Такое происходит при сужающем преобразовании.

Затем мы сделали наоборот и теперь dd равно ff. Весело, не правда ли? :)

 

Именно из-за такого поведения double и float не используют в финансовых расчётах.

И еще парочка статей на эту тему: раз и два.

Ну и на последок, очень хорошая статья на эту тему. А BigInteger и BigDecimal, рассматриваемые в ней, мы изучим чуть позже.

17 апр. 2015 г.

Примитивные вещественные типы Java - float, double

  Тип   Содержит   По умолчанию   Размер   Диапазон   Обертки
   float    вещественное знаковое   0.0    32 bits    от 1.4E−45 до 3.4028235E+38    Float
   double    вещественное знаковое   0.0    64 bits    от 4.9E−324 до 1.7976931348623157E+308    Double

F0000101

Вещественные числа в Java представлены типами данных float и double. Как показано в таблицах выше, float является 32 битным значением с плавающей точкой, с обычной точностью, а double представляет 64 битное значение с плавающей точкой, с двойной точностью. Количество бит отведенные под представление этих чисел смотрите в таблице выше. Оба типа соответствуют стандарту IEEE 754-1985, который определяет формат чисел и арифметические операции, применимые к этим числам. Но есть и небольшие отличия от этого стандарта. К обычным вещественным числам добавляются еще четыре значения:

  • положительная бесконечность, выражаемая константой POSITIVE_INFINITY и возникающая при переполнении положительного значения, например в результате операции умножения 3.0*6e307 или при делении на нуль;
  • отрицательная бесконечность NEGATIVE_INFINITY, возникающая при переполнении отрицательного значения, например в результате операции умножения -3.0*6e307 или при делении на нуль отрицательного числа;
  • "не число", записываемое константой NaN (Not a Number) и возникающее, например, при умножении нуля на бесконечность.
  • кроме того, стандарт различает положительный и отрицательный нуль, возникающий при делении на бесконечность соответствующего знака, хотя сравнение 0.0 == -0.0 дает в результате истину, true.

Операции с бесконечностями выполняются по обычным математическим правилам. Во всем остальном вещественные типы — это обычные вещественные значения, к которым применимы все арифметические операции и операции сравнения.

Вещественные литералы

F00002

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

123.45
0.0
.01
5.

Литералы с плавающей точкой можно также представить в экспоненциальной, или научной, нотации, в которой за числом следует буква e или E (показатель степени) и другое число. Второе число представляет степень десятки, на которую умножается первое число. Если же число записано в шестнадцатеричном формате, то экспонента это степень двойки. Например:

1.2345E02 // 1.2345 × 102, или 123.45
1e-6  // 1 × 10-6, или 0.000001
6.02e23  // Число Авогадро: 6.02 × 1023

Так же с Java 6, возможно записывать в шестнадцатеричном формате:

0xFp2 // 15x22=60

Литералы с плавающей точкой по умолчанию являются значениями типа double. При включении значения типа float в программу за числом следует поставить символ f или F:

double d = 6.02E23;
float f = 6.02e23f;

В принципе литералы типа double можно тоже обозначать суффиксом d или D, но это особо не имеет смысла, так как вещественные литералы всегда по умолчанию double.

Большинство вещественных чисел, по самой их природе, нельзя точно представить каким-либо конечным количеством битов. Таким образом, необходимо помнить, что значения float и double являются только приближенными значениями представляемых ими чисел. float – это 32 битное приближение, которое дает как минимум 6 значимых десятичных разрядов, а double – это 64 битное приближение, которое представляет по крайней мере 15 значимых десятичных разрядов. На практике эти числа подходят для большинства вычислений с вещественными числами.

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

Бесконечные значения с плавающей точкой ведут себя вполне логично. Например, прибавление к бесконечности или вычитание из нее любого конечного значения дает бесконечность. Поведение отрицательного нуля почти не отличается от положитель ного нуля; фактически оператор равенства == сообщает о равенстве отрицательного и положительного нуля. Единственный способ отличить отрицательный нуль от по ложительного или обычного нуля – разделить на него какоелибо число. 1.0/0.0 дает положительную бесконечность, а деление 1.0 на отрицательный нуль дает отрицательную бесконечность. И наконец, поскольку NaN не является числом, оператор == сообщает, что это значение не равно ни одному другому числу, включая само значение! Чтобы проверить, являются ли значения float и double нечисловыми (NaN), следует вызвать методы Float.isNaN()  и Double.isNaN().

Арифметические операции

F00003

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

если в операции один операнд имеет тип double, то и другой приводится к типу double;

иначе, если один операнд имеет тип float, то и другой приводится к типу float;

в противном случае действует правило приведения целых значений.

Побитовые операции с вещественными типами не поддерживаются.

Операция деление по модулю (или остаток) определяется так же как и для целочисленных типов:
a % b = a — (a / b) * b 

Так же для операции деления по модулю справедливо следующее выражение:

a = ((long)(a/b))*b+(a%b)

F00004

F00005

Вычисления чисел с плавающей точкой на одном и том же, а тем более на разных процессорах могут давать несколько разные результаты, поскольку виртуальная машина java выполняет эти операции на сопроцессоре (FPU), если он присутствует на устройстве. А в сопроцессоре обычно регистры для работы с  плавающей точкой 80 бит, что шире даже чем double. Поэтому, если в эти регистры положить числа double, а потом опять вернуть их в double, то результаты могут быть разные для одинаковых чисел, по умолчанию java эту проблему не решает, то есть нет гарантии, что написанный код работающий с целочисленными типами будет давать одинаковый результат на разных процессорах. Это сделано потому, что желательно использовать сопроцессор для скорости вычислений. Результаты в этом случае могут немного различаться.

Чтобы результаты были на всех процессорах одинаковые, то следует использовать ключевое слово strictfp в декларации метода, например:

public static strictfp void main(String[] args)

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

Все математические функции из библиотеки  java.lang.Math работают с числами типа double.

Ну и теперь немного практики:

F00006

И вывод этой программы:

F00007