Показаны сообщения с ярлыком Java. Показать все сообщения
Показаны сообщения с ярлыком Java. Показать все сообщения

воскресенье, 23 сентября 2012 г.

Почему я советую знакомым студентам учить Java?

Недавно снова наткнулся на вопрос «почему Java а не что то другое?». Я уже отвечал на форуме, теперь достал и оформил в виде статьи с акцентом почему я советую знакомым студентом учить Java.
Я не очень опытный специалист и здесь будет много субъективного мнения, и конечно же тема очень обширная и холиварная, но я хочу собрать все свои мысли в одном месте.
Вкратце суть статьи:
Учить нужно то, за что платят (т.е во что вкладывают) деньги, а больше платят за Java. Там и инновации, и широкий спектр решений и технологий.
Java нацелена на разработку больших проектов, поэтому её любят большие корпорации.
А платят за Java больше чем например за C#, в который тоже вбухивают деньги, потому что стоимость разработки и сопровождения ниже: много бесплатных инструментов, библиотек обкатанных временем, средства разработки и она кроссплатформенная и можно использовать бесплатный Linux, обратно совместимая, предсказуемая и надёжная.

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

Начнём с анализа текущих дел


Сейчас массовый софт работает в трёх основных областях:
  • Desktop приложения (офисы, АРМы, студии, системы документоборота, антивирусы), т.е. требующие установки на компьютер.
  • Web приложения сюда мы отнесём все сайты и онлайн сервисы (wikipedia, gmail). Многих она притягивает потому что не требуется установка ПО и данные остаются доступными в сети.
  • Мобильные приложения
  • Игры, особенно мобильные и броузерные для социальных сетей.
  • Embeded - встроенные системы (драйверы, контроллеры)

Примерно прикинем популярность по трендам индексатора американских сайтов работы indeed.com:


Веб самый популярный, а embeded совсем мал. Mobile и игры для социальных сетей растут как на дрожжах, это очень хорошо видеть если посмотреть тренд относительного прироста:


Embeded я убрал из-за странного дёрганого тренда.

А теперь график по языкам:

Тут несколько нюансов:
  • Для большего драматизма я добавил любимый но мертвый Delphi.
  • Также по Си из-за однобуквенного названия график выходит неоправданно завышенным, я использовал C Developer, тренд хоть кажется адекватным.
  • Objective-c подозрительно маленький тренд, чаще пишут iOS developer.
  • JavaScript (не путать с Java!) раньше имел ограниченное применение для небольших клиентских сценариев в броузере, но сейчас стал активно использоваться для создания игр и даже для серверного программирования.

Основные области применения Java это большие web приложения, банковские desktop приложения и мобильная разработка для Android. Т.е. она сразу попадает в три популярные категории.

Ну и собственно зарплаты по данным опроса DOU.ua
75 перцентиль midle разработчиков в Киеве. Справа шкала количества анкет.


Причины такой позиции Java

  • Строгая, статическая типизация что значительно уменьшает число ошибок и улучшает сопровождаемость кода, особенно если ещё использовать статический анализатор. При этом в яве есть ограниченные возможности динамического программирования через магию рефлексии и аннотаций. Но тем не менее есть её диалект Groovy который динамический и почти полностью совместим с ней на уровне синтаксиса. На нём кстати есть отличный Rails подобный фреймворк.
  • Java на рынке с 1995 года, обкатана и существуют сотни решений и технологий.
  • Что немаловажно является по сути упрощённым C++ который учили все в институтах и поэтому переход на неё проходит менее болезненно.
  • Язык прост, предсказуем, и минималистичен. Новые фичи вводятся очень осторожно, иногда даже черезчур долго (например замыкания).
  • Синтаксис Java является чем то вроде linva franca поэтому авторы книг часто выбирают её для примеров кода.
  • Мощные IDE, в частности Idea вообще вершина эволюции средств разработки и стоит в разы дешевле чем Visual Studio Ultimate для C# (700$ против 13300$).
  • Даже если кто и быдлокодит, это будет относительно легко разгрести через средства рефакторинга в IDE.
  • Кроссплатформенность, работает буквально на всём: от суперкомпьютеров до смарткарт и что особенно приятно на бесплатном Linux и FreeBSD.
  • Существуют около 350 гигабайт библиотек доступных в репозитории Maven. Для всего всего всего. И они в большинстве своём открытые, да.
  • В следствии очень дешёвая разработка.
  • Очень много программистов. Java самый популярный язык. Есть официальная сертификация программистов OSCJP.
  • Открытая стандартизация JSR, спецификации Java EE.
  • Компилируется в байт код, а не требует полной интерпретации как например Ruby и Python, что ускоряет работу. А благодаря оптимизирующему JIT компилятору может приближаться по скорости к нативному коду.
  • И последняя но далеко не последняя по важности: Полная обратная совместимость по API и ABI ещё с самых первых версий. Седьмая Java это всего лишь минорная версия, т.е. 1.7. Ни один другой язык не может похвастаться такой обратной совместимостью, даже C#.

Вывод

Остальные платформы и языки тоже хороши и востребованы и некоторые из них сейчас переживают бум. На их фоне Java выделяется самой сильной позицией и неплохими, пускай не самыми большими зарплатами. Но поскольку Java используется в корпоративном секторе и все проекты на ней большие то практически полностью отсутствует возможность фриланса на ней. Например количество вакансий на самой популярной фриланс бирже oDesk по запросу Spring (самый популярный веб фреймворк для Java) совсем не впечатляет.
Поэтому многим из тех кто хочет начать карьеру программиста но живут в городах где не устроится программистом прийдётся подаваться на фриланс и педалить на гнусном PHP.

«Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует.»
(c) Бьёрн Страуструп, автор C++

И Ява тоже далека от идеала. Кстати комментарий из этой статьи:
>> Ведь основная цель java была в облегчении жизни нам — простым разработчикам.
Это вас кто-то обманул. Главное преимущество джавы — надежность и предсказуемость. Ценой многократных, по сравнению с другими языками, усилий разработчиков.

Но это не самое важное, а главное «Вы должны писать на языке, который делает вас счастливее» (Пэт Аллан).


P.S. Наткнулся на очень толковое обсуждение где очень толковые специалисты высказали своё ведение проблемы.

понедельник, 20 августа 2012 г.

Dev database

HQSQL крут тем что сама БД в файл сохраняется как SQL скрипт который её создаёт и начиняет данными.

Это очень удобно, не нужно дополнительного клиента чтобы посмотреть содержимое БД.
Можно ложить его в VCS и отслеживтаь изменения.

А вот H2 так не умеет, у него БД имеет бинарный формат. Это кстати странно, потому что H2 разрабатывал автор HQSSQL, и вообще H2 значит Hipersonic 2.
Ну, ну конечно, можно дампится в SQL и его уже ложить под контроль VCS, но зачем лишние телодвижения?

вторник, 19 июня 2012 г.

[Перевод] Способы сравнения объектов дат в Java

Аннотация от переводчика

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

Итак, давайте обрисуем проблему — сравнение объектов Date в Java известный источник ошибок.
Это появляется в стандартном коде, но также бывает и в тестирующем коде, где нам регулярно нужно создавать объекты Date которые отмечают определенный момент времени на который потом будем ссылаться в сравнении.

Старый добрый устаревший и не рекомендуемый путь

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

Date date = new Date(112, 5, 3);
Date userCreatedDate = user.getCreatedDate();
if (userCreatedDate.equals(date)) { // Если даты равны…
  // делаем что нибудь…
}
Преимущества: это лаконично. Недостатки: это весьма не очевидно и вам нужны хорошие знания Java API чтобы знать что первый параметр это год минус 1900 и второй это индекс месяца который начинается с ноля для Января. И это сюрприз когда вы узнаёте что последний параметр это просто… просто день месяца.

Канонический способ

Начиная с Java 1.1 в Java API был добавлен класс Calendar чтобы разделить момент во времени (т.е. дату) от её представления в специфическом справочнике (календарь). Следующий фрагмент кода (snippet) это способ получения такого же результата как выше.

Calendar calendar = Calendar.getInstance();
calendar.set(YEAR, 2012);
calendar.set(MONTH, JUNE);
calendar.set(DAY_OF_MONTH, 3);

Это не только более многословно, тут также есть ошибка: часы, минут и остальное не равно нолю (берётся от момента непосредственного создания календаря), поэтому сравнение через equals() будет возвращать false. Вот правильный код:
Calendar calendar = Calendar.getInstance();
calendar.set(YEAR, 2012);
calendar.set(MONTH, JUNE);
calendar.set(DAY_OF_MONTH, 3);
calendar.set(HOUR_OF_DAY, 0);
calendar.set(MINUTE, 0);
calendar.set(SECOND, 0);
calendar.set(MILLISECOND, 0);

По меньшей мере это ухудшает краткость ;-)

Apache Commons Lang

Apache Commons изначально предоставляет различные утилитные библиотеки которые облегчают разработку в Java. Одна из таких библиотек Apache Commons Lang которая предоставляет функционал который заслуживает быть частью Java API. В нашем случае класс DateUtils позволит нам сократить код сохранив при этом его читаемость:

Calendar calendar = Calendar.getInstance();
calendar.set(YEAR, 2012);
calendar.set(MONTH, JUNE);
calendar.set(DAY_OF_MONTH, 3);
calendar = DateUtils.truncate(calendar, DAY_OF_MONTH); 

Даже лучше, DateUtils позволяет нам работать непосредственно с объектами Date в таком альтернативном виде:
Date date = new Date();
date = DateUtils.setYears(date, 2012);
date = DateUtils.setMonths(date, JUNE);
date = DateUtils.setDays(date, 3);
date = DateUtils.truncate(date, DAY_OF_MONTH);

Обратите внимание что он оставляет параметры нетронутыми, достигая неизменяемости (immutability) по принципа функционального программирования.
Преимущества: мы используем стандартное Java API.
Недостатки: да никаких. И ещё, не будет ли полностью своеобразный DSL казаться чем-то более подходящим?


Joda Time

Последний вариант это использование библиотеки Joda Time, которая нацелена стать заменой для Date и Calendar. Она также породила JSR-310 — новый и улучшенный API для манипуляции с датой и временем, который должен стать частью Java 8 (он был изначально запланирован для Java 7). Joda Time стоит посвятить отдельную статью (или даже мини-руководство). Для наших текущих нужд следующий фрагмент кода может выгодно заменить наш изначальный:
DateMidnight dm = new DateMidnight(2012, 6, 3);

Если сравнивать с первым примером, такой код кажется чище и лаконичней. И ещё, параметры самоописывающие, нет реальной нужды регулярно проверять JavaDocs чтобы узнать как инициализируется год. Кроме того, семантика имён классов ясна. И наконец, метод toDate() даёт нам мост к стандартному Java API.

Заключение

Вывод делайте сами. Лично я обычно использую Apache Commons Lang, но в последнее время склоняюсь к Joda Time. Архив с примерами кода доступен для скачивания тут в виде Maven проекта под Eclipse.

Автор: Nicolas Fränkel
Оригинал Ways of comparing Date Objects in Java.

суббота, 26 мая 2012 г.

Why do I hate Maven?

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

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

Документация к мавену ужасна. Только гугл, только stack-overflow.

Кодировка исходников и версия джавы под которую они написаны прописывается неявным образом:
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jdk>1.6</jdk>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupid>org.apache.maven.plugins</groupId>
                <artifactid>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>

            <plugin>
                <groupid>org.apache.maven.plugins</groupId>
                <artifactid>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${jdk}</source>
                    <target>${jdk}</target>
                    <compilerversion>${jdk}</compilerVersion>
                </configuration>
            </plugin>
        </plugins>
    </build>

И даже тут есть проблема: нужно укзывать версию стандартных плагинов. Какую ёё прописывать непонятно.

Упаси бог написать вам свой java agent - они не поддерживаются на уровне конфигурации мавена. И даже плагина ещё нет.
В итоге для прогона юнит тестов библиотеки javaagent приходится изголятся таким образом:
<plugin>
                <groupid>org.apache.maven.plugins</groupId>
                <artifactid>maven-surefire-plugin</artifactId>
                <configuration>
                    <forkmode>pertest</forkMode>
                    <argline>-javaagent:${project.build.directory}${file.separator}${project.build.finalName}.${project.packaging}</argLine>
                    <workingdirectory>${project.build.directory}</workingDirectory>
                </configuration>
            </plugin>
И при первом прогоне оно не работает, посокольку джарника ещё нет, а мы уже к нему обращаемся.
Но как ни странно стадия тестов от этого не валится и он таки билдитcя. Мрак.

И так же тут всплывает ещё одна проблема: в пропертях мавена нет пути к сбилдженому артефакту.
Приходится самому её имитировать:
${project.build.directory}${file.separator}${project.build.finalName}.${project.packaging}

Кстати сами проперти в отдельный файл никак не вынесеш, как это сделано в анте.

settings.xml явно не самая удобная вещь. И непонятно почему репозитории не перенесены туда.

Не до конца понятный build lifecycle:
  • clean и site запускается только вручную, по сути это самостоятельные таргеты а пункты лайфцайкла.
  • install это совсем не то что что и install в make.
  • deploy тоже у всех ассоциируется немного с другим процессом.


С версииированием артифактов всё тоже не всегда хорошо. Дописывать alpha beta к релиз пакетам геморно (через плагин). А ведь это вполне стандартная система версиирования.

Вообщем недостатков много, и всё равно в основе мавена лежит очень правильная идея: описывать проект, а не процесс его создания. Это очень правильная философия.

среда, 1 февраля 2012 г.

Инспекции Idea

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

Я немного потрудился на выходных и написал простой плагин к Идее который сгенерировал список инспекций. Вот он:


Теперь просто скопируйте его себе и покажите команде. Уверен дискуссия будет жаркой и познавательной :)

вторник, 31 января 2012 г.

Модификаторы в Java для начинающих (public, private, static)

Начинающие изучать Яву часто путаются в модификаторах.
Например когда объявлять классы публичными, а когда приватными?
Вот пару простых советов которые я частенько даю начинающим пилить Джаву. Они помогут вам выжить первое время:
  • Классы всегда создавайте публичными.
  • Не делайте вложенные классы (inner classes).
  • Все поля классов всегда создавайте приватными а для доступа к ним генерируйте гетеры и сетеры. Внутри класса обращайтесь к ним напрямик, а снаружи только через гетеры.
  • Как бы это не было тяжело, не создавайте статических полей или методов. Исключение константы — всегда объявляйте их публичными, статическими и финальными public static final.
  • Приватное private или защищённое protected поле это не для безопасности! Это просто уcловная черта для скрытия деталей от других классов чтобы вы случайно не поменяли извне сугубо внутреннее поле.

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

вторник, 24 января 2012 г.

Java Evenings в Cogniance

Побывал сегодня на Java Evenings в Cogniance. Понравилось. В голове всё перемешалось, но всё равно отпишусь.

Первым выступил Дмитирий Думанский с полезным и интересным докладом «Размер объектов в Java».
Для себя вынес следующее:
  • «Аппаратно» (т.е. не математично-алгоритмично) оптимизировать мы можем: память, процессор, ввод/вывод, блокировки.
  • Java прожорлива по памяти, приложения на джаве как правило отъедают не менее 10% всей памяти. И именно её и нужно оптимизировать в первую очередь.
  • Память сжирают: утечки, много данных и большие данные (? я разницу понял только интуитивно и забыл переспросить).
  • Даже более менее адекватно написанное приложение может вполне поймать неадекватный OutOfMemoryException
  • Оверхед на память способен довести бородатого сишника до инфаркта.

А теперь в цифрах.
У каждого объекта есть заголовок который всегда занимает 8 байт, из которых 4 байта это обязательный хешкод а остальные четыре отданы под другие ништяки, вроде Type Pointer.
Все поля выравниваются до double word, в итоге даже просто byte поле отъест 8 (ВОСЕМЬ!) байт. Однобитный примитивный булен тоже, но уже не факт, ибо его размер вообще не специфицирован плюс JVM их агрессивно оптимизирует.
Отсюда следует, что размеры все объектов всегда кратны 8ми. Можно этим затроллить синьора на собеседовании ;)

Объектные обвёртки примитивных типов - крайне плохая идея с точки зрения памяти. Зато их проще всего оптимизировать.
Если у вас есть not null поле
@NotNull
Long id;
то можно абсолютно безболезненно его сделать примитивным
@NotNull"
long id;

Дальше все ахнули узнав что строка из трёх букв съест 72 байта. Поэтому ещё одной из популярных оптимизаций является отказ от строк в качестве ключей во всяких там Map.
Пустой хешсет занимает около 200 байт. Хешсет из Guava уже два кибибайта.

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

Дальше больше, 64хбитная JVM расходует памяти на заголовки почти в два раза 32битной.
В итоге при переходе на 64битный JVM у вас вдруг откуда не возьмись... OutOfMemoryException.

Закончил Дмитрий предостережением от преждевременной оптимизации.

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

UPD
Дмитрий Думанский отметил множество технических неточностей в моём отчёте и любезно предоставил презентацию и статью на хабре. Настоятельно рекомендую.



Затем Андрей Гусев взбодрил всех докладом «Funny Java» на половину состоявшим из классических задачек SCJP и Java Puzzlers (ссылка для копимистов).
В очередной раз поймал себя на мысли сколько неочевидного можно встретить и как легко попасть на очевидном.

Выводы: 1) Нужно сертифицироваться, во время подготовки машинально запомнишь такие приколы. 2) Не экономьте на IDE, и включите все инспекции, а там как поймёте для чего они, то если что отключите. Лучше перестараться, чем недоглядеть.


Последним выступил Дмитирий Пекарь с рассказом про Hazelcast.
Это был не сколько доклад, а сколько формат беседы и обменом опыта.
Для себя вынес: Легко подключаемый и конфигурируемый джарник, который предоставляет деценетрализированое In-Memory хранилище (O_o синхронное?), с внятным API и возможностью писать запросы на чём-то похожем на SQL. Я правда не очень понял чем он от Касандры отличается.


Потом было пица и пивко, что было кстати очень кстати.

Спасибо огромное докладчикам, организаторам, всем присутствующим и компании Cogniance растящей таких специалистов!

пятница, 12 августа 2011 г.

Singleton не так прост

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

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

Singleton это практически обход проблемы что нельзя создать сразу сложный объект не описав его класс.

Например, в одном из самых навороченных языков программирования Scala для него ввели специальную конструкцию object которой заменили статические поля и методы. В JavaScript, как и во всех прототипных языках, мы вообще по сути сразу создаём такие объекты.

Так что они имеют точно такое же право на жизнь как например интерфейсы, которые частично можно заменить полностью абстрактными классами. Но это уже на правах холивара.

Если вы решили бороться с сиглтонами, то к вашим услугам в Idea есть специальная инспекция которая определяет синглтоны как проблемы. Также в Google написали инструмент Singleton Detector который находит синглетоны и помогает от них избавлятся.

Критику можно почитать на вики проекта и в энциклопедии хороших практик Cunningham & Cunningham.

Беглый осмотр матчасти

Я сделал небольшой Copy & Paste конспект из нагугленого матана, главным образом из этих статей:

Пример 1. Простейший, идиоматический синглтон

public class Singleton  {
 private static final Singleton instance = new Singleton();
 
 // Private constructor prevents instantiation from other classes
 private Singleton() {
 }
 
 public static Singleton getInstance() {
  return instance ;
 }
}
Именно такой и генерируется в Idea.

Пример 2. С отложенной инициализацией

В первом примере экземпляр класса instance статический, и создаётся сразу при загрузке класса. За всё время работы програмы он может так и не пригодится, поэтому делают ленивую инициализацию.
public class Singleton  {
 private static Singleton instance = null;
 
 // Private constructor prevents instantiation from other classes
 private Singleton() {
 }
 
 /**
  * Метод возвращает экземпляр SomeObject, при этом он
  * создаёт его, если тот ещё не существует
  */
 public static Singleton getInstance() {
  if (instance == null)
   instance = new Singleton();
  return instance;
 }
}
Проверка, создан уже экземпляр или нет очень дешёвая, поэтому по возможности старайтесь делать отложенную инициализацию.

Пример 3. Потокобезопасный

Несколько потоков могут одновременно дёргнуть getInstance() и он создаст несколько экземпляров класса. Чтобы этого избежать создание экземпляра делают в блоке synchronized
public class Singleton  {
 private static Singleton instance = null;
 
 // Private constructor prevents instantiation from other classes
 private Singleton() {
 }
 
 /**
  * Метод возвращает экземпляр SomeObject, при этом он
  * создаёт его, если тот ещё не существует
  */
 public static synchronized Singleton getInstance() {
  if (instance == null)
   instance = new Singleton();
  return instance;
 }
}
Продолжение следует...

UPD Продолжения не следует, тему раскрыли на хабре здесь и здесь

среда, 6 апреля 2011 г.

Интересные задачки из экзамена Oracle Certified Professional Java Programmer

На Хабре пару месяцев назад было две статьи (первая и вторая часть) с интересными задачками из экзамена по сертификации Oracle Certified Professional Java Programmer (бывший Sun Certified).

Если кому интересно, то я набил эти примеры и выложил их в репозиторий на GitHub.

Удачной сертификации.

вторник, 5 апреля 2011 г.

Возможности Java: статический импорт

Эта статья была размещена на сайте sun.ru, но позже она стала недоступной. Я её вытащил из кеша поисковика и сохранил на всякий случай здесь.

Аккуратное и правильное использование import static улучшит читаемость вашего кода.

Статический импорт

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

   double r = Math.cos(Math.PI * theta);

Чтобы обойти это, иногда добавляют статические методы в интерфейс и наследуются от этого интерфейса. Это плохая идея. Фактически это настолько плохая идея, что для нее есть свое название: Constant Interface Antipattern (см. Effective Java, 17 статья). Дело в том, что использование статических членов класса другим классом всего лишь деталь реализации. Когда же класс реализует интерфейс, его члены становится частью публичного АРI этого класса. Детали реализации не должны становиться публичным программным интерфейсом.

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

  import static java.lang.Math.PI;
или все целиком:
  import static java.lang.Math.*;

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

   double r = cos(PI * theta);

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

Так когда же следует использовать статический импорт? Только в некоторых случаях! Используйте его только, если иначе вы вынуждены объявлять локальные копии констант или при неправильном использовании наследования (Constant Interface Antipattern).

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

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

Перевод Комарова Е., Дмитриев А., 24.12.2007 г.

Константы, перечисления (enum), и static import'ы в Java

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

Константа — это, грубо говоря, переменная значение которой не меняется во время работы программы. Классический пример константы — число Пи (3,14...). Константы используются для борьбы с Магическими числами, т.е. непонятно что означающими числами или строками.

В Java нет специальной директивы для объявления констант как например const в Pascal/Delphi или в C/C++. Т.е. не выйдет написать такое:

const double PI = 3.14;

Но в замен есть более гибкий модификатор final:

final double PI = 3.14;
В этом примере была объявлена финальная переменная с названием PI и ей было сразу же задано значение 3.14. Финальным переменным значения можно задать только один раз и больше его нельзя менять. Любую попытку поменять значение финальной переменной (или поля) компилятор будет воспринимать как ошибку, т.е. всё так же как и с константами.

Разница между финальным переменной и константой в том что инициализацию можно отложить:

final String someFinalVariable; // Объявляем финальную переменную, но мы её не проинициализировали значением!
System.out.println(someFinalVariable); // Ошибка компиляции: значение не задано. javac: variable not have been initialized
someFinalVariable = "Some value"; // Наконец задаём значение
System.out.println(someFinalVariable); // Выводим содержимое переменной
someFinalVariable = "New value"; // Ошибка компиляции: значение переменной уже задано. javac: variable someFinalVariable already have been assigned
Такая отложенная инициализация например часто используется для создания неизменяемых объектов (англ. Immutable objects).

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

public static final double PI = 3.14;
Статическое поле (модификатор static) принадлежит классу и для доступа к нему не нужно создавать конкретный экземпляр объекта:
public class Math {
    public static final double PI = 3.14;
}

public class Test {
    public static void main(String[] args) {
        double d = 3 * Math.PI; // Мы не создавали объект класса Math а обратились к полю самого класса!
        System.out.println(d);
    }
}

Обратите внимание что в Яве, как и во всех Си подобных языках, принято имена констант писать БОЛЬШИМИ_БУКВАМИ_РАЗДЕЛЯЯ_ИХ_ПОДЧЁРКИВАНИЕМ.

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

static final double SECONDS_IN_DAY = 60 * 60 * 24; // Сколько секунд в дне
При этом всё вычисляется на этапе компиляции и не нужно заменять 60 * 60 * 24 на менее читаемое и очевидное уже вычисленное вами 86400 (к тому же возможно неправильно рассчитанное).

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

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

public class Modifier {
    public static final int PUBLIC = 1;
    public static final int PROTECTED = 2;
    public static final int PRIVATE = 3;
    public static final int ABSTRACT = 4;
    public static final int STATIC = 5;
    public static final int FINAL = 6;
    public static final int TRANSIENT = 7;
    public static final int VOLATILE = 8;
    public static final int SYNCHRONIZED = 9;
    public static final int NATIVE = 10;
    public static final int STRICTFP = 11;
}

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

  • Небезопасность типа (type unsafe): в методе, где требуется передать значение перечисления, можно передать любое число, а не только значения от 1 до 11;
  • Неинформативность: например, при отладке значение 3 не скажет нам ни о чем. А хотелось бы увидеть PRIVATE;
  • Подверженность ошибкам: например, при добавлении нового элемента или при изменении последовательности существующих. Например можно не уследить, и у PUBLIC и PROTECTED поставить одинаковое значение 1. Т.е. отсутствует контроль со стороны компилятора как за уникальностью значений констант, так и за возможностью случайного присваивания переменным значений, не соответствующих ни одной из этих констант.

Такие сгруппированные константы называются перечислениями и в Java для них есть специальная конструкция enum. Перепишем этот класс как перечисление:

enum Modifier {
    PUBLIC,
    PROTECTED,
    PRIVATE,
    ABSTRACT,
    STATIC,
    FINAL,
    TRANSIENT,
    VOLATILE,
    SYNCHRONIZED,
    NATIVE,
    STRICTFP;
}
И использовать его можно как обычную константу:
Modifier variableModifier = Modifier.PUBLIC;

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

    // Так не делайте!
    if (modifier.eqals(Modifier.PUBLIC)) {
        // ....
    }
    // Просто сравнивайте по ссылке ;)
    if (modifier == Modifier.PUBLIC) {
        // ....
    }


Вообще тут нужно отметить что раньше, до пятой версии Java перечислений не было, вместо них приходилось использовать классы со статическими константами. Это я говорю к тому, что многие классы с которыми вы столкнётесь остались написанными «дедовским» способом. Причём в стандартных классах их не меняли, например java.awt.Color.

Технически перечисления представляют собой полноценный класс который наследуется от java.lang.Enum, т.е. запись public enum Modifier равноценна abstract class Modifier extends java.lang.Enum.

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

enum Modifier {
    PUBLIC,
    PROTECTED,
    PRIVATE,
    ABSTRACT,
    STATIC,
    FINAL,
    TRANSIENT,
    VOLATILE,
    SYNCHRONIZED,
    NATIVE,
    STRICTFP;

    /**
     * Возвращает это имя модификатора в нижнем регистре.
     */
    @Override
    public String toString() {
        String lowercase = name().toLowerCase(java.util.Locale.US); 
        return lowercase;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Modifier variableModifier = Modifier.PUBLIC;
        System.out.println(variableModifier);
    }
}

При запуске этого примера в вывод будет напечатано public а не 1! У каждого enum'а хранится поле name с его именем (в нашем случае "PUBLIC"). Мы переопределили (@Override) метод toString() внутри которого приводим имя к нижнему регистру.

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

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

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

Перечисления естественно подходят не для всех случаев. Например константа натурального логарифма e и константу π нельзя объединять в перечисление. Давайте посмотрим на класс содержащий эту константы:

public class Math {
    public static final double E = 2.71;
    public static final double PI = 3.14;
}

Все его поля public static final. Чтобы каждый раз не писать эти модификаторы многие программисты объявляют константы в интерфейсе, потому что все его поля итак уже public static final:

interface Math {
    double E = 2.71;
    double PI = 3.14;
}

И вот кому то стрельнула идея: «А зачем каждый раз писать полное имя интерфейса с константами, если можно его заимплементить и получить к ним доступ напрямую?». И такие конструкции

public class Test {
    public static void main(String[] args) {
        double d = 3 * Math.PI; 
        System.out.println(d);
    }
}
превратились в такой кошмар
public class Test implements Math {
    public static void main(String[] args) {
        double d = 3 * PI; // Math. можно уже не писать 
        System.out.println(d);
    }
}

Так вот, так делать нельзя, это антипаттерн. Фактически это настолько плохая идея, что для нее есть свое название: Constant Interface Antipattern (см. Effective Java, 17 статья. Эта книга вообще обязательная для прочтения всем Ява разработчикам). Дело в том, что использование статических членов класса другим классом всего лишь деталь реализации. Когда же класс реализует интерфейс, его члены становится частью публичного АРI этого класса. Детали реализации не должны становиться публичным программным интерфейсом. Вместо этого используйте статические импорты.

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

import static Math.PI;
или все целиком:
import static Math.*;

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

double d = 3 * PI; // Math. можно не писать 

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

Так когда же следует использовать статический импорт? Только в некоторых случаях! Используйте его только, если иначе вы вынуждены объявлять локальные копии констант или при неправильном использовании наследования. Другими словами, использование его оправданно, когда требуется постоянное использование статических членов одного класса из одного или двух других классов.

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

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

Как правильно объявить константный класс?

Если всё таки очень надо, то объявляйте константный класс как обычный класс, а не интерфейс. Только делайте его ещё и финальным (final), чтобы от него нельзя было унаследоваться. А ещё было бы неплохо сделать его абстрактным (abstract), чтобы нельзя было создать его экземпляр, ведь все коснтанты у нас статические и мы обращаемся к полям класса а не объекта.

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

public final class Math {
    public static final double E = 2.71;
    public static final double PI = 3.14;
    private Math() {
    }
}

Заключение

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

вторник, 29 марта 2011 г.

Инспекция патерна Singleton для Idea

Самая интеллектуальная IDE Идея сегодня стала ещё умней. А всё потому что я вот написал для неё плагинчик-инспекцию который проверяет корректность применения шаблона проектирования Одиночка (англ. Singleton). Так сказать, внёс лепту в борьбу с говнокодом. Исходники кстати доступны из репозитория на GitHub.

Скриншотик

Итак, ним проверяется что:

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

Проверка является ли класс одиночкой делается в лоб — по наличию метода getInstance(). Не очень красиво, хотя будет ещё некрасивее если у вас он будет называться по другому.

Для решения этой проблемы можно использовать аннотации. В Groovy, например, уже давно есть стандартная аннотация @Singleton, которой можно пометить класс-одиночку. И в Идее даже есть инспекция которая проверяет одиночки определяя их по этой аннотации.

Для Явы уже есть целая библиотека аннотаций шаблонов проектирования JPatterns. У меня в планах наделать проверок для остальных шаблонов проектирования анализируя аннотации этой либы. Так что to be continued.

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

P.S. А ещё getInstance() может принимать параметры, эта ситуация нормальна и просто игнорируются этой инспекцией.

пятница, 25 марта 2011 г.

[Перевод| Анализ исходного кода с помощью Java 6 API

Вы когда-нибудь задумывались как инструменты вроде Checkstyle или FindBugs выполняют статический анализ кода, или как IDE такие как NetBeans или Eclipse выполняют быстрое исправление кода или находят ссылки на поле, объявленное в вашем коде? Во многих случаях, IDE имеют свои собственные API для разбора исходного кода и генерируют стандартную древовидную структуру, называемую aбстрактное синтаксическое дерево (AST) и разбирают дерево, которое может быть использовано для глубокого анализа элементов исходного кода. Хорошая новость: теперь возможно выполнять описанные задачи и многое другое с помощью трёх новых API введённых в Java как часть шестой редакции стандарта Java Standard Edition 6. Этими API, которые могут быть интересны разработчикам Java приложений которым нужно выполнять анализ исходного кода, являются Java Compiler API (JSR 199), Pluggable Annotation Processing API (JSR 269), и Compiler Tree API.

В этой статье мы рассмотрим возможности каждого из этих API и разработаем простое демо приложение, которое проверяет некие правила кодирования на Java на наборе исходных файлах переданных в него. Эта утилита также выводит сообщения о нарушения кодирования и местонахождение нарушения. Рассмотрим простой класс, который переопределяет (англ. overrides) метод equals() класса Object. Правило кодирования, которое проверяется, заключается в том, что каждый класс, который реализует метод equals() должен также переопределять метод hashCode() с правильной сигнатурой. Вы можете увидеть, что класс TestClass описанный ниже не объявляет метод hashCode(), хотя он имеет метод equals().

public class TestClass implements Serializable {
 
  int num;

  @Override
  public boolean equals(Object obj) {
        if (this == obj)
                return true;
        if ((obj == null) || (obj.getClass() != this.getClass()))
                return false;
        TestClass test = (TestClass) obj;
        return num == test.num;
  }
}

Давайте проанализируем этот класс как часть процесса компиляции с помощью этих трёх API.

Вызов компилятора программно из кода: Java Compiler API

Мы все используем утилиту командной строки javac для компиляции исходных файлов Java в class файлы. Так зачем же нам нужно API для компиляции Java файлов? Хорошо, ответ очень прост: как ясно из названия, это новое стандартное API позволяет нам вызывать компилятор наших собственных Java приложений; т.е., вы можете программировано взаимодействовать с компилятором и тем самым сделать компиляцию частью сервисов уровня приложения. Несколько типичных использований этого API перечислены ниже.

  • API компилятора помогает серверам приложений минимизировать время занимаемое развёртыванием (deploying) приложений. Например, избегая накладных расходов от использования внешнего компилятора для компилирования исходников сервлета сгенерированных из JSP страниц.
  • Инструменты разработки вроде IDE и анализаторы кода могут вызывать компилятор во время редактирования или утилиты сборки, которые значительно занимают время компиляции.

Классы Java компилятора расположены в пакете javax.tools. Класс ToolProvider из этого пакеты предоставляет метод getSystemJavaCompiler() который возвращает экземпляр определенного класса, который реализует интерфейс JavaCompiler. Этот экземпляр компилятора может быть создан для постановки компиляционных задач, которые будут выполнять саму компиляцию. Файлы исходного кода, которые нужно скомпилировать, должны быть переданы в компиляционную задачу. Для этого, API компилятора предоставляет абстракцию файлового менеджера JavaFileManager, который позволяет Java файлам быть полученными из различных источников, такие как файловая система, базы данных, память, и другие. В этом примере мы используем StandardFileManager, файл менеджер, основанный на java.io.File. Стандартный файловый менеджер может быть получен вызовом метода getStandardFileManager() из экземпляра JavaCompiler . Фрагмент кода для выполнения указанных шагов рассмотрен ниже:

  // Берём новый экземпляр Java компилятора
  JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  // Берём новый экземпляр реализации стандартного файлового менеджера
  StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
  // Получаем список файловых объектов java, в нашем случае мы имеем только один файл, TestClass.java
  Iterable<? extends JavaFileObject> compilationUnits1 = fileManager.getJavaFileObjectsFromFiles("TestClass.java");

Диагностический слушатель может быть опционально передан в метод getStandardFileManager() для выполнения диагностических отчётов о любых не фатальных проблемах. В этом фрагменте кода, мы передаём значения null, то есть мы не собираем диагностику из утилиты. Детали других параметров передаваемых в методы, вы можете узнать в Java 6 API. Метод getJavaFileObjectsfromFiles() класса StandardJavaFileManager возвращает все экземпляры StandardJavaFileManager, которые соответствуют предоставленным исходным Java файлам.

Следующим шагом будет постановка компиляционной задачи, которая может быть выполнена использованием метода getTask() класса JavaCompiler. В этом месте, компиляционная задача ещё не начинается. Задача может быть начата вызовом метода call() класса CompilationTask. Фрагмент кода для создания и запуска компиляционной задачи показан ниже.

  // Создаём задачу компиляции
  CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits1);
  // Выполняем задачу компиляции
  task.call();
Если, ошибок компиляции нет, то в папке назначения сгенерируется файл TestClass.class.

Обработка аннотаций: Подключаемое Annotation Processing API

Как мы все знаем, стандарт Java SE 5.0 внедрил поддержку для добавления и обработки метаданных или аннотаций к элементам вроде Java классов, полей, методов и прочего. Аннотации полностью обрабатываются сборочными утилитами или окружениями выполнения (runtime environments) для выполнения полезных задач наподобие управления поведением приложения, генерации кода, и.т.п.. Java 5 позволяет обрабатывать аннотированные данные, как во время компиляции, так и во время работы приложения. Обработчики аннотаций это утилиты, которые могут быть динамически подключены в компилятор для анализа исходных файлов и обработки аннотаций в них. Обработчики аннотаций могут полностью использовать метаданные для выполнения многих задач, включая и не ограничиваясь следующим:

  • Аннотации могут быть использованы для генерации дескрипторных файлов для развёртки приложений вроде persistence.xml или ejb-jar.xml, в случаях entity классов и enterprise beans соответственно.
  • Обработчики аннотаций могут использовать метаданные для генерации кода. Например, обработчик может генерировать интерфейсы Home и Remote для правильно аннотированного enterprise bean.
  • Аннотации могут быть использованы для проверки кода.

Стандарт Java 5.0 предоставил инструментарий обработки аннотаций (APT) и связанное зеркальное Reflection API (com.sun.mirror.*) для обработки аннотаций и моделирования обработанной информации. Инструмент APT запускает соответствующие обработчики аннотаций встречающихся в исходном файле. Зеркальное Reflection API предоставляет компиляционное, доступное только для чтения представление исходного файла. Главный недостаток APT в том, что он не стандартизирован; т.е. APT специфичен для Sun JDK.

Стандарт Java SE 6 ввёл новый инструментарий, называемый Pluggable Annotation Processing framework, который предоставляет стандартную поддержку для написания собственных обработчиков аннотаций. Он называется подключаемым ("pluggable") потому что обработчик аннотации может быть подключен в javac динамически, и может оперировать набором аннотаций которые встречаются в исходном коде. Этот инструментарий (framework) разделён на две части: API для объявления и взаимодействия с обработчиками аннотаций — пакет javax.annotation.processing, и API для моделирования языка программирования Java — пакет javax.lang.model.

Написание собственного обработчика аннотаций

В следующей секции описано как создать собственный обработчик аннотаций и подключить его к компиляционной задаче. Обработчик аннотации наследуется от AbstractProcessor (который является реализацией интерфейса Processor) и переопределяет его метод process().

Класс обработчика аннотации будет задекорирован двумя аннотациями классового уровня, @SupportedAnnotationTypes и @SupportedSourceVersion. Аннотация SupportedSourceVersion определяет последнюю поддерживаемую версию обработчика аннотации. Аннотация @SupportedAnnotationTypes помечает в каких аннотациях этот обработчик заинтересован. Например, @SupportedAnnotationTypes ("javax.persistence.*") будет использован если обработчику нужно обрабатывать только аннотации Java Persistence API (JPA).

Стоит отметить, что обработчик аннотации вызывается даже когда не предоставлено аннотаций, если указана поддержка типов аннотаций как @SupportedAnnotationTypes("*"). Это позволяет нам использовать все возможности API моделирования вместе с Tree API для основных нужд обработки исходного кода. Используя эти API, можно получить много полезной информации касающейся модификаторов, полей, методов, и всего остального.

Фрагмент кода обработчика аннотации дан ниже:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("*")
public class CodeAnalyzerProcessor extends AbstractProcessor {
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    for (Element e : roundEnvironment.getRootElements()) {
      System.out.println("Element is " + e.getSimpleName());
      // Напишите здесь код для анализа каждого корневого элемента
    }
    return true;
  }
}

Обработчики аннотаций вызываются на те аннотации на которые они настроены. Обработка аннотаций может проходить в несколько раундов. Например, в первом раунде, будут обработаны первоначальные исходные Java файлы; во втором раунде, будут учтены файлы сгенерированные во время первого этапа обработки, и так далее. Обработчик должен переопределять метод process() класса AbstractProcessor. Этот метод принимает два аргумента:

  1. Набор типов/элементов/аннотаций найденных в исходном файле.
  2. RoundEnvironment который инкапсулирует информацию о текущем раунде обработки аннотации.

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

Подключение обработчика аннотаций

Теперь, когда наш обработчик аннотации готов к использованию, давайте посмотрим, как вызвать этот обработчик как часть процесса компиляции. Обработчик может быть вызван либо из утилиты командной строки javac либо программно через специальный Java класс. Утилита javac из Java SE 6 предоставляет опцию -processor которая принимает полное имя (fully qualified name) обработчика аннотаций который нужно подключить. Синтаксис для этого следующий:

javac -processor demo.codeanalyzer.CodeAnalyzerProcessor TestClass.java
где CodeAnalyzerProcessor это класс обработчика аннотации и TestClass это входящий для обработки Java файл. Эта утилита ищет CodeAnalyzerProcessor в classpath; т.е., нужно поместить этот класс в classpath.

Модифицированный фрагмент кода для подключения обработчика программно показан ниже. Метод setProcessors() класса CompilationTask позволяет подключить в задачу компиляции множество обработчиков аннотаций. Этот метод нужно вызвать перед методом call(). Также помните, что если обработчик аннотаций подключен к задаче компиляции, то сперва будет выполнена обработка аннотации, а только потом задача компиляции. Стоит отметить, что обработка аннотаций не произойдет если код содержит ошибки компиляции.

  CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits1);
  // Создаём список обработчиков аннотаций
  LinkedList<AbstractProcessor> processors = new LinkedList<AbstractProcessor>();
  // Добавляем обработчик аннотаций в список
  processors.add(new CodeAnalyzerProcessor());
  // Передаём список обработчиков аннотаций в компиляционную задачу
  task.setProcessors(processors);
  // Выполняем компиляционную задачу
  task.call();

Если мы выполним этот код, то обработчик аннотации во время компиляции TestClass.java будет печатать его название "TestClass".

Работа с AST: The Compiler Tree API

Aбстрактное синтаксическое дерево (AST) это доступное только для чтения представление исходного кода как дерево узлов, где каждый узел представляет языковую конструкцию Java. Например, Java класс представлен как ClassTree, объявления методов представлены как MethodTrees, объявления переменных как VariableTrees, аннотации как AnnotationTree и т.д..

Compiler Tree API предоставляет доступ к AST исходного кода Java и также предоставляет несколько утилит вроде TreeVisitors, TreeScanners, и т.д., для выполнения операций над AST. Более глубокий анализ исходного кода может быть выполнен используя TreeVisitor, который обходит все дочерние узлы дерева для извлечения необходимой информации о полях, методах, аннотациях, и других элементах класса. Посетители дерева реализованы в стиле шаблона проектирования Посетитель (Visitor). Когда Посетитель передаётся в метод accept дерева, то будет вызван метод visitXYZ который более подходит к этому дереву.

Java Compiler Tree API предоставляет три реализации TreeVisitor: SimpleTreeVisitor, TreeScanner, и TreePathScanner который и используется в нашем демо приложении. TreePathScanner это TreeVisitor который обходит все дочерние узлы дерева и предоставляет возможность для поддержки путей к родительским узлам. Для сканирования дерева нужно вызывать его метод scan(). Для посещения узлов необходимого типа, просто переопределите соответствующий visitXYZ метод. Внутри вашего посещающего метода вызовете super.visitXYZ для посещения дочерних узлов. Фрагмент кода полного класса Посетителя показан ниже:

  public class CodeAnalyzerTreeVisitor extends TreePathScanner<Object, Trees>  {

    @Override
    public Object visitClass(ClassTree classTree, Trees trees) {
      // ...какие нибудь действия
      return super.visitClass(classTree, trees);
    }

    @Override
    public Object visitMethod(MethodTree methodTree, Trees trees) {
      // ...какие нибудь действия
      return super.visitMethod(methodTree, trees);
    }

  } 

Как видите, методы-посетители принимают два аргумента: дерево отображающее узлы (ClassTree для узлов классов, MethodTree для узлов методов, и т.д.) и объект Trees. Класс Trees предоставляет утилитные методы для извлечения информации о пути элементов в дереве. Важно помнить, что объект Trees выступает мостом между JSR 269 и Compiler Tree API. В этом примере, есть только один корневой элемент, т.е. сам TestClass.

  CodeAnalyzerTreeVisitor visitor = new CodeAnalyzerTreeVisitor();

  @Override
  public void init(ProcessingEnvironment pe) {
    super.init(pe);
    trees = Trees.instance(pe);
  }
  for (Element e : roundEnvironment.getRootElements()) {
    TreePath tp = trees.getPath(e);
    // Вызов сканера
    visitor.scan(tp, trees);
  }

Следующая секция показывает получение информации исходного кода используя Tree API и вводит общую модель используемую для проверки кода далее. Метод visitClass() вызывается когда класс, интерфейс или перечисляемый тип (enum) посещается из AST с аргументом ClassTrees. Точно также метод visitMethod() вызывается для всех методов с аргументом MethodTree, visitVariable() для всех переменных с аргументом VariableTree, и т.д..

  @Override
  public Object visitClass(ClassTree classTree, Trees trees) {
    // Сохранение деталей посещяемого класса в модель
    JavaClassInfo clazzInfo = new JavaClassInfo();
    // Получаем текущий путь узла
    TreePath path = getCurrentPath();
    // Берём тип элемента соответствующего классу
    TypeElement e = (TypeElement) trees.getElement(path);
    // Устанавливаем полное определёное имя класса в модель
    clazzInfo.setName(e.getQualifiedName().toString());
    // Устанавливаем предка от которого унаследован класс 
    clazzInfo.setNameOfSuperClass(e.getSuperclass().toString());
    // Устанавливаем интерфейсы которые класс реализовывает
    for (TypeMirror mirror : e.getInterfaces()) {
      clazzInfo.addNameOfInterface(mirror.toString());
    }
    return super.visitClass(classTree, trees);
  }

Класс JavaClassInfo используемый в этом коде это наша модель для хранения информации о исходном коде. После выполнения этого кода, информация касающаяся класса вроде полного имени класса (fully qualified name), имени родительского класса, интерфейсов реализуемых TestClass, и т.д., будет извлечена и сохранена в нашей собственной модели для будущих проверочных нужд.

Установление местонахождения токенов в исходном коде

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

Объект SourcePositions, который является частью Compiler Tree API, предоставляет позиции всех AST узлов. Этот объект содержит полезную информацию о позициях начала и конца всех ClassTree, MethodTree, FieldTree, и т.д., в файле. Позиция определена как простая позиция символа (offset, сдвиг) с начала компилированного модуля (CompilationUnit) где первый символ занимает офсет 0. Фрагмент кода ниже показывает как можно получить офсетную позицию символа переданного объекта Tree с начала компилируемого модуля.

  public static LocationInfo getLocationInfo(Trees trees, TreePath path, Tree tree) {
    LocationInfo locationInfo = new LocationInfo();
    SourcePositions sourcePosition = trees.getSourcePositions();
    long startPosition = sourcePosition.getStartPosition(path.getCompilationUnit(), tree);
    locationInfo.setStartOffset((int)startPosition);
    return locationInfo;
  }

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

  // Получаем делево компилируемых модулей 
  CompilationUnitTree compileTree = treePath.getCompilationUnit();
  // Берём исходный файл java который будет обработан
  JavaFileObject file = compileTree.getSourceFile();
  // Извлекаем символьное содержимое файла в строку
  String javaFile = file.getCharContent(true).toString();
  // Конвертируем содержимое java файла в символьный буфер
  CharBuffer charBuffer = CharBuffer.wrap (javaFile.toCharArray()); 

Следующий фрагмент кода находит позицию имени класса из исходного кода. Используются классы java.util.regex.Pattern и java.util.regex.Matcher для получения первого вхождения токена совпадающего с именем класса в символьном буфере, начиная с позиции начала дерева класса из дерева компилируемого модуля.

  LocationInfo clazzNameLoc = (LocationInfo) clazzInfo.getLocationInfo();
  int startIndex = clazzNameLoc.getStartOffset();
  int endIndex = -1;
  if (startIndex >= 0) {
    String strToSearch = buffer.subSequence(startIndex, 
    buffer.length()).toString();
    Pattern p = Pattern.compile(clazzName);
    Matcher matcher = p.matcher(strToSearch);
    matcher.find();
    startIndex = matcher.start() + startIndex;
    endIndex = startIndex + clazzName.length();
  } 
  clazzNameLoc.setStartOffset(startIndex);
  clazzNameLoc.setEndOffset(endIndex);
  clazzNameLoc.setLineNumber(compileTree.getLineMap().getLineNumber(startIndex));

Класс LineMap из Complier Tree API предоставляет карту символьных позиций и номеров строк из CompilationUnitTree. Мы можем получить номер строки нужного токена передав позицию начала в метод getLineMap() дерева CompilationUnitTree.

Проверка исходного кода на соответствие правилам

Теперь, когда мы полностью получили необходимую информацию из AST, следующая задача проверить что исходный код удовлетворяет определённым стандартам кодирования. Правила кодирования настроены в XML файле и управляются через наш класс называемые RuleEngine. Этот класс берёт правила из XML файла и применяет их одно за другим. Если класс не удовлетворяет правилу, RuleEngine возвращает список объектов ErrorDescription. Класс ErrorDescription инкапсулирует сообщения об ошибках и местонахождение ошибок в исходном коде.

  ClassFile clazzInfo = ClassModelMap.getInstance().getClassInfo(className);
  for (JavaCodeRule rule : getRules()) {
    // Применяем правила один за одним 
    Collection<ErrorDescription> problems = rule.execute(clazzInfo);
    if (problems != null) {
      problemsFound.addAll(problems);
    }
  }

Каждое правило реализовано как Java класс; модельная информация класса для проверки передаётся в этот класс правила. Класс правила инкапсулирует логику проверки используя модельную информацию. Реализация простого правила (OverrideEqualsHashCode) приведена ниже. Это правило проверят что класс, который переопределяет метод equal(), должен также переопределять метод hashCode(). Здесь мы итерируем по методам класса и проверяем, следуют ли они контракту по equals() и hashCode(). В TestClass, метод hashCode() отсутствует хотя есть метод equals(), в результате чего правило возвращает объект ErrorDescription содержащий соответствующее сообщение об ошибке и местонахождение ошибки.

  public class OverrideEqualsHashCode extends JavaClassRule {

    @Override
    protected Collection<ErrorDescription> apply(ClassFile clazzInfo) {
      boolean hasEquals = false;
      boolean hasHashCode = false;
      Location errorLoc = null;
      for (Method method : clazzInfo.getMethods()) {
        String methodName = method.getName();
        ArrayList paramList = (ArrayList) method.getParameters();
        if ("equals".equals(methodName) && paramList.size() == 1) {
          if ("java.lang.Object".equals(paramList.get(0))) {
            hasEquals = true;
            errorLoc = method.getLocationInfo();
          }
        } else if ("hashCode".equals(methodName) && method.getParameters().size() == 0) {
          hasHashCode = true;
        }
      }
      if (hasEquals) {
        if (hasHashCode) {
          return null;
        } else {
          StringBuffer errrMsg = new StringBuffer();
          errrMsg.append(CodeAnalyzerUtil.getSimpleNameFromQualifiedName(clazzInfo.getName()));
          errrMsg.append(" : The class that overrides equals() should ");
          errrMsg.append("override hashcode()");
          Collection<ErrorDescription> errorList = new ArrayList<ErrorDescription>();
          errorList.add(setErrorDetails(errrMsg.toString(), errorLoc));
          return errorList;
        }
      }
    return null;
  }

}

Запуск демонстрационного приложения

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

Заключение

Эта статья рассказывает как новое Java 6 API может быть использовано для вызова компилятора из Java кода и как разбирать и анализировать исходный код используя подключаемые обработчики аннотаций и Посетители (visitors) деревьев. Используя стандартное API вместо специфичной для IDE логики разбора/анализа делает возможным переиспользовать код в разных инструментах и окружениях. Мы только поверхностно очертили здесь эти три компиляционные API; вы можете найти много более полезных возможностей покопавшись глубже в них.

Исходный код демонстрационного приложения также расположен на GitHub

© Seema Richard.

четверг, 24 марта 2011 г.

JPatterns: Java аннотации для шаблонов проектирования

Шаблоны проектирования кодируются в Java код как придётся. Их названия даже включаются в имена классов или записаны в Javadoc'и. К несчастью невозможно точно определить шаблон только основываясь на структуре класса без знаний намерений автора кода.

Помочь решить эту проблему может библиотека JPatterns созданная Michael Hunger и Heinz Kabutz.

JPatterns это коллекция аннотаций которе делают простым взаимодействие использования шаблонов проектирования с вашим кодом для ваших разработчиков и вами самими в будущем. Библиотека следует принципу KISS используя резонные умолчания для атрибутов аннотации. Так, если вы пишете Компоновщик (англ. Composite pattern), вы можете просто указать:

@CompositePattern
public abstract class Contact {
  public abstract void sendMail(String msg);
  public void add(Contact contact) {}
  public void remove(Contact contact) {}
}
Или, если хотите, вы можете также указать точнее, например:
@CompositePattern(role = CompositeRole.COMPONENT,
 participants = {DistributionList.class, Person.class})
public abstract class Contact {
  public abstract void sendMail(String msg);
  public void add(Contact contact) {}
  public void remove(Contact contact) {}
}

JavaDoc проекта впечатляет: реализованы аннотации для GoF, JEE и PLoPD.

Этот проект поможет улучшить качество кода относительно малой кровью.

Например недавно я написал плагин-инспекцию для Idea который проверяет корректность реализации шаблона Одиночка (англ. Singleton pattern). Проверяется в частности что конструктор объявлен приватным. И мне пришлось определять Singleton чисто по наличию метода getInstance(), т.е. практически наугад. Если бы классы-одиночки были бы отмечены аннотацией @SingletonPattern, то жизнь стала бы ярче.

Кстати в Groovy уже давно есть две стандартные аннотации для шаблонов проектирования: @Singleton и @Immutable. А в Идее даже есть инспекции которые проверяют классы по этом аннотациям.

Репозиторий проекта находится на GitHub и ждёт героев готовых помочь проекту ;)

Ещё этими же авторами была создана библиотека аннотаций качества кода AtCode. Не очень впечатляет хотя тоже полезная.

Ну и в заключение хочу напомнить про прекрасную коллекцию аннотаций от Google про которую писали в первоапрельской статье на Хабре.

Код Java в bat-файле

Маленькая забавность о том, как использовать все возможности Java в обычных bat-скриптах. Или сделать скриптовый язык из Java.

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

Навеяно статьёй Код С# в bat-файле. Я реализовал тоже самое для Java. Только я не стал делать поиск папки в которую установлен JDK, т.е. в переменной %PATH% должен быть указан путь к %JDK_HOME%/bin

Не буду долго томить, вот код bat файла Test.bat:
/*
copy %0 .\%~n0.java
javac %~n0.java
cls
java %~n0
del %~n0.class
del %~n0.java
exit
*/

public class Test {
 public static void main(String[] args) throws Exception {
  System.out.println("Hello world from Java!");
  System.out.println("Press Enter to exit");
  System.in.read();
 }
}
Выполнив полученный батник увидим:
Работает это так:
  1. Сначала работает bat-интерпретатор. Первую строчку скрипта (комментарий Java /*) он не понимает и выдает ошибку. Но не страшно, это чисто визуальное неудобство.
  2. Затем мы копируем бат файл в файл с таким же именем но уже с расширением *.java, иначе компилятор javac откажется его компилировать. Выражение %~n0 вернёт имя файла без расширения: параметр 0 всегда имя файла а квалификатор n только имя файла. Следите за названием файла, поскольку имя *.java файла должно соответствовать имени класса внутри него.
  3. Далее компилируем консольным компилятором javac который в качестве аргумента принимает путь к файлу с исходником. В нашем случае исходник находится в теле bat-файла (строки 10–18). Текст bat-команд игнорируется, как комментарий (строки 1 и 9).
  4. После работы компилятора, получаем class файл, который запускаем (предварительно подчистив экран командой cls) и удаляем после завершения его работы.
  5. Работа bat-интерпретатора заканчивается на 8 строке (exit).

Если есть трудности с синтаксисом bat скриптов могу посоветовать хорошую статью.

Это был пример, я использую ещё немного улучшенный вариант bat файла. В случае ошибок компиляции он их выведет. Ещё также он добавит в CLASSPATH все *.jar файлы которые он найдёт рядом.
/*
@echo off
copy %0 .\%~n0.java
cls
javac -classpath CLASSPATH=.\*.jar;.\lib\*.jar  %~n0.java
if %ERRORLEVEL% equ 0 (
 cls
 java %~n0
 del %~n0.class
)
del %~n0.java
exit
*/

public class Test {
 public static void main(String[] args) throws Exception {
  System.out.println("Hello world from Java!");
  System.out.println("Press Enter to exit...");
  System.in.read();
 }
}

Целесообразность, скорее just for fun. Но иногда бывает удобнее выслать новичку один файл, уже с прописанной строчкой компиляции, который нужно просто выполнить.