Боротьба з null в Java-проєктах. Новий стандарт JSpecify

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер та технічний письменник, хочу розповісти вам про те, як у сучасних Java-проєктах борються із проблемою NullPointerException (NPE). У цій статті я опишу різні способи боротьби з NPE, які з’явилися в Java-індустрії (і багато з яких досі популярні), і також розповім про новий перспективний проєкт/стандарт JSpecify. Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про сучасні тенденції роботи з null-значеннями та про те, як уникнути пов’язаних із цим складнощів.

Передісторія

Боротьба з помилками, пов’язаними з null-значеннями, має дуже довгу історію, усіяну тушками провалених проєктів, звільнених менеджерів і розорених замовників. На жаль, розробники JDK дуже довгий час ігнорували цю проблему, що і призвело до таких сумних наслідків (а також до появи Kotlin). Давайте розглянемо засоби, які зараз існують для запобігання NPE.

Отже, припустимо, що у нас є ось такий код:

public interface TokenGenerator {
      String generate(int length);
}

Це API загального характеру, що дозволяє згенерувати якийсь токен. Що буде, якщо ми передамо неправильний аргумент length? Чи буде викинуто виключення, чи повернеться null? А може, повернеться порожній рядок? JavaDocs немає, як часто буває, а реалізації цього інтерфейсу вільні самі вирішувати, як обробляти цей випадок (причому кожна по-різному). Такий код є нічним кошмаром для будь-якого розробника, але якщо він існує, то необхідно знайти найбільш оптимальне рішення для того, щоб уникнути null там, де воно можливе.

Найперший і найпростіший спосіб — перевірити на null значення, що повертається:

public class UserRequst {
     public UserRequst(TokenGenerator generator) {
             String token = generator.generate(20);
             if(token == null) {

Такий підхід дуже популярний, але призводить до появи численних if перевірок, які тільки заплутують розробника. Більш компактно — використовувати клас Objects, який з’явився в Java 7:

public UserRequst(TokenGenerator generator) {
            String token = Objects.requireNonNull(generator.generate(20));

Такий код просто викидає NullPointerException, якщо йому передати null. Він оберігає від некоректного стану ваших об’єктів, але цей підхід теж є імперативним і не розв’язує проблему в цілому. Тому боротьба з NullPointerException у Java-проєктах пішла двома принципово різними шляхами. Перший шлях полягав у використанні спеціальних типів для API.

Клас-обгортка Optional з’явився в Java 8 в 2014 році і дозволяв явно вказати споживачам вашого API, що вони можуть отримати як валідний результат операції, так і його відсутність:

Optional<String> generate(int length);

Такий тип був відомий усім, хто використовував Google Guava, а з’явився він у Guava 10.0 у 2011 році. Загалом Optional був значним кроком уперед, але він мав кілька недоліків. При його інтенсивному використанні JVM засмічувалося величезною кількістю дрібних об’єктів. Optional об’єкти не можна серіалізувати. Крім того, навіть якщо ви використовували Optional, ви все одно могли б повернути null з методу generate().

Тому найбільш істотна робота пішла другим шляхом, пов’язаним з декларативною перевіркою на null. Ще в 2006-му році в Java з’явилися анотації @Nullable/@Nonnull (JSR 305), які дозволяли декларативно вказати, чи повертає ваш код null, чи ні:

@Nonnull String generate(int length);

Фактично саме по собі це було схоже на JavaDocs, оскільки жодної перевірки на null (статичної або run-time) воно не включало. Тому цей набір анотацій зазвичай використовувався разом із засобами статичного аналізу коду, спочатку FindBugs, а після заморозки цього проєкту — Spotbugs. На жаль, JSR 305 припинив свій розвиток ще на початку 2010-х, і це призвело до того, що розробники почали переходити на аналогічні анотації від FindBugs/SpotBugs.

Ще один цікавий підхід запропонували автори бібліотеки Lombok. Вони ввели свою анотацію @NonNull. І якщо ви додаєте її на аргумент конструктора чи методу:

public class AccountService {
      private final @NonNull TokenGenerator tokenGenerator;

То Lombok автоматично генерує захист від null:

public class AccountService {
    private final @NonNull TokenGenerator tokenGenerator;

    @Generated
    public AccountService(@NonNull TokenGenerator tokenGenerator) {
        if (tokenGenerator == null) {
            throw new NullPointerException("tokenGenerator is marked non-null but is null");
        } else {
            this.tokenGenerator = tokenGenerator;
        }
    }

Недолік такого підходу в тому, що якщо ви додасте таку анотацію до повертаного значення:

@NonNull String generate(int length);

То й помилки не буде, але й жодної перевірки на null зроблено не буде.

Такий підхід принаймні полегшував роботу з null, але автори Lombok при реалізації цієї фічі виявили, що в Java-проєктах вже існують і активно використовуються кілька десятків аналогічних анотацій:

Їм довелося підтримувати кожну з них. Виходило, що відсутність єдиного стандарту в цьому питанні ускладнювала роботу і IDE, і статичним аналізаторам коду, та й самим Java-розробникам, які ламали голову над тим, які ж анотації вибрати.

Найбільша технічна конфа ТУТ!🤌

А що ж розробники JDK? Кому, як не їм, пропонувати стандарти. Але на жаль, тільки в 2023 році з’явився перший тикет на створення nullable-типів. Його автор пропонував підхід, схожий на Kotlin:

  1. String! item — змінна item може бути null (null-restricted)
  2. String? item — змінна item може бути nullable

На жаль, якоїсь активності в цьому тикеті немає, тому навряд він незабаром буде реалізований. Можливо, в Enterprise Java є відповідне рішення? На жаль, але в Jakarta Annotations, який прийшов на зміну Java EE, є ті самі анотації @Nullable і @Nonnull, як і в JDK 15 років тому. Щоправда, у 2023-му з’явилася ідея розширити кількість анотацій та додати нові анотації @NonnullField та @NonnullAPI, але особливого прогресу там теж не видно

Там, де безсилі Java Core і Java EE, зазвичай у фарватері прогресу йдуть розробники Spring Framework. У 2017-му вийшов Spring Framework 5, куди були додані нові аннотації.:

  1. Nullable
  2. NonNull
  3. NonNullApi
  4. NonNullFields

Цікаво, що вони є мета-анотаціями, тобто включають існуючі анотації з JSR-305 (@CheckForNull, @Nonnull):

@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CheckForNull
@TypeQualifierNickname
public @interface Nullable {
}

Однак уже в 2022 році з’явився новий тикет, який передбачав відмовитися від даних анотацій в Spring і перейти на інші, з проєкту JSpecify. Що це за новий проєкт?

JSpecify — це спроба створити стандарт і задекларувати набір анотацій, які потім використовуватимуться статичними аналізаторами коду. Цей проєкт стартував у 2018 році й був випущений версією 1.0 у липні 2024 року. Очолює цю ініціативу Google, а до складу команди розробників входять представники JetBrains, Meta, Uber, Oracle та Microsoft.

Що нам, розробникам, дає JSpecify? Крім стандарту, він містить:

  • Специфікацію.
  • Реалізацію (jspecify-reference-checker).

Крім того, є і сторонні проєкти, які підтримують JSpecify:

  1. Бібліотека NullAway
  2. Intellij IDEA
  3. Checker Framework

Цікаво, що JSpecify як специфікація містить лише 4 анотації:

  1. NonNull
  2. Nullable
  3. NullMarked
  4. NullUnmarked

@NonNull — анотація, яка дозволяє вказати, що певне значення може бути null. У нашому прикладі:

public interface TokenGenerator {
      @NonNull String generate(int length);

На відміну від аналогічної анотації Lombok, ви можете її застосовувати до будь-якого елемента вашого коду, наприклад:

class Container<@NonNull E> {

Nullable — анотація, яка дозволяє вказати, що певне значення може бути null:

public interface TokenGenerator {
    @Nullable String generate(int length);

При використанні масивів потрібно бути особливо уважним, тому що тут не кожен може сказати, що означає цей код:

@NonNull String @Nullable[] items = new String[] {};

А тут ми говоримо, що змінна масив items може бути null, а ось її елементи — ні.

Виходить, якщо ви хочете перейти на JSpecify, то потрібно кожен аргумент, поле і локальну змінну позначати як @Nullable і @NonNull? Зовсім ні, тому що у вас в запасі є дві магічні анотації — @NullMarked і @NullUnmarked.

Якщо ви їх поставите на декларацію класу чи інтерфейсу:

@NullMarked
public interface TokenGenerator {
    String generate(int length);

Це означає, що скрізь всередині цього елемента null значення не допускається. Якщо вам потрібно дозволити використання null в одному конкретному місці, то ви явно вказуєте @Nullable:

@NullMarked
public interface TokenGenerator {
     @Nullable String generate(int length);

У сучасних проєктах можуть бути тисячі і десятки тисяч класів, і ставити анотації на кожен з них вимагає великих витрат часу. Тому є простіший варіант — файл package-info.java:

package demo;
import org.jspecify.annotations.NullMarked;

Тепер вважається, що анотація @NullMarked поширюється на всі елементи/типи в цьому пакеті (підпакети при цьому не торкаються).

Таким чином, після переходу на JSpecify ваші змінні можна поділити на три групи:

  1. Можуть бути null з @Nullable.
  2. Не можуть бути null з @NonNull/@NullMarked.
  3. Їхня можливість зберігати null невідома (це називається unspecified nullness) — як було б до використання JSpecify.

Проєкт JSpecify настільки сподобався розробникам Spring Framework, що вони ще в 2022 році захотіли на нього перейти і використовувати його за допомогою мета-анотацій. Але коли в 2024-му вийшов JSpecify 1.0, то виявилося, що мета-анотацію @Implies було вирішено видалити, щоб зробити проєкт максимально простим для вивчення та використання. Тому команді Spring довелося, скріпивши серце, запланувати не просто перехід на JSpecify, а ще й видалення всіх своїх null-анотацій, і використання аналогічних анотацій з JSpecify. Але оскільки це величезний обсяг роботи, то довелося все це перенести до Spring 7.0, який вийде восени цього року.

При цьому потрібно усвідомлювати, що в сучасних проєктах 90-95% коду — це код сторонніх проєктів, бібліотек і фреймворків. Тому повноцінна перевірка коду на коректну роботу з null можлива лише у випадку, коли вся індустрія перейде на JSpecify і почне використовувати його повсюдно...

Як перевірити, що це справді працює? Спробуємо перейти на JSpecify і застосувати для одного з невеликих проєктів.

Переходимо на JSpecify

У нашому випадку перехід на JSpecify буде максимально безболісним, оскільки ми до цього жодних анотацій та бібліотек із цієї категорії не використовували.

Додамо нову залежність для Gradle-конфігурації:

implementation("org.jspecify:jspecify:1.0.0″)

Тепер ми можемо використати його у нашому проєкті. Зрозуміло, що перевести весь проєкт на JSpecify — дуже затратне завдання, тому спробуємо перевірити, як це працює на невеликому наборі типів.

У нас є інтерфейс NumberGenerator:

public sealed interface NumberGenerator
permits RandomNumberGenerator, SecureRandomNumberGenerator, SequentialNumberGenerator {
     int generate();
}

Метод generate() повертає тип int, тому немає сенсу вказувати там @NonNull, адже це і так очевидно. Але спробуємо там поставити анотацію @Nullable і перевірити, чи визначить це статичний аналізатор як помилку:

@Nullable int generate();

Далі у нас є клас ReflectionUtil з методом createInstance:

public static <T> T createInstance(Class<T> clz) throws ConfigurationException {
   try {
          return clz.getDeclaredConstructor().newInstance();
    } catch (Exception e) {
         throw new ConfigurationException(e);
     }
}

Після аналізу цього методу стає зрозуміло, що і аргумент, і значення, що повертається, не може бути null, тому ми можемо вказати в сигнатурі @NonNull:

public static @NonNull <T> T createInstance(@NonNull Class<T> clz) throws ConfigurationException {

Далі візьмемо метод transform в інтерфейсі Transformer, який викликає цей метод:

default <T, P> P transform(T entity, Class<P> clz) {
checkParams(entity, clz);
P dest = ReflectionUtil.createInstance(clz);
return transform(entity, dest);
}

Тут також додамо дві помилкові анотації:

  1. @Nullable для аргумента clz
  2. @Nullable для локальної змінної dest
default <T, P> P transform(T entity, @Nullable Class<P> clz) {
     checkParams(entity, clz);
     @Nullable P dest = ReflectionUtil.createInstance(clz);
     return transform(entity, dest);
}

Крім того, в тому ж інтерфейсі додамо main() метод і спробуємо передати null у createInstance:

public static void main(String[] args) {
     ReflectionUtil.createInstance(null);
}

Зараз ми просто помітили анотації, але ніяких перевірок робитися не буде, тому що не підключений статичний аналізатор коду. У JSpecify є своя реалізація Reference Checker, але вона ще на стадії розробки і не готова для повноцінного використання.

Але, на щастя, вона має альтернативу — проєкт NullAway. Він стартував у 2017 році під крилом компанії Uber. Фактично це плагін до бібліотеки Google ErrorProne, про яку я вже писав. ErrorProne — статичний аналізатор коду, який перевіряє ваш проєкт на помилки під час складання.

NullAway має свої власні анотації, такі як @RequiresNonNull і @EnsuresNonNull, але в базовому варіанті він використовує анотації з JSpecify.

ErrorProne у нашому проєкті вже є, тому додамо цей плагін для Gradle-конфігурації:

errorprone("com.uber.nullaway:nullaway:0.12.4″)

І запустимо збірку:

gradle build

Відразу отримуємо помилку під час збирання:

Caused by: java.lang.IllegalStateException: DO NOT report an issue to Error Prone for this crash! NullAway configuration is incorrect. Must either specify annotated packages, using the -XepOpt:NullAway:AnnotatedPackages=[...] flag, or pass -XepOpt:NullAway:OnlyNullMarked (but not both). See github.com/...​llAway/wiki/Configuration for details. If you feel you have gotten this message in error report an issue at github.com/uber/NullAway/issues.

at com.uber.nullaway.ErrorProneCLIFlagsConfig.<init>(ErrorProneCLIFlagsConfig.java:259)

at com.uber.nullaway.NullAway.<init>(NullAway.java:295)

Ця помилка свідчить про те, що нам потрібно додати той пакет, який NullAway перевірятиме під час статичного аналізу:

tasks.withType<JavaCompile>(){
     options.errorprone  {
          option("NullAway:AnnotatedPackages«, «demo»)
     }
}

По дефолту всі помилки при використанні JSpecify трактуватимуться як попередження (тобто не зупинятимуть збирання), що нас цілком влаштовує. Знову запускаємо збірку і отримуємо цілу купу попереджень від NullAway. Розбиратимемо їх окремо.

warning: [NullAway] passing @Nullable parameter ’null’ where @NonNull is required

ReflectionUtil.createInstance(null);

Тут знайдено помилку з передачею null в метод, де аргумент @NonNull:

warning: [NullAway] passing @Nullable parameter ’clz’ where @NonNull is required

@Nullable P dest = ReflectionUtil.createInstance(clz);

Тут знайдено помилку, що ми передаємо @Nullable значення в метод, де аргумент @NonNull:

warning: [NullAway] passing @Nullable parameter ’clz’ where @NonNull is required

checkParams(entity, clz);

А ось тут цікавіше. Якщо ще раз поглянути на метод transform:

default <T, P> P transform(T entity, @Nullable Class<P> clz) {
    checkParams(entity, clz);
    @Nullable P dest = ReflectionUtil.createInstance(clz);

То NullAway лається на те, що ми передаємо @Nullable значення метод checkParams. Але checkParams не помічений анотаціями. Чому ж попередження? Єдине можливе пояснення — те, що NullAway помітив, що clz має бути @NonNull у createInstance і свариться скрізь, де ми намагаємося передати clz.

Тепер спробуємо виправити ці помилки:

default <T, P> P transform(T entity, @NonNull Class<P> clz) {

Цікаво, що наша спроба ввести в оману ErrorProne була припинена:

: warning: [NullablePrimitive] Nullness annotations should not be used for primitive types since they cannot be null

@Nullable int generate();

^

(see errorprone.info/...​pattern/NullablePrimitive)

Did you mean ’int generate();’?

Якщо метод повертає примітивне значення, ми не можемо додати анотацію @Nullable. Але цікаве інше. Це попередження не від NullAway, а від ErrorProne. Тобто він теж підтримує JSpecify? Насправді в ньому просто є патерн на те, як мають виглядати анотації, які не допускають або допускають null:

private static final Predicate<String> ANNOTATION_RELEVANT_TO_NULLNESS =
 Pattern.compile(
 «(Recently)?NonNull(Decl|Type)?|NotNull|Nonnull|»
 + «(Recently)?Nullable(Decl|Type)?|CheckForNull|PolyNull|MonotonicNonNull(Decl)?|»
 + «ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|»
 + «ProtoPassThroughNullness»)
 .asMatchPredicate();
 private static final Predicate<String> NULLABLE_ANNOTATION =
 Pattern.compile(
 «(Recently)?Nullable(Decl|Type)?|CheckForNull|PolyNull|MonotonicNonNull(Decl)?|»
 + «ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|»
 + «ProtoPassThroughNullness»)
 .asMatchPredicate();

Єдину помилку, яку NullAway не знайшов, це:

@Nullable P dest = ReflectionUtil.createInstance(clz);

Я створив тикет на цей приклад. І розробники досить швидко відповіли та пояснили, що головне завдання JSpecify/NullAway — уникнути NPE. І хоча конвертація @NonNull в @Nullable виглядає логічно некоректною, але помилки це не викличе, так як NullAway вимагатиме перевірки на null.

Однак NullAway видав цілу купу інших попереджень, які вимагають аналізу.

warning: [NullAway] initializer method does not guarantee @NonNull field client (line 46) is initialized along all control-flow paths (remember to check for exceptions or early returns).

public ConsulConfigSource() {

^

З одного боку, NullAway правильно визначив, що в конструкторі цього класу не ініціалізується поле client. Але чому це помилка, якщо це поле lazy й ініціалізується при першому зверненні?

У цьому випадку можна підказати NullAway назву методу, де ми ініціалізуємо це поле. Для цього потрібно створити анотацію @Initializer:

@Documented
@Target(TYPE_USE)
@Retention(RUNTIME)
public @interface Initializer {
}

І додати її на метод getClient у класі ConfigConsulSource:

@Initializer
private ConsulClient getClient() {
     if (client == null) {
          String host = getConfig().getOptionalValue(PROPERTY_CONSUL_HOST, String.class)
               .orElse(ConsulRawClient.DEFAULT_HOST);
          int port = getConfig().getOptionalValue(PROPERTY_CONSUL_PORT, Integer.class)
               .orElse(ConsulRawClient.DEFAULT_PORT);
          client = new ConsulClient(host, port);
     }
     return client;
}

Тепер це попередження більше не відображається.

Ще одне дивне попередження:

warning: [NullAway] passing @Nullable parameter ’e.getMessage()’ where @NonNull is required

throw new CommunicationException(e.getMessage(), e);

У конструкторі класу CommunicationException немає жодного натяку на @NonNull:

public CommunicationException(String message, Throwable cause) {

Але чомусь NullAway вважає інакше. Після детального аналізу документації з’ясовується, що NullAway розглядає всі оголошення як @NonNull, навіть якщо такої анотації там немає. Це особливість NullAway, не JSpecify, тому якщо ви будете використовувати іншу reference implementation, там можуть бути інші правила.

Поточне попередження можна усунути, додавши анотацію @NullUmarked до всього класу CommunicationException (або в package-info.java):

@NullUnmarked
public class CommunicationException extends AppException {

Ця анотація свідчить, що це елементи класу, не помічені анотаціями, можуть приймати і повертати null (unspecified nullness).

Що буде, якщо ви використовуєте анотації, але не з JSpecify, а з інших бібліотек? NullAway підтримує будь-які анотації під назвою @NotNull або @NonNull. Але навіть якщо ці анотації називаються інакше, то їх можна передати в конфігураційний блок файлу збірки. Єдиний виняток із цього правила — анотація @NotNull з Java/Jakarta Validation. Ця анотація не забороняє надавати null, але виявляє null при подальшій перевірці (якого може і не бути). Тому таку анотацію слід виключити зі списку у конфігураційному файлі збірки.

Висновки

Загалом можна сказати, що JSpecify — це досить перспективний проєкт і, по суті, перша спроба створити стандарт у Java-світі для боротьби з NPE. Поки що його можна використовувати лише за допомогою NullAway (а його — через ErrorProne), так що подивимося, можливо, з’являться інші реалізації, які також дозволять боротися з цією проблемою.

До переваг NullAway можна віднести гнучкість конфігурації та підтримку не тільки JSpecfy-анотацій, але й аналогічних анотацій з інших бібліотек.

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Jakarta Validation представляется достаточно хорошим вариантом, учитывая возможность валидации не только null-значений, но и других, таких как min, max и тому подобных.

Ще один варіант боротьби з null в джаві — перейти на котлін

Дякую за цікаву статтю! Чомусь така ностальгія читати про @NonNull зараз серед десятків новин про ШІ кожного дня — враження що повернувся на 15 років назад, де все стабільно і зрозуміло. Хоч щось в цьому світі лишається незмінним — і це моя улюблена Java. Пишу на ній зараз pet-проджект під андроїд. Розмір APK десь 200кб (при тому що можливостей вже немало), тоді як хеллоуВорлд на Котліні одразу декілька мегабайт.

різниця в розмірі апк не через мову

...а через стандартну бібліотеку Котліна? Ну так можна сказати і «через мову». Бо з Java такої проблеми нема, бо стандартна бібліотека Java вбудована в систему андроїда (якщо не брати до уваги опціональний desugaring).

String! item — змінна item може бути null (null-restricted)

не можe бути

І це не єдина помилка, ще є пропущені не.

Що тільки не придумають щоб не присати на JS...

Це все звісно круто.
Але як це рятує від ідіотів які наприклад в базу забули покласти значення для поля яке промарковане як NonNull або 3rd party API повертає null де його не повинно бути?

в базу забули покласти значення для поля яке промарковане як NonNull

Це сама база проконтролює.

Так не у всіх базах є схема

Правильно, рівно ніяк.
Так само як всі подібні подорожники не рятують від концептуального існуваннч нулла — чогось може не бути і все.

«Той, хто хоче бути строгішим за інших, має контролювати це сам»
Тобто, це ми контролюємо нулябельність поля, яке ми кладемо у базу, через нашу внутрішню валідацію схеми.
І всі третьосторонні API та прив’язки вертають нам тільки нулябельні значення, які вже перевіряються на null всередині нашої програми.

На 100% ми не вбережемось, але імовірність помилок все одно буде значно нижча.

Для борьбы с null в Java придумали Kotlin — и он прекрасно справляется с этой задачей.

І що, тепер null не існує?😂

просто там це на рівні мови хендлиться

100% вирішення проблеми null це закласти його у дизайн мови, і наразі воно вже вирішено і зветься kotlin) 100% сумісність з java дозволяє поступово вилікуватись від помилок в дизайні java що тягнуться із самого створення, немає жодних причин не пересісти на більш комфортну мову зі 100% сумісністю з java. І попрактикуватись можна на умовних unit тестах якщо є сумніви

і наразі воно вже вирішено і зветься kotlin

В Kotlin отримати NPE на раз, як два паільці об асвальт, Kotlin діалетк Java як Groovy чи Scala. Просто в дечому з Kotlin просто краще саме для обробки null значень і на цьому усе, фундаментально він не вирішує проблему. ЇЇ і Rust навіть не вирішує, хоча це одна з перших мов програмування де реалізовано те що хотів реалізувати Едгер Дейкстера в Algol 60, але йому це не вдалось через аппаратні ліміти того часу, особливо по пам’яті яка була дуже дорогою, тому компілятор ставав занадто великою программою щоби його компьютер міг виконувати так з’явилось NIL значення. Деніс Річі навпаки ще робив даунгрейд для мови С, яка є потомком Algol 60 — це була адресна арфіметика і т.д. щоби можна було писати близький до ассемблерного кода на ЯВР, для кращого портування ядра Unix. Java отримала null в спадок, і збірка сміття лише частково вирішила ряд проблем прямої роботи із пам’ятю. Тим не менше усе ще є виклики типу System.arrayCopy що під капотом мають ассемблерну реалізацію і т.д.
IMHO проблема помилок із роботою з вказівниками та пам’ятю не має з рішення в сучасних мовах високого рівня, без внесення ШІ алгоритмів в сам компілятор, це майже гарантоване фіаско. Завжди є взаємодія із низкорівневим аппаратно залежним кодом інакше программа працює занадто довго і витрачає занадто багато пам’яті. ШІ може санітайзити код і впевненно сказати що тут NPE. А так в цілому використовуюється науковий підхід і вам треба тествування від модульного до end to end.

якщо писати на котліні в джава-стайлі, то так, ті ж помилки будуть і ті ж null будуть...

Та як не пиши. Для прикладу з нещодавнього що я фіксив. Прийшов на сервер JSON його парсили Jakson стаби генерували з OpenAPI. В специфікації поле requared тобто не може бути null, в житті воно Optional і прийшло undefined/null. Отримали NPE.
Коротше тестування як було потрібно так і є.

да, тут буде NPE. Але таких ексепшинів на весь проект буде в десятки разів менше і не буде по всьому коду обробки тих можливих null

А тобто Elwis Operator це не обробка null ? В мові просто синтаксичний сахар для обробки null, еквівалента якого не було в Java до 1.8 версії.
Kotlin, як і Groovy чи Scala це JVM мова по факту діалект Java. В дечому краще за Java, в дечому гірша.

Це слова кожного, хто не писав ні на котліні, ні на скалі

За такою логікою джава — діалект С

Java офіційно С подібна мова. Власне тому і стала популярна на початковому етапі розвитку сучасного WWW, особливо під сервери Sun які лідували на ринку в бум .COM
Зараз Google обрали саме Kotlin серед безлічі JVM подібних мов, саме тому — що Kotlin не настільки сильно відрізняється від Java в синтаксисі, як скажімо Groovy, а тим більше Scala чи Lisp подібні мови. Хоча це швидше ML подібний синтаксис, та C подібний підтримується.
BTW NIL значення як і не зручний для бізнес коду Switch і т.д. це усе генетичні успадкування від C, які ще йдуть по лінійці Algol 60->BCPL->B->C.
Тут якраз Kotlin з його when, elwis operator і т.д. ML подібними вкрапленням синтаксису доволі не погано просунувся.

від помилок в дизайні java

Нули — це не помилка дизайну, а фіча.
Помилки — це проекти і ліби, які намагаютсья вирішити цю неіснуючу проблему.

Нули — це не помилка дизайну, а фіча.

billion dollar feature

Помилка

@NonNull — анотація, яка дозволяє вказати, що певне значення може бути null. У нашому прикладі:

Не вистачає частки «не». Певне значення не може бути null

При використанні масивів потрібно бути особливо уважним, тому що тут не кожен може сказати, що означає цей код:

@NonNull String @Nullable[] items = new String[] {};
А тут ми говоримо, що змінна масив items може бути null, а ось її елементи — ні.

— чи все тут вірно? мені здалось що навпаки — елементи nullable, а самий масив — ні

Нарешті хочь якісь просування в цьому напрямку!

P.S. Схоже пропущена частка «не» в цьому реченні:
@NonNull — анотація, яка дозволяє вказати, що певне значення може бути null. У нашому прикладі:

Підписатись на коментарі