Московский Государственный Технический Университет

имени Н.Э.Баумана

Кафедра САПР


Java после C/C++

Федорук В.Г.
Содержание

Свойства языка Java

Язык программирования Java создан в 1995 г. фирмой Sun Microsystems (Java - название любимого сорта кофе разработчиков). Основная цель - создать несложный язык для программирования в Internet. Отсюда вытекают его главные свойства.

Примечание. Далее будет показано, что Java - весьма привлекательный язык для для новоиспеченных программистов-"чайников". Профессиональных программистов (a-la Д. Кнут) в нем могут привлечь в основном средства создания системного-независимого графического интерфейса пользователя.

Объектно-ориентированный

Основные элементы языка - объекты, являющиеся экземплярами классов. Класс - совокупность данных и методов (функций, процедур) обработки этих данных. Классы организованы в иерархию в виде "строго" дерева (множественного наследования в духе C++ в Java нет). Корнем иерархии служит класс Object. Необъекты в Java - это только простые числовые, символьные и булевы типы. Любая программа в Java - это класс (например, с методом main для приложения). Единицей компиляции и выполнения также является класс.

Классы в Java объединены в пакеты (package), облегчающие разработку программ той или иной функциональности. Обычно в пакет попадают классы из разных ветвей дерева иерархии классов. Примеры пакетов: java.io - классы ввода-вывода; java.awt - классы компонентов графического интерфейса пользователя; java.net - классы для организации сетевого взаимодействия.

Интерпретируемый

Исходный текст программы (класса) обрабатывается компилятором Java (javac), однако, в отличие от традиционных компиляторов (таких, как C и C++), он создает не объектный файл (например, формата ELF), содержащий машинные команды, а файл с последовательностью байт-кодов. На этапе выполнения программы байт-коды считываются и интерпретируются виртуальной Java-машиной (JVM - Java Virtual Machine). Таким образом, откомпилированная Java-программа может быть выполнена на тех вычислительных архитектурах, для которых реализована JVM (в настоящее время спектр таких вычислительных средств - от супер-ЭВМ до мобильных телефонов).

Примечание. Ни что не запрещает JVM для выполнения Java-программы откомпилировать ее байт-коды в машинные команды.

Переносимый

Огромный вклад в переносимость Java-программ вносит независимость откомпилированных байт-кодов от архитектуры вычислительных средств исполнения. Но Java идет дальше, обеспечивая отсутствие зависящих от реализации аспектов в спецификации языка. Например, как мы знаем, в С (и C++) перменная типа int может иметь размер 2, 4 или 8 байт, переменная типа char может быть без- или знаковой, в Java же явно и строго определены размеры и свойства каждого простого типа данных. Также существенно, что Java обеспечивает единые средства создания графического интерфейса пользователя для разных операционных сред (UNIX, Windows, MacOS), проповедующих совершенно противоположные идеологии графического интерфейса.

Динамический

При подготовке и запуске Java-программ на выполнение отсутствует традиционный этап компоновки - объединения кода программы с кодом всех используемых ее функций ( в C) и классов (в C++). Интепретатор Java (JVM) загрузку необходимых классов осуществляет динамически в момент возникновения потребности в них. Более того, в Java существует возможность (пакет java.lang.reflect) динамически получать исчерпывающую информацию о любом классе (переменные, конструкторы, методы).

Примечание. Необходимо, правда, отметить, что динамическое связывание - это, в настоящее время, стандарный атрибут любой вычислительной среды (вспомним ".so" в UNIX и ".dll" в Windows).

Распределенный

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

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

В-третьих (что наиболее интересно), средства динамической загрузки классов, позволяют JVM загружать и запускать на выполнение методы удаленных объектов. Достигается такая возможность использованием средств RMI (Remote Method Invocation), собранных в пакеты java.rmi.*.

Простой

Java - простой язык, поскольку он легок для изучения, что объясняется двумя факторами: небольшое количество языковых конструкций ( что важно для "чайников"), "похожесть" на самые употребляемые языки C и C++ (что важно для миграции на Java). Например, в Java нет конструкций struct и union (из C), т.к. он объектно-ориентированный язык, но в нем нет множественного наследования и перегрузки операций (из C++). Но самое главное - в Java нет указателей!

Устойчивый

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

Автоматический сбор мусора (garbage collection) предотвращает "утечки" памяти. Повышению устойчивости программ на Java способствует также использование конструкции try/catch/finally, позволяющей сгруппировать код, ответственный за обработку исключений (exceptions), в том числе ошибок выполнения, в одном месте программы.

Безопасный

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

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

Второй уровень - проверка на корректность байт-кода, загружаемого в виртуальную Java-машину.

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

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

Надо также отметить, что средств защиты от DOS-атак (отказ в обслуживании) в Java нет.

Эффективный

Конечно же, программы на Java, как на интерпретируемом языке, не могут быть столь экономичны, как на компилируемых C и C++. Однако компиляция в байт-коды делает Java-программы более быстрыми, чем программы на других интерпретируемых языках (sh/bash, csh, Tcl и даже Perl и PHP). Более того, современные JVM обладают способностью компилировать байт-коды в машинные команды целевой ЭВМ "на лету", а это уже делает скорость выполнения Java-программ сопоставимой со скоростью программ, написанных на C и C++. Кроме этого Java обеспечивает возможность для критических участков программы создавать код на C или C++.

С другой стороны, Java - язык для создания сетевых интерактивных программ и апплетов (в которых львиная доля времени уходит на ожидание ответов по сети или реакции пользователя). Здесь производительность JVM вполне адекватна решаемой задаче.

Многопотоковый

При программировании на C и C++ многопоточность задачи обеспечивается средствами целевой ОС и библиотекой соответствующих системных системных вызовов и функций. В Java поддержка многопоточности реализована языковыми средствами. Так, в пакете java.lang содержится класс Thread, обеспечивающий методы запуска и завершения потоков. Имеется также конструкция synchronized, позволяющая отмечать участки программы и целык методы, которые должны одновременно выполняться только одним потоком.

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

Отличия Java от C

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

Структура программы

Java-программа состоит из одного или нескольких определений классов, размещенных в одном или нескольких файлах (суффикс имени файла - .java). Для компиляции программ используется java-компилятор javac, например,
javac FirstExample.java

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

Один из классов программы должен быть открытым (public) классом и содержать метод main, с которого начинается выполнение программы.

Для выполнения программы необходимо запустить интерпретатор java, указав ему имя класса, содержащего метод main, например,
java FirstExample

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

Java-программа завершает свою работу оператором return в методе main или после выполнения последнего оператора метода main. Однако, многопотоковая программа заканчивается с завершением последнего потока.

Прототип метода main

Прототип функции main в C/C++ имеет следующий вид:

int main (int argc, char* argv[], char* envp[]) ,
где argc - количество аргументов командной строки, argv - массив указателей на аргументы командной строки, envp - массив указателей на среду выполнения (множество пар "имя=значение").

Метод main в Java имеет следующий прототип:

public static void main (String argv[]) ,
где argv - это массив строк, представляющих собой аргументы командной строки вызова интерпретатора и располагающихся в ней после имени класса. Подчеркнем, что argv[0] в Java - это не имя класса, как мог бы ожидать программист С/С++, а действительно первый аргумент метода.

Приведенная ниже Java-программа реализует функцию команды echo из UNIX - выводит свои аргументы

public class echo {
  public static void main (String argv[]) {
    for (int i; i<argv.length; i++)
      System.out.print (argv[i] + " ");
    System.out.print ("\n");
    System.exit (0);
    };
  }

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

Из Java-программы (в отличие от C/C++-программ) переменные среды (environment), во многом определяемые ОС, недоступны. В качестве альтернативы в Java определен независимый от вычислителной платформы механизм, называемый списком системных переменных (system properties list). Для получения значения системной переменной используется метод System.getProperty(), например:

String homedir = System.getProperty("user.home");
String debug = System.getProperty("myapp.debug");

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

java -Dmyapp.debug=true myapp

Области видимости имен

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

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

ru.bmstu.students.rk6.Ivanov.games

Файлы классов хранятся в каталогах, определяемых именем пакета. Например, все игровые классы студента Иванова хранятся в каталоге с относительным именем

ru/bmstu/students/rk6/Ivanov/games

Для определения полного путевого имени каталога, содержащего файлы классов пакета, Java-интепретатор java использует системную переменную CLASSPATH, устанавливать которую можно, например, в bash, командой

export CLASSPATH=.:~/classes:/usr/local/java/classes

Пути к системным классам включать в CLASSPATH нет необходимости, так как интерпретатор Java эти пути присоединяет к списку справа самостоятельно.

Похожую, но не совсем аналогичную роль играет параметр -classpath интерпретатора java.

Для включения класса в пакет используется оператор

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

За необязательным оператором package может следовать любое количество операторов import, имеющих две формы записи:

import <имя-пакета>.<имя-класса>;
import <имя-пакета>.*;

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

Доступ к пакетам, классам и членам классов управляется использованием ключевых слов-модификаторов public (открытый), protected (защищенный) и private (закрытый) и регламентируется следующими правилами.

Комментарии

В Java поддерживаются комментарии трех видов.

  1. В стиле языка C (от "/*" до "*/").
  2. В стиле языка C++ (от "//" до "\n").
  3. Специальные комментарии "для документирования" (от "/**" до "*/"), используемые программой javadoc для создания простой интерактивной документации из исходных файлов на языке Java.

Отсутствие препроцессора

В Java отсутствует препроцессор, подобный препроцессору cpp в C/C++. А значит и нет #-директив таких, как #define, #include, #ifdef. Дело в том, что в Java нет необходимости в них .

Эквивалентом константы, объявленной в C/C++ через #define, служит переменная, объявленная в классе Java как static final, например java.lang.Math.PI. Преимущества такого подхода: строгая типизация констант и уникальность иерархических имен, что исключает конфликты (то же PI нельзя переопределить).

Макросы (другое использование #define) заменены inline-подстановкой, осуществляемой компилятором Java для "коротких" иетодов автоматически.

Ненужность #include объясняется двумя факторами. Во-первых, файл класса (с суффиксом .class) одновременно является и объявлением класса, и его определением (реализацией), значит, отпадает необходимость в загаловочных h-файлах. Во-вторых, стандартизованность в размещении файлов классов по каталогам дпет возможность интепретатору Java однозначно определять местоположение загружаемого класса, значит отпадает необходимость директивного включения файлов исходного текста.

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

Условная компиляция (для нужны #ifdef/#endif в C/C++) в Java выполняется неявно. Дело в том, что нормальный Java-компилятор (например, javac) определяет участки исходного текста, которые никогда не будут выполняться, и игнорирует их, не генерируя для них байт-кода. Это значит, что конструкция C/C++ в виде

#ifdef DEBUG ... отладочный код ... #endif может быть смоделированиа в Java конструкцией private static final boolean DEBUG = true/false; if ( DEBUG ) { ... отладочный код ... };

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

Кодировка

Символы, строки и идентификаторы (т.е. имена классов, переменных методов) в Java формируются 16-битовыми символами Unicode. Это обеспечивает, например, возможность давать классам и и их членам русские имена.

Примечание. Представление в Unicode символов латинского алфавита совпадает с их представлением в ASCII и ISO8859-1 (Latin-1).

Но, к сожалению, Unicode включает в себя кодировку кириллицы совпадающую со стандартом ISO8859-5, не пользующимся у нас популярностью (хотя все UNIX-системы его поддерживают).

Для представления в исходных текстах Java символов, не имеющих графического представления (например, из-за отсутствия соответствующих шрифтов) используется следующая esc-последовательность \uxxxx, где x - шестнадцатиричная цифра. Например, \u044E представляет строчную русскую букву "ю".

Простые типы данных

К списку простых типов данных C в Java добавлены byte и boolean. К тому же четко определены размер, знаковость, диапазон представления и значение "по умолчанию". Приведенная ниже таблица содержит эти характеристики для всех простых типов.

Тип Содержимое Умолчание Размер Диапазон
boolean true/false false 1 бит false ... true
char символ Unicode \u0000 16 битов \u0000 ... \uFFFF
byte целое со знаком 0 8 битов -128 ... 127
short целое со знаком 0 16 битов -32768 ... 32767
int целое со знаком 0 32 бита -2147483648 ... 2147483647
long целое со знаком 0 64 бита -9223372036854775808 ... 9223372036854775807
float числа с плавающей точкой в формате IEEE 754 0.0 32 бита +/-3.40282347E+38 ... +/-1.40239846E-45
double числа с плавающей точкой в формате IEEE 754 0.0 64 бита +/-1.7976933486231570E+308 ... +/-4.94065645841246544E-324

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

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

boolean b; int i; i = (b) ? 1 : 0; // из boolean в int b = (i != 0); // из int в boolean

Тип char. Значения этого типа служат для представления символов в кодировке Unicode, Символьные литералы (как и в C/C++) заключаются в одинарные кавычки (апострофы), например, char c = 'A'; .

Поддерживаюся все esc-последовательности C и Unicode, например:

char newline = '\n'; char apostrophe = '\''; char delete = '\377'; char aleph = '\u05D0'; Тип char - целый тип, не имеющий знака, при преобразовании к типу byte или short результат может оказаться отрицательным.

Целые типы. К ним в Java относятся byte, short, char, int, long. Из них беззнаковым является только char. Модификатора unsigned в Java нет. В остальном они полностью аналогичны целым типам языка C.

Арифметические операции над целыми числами деления на нуль или взятия остатка по основанию нуль генерируют исключение ArithmeticException.

Типы данных с плавающей точкой включают в себя типы float и double. Литералы этих типов имеют тот же синтаксис, что и в языке C.

В работе с числами с плавающей точкой Java придерживается стандарта IEEE 754. При выполнении некорректных арифметических операций возможно получение особых значений: плюс бесконечность, минус бесконечность, не число, минус нуль. В классах java.lang.Float и java.lang.Double для некоторых из них определены символьные константы: POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN.

Величина NaN - особенная, ее сравнение с другими величинами, включая ее саму, всегда дает false. Для проверки "на равенство" величине NaN используются методы Float.isNaN() и Double.isNaN().

Строки в Java не являются массивами символов, а являются объектами класса String. Однако, для представления строковых литералов в Java заимствован синтаксис языка C, например,

String str = "Short string";

Ссылочные типы данных

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

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

Еще один пример. Для объектов (ссылочный тип):

Button p, q; p = new Button; q = p; p.setLabel("QQ"); String s = q.getLabel(); // Теперь s содержит "QQ" Для объектов простого типа: int i, j; i = 1; j = i; i = 2; // Здесь i = 2, а j = 1

И еще один пример.

public void swap(Object a, Object b) { Object temp = a; a = b; b = temp; } Можно предположить, что автор данного метода намеревался "поменять местами" (swap) два объекта класса Object, но ясно, что его усилия ушли впустую.

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

Vector a, b; a = new Vector; b = a.clone(); После выполнения этого фрагмента b будет ссылаться на дубликат объекта a. Следует отметить, что метод clone() поддерживается только теми классами, которые реализуют интерфейс Cloneable (об интерфейсах позже).

Сравнение данных ссылочного типа реализуется, конечно, не оператором "==", а методом equals(), реализованном в ряде классов (в C/C++ строки, например, тоже сравниваются не с помощью "==", а с помощью strcmp()).

Указатели (pointers), еще раз подчеркнем, в Java отсутствуют и запрещены любые действия с адресами, а именно:

null в Java - это зарезервированное ключевое слово, означающее отсутствие ссылки. Именно null является значением "по умолчанию" для всех ссылочных типов. Значение null нельзя привести к какому-либо простому типу, его нельзя считать равным 0 (как помним, в C/C++ NULL определен через #define как 0).

Объекты

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

Создание объекта осуществляется оператором new в точности так же, как в C++, например:

ComplexNumber c = new ComplexNumber(1.0, 1.414);

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

String s = "This is a string";

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

В-третьих, объекты создаются путем их десериализации, т.е. путем их воссоздания из состояния хранения (например, на диске), в которое они были переведены путем сериализации.

Память для объектов в Java выделяется динамически, но операторов типа delete (C++) или методов типа free() (язык C) для явного освобождения ставшей ненужной памяти нет.

В Java для освобождения неиспользуемой памяти употребляется механизм автоматической сборки мусора (garbage collection), реализованный в виде низкоприоритетного потока.

Для доступа к членам классов и их экземпляров используется разделитель ".", например:

ComplexNumber c = new ComplexNumber; c.x =1.0; c.y = 1.414; double m = c.magnitude();

Массивы

Практически все, что сказано выше про объекты, справедливо и для массивов:

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

byte buff[] = new byte[1024]; Button bs[] = new Button[10]; В первой строке 1024 элементов массива buff заполняются нулями, во второй - 10 элементов массива bs заполняются значениями null.

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

int table[] = {1, 2, 4, 8, 16, 32, 64, 128}; Кстати, Java допускает и новый (относительно C/C++) синтаксис объявления массивов, как это показано ниже: int[] table = {1, 2, 4, 8, 16, 32, 64, 128};

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

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

Для определения длины массива используется переменная экземпляра length, определенная как константа (final).

Строки

В Java строки не являются массивами символов, заканчивающимися символом NULL. Напротив, строки - это экземпляры класса java.lang.String. Но специально для этого класса в Java реализован оператор "+", осуществляющий конкатенацию строк.

Важной особенностью объектов String является их неизменяемость, т.е. в классе String методов, модифицирующих содержимое строки нет. Если необходима модификация, то из объекта String следует создать объект StringBuffer. Наиболее важные методы класса String - length(), charAt(), equals(), compareTo(), indexOf(), lastIndexOf(), substring().

Операторы

В Java поддерживаются почти все операторы C (с теми же правилами приоритета и ассоциативности). Исключены операторы работы с указателями (*, ->, &), оператор sizeof, а конструкции "[]" (доступ к массиву) и "." (доступ к члену) операторами не считаются.

В Java добавлены следующие новые операторы.
instanceof
Возвращает значение true, если объект, стоящий слева, принадлежит классу/подклассу или реализует интерфейс, стоящий справа.
>>>
Выполняет побитовый сдвиг вправо, устанавливая старшие биты результата в 0.
+
Выполняет конкатенацию строк.
& и |
Для типа boolean выполняет логические операции И и ИЛИ.

Управляющие конструкции

Java повторяет большинство управляющих конструкций языка C (исключая goto), добавляя две новые: try/catch/finally и synchronized. Кроме того в операторах break и continue обеспечена возможность перехода по метке.

Оператор break без метки работает точно так же, как и в C: он прерывает выполнение тела того цикла for, while, do или конструкции switch, внутри которого/ой он размещен. Оператор break с меткой завершает выполнение внешнего блока, помеченного меткой, при этом автоматически выполняются все блоки finally, если они определены (об операторе try, в состав которого входит блок finally ниже). Например.

test: if ( check(i) ) { try { for (int j=0; j<10; j++) { if (j>i) break; // Завершение цикла for if (a[i][j] == null) break test; // Выполнение оператора finally }; // и завершение if } finally { cleanup (a, i,j); }; }

Оператор continue с меткой передает управление на конец помеченного внешнего цикла for, while или do. Если между оператором continue и концом соответствующего цикла находится блок finally, то он автоматически выполняется до перехода к концу цикла. Например.

big_loop: while (!done ) { if ( test(a,b)==0 ) continue; // Переход в точку 2 try { for (int i=0; i<10; i++) { if (a[i] == null) { continue; // Переход в точку 1 } else { if (b[i] == null) continue big_loop; // Переход в точку 2 после выполнения блока finally }; }; // Точка 1. Приращение счетчика i и переход в начало цикла } finally { cleanup (a, b); // Точка 2. }; }

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

synchronized (выражение) {блок-операторов}
, где выражение представляет собой объект или массив. Блок-операторов будет запущен на выполнение только тогда, когда synchronized убедится, что выражение не используется другими потоками управления. На время исполнения блок-операторов доступ к объекту или массиву блокируется.

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

Исключения и их обработка

Исключения - важная особенность Java, отличающая ее от C (но не C++).

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

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

Исключения в Java - это объекты, являющиеся экземплярами подклассов класса java.lang.Throwable. Из Throwable порождены 2 стандартных подкласса: java.lang.Error и java.lang.Exception. Исключения, являющиеся подклассом класса Error, сигнализируют, главным образом, об неустранимых проблемах (нехватка памяти, ошибки динамической загрузки и т.п.), поэтому они перехватываться не должны. Исключения же, являющиеся подклассами Exception, свидетельствуют о ситуациях, которые можно корректно обработать, Например, java.io.EOFException говорит о достижении конца файла, а java.lang.ArrayAccessOutOfBounds - о попытке доступа к элементу, находящемуся за пределами массива.

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

Обработка исключений в Java реализуется с помощью конструкции try/catch/finally, имеющей следующий общий вид:

try { // Блок кода, в котором могут присутствовать // операторы break, continue и return, // и который может генерировать исключения } catch (SomeException e1) { // Обработка исключения e1 типа SomeException // или подкласса этого типа } catch (AnotherException e2) { // Обработка исключения e2 типа AnotherException // или его подкласса } finally { // Этот код всегда выполняется после кода в try независимо от условия выхода } Внутри блока try содержится код, в ходе выполнения которого могут возникать исключения или неустранимые ошибки. Внутри блока могут содержаться операторы передачи управления break, continue и return.

После блока try может следовать любое (в том числе и нулевое) количество блоков catch, в которых содержится код обработки исключений различных типов. При возникновении исключения в блоке try его выполнение прерывается. Далее последовательно просматриваются блоки catch и управление передается тому блоку, чей тип аргумента совпадает с классом объекта исключения или является его суперклассом. Аргумент блока catch действителен только внутри своего блока и ссылается на сгенерированный в блоке try объект исключения.

Блок finally содержит код, который безусловно вызывается на выполнение, если выполнялась хотя бы минимальная часть блока try. Конкретнее.

Отметим, что, если код блока finally сам передает управление посредством return, break или continue или посредством генерации нового исключения, то отложенная передача управления отменяется и реализуется эта новая передача.

Блок finally обычно используется для корректного завершения блока try (освобождение ресурсов, закрытие файлов и т.п.).

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

public void myfunc (int arg) throws MyExcept1, MyExcept2 { код, генерирующий исключения MyExcept1 и MyExcept2 }

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

Также излишне перечислять в throws исключения, являющиеся подклассами классов Error и RuntimeException (например, исключение InternalError), поскольку они присутствуют там неявно. Дело в том, что такие исключения способен генерировать каждый метод.

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

Стандартные исключения, которые часто приходится объявлять - java.lang.io.IOException и java.lang.InterruptedException.

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

throw new MyExcept("My exception occured.");

Прочие отличия от C

Классы и объекты в Java (отличия от C++)

Пример
Ниже приведен пример текста на Java, описывающего (частично) класс Circle - класс окружностей. public class Circle { // Переменные экземпляров public double x, y; // Координаты центра public double r; // Радиус // Конструкторы public Circle (double x, double y, double r) { this.x = x; this.y = y; this.r = r; }; public Circle (double r) { this(0.0, 0.0, r); // Вызывает трехаргументный конструктор }; public Circle () { this(0.0, 0.0, 1.0); }; // Методы экземпляров класса (объектов) public double circumference () { return 2*3.14159*r; }; public double area () { return 3.14159*r*r; }; } Обратите внимание, в Java объявлениекласса и его определение даются в одном месте (представляют собой единое целое).

Создание экземпляра класса (объекта класса) обычно реализуется с помощью оператора new, как это показано ниже.

Circle c; c = new Circle(); или Circle c = new Circle();

Доступ к открытым (public) членам класса осуществляется через ".":

Circle c = new Circle(); c.x = 2.0; c.y =2.0; c.r = 1.0; double a = c.area(); // Но не area(c)!

Конструкторы

В примерах выше объект класса Circle создавался путем вызова конструктора "по умолчанию" Circle() без аргументов. Таким конструктором автоматически снабжается каждый вновь создаваемый класс в Java (хотя его можно и переопределить). Задача конструктора "по умолчанию" состоит в выделении памяти под переменные объекта и инициализации их значениями "по умолчанию".

В примере, описывающем класс Circle, определен еще один конструктор с тремя аргументами. Он не только выделяет память под объекты класса Circle, но и инициализирует значения их переменных своими аргументами. Обратите внимание на необходимость использования в этом конструкторе указателя this для исключения конфликта имен. This в Java (как и в C++) является указателем на это (this pointer), т.е. на текущий объект.

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

Перегрузка методов

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

Переменные класса

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

Переменные класса в Java маркируются модификатором static, как это показано ниже:

public class Circle { static int num_circles = 0; public static final double PI = 3.14159265358979323846; // далее известный код }

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

Переменная PI, тоже принадлежащая классу, а не к экземпляру, на самом деле переменной не является. Она - константа, о чём говорит модификатор final в её объявлении.

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

System.out.println ("Num = " + Circle.num_circles); Circle.PI = 3.62; double circumference = 2*Circle.PI*radius; Заметим, что попытка изменить константу PI во второй строке вызовет ошибку на этапе компиляции. Кроме того, компилятор Java достаточно интеллектуален для того, чтобы произведение двух констант 2*Circle.PI превратить в одну.

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

static { код инициализации };

Методы класса

Методы класса не связаны с конкретными объектами класса и поэтому в них недопустимо использование "указателя на это" this ("этого" для них просто не существует). Расширим класс Circle следующими двумя методами:

public Circle bigger (Circle c) { // Метод экземпляра if (c.r > r) return c; else return this; }; public static Circle bigger (Circle a, Circle b) { // Метод класса if (a.r > b.r) return a; else return b; };

Использование этих методов иллюстрирует следующий пример:

Circle a = new Circle(2.0); Circle b = new Circle(3.0); Circle c; c = a.bigger(b); // или c = b.bigger(a); c = Circle.bigger(a, b);

Легко видеть, что методы классов - это аналог функций языков C/C++ (нет this - нет никаких объектов). Хотя Java - объектно-ориентированный язык, оказалось, что без функций в духе C в нем не обойтись, т.к. существуют простые типы данных, не являющиеся объектами (например, double). А для простых типов данных тоже нужны функции. Так, класс Math - это набор метод класса (математических функций) для оперирования с числами с плавающей точкой. Другой класс, где определены только методы класса - это System.

Уничтожение объекта

В Java нет средств явного удаления созданных объектов (конструкции типа free() или delete отсутствуют). Как уже говорилось, объекты уничтожаются сборщиком мусора (garbage collector).

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

matrix = null;

Перед удалением объекта сборщик мусора вызывает на выполнение метод finalize(), если он определен в классе бъекта. Этот метод завершения (аналог деструктора в C++) представляет собой метод экземпляра (не static), не имеет аргументов и не возвращает значения (void).

Вот что еще необходимо знать о методах завершения Стр. 85

Подклассы и наследование

Для определения подкласса некоторого класса в Java используется ключевое слово extends (расширяет), как это показано ниже:

public class GraphicCircle extends Circle { Color outline, fill; public void draw (DrawWindow dw) { dw.drawCircle (x, y, r, outline, fill); }; }

Каждый подкласс наследует все переменные и методы своего родителя - суперкласса. Поэтому представленный ниже оператор вполне корретен

GraphicCircle gc = new GraphicCircle(-2.0, 2.0, 3.0); double area = gc.area();

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

Circle c = gc; Однако, теперь через c доступ к графическим возможностям наследующего класса GraphicCircle стал невозможен.

Финальные классы - это классы, объявленные с модификатором final. Для таких классов запрещено определять подклассы. Объявление классов как нерасширяемых позволяет компилятору Java выполнять некоторую оптимизацию вызовов его методов.

У любого класса есть суперкласс. Если он не указан явно в конструкции extends, то родителем класса считается класс Object, возглавляющий всю иерархию объектов Java (только класс Object не имеет родителя).

Конструктор подкласса для вызова конструктора своего суперкласса может использовать конструкцию super(). Например, конструктор для класса GraphicCircle может иметь следующий вид:

public GraphicCircle (double x, double y, double r, Color outline, Color fill) { super (x, y, r); this.outline = outline; this.fill = fill; };

Ограничения на использование super() таковы:

Если в конструкторе подкласса отсутствует явный вызов конструктора суперкласса, то Java вставляет в самое его начало вызов super() неявно. Если в суперклассе отсутствует конструктор без аргументов, то возникает ошибка компиляции. Таким образом при создании объекта любого класса в Java последовательно вызывается цепочка конструкторов, начинающаяся с конструктора головного класса Object.

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

Переопределение методов

Метод подкласса переопределяет (override) метод суперкласса, если он имеет те же имя, количество аргументов, тип и порядок расположения аргументов, а также тип возвращаемого значения. Необходимо четко понимать, что переопределение (override) метода - это не его перегрузка (overload). Для вызова из метода подкласса переопределенного метода суперкласса в Java используется префикс "super.". Например, класс A определяет метод int func(int), его подкласс B переопределяет этот метод, тогда, если где-то в методах класса B необходимо вызвать метод func() класса A, то необходимо использовать оператор

int i = super.func(25); Однако конструкция super.super.func() для вызова метода func() не отца, а деда, недопустима!

Конструкция "super." часто используется в методах завершения (помним, что их имя фиксировано - finalize). Если из метода завершения подкласса необходимо вызвать метод завершения суперкласса, то используется оператор

super.finalize(); в качестве последнего оператора метода завершения.

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

Динамический вызов методов

Пусть есть класс A, определяющий метод f(), два его подкласса B и C переопределяют этот метод (каждый по своему). Создается вектор v элементов типа A, который заполняется вперемежку объектами классов A, B и C. Далее осуществляется вызов v[i].f(). Метод f() какого класса будет вызван на выполнение? Понятно, что компилятор Java заранее знать конкретный класс объекта, который попадет в i-ый элемент вектора на этапе выполнения знать не может. Поэтому он генерирует байт-код динамического поиска необходимого метода и его вызова. Такой поиск, естественно, замедляет вызов метода.

К счастью, во многих случаях компилятор Java имеет возможность создавать байт-код непосредственного вызова метода, руководствуясь модификаторами static, private и final в описании методов и классов. Кроме того, методы, вызываемые непосредственно, являются кандидатами на inline-подстановку (если, конечно, их размер невелик).

Модификаторы видимости

В Java используются 4 типа ограничений на доступ к членам класса. Три из них задаются модификаторами public (открытый), protected (защищенный) и private (закрытый). Четвертый тип - "по умолчанию", действующий при отсутствии в объявлении члена какого-либо из этих модификаторов.

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

Доступен для Тип доступа
public protected по умолчанию private
Того же класса да да да да
Класса из того же пакета да да да нет
Подклассов из того же пакета да да нет нет
Подклассов из других пакетов да нет нет нет

Могут быть даны следующие рекомендации по использованию модификаторов доступа.

Абстрактные классы

Java позволяет определять метод без его реализации, отмечая его как abstract. Вместо тела таких методов указывается ";" (точка с запятой). Ниже даны правила для абстрактных методов и содержащих их абстрактных классов.

Предположим, что планируется создать ряд классов геометрических фигур: Circle, Ellipse, Rectangle, Triangle и т.п. Для каждого из них необходимо реализовать два общих метода: area() (площадь) и circumference() (периметр). Для обеспечения возможности работы с массивом фигур полезно иметь для всех этих классов суперкласс Shape с методами area() и circumference(). Однако реализовать эти методы в Shape невозможно. В этом случае Shape должен объявить эти методы абстрактными (и автоматически стать абстрактным классом).

public class Shape { public abstract double area(); public abstract double circumference(); }

Подчеркнем два важных момента.

Интерфейсы

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

public interface Drawable { public void setColor (Color c); public void setPosition (double x, double y); public void draw (DrawWindow dw); }

Если в состав интерфейса входит переменная, то она обязательно должна быть объявлена как static final (т.е. быть константой).

Интерфейсы введены в Java для обеспечения некоторого подобия множественного наследования, которое в своем чистом виде в Java не поддерживается. Любой класс Java может реализовывать (implement) любое количество интерфейсов.

public class DrawableCircle extends Circle implements Drawable { private Color c; // Конструктор public DrawableCircle (double x, double y, double r) { super(x, y, r); }; // Методы интерфейса Drawable public void setColor (Color c) { this.c = c; }; public void setPosition (double x, double y) { this.x = x; this.y = y; }; public void draw (DrawWindow dw) { dw.drawCircle(x, y, r, c); }; }

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

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

В Java имеет смысл определять и использовать пустые интерфейсы-метки, не объявляющие ни одного метода. Смысл использования таких интерфейсов - сгруппировать на основе какого-либо свойства несколько классов. Примером интерфейса-метки служит интерфейс Clonable из пакета java.lang, он позволяет идентифицировать класс, для которого существует метод clone() класса Object, позволяющий получить копию объекта. Проверить, реализует ли некоторый класс какой-либо интерфейс (в том числе интерфейс-метку) позволяет оператор instanceof.