Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

3: Управление течением программы

Подобно мыслящему существу, программа должна управлять собственным миром и делать выбор при выполнении.

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

Если вы почувствуете, что вам немного трудно двигаться вперед в этой главе, просмотрите мультимедиа CD ROM прилагающийся к этой книге: Thinking in C: Foundations for Java and C++. Он содержит аудио лекции, слайды, упражнения и решения, специально подобранные так, чтобы быстро ввести вас в синтаксис C, необходимый для изучения Java.

Использование операторов Java

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

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

Почти все операторы работают только с примитивными типами. Исключение составляют ‘=’, ‘==’ и ‘!=’, которые работают со всеми объектами (и являются смущающим местом для объектов). Вдобавок, класс String поддерживает ‘+’ и ‘+=’.

Предшествование

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

A = X + Y - 2/2 + Z;

имеет весьма разную трактовку для того же выражения с круглыми скобками:

A = X + (Y - 2)/(2 + Z);

Присваение

Присваение выполняется с помощью оператора =. Это означает “взять значение правой части (часто называемое rvalue) и скопируй его в левую сторону (часто называемую lvalue). rvalue - это любая константа, переменная или выражение, которое может произвести значение, но lvalue должно быть определенной, поименованной переменной. (То есть, здесь должно быть физическое пространство для хранения значения.) Например, вы можете присвоить постоянное значение переменной (A = 4;), но вы не сможете присвоить ничего постоянному значению — оно не может быть lvalue. (Вы не можете сказать 4 = A;.)

Присвоение примитивов достаточно прямое и понятное. Так как примитивы хранят реальное значение, а не ссылку на объект, то когда вы присваиваете примитивы, вы копируете содержимое с одного места в другое. Например, если вы говорите A = B для примитивов, то содержимое B копируется в A. Если вы потом измените A, B не подвергнется изменениям. Как программист, это то, что вы хотите ожидать в большинству случаев.

Однако когда вы присваиваете объекты, все меняется. Когда бы вы ни манипулировали объектом, то, чем вы манипулируете - ссылка, так что когда вы присваиваете “один объект другому”, на самом деле вы копируете ссылку из одного места в другое. Это означает, если вы скажете C = D для объектов, в конце вы получаете, что C и D указывают на объект, на который первоначально указывает только D. Приведенный ниже пример будет демонстрировать это.

Вот этот пример:

//: c03:Assignment.java
// Присвоение объектов немного хитрая вешь.

class Number {
  int i;
}

public class Assignment {
  public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    n1.i = 9;
    n2.i = 47;
    System.out.println("1: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
    n1 = n2;
    System.out.println("2: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
    n1.i = 27;
    System.out.println("3: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
  }
} ///:~

Класс Number - прост и внутри функции main( ) создаются два его экземпляра .(n1 и n2). Переменная Значения i в каждом из Number имеют разные значения, а затем n2 присваивается n1, а n1 изменяется. Во многих языках программирования вы можете ожидать, что n1 и n2 независимы все время, но потому что вы присвоили ссылку, здесь приводится вывод, который вы увидите:

1: n1.i: 9, n2.i: 47
2: n1.i: 47, n2.i: 47
3: n1.i: 27, n2.i: 27

Изменение объекта n1 проявляется в изменении объекта n2! Это потому, что и n1 и n2 содержат одну и ту же ссылку, которые указывают на один и тот же объект. (Начальная ссылка, которая была в n1 и указывала на объект, содержащий значение 9 была переписана во время присвоения и на самом деле потерялась; ее объект будет очищен сборщиком мусора.)

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

n1.i = n2.i;

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

Эффект наложения при вызове методов

Эффект наложения также случается при передаче объектов в метод:
//: c03:PassObject.java
// Передача объектов в метод может быть не тем,
// что вы использовали.

class Letter {
  char c;
}

public class PassObject {
  static void f(Letter y) {
    y.c = 'z';
  }
  public static void main(String[] args) {
    Letter x = new Letter();
    x.c = 'a';
    System.out.println("1: x.c: " + x.c);
    f(x);
    System.out.println("2: x.c: " + x.c);
  }
} ///:~

Во многих языках программирования для метод f( ) ожидается создание копии его аргумента Letter y внутри границ этого метода. Но так как передается ссылка, то строка

y.c = 'z';

на самом деле меняет объект внутри f( ). Вывод покажет следующее:

1: x.c: a
2: x.c: z

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

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

Основные математические операторы те же, что и допустимые в большинстве языков программирования: сложение (+), вычитание (-), деление (/), умножение (*) и остаток от деления (%, которое производит остаток от целочисленнгого деления). Целочисленное деление в результате выполняет отсечение, а не округление.

Java также использует стенографическую запись для одновременного выполнения операции и присвоения. Это обозначается оператором, следующим за знаком равенства и совместимо со всеми операциями языка (когда это имеет смысл). Например, для добавления 4 к переменной x и присвоения результата x, используйте: x += 4.

Этот пример показывает использование математических операторов:

//: c03:MathOps.java
// Демонстрация математических операторов.
import java.util.*;

public class MathOps {
  // Создает стенографию, чтобы меньше печатать:
  static void prt(String s) {
    System.out.println(s);
  }
  // стенография для печати строки и int:
  static void pInt(String s, int i) {
    prt(s + " = " + i);
  }
  // стенография для печати строки и float:
  static void pFlt(String s, float f) {
    prt(s + " = " + f);
  }
  public static void main(String[] args) {
    // Создает генератор случайных чисел,
    // принимающий текущее время по умолчанию:
    Random rand = new Random();
    int i, j, k;
    // '%' ограничивает максимальное значение величиной 99:
    j = rand.nextInt() % 100;
    k = rand.nextInt() % 100;
    pInt("j",j);  pInt("k",k);
    i = j + k; pInt("j + k", i);
    i = j - k; pInt("j - k", i);
    i = k / j; pInt("k / j", i);
    i = k * j; pInt("k * j", i);
    i = k % j; pInt("k % j", i);
    j %= k; pInt("j %= k", j);
    // Проверка чисел с плавающей точкой:
    float u,v,w;  // Также применима к числам двойной точности
    v = rand.nextFloat();
    w = rand.nextFloat();
    pFlt("v", v); pFlt("w", w);
    u = v + w; pFlt("v + w", u);
    u = v - w; pFlt("v - w", u);
    u = v * w; pFlt("v * w", u);
    u = v / w; pFlt("v / w", u);
    // следующее также работает для
    // char, byte, short, int, long,
    // и double:
    u += v; pFlt("u += v", u);
    u -= v; pFlt("u -= v", u);
    u *= v; pFlt("u *= v", u);
    u /= v; pFlt("u /= v", u);
  }
} ///:~

Первое, что вы увидите - это несколько стенаграфических методов для мечати: метод prt( ) печатает String, метод pInt( ) печатает String, а следом за ней int, a pFlt( ) печатает String, а следом float. Конечно они в конце концов используют System.out.println( ).

Для генерации чисел программа сначала создает объект Random. Поскольку во время создания не передаются аргументы, Java использует текущее время как источник для генератора случайных чисел. Программа генерирует несколько случайных чисел разных типов с помощью объекта Random, просто вызывая разные методы: nextInt( ), nextLong( ), nextFloat( ) или nextDouble( ).

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

Операторы унарного минуса и плюса

Унарный минус (-) и унарный плюс (+) это такие же операции, как и бинарный минус и плюс. компилятор вычисляет какое использование имеется в виду по спообу записи выражения. Например, выражение
x = -a;

имеет очевидный смысл. Компилятор способен вычислить:

x = a * -b;

но читатель может быть сконфужен, так что лучше сказать:

x = a * (-b);

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

Автоинкремент и автодекремент

Java, как и C, полон сокращений. Сокращения могут сделать код более простым в наборе и либо легким, либо трудным для чтения.

Два из лучших сокращений - это операторы инкремента и декремента (часто называемые операторами автоинкремента и автодекремента). Оператор декремента является -- и обозначает “уменьшение на одну единицу измерения”. Оператор инкремента - ++ и означает “увеличить на одну единицу измерения”. Если, например, a - int, выражение ++a еквивалентно (a = a + 1). Операторы инкремента и декремента в результате производят такое же значение, что и переменная.

Есть две версии каждого типа оператора, часто называемые префиксной и постфиксной версией. Преинкремент означает, что оператор ++ стоит перед переменной или выражением, а постинкремент означает, что оператор ++ стоит после переменной или выражения. Аналогично, предекремент означает, что оператор -- стоит перед переменной или выражением, а постдекремент означает, что оператор -- стоит после переменной или выражения. Для преинкремента и предекремента (т.е. ++a или --a), выполняется операция и производится значение. Для постинкремента и постдекремента (т.е. a++ или a--) сначала производится значение, а затем выполняется операция. Как пример:

//: c03:AutoInc.java
// Демонстрирует операторы ++ и --.

public class AutoInc {
  public static void main(String[] args) {
    int i = 1;
    prt("i : " + i);
    prt("++i : " + ++i); // Преинкремент
    prt("i++ : " + i++); // Постинкремент
    prt("i : " + i);
    prt("--i : " + --i); // Предекремент
    prt("i-- : " + i--); // Постдекремент
    prt("i : " + i);
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

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

i : 1
++i : 2
i++ : 2
i : 3
--i : 2
i-- : 2
i : 1

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

Оператор инкремента - это одно из объяснений для имени C++, подразумевающее “один шаг в сторону от C”. В ранней речи о Java Bill Joy (один из создателей) сказал, что “Java=C++--” (C плюс плюс минус минус), намекая, что Java - это C++ с удаленной ненужной сложной частью и поэтому более простой язык. Когда вы будете продвигаться п окниге, вы увидите, что многие части проще, и теперь Java не так прост, как C++.

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

Операторы сравнения генерируют булевый результат. Они вычисляют отношения между значениями и операндами. Выражение отношения производит true, если выражение истинное, а false, если выражение ложное. Выражения отношения, это: меньше чем (<), больше чем (>), меньше либо равно, чем (<=), больше либо равно, чем (>=), равно (==) и не равно (!=). Равно и неравно работает со всеми встроенными типами данных, но другие сравнения работают только с типом boolean.

Проверка равенства объектов

Операторы сравнения == и != также работают со всеми объектами, но их значение часто смущает новичков в программировании на Java. Вот пример:

//: c03:Equivalence.java

public class Equivalence {
  public static void main(String[] args) {
    Integer n1 = new Integer(47);
    Integer n2 = new Integer(47);
    System.out.println(n1 == n2);
    System.out.println(n1 != n2);
  }
} ///:~

Выражение System.out.println(n1 == n2) напечатает результат булевского сравнение, заключенного в нем. Конечно, на выходе должно быть true, а затем false, так как оба объекта Integer обинаковы. Но пока содержимое объектов одинаковое, ссылки не одинаковы, и операторы == и != сравнивают ссылки объектов. Так что на самом деле на выходе вы получите false, а затем true. Естественное, сначало это удивляет людей.

Что, если вы хотите сравнить реальное содержимое объектов на равентсво? Вы должны использовать специальный метод equals( ), который существует для всех объектов (не для примитивов, которые отлично работают с == и !=). Здесь показано как это использовать:

//: c03:EqualsMethod.java

public class EqualsMethod {
  public static void main(String[] args) {
    Integer n1 = new Integer(47);
    Integer n2 = new Integer(47);
    System.out.println(n1.equals(n2));
  }
} ///:~

Результатом будет true, как вы можете ожидать. Да, но это не так проссто. Если вы создаете свой собственный класс, как здесь:

//: c03:EqualsMethod2.java

class Value {
  int i;
}

public class EqualsMethod2 {
  public static void main(String[] args) {
    Value v1 = new Value();
    Value v2 = new Value();
    v1.i = v2.i = 100;
    System.out.println(v1.equals(v2));
  }
} ///:~

вы снова вернетесь к предыдущему? результат - false. Это происходит потому, что поведение по умолчанию equals( ) - это сравнение ссылок. Так что, если вы не перегрузите equals( ) в вашем новом классе, вы не получите описанное поведение. К сожалению, вы не будете учить о перегрузке до Главы 7, но начальные знания о способах поведения equals( ) может спасти вас от печали.

Большинство библиотек классов Java реализуют equals( ), так что он сравнивает содержимое объектов вместо их ссылок.

Логические операторы

Логические операторы И (&&), ИЛИ (||) и НЕ (!) производят булевое значение true или false, в зависимости от отношений оргументов. Этот пример использует отношения и логические операторы:
//: c03:Bool.java
// Отношения и логические операторы.
import java.util.*;

public class Bool {
  public static void main(String[] args) {
    Random rand = new Random();
    int i = rand.nextInt() % 100;
    int j = rand.nextInt() % 100;
    prt("i = " + i);
    prt("j = " + j);
    prt("i > j is " + (i > j));
    prt("i < j is " + (i < j));
    prt("i >= j is " + (i >= j));
    prt("i <= j is " + (i <= j));
    prt("i == j is " + (i == j));
    prt("i != j is " + (i != j));

    // Трактовка int как boolean
    // в Java недопустимо
//! prt("i && j is " + (i && j));
//! prt("i || j is " + (i || j));
//! prt("!i is " + !i);

    prt("(i < 10) && (j < 10) is "
       + ((i < 10) && (j < 10)) );
    prt("(i < 10) || (j < 10) is "
       + ((i < 10) || (j < 10)) );
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

Вы можете применять И, ИЛИ или НЕ только к значениям boolean. Вы не можете использовать не boolean, как будто это boolean в логических выражениях, как вы это можете делать в C и C++. Вы можете видеть неудачную попытку этого, убрав коментарий в строках, помеченных //!. Однако последующие выражения производят значения boolean, используя отношения сравнения, затем используя логические выражения для результата.

Список вывода выглядит примерно так:

i = 85
j = 4
i > j is true
i < j is false
i >= j is true
i <= j is false
i == j is false
i != j is true
(i < 10) && (j < 10) is false
(i < 10) || (j < 10) is true

Обратите внимание, что значение boolean автоматически преобразуется в соответствующую текстовую форму, если он используется в месте, где ожидается String.

Вы можете заменить определение для int в приведенной выше программе на любой другой примитивный тип данных, за исключением boolean. Однако осознавайте, что сравнение чисел с плавающей точкой очень строгое. Число, которое на бесконечно малую величину отличается от другого - “не равно”. Число, которое на бесконечно малую величину больше нуля - не ноль.

Короткое замыкание

Когда имеете дело с логическими операторами, вы входите в феномен, называемый “короткое замыкание”. Это означает, что выражение будет вычислятся только до тех пор, не будет определена неоднозначно правдивость или ложность всего выражения. как результат, все части логического выражения могут не вычислятся. Хдесь приведен пример, который демонстрирует короткое замыкание:

//: c03:ShortCircuit.java
// Демонстрирует поведение короткого замыкания
// с логическими операциями.

public class ShortCircuit {
  static boolean test1(int val) {
    System.out.println("test1(" + val + ")");
    System.out.println("result: " + (val < 1));
    return val < 1;
  }
  static boolean test2(int val) {
    System.out.println("test2(" + val + ")");
    System.out.println("result: " + (val < 2));
    return val < 2;
  }
  static boolean test3(int val) {
    System.out.println("test3(" + val + ")");
    System.out.println("result: " + (val < 3));
    return val < 3;
  }
  public static void main(String[] args) {
    if(test1(0) && test2(2) && test3(2))
      System.out.println("expression is true");
    else
      System.out.println("expression is false");
  }
} ///:~

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

if(test1(0) && test2(2) && test3(2))

Вы на самом деле можете подумать, что выполняются все три теста, но выходные данные говорят об обратном:

test1(0)
result: true
test2(2)
result: false
expression is false

Первый тест возвращает в результате true, так что продолжается вычисление выражения. Однако второй тест в результате возвращает false. Так как это означает, что все выражение должно быть false, то зачем продолжать вычисления оставшегося выражения? Это было бы слишком дорого. Оправдание короткого замыкания, фактически, заключается именно в этом; вы можете получить потенциальное увеличение производительности, если не будет необходимости вычислять все части логического выражения.

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

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

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

Битовый оператор И (&) производит единицу в выходном бите, если оба входных бита были единицами; в противном случае результат - ноль. Битовый оператор ИЛИ (|) производит единицу в выходном бите, если один из входных бит - единица, и производит ноль, если оба бита - нули. Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ, или XOR (^), производит единицу в выходном бите, если один или другой входной бит - единица, но не оба. Битовая операция НЕ (~, также называемый оператором дополнения) - это унарный оператор; он принимает только один аргумент. (Все остальные битовые операторы - бинарные.) Битовое НЕ на выходе производит бит, противоположных входящему — единицу, если входящий бит - ноль, и ноль, если входящий бит - единица.

Битовые операторы и логические операторы используют одинаковые символы, так что полезно иметь мнемоническуе схему, которая поможет вам запомнить значения: так как биты “малы”, то используется только один символ в битовых операторах.

Битовые операторы можно комбинировать со знаком = для соединения операции и присвоений: &=, |= и ^= являются допустимыми. (Так как ~ - это унарный оператор, он не может комбинироваться со знаком =.)

Тип boolean трактуется как однобитное значение, так что это кое в чем отличается. Вы можете выполнять битовое И, ИЛИ и XOR, но вы не можете выполнять битовое НЕ (предположительно для предотвращения путаницы с логическим НЕ). Для булевских битовых операций имеется то же эффект, что и для логических операций, за исключением того, что они не подвержены короткому замыканию. Также, битовые операции на булевыми типами, включают логический оператор XOR, который не включен в список “логических” операторов. Вы предохранены от использования булевских типов в выражениях сдвига, которые описаны далее.

Операторы сдвига

Операторы сдвига также манипулируют битами. Они могут использоваться исключительно с примитивными, целыми типами. Оператор сдвига влево (<<) производит действия над операндом, расположенным слева от оператора, сдвигая влева на число бит, указанное после оператора (вставляя нули в биты младшего порядка). Оператор сдвига вправо с учетом знака (>>) производит действия над операндом, расположенным слева от оператора, сдвигаяя вправо на число бит, указанное после оператора. Сдвиг в право с учетом знака >> использует знаковое дополнение: если значение положительное в биты старшего порядка вставляются нули; если значение отрицательное, в старшие биты вставляются единицы. Java также добавлен беззнаковый сдвиг вправо >>>, который использует дополнение нулями: независимо от знака, в старшие биты вставляются нули. Этот оператор не существует ни в C, ни в C++.

Если вы сдвигаете char, byte или short, это переводится в int перед сдвигом, а результат будет типа int. Будут использоваться только пять младших бит с правой стороны. Это предохранит вас от сдвига на болешее число бит, чем есть в int. Если вы работаете с long, в результате вы получите long. Будут использоваться только шесть младших бит с правой стороны, так что вы не сможете сдвинуть на большее число бит, чем есть в long.

Сдвиг может быть скомбинирован со знаком равенства (<<= или >>= или >>>=). lvalue заменяется на lvalue, сдвинутое на правое rvalue. Однако, есть проблема с беззнаковым правым сдвигом, скомбинированным с присваиванием. Если вы используете byte или short, вы не получаете корректный результат. Вместо этого происходит преобразование к int и правый сдвиг, но затем происходит усечение, так как результат снова присваивается к той же переменной, так что в этих случаях вы получите -1. Приведенный пример демонстрирует это:

//: c03:URShift.java
// Проверка беззнакового правого сдвига.

public class URShift {
  public static void main(String[] args) {
    int i = -1;
    i >>>= 10;
    System.out.println(i);
    long l = -1;
    l >>>= 10;
    System.out.println(l);
    short s = -1;
    s >>>= 10;
    System.out.println(s);
    byte b = -1;
    b >>>= 10;
    System.out.println(b);
    b = -1;
    System.out.println(b>>>10);
  }
} ///:~

В последней строке результирующее значение не присваивается назат переменной b, а сразу выводится на печать, поэтому мы видим правильное поведение.

Здесь мы видим пример, который демонстрирует использование всех операторов, использующих биты:

//: c03:BitManipulation.java
// Использование битовых операторов.
import java.util.*;

public class BitManipulation {
  public static void main(String[] args) {
    Random rand = new Random();
    int i = rand.nextInt();
    int j = rand.nextInt();
    pBinInt("-1", -1);
    pBinInt("+1", +1);
    int maxpos = 2147483647;
    pBinInt("maxpos", maxpos);
    int maxneg = -2147483648;
    pBinInt("maxneg", maxneg);
    pBinInt("i", i);
    pBinInt("~i", ~i);
    pBinInt("-i", -i);
    pBinInt("j", j);
    pBinInt("i & j", i & j);
    pBinInt("i | j", i | j);
    pBinInt("i ^ j", i ^ j);
    pBinInt("i << 5", i << 5);
    pBinInt("i >> 5", i >> 5);
    pBinInt("(~i) >> 5", (~i) >> 5);
    pBinInt("i >>> 5", i >>> 5);
    pBinInt("(~i) >>> 5", (~i) >>> 5);

    long l = rand.nextLong();
    long m = rand.nextLong();
    pBinLong("-1L", -1L);
    pBinLong("+1L", +1L);
    long ll = 9223372036854775807L;
    pBinLong("maxpos", ll);
    long lln = -9223372036854775808L;
    pBinLong("maxneg", lln);
    pBinLong("l", l);
    pBinLong("~l", ~l);
    pBinLong("-l", -l);
    pBinLong("m", m);
    pBinLong("l & m", l & m);
    pBinLong("l | m", l | m);
    pBinLong("l ^ m", l ^ m);
    pBinLong("l << 5", l << 5);
    pBinLong("l >> 5", l >> 5);
    pBinLong("(~l) >> 5", (~l) >> 5);
    pBinLong("l >>> 5", l >>> 5);
    pBinLong("(~l) >>> 5", (~l) >>> 5);
  }
  static void pBinInt(String s, int i) {
    System.out.println(
      s + ", int: " + i + ", binary: ");
    System.out.print("   ");
    for(int j = 31; j >=0; j--)
      if(((1 << j) &  i) != 0)
        System.out.print("1");
      else
        System.out.print("0");
    System.out.println();
  }
  static void pBinLong(String s, long l) {
    System.out.println(
      s + ", long: " + l + ", binary: ");
    System.out.print("   ");
    for(int i = 63; i >=0; i--)
      if(((1L << i) & l) != 0)
        System.out.print("1");
      else
        System.out.print("0");
    System.out.println();
  }
} ///:~

Два метода в конце: pBinInt( ) и pBinLong( ) tполучают int или long соответственно, и печатают их в бинарном формате вместе с описательной строкой. Вы можете пока проигнорировать их реализацию.

Вы обратите внимание на использование System.out.print( ) вместо System.out.println( ). Метод print( ) не вызывает появление новой строки, так что это позволяет вам выводить строку по кусочкам.

Заодно здесь демонстрируется эффект для всех битовых операций для int и long, этот пример также показывает минимальное, максимальное, +1 и -1 значения для int и long, так что вы можете увидить как они выглядят. Обратите внимание на битовое представление знака: 0 означает положительное число, 1 означает отрицательное. Вывод для части int выглядит так:

-1, int: -1, binary: 
   11111111111111111111111111111111
+1, int: 1, binary: 
   00000000000000000000000000000001
maxpos, int: 2147483647, binary: 
   01111111111111111111111111111111
maxneg, int: -2147483648, binary: 
   10000000000000000000000000000000
i, int: 59081716, binary: 
   00000011100001011000001111110100
~i, int: -59081717, binary: 
   11111100011110100111110000001011
-i, int: -59081716, binary: 
   11111100011110100111110000001100
j, int: 198850956, binary: 
   00001011110110100011100110001100
i & j, int: 58720644, binary: 
   00000011100000000000000110000100
i | j, int: 199212028, binary: 
   00001011110111111011101111111100
i ^ j, int: 140491384, binary: 
   00001000010111111011101001111000
i << 5, int: 1890614912, binary: 
   01110000101100000111111010000000
i >> 5, int: 1846303, binary: 
   00000000000111000010110000011111
(~i) >> 5, int: -1846304, binary: 
   11111111111000111101001111100000
i >>> 5, int: 1846303, binary: 
   00000000000111000010110000011111
(~i) >>> 5, int: 132371424, binary: 
   00000111111000111101001111100000

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

Тернарный оператор if-else

Этот оператор необычен, поскольку использует три операнда. Это действительно деле оператор, поскольку он производит значение, в отличие от обычного выражения if-else, которое вы увидите в следующем разделе этой главы. Это выражение имеет форму:

boolean-exp ? value0 : value1

Если boolean-exp вычисляется как true, вычисляется value0 и оно становится результатом, производимым оператором. Если boolean-exp - false, вычисляется value1 и оно становится результатом, производимым оператором.

Конечно вы можете использовать обычное выражение if-else (описанное позже), но тернарный оператор более краткий. Хотя C (откуда пришел этот оператора) гордится собой, как кратким языком, а тернарный оператор может быть введен частично для эффективности, вы иногда должны быть осторожны при каждодневном его использовании — он легко делает код нечитаемым.

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

static int ternary(int i) {
  return i < 10 ? i * 100 : i * 10;
} 

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

static int alternative(int i) {
  if (i < 10)
    return i * 100;
  else
    return i * 10;
}

Вторая форма легче для понимания, и не требует намного большего набора. Так что будьте уверены, когда выбираете тернарный оператор.

Оператор запятая

Запятая используется в C и C++ нетолько как разделитель в списке аргументов функции, но также как оператор последовательности вычислений. Единственное место, где оператор запятая используется в Java - это цикл for, который будет описан позже в этой главе.

String: оператор +

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

Использование String + имеет некоторые интересные черты поведения Если выражение начинается со String, то все операнды, которые идут дальше, должны быть типа String (помните, что компилятор превратит указанную последовательность символов в String):

int x = 0, y = 1, z = 2;
String sString = "x, y, z ";
System.out.println(sString + x + y + z);

Здесь компилятор Java преобразует x, y и z в предстваление String, вместо того, чтобы сначала их сложить вместе. А если вы скажете:

System.out.println(x + sString);

Java переведет x в String.

Общие ловушки при использовании операторов

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

Чрезвычайно общая ошибка в C и C++ выглядит так:

while(x = y) {
    // ....
}

Программист пробовал проверить на равенство (==), а выполнил присвоение. В C и C++ результат присвоения всегда будет true, если y не ноль, и вы, вероятно, получите бесконечный цикл. В Java, результат этого выражения не boolean, а компилятор ожидает boolean и не может преобразовать int, так что он выдаст вам ошибку времени компиляции и выявит проблему до того, как ы запустите программу. Так что ловушка никогда не случится в Java. (Вы не получите сообщение об ошибке времени компиляции, когда x и y - boolean, в таком случае x = y i- допустимое выражение, но в приведенном примере, вероятно, ошибочное.)

Аналогично C и C++ есть проблема использование битовыз И и ИЛИ вместо логической версии. Битовые И и ИЛИ используют один символ (& или |), а логические И и ИЛИ используют два (&& и ||). Как и с = и ==, легко напечатать только один символ вместо двух. В Java компилятор опять предотвратит это, потому что он не позволит вым бесцеремонно использовать один тип, где это не применимо.

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

Слово приведение используется в смысле “приведение к шаблону”. Java будет автоматически менять тип данных на другой при присвоении. Например, если вы присваиваете целочисленное значение переменной с плавающей точкой, компилятор автоматически конвертирует int в float. Приведение позволяет вам сделать такой тип преобразования более точным или форсировать его, когда оно не может выполнится нормально.

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

void casts() {
  int i = 200;
  long l = (long)i;
  long l2 = (long)200;
}

Как вы можете видеть, возможно выполнить приведение для числового значения так же как и для переменной. Однако, вобоих показанных здесь приведениях излишне, так как компилятор автоматически переводит значение int в long, когда это необходимо. Но вам позволено применять излишнее преобразование, чтобы сделать какое-то место или сделать ваш код более понятным. В остальных ситуациях приведение может быть очень важно просто для того, чтобы код скомпилировался.

В C и C++ приведение может стать причиной головной боли. В Java приведение безопасно, за исключением тех случаев, когда вы выполняете так называемое сужающее преобразование (то есть, когда вы переходите от одного типа данных, который содержит больше информации, к другому, который не содержит так много), вы рискуете потерять информацию. Здесь компилятор заставляет вас выполнить преобразование и при этом говорит: “выполнение это может быть опасным — если вы хотите от меня, чтобы я все равно сделал это, вы должны выполнить явное преобразование”. При расширенном преобразовании в явном приведении нет необходимости, потому что новый тип будет содержать больше информации, в отличае от старого типа, так что не будет потерь в информации.

Java позволяет вам выполнить приведение любого примитивного типа к любому другому примитивному типу, за исключением boolean, для которого не допускается любое приведение. Типы классов не позволяют приведение. Для преобразования одного к другому должны быть специальные методы. (String - особый случай и вы позже найдете в этой книге, что объекты могут приводится в пределах семейства типов; Oak может быть преобразован к Tree и наоборот, но не к постороннему типу, такому как Rock.)

Литералы

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

//: c03:Literals.java

class Literals {
  char c = 0xffff; // максимальное шестнадцатиричное значение для char
  byte b = 0x7f; // максимальное шестнадцатиричное значение для byte
  short s = 0x7fff; // максимальное шестнадцатиричное значение для short
  int i1 = 0x2f; // Шестнадцатирично-десятичное (в нижнем регистре)
  int i2 = 0X2F; // Шестнацчатирично-десятичное (в верхнем регистре)
  int i3 = 0177; // Восьмеричное (ведущий ноль)
  // Шестнадцатиричные и восьмиричные также работают с long.
  long n1 = 200L; // суффикс для long
  long n2 = 200l; // суффикс для long
  long n3 = 200;
  //! long l6(200); // не допустимо
  float f1 = 1;
  float f2 = 1F; // суффикс для float
  float f3 = 1f; // суффикс для float
  float f4 = 1e-45f; // 10 - основание степени
  float f5 = 1e+9f; // суффикс для float
  double d1 = 1d; // суффикс для double
  double d2 = 1D; // суффикс для double
  double d3 = 47e47d; // 10 - основание степени
} ///:~

Шестнадцатерично-десятичные (основание 16), которые работают со всеми интегрированными типами данных, указываются лидирующим символом 0x или 0X, за которыми следует 0—9 и далее a—f в верхнем, либо в нижнем регистре. Если вы попробуете проинициализировать переменную с помощью значения, большего, чем она может принять (не зависимо от числовой формы значения), компилятор выдаст вам сообщение об ошибке. Обратите внимание в приведенном выше коде на максимально допустимое шестнадцатирично-десятичное значение для char, byte и short. Если вы превысите его, компилятор автоматически преобразует значение к int и скажет вам, что необходимо сужающее приведение для присваения. Вы найдете это место, остановившись на этой строке.

Восьмеричные (основание 8) указываются лидирующим нулемв цисле и цифррами 0-7. Нет специальных литералов для бинарного впедсталения в C, C++ или Java.

Замыкающие символы после литерного значения устанавливают тип. Символ L в верхнем или нижнем регистре означает long, верхний или нижний регистр F означает float, а верхний или нижний регистр D означает double.

Используется експонентная запись, которую я всегда находил пугающей: 1.39 e-47f. В науки и инженерии ‘e’ означает основание натурального логарифма, примерно 2.718. (Более точное значение типа double доступно в Java, как Math.E.) Здесь используется экспонентное выражение, такое как 1.39 x e-47, которое означает 1.39 x 2.718-47. Однако когда был создан FORTRAN, то решили, что e на самом деле будет означать “десять в степени”, что было странным решением, потому что FORTRAN был предназначен для науки и инжененрии, и можно подумать, что его разработчики будут чувствительны к введению такой неоднозначности. [25] В любом случае это перешло в C, C++ и теперь в Java. Так что, если вы используете мышление в терминах e, как основания натурального логарифма, вы должны в уме выполнить перевод, когда используете такое выражение, как 1.39 e-47f в Java; это означает 1.39 x 10-47.

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

long n3 = 200;

нет неоднозначности, так что L после 200 будет излишним. Однако в примере

float f4 = 1e-47f; // 10 в степени

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

Повышение

Вы обнаружите, что если вы выполняете любую математическую или битовую операцию над примитивными типами данных, которые меньше, чем int (то есть, char, byte или short), эти значения будут повышены до int перед выполнением операций, а результирующее значение будет типа int. Так что, если вы хотите присвоить обратно к маленькому типу, вы должны использовать приведение. (И, так как вы обратно присваиваете к меньшему типу, вы можете потерять информацию.) В общем, большие типы данных в выражениях - это то, что определяет размер результата выражения; если вы умножаете float на double, результатом будет double; если вы складываете int и long, результатом будет long.

В Java нет “sizeof”

В C и C++ оператор sizeof( ) удовлетворяет специфическим требованиям: он говорит вам число байт, занимаемых элементом данных. Наиболее неотразимая черта sizeof( ) в C и C++ - это компактность. Различные типы данных могут быть различных размеров на разных машинах, так что программист должен определить насколько велик этот тип данных, когда он выполняет операцию, чувствительную к размеру. Например, один компьютер может хранить целые числа в 32 битах, а другой компьютер хранит целые как 16 бит. Программы могут хранить большие значения в целых числах на первой машине. Как вы можете заметить, компактность - огромная головная боль для программистов C и C++.

В Java нет необходимости в операторе sizeof( ) для этих целей, потому что все типы данных имеют один размер на всех машинах. У вас нет необходимости думать о компакности на этом уровне — она встроена в язык.

Снова о предшествовании

Слушая мои объяснения о сложности запомнинания последовательности операторов, студенты подсказали мнемонику, которая одновременно является комментарием: “У нас Авария Случилась, Лежу Теперь Полуживой”.*

Мнемоника Типе оператора Операторы
У нас Унарные + - ++--
Авария Арифметические (и сдвиг) * / % + - << >>
Случилась Сравнение > < >= <= == !=
Лежу Логические (и битовые) && || & | ^
Теперь Тернарная A > B ? X : Y
Полуживой Присваивание = (и комбинированное присваивание, как *=)

Конечно с операторами сдвига и битовыми операторами, распределенными по таблице это не совсем точная мнемоника, но для не битовых операций она работает.

Руководство по операторам

Следующий пример показывает какие примитивные типы данных могут быть использованы с определенными операторами. В основном это пример, который повторяется снова и снова, но использует различные примитивные типы данных. Файл будет компилироваться без ошибок, потому что строки, которые могут стать причиной ошибки, закоментированы с помощью //!.

//: c03:AllOps.java
// Проверка всех операторов для всех
// примитивных типов данных, чтобы показать,
// какие принимаются компилятором Java.

class AllOps {
  // Для получения результата булевой проверки:
  void f(boolean b) {}
  void boolTest(boolean x, boolean y) {
    // Арифметические операторы:
    //! x = x * y;
    //! x = x / y;
    //! x = x % y;
    //! x = x + y;
    //! x = x - y;
    //! x++;
    //! x--;
    //! x = +y;
    //! x = -y;
    // Сравнение и логика:
    //! f(x > y);
    //! f(x >= y);
    //! f(x < y);
    //! f(x <= y);
    f(x == y);
    f(x != y);
    f(!y);
    x = x && y;
    x = x || y;
    // Битовые операторы:
    //! x = ~y;
    x = x & y;
    x = x | y;
    x = x ^ y;
    //! x = x << 1;
    //! x = x >> 1;
    //! x = x >>> 1;
    // Совмещение присваения:
    //! x += y;
    //! x -= y;
    //! x *= y;
    //! x /= y;
    //! x %= y;
    //! x <<= 1;
    //! x >>= 1;
    //! x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! char c = (char)x;
    //! byte B = (byte)x;
    //! short s = (short)x;
    //! int i = (int)x;
    //! long l = (long)x;
    //! float f = (float)x;
    //! double d = (double)x;
  }
  void charTest(char x, char y) {
    // Арифметические операторы:
    x = (char)(x * y);
    x = (char)(x / y);
    x = (char)(x % y);
    x = (char)(x + y);
    x = (char)(x - y);
    x++;
    x--;
    x = (char)+y;
    x = (char)-y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x= (char)~y;
    x = (char)(x & y);
    x  = (char)(x | y);
    x = (char)(x ^ y);
    x = (char)(x << 1);
    x = (char)(x >> 1);
    x = (char)(x >>> 1);
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    x <<= 1;
    x >>= 1;
    x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    byte B = (byte)x;
    short s = (short)x;
    int i = (int)x;
    long l = (long)x;
    float f = (float)x;
    double d = (double)x;
  }
  void byteTest(byte x, byte y) {
    // Арифметические операторы:
    x = (byte)(x* y);
    x = (byte)(x / y);
    x = (byte)(x % y);
    x = (byte)(x + y);
    x = (byte)(x - y);
    x++;
    x--;
    x = (byte)+ y;
    x = (byte)- y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x = (byte)~y;
    x = (byte)(x & y);
    x = (byte)(x | y);
    x = (byte)(x ^ y);
    x = (byte)(x << 1);
    x = (byte)(x >> 1);
    x = (byte)(x >>> 1);
    // Совмещение присваения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    x <<= 1;
    x >>= 1;
    x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    short s = (short)x;
    int i = (int)x;
    long l = (long)x;
    float f = (float)x;
    double d = (double)x;
  }
  void shortTest(short x, short y) {
    // Арифметические операторы:
    x = (short)(x * y);
    x = (short)(x / y);
    x = (short)(x % y);
    x = (short)(x + y);
    x = (short)(x - y);
    x++;
    x--;
    x = (short)+y;
    x = (short)-y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x = (short)~y;
    x = (short)(x & y);
    x = (short)(x | y);
    x = (short)(x ^ y);
    x = (short)(x << 1);
    x = (short)(x >> 1);
    x = (short)(x >>> 1);
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    x <<= 1;
    x >>= 1;
    x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    byte B = (byte)x;
    int i = (int)x;
    long l = (long)x;
    float f = (float)x;
    double d = (double)x;
  }
  void intTest(int x, int y) {
    // Арифметические операторы:
    x = x * y;
    x = x / y;
    x = x % y;
    x = x + y;
    x = x - y;
    x++;
    x--;
    x = +y;
    x = -y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x = ~y;
    x = x & y;
    x = x | y;
    x = x ^ y;
    x = x << 1;
    x = x >> 1;
    x = x >>> 1;
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    x <<= 1;
    x >>= 1;
    x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    byte B = (byte)x;
    short s = (short)x;
    long l = (long)x;
    float f = (float)x;
    double d = (double)x;
  }
  void longTest(long x, long y) {
    // Арифметические операторы:
    x = x * y;
    x = x / y;
    x = x % y;
    x = x + y;
    x = x - y;
    x++;
    x--;
    x = +y;
    x = -y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x = ~y;
    x = x & y;
    x = x | y;
    x = x ^ y;
    x = x << 1;
    x = x >> 1;
    x = x >>> 1;
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    x <<= 1;
    x >>= 1;
    x >>>= 1;
    x &= y;
    x ^= y;
    x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    byte B = (byte)x;
    short s = (short)x;
    int i = (int)x;
    float f = (float)x;
    double d = (double)x;
  }
  void floatTest(float x, float y) {
    // Арифметические операторы:
    x = x * y;
    x = x / y;
    x = x % y;
    x = x + y;
    x = x - y;
    x++;
    x--;
    x = +y;
    x = -y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    //! x = ~y;
    //! x = x & y;
    //! x = x | y;
    //! x = x ^ y;
    //! x = x << 1;
    //! x = x >> 1;
    //! x = x >>> 1;
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    //! x <<= 1;
    //! x >>= 1;
    //! x >>>= 1;
    //! x &= y;
    //! x ^= y;
    //! x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    byte B = (byte)x;
    short s = (short)x;
    int i = (int)x;
    long l = (long)x;
    double d = (double)x;
  }
  void doubleTest(double x, double y) {
    // Арифметические операторы:
    x = x * y;
    x = x / y;
    x = x % y;
    x = x + y;
    x = x - y;
    x++;
    x--;
    x = +y;
    x = -y;
    // Сравнение и логика:
    f(x > y);
    f(x >= y);
    f(x < y);
    f(x <= y);
    f(x == y);
    f(x != y);
    //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    //! x = ~y;
    //! x = x & y;
    //! x = x | y;
    //! x = x ^ y;
    //! x = x << 1;
    //! x = x >> 1;
    //! x = x >>> 1;
    // Совмещение присвоения:
    x += y;
    x -= y;
    x *= y;
    x /= y;
    x %= y;
    //! x <<= 1;
    //! x >>= 1;
    //! x >>>= 1;
    //! x &= y;
    //! x ^= y;
    //! x |= y;
    // Приведение:
    //! boolean b = (boolean)x;
    char c = (char)x;
    byte B = (byte)x;
    short s = (short)x;
    int i = (int)x;
    long l = (long)x;
    float f = (float)x;
  }
} ///:~

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

Для char, byte и short вы можете увидеть эффект повышения с арифметическими операторами. Каждая арифметическая операция для любого их этих типов дает в результате int , который должен быть явно преобразован обратно к начальному типу (сужающее преобразование, из-за которого может быть потеряна информация), чтобы присвоить обратно к этому типу. Однако со значением типа int вам нет необходимости выполнять приведение, потому что все и так типа int. Но не успокаивайтесь, думая, что все безопасно. Если вы умножите два числа типа ints, которые достаточно большие, вы получите в результате переполнение. Следующий пример демонстрирует это:

//: c03:Overflow.java
// Сюрприз! Java позволяет переполнение.

public class Overflow {
  public static void main(String[] args) {
    int big = 0x7fffffff; // максимальное значение типа int
    prt("big = " + big);
    int bigger = big * 4;
    prt("bigger = " + bigger);
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

На выходе получим это:

big = 2147483647
bigger = -4

и вы не получили ошибок или предупреждений от компилятора, так же нет исключений времени выполнения. Java - это хорошо, но не так хорошо.

Совмещение с присвоением не требует приведения для char, byte или short, даже хотя выполняется повышение, которое дает тот же результат, что и прямые арифметические операции. С другой стороны, отсутствие приведение конечно упрощает код.

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

Контроль за выполнением

Java использует все выражения, управляющие выполнением, присущие C, так что если вы программировали на C или C++, то вы знакомы с большинством из того, что вы будете применять. Большинство языков процедурного программирования имеют аналогичные управляющие выражения, и они часто похожи во многих языках. В Java ключевые слова включают if-else, while, do-while, for и выражение выбора, называемое switch. Однако Java не поддерживает всеми ругаемое goto (которое все же остается наиболее подходящим способом для решения определенных проблем). Вы все еще можете делать переходы по типу goto, но они более ограничены, чем типичные goto.

true и false

Все сравнительные выражения используют правдивое или ложное выражение сравнения для определения пути выполнения. Примером сравнительного выражения является A == B. Здесь используется сравнительный оператор ==, чтобы увидеть, если значение A равно значению B. Выражение возвращает true или false. Все операторы отношений, видимые вами ранее в этой главе могут быть использованы для производства сравнительных выражений. Обратите внимание, что Java не допусскает использование чисел, как значения типа boolean, несмотря на то, что это допустимо в C в C++ (где истинным является ненулевое значение, а ложным - нулевое). Если вы хотите использовать не boolean в булевских проверках, таких как if(a), вы должны сначала перевести его в значение типа boolean, используя выражения сравнения, такие как if(a != 0).

if-else

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

if(Логическое выражение)
  инструкция

или

if(Логическое выражение)
  инструкция
else
  инструкция

Сравнение должно производит результат типа boolean. Под ниструкцией понимается либо простая инструкция, завершающаюся точкой с запятой, либо составная инструкция, которая группирует простые инструкции, обрамленные фигурными скобками. Везде, где используется слово “инструкция” , оно всегда подразумевает, что инструкция может быть простой или составной.

Как пример if-else, здесь приведен метод test( ), который говорит вам является ли тестовое значение больше, меньше или равным контрольному значению:

//: c03:IfElse.java
public class IfElse {
  static int test(int testval, int target) {
    int result = 0;
    if(testval > target)
      result = +1;
    else if(testval < target)
      result = -1;
    else
      result = 0; // Совпадает
    return result;
  }
  public static void main(String[] args) {
    System.out.println(test(10, 5));
    System.out.println(test(5, 10));
    System.out.println(test(5, 5));
  }
} ///:~

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

return

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

//: c03:IfElse2.java
public class IfElse2 {
  static int test(int testval, int target) {
    int result = 0;
    if(testval > target)
      return +1;
    else if(testval < target)
      return -1;
    else
      return 0; // Совпадает
  }
  public static void main(String[] args) {
    System.out.println(test(10, 5));
    System.out.println(test(5, 10));
    System.out.println(test(5, 5));
  }
} ///:~

Здесь нет необходимости в else, потому что метод не будет продолжаться после выполнения return.

Итерации

while, do-while и for управляют циклом и иногда классифицируются как итерационные инструкции. Инструкция повторяется до тех пор, пока управляющее логическое выражение не станет ложным. Форма цикла while следующая:

while(Логическое выражение)
  инструкция

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

Здесь приведен пример, который генерирует случайные числа, пока пока не достигнится определенное состояние:

//: c03:WhileTest.java
// Демонстрация цикла while.

public class WhileTest {
  public static void main(String[] args) {
    double r = 0;
    while(r < 0.99d) {
      r = Math.random();
      System.out.println(r);
    }
  }
} ///:~

Здесь используется статический метод random( ) из библиотеки Math, который генерирует значения типа double в пределах от 0 до 1. (Это включает 0, но не включает 1.) Сравнительное выражение для while говорит, “продолжать выражение этого цикла, пока не встретится число 0.99 или больше”. Всякий раз, когда вы запускаете программу, вы будете получать список чисел разной длины.

do-while

Форма для do-while следующая:

do
  инструкция
while(Логическое выражение);

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

for

Цикл for выполняет инициализацию перед первой итерацией. Затем он выполняет сравнение, а в конце каждой итерации выполняется, некоторого рода, “шагание”. Форма цикла for следующая:

for(инициализация; логическое выражение; шаг)
  инструкция

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

Цикл for обычно используется для задач “подсчета”:

//: c03:ListCharacters.java
// Демонстрация цикла "for" для составления
// списка всех ASCII символов.

public class ListCharacters {
  public static void main(String[] args) {
  for( char c = 0; c < 128; c++)
    if (c != 26 )  // ANSI Очистка экрана
      System.out.println(
        "value: " + (int)c +
        " character: " + c);
  }
} ///:~

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

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

Вы можете определит несколько переменных внутри инструкции for, но они должны быть одного типа:

for(int i = 0, j = 1;
    i < 10 && j != 11;
    i++, j++)
  /* тело цикла for */;

Определение int в инструкции for распрастраняется на i и j. Способность определять переменные в управляющем выражении является ограничением для цикла for. Вы не можете использовать этот метод на с каким другим выражением выбора или итераций.

Оператор запятая

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

//: c03:CommaOperator.java
public class CommaOperator {
  public static void main(String[] args) {
    for(int i = 1, j = i + 10; i < 5;
        i++, j = i * 2) {
      System.out.println("i= " + i + " j= " + j);
    }
  }
} ///:~

Вот вывод:

i= 1 j= 11
i= 2 j= 4
i= 3 j= 6
i= 4 j= 8

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

break и continue

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

Эта программа показывает пример для break и continue внутри циклов for и while:

//: c03:BreakAndContinue.java
// Демонстрирует break и continue.

public class BreakAndContinue {
  public static void main(String[] args) {
    for(int i = 0; i < 100; i++) {
      if(i == 74) break; // вызод из цикла for
      if(i % 9 != 0) continue; // Следующая итерация
      System.out.println(i);
    }
    int i = 0;
    // "Бесонечный цикл":
    while(true) {
      i++;
      int j = i * 27;
      if(j == 1269) break; // Выход из цикла
      if(i % 10 != 0) continue; // В начало цикла
      System.out.println(i);
    }
  }
} ///:~

В цикле for значение i никогда не дойдет до 100, потому, что инструкция break прервет выполнение цикла, когда i будет равно 74. Обычно, вы будете использовать break как здесь, если вы не будете знать когда возникнет прерывающее условие. Инструкция continue влечет за собой возврат к началу цикла (при этом инкрементируя i) в любом случае, когда i не делится на 9 без остатка. Если это так, значение печатается.

Второй раздел показывает “бесконечный цикл”, который, теоретически, никогда не закончится. Однако, внутри цикла есть инструкция break, которая оборвет цикл. Дополнительно, вы увидите, что continue возвращает назад к началу цикла не завершив оставшегося. (Таким образом печать происходит во втором цикле только когда значение i делится на 10.) Вот результаты:

0
9
18
27
36
45
54
63
72
10
20
30
40

Значение 0 печатается, потому что 0 % 9 равно 0.

Вторая форма бесконечного цикла: for(;;). Компилятор трактует и while(true) и for(;;) одинаково, что бы вы не использовали - это вопрос стиля программирования.

Имеющий дурную славу “goto”

Ключевое слово goto существовало в языках программирования с самого начала. Несомненно, goto было рождено из ассемдлерных языков программирования: “Если условие A, то перейти сюда, в противном случае, перейти сюда”. Если вы читаете ассемблерный код, который в конце концов генерируется практически каждым компилятором, вы увидите, что такое управление программой содержит много переходов. Однако goto - это переход на уровне исходного кода, и это то, что снискало дурную славу. Если программа будет всегда перепрыгивать из одного места в другое, то будет ли способ реорганизовать код так, чтобы управление не было таким прыгающим? goto попал в немилость после известной газетной публикации “Goto considered harmful” Edsger Dijkstra, и с тех пор избиение goto было популярным занятием.

Как обычно в такой ситуации середина наиболее плодотворна. Проблема не в использовании goto, а в перегрузке операторами goto — в редких ситуациях goto действительно лучший способ для структурирования управления течением прораммы.

Хотя goto - это зарезервированное слово в Java, оно не используется в языке; в Java нет goto. Однако здесь есть кое что, что выглядит немного как переход при использовании ключевых слов break и continue . Это не переход, а способ прервать инструкцию итерации. Объяснение часто всплывает в дискусси о goto: потому что тут используется тот же механизм: метка.

Метка - это идентификатор, за которым следует двоеточие, например:

label1:

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

label1: 
outer-iteration {
  inner-iteration {
    //...
    break; // 1
    //...
    continue;  // 2
    //...
    continue label1; // 3
    //...
    break label1;  // 4
  }
}

В случае 1, break прерывает внутреннюю итерацию и вы выходите во внешнюю итерацию. В случие 2, continue перемещает к началу внутренней итерации. Но в случае 3, continue label1 прерывает внутреннюю итерацию и внешнюю итерацию, все пути ведут к label1. Затем фактически продолжаются итерации, но начиная со внешней итерации. В случае 4, break label1 также прерывает все пути к метке label1, но не происходит повторного входа в итерацию. Реально происходит прерывание обеих итераций.

Вот пример использования цикла for:

//: c03:LabeledFor.java
// "Помеченный цикл for" в Java.

public class LabeledFor {
  public static void main(String[] args) {
    int i = 0;
    outer: // Здесь не может быть инструкций
    for(; true ;) { // бесконечный цикл
      inner: // Здесь не может быть инструкций
      for(; i < 10; i++) {
        prt("i = " + i);
        if(i == 2) {
          prt("continue");
          continue;
        }
        if(i == 3) {
          prt("break");
          i++; // В противном случае i никогда
               // не получит инкремент.
          break;
        }
        if(i == 7) {
          prt("continue outer");
          i++; // В противном случае i никогда
               // не получит инкремент.
          continue outer;
        }
        if(i == 8) {
          prt("break outer");
          break outer;
        }
        for(int k = 0; k < 5; k++) {
          if(k == 3) {
            prt("continue inner");
            continue inner;
          }
        }
      }
    }
    // Здесь нельзя использовать break или continue
    // с меткой
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

Здесь используется метод prt( ), который был использован в других примерах.

Обратите внимание, что break прерывает цикл for, и при этом не происходит инкрементации, пока не будет завершен проход цикла for. Так как break пропускает выражение инкремента, инкремент выполняется прямо в случае i == 3. Инструкция continue outer в случае i == 7 также переходит к началу цикла и также пропускает инкремент, так что нужно инкрементировать в ручную.

Вот результат работы:

i = 0
continue inner
i = 1
continue inner
i = 2
continue
i = 3
break
i = 4
continue inner
i = 5
continue inner
i = 6
continue inner
i = 7
continue outer
i = 8
break outer

Если не использовать инструкцию break outer, то нет способа выйти во внешний цикл из внутреннего цикла, так как break сам по себе прерывает только самый внутренний цикл. (То же самое верно и для continue.)

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

Вот демонстрация использования помеченных инструкций break и continue с циклом while:

//: c03:LabeledWhile.java
// "Помеченный цикл while" в Java.

public class LabeledWhile {
  public static void main(String[] args) {
    int i = 0;
    outer:
    while(true) {
      prt("Outer while loop");
      while(true) {
        i++;
        prt("i = " + i);
        if(i == 1) {
          prt("continue");
          continue;
        }
        if(i == 3) {
          prt("continue outer");
          continue outer;
        }
        if(i == 5) {
          prt("break");
          break;
        }
        if(i == 7) {
          prt("break outer");
          break outer;
        }
      }
    }
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

Те же правила применимы для while:

  1. Обычный continue переводит в начало самого внутреннего цикла и продолжает выполнение.
  2. Помеченный continue переходит к метке и вновь входит в цикл, расположенный сразу за этой меткой.
  3. break “выбрасывает в низ” цикла.
  4. Помеченный break выбрасывает в низ после конца цикла, у которого объявлена метка.

Вывод этого метода становится достаточно ясным:

Outer while loop
i = 1
continue
i = 2
i = 3
continue outer
Outer while loop
i = 4
i = 5
break
Outer while loop
i = 6
i = 7
break outer

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

В газетной статье Dijkstra “Goto considered harmful”, то, против чего он действительно возражал - это метки, а не goto. Он заметил, что число ошибок увеличивается с увеличением числа меток в программе. Метки и переходы делают программу трудной для статического анализа, так как это вводит в программу циклы графов исполнения. Обратите внимание, что метки Java не испытывают этой проблемы, так как они ограничены своим местом и не могут быть использованы для передачи управления другим образом. Также интересно заметить, что это тот случай, когда особенности языка становятся более полезными при ограничении инструкции.

switch

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

switch(целочисленный_переключатель) {
  case целочисленное_значение1 : инструкция; break; 
  case целочисленное_значение2 : инструкция; break;
  case целочисленное_значение3 : инструкция; break;
  case целочисленное_значение4 : инструкция; break;
  case целочисленное_значение5 : инструкция; break; // 
  ... default: инструкция; }

Целочисленный_переключатель - это выражение, которое производит целое значение. switch сравнивает результат целочисленного_переключателя с каждым целочисленным_значением. Если он находит совпадение, выполняется соответственная инструкция (простая или составная). Если нет совпадений, выполняется инструкция default.

Обратите внимание, что в приведенном выше определении каждый case заканчивается break, который является причиной того, что выполнение перепрыгивает на конец тела switch. Это традиционный способ для построения инструкции switch, но break не обязателен. Если его нет, выполняется код случая следующей инструкции, пока не обнаружится break. Хотя обычно поведение такого рода не нужно, это может быть полезно для опытных программистов. Обратите внимание, что последняя инструкция, следующая за default, не имеет break, потому что выполнение переходит туда же, куда оно и так перейдет после break. Вы можете поместить break в конце инструкции default без всякого ущерба, если вы решите, что это важно для стиля.

Инструкция switch - это ясный способ для реализации множественного выбора (т.е., выбора из большого числа разных путей выполнения), но это требует переключателя, при вычислении которого получается целое значение типа int или char. Если вы хотите использовать, например, строку или число с плавающей точкой в качестве переключателя, они не будут работать в инструкции switch. Для не целых типов вы должны использовать серию инструкций if.

Вот пример, в котором в случайном порядке создаются буквы и проверяются являются ли они гласными или согласными:

//: c03:VowelsAndConsonants.java
// Демонстрация инструкции switch.

public class VowelsAndConsonants {
  public static void main(String[] args) {
    for(int i = 0; i < 100; i++) {
      char c = (char)(Math.random() * 26 + 'a');
      System.out.print(c + ": ");
      switch(c) {
      case 'a':
      case 'e':
      case 'i':
      case 'o':
      case 'u':
                System.out.println("vowel");
                break;
      case 'y':
      case 'w':
                System.out.println(
                  "Sometimes a vowel");
                break;
      default:
                System.out.println("consonant");
      }
    }
  }
} ///:~

Так как Math.random( ) генерирует значения в пределах от 0 до 1, вам необходимо только умножить его на верхний предел границы чисел, которые вы хотите производить (26 для букв алфавита) и прибавлять смещение для установки нижней границы.

Хотя здесь используется переключение для символов (char), инструкция switch на самом деле использует целое значение для символов. Символы в одинарных кавычках в инструкциях case также производят целочисленные значения, которые также используются для сравнения.

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

Детали расчета

Инструкция:

char c = (char)(Math.random() * 26 + 'a');

заслуживает более подробного рассмотрения. Math.random( ) производит double, так что значение 26 переводится в double для выполнения умножения, которое также производит double. Это означает, что ‘a’ должно переводится в double для выполнения сложения. Результат типа double переводится назад к char с помощью приведения.

Что делает приведение к char? То есть, если вы имеете значение 29.7 и вы приводите его к char, будет ли результирующее значение равно 30 или 29? Ответ можно найти в этом примере:

//: c03:CastingNumbers.java
// Что случается, когда вы приводите float 
// или double к целому значению?

public class CastingNumbers {
  public static void main(String[] args) {
    double
      above = 0.7,
      below = 0.4;
    System.out.println("above: " + above);
    System.out.println("below: " + below);
    System.out.println(
      "(int)above: " + (int)above);
    System.out.println(
      "(int)below: " + (int)below);
    System.out.println(
      "(char)('a' + above): " +
      (char)('a' + above));
    System.out.println(
      "(char)('a' + below): " +
      (char)('a' + below));
  }
} ///:~

Вот результат:

above: 0.7
below: 0.4
(int)above: 0
(int)below: 0
(char)('a' + above): a
(char)('a' + below): a

Так что ответ такой: приведение float или double к целому значению происходит простым обрезанием.

Второй вопрос относительно Math.random( ). Тут производится значение от нуля до одного, включая или не включая значение ‘1’? На математическом языке: (0,1) или [0,1], или (0,1] или [0,1)? (Прямоугольная скобка означает “включая”, а круглая скобка означает “не включая”.) И в этот раз тестовая программа поможет получить ответ:

//: c03:RandomBounds.java
// Может ли Math.random() производить 0.0 и 1.0?

public class RandomBounds {
  static void usage() {
    System.out.println("Usage: \n\t" +
      "RandomBounds lower\n\t" +
      "RandomBounds upper");
    System.exit(1);
  }
  public static void main(String[] args) {
    if(args.length != 1) usage();
    if(args[0].equals("lower")) {
      while(Math.random() != 0.0)
        ; // Продолжаем пробовать
      System.out.println("Produced 0.0!");
    } 
    else if(args[0].equals("upper")) {
      while(Math.random() != 1.0)
        ; // Продолжаем пробовать
      System.out.println("Produced 1.0!");
    } 
    else 
      usage();
  }
} ///:~

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

java RandomBounds lower

или

java RandomBounds upper

В обоих случаях вы можете прервать программу в ручную в том случае, если окажется, что Math.random( ) никогда не производит 0.0 или 1.0. Но такой экспериметн может обмануть. Если вы узнаете, [26] что есть примерно 262 различных значений типа double в пределах от 0 до 1, вероятность достижения любого единичного значения экспериментально может превышать время жизни компьютера и даже экспериментатора. Считается, что 0.0 - включается в выходные значения Math.random( ). или, на математическом языке, [0,1).

Резюме

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

Упражнения

Решения для выбранных управжнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.

  1. Есть два примера в разделе, озаглавленном “Предшествование” в начале этой главы. Соберите эти примеры в программу и посмотрите почему они дают разный результат.
  2. Поместите методы ternary( ) и alternative( ) в работающую программу.
  3. Из разделов, озаглавленных “if-else” и “return”, поместите методы test( ) и test2( ) в работающую программу.
  4. Напишите программу, которая печатает значения от одного до 100.
  5. Измените упражнение 4 так, чтобы программа выходила при использовании ключавого слова break на значении 47. Попробуйте вместо этого использовать return.
  6. Напишите функцию, получающую два аргумента String и использующую все логические сравнения для сравнения двух строк и печати результата. Для == и != также выполните проверку equals( ). В main( ) вызовите вашу функцию с несколькими разными объектами String.
  7. Напишите программу, которая генерирует 25 случайных значений. Для каждого значения используйте инструкцию if-then-else, чтобы узнать, является ли число больше, меньше или равным другому случайному числу.
  8. Измените упражнение 7 так, чтобы ваш код был окружен “бесконечным” циклом while. Она будет работать до тех пор, пока вы не прервете ее с клавиатуры (обычно при нажатии Control-C).
  9. Напишите программу, которая использует два вложенных цикла for и оператор остатка от деления (%) для определения простых чисел для печати (целых чисел, которые не точно делятся на любое число за исключением себя и 1).
  10. Создайте инструкцию switch, которая напечатает сообщение для каждого варианта, и помесите switch в цикл for, который опробует каждый случай. Поместите break после каждого случая и проверьте это, затем уберите break и посмотрите, что случится.

[25] John Kirkham пишет: Я начал заниматься компьютерами в 1962, испоьзуя FORTRAN II для IBM 1620. В то время и на протяжении 1960-х и до 1970-х FORTRAN был языком с буквами верхнего регистра. Это, вероятно, произошло потому, что многие вводные устройства были старыми терминальными устройствами, которые использовали 5-ти битный код Боде, в котором не было маленьких букв. ‘E’ в экспоненциальной записи было также всегда в верхнем регистре и никогда не путалось с основанием натурального логарифма ‘e’, которое всегда в нижнем регистре. ‘E’ просто оставили для экспоненты, которая используется в обычной системе счисления — обычно это 10. В то время восмеричная система также широко использовалась программистами. Хотя я никогда не видел ее использования, если я видел восмеричное число в экспоненциальной записи, я рассматривал его с основанием 8. Первое время, помня вид экспоненциального использования ‘e’ в нижнем регистре, позднее 1970 я также находил это запутывающим. Проблема возникла, поскольку нижний регистр пришел в FORTRAN не с самого начала. Мы на самом деле имели функции, в которых можно было использовать натуральный логарифм, но они все были в верхнем регистре.

[26] Chuck Allison пищет: “Полное количество чисел в системе чисел с плавающей точкой:
2(M-m+1)b^(p-1) + 1
где b - основание (обычно 2), p - показатель (число в мантиссе), M - наибольшая експонента, а m - наименьшая экспонента. IEEE 754 использует:
M = 1023, m = -1022, p = 53, b = 2
так что полное количество чисел:
2(1023+1022+1)2^52
= 2((2^10-1) + (2^10-1))2^52
= (2^10-1)2^54
= 2^64 - 2^54
Половина этих чисел (соответствующих экспоненте в пределах [-1022, 0]) меньше 1 по величине (и положительные, и отрицательные), так что 1/4 этого выражения, или 2^62 - 2^52 + 1 (примерно 2^62) в диапазоне [0,1). Смотри мою статью на http://www.freshsources.com/1995006a.htm (в конце текста)”.

* Конечно в английском языке эта мнемоника звучит по другому: Ulcer Addicts Really Like C A lot”. (Язвы наркомана во многом похожи на C).

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]