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

воскресенье, 27 апреля 2008 г.

2.3 Арифметические операторы: умножение, деление, деление по модулю

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

По приоритетности за унарными операторами следуют арифметические операторы. Эта группа включает в себя четыре наиболее распространённых оператора: сложение, вычитание, умножение, деление. И не только их. Существует также оператор деления по модулю, который обозначается знаком %. Арифметические операторы разделены на две группы. В первой, более приоритетной, группе находятся *, /, %. Во второй, соответственно, + и -.

Умножение и деление (* и /)

Операторы * и / выполняют умножение и деление над всеми примитивными числовыми типами и char. При делении на ноль возникает ArithmeticException.

Вы, наверное, недоумеваете, зачем я вам рассказываю про умножение и деление известное вам с первого класса. Однако, в программировании мы имеем дело с некоторыми ограничениями, связанными с представлением чисел в компьютере. Эти ограничения накладываются на все числовые форматы, от byte до double. Но наиболее заметны они для целочисленного типа int.

Если вы умножаете или делите два числа, результат вычисляется посредством целочисленной арифметики и сохраняется либо в int, либо в long. Если числа очень большие, то результат будет больше максимального числа, которое можно представить в этих числах. А значит, результат не сможет правильно закодироваться компьютером и не будет иметь смысла. Например, тип byte используется для представления чисел в диапазоне от -128 до 127. Если мы умножим 64 и 4, то результат 256, имеющий в двоичной записи 100000000 девять символов, будет закодирован, как 0, потому что byte использует лишь 8 символов.

Рассмотрим деление. Если вы делите в целочисленной арифметике, результат должен быть обязательно целочисленным. И значит, дробная часть будет потеряна. Например, 7/4 даёт нам 1.75, но в целочисленной арифметике это будет 1.

Таким образом, если вы имеете дело со сложными выражениями, вы можете выбирать последовательность умножений и делений. Но имейте в виду, что умножение может привести к переполнению, а деление - к потере точности. Народная мудрость считает, что выполнение сначала умножений, а потом делений в большинстве случаев выдаёт правильный результат. Рассмотрим пример:

1. int a = 12345, b = 234567, c, d;
2. long e, f;
3.
4. c = a * b / b; // должно равняться а=12345
5. d = a / b * b; // тоже должно равняться а=12345
6. System.out.println(“a is “ + a +
7. “\nb is “ + b +
8. “\nc is “ + c +
9. “\nd is “ + d);
10.
11. e = (long)a * b / b;
12. f = (long)a / b * b;
13. System.out.println(
14. “\ne is “ + e +
15. “\nf is “ + f);

Результат работы данного фрагмента выдаст следующее:

a is 12345
b is 234567
c is -5965
d is 0
e is 12345
f is 0

Пусть вас не смущают числовые значения данного примера. Важно то, что при выполнении умножения первым мы получили переполнение (c is -5965), когда закодировали его в тип int. Однако мы можем получить правильный результат, если закондируем его в более длинный тип, как, например, long. В обоих случаях применение первым деления будет катастрофическим для результата, независимо от длины его типа.

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

Результат деления по модулю - остаток от деления. Например, 7/4 равно 1 с остатком 3. Поэтому 7%4 = 3. Обычно операнды имеют целочисленный тип, но иногда оператор применяется и к числам с плавающей точкой. Также следует знать некоторые особенности данного оператора, когда операнды отрицательные.

При негативных или дробных операндах правило такое: вычитайте правый операнд из левого до тех пор, пока последний не станет меньше первого. Примеры:

17%5 = ? 17-5=12>5; 12-5=7>5; 7-5=2<5. Значит 17%5 = 2

21%7? 21-7=14>7; 14-7=7=7; 7-7=0<7. Значит 21%7 = 0

7.6%2.9? 7.6-2.9=4.7>2.9; 4.7-2.9=1.8<2.9. Значит 7.6%2.9=1.8

Заметьте: знак результата (положительный или отрицательный) целиком и полностью определён знаком левого операнда, то есть делимого.

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

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

Деление по модулю, как и нормальное деление, может выбросить исключение ArithmeticException, если делитель (правый операнд) ровняется нулю.


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

2. Операторы и присваивания (Выпуск 4)

2. Операторы и присвоения" (Operators and assignments)

Операторы в Java используются для разнообразных операций (всех необходимых). Все Javaоператоры перечислены в Таблице 2.1:


Таблица 2.1: Операторы Java в нисходящем порядке приоритетности массива

КатегорияОператоры
Унарные++ -- + - ! ~ ()
Арифметические* / %
+ -
Сдвиг<< >> >>>
Сравнение< <= > >= instanceof
== !=
Поразрядные& ^
Быстрого принятия решения&&
Условные?:
Присвоение= op=

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

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

1. int [] a = { 4, 4 };
2. int b = 1;
3. a[b] = b = 0;

Какой элемент массива модифицирован? Какое значение b использовано для выбора элемента массива: 0 или 1? Согласно правилу вычисления слева направо самое левое выражение a[b] должно быть вычислено первым. Так что это a[1]. Затем вычисляется b, то есть активируется ссылка на переменную b. Затем вычисляется константа 0, что не требует никаких операций. Затем в вычисление включаются операторы. Вычисление при этом проводится в порядке приоритетности и ассоциацивности. Для присвоений ассоциативность - справа налево. Таким образом, сначала значение 0 присваивается переменной b, которая, в свою очередь, затем присваивается последнему элементу массива a.

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

2.2. Унарные операторы

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

  • Инкремент (увеличение значения на 1) и декремент (уменьшение значения на 1): ++ и --
  • Унарный плюс и минус: + и -
  • Поразрядное инвертирование: ~
  • Логическое дополнение: !
  • Приведение типа: ( )

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

Инкремент (увеличение значения на 1) и декремент (уменьшение значения на 1): ++ и --

Эти операторы изменяют значение выражения добавлением или вычитанием 1. Например, если переменная x типа int равна 10, то ++x (--x) равно 11 (соответственно, 9). Результат записывается в x.

В предыдущих примерах операторы находились перед переменной. Они могут находиться и после неё. И ++x, и x++ дают один и тот же результат, сохраняемый в x. Но значение всего выражения отличается. Например, если y = x++, то значение y равно исходному значению x. Если же y = ++x, то значение y на единицу больше, чем исходное значение x. В обоих случаях значение x увеличивается на 1.

Если инкремент (декремент) расположен слева от выражения, то выражение модифицируется перед тем (после того), как оно начинает участвовать в остальном вычислении. Это называется пре-инкремент (пре-декремент). Соответсвенно, если оператор находится справа от выражения, то в остальном вычислении участвует исходное значение этого выражения. И инкремент (декремент) происходит после того, как вычисление всего выражения завершено.

Таблица 2.2. показывает значения x и y после применения инкремента и декремента справа и слева.


Таблица 2.2: Примеры премодификации и постмодификации инкрементом и декрементом

КатегорияОператоры
Начальное значение xВыражениеИтоговое значение yИтоговое значение x
5y = x++56
5y = ++x66
5y = x--54
5y = --x44

Унарный плюс и минус: + и -

Унарные операторы + и - отличаются от обычных бинарных операторов + и -, которые трактуются, как сложение и вычитание. Унарный + не имеет никакого эфекта, кроме подчёркивания положительной природы численного литерала. Унарный - отрицает выражение (было положительным - стало отрицательным, было отрицательным - стало положительным). Примеры:

1. x = -3;
2. y = +3;
3. z = -(y + 6);

В примере единственное обоснование использования унарного плюса - подчёркивание, что переменной y присваивается положительное число. Общее выравнивание кода также более красиво с эстетической точки зрения :). Заметим, что в третьей строке унарный оператор применяется не к литералу, а к выражению. Таким образом, значение переменной z присваивается -9.

Поразрядное инвертирование: ~

Каждый примитивный тип Java представляется в виртуальной машине так, что представление не зависит от платформы. Это означает, что битовый шаблон, используемый для представления некоего отдельного числа, будет всегда тем же самым. Таким образом и манипулирование битами - процесс более эффективный, в силу независимости от платформы. Поразрядное инвертирование означает, что в двоичном представлении числа 0 заменяется на 1, а 1 - на 0.

Например, применение этого оператора к байту с содержимым 00001111 даст 11110000.

Логическое дополнение: !

Оператор ! инвертирует логическое значение выражения. Например, !true = false. Этот оператор часто используется в тестовой части if () конструкции. Эффект этого - изменение значения логического выражения. То есть обработчики if () и else могут быть легко обменяться местами. Рассмотрим два эквивалентных фрагмента кода:

1. public Object myMethod(Object x) {
2. if (x instanceof String) {
3. // do nothing
4. }
5. else {
6. x = x.toString();
7. }
8. return x;
9. }

и

1. public Object myMethod(Object x) {
2. if (!(x instanceof String)) {
3. x = x.toString();
4. }
5. return x;
6. }

В первом фрагменте, тестирование происходит в строке 2, но присвоение - в строке 6, если тест не пройдёт. Это сделано немного громоздко при помощи ветви else конструкции if/else. Второй фрагмент использует оператор логического дополнения, поэтому во второй строке тест инвертирован и может быть прочтён как Если x не строка. Если тест проходит, то обработка происходит в третьей строке и никакого отдельного else не требуется и результирующий код более краток и более читаем.

Приведение типа: ( )

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

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

int circum = (int)(Math.PI * diameter);

Здесь приведение типа выражается фрагментом (int). Если бы этот фрагмент отсутсвовал, то компилятор бы выдал ошибку, поскольку double значение, возвращаемое арифметическим выражением не может быть точно представлено int значением, к которому присваивается. Присвоение типа - это способ, которым программист говорит компилятору: "Я знаю, что такое присвоение может быть рискованным, но верь мне, ведь я - специалист". Конечно, если результат при этом теряет точность так, что программа не функционирует должным образом, - это ответсвенность программиста.

Присвоение типа может быть применено и к объектным (непримитивным) типам. Это типично, например, для случаев, когда используются контейнеры типа Vector. Если вы помещаете объект типа String в Vector, то когда вы его извлекаете, тип, возвращаемый методом elementAt(), будет Object. Для использования извлечённого объекта как String нужно применить приведение типа, как, например, в следующем фрагменте кода:

1. Vector v = new Vector();
2. v.add ("Hello");
3. String s = (String)v.get(0);

В данном примере приведение типа имеет место в третьей строке (конструкция (String)). И хотя компилятор допускает такое приведение типа, во время выполнения программы всё равно проверяется, является объект извлечённый из Vector на самом деле объектом типа String. В будущих выпусках я расскажу о допустимых приведениях типов.


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

Ответы на тест по основам языка (Выпуск 4)



  1. Ответ В: разность между количеством отрицательных чисел и количеством положительных чисел равна единице (отрицательных чисел больше).
  2. Ответ A, В, C, D, E: все идентификаторы допустимы.
  3. Ответ В, D: все варианты являются допустимыми сигнатурами методов. Но для того, чтобы быть точкой входа в приложение, метод main() должен быть public static void и принимать один параметр типа String[].
  4. Ответ D: порядок Декларация пакета, импорты, классы должен быть соблюдён строго.
  5. Ответ A, E: массив из 25-ти элемнтов индексируется от 0 до 24. Все элементы инициализируются нулём.
  6. Ответ D: объект типа Holder создаётся в третьей строчке. Ссылка на этот объект передаётся в метод bump() в пятой строчке. И внутри метода поле held объекта изменяет значение с 100 на 101.
  7. Ответ C: в метод decrement() передаётся копия аргумента d; копия увеличивает значение на единицу, но сам аргумент нет.
  8. Ответ A: сборщик мусора не может быть вызван принудительно. На вызовы System.gc() и Runtime.gc() нельзя полагаться на 100%, потому что сборка мусора может быть отсрочена в пользу потока более высокой приоритетности. Поэтому ответы B и D неверны. Ответ C неверен хотя бы потому, что метод gc() не принимает никаких аргументов. Ответ E просто напросто декларирует, что объект может быть собран сборщиком мусора, когда тот начнёт свою работу.
  9. Ответ D: 16-тибитный тип short вартируется от -215 до 215-1 согласно Java спецификации вне зависимости от платформы.
  10. Ответ D: 8-мибитный тип byte вартируется от -27 до 27-1 согласно Java спецификации вне зависимости от платформы.


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

Тест по основам языка (Выпуск 3)

1.10. Тест для самопроверки

  1. Данные типов со знаком имеют равное количество положительны ненулевых и отрицательных значений.
    1. Истинно
    2. Ложно
  2. Выберите все идентификаторы, которые допускаются в Java программе.
    1. BigOlLongStringWithMeaninglessName
    2. $int
    3. bytes
    4. $1
    5. finallist
  3. Какие из нижеследующих сигнатур могут быть использованы для объявления методаmain()? (Выберите все допустимые.)
    1. public static void main()
    2. public static void main(String arg[])
    3. public void main(String [] arg)
    4. public static void main(String[] args)
    5. public static int main(String [] arg)
  4. Если исходый файл включает в себя все три высокоуровневых элемента, какова их последовательность?
    1. Импорты, декларация пакета, классы
    2. Классы, импорты, декларация пакета
    3. Декларация пакета идёт первой, а порядок импортов и классов не существенен
    4. Декларация пакета, импорты, классы
    5. Импорты идёт первыми, а порядок декларации пакета и определений классов несущественен
  5. Рассмотрим следующую строку кода: int[] x = new int[25];. После её выполения, какие утверждения истинны? (Выберите все истинные утверждения.)
    1. x[24] = 0
    2. x[24] не определено
    3. x[25] = 0
    4. x[0] = null
    5. x.length = 25
  6. Рассмотрим следующий код:
    1. class Q6 {
    2. public static void main(String args[]) {
    3. Holder h = new Holder();
    4. h.held = 100;
    5. h.bump(h);
    6. System.out.println(h.held);
    7. }
    8. }
    9.
    10. class Holder {
    11. public int held;
    12. public void bump(Holder theHolder) {
    13. theHolder.held++;
    14. }
    15. }

    Что печатается в строке 6?
    1. 0
    2. 1
    3. 100
    4. 101
  7. Рассмотрим следующий код:
    1. class Q7 {
    2. public static void main(String args[]) {
    3. double d = 12.3;
    4. Decrementer dec = new Decrementer();
    5. dec.decrement(d);
    6. System.out.println(d);
    7. }
    8. }
    9.
    10. class Decrementer {
    11. public void decrement(double decMe) {
    12. decMe = decMe - 1.0;
    13. }
    14. }

    Что печатается в строке 6?
    1. 0.0
    2. -1.0
    3. 12.3
    4. 11.3
  8. Как можно заставит сборщик мусора освободить память, выделенную под некий объект?
    1. Никак
    2. Вызвав System.gc()
    3. Вызвав System.gc(), и передав параметром ссылку на объект, память под который хотим освободить
    4. Вызвав Runtime.gc()
    5. Установив ссылку на объект в null
  9. Каков диапазон значений, которые могут быть присвоены перемнной типа short?
    1. Зависит от аппаратного обеспечения компьютера, на котором запускаетя программа
    2. От 0 до 216-1
    3. От 0 до 232-1
    4. От -215 до 215-1
    5. От -231 до 231-1
  10. Каков диапазон значений, которые могут быть присвоены перемнной типа byte?
    1. Зависит от аппаратного обеспечения компьютера, на котором запускаетя программа
    2. От 0 до 28-1
    3. От 0 до 216-1
    4. От -27 до 27-1
    5. От -215 до 215-1


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

Резюме по основам языка (Выпуск 3)

1.9. Итоги первой главы

  • Элементы исходного файла должны идти в следующем порядке:
    1. Декларация пакетов
    2. Утверждения импортов
    3. Определения классов
  • В исходном файле должно быть хотя бы одно общедоступное (public) определение класса, имя которого должно совпадать с именем файла.
  • Идентификатор должен начинаться с буквы, символа доллара или подчёркивания; остальными символами могут быть буквы, цифры, символы доллара или подчёркивания.
  • В Java имеются четыре примитивных целочисленных типа со знаком: byte, short, int, long.
  • Два примитивных типа с плавающей точкой: float, double.
  • Тип char беззнаковый и используется для представления символов Unicode.
  • Тип boolean принимает только два значения: true или false.
  • Массивы должны быть (в порядке перечисления):
    1. Объявлены
    2. Созданы
    3. Инициализированы
  • Инициализация по умолчанию может быть применена и к переменным членам класса, и к элементам массивов, но не к автоматическим переменным (создаются в методе или передаются в него параметром). Значения по умолчанию: 0 - для численных типов, null - для ссылок на объекты и для типа char, false - для boolean.
  • Член length массивов возвращает количество элементов в массиве.
  • Класс с методом main() может быть вызван из командной строки, как Java приложение. Сигнатура для этого метода: public static void main(String[] args), где аргумент args содержит все параметры входной строки, которые идут следом за именем класса.
  • Параметры метода - копии, а не оригиналы. Для параметров примитивных типов это означает, что модификации переменных внутри вызываемого метода не видны методу вызывающему. Для параметров объектого типа (в том числе и для массивов) это тоже имеет место, но модификация объекта или массива, на который ссылается параметр, видна в вызывающем методе.
  • Сборщик мусора может освободить память, если ясно, что она больше не используется.
  • Достоверная сборка мусора невозможна.
  • Невозможно предсказать, когда неиспользуемая память будет освобождена.
  • Сборка мусора не гарантирует, что не случится утечка памяти, которая может иметь место, если ссылки на неиспользуемые объекты не установлены в null или не уничтожены (например, с выходом из вызываемого метода).


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

Передача параметров, сборщик мусора (Выпуск 3)

1. Основы языка (окончание)

1.7. Передача параметров

Параметр = Аргумент (argument)

При передаче параметров при вызове функции в Java, на самом деле передаётся копия параметра. Рассмотрим фрагмент кода:

1. double radians = 1.2345;
2. System.out.println("Синус от " + radians + " = " + Math.sin(radians));

Переменная radians состоит из битового шаблона, который представляет собой число 1.2345. Во второй строке копия этого битового шаблона передаётся в аппарат вызывания методов Виртуальной Java Машины (Java Virtual Machine, JVM).

Когда параметр передаётся в метод, изменения значения параметра методом не отражаются на исходном значении параметра вне метода. Для наглядности рассмотрим пример:

1. public void bumper(int bumpMe) {
2. bumpMe += 15;
3. }

Метод bumper вызывается в следующем коде:

1. int xx = 12345;
2. bumper(xx);
3. System.out.println("Сейчас xx равен " + xx);


Во второй строке xx копируется и передаётся в bumper(), который изменяет значение 12345 на 12360 (+15). Но в третьей строке выводится всё равно 12345, потому что вне метода bumper() значение xx не изменяется.

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

1. Button btn;
2. btn = new Button("Ok");

Во второй строке конструктор Button возвращает ссылку на только что созданнную кнопку посредством переменной btn. Это не сам объект и не его копия, но ссылка на объект. Например, в некоторых имплементациях JVM ссылка - это адрес объекта.

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

Рассмотрим следующий фрагмент кода:

1. Button btn;
2. btn = new Button("Pink");
3. replacer(btn);
4. System.out.println(btn.getLabel());
5.
6. public void replacer(Button replaceMe) {
7. replaceMe = new Button("Blue");
8. }

Во второй строке создаётся кнопка, ссылка на которую сохраняется в переменной btn. В третьей строке копия ссылки на кнопку Pink передаётся в метод replacer(), который (в строке 7) создаёт вторую кнопку Blue, ссылку на которую сохраняет в переданном параметре replaceMe. Но вне метода replacer() ссылка на кнопку Pink остаётся той же самой, поэтому строка 4 напечает Pink.

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

7. replaceMe.setLabel("Blue");

то, хоть переданная ссылка btn и та же самая, что и в предыдущей версии примера, но сам объект изменён. Ссылка на объект не изменяется, но используется для доступа к самому объекту. В результате четвёртая строка выдаст Blue в выходном потоке.

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

Как создать ссылку на переменную примитивного вида

Это полезная техника для для создания эффекта передачи переменных примитивного вида по ссылке (когда вызываемый метод может изменять значение переменной). В вызываемый метод достаточно просто передать массив, состоящий из одного элемента, которым является заданная переменная. В коде это будет выглядеть, например, так:

1. public class PrimitiveReference {
2. public static void main(String args[]) {
3. int [] myValue = { 1 };
4. modifyIt(myValue);
5. System.out.println("myValue contains " + myValue[0]);
6. }

7. public static void modifyIt(int [] value) {
8. value[0]++;
9. }
10. }

1.8. Сборщик мусора (Garbage Collection)

Большинство современных языков программирования позволяют выделять память во время выполнения программы. В Java это делается явно тогда, когда используется оператор new для создания объекта, и неявно - когда вызывается метод, который имеет локальные переменные или параметры. В случае неявного выделения памяти используется стековое пространство, которое освобождается, как только совершается выход из метода. В случае явного выделения памяти (под объекты) используется пространство кучи (heap). Освобождение этой памяти не так тривиально, но об этом поговорим ниже.

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

public void aMethod() {
MyClass mc = new MyClass();
}

локальная переменная mc - это ссылка, память для которой выделяется в стеке. Но память для объекта типа MyClass, на который ссылается mc, выделяется в куче.

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

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

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

Однако мы до сих пор не ответили явно на вопрос, поставленный вначале. Самый лучший ответ: память не освобождается до тех пор, пока она используется. Даже если объект больше не используется, нельзя уверенно сказать, когда память, выделенная под него, будет освобождена. Это может случиться через 1 милисекунду, или через 100 милисекунд, или не случиться вовсе. Методы System.gc() и Runtime.gc() выглядят, как "запустить сборщик мусора", но и на них нельзя полагаться, поскольку другие более приоритетные потоки могут отложить их выполнение. И на самом деле, документация для методов gc() гласит: "Вызов этих методов подразумевает, что JVM приложит усилия для переработки неиспользуемых объектов".

Сама природа автоматического сбора мусора имеет важное последствие: утечка памяти (ведущая к её нехватке) всё ещё может иметь место. Достижимые ссылки на неиспользуемые объекты позволяют последним занимать память и не быть обработанными сборщиком мусора. Решением этой проблемы может быть явное присвоение null значения ссылке на объект, который вам уже не нужен. Это особенно просто в случае имплементации коллекции. Например, пусть массив storage используется для реализации стека. Типичная имплементация метода pop() такова:

1. public Object pop() {
2. return storage[index--];
3. }

Если метод, вызывающий pop(), не позаботится о вытолкнутом объекте после его использования, память для этого объекта не будет освобождена до тех пор, пока не произойдёт выход из вызывающего метода или пока ссылка на этот объект (находящаяся в массиве storage) не будет перезаписана. Это может занять много времени. Для ускорения процесса, метод pop() может быть модифицирован следующим образом:

1. public Object pop() {
2. Object returnValue = storage[index];
3. storage[index--] = null;
4. return returnValue;
5. }


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

Литералы, массивы, основы классов (Выпуск 2)

1. Основы языка (продолжение)

1.4. Литералы

Литерал - это костанта (не литерал - это значение определённое во время выполнения программы, например, сумма двух дитералов). Литералы могут являться значениями переменных как примитивных, так и строковых типов. Они появляются в присвоении значения (assignment) справа или в вызовах методов. Вы не можете присвоить какое-то значение литералу, то есть литералы не могут находиться слева в утверждении присвоения.

1.4.1. Литералы типа boolean

Единственные правомерные литералы типа boolean - true и false. Например:

1. boolean isBig = true;
2. boolean isLittle = false;

Здесь boolean isBig = true; - это утверждение присвоения, boolean isBig - это объявление переменной isBig типа boolean, true - это литерал. Заметьте, что литерал находится справа.

1.4.2. Литералы типа char

Литералы типа char могут быть выражены как символ в одинарных кавычках. Например:

char c = 'w';

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

char c1 = '\u4567';

Специальные символы в Java выражаются так:

  • '\n' - для новой строки;
  • '\r' - для возврата каретки на начало строки;
  • '\t' - для табуляции;
  • '\b' - для возврата каретки на одну позицию назад;
  • '\f' - для новой страницы;
  • '\'' - для одинарной кавычки;
  • '\"' - для двойной кавычки;
  • '\\' - для обратной наклонной линии.

1.4.3. Целочисленные литералы

Целочисленные литералы могыт быть выражены в десятичной, восьмеричной (префикс литерала - 0) и шестнадцатиречной (префикс литерала - 0х или 0Х) форме. В последнем случае и верхний и нижний регистр может использоваться. Например, 28 может быть выражено шесьтью способами:

  • 28
  • 034
  • 0х1с
  • 0х1С
  • 0Х1с
  • 0Х1С

По умолчанию целочисленный литерал занимает 32 бита. Чтобы указать 64-хбитовый (long) литерал, нужно добавить суффикс L к литеральному выражению. (Можно указывать l и в нижнем регистре, но тогда этот суффикс выглядит как 1 (единица), что может привести к конфузам при чтении кода).

1.4.4. Литералы с плавающей точкой

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

  • десятичная точка: 1.414
  • Буква E или e, указывающая на научную нотацию: 4.23E+21
  • Суффикс F или f, указывающий на литерал типа float: 1.828f
  • Суффикс D или d, указывающий на литерал типа double: 1234d

Литерал с плавающей точкой без суффикса F или D по умолчанию является литералом типа double.

Если вы присваиваете переменной литеральное значение, например, short s = 9;, компилятор определяет размер литерала согласно типу переменной. Поэтому, только что показанное присвоение сработает без ошибок. Однако, выражение с переменными, как, например, short s1 = 9 + s;, выдаст ошибку компиляции, потому что тип выражения 9 + s есть int, а не short.

1.4.5. Строковые литералы

Строковый литерал - это последовательность символов, заключённых в двойные кавычки. Например: String s = "Characters in strings are 16-bit Unicode.";.

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

1.5. Массивы (Arrays)

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

Массив создаётся последовательностью трёх шагов: объявление, создание и инициализация.

В объявлении содержится имя массива, и тип его допустимых элементов. Например:

1. int[] ints; // массив, состоящий из элементов примитивного типа int
2. double[] dubs; // массив, состоящий из элементов примитивного типа double
3. Dimension[] dims; // массив из указателей на объекты (Dimension - класс в пакете java.awt)
4. float[][] twoDee; // двухмерный массив (т.е. массив массивов) из элементов типа float

Квадратные скобки могут идти и до, и после названия массива. Это также имеет место в декларации методов. Например, метод, который принимает массив элементов типа double как параметр, может быть объявлен так: takeMethod(double[] dubs) или takeMethod(double dubs[]). Метод, который возвращает массив элементов типа double, может быть объявлен как double[] returnMethod() или double returnMethod()[]. Первый способ, лично для меня, более удобен для чтения.

Заметьте, что при объявлении массива не указывается его размер, т.е. число элементов, в него входящих. Размер определяется в момент выполнения программы, когда память под массив выделяется через ключевое слово new. Например:

1. int[] ints; // Объявление массива
2. ints = new int[25]; // Создание массива во время выполнения программы


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

1. int size = 1152 * 900;
2. int[] raster;
3. raster = new int[size];

Объявление и создание массива может быть задано одной строкой:

int[] ints = new int[25];

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


Таблица 1.4: Значения по умолчанию для инициализации элементов массива

Тип элементаЗначение по умолчаниюТип элементаЗначение по умолчанию
byte0short0
int0long0L
float0.0fdouble0.0d
char'\u0000'booleanfalse
указатель на объектnull

Методы (и атрибуты) для Java объекта вызываются через точку: имяОбъекта.имяМетода (соответсвенно имяОбъекта.имяАтрибута).

На самом деле, массивы - это объекты, вплоть до того, что вы можете вызывать методы (через точку!) для них (в большинстве своём - это методы класса Object), хотя вы и не можете создавать подклассы для массивов.

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

float[] diameters = {1.1f, 2.2f, 3.3f, 4.4f, 5.5f};

Размер массива вычисляется автоматически и равен числу элементов в фигурных скобках.

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

1. long[] squares;
2. squares = new long[6000]; // массив создан, его значения по умолчанию равны 0L
3. for (int i = 0; i < 6000; i++) {
4. squares[i] = i * i; // значения по умолчанию изменены
5. }

Если в приведенном примере размер массива поменяется (строка 2), то цикл в строке 3 может функционировать с ошибками. Для целостности программы мы перепишем строку 3 следующим образом:

3. for (int i = 0; i < squares.length; i++) { // length - встроенный атрибут массива, обозначающий длину массива


Нумерация элементов в массиве начинается с нуля (0).

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

1.6. Основы классов

Java построена на классах. Более детальное изучение классов мы разберём в других выпусках рассылки. Сейчас же обратимся к базовым понятиям.

1.6.1. Метод main()

Метод main() - это входная точка в приложениях, написанных на Java. То есть для создания приложения, вы пишете класс, в котором присутствует метод main(). Для выполнения приложения наберите в командной строке java и имя класса с методом main().

Форма задания (сигнатура) метода main() такова:

public static void main(String[] args)

Заметьте, метод main() объявлен, как общедоступный (public). Кроме того, требуется, чтобы он был также статическим (static), что означает, что он может быть вызван без создания соответсвующего объекта.

Параметр args - аргументы, которые пользователь ввёл в командную строку после имени класса. Например:

% java Mapper France Belgium

означает, что классу Mapper в командной строке было передано два аргумента France, помещённый в args[0] и Belgium - в args[1]. Имя массива args может быть заменено любым допустимым именем переменной. Главное, что его тип - массив строк.

1.6.2. Переменные и инициализация

Java поддерживает переменные двух типов, зависящих от области видимости переменной:

  1. Член класса: создаётся (уничтожается) при создании (уничтожении) объекта класса. Может иметь явное объявление доступности (общедоступный, защищённый или приватный). Вызывается через обращение к объекту класса. Видимость до закрывающей скобки класса.
  2. Автоматическая переменная метода: создаётся в методе или передаётся в него параметром. Существует во время выполнения метода и, следовательно, видна внутри метода и доступна во время выполнения метода. (Исключения бывают во внутренних классах, но о них мы поговорим в других выпусках рассылки.)

Все переменные первого типа, которым не были присвоены явные значения, автоматически инициализируются значением по умолчанию, которое зависит от типа переменной (см. Таблицу 1.4).


Член класса может быть инициализирован в строке объявления:

1. class HasVariables {
2. int x = 20; // x устанавливается в значение 20 перед вызовом конструктора класса HasVariables
3. static int y = 30; // y принимает значение 30 в момент загрузки класса HasVariables

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

Автоматические переменные (также известные как локальные перемнные (local variables)) не инициализируются системой, но должны быть явно инициализированы перед использованием. Например, этот код не откомпилируется:

1. public int wrong() {
2. int i;
3. return i+5;
4. }

Ошибка компиляции в строке 3 "Variable i may not have been initialized." (Переменная i, возможно, не инициализирована) возникает часто потому, что некоторые программисты начинают использовать переменные раньше, чем инициализируют их.

1.public double fourthRoot(double d) {
2. double result;
3. if (d >= 0) { // использование раньше, чем инициализация
4. result = Math.sqrt(Math.sqrt(d)); // инициализация
5. }
6. return result;
7. }

В шестой строке компилятор выдаст ошибку "Variable result may not have been initialized". Для того, чтобы этого не произошло, перепишем строку 2 следуюшим образом:

2. double result = 0.0; // Инициализация

Теперь переменная result инициализирована. Только что показанная модификация строки 2 демонстрирует, что автоматические перемнные могут быть инициализированы в строке объявления. Хотя инициализация в отдельной строке тоже возможна.


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

Основы языка Java (Выпуск 1)

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

1. Основы языка

1.1. Исходные файлы

Все исходные файлы (они же "исходники"), написанные в Java, должны заканчиваться расширением .java. Файл должен содержать по крайней мере одно открытое и общедоступное (public) определение класса.

Лирическое отступление для совсем новичков

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

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

Если общедоступное определение класса присутствует, оно должно совпадать с именем файла без расширения. Например, если исходник содержит класс Apple, то файл должен называться Apple.java. Исходник может содержать неограниченное число не общедоступных определений классов.

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

Три элемента верхнего уровня известные как единицы компиляции (compilation units) могут также присутствовать в файле:

  1. Объявление пакета (package declaration); например,package exam.problems;
  2. Утверждение импорта (imposrt statement); например, import java.awt.Button; // импортирует специфический класс import java.util.*; // импортирует целый пакет;
  3. Определение класса (class definition); например, public class Test {...}.

Формат объявления пакета очень прост. Сначала идёт ключевое слово package, за которым следует название пакета. А название пакета, в свою очередь, состоит из последовательности определённых элементов, разделённых точкой. Эта последовательность элементов отображает иерархию директорий на диске. В директорию с именем последнего элемента последовательности и находится класс. Рассмотрим пример выше: класс с таким объявлением пакета помещается в директорию problems, которая размещена в директории exam.

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

Лирическое отступление для совсем новичков

Пакеты представляют собой совокупность классов логически связанных между собой. Например, пакет "деньги" может содержать классы "ДолларСША", "Евро", "РоссийскийРубль", "КурсВалют". Пакет(директория) "деньги" может находиться в директории "активы", и тогда объявление пакета имеет вид: package assets.money;, где assets - "активы", а money - "деньги", ясное дело.

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

Лирическое отступление для совсем новичков

Импортирование пакетов (классов) необходимо объявлять в том случае, если классы этих пакетов (эти классы) используются в текущем классе. Например, в класс "ОплатавМагазине" мы импортируем класс "РоссийскийРубль", то есть пишем: import assets.money.RussianRuble;, где RussianRuble означает "РоссийскийРубль". А в класс "ОплатаНаЧёрномРынке" мы пишем: import assets.money.*;, потому что нам могут понадобиться все заявленные валюты и их курс.

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

Пробелы и коментарии могут предшествовать любой единице компиляции. Например, класс Test может иметь такой вид:

1. // Объявление пакета
2. package exam.prepguide;
3.
4. // Импорты
5. import java.awt.Button; // импортирует специфический класс
6. import java.util.*; // импортирует целый пакет
7.
8. // Определение класса
9. public class Test {...}

Случается, что существуют два класса с одинаковым именем, но в разных пакетах. Например, Date класс в java.util и в java.sql. Если вы используете звёздочку для импорта обоих пакетов, а затем пытаетесь использовать класс названный просто Date, вы получите ошибку компиляции о том, что такое использование неоднозначно. Вы должны или сделать явный импорт класса, или обращаться к классу, используя полный путь к нему.

1.2. Ключевые слова и дентификаторы

В языке Java существует 52 ключевых и зарезервированных слов. Они приведены в Таблице 1.1.


Таблица 1.1: Ключевые и зарезервированые слова в Java

abstractclassfalseimportpackagesupertry
assertconstfinalinstanceofprivateswitchvoid
booleancontinuefinallyintprotectedsyncronizedvolatile
breakdefaultfloatinterfacepublicthiswhile
bytedoforlongreturnthrow
casedoublegotonativeshortthrows
catchelseifnewstatictransient
charextendsimplementsnullstrictfptrue


Слова goto и const являются зарезервированными. Хоть они и не имеют никакого смысла в Java, программисты не могут использовать их как идентификатор.

Идентификатор - это слово, используемого программистом, чтобы дать имя переменной, методу, классу или метке. Ключевые и зарезервированные слова не могут быть использованы в качестве идентификаторов. Идентификатор может начинаться с буквы, символа подчёркивания (_) или символа доллара ($); последующими символами могут быть буквы, цифры, символы подчёркивания или символы доллара. Несколько примеров:

1. foobar // верно
2. BIGinterface // верно, хоть и со встроенным ключевым словом
3. // OK
4. $incomeAfterExpenses // верно
5. 3_node5 // неверно: начинается с цифры
6. !theCase // неверно: должно начинаться с буквы, $, или _

Идентификаторы чувствительны к регистру. Например, Radius и radius - различные идентификаторы.

1.3. Примитивные типы данных

Лирическое отступление для совсем новичков

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

Примитивные типы данных в Java: boolen, char, byte, short, int, long, float, double. Размеры этих типов приведены в Таблице 1.2.


Таблица 1.2: Примитивные
типы данных и их размер

ТипРазмер (в битах)Тип Размер (в битах)
boolean1char16
byte8short16
int32long64
float32double64

Переменные типа boolean принимают только два значения: true и false.

Четыре целочисленных типа со знаком: byte, short, int, long. Их интервалы значений приведены в Таблице 1.3.


Таблица 1.3: Интервалы значений целочисленных типов со знаком

ТипРазмер (в битах)Минимум Максимум
byte8-2727-1
short16-215215-1
int32-231231-1
long64-263263-1

Тип char - целочисленный, но без знака. Его диапазон варьируется от 1 до 2^16-2. Java символы (char тип) кодируются в системе Unicode, то есть 16-тибитное представление значительной части символов, используемых в различных письменных системах. Если 9 первых значащих битов установлены в 0, то кодировка остальной части символа совпадает с кодировкой 7-битной
системы ASCII.

Типы с плавающей десятичной запятой: float и double. Они соответсвуют спецификации IEEE 754. Некоторые математические оперции приводят к значению, не имеющему представления в цифрах (например, бесконечность). Для описания таких нецифровых ситуаций и float и double могут представляться специальными битовыми паттернами, которые не соответствуют числам. Эти паттерны определены в классах Float и Double и обращение к ним происходит следующим образом (здесь и далее NaN означает Not a Number, не число):

  • Float.NaN
  • Float.NEGATIVE_INFINITY
  • Float.POSITIVE_INFINITY
  • Double.NaN
  • Double.NEGATIVE_INFINITY
  • Double.POSITIVE_INFINITY

Следующий фрагмент кода показывает пример использования этих паттернов:
1. double d = -10.0 / 0.0;
2. if (d == Double.NEGATIVE_INFINITY) {
3. System.out.println("d только что ушёл в бесконечность: " + d);
4. }

Запомним!
Все численные примитивные типы (то есть кроме boolean и char) - со знаком.


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