Этой публикацией мы начинаем серию небольших статей с разбором «сложных» заданий из сертификации для Java‑разработчиков, чтобы помочь вам подготовиться к сдаче сертификационного экзамена и помочь вам лучше понять, как работает Java и как эффективнее использовать его в своих проектах.

Итак, один из подвопросов, традиционно выносимых на экзамен — будь это сертификация Oracle или сертификации для Java‑разработчиков от Учебного центра IBS, — интересуется разработкой кода с использованием классов‑оболочек, в частности, Boolean, Double или, скажем, Integer. Такая задача может быть сформулирована в следующей форме:

Дано:

String five = "5";
Boolean boo = Boolean.valueOf(five);  // строка X
Integer i1 = new Integer(five);
Integer i2 = 5;
if (boo) System.out.print(i1 == i2);

Каков результат?

  • A.   Выброс исключения на строке Х

  • B.   true

  • С.   false

  • D.   Код успешно компилируется и исполняется, но ничего не печатает

Как мы видим, в вопросе задействованы два класса‑оболочки, а именно Boolean (в лице его статического фабричного метода) и Integer. Следует отметить, что для нашего экзамена характерен подход, сочетающий в себе сразу несколько тестируемых аспектов. Поэтому не всегда очевидно, чем именно задача интересуется: тем, как работают «фабрики», механизмом автоупаковки/распаковки или оператором сравнения и т. д.

Обсудим все по порядку. Практически в каждом классе‑оболочке есть два популярных статических метода для создания объекта: valueOf(), который принимает подстилающий примитив, и parseXXX(), который — как и всякий парсер — принимает стринг (здесь вместо XXX надо подставить, например, Double, Int и т. д.). Исключением является класс Character: в нем парсера нет. Встречаются и перегруженные парсеры, где можно указать систему счисления, но наш экзамен таких тонкостей не касается.

Далее, присваивание примитива к ссылке на класс‑оболочку приводит к автоупаковке (примитив, разумеется, должен быть при этом соответствующего типа), поэтому такой подход часто используется при создании объектов, одно из преимуществ — возможность вызвать конструктор, хотя этот способ был депрецирован еще в 9-й версии. Постараемся дать ответ, почему такая депрекация вообще понадобилась.

Мы знаем, что при вызове конструктора через ключевое слово new возможны два результата: мы получим совершенно новый объект данного типа, либо вылетит исключение. Из‑за такой ограниченности функционала конструкторов предпочтение отдается фабричным методам, которые — в отличие от любого конструктора — способны вернуть ссылку на уже существующий объект.

И вот что интересно: поскольку класс‑оболочка Integer является немутирующим, два объекта этого класса с одним и тем же значением способны взаимозаменять друг друга. Вот почему нет никакой необходимости иметь два разных объекта с одинаковым подстилающим значением. Мало того, помимо экономии памяти, мы получаем также возможность сравнивать объекты через == вместо вызова метода equals(Object o). В частности, для класса Integer такой подход популярен с объектами, чьи значения лежат в пределах диапазона байта, т. е. от -128 до 127 (и если тут возникает ощущение дежавю, то все правильно: со стрингами аналогичная история, мы предпочитаем пользоваться строковыми константами в двойных кавычках вместе вызова, скажем, new String(«5»).

Фабричные методы обладают и другими преимуществами. Например, мы можем иметь несколько аналогичных методов с разными именами, но одинаковыми списками формальных параметров. А вот с конструкторами такой фокус не пройдет: их можно только перегрузить.

В книге Джошуа Блока, «Effective Java«, приведен полный список преимуществ статических фабричных методов над конструкторами. Еще одна выгода в том, что конструктор способен вернуть объект одного типа, а вот фабричный метод может вернуть что угодно, если это не противоречит задекларированному типу возвращаемого значения. Одно из наиболее наглядных и понятных проявлений — это имплементация интерфейсов, где можно аккуратно скрыть специфические детали реализации.

Метод Boolean.valueOf() может вернуть одно из двух значений, причем это константные объекты, а именно: Boolean.TRUE и Boolean.FALSE. Именно эти объекты будут использоваться раз за разом, не требуя дополнительного выделения памяти, что не представляется возможным с ключевым словом new.

Далее, большинство оболочечных классов бросают исключение в случае null‑аргумента или стринга, чей формат нарушает требования к подстилающему примитиву (например, если бы мы вызвали Integer.valueOf() с аргументом «five», а не «5»). Но здесь следует отметить, что парсер класса java.lang.Boolean всегда тестирует аргумент: во‑первых, существует ли он и, во‑вторых, содержит ли значение «true» при любом сочетании букв в верхнем и нижнем регистрах. Если да, булевый парсер вернет Boolean.TRUE, а в противном случае — Boolean.FALSE. Другими словами, парсеру можно скормить хоть «что угодно», хоть даже null — и он просто вернет Boolean.FALSE без вылета какого‑либо исключения.

Вот почему строка Х в нашем примере не бросает исключение и присваивает переменной boo значение Boolean.FALSE. Следовательно, вариант A неверный.

Теперь давайте займемся поведением оператора if. Мы знаем, что тестируемое выражение обязано иметь тип boolean. Привыкнув к механизму автоупаковки и распаковки, мы ожидаем, что Boolean‑объект будет транспарентно распакован в подстилающий примитив. Так оно и случится, поэтому код скомпилируется — но из‑за того, что наш экзамен рассчитан на Java11; До появления автоупаковки в Java5 эта же строчка вызвала бы синтаксическую ошибку. Впрочем, в нашей задаче вообще нет такого варианта.

После распаковки булевый тест получит значение false, и, стало быть, оператор печати не исполнится. Что ж, выходит, вариант D — единственный правильный.

А теперь давайте задумаемся над тем, что было бы, если бы булевый тест дал значение true.

Как мы знаем, Java предоставляет две идиомы сравнения. Одна из них является конструкцией, встроенной непосредственно в лексику языка: это оператор ==. Вторая идиома — а именно метод equals(Object o) — предоставлена стандартной библиотекой и, будучи частью функционала класса java.lang.Object, доступна любому объекту. Она не делает ничего полезного и своим поведением не отличается от оператора «double equals«, поэтому классам‑наследникам предписывается по‑своему решать, каким образом переопределить такое поведение. Метод equals() имеет интересный контракт из пяти пунктов, но в нашем примере он не используется, поэтому мы не будем обсуждать его; вместо этого давайте займемся оператором ==.

Мы знаем, что «double equals» принимает два операнда, точнее, два выражения. Нюанс в том, что выражения могут иметь разные типы — и это различие сказывается на результате сравнения самым радикальным образом.

Что за разные типы? Это наши старые знакомые: примитивы (коих в Java восемь: boolean, byte, short, char, int, long, float и, наконец, double), либо то, что мы именуем термином ссылка (reference). Напомним, что ссылка в Java напоминает указатель (pointer), который показывает, где именно в памяти расположен тот или иной объект.

Если выражение имеет примитивный тип, то это бинарная репрезентация некоего числа, например 0b101 010, то есть десятичное 42. Это число будет записано в стеке, и ему будет присвоена некая метка, которую мы привыкли называть словом «переменная». Но вот если речь идет не про примитив 42, а про Integer‑объект, который инкапсулирует подстилающий примитив 42, то в стеке будет сидеть уже не 0b101 010, а нечто куда более «вихрастое», например 0xffff5637. Эта комбинация шестнадцатиричных литералов, по сути, является значением ссылки на объект, который в языке Java живет не в стеке, а в т. н. динамически аллоцируемой памяти, в знаменитой «куче» (heap). К примеру, в языке C/С++, прародителе Java, значением пойнтера является адрес объекта. В Java ситуация несколько сложнее, но в первом приближении мы тоже можем сказать, что значением ссылки (которая живет в стеке) является адрес объекта (который, напомним, живет на heap'е). Что в итоге? Пусть оператор «double equals» сравнивает значения двух переменных. Если это примитивы, JVM сравнит их бинарные значения, то есть — числа. Если это ссылки, то JVM сравнит их значения, но ведь значением ссылки является адрес того объекта, на который данная ссылка указывает. Вот почему принято говорить, что оператор == проверяет эквивалентность примитивных значений, а в случае ссылок он сравнивает уже идентичность объектов. Если у двух ссылочных переменных (например, класса Integer) одно и то же значение, мы имеем дело с одним и тем же объектом (один и тот же адрес!), но если объектов два, то они расположены по разным адресам. В этой ситуации оператор == вернет false.

Сейчас не должно вызывать удивления, что данный код:

Integer v1 = new Integer("1");
Integer v2 = new Integer("1");
System.out.print(v1 == v2);

обязательно покажет false: ведь успешный вызов конструктора через new, как мы уже упоминали, приведет к рождению очередного объекта. Простое правило на нашем экзамене: сколько видим слов new, как минимум столько же будет объектов. Стало быть, переменные v1 и v2 по необходимости ссылаются на разные объекты, и сравнение их адресов оператором == даст нам false.

А вот небольшая вариация на эту же тему: пусть у нас такой код:

Integer v1 = new Integer("1");
Integer v2 = 1;
System.out.print(v1 == v2);

Опять мы видим new, и это означает, что у нас вновь два разных объекта: один родился благодаря стараниям конструктора, а второй прилетел к нам из статического фабричного метода, который был вызван имплицитно и полностью для нас транспарентно механизмом автоупаковки.

В заключение следует отметить, что фабрики для немутирующих объектов часто пишутся так, чтобы возвращать все тот же объект, если в метод были переданы те же аргументы. В частности, в документации на java.lang.Integer API мы видим следующую ремарку о методе valueOf(int):

“This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.”

Другими словами, следующий кодовый сниппет:

Integer v1 = Integer.valueOf(1);
Integer v2 = Integer.valueOf(1);
System.out.print(v1 == v2);

даст нам true.

Официально задокументированная гарантия присутствует лишь для метода valueOf(int), хотя на практике valueOf(String)также демонстрирует это же поведение: Integer‑объекты в пределах диапазона байта помещаются в константный пул.

Подведем итоги: поскольку код пользуется механизмом автоупаковки (в форме вызова Integer.valueOf(int) для создания одного объекта, а для второго применяет вызов конструктора, это означает, что, если бы отработал оператор печати, мы бы увидели false. Поэтому правильным вариантом ответа остается D, о чем мы уже упоминали.

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


  1. rukhi7
    02.06.2023 07:19

    еще интересно зависит ли результат от Java -компилятора (от IDE)


    1. IBS_habrablog Автор
      02.06.2023 07:19

      Если имеется в виду «результат» как ответ на вопрос, например, «скомпилируется ли данный код», то, вообще говоря, да: это зависит от компилятора, потому что всякий компилятор привязан к конкретной версии Java, вот почему код с новейшими фичами попросту не скомпилируется на древних версиях.
      Но отметим, что наши сертификационные экзамены совершенно строго «заточены» под 11-ю версию (причем об этом, конечно же, объявлено открыто), поэтому вопросы категорически не будут содержать что-то чересчур новое.
      Что касается IDE, то они никак не влияют на результат (за исключением экзотических ситуаций, но такие ситуации на экзамене гарантированно не встретятся).


  1. makariyp
    02.06.2023 07:19

    Опечатка в статье про пул интов:
    "от -128 до 1270 " - тут лишний ноль
    А в целом статья очень полезная, спасибо.


    1. IBS_habrablog Автор
      02.06.2023 07:19

      Спасибо! Поправили, действительно, в процессе верстки закрался лишний ноль.


  1. quaer
    02.06.2023 07:19
    +3

    String five = "5";

    Boolean boo = Boolean.valueOf(five);  // строка X

    На экзамене предполагается, что человек помнит все особенности реализации методов и их поведений на память?


    1. ValeryIvanov
      02.06.2023 07:19

      Причём, это довольно странное поведение. Это что-то на уровне PHP/JS, которые когда парсят целое число, отбрасывают символы отличные от чисел в конце строки. Скорее всего, это следствие обратной совместимости, также как и, например, метод boolean Boolean::getBoolean(Strng name). getBoolean считывает значение из системных свойств и парсит его, хотя казалось бы, почему обёртка примитивного типа должна этим заниматься?


    1. IBS_habrablog Автор
      02.06.2023 07:19

      И да, и нет. Как всегда в жизни, увы-увы :((

      В Java 4000+ классов, в каждом по доброму десятку методов, поэтому никто не требует полного знания.

      Но рассматриваемый случай особенный: речь идет про т.н. «классы-оболочки», которые крайне популярны и полезны. Вот их надо бы знать на память, причем и тут объем совсем не такой большой. В частности, в каждом из них есть по 3 метода, которые и называются примерно одинаково, и работу выполняют похожую. Метод из примера, т.е.valueOf() по принятой конвенции ВСЕГДА принимает подстилающий примитив, а вовсе не строковый литерал, как в задаче. Для работы со строковыми литералами есть другой метод, parseXXX(), где XXX может означать Int, Boolean и проч. А можно еще взять конструктор, который умеет работать и так, и эдак… Одним словом, есть единый подход, эдакая конвенция, знание которой стократно облегчает жизнь кодера и позволяет ему делать меньше ошибок.

      Так что в данном конкретном случае… Да, тут конвенцию надо знать наизусть, ничего не попишешь. Вообще говоря, Java-кодер средней руки должен неплохо знать специфику нескольких сотен классов и, соответственно, их методов. C’est la vie…


      1. quaer
        02.06.2023 07:19

        Захотелось пройти этот квест, может быть есть где-то онлайн с аввтоматической выпиской сертификата?


        1. IBS_habrablog Автор
          02.06.2023 07:19

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


          1. quaer
            02.06.2023 07:19

            А как это в офлайн-формате происходит?


            1. IBS_habrablog Автор
              02.06.2023 07:19

              Вот здесь довольно подробно расписан весь процесс: https://ibs-training.ru/certification/java/rules/


      1. rukhi7
        02.06.2023 07:19

        Вообще говоря, Java-кодер средней руки должен неплохо знать специфику нескольких сотен классов и, соответственно, их методов.

        Почему то к этому очень хочется добавить:

        "И не задавать лишних вопросов синьорам-архитекторам, потому что он должен в первую очередь помнить что он всего лишь кодер средней руки"


      1. rukhi7
        02.06.2023 07:19
        +1

        А еще:

        Да, тут конвенцию надо знать наизусть, ничего не попишешь

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

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

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


        1. IBS_habrablog Автор
          02.06.2023 07:19

          С нашей бизнес-моделью все в порядке, мы всего лишь следуем устоявшейся, канонической практике. Точно такой же подход принят и у Oracle: в частности, сигнатуры популярных методов надо знать. Как и всякую материальную часть своей предметной области. Тем более в классах из пакета java.lang.


          1. rukhi7
            02.06.2023 07:19
            +5

            java.lang является предметной областью для тех кто пишет компилятор java, вроде как Oracle как раз имеет свой java-компилятор. Вы же не пишете свой java компилятор?

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

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