Предположим, нам дали два целых числа, но не примитивы, а Integer-объекты…
Итак, один из подвопросов, традиционно выносимых на экзамен – будь это сертификация Oracle или сертификации для Java-разработчиков от Учебного центра IBS – интересуется разработкой кода с использованием классов-оболочек, в частности, Boolean, Double или, скажем, Integer. Такая задача может быть сформулирована в следующей форме:
Как мы видим, в вопросе задействованы два класса-оболочки, а именно 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), который показывает, где именно в памяти расположен тот или иной объект.
Если выражение выражение имеет примитивный тип, то это бинарная репрезентация некоего числа, например, 0b101010, то есть, десятичное 42. Это число будет записано в стеке и ему будет присвоена некая метка, которую мы привыкли называть словом "переменная". Но вот если речь идет не про примитив 42, а про Integer-объект, который инкапсулирует подстилающий примитив 42, то в стеке будет сидеть уже не 0b101010, а нечто куда более "вихрастое", например, 0xffff5637. Эта комбинация шестнадцатиричных литералов по сути является значением ссылки на объект, который в языке Java живет не в стеке, а в т.н. динамически аллоцируемой памяти, в знаменитой "куче" (heap). К примеру, в языке C/С++, прородителе Java, значением пойнтера является адрес объекта. В Java ситуация несколько сложнее, но в первом приближении мы тоже можем сказать, что значением ссылки (которая живет в стеке) является адрес объекта (который, напомним, живет на heap'е). Что в итоге? Пусть оператор "double equals" сравнивает значения двух переменных. Если это примитивы, JVM сравнит их бинарные значения, то есть – числа. Если это ссылки, то JVM сравнит их значения, но ведь значением ссылки является адрес того объекта, на который данная ссылка указывает. Вот почему принято говорить, что оператор == проверяет эквивалентность примитивных значений, а в случае ссылок он сравнивает уже идентичность объектов. Если у двух ссылочных переменных (например, класса Integer) одно и то же значение, мы имеем дело с одним и тем же объектом (один и тот же адрес!), но если объектов два, то они расположены по разным адресам. В этой ситуации оператор == вернет false.
Сейчас не должно вызывать удивление, что данный код:
обязательно покажет false: ведь успешный вызов конструктора через new, как мы уже упоминали, приведет к рождению очередного объекта. Простое правило на нашем экзамене: сколько видим слов new, как минимум столько же будет объектов. Стало быть, переменные v1 и v2 по необходимости ссылаются на разные объекты, и сравнение их адресов оператором == даст нам false.
А вот небольшая вариация на эту же тему: пусть у нас такой код:
Опять мы видим 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, о чем мы уже упоминали.Расскажи друзьям: