DataArt

Праздник на улице Java


Юрий Кравцов
Java Developer

Дмитрий Куперман
Senior Developer

Вадим Михайлюк
Developer

Анастасия Сало
Developer

Ярослав Трохименко
Developer

Блог DataArt, Июль 2013, Праздник на улице Java

JEEConf 2013 — третья специализированная Java-конференция в Киеве, которую проспонсировал DataArt, благодаря этому мы в ней и участвовали. А вместе с нами — весьма заметные в java world персоны: Reza Rahman, Сергей Куксенко, Dr. Venkat Subramaniam, Алексей Шипилев , Yakov Fain, Владимир Иванов… перечислять можно долго! В силу этого предоставилась великолепная возможность общаться лицом к лицу, задать вопросы, непосредственно выслушать мнение мэтров.

Блог DataArt, Июль 2013, Праздник на улице JavaСамым интересным, естественно, были доклады. Традиционно они проходили параллельно на нескольких сценах, в этот раз таких площадок было четыре: «Главная сцена», «Core Java», «Языки и библиотеки», «Народная». Темы всех докладов представлялись очень интересными, и самым сложным оказалось жертвовать одним выступлением в пользу другого.

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

Доклад Виталия Тимчишина «Структурируем большое приложение с помошью OSGi»

Что же такое OSGi?

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

Основная идея фреймворка OSGi — все в системе есть плагины (в терминах OSGi — бандлы (bundles)). Основной способ взаимодействия между бандлами — сервисы, т. е. объекты, зарегистрированные в ядре системы с заявленными реализованными интерфейсами.

Очень большое преимущество OSGi — каждый bundle может определить свои экспортируемые пакеты и его зависимости. Таким образом, вы можете эффективно контролировать предоставляемый API и зависимости ваших плагинов.

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

Здесь мы сталкиваемся с понятием «динамическая шина». Что же это такое? Это означает, что можно на лету, т. е. не перезапуская приложение, устанавливать, подключать, отключать и обновлять модули системы (горячий деплой). Это очень удобно, в частности, для Enterprise-приложений, где важен высокий аптайм.

OSGi разрабатывается с 1999-го года, за это время было реализовано великое множество коннтейнеров и фреймворков. Ниже приведу некоторые из них.

Реализации с открытыми исходниками:

  • Knopflerfish — расширенная реализация OSGi R3.
  • Oscar — реализация OSGi R3.
  • Felix — продолжение проекта Oscar, разрабатывается Apache Foundation, реализация OSGi R3 и R4.
  • Equinox — реализация OSGi R4 от Eclipse Foundation.

Коммерческие реализации:

  • Connected Systems — HubTea Embedded Server.
  • Echelon — LONWORKS Bundle Deployment Kit.
  • Espial — Espial DeviceTop.
  • Samsung — Samsung Service Provider 3.1.
  • Siemens — RIO framework.

Доклад Сергея Куксенко «Я, лямбда» и «Молот лямбд»

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

Функциональный интерфейс — интерфейс, содержащий один абстрактный метод. Например, функциональным интерфейсом является всем известный Comparator или Runnable.

А λ — выражение, описывающее анонимную функцию, результатом исполнения которого является объект неизвестной природы, реализующий требуемый функциональный интерфейс.

Заметьте, «некоторый объект неизвестной природы»! Т. е. поведение объекта задаем мы, но каким именно он будет, намеренно не специфицировано. Компаратор можно задать вот так:

Comparator <Integer> cmp = (x, y) -> x - y;

Неправильный компаратор, но принцип понятен.

Правильный будет выглядеть так:

Comparator <Integer> cmp = (x, y) -> (x < y) ? -1 : (x > y) ? 1 : 0;

Еще примеры:

Интерфейс BinaryOperator<T> с методом T apply (T t, T u)
Выражение BinaryOperator<Integer> sub = (x, y) -> x — y;
Интерфейс BiFunction<T, U, R> с методом R apply (T t, U u)
Выражение BiFunction<Integer, Integer, Integer> biSub = (x, y) -> x — y;

Можно явно указывать типы аргументов:

Comparator <Integer> cmp =(Integer x, Integer y) -> (x < y) ? -1 : (x > y) ? 1 : 0;

Можно вообще без аргументов:

Интерфейс Supplier<T> с методом T get ();
Выражение Supplier <Integer> ultimateAnswerFactory = () -> 42;

Можно с одним:

Интерфейс Function <T, R> с методом R apply (T t)
Выражение Function <String, Integer> f1 = (s) -> Integer.parseInt (s);
или
Function <String, Integer> f1 = s -> Integer.parseInt (s);
(то есть, один аргумент можно в скобки не брать)

А можно и без результата:

Интерфейс Consumer <T> с методом void accept (T t)
Выражение Consumer <String> c = s -> {System.out.println(s);};

И даже вот так:

Arrays.asList("Foo", "Bar", "Baz")
.forEach(s -> System.out.println(s));

forEach – тоже новая фича Java 8, возможность для каждого элемента списка вызвать функцию, описанную лямбдой (см. java.lang.Iterable).

И последнее о синтаксисе лямбд. Нельзя использовать variable hiding. Т. е., вот такое не скомпилируется:

public void foo () {
	int x = 42;
	Comparator <Integer> cmp = (x, y)> (x < y) ?1 :
		(x > y) ? 1 : 0;}

и вот такое – тоже:

public void foo () {
	int x = 42;
	Comparator < Integer > cmp = (a, b)> {
		int x = a;
		int y = b;
		return (x < y) ?1: (x == y) ? 0 : 1;
	};}

λ— выражения могут использоваться:

  • В правой части оператора присвоения
    FI f = () -> 42;
  • В return
    return () -> 42;
  • В качестве аргументов методов/конструкторов
    foo (1 , () -> 42)
  • В инициализаторах массивов
    FI [] fis = { () -> 41, () -> 42 };
  • В приведении типов
    (FI)() -> 42

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

Прежде всего, добавлен пакет java.util.functions, содержащий функциональные интерфейсы если не на все случаи жизни, то на многие. BinaryOperator, BiFunction, Supplier и Consumer нам уже встречались в примерах выше.

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

Comparator cmp = (x, y) -> Integer.compare(x, y);
Comparator cmp = Integer::compare;

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

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

Arrays.asList("Foo", "Bar", "Baz")
.forEach(System.out::println);

Для передачи конструктора пишется ::new, например:

Интерфейс Function<T, R> с методом R apply(T t)
Выражение Function<String, Integer> f1 = Integer::new;

Появилось пересечение типов (type intersection).

Назовем функциональный интерфейс SAM (Single Abstract Method) и интерфейс-маркер, не имеющий ни одного метода, как, например, Serializable, ZAM (Zero Abstract Methods).

Если нужно прикастить объект к SAM и нескольким ZAM-интерфейсам сразу, то можно писать

(SAM & ZAM1 & ZAM2 & … & ZAMn) obj

Например, если мы хотим, чтобы результат выполнения λ-выражения был сериализуемым, то пишем:

(Comparator<Integer> & Serializable)
	(x, y) -> Integer.compareUnsigned(x, y)

Intersection же нескольких SAM-ов запрещен.

Для capturing-переменных из внешнего класса теперь не нужно, чтобы эти переменные были final.

То есть, если раньше для анонимного класса нужно были писать:

public Comparator < Integer > makeComparator (){
	final int less = —1;
	final int equal = 0;
	final int greater = 1;
	return new Comparator < Integer >(){
		@Override
		public int compare ( Integer x, Integer y){
			return (x < y) ? less
				: (x > y) ? greater
				: equal;
		}
	};
}

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

Просто так добавить новый метод в интерфейс (скажем, тот же forEach в Iterable, а в JDK 8 подобных нововведений куча) нельзя из соображений обратной совместимости. Но очень хочется. Поэтому в интерфейсы ввели возможность добавления дефолтной имплементации метода на случай, если он не определен в классе-наследнике. И имеем:

Interface Iterable<T> {
	default void forEach(Consumer<? super T> action) {
		//implementation here
	}
}

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

Ну и, раз уж в интерфейсах теперь есть тела методов, то до кучи, кроме дефлотных разрешили определять еще и статические. Теперь вместо привычной пары «интерфейс + утильный класс со статическими методами» достаточно одного интерфейса. Можно, например, утащить с собой в интерфейсе фабрику по созданию себя же:

public interface Ticket {
	String qDublin ();
	static Ticket random () {
		return ()> "toDublin";
	}
}
assertEquals ("toDublin", Ticket.random().qDublin());

Еще одно нововведение – Stream-ы. Ничего общего с потоками ввода-вывода, только название. Stream – это некое подобие коллекции, над которым можно выполнять операции, заданные логикой, переданной в виде функциональный интерфейсов.

public void printGroups (List<People>people) {
people.stream ()
	.filter(p —> p.getAge() > 65)
	.map(p —> p.getGroup())
	.distinct()
	.sorted(comparing(g —> g.getSize())
	.map(g —> g.getName())
	.forEach(n —> System.out.println(n));
}

Работа стрима укладывается в схему source → operation → operation → … → operatoin → sink.

Источники – это:

  • Коллекции
    List<T> list; Stream<T> s = list.stream();
  • Генераторы
    Stream<Integer> s = Stream.generate(() -> x++);
  • Утилиты
    IntStream s = IntStream.range(0, 100);
  • Что угодно, возвращающее Stream
    Stream<String> s = bufferedReader.lines();

Операции делятся на промежуточные и терминальные.

Промежуточные возвращают поток (например, map, reduce, distinct из примера выше).

Терминальные позволяют получить результат, не являющийся потоком (forEach из примера выше).

Некоторые операции, например, findFirst() и findAny() могут «бросить» поток, не проходя его до конца. Таким образом, операции над бесконечными потоками не лишены смысла:

int v = Stream.generate(() -> x++).findFirst().get();

Можно «попросить» операции потока выполняться параллельно stream.parallel().

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

После докладов возникло желание скачать OpenJDK и поиграться с ним. Кстати, если вдруг кто-то считает, что OpenJDK и Oracle JDK – это разные реализации, он ошибается, как и я до конференции. Oracle билдит свои сборки из исходников OpenJDK с добавлением туда некоторых коммерческих фич, не относящихся непосредственно к ядру Java.

Доклад Олега Шелаева «Taming Java Agents»

Тему «Taming Java Agents» представлял инженер ZeroTurnaround Олег Шелаев. Как известно, у JVM есть возможность подключения агентов, позволяющих инструментировать код выполняющихся приложений. Они часто используются для профилирования, оценки покрытия кода и т. п. В рамках доклада были рассмотрены возможности библиотеки манипулирования байт-кодом javassist, такие как:

  • импорт Java packages.
  • добавление полей, интерфейсов, методов к классам, переменных в методы.
  • добавление параметров методов.
  • изменения метода, добавления кода перед/после его выполнением

Также Олег сделал обзор средств, построенных на базе агентов.

  • Byteman — средство для трассировки и тестирования Java программ с встроенным языком правил. Самый простой пример, где может пригодиться Byteman, — трассировка. Например, можно отследить все создания нитей, добавив трассировочный вывод для всех вызовов start() у класса java.lang.Thread. Также инструментирование позволяет делать fault injection. Например, нужно протестировать поведение метода, использующего сокет при таймауте соединения. Вместо замены класса-реализации сокетов на мок можно с помощью Byteman бросить IOException при вызове Socket.connect(). Это правило можно задать прямо в тесте на базе JUnit4 с помощью аннотаций (также потребуется использовать JUnit4 test runner от Byteman).
  • JRebel — средство, позволяющее разработчикам немедленно использовать измененный код приложения без редеплоя. Кроме того, в него встроена поддержка ряда фреймворков, в том числе, Spring, что позволяет реагировать и на изменения в XML конфигурации.
  • Chronon — отладчик с возможностью «путешествия во времени». Другими словами, кроме обычных операций перехода на следующую строку программы и т. д. есть и обратные — перехода назад. При этом можно посмотреть значения переменных, которые были до этого. Это достигается записью всех данных на каждом шаге выполнения: истории переменных, вызовов методов, исключений, консольного вывода и выполнявшихся нитей. На данный момент есть плагин для Eclipse.
  • Plumbr — средство для поиска утечек памяти, позволяющее автоматически анализировать работу приложения в рантайме, в отличие от средств анализа heap dump а также ручного поиска с профайлером. При этом собираются высокоуровневые метрики, позволяющие избирательно собирать более детальную информацию при выявлении предполагаемых участков с утечками.

Доклад доктора Венкант Субраманиам «Programming with Actors»

Доклад «Programming with Actors» Dr. Venkat Subramaniam, отмеченного наградами автора и основателя Agile Developer Inc., был посвящен вопросам многопоточности на основе модели актеров. При многопоточной работе возникает проблема конфликтов из-за изменения общих данных. Для ее решения был придуман подход обмена сообщениями на основе событий. При этом внутри JVM создаются легковесные процессы, обменивающиеся неизменяемыми сообщениями. Как только эти асинхронные задачи, называемые актерами, завершаются, они передают также неизменяемые результаты их работы другим, координирующим, задачам. Для демонстрации данного подхода использовались примеры на Scala с использованием фреймворка Akka.

Доклад доктора Венкант Субраманиам «Picking a JVM Language, which one is right for you?»

В докладе Picking a JVM Language, which one is right for you? Dr. Venkat Subramaniam рассмотрел особенности наиболее распостраненных JVM языков, таких как Scala, Groovy, JRuby и Clojure, и показал, как выбрать язык для проекта по его характеристикам. В результате по мере обучения можно получить ощутимый прирост производительности разработки.

Языки оценивались по реализации типизации (статическая/динамическая), количеству вспомогательного кода, который нужно писать, возможности работы в функциональном стиле, реализации многопоточности с иммутабельными данными, возможностям метапрограммирования, и построения DSL (fluency).

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

Таблица исходных оценок языков по характеристикам (до умножения на коэффициент):

  Java Groovy JRuby Scala Clojure
Static typing 9 5 0 10 0
Dynamic typing 0 9 10 0 10
Low Ceremony 3 10 10 10 10
Functional Style 1 10 10 10 10
Immutability 2 4 0 9 10
Metaprogramming 2 10 10 6 10
Fluency 5 10 10 10 10

Доклад Яцека Ласковского «Introduction to functional programming in Scala»

Jacek Laskowski (IBM, Poland) — показался самым веселым докладчиком с очень понятным английским. Он искренне удивлялся, как на Украине может быть столько девчонок-программистов. Его презентацию можно глянуть тут: http://blog.japila.pl/2013/05/introduction-to-functional-programming-in-scala-at-jeeconf-in-kiev-ukraine/

По словам Яцека, Scala — штука «бесконечная в изучении» и во многих случаях очень полезная. Многие изучают его ради развлечения, ибо он непрост. Плюсом этого языка есть его приспособленность к масштабируемости в том смысле, что есть возможность с помощью одних и тех же концепций описать как маленькие, так и большие части – а ля абстракции, композиции, декомпозиции вместо примитивов.

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

Для тех, кто совсем в танке, вот некоторые ключевые аспекты языка:

  • Scala-программы во многом похожи на Java-программы и могут свободно взаимодействовать с Java-кодом.
  • Scala включает единообразную объектную модель в том смысле, что любое значение является объектом, а любая операция — вызовом метода.
  • Scala — также функциональный язык в том смысле, что функции — это полноправные значения.
  • В Scala включены мощные и единообразные концепции абстракций как для типов, так и для значений.
  • Scala содержит гибкие симметричные конструкции примесей для композиции классов и trait.
  • Scala позволяет производить декомпозицию объектов путем сравнения с образцом.
  • образцы и выражения были обобщены для поддержки естественной обработки XML-документов.
  • в целом, эти конструкции позволяют легко выражать самостоятельные компоненты, использующие библиотеки Scala, без привлечения специальных языковых конструкций.
  • Scala допускает внешние расширения компонентов с использованием видов (views).
  • Наличие шаблонов (generics) и шаблонов высших порядков (generics of a higher kind).
  • Есть поддержка структурных и экзистенциальных типов.
  • На текущий момент Scala реализована на платформах Java и .NET.

Доклад Владимира Иванова «JIT-компилятор в JVM глазами Java-программиста»

Владимир Иванов (Oracle, Россия) — грамотный товарищ из грамотной конторы.

Его презентацию можно посмотреть тут: http://jeeconf.com/materials/jit-compiler/

Блог DataArt, Июль 2013, Праздник на улице JavaДумаю, все серьезные Java developers знают, что такое JIT, но все же вкратце напомню.

Существует два подхода к компилированию — AOT и JIT.

AOT (“ahead-of-time” компиляция, при которой весь исходный код сразу компилируется в нативный) в докладе не рассматривался, так как в Java используется JIT.

JIT – “just-in-time” компилятор, который лопатит байт код в бинарный на этапе выполнения. Это и интересно!

Основное внимание Владимир уделил оптимизационному аспекту JIT -компиляции в HotSpot, так называемому сбору статистики и информации профилирования о методах. Сюда запихивается вся информация о типах, константах, вызовах и т. д., и в последующем компилятор творит магию, он ищет «hot» код и компилит его уже не в байт, а в нативный код (более 1,5к вызовов), что дает магический прирост производительности приложения.

А вообще, чуть подробней!

«VM использует интерпретатор, чтобы собрать информацию профилирования о методах, которая подается в компилятор. В разделенной на уровни схеме, в дополнение к интерпретатору, клиентский компилятор используется, чтобы генерировать скомпилированные версии методов, которые собирают информацию профилирования о себе. Т. к. скомпилированный код существенно быстрее чем интерпретатор, программа выполняется с большей производительностью во время фазы профилирования. Во многих случаях запуск, который еще быстрее чем с клиентом VM, может быть достигнут, потому что заключительный код, произведенный компилятором сервера, может быть уже доступным во время ранних стадий инициализации приложения. Разделенная на уровни схема может также достигнуть лучшей пиковой производительности чем регулярный сервер VM, потому что более быстрая фаза профилирования позволяет более длительный период профилирования, которое может привести к лучшей оптимизации. Oracle ©»

Блог DataArt, Июль 2013, Праздник на улице JavaИ это далеко не все! Рассматривались такие трюки как «инлайнинг», прогнозирование и большой-большой список «Optimizations in HotSpot JVM».

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

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

Доклад Владимира Иванова «Уменьшение расхода оперативной памяти в Java-приложениях»

Его презентацию можно глянуть тут: http://jeeconf.com/materials/memory-reduce/

В основном речь шла о footprint.

Многие, наверное, знают о существовании такой полезной вещи как GC. Он обладает тремя ключевыми характеристиками: throughput, predictability, footprint. Для получения более подробной информации стоит просмотреть доклад, а здесь приведем его основные тезисы, которые стоит запомнить.

  • Возможно, не все знают, что откалибровать коллектор можно на выигрыш только по двум ключевым характеристикам, упомянутым выше.
  • Чем больше памяти доступно, тем GC продуктивней и резвее работает (более редкие сборки, меньше затраты).
  • Распаковка OOPS-указателей — «дешевая» операция.
  • Неграмотное использование «сжатых» указателей чревато увелечением размера данных в 1.4 раза (работает только до 32Гб) ХХ:+UseCompressedOops.
  • Грамотно используйте типы ссылок (Strong, Soft, Weak, Phantom)!
  • Организуйте кэширование на уровне вашего приложения с использованием «мягких» ссылок, а ля GC-friendly cache!
  • Не используйте финализаторы! Никогда не используйте финализаторы! Более безопасное и практичное решение – использование «фантомных» ссылок.
  • Пользуйтесь параметром агрессивности очистки: -XX:SoftRefLRUPolicyMSPerMB !
  • Выставляйте ожидаемый размер коллекции при создании
  • ThreadLocal + ThreadPool = мусор в пулах.
  • Используйте NIO !

Доклад Михаила Хлуднева «Тонкости поиска в Lucene»

Презентацию можно просмотреть здесь.

Михаил Хлуднев, ведущий инженер по поиску, поделился своим опытом и знаниями о таком мощном поисковом движке как Lucene.

Михаил построил свое выступление не только на описании возможностей движка, но и обратил внимание на используемые алгоритмы. Он рассказал об особенностях и отличиях Lucene и MySQL, а именно, почему их сравнение похоже на «сравнение яблока с апельсином».

Lucene базируется на OLAP, MySQL —- на OLTP.

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

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

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

Масштабируемая и высокоскоростная индексация:

  • Свыше 95GB в час на современном оборудовании.
  • Требуется малый объем RAM — «heap» всего 1MB.
  • Размер индекса примерно 20-30 % от размера исходного текста.

Мощный, точный и эффективный поисковый алгоритм:

  • Ранжированный поиск — лучшие результаты показываются первыми.
  • Множество мощных типов запросов: запрос фразы, wildcard-запросы, учет интервалов и т.д.
  • Поиск, ориентированный на «поля» (такие как название, автор, текст).
  • Возможность сортировать поисковый отклик по различным полям.
  • Multiple-index-поиск с возможностью объединения результатов.
  • Возможность одновременного поиска и обновления индекса.

Кроссплатформное решение:

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

Блог DataArt, Июль 2013, Праздник на улице Java

JEEConf 2013 photo:
Dr.Venkat Subramaniam, Yaro Trokhimenko, Mohamed Taman, Reza Rahman, Jacek Laskowski, Anton Keks

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

Еще фотографии

Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Блог DataArt, Июль 2013, Праздник на улице Java Блог DataArt, Июль 2013, Праздник на улице Java
Поделиться:

Оставить комментарий: