К написанию статьи меня побудил интерес разработчиков Oracle к изучению Java. Статья не носит обучающий характер и не является инструкцией для перехода с одной технологии на другую. Цель — рассказать, как я переходил на Java и с какими трудностями столкнулся.
О себе
В свое время я отучился на программиста. Начинал карьеру на Delphi. Что такое ООП, помню, хотя на Delphi оно использовалось достаточно условно. С 2005 года начал плотно программировать на Oracle. Получил несколько сертификатов. Работал в компаниях Luxoft, РДТЕХ и CUSTIS. С последней сотрудничаю до сих пор, уже более 11 лет. Участвовал в проектах по автоматизации торговых сетей и банков.
С чего все началось
Два года назад я вернулся в команду разработки банковских продуктов, в которой уже когда-то работал и потому хорошо знал большинство систем. Но работы на PL\SQL было недостаточно для full-time, поэтому мне было предложено присоединиться к разработке на Java. В результате у меня появилась возможность и, главное, время для изучения Java в рамках производственного процесса.
На самом деле это не первая моя попытка изучения Java. Была еще одна, но она не увенчалась успехом. Особой производственной необходимости тогда не было, а желание учить ради изучения быстро прошло.
Обучение
Я посмотрел проекты, «потыкал» код… И быстро осознал, что нужно учить матчасть. Даже очевидный синтаксис был не всегда понятен, начиная от непривычных операторов &&
и ||
и заканчивая конструкциями:
List<LedgerAccount> accList = new ArrayList<LedgerAccount>();
DealGrid dealGrid = (DealGrid)grid;
Интуитивно понятно, что это некоторая типизация, но как это работает и чем они различаются, было неясно. А такие конструкции вообще вводили в ступор:
protected <E extends Editor<?>> E add(String id, E editor) {}
Особенно если учесть, что типов E
и ?
не существует.
Ходить по курсам не было ни времени, ни желания, поэтому начал с простого: купил книгу Барри Бёрда «Java 8 для чайников». Она не очень большая, 400 страниц, читается легко. После прочтения стало более-менее понятно, что такое Java и как на ней программировать. По крайней мере, отпали совсем глупые вопросы.
Далее, в процессе разработки, стали появляться более осознанные вопросы. Очевидно, не хватало знаний. По совету коллеги купил книгу Брюса Эккеля «Философия Java». Эта книга довольно объемная — 1168 страниц. Затрагиваются практически все темы, необходимые для работы, начиная от понятий «класс» и «объект» и заканчивая многопоточностью. Автор доступным языком на примерах объясняет материал. Я бы эту книгу рекомендовал как начинающим, так и опытным разработчикам.
Сложности восприятия
Объекты
Первое, с чем я столкнулся в Java после многолетней разработки на Oracle, — это объекты. Простой и понятный, казалось бы, код поначалу вызывал недоумение.
Deal deal = new Deal();
deal.setDealNum(1L);
После этого кода каким-то магическим образом данные появляются в таблице БД. Если заглянуть в класс Deal
, то там просто некоторое описание атрибутов и методов.
@Entity
@Table(name = "T_DEAL_EXAMPLE")
@SequenceGenerator(name = "SEQ_DEAL", sequenceName = "SEQ_DEAL", allocationSize = 1)
public class Deal {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_DEAL")
@Column(name = "ID_DEAL")
private Long id;
/**
* Номер сделки
*/
@Column(name = "DEAL_NUM")
private Long dealNum;
Где инсерты? Где апдейты? Мне как человеку, привыкшему работать непосредственно с данными, было странно, что теперь за меня это делает некий фреймворк. Конечно, если посмотреть в лог, то можно увидеть SQL, который генерирует Hibernate.
Hibernate: select SEQ_DEAL.nextval from dual
Hibernate: insert into T_DEAL_EXAMPLE (ID_CONTRACTOR, DEAL_CODE, DEAL_NUM, DT_DEAL, ID_DEAL) values (?, ?, ?, ?, ?)
Все есть класс
Еще одним открытием стало то, что в Java все типы, за исключением примитивных (boolean, byte, char, short, int, long, float, double
), являются классами. Даже String
и BigDecimal
— это классы. Из-за этого возникают некоторые особенности. Нельзя просто так взять и сравнить два числа.
BigDecimal bigDecimal1 = new BigDecimal("1");
BigDecimal bigDecimal2 = new BigDecimal("1");
System.out.println(bigDecimal1==bigDecimal2);
System.out.println(bigDecimal1.compareTo(bigDecimal1));
----
false
0
Метод compareTo()
сравнивает значения объектов и, если они равны, возвращает 0.
При операторе сравнения ==
сравниваются ссылки на объекты: равны будут только ссылки на один и тот же объект. В общем случае объекты должны сравниваться при помощи метода equals()
, который сравнивает их по содержимому.
Если мы просто присвоим один объект другому, то они станут равны. То есть два объекта будут иметь одинаковые ссылки.
BigDecimal bigDecimal1 = new BigDecimal("1");
BigDecimal bigDecimal2 = bigDecimal1;
System.out.println(bigDecimal1==bigDecimal2);
---
true
Арифметические действия также необходимо производить при помощи методов. Нельзя просто взять и сложить два объекта BigDecimal
.
BigDecimal bigDecimal1 = new BigDecimal("1");
BigDecimal bigDecimal2 = new BigDecimal("2");
//BigDecimal bigDecimal3 = bigDecimal1 + bigDecimal2; //Не допустимо
BigDecimal bigDecimal3 = bigDecimal1.add(bigDecimal2);
System.out.println(bigDecimal3);
---
3
Передача значений по ссылке
В Oracle по умолчанию значения параметров передаются по ссылке (режим IN
), и значения этих параметров внутри процедуры неизменны. То есть если в процедуру передали массив, то за пределами этой процедуры мы уверены в его неизменности. Разумеется, если это не OUT
-параметр.
В Java подход немного иной. Параметры передаются по ссылке (кроме примитивных типов), сама ссылка передается по значению. Таким образом, мы получаем полный доступ к объекту и можем менять у него любые атрибуты.
private void changeDealNum(Deal deal) {
deal.setDealNum(2L);
}
/**
* Изменение значения по ссылке
*/
public void changeLinkDeal() {
Deal deal = new Deal();
deal.setDealNum(1L);
System.out.println("Номер сделки до изменения по ссылке "+deal.getDealNum());
changeDealNum(deal);
System.out.println("Номер сделки после изменения по ссылке "+deal.getDealNum());
}
---
Номер сделки до изменения по ссылке 1
Номер сделки после изменения по ссылке 2
Если объект содержит атрибуты, которые в свою очередь являются объектами, их также можно менять через базовый объект. Думаю, для любого Java-разработчика это очевидная вещь, но у меня она поначалу вызвала некоторое удивление.
Например, у сделки Deal
есть ссылка на контрагента Contractor
.
/**
* Контрагент
*/
@ManyToOne()
@JoinColumn(name = "ID_CONTRACTOR")
private Contractor contractor;
Можно легко менять контрагента через сделку.
Deal deal = new Deal();
deal.setDealNum(1L);
// Создаем контрагента с номером 100
Contractor contractor = new Contractor("100");
deal.setContractor(contractor);
System.out.println("Номер созданного контрагента: " + contractor.getRegisterNum());
// Изменяем контрагента через сделку
deal.getContractor().setRegisterNum("200");
System.out.println("Номер контрагента через сделку: " + deal.getContractor().getRegisterNum());
System.out.println("Номер контрагента в исходном объекте: " + contractor.getRegisterNum());
---
Номер созданного контрагента: 100
Номер контрагента через сделку: 200
Номер контрагента в исходном объекте: 200
Как видно из примера, мы изменили значение одного объекта через другой.
Функциональное программирование
Пожалуй, для меня это было самым сложным. В функциональном программировании функция по сути является объектом. Ее можно передавать в качестве аргументов, присваивать переменным, вызывать методы и так далее. Например, нам нужно вывести идентификаторы всех сделок через разделитель ;
. Само собой напрашивающееся решение — перебрать сделки в цикле и вывести ID.
// Получаем список сделок
List<Deal> deals = dealService.findAll();
// Результирующая строка
StringBuilder result = new StringBuilder();
boolean first = true;
for (Deal deal : deals) {
// Условие добавления разделителя
if (first) {
first = false;
} else {
result.append("; ");
}
// Собираем строку с ID
result.append(deal.getId());
}
System.out.println(result);
В функциональном программировании подход немного иной. Пример для Java 7.
// Получаем список ID сделок
List<Long> dealIds = newArrayList(Iterables.transform(deals, new Function<Deal, Long>() {
@Nullable
public Long apply(@Nullable Deal input) {
return input.getId();
}
}));
// Выводим список через разделитель
System.out.println(Joiner.on("; ").join(dealIds));
Метод Iterables.transform()
получает в качестве аргументов два интерфейса Iterable<F>
и Function<F, T>
. Интерфейс Iterable
реализован у списка deals
, а вот готовой реализации функции у нас нет. Поэтому реализовываем ее в анонимном классе.
Метод Iterables.transform()
берет поэлементно значения из коллекции deals
, преобразует их с помощью анонимной функции и возвращает список результатов преобразования в том же порядке. Далее методом Joiner.join()
собираем список в строку с разделителями.
На первый взгляд конструкция с функцией кажется немного громоздкой и сложной для понимания. Перебор массива проще и понятней. Но со временем привыкаешь, и такие конструкции уже не кажутся страшными. Более того, они оказываются удобными. IntelliJ IDEA эти конструкции красиво схлопывает, показывая, что на входе и на выходе.
В Java 8 появились лямбда-выражения, которые еще больше упрощают запись. Пример получения списка ID сделок с использованием лямбда-выражения:
List<Long> dealIds = deals.stream().map(Deal::getId).collect(Collectors.toList());
В целом функциональное программирование хорошо подходит, например, для работы с коллекциями. Получается простая и лаконичная запись. Но если применять его для реализации бизнес-логики, то, на мой взгляд, сложность восприятия кода сильно возрастает. Но это достаточно спорная тема, и углубляться в нее я не буду. Скажу лишь, что если вы начинающий Java-разработчик и попали на проект, где сложная бизнес-логика реализована в функциональном стиле, то вам не повезло.
Case sensitivity
В Java код регистрозависим. После Oracle это кажется избыточным, да и вообще ненужным. Но немного поработав с Java, начинаешь понимать всю прелесть этого решения. Не нужно ломать голову над тем, как назвать переменную: называешь ее так же, как класс, только с маленькой буквы.
Deal deal = new Deal();
Если бы код был регистронезависим, пришлось бы придумывать какие-то префиксы. И в целом, на мой взгляд, именование, разделенное регистром, выглядит приятней, чем подчеркивания.
Запросы к БД
Как уже, наверное, понятно, в наших проектах для работы с базой используется фреймворк Hibernate. По сути, это ORM, которая мапит объекты Java на таблицы базы. Hibernate использует для построения запросов язык HQL (Hibernate Query Language). В принципе, он похож на SQL, но реализуется в терминах объектов и сильно ограничен по возможностям в сравнении с нативным SQL. Простой запрос к сделке будет выглядеть так:
SQL:
SELECT * FROM t_deal_example t WHERE t.deal_num = :deal_num;
HQL:
"From Deal where dealNum = :dealNum"
Для человека, знающего SQL, понимание HQL труда не составит. При парсинге HQL в SQL объект Deal
заменится на замапленное значение аннотации @Table(name = "T_DEAL_EXAMPLE")
, атрибут dealNum
заменится на @Column(name = "DEAL_NUM")
и так далее.
В целом неплохая идея. Можно легко менять поля, таблицы и даже базы практически без изменения приложения. Перемапил объекты — и все заработало. Но есть нюанс. Все это хорошо работает, пока вы используете простые запросы к нескольким табличкам. Как только начинаются относительно сложные запросы, писать их на HQL становится сложно, а иногда и невозможно.
Вторая проблема заключается в том, что Hibernate строит запросы по всем связанным объектам. В зависимости от типов связей Hibernate может построить один или несколько запросов. Например, есть таблица сделок и связанный с ней контрагент, который не является обязательным к заполнению.
Для HQL-запроса "From Deal where dealNum = :dealNum"
Hibernate выполнит два запроса: к сделке и контрагенту.
select deal0_.ID_DEAL as ID_DEAL1_1_,
deal0_.ID_CONTRACTOR as ID_CONTRACTOR5_1_,
deal0_.DEAL_CODE as DEAL_CODE2_1_,
deal0_.DEAL_NUM as DEAL_NUM3_1_,
deal0_.DT_DEAL as DT_DEAL4_1_
from T_DEAL_EXAMPLE deal0_
where deal0_.DEAL_NUM = ?
;
select contractor0_.ID_CONTRACTOR as ID_CONTRACTOR1_0_0_,
contractor0_.FULL_NAME as FULL_NAME2_0_0_,
contractor0_.NAME as NAME3_0_0_,
contractor0_.REGISTER_NUM as REGISTER_NUM4_0_0_
from T_CONTRACTOR_EXAMPLE contractor0_
where contractor0_.ID_CONTRACTOR = ?
;
Таким образом, при достаточно большом и сложном объекте у вас будет загружаться «полбазы».
Конечно, есть возможность сделать «ленивую» загрузку ссылок (@ManyToOne (fetch = FetchType.LAZY)
, но в таком случае нельзя будет использовать «ленивый» объект вне контекста Hibernate, если он не был загружен в сессии. Ну и разбираться с производительностью запросов HQL довольно сложно. Мало того что у тебя получается несколько страниц автогенерируемого SQL, так еще и непонятно, что со всем этим делать.
Приятные «плюшки»
Среда разработки
В качестве среды разработки у нас используется IntelliJ IDEA 2019. Когда я начал ею пользоваться после оракловых IDE, меня не покидала мысль: «Надо же, как тут все сделано для людей». Довольно удобные поиски, переходы, подсказки и так далее. Но сравнивать IDE для работы с БД и Java, наверное, не совсем корректно. Все-таки оракловые IDE в первую очередь предназначены для работы с данными.
Debug
Не могу не отметить дебаг, он меня прямо впечатлил. При дебаге в коде сразу видны значения.
Можно посмотреть весь объект со всеми связями.
Или получить значения методов.
Применить какое-то вычисление.
Но больше всего мне понравилось, что можно менять значения объектов. Это может быть удобно, если нужно зайти в какую-то ветку кода при дебаге.
Заключение
На самом деле начать разрабатывать на Java не так уж и сложно, как казалось на первый взгляд. Но все-таки стоит учить матчасть. Простого знания ООП или того же Delphi будет недостаточно. В Java очень много своей специфики. Конечно, можно начать что-то писать и без предварительного изучения, интуитивно вы рано или поздно все равно поймете, что к чему. Но, как мне кажется, если стоит задача быстро освоить язык и начать на нем работать, проще прочесть пару книг или прослушать небольшой курс.