Африканец

Заметки про Жабу.


Замечание. Везде в этих заметках слово "жаба" означает язык Java, разработанный фирмой Sun, кажется, в 1995 году. Я его буду использовать, потому что переключать регистры мне лень.

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


Часть 1. Жаба

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

Теперь по порядку.

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

2) Даже лексика Жабы неочевидна - правила о том, что раньше раскрывается, \n или \u, кого угодно сведут с ума. Скажите-ка, что выведется таким вот операторомж

  System.out.println ("a\u005Cnb");

3) Неясно, зачем у Жабы синтаксис, смахивающий на С. Единственное объяснение - желание завлечь программистов, пишущих на С или С++. То есть выбор синтаксиса по политическим, а не по техническим причинам. Синтаксис С хорош только если язык, сделанный поверх этого синтаксиса - С. Когда можно написать

  while (*p++ = *q++);

Если же такого написать нельзя, то следует принять наиболее безопасный с точки зрения возможных ошибок синтаксис. Синтаксис С - не для чайников. Он требует внимания. Можно забыть фигурные скобки, можно поставить (случайно) точку с запятой после заголовка while. Профессионалы никогда не сделают подобных ошибок. Но ведь Жаба-то разрабатывалась для не совсем профессионалов!

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

  if (a = b)

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

А вот неполный список проблем синтаксиса С, которые в Жабе не решены:
 - странные приоритеты ряда операций (у >> ниже, чем у +, и у + выше, чем у &)
 - отсутствие явной закрывающей скобки цикла и условного оператора и применение вместо этого фигурных скобок - очень сильно чревато ошибками
 - оператор switch, где люди очень часто забывают break
 - совпадение оператора выхода из цикла (break) и из ветви switch

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

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

5) Они выкинули goto. Знаю, знаю, что сейчас тысяча возмущенных людей напишут мне, что goto вреден, должен быть запрещен, и без него программы лучше и яснее. Отвечу - оператор присваивания не менее вреден, ведь с помощью него можно составить совершенно нечитаемые программы. Не зря его нет в LISP. Впрочем, программы на LISP ничуть не яснее, из чего следует, что и вызов процедуры должен быть запрещен тоже. Более того, все неправильные, не работающие или ненужные программы, как правило, содержат хотя бы один из указанных операторов.

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

6) Я никогда не видел языка с модульной структурой, такой же идиотской, как в Жабе. Близко к тому - видел. Скажем, у Ады модульная система достаточно убогая. Но Жаба! Ощущение такое, что люди вообще не знали ни одного языка, и никаких книжек не читали. Для сравнения.

В Модуле-2 есть два способа проимпортировать объект из модуля:
- IMPORT module;
- FROM MODULE import object;

В первом случае объект употребляется как module.object, во втором - просто object. В итоге по любому объекту всегда есть возможность определить, откуда он пришел - достаточно только посмотреть на начало файла. И наоборот, посмотрев на начало любого файла, всегда видно, что конкретно используется в файле. На жабе же есть три способа:

 - import пакет.класс, например, import com.dvsoft.cts.event.CallEvent;
 - import пакет.*, например, import com.dvsoft.cts.event.*;
 - вообще без импорта, просто использовать в тексте с полным именем:

 com.dvsoft.cts.event.CallEvent event = new com.dvsoft.cts.event.CallEvent ();

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

7) Вообще, система с пакетами через точку явно непродумана. Например, отсутствует относительная адресация. Нельзя написать

 import com.dvsoft.cts.*;

и потом употребить event.CallEvent - только полная квалификация! И если наш класс сам принадлежит к пакету com.dvsoft.cts - к подпакету доступ тоже только с полным путем!

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

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

9) Необходимость помещать каждый публичный класс в отдельный файл приводит к появлению сотен небольших файлов, и программу становится невозможно читать. Я хочу спросить у фанатов Жабы - вы свои программы хоть иногда печатаете? Не говорите мне, что это устарело и более ненужно. Просто вы не можете ее напечатать, вот и говорите так. А я свои программы изредка печатаю. Приятно почитать программу утром в кафе за чашкой двойного испрессо.

А читать чужую программу с сотней классов просто невозможно! И разные класс-броузеры помогают слабо. Вообще я консервативен - программа должна иметь исходный текст, пригодный к чтению в обычном редакторе, и к напечатанию. А также к обработке различными текстовыми утилитами, хотя бы grep и diff.

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

11) процедуры и классы можно использовать до описания. Это не только усложняет компилятор, но и делает программу менее понятной, потому что труднее найти определение нужного тебе объекта.

Вообще отступление о компиляторах. В свое время языки писались для людей, без учета сложности компиляторов. Это часто приводило к зауми - достаточно упомянуть передачу параметров по изображению в Алголе-60. Написание компиляторов с некоторых языков было нетривиальной задачей - например, по синтаксическому разбору Ады или Алгола-68 защищались диссертации. Примерно со времени С и Паскаля наметилась тенденция делать языки так, чтобы их проще было компилировать, и оказалось, что такие языки и для человека удобнее, ведь человек - это тоже компилятор. Человек читает программу с целью ее понять, и язык, который легче разбирается компилятором, и для человека лучше. Так вот, в Жабе явный откат назад - считается, что машина железная, пусть и трудится. В итоге программы легче писать, но труднее читать. А читать - важнее.

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

13) Переменную можно описать в заголовке цикла, но она не локальна в этом цикле, что похоже на маразм.

14) Почти что во всяком серьезном языке есть возможности для метапрограммирования, то есть описания макросов. В некоторых языках они развиты очень хорошо (POP-2, PL/1, некоторый ассемблеры), в некоторых - похуже, как, скажем, в С. В некоторых языках их нет, но реализаторы добавляют их в виде расширений. В некоторых языках эти возможности присутствуют в виде средств расширения синтаксиса (Алгол-68), или написания обобщенных процедур (Ада, С++). И только в жабе их нет и не планируется. Это оправдывается тысячей аргументов - и непонятны, мол, макросы, и с помощью классов можно без них обойтись, надо только предусмотреть побольше виртуальных методов (черт с ней, с эффективностью). А в итоге писать сколько-нибудь сложную реальную программу практически полностью невозможно.

Рассмотрим простой пример. Мы хотим иметь хэш-таблицу, ключами в которой являются целые числа - самый простой и эффективный вид хэш-таблицы. В то же время мы не хотим дублировать код для хэш-таблицы на все возможные случаи. Так вот, в С для этого мы напишем макрос, в Аде - generic package, в С++ хорошо сгодится template. В Жабе мы применим стандартный класс Hashtable, который берет в качестве ключей произвольные объекты, завернем целые числа в эти объекты, выделив каждый из них в куче, и будем вызывать в них виртуальные метода hashCode и equals. Ну да, работать будет. А кого в наше время заботит эффективность?

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

Представьте себе программу для ракеты, имеющей три модификации - не требующей проверки двигателя, требующей и имеющей 10 двигателей, и требующей, но имеющей их 20, и еще в двух ступенях. На С это было бы написано примерно так:

#if TEST_ENGINES
#if TWO_STAGES
  for (i = 0; i < FIRST_STAGE_NENGINES; i++)
    check_engine (0, i);
  for (i = 0; i < SECOND_STAGE_NENGINES; i++)
    check_engine (1, i);
#else
  for (i = 0; i < NENGINES; i++)
    check_engine (i);
#endif

А как на Жабе? Можно, конечно, заменить #if на обычный if, но имейте в виду, что определить константы в зависимости от внешних условий, нельзя, и поэтому это будут настоящие исполняемые операторы. А скорее всего, это вообще будут не константы, и даже не переменные, а вызовы виртуальных методов. В общем, будет что-то вроде такого:

  if (checkNeedTestEngines ()) { // виртуальный метод
    if (checkTwoStages ()) { // виртуальный метод
      for (i = 0; i < 2; i++)
        for (j = 0; j < StageManager.instance().getStage (i).getNumEngines (); j++)
          StageManager.instance().getStage (i).getEngine (j).check ();
    } else {
      for (i = 0; i < Stage.instance().getNumEngines (); i++)
        Stage.instance().getEngine (i).check ();
    }
  }

Разумеется, можно заменить 2 на StageManager.instance().getNumStages (), можно перетащить код по проверке двигателей в класс Stage, можно сделать еще массу дизайнерских изысканий, но всегда сохранятся два свойства:
 - все проверки будут во время исполнения и будут есть время
 - на орбиту придется брать с собой весь код, то есть одноступенчатая ракета должна будет иметь столько же памяти, сколько и многоступенчатая.

Неужели это именно то, чего добивались?

Кроме того, условная компиляция и макросы очень удобны для отладки. Например, в текст вставляются вызовы макроса ASSERT, которые в отладочном режиме раскрываются в проверку условия, а в боевом - в ничего. На Жабе это невозможно - код будет исполняться и тратить время. Для этого придуман объезд - условие пишется в специального вида комментарии, а специальные программы вставляют его из комментария в код, после чего компилируют обычным компилятором. Не есть ли это маразм?

16) Нормальные языки содержат конструкции для описания статических данных - инициализируемые массивы и записи, например. На Жабе с этим грустно. Есть убогое подобие инициализации массивов. Инициализация записей (гордо называемых классами) делается с помощью конструкторов. И то, и другое выполняется во время исполнения, то есть во время запуска такой программы куча времени будет съедена на эту инициализацию, и в программе будет, кроме самих данных, присутствовать код, исполняемый ровно один раз. Зачем это надо? Представьте себе, что вы пишете восходящий синтаксический анализатор. Такой анализатор имеет обычно большую-большую таблицу, которая на С записывается просто и естественно, и не тратит для своего создания ни такта процессорного времени. А на жабе это все - исполняемый код!

Более того - большинство реализаций даже константы (то, что описано как public static final int) - определяет во время исполнения!

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

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

19) Система экспорта-импорта в Жабе настолько сложна, что правильная ее реализация становится почти непосильной задачей. Особенно с учетом того, что импорт классов может быть взаимным. В результате ни в одной из существующих реализаций нельзя быть уверенным, что проект пересобран полностью. Это наблюдается в J++, JDK и JBuilder. Единственный надежный способ - стереть все class-файлы, после чего сделать полный Build. При этом надо, конечно, следить, чтобы в classpath случайно не было ссылок на так же называющиеся классы. В общем, поддержка проекта превратилась в постоянный геморрой и, похоже, так и было задумано. Более того, все стандартные компиляторы вслед за JDK валят class-файлы туда же, где уже лежат Java-файлы, делая крайне затруднительным стирание всех class-файлов.

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

21) Для "Надежного" языка разумно было бы иметь (возможно отключаемый) контроль целого переполнения. Либо язык для приложений - тогда нужен контроль. Либо для ковыряния битов (как в С) - тогда не нужен. Жаба вроде бы всегда провозглашалась языком для приложений. Так где этот контроль?

22) Каждый раз, когда мы описываем структуру (она же запись, она же класс), происходит выделение памяти в куче. Каждый раз, когда мы ее потом используем, происходит обращение по указателю. Зачем? Я понимаю, все это может быть полезно для "классов" - из-за возможного наследования их как раз полезно в куче размещать. Но что, если мы просто определили структуру Point с двумя полями, x и y - ну зачем тут куча? А представьте себе, что мы определили "линию" как две точки:

class Point
{
  int x;
  int y;
}

class Line
{
  Point p1;
  Point p2;
}

Line line = new Line ();
line.p1 = new Point ();
line.p2 = new Point ();
line.p1.x = 10;
line.p1.y = 20;
line.p2.x = 30;
line.p2.y = 40;

Разумеется, с помощью конструкторов это можно сократить до

Line line = new Line (new Point (10, 20), new Point (30, 40));

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

23) Хотя Сотрудник MS и пытался меня как-то убедить, что в сборщике мусора нет ничего страшного, я все же отношусь к нему подозрительно. Я предпочитаю точно знать, когда и что в программе происходит, и почему. Особенно в приложениях, критичных по времени.


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

  int gen_next_lexem (char ** p);

  next_lexem = get_next_lexem (&p);

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

  Parser pars = new Parser (p);

  next_lexem = pars.getNextLexem ();

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

А теперь модифицируем это, чтобы возвращался не только тип лексемы, но и сама лексема.

На С это так:

  int gen_next_lexem (char ** p, char ** lexem, int * lexem_len);

  next_lexem = get_next_lexem (&p, &lextext, &lexlen);

На Жабе придется создавать еще один класс - лексема, в котором хранится и лексема, и ее длина, и ее тип. И, конечно, придется выделять память для нового класса и для нового объекта String (хорошо еще, если сам текст копировать не придется, но это зависит от реализации String).

Ну зачем это все? Неужели специально все сделано, чтобы эффективность поменьше была?

25) Одно из замечательных изобретений Вирта, реализованное в Модуле-2, а также в каком-то виде в С - это разделение текста на definition и implementation. Хорошо, когда отдельно описан интерфейс, отдельно - реализация. Соответственно, один файл содержит комментарии о том, что делается, а другой - как делается. И, чтобы понять, что делает данная процедура, не надо лопатить кучу текста. А файл с реализацией, в свою очередь, не содержит гигантских пользовательских комментариев, и потому более обозрим. Так вот, в жабе этого нет. Всегда советуют моделировать это с помощью интерфейсов либо абстрактных классов. То есть применять виртуальные методы. С какой, спрашивается, стати, я должен применять косвенный вызов процедуры, а не прямой, только из-за того, что язык сочиняли идиоты?

26) Теперь немножко о библиотеке. Это не недостаток языка, а мое личное неудовлетворение. Бывают языки, содержащие библиотеку (или большую ее часть) в себе, например, PL/1 с его оператором PUT EDIT, или BASIC. Бывают языки, от библиотеки почти независимые, как С. Мне всегда больше нравились последние, потому что позволяли обходиться без библиотеки. Обычно, если ее не использовать, то в С можно обойтись небольшим рантаймом, что особенно здорово для встроенных приложений.

В Жабе же реализован самый извращенный, третий вариант. А именно язык вроде бы и отделен от библиотеки, а вроде она и вылазит в самых неожиданных местах. Ну например, то, что строка в кавычках - значение класса String, а исключительная ситуация "указатель равен null" - класса NullPointerException. И таких случаев много - когда что-то является объектом какого-то левого класса. В результате никогда неясно, что используется в программе, а что нет.

27) Сама библиотека оставляет желать лучшего. Почему, например, для узнавания текущей даты я должен выделять в памяти объект? А вот еще задачка, достойная пера. Напечатать текущую дату в формате

  дд.мм.гггг чч:мм:чч.МММ (МММ - миллисекунды).

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

28) В Жабе появилось слово "deprecated". Это то есть когда в новой версии старый метод еще поддерживается, но не поощряется. Хорошая, на первый взгляд, возможность. Но вот почему выходит версия жабы (JDK 1.0), а через полгода другая (1.1), и в ней половина библиотечных функций уже "deprecated"? А о чем думали полгода назад? А еще "отдепрекатили" целую графическую библиотеку (AWT), сделав новую версию (Swing), причем специально сделали это, дождавшись, пока все выпустят инструменты для поддержки AWT.

29) При написании на С много внимания уделяется всяким случаям вроде "А что если функция malloc вернула NULL?" Так вот, в Жабе считается, что память всегда есть. В общем-то никаких оснований для этого нет. А когда память кончится, будет брошена эксепция вроде OutOfMemoryException, которая есть объект и должна быть выделена в куче...

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

31) Меня очень раздражает оператор "+" для строк. Не люблю исключений. Вообще, значит, операторы не переопределяются, а вот для строк пожалуйста. При этом, если объект не строка, вызывается специальный метод со специальным именем toString... Ну зачем превращать язык в BASIC? Ясно же, что все так и будут применять этот оператор для складывания строк, плюя на эффективность. Есть класс StringBuffer, а кто им пользуется?

32) С определяет int как "наиболее эффективное целое число, не короче, чем 16 бит". То есть если на машине длина слова 18 бит, то и целое будет 18 бит. Жаба же определяет int как ровно 32 бита, а long как ровно 64. И если на машине нет 32-битных чисел (например, есть только 64-битные), придется моделировать. Мне это не нравится. Отсутствие беззнаковых чисел мне тоже не нравится.

33) Жаба предоставляет некоторые особо высокоуровневые возможности, такие, как клонирование (глубокое копирование) объектов и сериализация. Обе опасны в неумелых руках. Чайник, не задумываясь, склонирует (или сериализует) громадный граф, лишь бы добиться какой-то сиюминутной цели. Сериализация уже привела к тому, что программмы хранят свои настройки не в приятных для глаза ini-файлах, и не в Registry, а в многочисленных файлах, полученных путем сериализации объектов.


На сем я и закончу про жабу. Достаточно этого или надо еще?