Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

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

2: Все есть объекты

Хотя он основывается на C++, Java более “чистый” объектно-ориентированный язык.

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

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

Вы управляете объектами через ссылки

Каждый язык программирования вкладывает совой собственный смысл в управление данными. Иногда программисты должны постоянно осознавать, какого типа управление происходит. Управляете ли вы объектом напрямую, или вы имеете дело с определенного рода непрямым представлением (указатель в C и C++), которое должно трактоваться в специальном синтаксисе?

Все это упрощено в Java. Вы трактуете все как объекты, так что здесь однородный синтаксис, который вы используете везде. Хотя вы трактуете все, как объекты, идентификатор, которым вы манипулируете, на самом деле является “ссылкой” на объект [20]. Вы можете вообразить эту сцену, как телевизор (объект) с вашим пультом дистанционного управление (ссылка). Столько, сколько вы держите эту ссылку, вы имеете связь с телевизором, но когда что-то говорит: “измените канал” или “уменьшите звук”, то, чем вы манипулируете, это ссылка, которая производит модификацию объекта. Если вы хотите ходить по комнате и все равно хотите управлять телевизором, вы берете пульт/ссылку с собой, но не телевизор.

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

String s;

Но здесь вы создаете только ссылку, а не объект. Если вы решите послать сообщение для s в этом месте, то вы получите ошибку (времени выполнения), потому что s ни к чему не присоединено (здесь нет телевизора). Безопасная практика, поэтому, всегда инициализировать ссылку, когда вы создаете ее:

String s = "asdf";

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

Вы должны создавать все объекты

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

String s = new String("asdf");

Это значит не только “Создать мне новый String”, но это также дает информацию о том, как создать String, указывая инициализирующую строку.

Конечно, String - это не только существующий тип. Java пришла с полноценными готовыми типами. Что более важно, так это то, что вы можете создать свои собственные типы. Фактически, это основной род деятельность при программировании на Java, и это то, что вы будите учиться делать в оставшейся части книги.

Где живет хранилище

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

  1. Регистры. Это самое быстрое хранилище, потому что оно существует в месте, отличном от других хранилищ: внутри процессора. Однако число регистров сильно ограничено, так что регистры резервируются компилятором в соответствии с его требованиями. Вы не имеете прямого контроля, и при этом вы не видите никакого свидетельства в вашей программе, что регистры вообще существуют.
  2. Стек. Он расположен в области обычной RAM (память произвольного доступа - random-access memory), но имеет прямую поддержку процессора через указатель стека. Указатель стека перемещается вниз при создании новой памяти, и перемещается вверх при освобождении памяти. Это чрезвычайно быстрый и эффективный способ для выделения хранилища, второй после регистров. Компилятор Java должен знать во время создания программы точный размер и продолжительность жизни всех данных, которые хранятся в стеке, потому что он должен генерировать код для перемещения указателя стека вверх и вниз. Это ограничение сказывается на гибкости ваших программ, так что пока хранилище Java существует в стеке — обычно, для ссылок на объекты — объекты Java не помещаются в стек.
  3. Куча. Это пул памяти общего назначения (также в области RAM), где живут объекты Java. Главная прелесть кучи, в отличие от стека, в том, что компилятору нет необходимости знать, как много места необходимо выделить из кучи для хранилища или как долго это хранилище будет оставаться в куче. Поэтому, большой плюс для гибкости при создании хранилища в куче. Когда бы вам ни понадобилось создавать объект, вы просто пишите код для его создания, используя new, а когда такой код выполняется, хранилище выделяется в куче. Конечно, вы платите за эту гибкость: это занимает больше времени при выделении хранилища в куче, чем при выделении хранилища в стеке (если бы вы могли создать объект в стеке в Java, как вы это можете в C++).
  4. Статическое хранилище. “Статическое” здесь используется в смысле “в фиксированном месте” (хотя это тоже в RAM). Статическое хранилище содержит данные, которые доступны в течение всего времени выполнения программы. Вы можете использовать ключевое слово static, чтобы указать, что определенный элемент объекта - статический, но Java объект никогда не помещается в статическое хранилище.
  5. Хранилище констант. Константные значения часто помещаются прямо в код программы, что является безопасным, так как они никогда не могут измениться. Иногда константы огораживают себя так, что они могут быть по выбору помещены в память только для чтения (ROM).
  6. Не RAM хранилище. Если данные живут полностью вне программы, они могут существовать, пока программа не работает, вне управления программы. Два основных примера - это потоковые объекты, в которых объекты переведены в поток байтов, обычно для посылки на другую машину, и объекты представления, в которых объекты помещаются на диск, так что они сохраняют свое состояние, даже когда программа завершена. Фокус этих типов хранилищ в переводи объектов во что-то, что может существовать на другом носителе, и даже могут быть воскрешены в обычный объект в RAM, когда необходимо. Java обеспечивает поддержку для легковесной живучести, и будущие версии Java могут предлагать более полное решение для живучести.

Особый случай: примитивные типы

Есть группа типов, имеющих особое обращение; вы можете думать о них, как о “примитивных” типах, которые вы достаточно часто используете в вашем программировании. Причина специального использования в том, что создание объектов с помощью new —особенно маленьких, простые переменных — не очень существенно, поскольку new помещает объекты в кучу. Для этих типов Java возвращается к подходу, принятому в C и C++. Так что, вместо создания переменной с использованием new, “автоматические” переменные создаются не по ссылке. Переменная хранит значение, и оно помещается в стек, так как это более эффективно.

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

Примитивный тип
Размер
Минимум
Максимум
Тип оболочки
boolean Boolean
char 16-бит Unicode 0 Unicode 216- 1 Character
byte 8-bit -128 +127 Byte
short 16-bit -215 +215 — 1 Short
int 32-bit -231 +231 — 1 Integer
long 64-bit -263 +263—1 Long
float 32-bit IEEE754 IEEE754 Float
double 64-bit IEEE754 IEEE754 Double
void Void

 

Все числовые типы знаковые, так что не ищите беззнаковые типы.

Размер boolean типов точно не определено; только указано, что они способны принимать литерные значения true или false.

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

char c = 'x';
Character C = new Character(c);

Или вы также моги использовать:

Character C = new Character('x');

Обоснования для этого действия будет дано в последующих главах.

Числа высокой точности

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

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

BigInteger поддерживают целые числа произвольной точности. Это означает, что вы можете точно представить значение целого числа любого размера без потерь любой информации во время операций.

BigDecimal для чисел с фиксированной точкой произвольной точности; вы можете использовать это, например, для точных денежных расчетов.

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

Массивы в Java

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

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

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

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

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

Вам никогда не нужно уничтожать объекты

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

Ограничивание

Большинство процедурных языков имеют концепцию границ. Они определяют и видимость, и время жизни имен, определенных в таких границах. В C, C++ и Java границы определяются расстановкой фигурных скобок {}. Так, например:

{
  int x = 12;
  /* доступно только  x */
  {
    int q = 96;
    /* доступны и x, и q */
  }
  /* Доступно только x */
  /* q “за границами” */
}

Переменная, определенная внутри границ доступна только до конца этой границы.

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

Обратите внимание, что в не можете сделать следующее, хотя это разрешено в С и C ++:

{
  int x = 12;
  {
    int x = 96; /* недопустимо */
  }
}

Компилятор объявит, что переменная x уже определена. Таким образом, C и C++ способны “прятать” переменные в больших границах, что не позволяется в Java, поскольку разработчики подумали, что это будет запутывать программы.

Границы объектов

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

{
  String s = new String("a string");
} /* конец блока */

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

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

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

Создание новых типов данных: классов

Если все - это объекты, что определяет, как выглядит и ведет себя объект определенного класса? Или, другими словами, что основывает тип объекта? Вы можете ожидать здесь ключевого слова “type”, и, конечно, это бы имело смысл. Однако исторически сложилось, что большинство объектно-ориентированных языков используют ключевое слово class, которое означает: “Я говорю тебе, как выглядит новый тип объекта”. За ключевым словом class (которое является настолько общим, что оно не будет поощряться в этой книге) следует имя нового типа. Например:

class ATypeName { /* Здесь помещается тело класса */ }

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

ATypeName a = new ATypeName();

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

Поля и методы

Когда вы определяете класс (а все, что вы делаете в Java - это определение классов, создание объектов этих классов и посылка сообщений этим объектам), вы можете поместить два типа элементов в ваш класс? Члены-данные (иногда называемые полями) и члены-функции (обычно называемые методами). Члены-данные - это объекты любого типа, с которыми вы можете взаимодействовать через ссылку. Они также могут быть примитивными типами (которые не являются ссылками). Если это ссылка на объект, вы должны инициализировать эту ссылку, присоединив ее к реальному объекту (используя new, как показано ранее), в специальной функции, называемой конструктором (полностью описано в Главе 4). Если это примитивный тип, вы можете инициализировать его напрямую в точке определения в классе. (Как вы увидите позже, ссылки также могут быть инициализированы в месте определения.)

Каждый объект держит свое собственное место для своих членов-данных; члены-данные не делятся между объектами. Здесь приведен пример класса с какими-то членами-данными:

class DataOnly {
  int i;
  float f;
  boolean b;
}

Это класс не делает ничего, но вы можете создать объект:

DataOnly d = new DataOnly();

Вы можете присвоить значение члену-данному, но вы сначала должны узнать, как обратиться к члену объекта. Это совершается, начиная с имени ссылки объекта, далее следует разделитель (точка), далее следует имя члена внутри объекта:

objectReference.member

Например:

d.i = 47;
d.f = 1.1f;
d.b = false;

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

myPlane.leftTank.capacity = 100;

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

Значения по умолчанию примитивных членов

Когда примитивные типы данных являются членами класса, они гарантировано получают значения по умолчанию, если вы их не инициализировали:

Тип примитива Умолчание
boolean false
char ‘\u0000’ (null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

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

Эта гарантия не применяется к “локальным” переменным, которые не являются полями класса. Так что, если в пределах определения функции вы имеете:

int x;

Затем x получит произвольное значение (как и в C, и в C++); она не будет автоматически инициализирована нулем. Вы несете ответственность за присвоение и соответствие значения прежде, чем вы будете использовать x. Если вы забудете, Java определенно лучше C++: вы получите ошибку времени компиляции, которая скажет вам, что переменная возможно не инициализирована. (Многие C++ компиляторы предупредят вас об отсутствии инициализации переменной, но в Java - это ошибка.)

Методы, аргументы и
возвращаемое значение

До этих пор термин функция использовался для описания поименованной процедуры. Термин, наиболее часто используемый в Java, это метод, как “способ что-то сделать”. Если вы хотите, вы можете продолжать думать в терминах функций. На самом деле, это только семантическое различие, но далее в этой книге будет использоваться “метод”, а не “функция”.

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

Фундаментальные части метода - это его имя, аргументы, возвращаемое значение и тело. Посмотрите на основную форму:

returnType methodName( /* список аргументов */ ) {
  /* Тело метода */
}

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

Методы в Java могут создаваться только как часть класса. Метод может быть вызван только для объекта, [21] а этот объект должен быть способен выполнить этот вызов метода. Если вы попытаетесь вызвать неправильный метод для объекта, вы получите сообщение об ошибке во время компиляции. Вы вызываете метод для объекта по имени объекта, за которым следует разделитель (точка), а далее идет имя метода и список его аргументов, как здесь: objectName.methodName(arg1, arg2, arg3). Например, предположим, что вы имеете метод f( ) , который не принимает аргументов и возвращает значение типа int. Тогда, если вы имеете объект с именем a для которого может быть вызван f( ) , вы можете сказать:

int x = a.f();

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

Этот вызов метода часто называется посылкой сообщения объекту. В приведенном выше примере сообщение - f( ) , а объект - a. Объектно-ориентированное программирование часто резюмирует, как просто “посылку сообщения объекту”.

Список аргументов

Список аргументов метода определяет, какую информацию вы передаете в метод. Как вы можете догадаться, это информация — как и все в Java — принимает форму объекта. Таким образом, то, что вы должны указать в списке аргументов - это типы объектов для передачи и имена для использования каждого из них. Как и в любой ситуации в Java, где вы кругом видите объекты, на самом деле вы передаете ссылки [22]. Однако, тип ссылки должен быть правильным. Если аргумент, предположим, String, то, что вы передаете должно быть строкой.

Относительно метода, который получает String как аргумент. Здесь приведено определение, которое должно быть помещено в определение класса для компиляции:

int storage(String s) {
  return s.length() * 2;
}

Этот метод говорит вам как много байт требуется для хранения информации в обычном String. (Каждый char в String - это 16 бит длины, или два байта, для поддержки символов Unicode.) Аргумент типа String и он называется s. Как только s передается в метод, вы можете трактовать его, как и любой другой объект. (Вы можете посылать ему сообщения.) Здесь вызывается метод length( ), который является одним из методов для String; он возвращает число символов в строке.

Вы также можете увидеть использование ключевого слова return, которая делает две вещи. Во-первых, оно означает “покинуть метод, Я закончил”. Во-вторых, если метод произвел значение, это значение помещается справа сразу за выражением return. В этом случае, возвращаемое значение производится путем вычисления выражения s.length( ) * 2.

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

boolean flag() { return true; }
float naturalLogBase() { return 2.718f; }
void nothing() { return; }
void nothing2() {}

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

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

Построение Java программы

Есть несколько других проблем, которые вы должны понимать, прежде чем увидите свою первую Java программу.

Видимость имен

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

Java способен предотвратить все это, выбрав свежий подход. Для производства недвусмысленных имен для библиотеки, используется спецификатор, мало чем отличающийся от доменных имен Internet. Фактически, создатели Java хотят использовать ваши доменные имена Internet в обратном порядке, так как это гарантирует их уникальность. Так как мое доменное имя BruceEckel.com, мои библиотеки утилит foibles будет называться com.bruceeckel.utility.foibles. После того, как вы развернете доменное имя, точки предназначены для представления директорий.

В Java 1.0 и Java 1.1 доменное расширение com, edu, org, net, и т.д. по соглашению печатаются большими буквами, так что библиотека будет выглядеть: COM.bruceeckel.utility.foibles. Однако, отчасти из-за разработки Java 2, это стало причиной проблемы и теперь все имя пакета пишется маленькими буквами.

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

Использование других компонентов

Когда бы вы ни пожелали использовать предопределенные классы в вашей программе, компилятор должен знать, где они расположены. Конечно, класс может существовать в том же самом файле исходного кода, из которого он вызывается. В этом случае вы просто используете класс — даже если класс определен дальше по файлу. Java устранит проблему “ранней ссылки”, так что вам нет необходимости думать об этом.

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

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

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

import java.util.ArrayList;

чтобы сказать компилятору, что вы хотите использовать Java класс ArrayList. Однако util содержит несколько классов, и вы можете использовать некоторые из них, не объявляя их точно. Это легче всего выполнить, используя ‘*’, чтобы указать чистую карту:

import java.util.*;

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

Ключевое слово static

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

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

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

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

class StaticTest {
    static int i = 47;
}

Теперь, даже если вы сделаете два объекта StaticTest, будет только одна часть хранилища для StaticTest.i. Оба объекта будут разделять одну и ту же i. Рассмотрим:

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

В этом месте и st1.i, и st2.i имеют одно и то же значение 47, так как они ссылаются на одну и ту же область памяти.

Есть два способа сослаться на статическую переменную. Как показано выше, вы можете назвать ее через объект, например, сказав st2.i. Вы также можете прямо сослаться через имя класса, что вы не можете сделать с не статическими членами. (Это предпочтительный способ сослаться на статическую переменную, та как это подчеркивает, что переменная имеет статическую природу.)

StaticTest.i++;

Оператор ++ инкрементирует переменную. В этом месте и st1.i, и st2.i будут иметь значение 48.

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

class StaticFun {
  static void incr() { StaticTest.i++; }
}

Вы можете увидеть, что метод incr( ) класса StaticFun инкрементирует статическую переменную i. Вы можете вызвать incr( ) обычным способом, через объект:

StaticFun sf = new StaticFun();
sf.incr();

Или, потому что incr( ) - статический метод, вы можете вызвать его прямо через класс:

StaticFun.incr();

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

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

Ваша первая Java программа

Наконец, здесь приведена программа. [23] Она начинается с печати строки, а затем даты, используя класс Date из стандартной библиотеки Java. Обратите внимание, что здесь приведен дополнительный стиль комментариев: ‘//’, который объявляет комментарий до конца строки:

// HelloDate.java
import java.util.*;

public class HelloDate {
  public static void main(String[] args) {
    System.out.println("Hello, it's: ");
    System.out.println(new Date());
  }
}

В начале каждого файла программы вы должны поместить объявление import об использовании любых дополнительных классов, которые вам нужны в этом файле. Обратите внимание на слово “дополнительные”; это потому, что есть определенные библиотеки классов, которые подключаются автоматически к любому Java файлу: java.lang. Запустите ваш Web броузер посмотрите документацию от Sun. (Если вы не загрузили ее с java.sun.com или не установили документацию Java, сделайте это сейчас.) Если вы посмотрите на первую страницу, вы увидите все различные библиотеки классов, которые поставляются с Java. Выберите java.lang. Появится список всех классов, являющихся частью этой библиотеки. Так как java.lang косвенно включается в каждый файл с Java кодом, эти классы поддерживаются автоматически. В списке классов java.lang нет класса Date, это означает, что вы должны импортировать другую библиотеку, чтобы использовать его. Если вы не знаете библиотеку, где есть определенный класс, или если вы хотите просмотреть все классы, вы можете выбрать “Дерево” в документации Java. Теперь вы можете найти каждый единичный класс, который поставляется с Java. Теперь вы можете использовать функцию поиска броузера для нахождения Date. Когда вы сделаете это, вы увидите в списке java.util.Date, что позволяет вам узнать, что она в библиотеке util и что вы должны написать import java.util.* для использования Date.

Если вы вернетесь к началу, выберите java.lang, а затем System, вы увидите, что класс System имеет несколько полей, и если вы выберите out, вы обнаружите, что это объект static PrintStream. Так как это static, вам нет необходимости создавать что-либо. Объект out всегда здесь и вы можете просто использовать его. Что вы можете сделать с этим объектом out, определяется типом: PrintStream. Удобство в том, что PrintStream в описании показан как гиперссылка, так что если вы кликните на ней, вы увидите все методы, которые вы можете вызвать для PrintStream. Это не все и подробнее будет описано позже в этой книге. Мы же сейчас интересуемся println( ), которая подразумевает “печатать то, что я передаю, на консоль и выполнять переход на новую строку”. Таким образом, в Java программе вы пишите то, что хотите сказать в виде System.out.println(“things”) в любом месте, где бы вы ни захотели напечатать что-нибудь на консоль.

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

public static void main(String[] args) {

Ключевое слово public означает, что метод доступен извне (детально описано в Главе 5). Аргументом main( ) является массив объектов String. args не используется в этой программе, но компилятор Java настаивает, чтобы он был, потому что он сохраняет аргументы вызова командной строки.

Строка, печатающая дату, мало интересна:

System.out.println(new Date());

Относительно аргумента: объект Date создается только для передачи его значения в println( ). Как только это выражение закончится, Date становится ненужным и сборщик мусора может пройтись и собрать его в любое время. Нам нет необходимости заботиться о его очистке.

Компиляция и запуск

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

Сходите в Internet на java.sun.com. Здесь вы найдете информацию и ссылки, которые проведут вас через процесс скачивания и установки JDK для вашей платформы.

Когда JDK установлен, и вы проставили информацию о путях на своем компьютере, чтобы он нашел javac и java, скачайте и распакуйте исходник для этой книге (вы можете найти его на CD ROM, прилагающийся к этой книге, или на www.BruceEckel.com). Вы получите каталог для каждой главы этой книги. У вас создадутся каталоги для каждой главы этой книги. Перейдите в каталог c02 и наберите:

javac HelloDate.java

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

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

java HelloDate

и вы получите сообщение и дату в качестве вывода.

Этот процесс вы можете использовать для компиляции и запуска каждой программы в этой книге. Однако вы увидите, что исходный код для этой книги также имеет файл, называемый makefile в каждой главе и есть команда “make” для автоматического построения файлов этой главы. Посмотрите Web страницу этой книги на www.BruceEckel.com для получения более подробной информации об использовании makefiles.

Комментарии и встроенная документация

Есть два типа комментариев в Java. Первый - традиционный комментарий в стиле C, который был унаследован из C++. Этот комментарий начинается с /* и распространяется на много линий, пока не встретится */. Обратите внимание, что многие программисты начинают каждую линию продолжающихся комментариев с *, так что вы часто можете видеть:

/* Это комментарий,
*  который распространяется
*  на несколько строк
*/

Однако помните, что все внутри /* и */ игнорируется, так что это ничем не отличается от:

/* Это комментарий, который
распространяется на несколько строк */

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

// Это однострочный комментарий

Комментарий-Документация

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

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

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

Синтаксис

Все команды javadoc встречаются только внутри комментариев /**. Комментарий заканчивается */, как обычно. Есть два основных способа использовать javadoc: вставление HTML или использование “ярлыков документации”. Ярлыки документации являются командами, которые начинаются с ‘@’, которая помещается с начала строки комментария. (Однако лидирующая ‘*’ игнорируется.)

Есть три “типа” комментариев документации, которые соответствуют элементам, предшествующий комментарию: класс, переменная или метод. Таким образом, компоненты класса появляются прямо перед определением класса; компонент переменная появляется прямо перед определением переменной, а компонент метода появляется прямо перед определением метода. Как простой пример:

/** Компонент - класс */
public class docTest {
  /** Компонент - переменная */
  public int i;
  /** Компонент - метод */
  public void f() {}
}

Обратите внимание, что javadoc будет обрабатывать компоненты документации только для public и protected членов. Компоненты для private и “дружественных” членов (смотрите Главу 5) игнорируются, и вы не увидите их в выводе. (Однако вы можете использовать флаг -private для включения private членов наряду с остальными.) Это имеет смысл, так как только public и protected члены доступны извне файла, которые просматривают программисты-клиенты. Однако все комментарии для class включаются в выходной файл.

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

Вставка HTML

Javadoc пропускает HTML команды для генерации HTML документа. Это позволяет вам использовать HTML; однако главный мотив состоит в позволении вам форматировать код, например так:

/**
* <pre>
* System.out.println(new Date());
* </pre>
*/

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

/**
* Вы можете <em>даже</em> вставить список:
* <ol>
* <li> Первый элемент
* <li> Второй элемент
* <li> Третий элемент
* </ol>
*/

Обратите внимание, что внутри комментариев-документации звездочки в начале строки выбрасываются javadoc вместе с начальными пробелами. Javadoc переформатирует все так, что документ принимает внешний вид стандартного. Не используйте заголовки, такие как <h1> или <hr> в качестве встраиваемого HTML, потому что javadoc вставляет свои собственные заголовки, и вы можете запутаться в них.

Все компоненты документации — класс, переменная и метод — могут поддерживать вставку HTML.

@see: ссылка на другой класс

Все три типа компонентов документации (класс, переменная и метод) могут содержать ярлык @see, который позволяет ссылаться на документацию другого класса. Javadoc генерирует HTML с ярлыком @see, как гиперссылку на другой документ. Форма:

@see classname
@see fully-qualified-classname
@see fully-qualified-classname#method-name

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

Ярлыки документации класса

Наряду со встроенным HTML и ссылками @see, документация класса может включать ярлыки для информации о версии и имени автора. Документация класса также может быть использована для интерфейса (смотрите Главу 8).

@version

Вот форма:

@version version-information

в которой version-information - это любая важная для вас информация., которую вы хотите включить. Когда вносится флаг -version в командную строку javadoc, информация о номере версии специально будет включена в генерируемый HTML документ.

@author

Вот форма:

@author author-information

в которой author-information это, предположительно, ваше имя, но она так же может включать ваш электронный адрес или любую другую подходящую информацию. Когда флаг -author вносится в командную строку javadoc, информация об авторе специально включается в генерируемый HTML документ.

Вы можете иметь несколько ярлыков авторства для всего списка авторов, но они будут помещены последовательно. Вся информация об авторах будет собрана вместе в один параграф генерируемого HTML.

@since

Этот флаг позволяет вам указывать версию того кода, с которого началось использование определенной особенности. Вы увидите, что он появится в HTML документации Java для указания какая версия JDK используется.

Ярлыки документации переменных

Документация переменных может включать только встроенный HTML код и ссылки @see.

Ярлыки документации методов

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

@param

Эта форма:

@param parameter-name description

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

@return

Эта форма:

@return description

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

@throws

Исключения будут продемонстрированы в Главе 10, но если коротко: это объекты, которые могут быть “выброшены” из метода, если метод окончится неудачей. Хотя только один объект исключение может появиться при вызове метода, обычно метод может производить любое число исключений различных типов, все они требуют описания. Форма ярлыка исключения следующая:

@throws fully-qualified-class-name description

в которой fully-qualified-class-name дает вам уникальное имя класса исключения, который где-нибудь определен, а description (которое может продолжаться на последующих линиях) говорит вам, почему этот тип исключения может возникнуть при вызове метода.

@deprecated

Это используется для ярлыка особенностей, которые были заменены улучшенными особенностями. Ярлык deprecated советует вам больше не использовать эту определенную особенность, так как когда нибудь в будущем она будет удалена. Метод, помеченный как @deprecated заставляет компилятор выдавать предупреждение, если он используется.

Пример документации

Здесь снова приведена первая Java программа, на этот раз с добавленными комментариями-документацией:

//: c02:HelloDate.java
import java.util.*;

/** Первая программа - пример Thinking in Java.
 * Отображает строку и сегодняшнюю дату.
 * @author Bruce Eckel
 * @author www.BruceEckel.com
 * @version 2.0 
*/
public class HelloDate {
  /** Единственная точка входа для класса и приложения
   * @param args массив строк аргументов
   * @return Возвращаемого значения нет
   * @exception Исключения не выбрасываются
  */
  public static void main(String[] args) {
    System.out.println("Hello, it's: ");
    System.out.println(new Date());
  }
} ///:~

Первая строка файла использует мою собственную технику помещения ‘:’ как специальный маркер для строки комментария исходного имени файла. Эта строка содержит информацию о пути к файлу (в этом случае c02 указывает Главу 2), за которой следует имя файла [24]. Последняя строка также завершается комментарием, который означает конец исходного кода, который позволяет автоматически выбирать его из текста этой книги и проверять компилятором.

Стиль кодирования

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

class AllTheColorsOfTheRainbow { // ...

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

class AllTheColorsOfTheRainbow {
  int anIntegerRepresentingColors;
  void changeTheHueOfTheColor(int newHue) {
    // ...
  }
  // ...
}

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

Java код, который вы увидите в библиотеках от Sun, следует размещению открывающих-закрывающих фигурных скобок, как вы видите в этой книге.

Резюме

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

Упражнения

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

  1. Следуя примеру HelloDate.java из этой главы, создайте программу “hello, world”, которая просто печатает выражение. Вам необходим один метод в вашем классе (метод “main” принимает выполнение, когда программа начинается). Не забудьте сделать его статическим и включить список аргументов, даже если вы не используете его, скомпилируйте программу с помощью javac и запустите ее, используя java. Если вы используете другую среду разработки, отличную от JDK, выучите, как скомпилировать и запустит программу в этой среде.
  2. Найдите фрагмент кода, вводящий ATypeName, и включите его в программу, затем скомпилируйте и запустите.
  3. Включите фрагмент кода DataOnly в программу, затем скомпилируйте и запустите.
  4. Измените упражнение 3 так, чтобы значение данных в DataOnly назначалось и печаталось в main( ).
  5. Напишите программу, которая включает и вызывает метод storage( ), определенный как фрагмент кода в этой главе.
  6. Включите фрагмент кода StaticFun в работающую программу.
  7. Напишите программу, которая печатает три аргумента, принимаемые из командной строки. Чтобы сделать это, вам нужно ввести индекс в массив командной строки Strings.
  8. Включите AllTheColorsOfTheRainbow пример в программу, затем скомпилируйте и запустите.
  9. Найдите код для второй версии HelloDate.java, который является просто примером документации. Запустите javadoc для файла и просмотрите результат в вашем Web броузере.
  10. Включите docTest в файл, затем скомпилируйте и пропустите его через javadoc. проверьте результат в вашем Web броузере.
  11. Добавьте HTML список элементов в документацию упражнения 10.
  12. Возьмите программу в упражнении 1 и добавьте в нее комментарии-документацию. Выберите эту документацию в HTML файл, используя javadoc и просмотрите его в вашем Web броузере.

[20] Это может быть озарением. Есть те, кто может сказать: “понятно, это указатель”, но это, предположительно, лежащая в основе реализация. Также, ссылки Java во многом похожи на ссылки C++, чем на указатели с их синтаксисом. В первой редакции книги я изобрел новый термин “handle”, потому что ссылки C++ и ссылки Java имеют некоторое важное различие. Я пришел из C++ и не хочу смущать программистов C++, которые будут составлять самую большую аудиторию для Java. Во второй редакции я решил, что “ссылка” будет наиболее часто используемым термином, и тот, кто переходит с C++ будет иметь много больше для копирования с этой терминологией ссылок, так что они могут прыгнуть сразу на обе ноги. Однако есть люди, которые не согласны даже с термином “ссылка”. Я читал в одной книге, где было “абсолютно неправильно сказано, что Java поддерживает передачу по ссылке”, потому что идентификаторы объектов Java (в соответствии с авторами) реально являются ссылками на объект”. И все реально передается по значению. Так что вы не передаете по ссылке. Вы “передаете ссылку объекта по значению”. Можно было приводить доводы в пользу точности таких замысловатых объяснений, но я думаю, что мой подход упрощает понимание концепции без того, чтобы повредить чему-нибудь (адвокаты языка могут утверждать, что я лгу вам, но я скажу, что я обеспечиваю подходящую абстракцию).

[21] статические методы, которые вы скоро выучите, могут вызываться для класса без объекта.

[22] С обычным исключениями вышеупомянутых “специальных” типов данных boolean, char, byte, short, int, long, float и double. В общем, однако, вы передаете объекты, которые реально означают, что вы передаете ссылку на объект.

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

    try {
      System.in.read();
    } catch(Exception e) {}

Это приостановит вывод, пока вы не нажмете “Enter” (или любую другую кнопку). Этот код включает концепцию, которая будет введена много позже в книге, так что вы не сможете понять его до этого места, но это будет работать.

[24] Инструмент, которым я создавал, используя Python (смотрите www.Python.org), использует эту информацию для выделения файлов кода, помещения их в соответствующие поддириктории и создание мейкфайла.

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