Байки джависта

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
public class Main
{
	public static void main(String[] args) {
		System.out.println("Hello, Habr!");
	}
}

Привет, Habr!

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

  • когда я мучительно пытался понять, как работает только что написанный код

  • во время холиварных споров с коллегами

  • и особенно в моменты дебага

image-20230902112839709

И со временем я только убедился в их важности.

Мысли эти о типичных ошибках в коде, применении классных инструментов типо лямбд, о софтовых скиллах, которые напрямую влияют на удовольствие от работы. Некоторые мысли более очевидны, другие — совсем нет. Некоторые были для меня откровением — "А что, так можно было?"

Уверен, что каждый пролиставший данный пост обязательно найдёт для себя что-то полезное.

Если же так получилось, что всё тут было для тебя тривиально и элементарно — ты реально крут, поставь отметку в голосовании в самом конце. Интересно увидеть, сколько просветлённых джавистов на Хабре)

А ещё за эти годы я насобирал массу экстремально полезного контента, небольшой частью которого делюсь в виде ссылок в самом конце

Приглашаю под кат, поехали)

З.Ы. в статье есть несколько острых холиварных вещей, поэтому прошу в комменты

Как примирить перфекциониста и реалиста?

В реальной програмерской жизни (как оказалось) не всегда возможно следовать набору простых правил, чтобы код был красивым, читаемым, более линейным, без костылей, а ещё и оптимизированным. Почему? Во-первых, не всегда ты имеешь возможность написать код с нуля. Бывает тебе прилетает несколько мегабайт легаси, которые тебе предстоит патчить — и переписывать ВСЁ было бы слегка накладно. С меньшим количеством кода тоже проблемки — только ты его отрефакторил, а другой разраб уже накоммитил ...эмм ...ещё немного плохого кода.

Поэтому в такой ситуации на первый план выдвигаются такие мастер-техники:

  • скорочтение спагетти (или лазаньи, если ООП)

  • понимание, где фикс нужен, а где нет

  • рефакторинг костылей

  • быстрое переключение между гипотезами

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

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

  • переписать всё с нуля невозможно, и ты сделал всё, что мог

  • ты сэкономил себе N дней жизни (а иногда и гораздо больше, написание идеального кода — та ещё задача)

  • данный проект не требует 10+ лет поддержки, так что можно не париться

Если оптимизировать всё, что можно, то вы будете вечно несчастным.

© Дональд Кнут

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

Что ж поделать, иногда код приходиться делать таким:

image-20230904103234825

Что ж поделать, если

большинство программ на сегодняшний день подобны египетским пирамидам из миллиона кирпичиков друг на друге и без конструктивной целостности — они просто построены грубой силой и тысячами рабов.

© Alan Kay

Внешность обманчива

Загадка: что можешь сказать про скорость выполнения этих 2 программ?

1 версия

byte[] bunn;

for (int i = 0; i < bunn.length; i++) {
use(bunn[i]);
}

2 версия

byte[] bunn;

for (byte bunny : bunn) {
use(bunny);
}

Очевидно же, они выполняются за примерно одинаковое время. Да?

А вот нет — вторая реализация в 2 раза быстрее. Почему? Есть нюанс, связанный с созданием байт-кода: в процессе этого компилятор добавляет неявную инструкцию и читает поле в локальную переменную. Можно ли было это понять, взглянув на код? В поле моего зрения такие люди не попадались.

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

Иногда важно поковыряться и увидеть, как это устроено под капотом

image-20230904235431343

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

Область видимости переменной должна быть как можно меньше, это казалось бы must have. Не поверишь, сколько реальных кейсов с торчащими переменными я видел....!

Не надо так:

int i = 0;
for (i = 0; i < 10; i++) {
/* ... */
}

Разве сложно написать так:

for (int i = 0; i < 10; i++) {
/* ... */
}

И вот так тоже не нужно плиз:

int count = 0;
void counter() {
if (count++ > 10) {
return;
}
/* ... */
}

Сделай так:

void counter() {
int count = 0;
if (count++ > 10) {
return;
}
/* ... */
}

Удивительно и невероятно, сколько проблем встречается в реальных кейсах из-за глобальных переменных где попало. Из-за переменных, Карл!

Максимально просто, но здесь происходит колоссальное количество ошибок.

Будь осторожен и не плоди ошибки, где не нужно

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

© Christopher Thompson

Используй лямбды — это прекрасно

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

Условно, нужна программа, которая находит все SSD-накопители с объёмом до 2 Тб. Первая реализация, которая приходит в голову:

public class LambdaExpressions {
    public static List<SSD> findSSDsOldWay(List<SSD> ssds) {
        List<SSD> selectedSSDs = new ArrayList<>();
        for (SSD ssd : ssds) {
            if (ssd.volume < 2) {
                selectedSSDs.add(ssd);
            }
        }
        return selectedSSDs;
    }
}

Посыпем немного лямбд — и вуаля:

public class LambdaExpressions {
    public static List<SSD> findSSDsUsingLambda(List<SSD> ssds) {
        return ssds.stream().filter(ssd -> ssd.volume < 50000)
                .collect(Collectors.toList());
    }
}

Просто, компактно и приятно

Лень — главное достоинство программиста.

© Larry Wall

По возможности используй все инструменты языка, которые упрощают код

Проблема с null. Null safety.

В Java мы работаем с объектами через ссылки и может возникнуть ситуация, когда ссылка не указывает ни на один объект (значение этой ссылки null)

image-20230905082437306

Если попытаться и дальше использовать эту ссылку, то возникнет ошибка NullPointerException — и наша программа завершит своё выполнение.

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

К слову сказать, при проектировании Kotlin эта проблема была учтена и её можно без проблем обойти — просто указываешь при создании, что данная ссылка не может быть null. Красота! Такими темпами я чувствую, что скоро перейду на Kotlin)

Советы для Java довольно просты — либо не использовать null, либо изолировать те функции, которые его используют, от остальной части кода. Также можно помечать "опасные" методы и поля аннотацией @Nullable или @NotNull. Ну и существуют null-safe библиотеки, избавляющие от написания этих рутинных и длинных проверок.

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

void proc1(Object obj) {
    if (!isValid(obj)) return;
    ((MyObject) obj).call1();
}

void proc2(Object obj) {
    if (!isValid(obj)) return;
    ((MyObject) obj).call2();
}

Object obj = factory.getObject();
if (isValid(obj)) {
    proc1(obj);
    proc2(obj);
}

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

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

Любишь усложнять?

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

К примеру, когда-то я бы использовал такой код:

int x = 0;
for (int i = 0; i < 1; i++) {
    x = x + 1;
} 

Но не лучше ли просто написать так?

int x = 0;
x++;

А вместо этого шедевра...

processOrder(String customerCode, String customerName, String deliveryAddress, BigDecimal unitPrice, int quantity, BigDecimal discountPercentage);

...я бы посоветовал сделать проще

processOrder(CustomerDetail customer, OrderDetail order);

По этой теме предлагаю вашему вниманию статью на Хабре «Дайте крудошлепа». Автор пишет про поддержку кода гениев, напрочь оторванных от реальной жизни. Гениальный совет в статье — "Сила в стандартности и простоте решений".

Может любовь к усложнению — это профессиональная болезнь сеньоров и прочих? Уж очень хочется им превратить обычный скучный код во что-то яркое и креативное

img

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

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

© Джон Вудс

Как говорится, Keep It Simple Stupid)

Zero-day и прочие радости

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

Дело было 31 марта прошлого 2022 года. Я мирно пил кофеёк и обдумывал, как преобразовать очередной легаси-спагетти код в читабельный, как вдруг ко мне врывается сисадмин Петя с невнятными криками про "взлом Spring".

И правда, в течение пары часов появилась инфа о свежих 0-day в Spring, уязвимость назвали Spring4Shell. С её помощью можно было удалённо выполнять любой код без аутентификации — достаточно было просто кинуть правильный POST-запрос. Разумеется, это работало не везде, должен был быть включён DataBinder в последнем эндпоинте, ну и контейнер сервлетов тоже важен.

Пришлось в срочном порядке анализировать код, проверить, где мы используем паттерны Spring Core DataBinder и запретить передачу некоторых паттернов. Уверенности, что это поможет не было, но хоть что-то. К счастью, всё обошлось и до закрытия уязвимости у нас ничего не произошло. А могло

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

Хороший программист — это тот, кто смотрит в обе стороны, переходя дорогу с односторонним движением

© Даг Линдер

У кода нет цели, есть только путь

image-20230904223143309

Или всё-таки цель есть?

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

В частности, если мы работаем с конфиденциальной информацией, скажем, с паролями, предпочтительнее для их хранения использовать char[], а не String.

Почему? Всё на поверхности: строки не изменяемы, поэтому после создания объекта String хранимые им данные будут жить в памяти ещё долго, аж до момента сбора мусора.

В то же самое время если использовать массив char[], можно в явном виде стереть данные после окончания работы с ними. Таким образом, пароль исчезнет из системы тогда, когда мы этого захотим.

Ну и есть ещё кое-что. При использования String случайно засветить пароль шансов гораздо больше, как видно тут:

public static void main(String[] args) {
    Object pw = "Password";
    System.out.println("String: " + pw);

    pw = "Password".toCharArray();
    System.out.println("Array: " + pw);
}
String: Password
Array: [C@5829428e

При печати массива мы видим className + "@" + шестнадцатеричный номер hashCode, что вполне неплохо для конспирации

Вроде всё просто и понятно, но писать код, исходя из его применения, может быть не так просто.

Подытог

Итак, это была небольшая часть важных мыслей по Java, осознание которых потребовало времени, но очень помогло потом. По хорошему, их нужно было осветить более глубоко с большим количеством примеров — может, это и произойдёт в новых статьях. Ещё очень много важные вещей, о которых я не успел рассказать: проблема сравнения строк и литералов, переопределение equals() и hashCode(), правильная работа с ресурсами и БД, работа с исключениями (в частности с ConcurrentModificationException), утечка памяти, лимит стека вызовов, взаимоблокировки потоков. Хочется обо всё этом рассказать, потому что это крайне важно; видимо в следующей статье. А пока со всеми вопросами можно обращаться по ссылкам ниже.

Судьба языка Java

Что ж, напоследок порассуждаем над судьбой этого чудного языка

Java уже много лет (28 лет c 1995 года) является востребованным языком программирования (стабильно в десятке популярнейших) и продолжает занимать прочное место в энтерпрайзе. Хотя Kotlin вытеснил Java из разработки под Android, и современные приложения нечасто пишутся на Java, в остальных сферах этот язык закрепился очень прочно. Java имеет большую и развитую экосистему, кроме того, он широко используется в таких областях как разработка серверных приложений и Big Data.

Текущий 23 год всколыхнул IT-комьюнити, на первые позиции вышли языки, которые умеют в Machine Learning (любимый наш Python, скоро ещё и Mojo). Благо Java применяется и в области нейронных сетей и машинного обучения тоже. Из библиотек для развертывания нейронных сетей можно выделить, например, Deeplearning4j, DL4J и Weka. Они делают Java жизнеспособным вариантом для разработки приложений машинного обучения.

Справедливости ради нужно отметить, что конкуренты Java довольно сильны; это Python, JavaScript и C#. На данном этапе Java выезжает за счёт широкого распространения в корпоративных приложениях, обратной совместимости со старыми версиями и подобных штук.

0.01% вероятности, что Java будет полностью вытеснена каким-либо одним языком в ближайшие 10 лет.

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

Так что пока ИИ нас не заменит)


Полезные ссылки

Статьи:

  • 8 ловушек программирования - https://habr.com/ru/articles/218603/

  • Что такое красивый код, и как его писать? - https://habr.com/ru/articles/266969/

  • Производительность: нюансы против очевидностей - https://assets.ctfassets.net/oxjq45e8ilak/6UPA9qw0UKKdhlHbdPnN7/a8f7fea2eedcd3cbb8ec37c91820c201/Sergey_Tsypanov_Proizvoditelnost_Nyuansy_protiv_ochevidnostey_2021_04_14_10_29_41.pdf

  • Глубокое погружение в Java Memory Model - https://habr.com/ru/articles/685518/

  • Большой гайд. Пишем микросервисы на Java и Spring Boot, заворачиваем в Docker, запускаем на EKS, мониторим на Grafana - https://habr.com/ru/articles/682720/

  • Java байткод «Hello world» - https://habr.com/ru/articles/264919/

  • Размеры массивов в Java - https://habr.com/ru/articles/142409/

  • Особенности разработки высоконагруженного сервера на Java - https://www.lektorium.tv/sites/lektorium.tv/files/additional_files/1344195342_13887_java_highload.pdf

Годную литературу в .pdf довольно легко нагуглить, одновременно можно поймать несколько шедевров.

Запрос java concurrency на практике Джошуа Блох filetype:pdf

  • Effective Java - Джошуа Блох

  • Java Concurrency на практике - Джошуа Блох

Запрос Spring Boot 2 лучшие практики для профессионалов filetype:pdf

  • Spring Boot 2 лучшие практики для профессионалов - Фелипе Гутьеррес

  • Spring 4 для профессионалов - Крис Шеффер

Посмотреть для души:

  • Самый популярный java-разработчик - Евгений Борисов / про инженерию, java, spring и devrel - https://youtu.be/m8UaCpo62Cs

  • Java. Проблема с null. Null safety - https://youtu.be/QIfIoXW1SHA

  • Александр Маторин — Неочевидные Дженерики - https://youtu.be/_0c9Fd9FacU

  • Александр Маторин — Неадекватное Java-интервью - https://youtu.be/AR9dtVaEUSM

  • Антон Кекс — Как нам спасти Java? — Часть 2 - https://youtu.be/cPXTozVjSHo

  • Реактивное программирование. Как работает фреймворк Spring WebFlux - https://youtu.be/h9p0zJ_niPE

  • Optional или как избавиться от NPE в Java - https://www.youtube.com/watch?v=fbEnhHjEX3M

  • Андрей Паньгин — Память Java процесса по полочкам - https://youtu.be/kKigibHrV5I?si=ubkjAnbQEGK4yH4M

  • Что такое Java Garbage Collector, Как работает сборщик мусора в Java? - https://youtu.be/L6TYAU4z8CE?si=9I7hRN6KF2ZpG3w_

Источник: https://habr.com/ru/articles/759102/


Интересные статьи

Интересные статьи

Привет! Я Кирилл, с этой недели курирую на Хабре поток «Менеджмент». Второй месяц года пополнил наш раздел огромным количеством интересных постов. На случай, если вы что-...
Бугага, хабровчане! Наверняка с вами или коллегами случались в работе трешовые ситуации, о которых вы до сих пор вспоминаете с холодком? Готовы пощекотать себе нервы? Тогда заходите почит...
10 августа в наших соцсетях прошел стрим с Джоном Ромеро — создателем игр Doom, Quake и Wolfenstein 3D. Это был вечер теплых ламповых историй по заявкам: вы задавали вопросы в комме...
Приветствую. Мы опять держим путь в каморку Токсичного Деда. На сей раз дверь нараспашку, почти не пахнет табаком и не слышно музыки. Дед лежит на диване и вполголоса говорит по телефону. ...
Коллеги из Европы попросили включить эти статьи в договор на предоставление облачных услуг. Когда вступил в силу закон о хранении персональных данных в России, к нам в облако начали массово ...