Эпизод 1. Скрытая угроза Java Core. Уровень Юнглинг

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
image

Мы публикуем серию статей для подготовки к собеседованиям Java-разработчиков. Будем рассказывать о том, как разработчику успешно пройти собеседование и не поседеть во время чтения тонн мануалов. Мы не пытаемся создать энциклопедию, в которой будут отражены тысячи вопросов на интервью, но поможем понять – о чем могут спрашивать и как отвечать на сложные вопросы, чтобы избежать стресса. Итак, первый материал посвящен базовому уровню языка программирования Java Core.

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

Давным-давно,
в далекой-далекой галактике…

Юного Люка Скайуокера мучает разного рода вопросами пытливый мастер Йода. А Йода, как известно, писал код, когда мы еще с вами под стол ходили. Причем кодил он прямо в блокноте без дебаггера, intellij idea и прочей богомерзкой ерунды. Когда же он уставал от нововведений, то просто пихал в дисковод компьютера перфокарты…

Мир тебе, юный Люк. Вопрос мой первый слушай ты.

1. Какие есть пулы и представляют они из себя что? Расширять можно ли их?

Ответ:
Мастер Йода, пулы бывают целых чисел: Byte, Short, Integer, Long. Они представляют из себя массив, его размерность по умолчанию от -128 до 127. Хотя эту самую размерность можно менять с помощью ключа -XX:AutoBoxCacheMax=<размер>

Также есть пул Character. Это тоже массив значений от \u0000 и до \u07F. Символ \u07F — это DELETE.

Историю про перфокарты и символ таинственный этот тебе расскажу я:

Символ \u07F — это семь единиц в битовом представлении. Данные на перфокартах располагались обычно в 7 рядов (соответствующих семи битам байта). Единицам соответствовало пробитое отверстие, нулю, соответственно, отсутствие отверстия. Таким образом, байт со всеми единицами в разрядах можно было пробить поверх любого другого.

В случае ошибочной записи на перфокарту неправильные байты забивались этим символом. При исполнении же программы данный символ просто игнорировался.

Так \u07F стал обозначать DELETE или RUBOUT. Понятно, Люк?

Потрясающе, мастер.

2. Тут мудрость скрыта, над ней помедитируй. А сейчас продолжим. Слухи до меня дошли, что есть еще один пул. Правда ли это?

Ответ:
Есть пул String. В нем содержаться строки и на одну строку может быть несколько ссылок.

3. Ответишь без труда ты, сколько будет создано объектов после выполнения кода:

String str = new String(“Очень важная строка, вах!”)?

Ответ:
Два объекта. Тебе меня не провести. Вызов конструктора всегда приводит к помещению нового объекта в область, не являющуюся пулом. Смекаешь? А аргумент конструктора — тоже строка. Следовательно, она будет помещена в пул строк при условии, конечно, что там нет строки такого же содержания. Получается, что создается два объекта: один просто в heap, а другой — именно в сам пул строк.

4. Достойно рассказал. Есть же метод специальный, принудительно строку в пул строк помещает который. Знаешь про него?

Ответ:
Конечно. Это метод intern(). Он принудительно помещает строку в пул строк, если там ее еще нет, и возвращает ссылку на нее. Если же строка уже есть, то метод просто вернет ссылку на этот объект. Проще пареной репы.

Вот пример:
image

5. Заметил верно ты — строки неизменяемые. Но зачем это?

Причина 1. JVM хитрая, неизменяемость строк позволяет ей знатно экономить место. Вот, допустим, один объект, а ссылок на него три, иначе пришлось бы создавать на три ссылки — три объекта.

Причина 2. Безопасность. Отправили мы какие-либо параметры для авторизации и сидим себе уверенные в том, что именно то, что отправили, и придет. Строки-то не меняются!

Причина 3. Очень удобно для многопоточности. Строки потокобезопасны, поэтому один экземпляр может вовсю использоваться разными потоками и будет всем им невообразимое счастье.

Причина 4. А это уж сам Бог велел использовать их как ключи для HashMap. hashcode строки кэшируется в момент создания и нет никакой необходимости рассчитывать его снова.

Вот по этим причинам строки immutable.

6. Знатно рассказал. А вот эти пулы все — где хранятся-то они?

Ответ: Естественно, пулы хранятся в Heap, а ссылки в Stack.

7. А в чем вообще этих памяти областей отличия?

Ответ:
Вот основные отличия:
— Heap (она же куча) — используется всем приложением, Stack — одним потоком исполняемой программы;
— Новый объект создается в heap, в stack размещается ссылка на него. В стеке размещаются локальные переменные примитивных типов;
— Объекты в куче доступны из любого места программы, стековая память не доступна для других потоков;
— Если память стека закончилась JRE вызовет исключение StackOverflowError, если куча заполнена OutOfMemoryError;
— Размер памяти стека, меньше памяти кучи. Стековая память быстрее памяти кучи;
— В куче есть ссылки между объектами и их классами. На этом основана рефлексия.

8. И про память ты знаешь. А что можешь рассказать за Гарбач Коллектор?

Ответ:
Могу много рассказать, но давай поговорим об этом отдельно. Очень большая и интересная тема, обсудим ее в следующий раз.

9. А что такое сигнатура метода и контракт метода?

Сигнатура: название метода и параметры. Всё, больше там ничего нет.
Контракт: сигнатура метода плюс тип возвращаемого значения, а также бросаемые исключения

10. Давай обсудим вопрос явного приведения типов. Что будет, допустим, попробуем если Integer запихнуть в Byte мы?

Если значение входит в диапазон, то приведение типов пройдет корректно. Если же нет, то будут отброшены старшие биты и значение не будет релевантным.

11. Хорошо. Short занимает 2 байта, а Character также занимает 2 байта. Получается можем безопасно приводить эти типы друг другу мы?

Ответ:
С одной стороны да, а с другой — нет. В Character значения только положительные, а вот в Short у нас примерно от -32000 до 32000.
Следовательно, если значение не будет влезать, то будут просто-напросто отброшены старшие биты.

12. Ладно. А вот для того, чтобы long к float привести потребуется явное приведение типов?

Ответ:
Не-а. Не потребуется. Так велел Великий Бог Машина Омниссия создателям Java

13. Float зверь интересный вообще. Можно в конструкции switch case использовать его?

Нет, мастер. В этой конструкции можно использовать только целочисленные типы, а также с 7 версии Java можно String. Сравнение будет не как раньше по ссылке, а через equals.
При этом надо помнить, что строки чувствительны к регистру. А также, что оператор switch использует метод String.equals() для сравнения полученного значения со значениями case, поэтому обязательно нужна проверку на NULL во избежание NullPointerException.

14. Многое узнать ты можешь еще, падаван. Это только начало. Про принципы ООП поговорим. Известно ли то самое правильное определение инкапсуляции тебе?

Ответ:
Конечно, мастер Йода. Инкапсуляция — это объединение данных и методов работы с этими данными в одной упаковке.

15. Знаком ли ты с ранним и поздним связыванием вызова метода с телом его?

Ответ:
О, да.
Раннее связывание — это связывание перед запуском программы. Его еще называют статическим.
Позднее связывание или динамическое — это связывание во время выполнения программы.
Для всех методов в Java используется позднее связывание. Исключением являются final методы, а приватные методы final по умолчанию.

16. Ответ на вопрос должен дать ты: отличие композиции от агрегации и ассоциация такое что?

Ответ:
Ассоциация — это просто связь между объектами.
Агрегация — это связь по типу часть — целое.
А композиция — это связь часть — целое, при которое экземпляр части может принадлежать только одному целому.

17. Я вижу Силу в тебе. Про ClassLoader расскажешь ты мне.

Ответ:
Загрузчик классов отвечает за поиск библиотек, чтение их содержимого и загрузку классов, содержащихся в библиотеках. Эта загрузка обычно выполняется «по требованию», поскольку она не происходит до тех пор, пока программа не вызовет класс. Класс с именем может быть загружен только один раз данным загрузчиком классов.

Сам загрузчик классов является частью JRE, которая динамически загружает Java классы в JVM. Система исполнения в Java не должна знать о файлах и файловых системах благодаря загрузчику классов. Делегирование является важной концепцией, которую выполняет загрузчик.

Всего их три вида: загрузчик класса bootstrap, загрузчик класса расширений и системный загрузчик.

Первый загружает основные библиотеки расположенные в папке <JAVA_HOME>/jre/lib.

Второй загружает код в каталоги расширений (<JAVA_HOME>/jre/lib/ext или любой другой каталог, указанный системным свойством java.ext.dirs).
Третий загружает код, найденный в java.class.path, который сопоставляется с переменной среды CLASSPATH.

18. Про JRE упомянул ты. Что такое JRE? От JDK отличие в чем его?

Ответ:
JDK (Java Development Kit) — включает JRE и набор инструментов разработчика приложений на языке Java:
— компилятор Java (javac)
— стандартные библиотеки классов java
— документацию
— различные утилиты

JRE (java Runtime Environment) — минимально-необходимая реализация виртуальной машины для исполнения Java-приложений. Если нужно просто запустить программу, то понадобится только JRE.
В JRE входит: JVM, ClassLoader и стандартного набора библиотек и классов Java

19. Я не буду тебя спрашивать про SOLID. Лучше скажи, в нашей любимой Java нарушения этих принципов есть ли?

Ответ:
Да, допустим, нарушается принцип подстановки Барбары Лисков в коллекциях. У нас есть ArrayList параметризованный Number. И хотя Integer является наследником Number мы не сможем положить объект этого типа в коллекцию.

20. Сколько методов у функционального интерфейса может быть?

Ответ:
У функционального интерфейса может быть только один абстрактный метод. А вот дефолтных методов — сколько душе разработчика заблагорассудится.

21. Какой в них смысл? Какая суть?

Ответ:
Их не было до эпохальной 8 версии. Нужны же они для обратной совместимости, а также это позволяет избежать создания служебных классов, так как все необходимые методы могут быть представлены в самих интерфейсах.

22. Неплохо. Поговорим про исключения, а точнее про конструкцию try — catch. Скажи мне, блок finally выполняться не будет когда?

Ответ:
О, он выполняется почти всегда. Кроме как в ситуации, когда в блоке try будет бесконечный цикл, а также если кто-то надумает вызвать метод System.exit().
Если операционная система завершит работу JVM.
И ещё случай. Если блок finally будет выполняться потоком демона, а все остальные потоки не демоны завершат свое выполнение.

23. А вот такой вопрос. Если будет исключение в блоке try — catch у нас и в блоке finally. Получим в итоге что?

Ответ:
Исключение из finally. Finally затащит!

24. А если у нас конструкция try-with-resources. И произойдет исключение при ресурса закрытии, то исключение получим какое?

Ответ:
Брошенное try-блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия.

25. Верно. А если у нас будет return и в блоке try, и в блоке finally? finally сработает ли вообще?

Ответ:
Конечно, сработает. Finally strong! Мы получим то значение, которое будет передано через return в блоке finally.

26. Вижу жажда знания твоя глубока. Поведай — что про ромбовидное наследование знаешь ты.

Ответ:
Его, можно сказать, нет в Java. Вернее, для классов множественное наследование вообще запрещено, а вот для интерфейсов есть. Можно наследовать интерфейс от двух других интерфейсов. В таком случае нужно будет просто явно вызвать через super конкретный метод родителя.

27. Про блоки инициализации и порядок их вызова знаешь ты?

Ответ:
Существуют статические и нестатические блоки инициализации. Статические используются для кода, который должен выполниться только один раз при инициализации класса загрузчиком классов. Нестатические же, соответственно, каждый раз при создании экземпляра класса.

Что касается порядка их вызова, то сначала вызываются статические блоки от первого родителя и до последнего предка наследника. Затем попарно динамической блок инициализации и конструктор от первого до последнего предка.

28. Время обсудить equals и hashcode. Как друг с другом связаны они?

Ответ:
Между ними есть контракт.
1) Если два объекта возвращают разные значения hashcode(), то они не могут быть равны;
2) Если equals объектов true, то и хэш коды должны быть равны;
3) Переопределив equals, всегда переопределять и hashcode.

А вот если хэш-коды одинаковые, то объекты могут быть как одинаковыми, так и разными. Ситуация, когда у разных объектов одинаковый хэш-код, называется коллизией. Коллизии всегда будут, так как количество объектов не ограничено, а вот количество хэш кодов ограничено типом int.

29. Получается, нужно переопределить equals и hashcode. hashcode. Метод equals реализовать как угодно можно?

Ответ:
Нет, конечно. Есть несколько правил:

1) Рефлексивность. Для любого заданного значения x, выражение x.equals(x) должно возвращать true;
2) Симметричность. Для двух ссылок, a и b, a.equals(b) тогда и только тогда, когда b.equals(a);
3) Транзитивность. Если a.equals(b) и b.equals©, то тогда a.equals©;
4) Консистентность. Повторный вызов метода equals() должен возвращать одно и тоже значение до тех пор, пока какое-либо значение свойств объекта не будет изменено;
5) Совместимость с hashCode(). Два тождественно равных объекта должны иметь одно и то же значение hashCode().

30. Что касается методов, то знаешь ли ты, юный Люк, как передаются параметры в метод: по ссылке или по значению? На этом вопросе сыпались даже самые продвинутые падаваны…

Ответ:
Мастер Йода, ответ на этот вопрос я знал еще в детском саду. Кто не знал ответа, тех самозабвенно пинали ногами… Параметры передаются всегда по значению. Если это примитив, то передается само значение. Следовательно, изменения никак не влияют на исходные данные. Если же это ссылка, то передается ее значение. Изменить саму исходную ссылку, понятное дело, нельзя, а вот объект по ссылке можно. Так что тут нужно быть очень осторожным в своем безудержном стремлении менять этот мир.

31. Люк, я не твой отец и даже не твой дед… Но вопрос про коллекции слушай ты.

Ответ:
О, Господи…
32. Когда нужно использовать LinkedList вместо ArrayList?

Ответ:
Представлю слово автору Framework Collection Джошуа Блоху:

image

Он-то уж точно понимает в Java. Вообще, нужно максимально избегать использования LinkedList. Единственный способ, когда есть смысл его использовать — это многократная вставка или удаление элементов в начале списка. В таком случае в ArrayList будет происходить постоянное копирование всех элементов. Хотя под капотом для этого и используется очень быстрый нативный метод System.arrayCopy(), но даже для него это слишком и он в такой ситуации категорически не вывозит.

При этом, если это не ситуация, когда нам нужно записывать, а потом достаточно быстро читать данные. В случае буфера обмена отлично подойдет ArrayDeque.

33. Знаешь о нем ты?
Ответ:
Конечно, это закольцованный массив с указателями на начало и конец. Хорошая вещь.

У ArrayDeque интересный принцип увеличения размера. При первом заполнении он увеличивается в два раза, но плюс еще 2 байта. 16 + 16 + 2. Итого 34. Во второй раз также. 34 + 34 + 2. Итого 70. А потом уже увеличивается каждый раз на 50%.

34. Разработчики некоторые используют конструкцию List.of() или Map.of(). Особенность в чем?

Ответ:
List.of() как и Map.of() неизменяемы.
Map.of() под капотом представляет собой массив Object. Причем нечетные значения — это ключ, четные — значения. При коллизии происходит сдвиг вправо и так пока место не будет найдено. Этот подход называется методом открытой адресации, также известный как linear probing.
35. Через Силу увидишь вещи. Другие места. Будущее … прошлое. Старые друзья давно ушли…

*Тут Люк подумал, что маразм победил мастера Йоду окончательно, но он отогнал от себя эти мысли как наваждение Тьмы*

Мастер Йода продолжает: Расскажи про PriorityQueue мне.

Ответ:
PriorityQueue — это очередь с приоритетом. Представляет собой массив, при этом есть два условия, собственно, тот приоритет, о котором говорится в названии:
q[n] <= q[2n + 1]
q[n] <= q[2n + 2]

Это приводит к тому, что самый первый элемент будет всегда минимальным в структуре. Алгоритмическая сложность вставки/удаления элемента O(logN). По умолчанию первый раз создается массив из 11 элементов, уникальное свойство, между прочим. Так как у map по умолчанию 16 элементов, а у ArrayList — 10.

36. Ответил достойно ты. Вижу, что знаешь ты. Расскажи про разные виды итераторов.

Ответ:
Есть итератор fail-fast. С ним проблема: он генерирует исключение ConcurrentModificationException, если коллекция меняется во время итерации, но работает быстро. Пример fail-fast — Vector и Hashtable.

Допустим, вот такой код упадет с ошибкой:

image

И это печально.

А есть итератор fail-safe не вызывает исключений при изменении структуры коллекции, потому что работает с её клоном. Пример fail-safe — CopyOnWriteArrayList и итератор keySet коллекции ConcurrentHashMap. Вещь!

37. Ответ твой прекрасен. Что про красно-черные деревья скажешь вообще?

Ответ:
Красно-чёрное дерево — это бинарное дерево поиска с дополнениями. Само же бинарное дерево поиска представляет собой дерево, в котором у узла может быть ноль, один или два потомка. При этом слева значения меньше родителя, справа — больше. Вроде бы неплохо, но если мы будет добавлять отсортированные элементы, то наше дерево выродится в список и алгоритмическая сложность вместо O(logN) будет O(N).

А это плохо, поэтому и используются красно-черные деревья. У них есть дополнительные свойства, а именно появляется маркер цвета:

Корень всегда черный
У красного родителя всегда черные потомки
Листья черные и не содержат значений
Количество черных узлов в глубину одинаковое

При добавление элемента может потребоваться до двух поворотов дерева, при удалении — до трех.

38. Вот у нас есть такая коллекция TreeSet, которая под капотом как и все Set, представляет собой Map, а именно — TreeMap. Я хочу добавить туда null. Что будет?

Ответ:
Нельзя добавить null в TreeSet. Вернее добавить-то можно, но при запуске программа свалится с ошибкой. Все элементы, которые добавляем, мы должны сравнивать. Как ты сравнишь null? Никак. Это же дерево! Если только, конечно, не напишем специальный компаратор, который передадим TreeSet в конструктор.

39. А как вообще TreeSet превращается в TreeMap?

Ответ:
Очень просто, Мастер. Мы используем в качестве key добавляемое значение.

Допустим. А что же в качестве value положим мы?

Есть специальная константа, которая используется как заглушка. Она типа Object. Зовется красивым английским словом — Present.

40. Вижу на мякине не проведешь тебя. Достойно. Вопрос мой следующий ты слушай. Сколько будет занимать Node, в которой значение примитива типа byte в LinkedList и в ArrayList лежит?

Ответ:
Учитель, ты меня не уважаешь, раз спрашиваешь столь простые вопросы?

Начнем с ArrayList. Тут все предельно просто. ArrayList основан на массиве. Для примитивных типов данных осуществляется автоматическая упаковка значения, поэтому 16 байт тратится на хранение упакованного объекта и 4 байта (8 для x64) — на хранение ссылки на этот объект в самой структуре данных. Таким образом, в x32 JVM 4 байта используются на хранение одного элемента и 16 байт — на хранение упакованного объекта типа Byte. Для x64 — 8 байт и 24 байта соответственно.

С LinkedList все интереснее.

Для 32-битных систем каждая ссылка занимает 32 бита (4 байта). Сам объект (заголовок) вложенного класса Node занимает 8 байт. 4 + 4 + 4 + 8 = 20 байт. Размер каждого объекта в Java кратен 8, соответственно получаем 24 байта.
Примитив типа byte занимает 1 байт памяти, но в Java Collections Framework примитивы упаковываются: объект типа Byte занимает в памяти 16 байт (8 байт на заголовок объекта, 1 байт на поле типа byte и 7 байт для кратности 8).
Но значения от — 128 до 127 кэшируются и для них новые объекты каждый раз не создаются. Таким образом, в x32 JVM 24 байта тратятся на хранение одного элемента в списке и 16 байт — на хранение упакованного объекта типа Byte. Итого 40 байт.

Для 64-битной JVM каждая ссылка занимает 64 бита (8 байт), размер заголовка каждого объекта составляет 16 байт (два машинных слова). Вычисления аналогичны: 8 + 8 + 8 + 16 = 40 байт и 24 байта. Итого 64 байта!

Мастер Йода:
Да пребудет с нами Сила! Знаю я, что LinkedList помогал писать сам Дарт Вейдер, порождение Темной стороны он. Встретимся завтра и поговорим про память, виды ссылок, многопоточность и Гарбач Коллектор.

Люк:
До завтра, Учитель!

directed by Григорий Аверьянов, старший специалист филиала ЦК АРГО компании Neoflex.

Выражаю благодарность за советы и поддержку следующим хорошим людям: Михаилу и Алексею Деевым, Андрею Ястребову, Александру Резцову.
Источник: https://habr.com/ru/company/neoflex/blog/675484/


Интересные статьи

Интересные статьи

В мире, где цифровая трансформация правит бал, Интернет вещей (IoT) играет ключевую роль в переосмыслении нашего образа жизни и бизнеса. Интернет вещей экономит наше врем...
2020 год для Solar JSOC CERT оказался непростым, но и 2021-й не отстает, постоянно подкидывая нам интересные кейсы. В этом посте мы расскажем, как обнаружили вредонос (даже целую сеть вре...
Привет, Хабр! Мы любим, когда все необходимые вещи у нас под рукой. Проблема только в том, что необходимые вещи почему-то имеют свойство накапливаться, а рук у нас всего две, и чер...
JavaScript язык особенный. Сколько его не изучай, всегда найдутся моменты, которые заставят даже матёрого профессионала начать чесать репу. В этой статье приводятся несколько задачек на JavaSc...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?