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, он стабилен и пока умирать не планирует.

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


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

Статьи:

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

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

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

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

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

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

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

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

Комментарии (16)


  1. sshikov
    05.09.2023 18:30
    +3

    А вот нет — вторая реализация в 2 раза быстрее.

    И пруфы конечно же будут? Не, я не про бенчмарк этого кода, а скорее про то, что вторая реализация будет быстрее в два раза (да даже пусть на 20%) всегда, а не только в этой версии JRE, и без учета JIT (ну или что там еще не учли при измерениях)?


    Речь не о том, что вы что-то неправильно намеряли (хотя такое тоже бывает), а скорее о том, что никакие практические выводы из этого делать нельзя, потому что завтра выйдет очередная версия — и там все станет иначе. А кто-то, как я, все еще сидит на старой 1.8, и там все еще иначе.


    1. poxvuibr
      05.09.2023 18:30
      +5

      И пруфы конечно же будут?

      Практически моментально. Вот статься тут же на хабре, которая начинается с того, что кратко объясняет почему нет никакой разницы в два раза и что не так с измерениями, которые эту разницу показывают ))) https://habr.com/en/articles/518744/


  1. stackjava
    05.09.2023 18:30
    +5

    Извиняюсь за критику, но получилось ни рыба ни мясо...

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


  1. Felan
    05.09.2023 18:30
    +8

    А мне одному кажется что здесь что-то совсем не так?


    1. BugM
      05.09.2023 18:30
      +4

      Там не только это не так. Пример с производительностью просто некорректный. Я в отличии от автора пробенчмаркал.

      Пример с for абсолютно искуственный. Я не представляю что даже студент так напишет.

      Лямбды хороши совсем не всегда. Исключения в них доставляют много радости на проде. Иногда точно лучше явно все написать. Так как-то понятнее выходит.

      Пароли и строки просто глупость. Есть злоумышленник получил доступ к памяти вашей приложеньки то все. Можно дальше не защищаться. Ничего не поможет.


      1. Felan
        05.09.2023 18:30

        Да ну да, но в других случаях это хотя бы как-то работает. Ну хоть какой-то смысл можно высосать... Ну если сильно постараться... А тут просто поломка программы... :)


      1. mvv-rus
        05.09.2023 18:30
        +1

        Пароли и строки просто глупость. Есть злоумышленник получил доступ к памяти вашей приложеньки то все. Можно дальше не защищаться. Ничего не поможет.

        Память бывает не только оперативной, но и виртуальной — то есть, ее содержимое способно внезапно отъехать на диск. А злоумышленник (а уж тем более — доброумышленнник: который с корочками и постановлением на обыск и изъятие) может получить доступ к диску куда проще, чем к оперативной памяти в процессе работы.
        Я не знаю подробностей работы GC в Java, но в .NET попавшая по каким-то причинам в поколение 2(старшее) строка, да ещё — и не в текущий сегмент, может жить в памяти достаточно долго и в достаточно неактивном месте, чтобы ОС возжелала сбросить ее на диск.
        Так что про пароли и строки — не то чтобы совсем не глупость (глупость — это вообще писать свой велосипед для защиты и/или криптографии), но IMHO это про вполне возможный вектор атаки, особенно — оффлайновой.


        1. vvbob
          05.09.2023 18:30

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

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


    1. a_beacon
      05.09.2023 18:30

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


  1. aleksandy
    05.09.2023 18:30
    +2

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

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

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

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

    Короче, статья ни о чём.


  1. anticyclope
    05.09.2023 18:30
    +13

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

    int x = 0;
    x++;

    Лучше написать так, инфа 100%

    int x = 1;


    1. ValeryIvanov
      05.09.2023 18:30
      +4

      В вашем варианте теряется изначальный смысл. Лучше так:

      int x = 0 + 1;
      


  1. vvbob
    05.09.2023 18:30
    +4

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

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

    Я, помню, когда только начинал осваивать ООП, такие дикие иерархии городил.. Ну а что, в умных книжках ведь пишут что все это облегчает разработку, значит надо писать. Если у меня в коде есть данные сотрудника, то что? Сотрудник это человек? Ну да! Значит нужен объект Человек, от которого его и унаследуем! Хотя! Есть ведь еще резиденты и нерезиденты! Надо унаследовать их от Человека, а от них уже унаследовать Сотрудника, правда вот в Яве множественное наследование запрещено, но не беда, пускай будут два вида Сотрудника, унаследованные от Резидента и Нерезидента!

    А Сотрудники ведь тоже разные бывают, есть Руководитель, а есть Работник! Унаследуем от Сотрудников! Теперь у нас уже Четыре вида сотрудников...

    В итоге я так и не добирался до решения задачи, утонув в иерархии :))))


  1. Slaiter89sp
    05.09.2023 18:30

    Очевидный вердикт: можно смело вкатываться в IT при помощи Java, он стабилен и пока умирать не планирует.
    Спорное заявление, кажется что вкатываться все же проще через Python, но Java не умрет еще долго потому что большинство российского финтеха написано на Java, а в нем некоторые core сервисы захотят переписать в последнюю очередь.


    1. vvbob
      05.09.2023 18:30
      +1

      Мне кажется, что сам подход со стороны ЯП неверен. Лучше вкатываться там, где тебе интереснее всего, и уже исходя из этого выбирать основной ЯП на изучение которого и тратить время, тем более что учить обычно приходится не сам ЯП (там обычно не слишком много и надо учить), а его инфраструктуру - фреймворки всевозможные.


      1. poxvuibr
        05.09.2023 18:30
        +1

        Мне кажется, что сам подход со стороны ЯП неверен. Лучше вкатываться там, где тебе интереснее всего, и уже исходя из этого выбирать основной ЯП на изучение которого и тратить время

        Я уже года три повторяю эту фразу раза три в неделю!