17. Приведите примеры удачного и неудачного использования Optional
Предположим, у нас есть некоторый ряд значений, по которому мы проходимся стримом, и в итоге получаем некоторый Optional как результат:
Optional<String> stringOptional = Stream.of("a", "ab", "abc", "abcd")
.filter(str -> str.length() >= 3)
.findAny();
Нам, как полагается, нужно достать из этого Optional значение.
Просто использовать get() — плохой способ:
String result = stringOptional.get();
Но ведь этот метод должен достать значение из Optional и вернуть нам? Это, конечно, так, но если в нём есть значение.
Ну а если значения в стриме были другие, и в итоге мы получили пустой Optional, при попытке взять значение из него посредством использования метода get() будет выброшено:
Что не есть хорошо.
В таком случае лучше использовать конструкции:String result = null; if (stringOptional.isPresent()) { stringOptional.get(); }
В данном случае мы проверяем, есть ли элемент в Optional. Если нет — результирующая строка имеет свое старое значение.
String result = stringOptional.orElse("default value");
В данном случае мы указываем некоторое значение по умолчанию, которое будет заданно результирующей строке в случае пустого Optional.
String result = stringOptional.orElseThrow(() -> new CustomException());
В данном случае мы сами выбрасываем исключение при пустом Optional.
18. Можно ли объявлять main method как final?
Да, несомненно, ничто не мешает нам объявить метод main() как final. Компилятор не выдаст ошибок. Но стоит помнить, что какой-либо метод после объявления его как final станет последним методом — не переопределяемым. Хотя, кто будет переопределять main???19. Можно ли импортировать те же package / class дважды? Какие могут быть последствия?
Да можно. Последствия? У нас будет пара ненужных импортов, которые Intelijj IDEA будет отображать как серые, т.е. неиспользуемые.20. Что такое Casting? Когда можем получить исключение ClassCastException?
Casting, или приведение типов — это процесс преобразования одного типа данных в другой тип данных: вручную (неявное приведение) или автоматически (явное приведение типов). Автоматическое преобразование выполняет компилятор, а ручное — разработчик. Приведение типов для примитивов и классов несколько отличается, поэтому и рассмотрим их по отдельности. Примитивные типы Пример автоматического приведения примитивных типов:
int value = 17;
double convertedValue = value;
Как видите, никаких дополнительных манипуляций помимо знака = тут не нужно.
Пример ручного приведения примитивных типов:
double value = 17.89;
int convertedValue = (int)value;
В этом случае мы можем наблюдать ручное приведение, которое реализуется с помощью (int), при этом часть за запятой будет отброшена, и convertedValue будет иметь значение - 17.
Подробнее о приведении примитивных типов читайте в этой статье.
Ну а теперь давайте перейдем к объектам.
Ссылочные типы
Для ссылочных типов автоматическое приведение возможно для классов наследников к классам родителям. Это так же называется полиморфизмом.
Предположим, у нас есть класс Lion, который наследуется от класса Cat. В этом случае автоматическое преобразование будет выглядеть так:
Cat cat = new Lion();
А вот с явным приведением всенесколько сложнее, ведь нет функционала обрезания лишнего, как у примитивов.
И сделав просто явно преобразование вида:
Lion lion= (Lion)new Cat();
Вы и получите ошибку:
В самом деле, вы можете классу наследнику Lion добавить методы, которых не было изначально в классе Cat, и потом пытаться их вызвать, ведь типа объекта у вас станет Lion. Ну а в этом логики никакой нет.
Поэтому, сужение типа возможно лишь когда изначальный объект типа Lion, но был позже приведен к классу родителя:
Lion lion = new Lion();
Cat cat = lion;
Lion newLion = (Lion)cat;
Также, для большей надежности, сужающее приведение для объектов рекомендуется с использованием конструкции instanceOf:
if (cat instanceof Lion) {
newLion = (Lion)new Cat();
}
Подробнее о приведениях ссылочных типов — в этой статье.
21. Почему современные фреймворки используют в основном только unchecked exceptions?
Думаю, это все потому, что обработка checked исключений — это ещё тот спагетти код, который повсеместно повторяется, при этом не во всех случаях действительно нужен. В таких случаях легче уже сделать обработку внутри фреймворка, чтобы лишний раз не перекладывать это на плечи разработчиков. Да, несомненно, аварийная ситуация может возникнуть, но эти самые uncheked исключения можно обрабатывать более удобным способом, не заморачиваясь над обработкой в try-catch и не прокидывая дальше по методам. Достаточно всего лишь в exceptionHandler-е конвертировать исключение в некоторый HTTP-ответ.22. Что такое static import?
При использовании статических данных (методов, переменных) можно не создавать сам объект, а делать это по имени класса, но и в таком случае нам необходима ссылка на класс. С ней все просто: она добавляется при помощи обычного импорта. Но что если мы заходим использовать статический метод без написания имени класса, как будто это статический метод текущего класса? Это возможно с помощью статического импорта! В таком случае мы должны прописывать static import и ссылку на тот метод. Как вот например, статический метод класса Math для вычисления значения косинуса:
import static java.lang.Math.cos;
В итоге мы можем использовать метод без указания имени класса:
double result = cos(60);
Также элементарно мы можем подгрузить сразу все статические методы класса с помощью статического импорта:
import static java.lang.Math.*;
23. Какая связь между методами hashCode() и equals()?
Согласно Oracle, существует следующее правило: Если два объекта равны (т.е. метод equals() возвращает true), у них должен быть одинаковый хэш-код. При этом не стоит забывать, что одинаковый хэш-код может быть у двух разных объектов. Чтобы разобраться, почему же equals() и hashCode() переопределяют всегда в паре, рассмотрим следующие случаи:Оба метода переопределенные.
В таком случае два разных объекта с одинаковыми внутренними состояниями будут возвращать при equals() — true, в то время как и hashCode() будет у обоих возвращать одно и то же число.
Получается, все окей, ибо правило выполняется.
Оба метода не переопределенные.
В таком случае два разных объекта с одинаковыми внутренними состояниями при equals() будут возвращать false, так как сравнение идёт по ссылке через оператор ==.
Метод hashCode() также вернет разные значения (скорее всего), так как он выдает преобразованное значение адреса ячейки памяти. Но у одного и того же объекта это значение будет одинаковым, как и equals() в данном случае вернет true, только когда ссылки указывают на один и тот же объект.
Получается, и в данном случае все окей и правило выполняется.
Переопределен equals(), не переопределен hashCode().
В таком случае для двух разных объектов с одинаковыми внутренними состояниями equals() будет возвращать true, а hashCode() будет возвращать (скорее всего) разные значения.
Происходит нарушение правила, поэтому так делать не рекомендуется.
Не переопределен equals(), переопределен hashCode().
В таком случае для двух разных объектов с одинаковыми внутренними состояниями equals() будет возвращать false, а hashCode() будет возвращать одинаковые значения.
Происходит нарушение правила, поэтому подход неверный.
24. Когда используют BufferedInputStream и BufferedOutputStream классы?
InputStream используется для побайтового чтения данных из некоторого ресурса, а OutputStream — для побайтовой записи. Но побайтовые операции могут быть весьма неудобными и требуют дополнительной обработки (чтобы нормально считывать/записывать тексты). Собственно, для упрощения таких байтовых записей ввели BufferedOutputStream, а для чтения BufferedInputStream. Эти классы являются не чем иным как буферами, накапливающими данные, позволяющими работать с данными не побайтово, а целыми пакетами данных (массивами). При создании BufferedInputStream принимает в конструктор экземпляр типа InputStream, с которого идёт считка данных:
BufferedInputStream bufferedInputStream = new BufferedInputStream(System.in);
byte[] arr = new byte[100];
bufferedInputStream.read(arr);
System.in — это объект типа InputStream, который считывает данные с консоли.
То есть с помощью данного объекта BufferedInputStream мы можем читать данные с InputStream, записывая их в переданный массив. Получается своего рода обертка класса InputStream.
Массив arr из данного примера — массив которому достаются данные из BufferedInputStream. Тот в свою очередь читает данные из InputStream другим массивом, который по умолчанию имеет размер 2048 байт.
Аналогично и для BufferedOutputStream: в конструктор необходимо передать экземпляр типа OutputStream, в который мы будем писать данные целыми массивами:
byte[] arr = "Hello world!!!".getBytes();
BufferedOutputStream bufferedInputStream = new BufferedOutputStream(System.out);
bufferedInputStream.write(arr);
bufferedInputStream.flush();
System.out — это объект типа OutputStream, который записывает данные в консоли.
Метод flush() отправляет данные с BufferedOutputStream в OutputStream, очищая при этом BufferedOutputStream. Без этого метода ничего записываться и не будет.
И аналогично предыдущему примеру: arr — это массив, из которого записываются данные в BufferedOutputStream. С него же они пишутся в OutputStream уже другим массивом, который по умолчанию имеет размер 512 байт.
Подробнее об этих двух классах — в статье.
25. Какая разница между классами java.util.Collection и java.util.Collections?
Collection — интерфейс, который является головой в иерархии коллекций. Он представляет классы, позволяющие создавать, содержать и изменять целые группы объектов. Для этого предоставляется множество методов, вроде add(), remove(), contains() и других. Основные интерфейсы класса Collection:Set — интерфейс, описывающий множество, которое содержит неупорядоченные уникальные (неповторяющиеся) элементы.
List — интерфейс, описывающий структуру данных, которая хранит упорядоченную последовательность объектов. Эти объекты получают свой индекс (номер), используя который можно взаимодействовать с ними: брать, удалять, изменять, перезаписывать.
Queue — интерфейс, описывающий структуру данных с хранением элементов в виде очереди, которая следует правилу — FIFO — First In First Out.
addAll(Collection<? super T> collection, T…element) — добавляет в collection переданные элементы типа Т.
сopy(List<? super T> dest, List<? extends T> src) — копирует все элементы из списка src в список в dest.
emptyList() — возвращает пустой список.
max(Collection<? extends T> collection, Comparator<? super T> comp) — возвращает максимальный элемент данной коллекции в соответствии с порядком, установленным указанным компаратором.
unmodifiableList(List<? extends T> list) — возвращает неизменяемое представление переданного списка.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ