Все вопросы:
В этой статье:
(нажмите для перехода)
➤ Какие есть типы данных в Java и какой у них размер
➤ Какие есть модификаторы в Java и Kotlin
➤ Какие вы знаете Java Collection
➤ Какие уровни сложности для операций с коллекциями
➤ Что такое HashMap
➤ Как отсортировать коллекцию, содержащую сложный класс, Comparable и Comparable
➤ В чем разница между ArrayList и LinkedList
➤ Как работает TreeMap
➤ Какие правила нейминга в Java
➤ Что такое статические классы в Java
➤ Какая разница между StringBuffer и StringBuilder
➤ Что такое рефлексия в Java
➤ В каких ситуациях возникает Memory leak
➤ Что такое Generic (Дженерики)
➤ Какие методы есть у класса Object в Java и Any в Kotlin
➤ Что такое Enum в Java
➤ Какой в Java приоритет при преобразовании примитивных типов
➤ Что такое приведение типов / преобразование типов / каст
➤ Для чего нужны и как создавать аннотации в Java
➤ Что такое Wrappers (обертки) в Java
➤ Какие есть операторы перехода
➤ Что такое Stack (стек) и Heap (куча) в Java
➤ Что такое JDK, JRE и JVM
➤ Какими значениями инициализируются переменные по умолчанию
➤ Возможно ли сузить уровень доступа/тип возвращаемого значения при переопределении метода
➤ Какие существуют типы ссылок в Java
➤ В чем разница между map и flatMap
➤ Какие есть типы ошибок в Java
➤ Как работает Garbage Collector и как организована память в JVM
➤ Что такое Java NIO (New Input/Output)
➤ Какой порядок выполнения Java класса
➤ Может ли абстрактный класс наследоваться от обычного
➤ Какие бывают циклы в Java и Kotlin
➤ Что такое ClassLoader
➤ Что такое SecurityManager
➤ Что такое JavaCompiler
➤ Как запустить JavaScript из Java
➤ Что такое Optional в Java и Kotlin
➤ Какие есть побитовые (поразрядные) операции в Java и Kotlin
➤ Что такое ByteBuffer и Unsafe
➤ Что такое лямбда (lambda) в Java
➤ Как в Java загрузить исполняемый код из сети
➤ Что такое AsynchronousServerSocketChannel
➤ Что такое функциональные интерфейсы и аннотация @FunctionalInterface в Java
➤ Какие есть встроенные функциональные интерфейсы в Java
➤ Какие есть типовые параметры в Java
➤ Какой механизм работы у volatile переменных в Java
➤ Как в JVM работают стеки потоков, с точки зрения взаимодействия с процессором
➤ Какие есть типы данных в Java и какой у них размер
Целочисленные:
byte:
8 бит, от -128 до 127
short:
16 бит или 2 байта, от -32_768 до 32_767
int:
32 бит или 4 байта, от -2_147_483_648 до 2_147_483_647
long:
64 бит или 8 байт, от -9_223_372_036_854_775_808 до 9_223_372_036_854_775_807
если значение, записываемое в long, превышает максимальное значение для int, после числа нужно добавлять букву l или L
С плавающей точкой:
float:
32 бит или 4 байта, от ~1,410 в -45 степени до ~3,410 в 38 степени
double:
64 бит или 8 байт, от ~4,910 в -324 степени до ~1,810 в 308 степени
Символ:
char:
16 бит или 2 байта, от ‘\u0000’ или 0 до ‘\uffff’ или 65_535
Логический:
boolean: 8 бит или 1 байт;
Также нужно помнить, что:
String не является примитивным типом данных
в Java примитивные типы не могут быть null, ссылочные типы могут быть null
в Kotlin и примитивные, и ссылочные типы могут быть null, если они объявлены как null не безопасные, то есть что они могут быть null (var i: Int? = null). В Kotlin нет примитивных типов в явном виде, компилятор сам решает, использовать примитивный тип или обертку
➤ Какие есть модификаторы в Java и Kotlin
В Java и Kotlin зоны видимости (модификаторы доступа) определяют, кто может получить доступ к классам, методам, полям и другим элементам кода. Рассмотрим основные зоны видимости для обоих языков.
Модификаторы доступа в Java:
В Java есть 4 уровня видимости:
private:
Доступен только внутри класса, в котором объявлен.
class MyClass { private int myVar = 10; // доступен только внутри MyClass }
default (пакетный уровень видимости):
Если модификатор не указан, используется видимость по умолчанию. Элемент доступен только внутри пакета, но недоступен для классов из других пакетов.
class MyClass { int myVar = 10; // доступен внутри текущего пакета }
protected:
Доступен внутри того же пакета и для классов-наследников, даже если они находятся в другом пакете.
class MyClass { protected int myVar = 10; // доступен в пакете и наследникам }
public:
Доступен из любого места.
Модификаторы доступа в Kotlin:
В Kotlin есть 4 уровня видимости, но их семантика несколько отличается от Java:
private:
Для класса: доступен только внутри этого класса.
Для пакета: доступен только внутри файла, в котором объявлен.
class MyClass { private val myVar = 10 // доступен только в MyClass }
internal:
Доступен внутри одного модуля. Это уникальный для Kotlin уровень видимости, который ограничивает доступность элементом модулем, а не пакетом.
internal class MyClass { internal val myVar = 10 // доступен только внутри модуля }
protected:
Доступен в классе и его подклассах. В отличие от Java, не доступен в рамках пакета (если класс не является наследником).
open class MyClass { protected val myVar = 10 // доступен в классах-наследниках }
public:
Доступен везде. Это видимость по умолчанию в Kotlin (если модификатор не указан).
class MyClass { val myVarOne = 10 // доступен всем public val myVarTwo = 10 // доступен всем }
Сравнение Java и Kotlin:
private: в обоих языках работает похоже, за исключением того, что в Kotlin private на уровне пакета ограничивает доступ внутри файла, а не всего пакета.
default: в Java аналогичен internal в Kotlin, но видимость Java ограничена пакетом, а Kotlin — модулем.
protected: в Java доступен в пакете и наследниках, а в Kotlin только наследникам.
public: одинаков в обоих языках.
internal: модификатор, которого нет в Java. Он ограничивает доступ на уровне модуля.
Все модификаторы:
У Java методов и переменных:
private:
доступен внутри класса
default (не указан):
доступен внутри пакета
protected:
доступен внутри пакета или из классов наследников
public:
доступен везде
static:
метод или переменная являются статическими
transient:
переменная не подлежит сериализации и не должна участвовать в переопределении equals() и hashCode()
final для метода:
метод нельзя перезаписать
final для переменной:
переменная является константой
abstract:
метод абстрактного класса является абстрактным и не имеет реализации
У Java классов:
default (не указан):
доступен внутри пакета
public:
доступен везде
final:
от класса нельзя наследоваться
abstract:
класс является абстрактным и может содержать абстрактные методы
record:
это специальный тип класса, введенный в Java 14 (в предварительном виде) и окончательно включенный в Java 16. Record предоставляет краткий и выразительный способ определения классов, которые являются простыми контейнерами для неизменяемых данных. Они значительно упрощают создание классов, которые просто хранят данные, автоматически генерируя конструкторы, методы equals(), hashCode() и toString().
У Java интерфейсов:
default:
начиная с Java 8 обычный метод внутри интерфейса, призван уменьшить дублирование кода, если во всех реализациях интерфейса будет один и тот же метод, при необходимости можно переопредеелить в реализации. Если класс имплементирует 2 интерфейса, в которых есть default методы с одинаковыми именами, произойдет конфликт имен, метод нужно будет переопределить.
static:
начиная с Java 8 статический метод в интерфейсе, вызывается только от имени интерфейса, но не от имени инстанса. Статические методы не наследуются имплементирующим классом и их нельзя переопределить. Начиная с Java 9 может быть private, чтобы можно было вызвать из другого статического метода.
В Kotlin также есть:
internal:
доступен внутри модуля
public (не указан)
open для класса:
от класса можно наследоваться, в противном случае нельзя,
open для метода:
метод можно перезаписать, в противном случае нельзя
object:
используется для объявления singletone и для создания анонимных объектов, которые являются заменой анонимных внутренних классов в Java, либо для внутренних параметров
vararg:
позволяет передавать нефиксированное число аргументов для параметра
init:
блок инициализации
lateinit:
свойство с поздней инициализацией
typealias:
предоставляет альтернативные имена для существующих типов
::
ссылка на класс, функцию или свойство
// ссылка на класс val c = MyClass::class // ссылка на свойство var x = 1 fun main(args: Array<String>) { println(::x.get()) // выведет "1" ::x.set(2) println(x) // выведет "2" } // ссылка на функцию fun isOdd(x: Int) = x % 2 != 0 val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // выведет [1, 3] // ссылка на конструктор class Foo fun function(factory : () -> Foo) { val x : Foo = factory() }
value class:
инлайн класс, отсутствует в байт коде- вместо него будет его значение, предназначен для оборачивания данных, не приводит к падению производительности
constructor:
дополнительныq конструктор (secondary constructors)
inline:
модификатор функции, использование функций высшего порядка (функция высшего порядка — это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.) влечёт за собой снижение производительности. Модификатор inline влияет и на функцию, и на лямбду, переданную ей: они обе будут встроены в место вызова. Встраивание функций может увеличить количество сгенерированного кода, но если вы будете делать это в разумных пределах (не инлайнить большие функции), то получите прирост производительности, особенно при вызове функций с параметрами разного типа внутри циклов.
noinline:
в случае, если вы хотите, чтобы только некоторые лямбды, переданные inline-функции, были встроены, вам необходимо отметить модификатором noinline те функции-параметры, которые встроены не будут
data class:
класс данных, компилятор автоматически формирует следующие члены данного класса из свойств, объявленных в основном конструкторе: equals()/hashCode(), toString(), componentN(), copy()
inner class:
в Kotlin статические внутренние классы не имеют никакого модификатора кроме class, а обычные внутренние классы, для доступа к которым необходим экземпляр внешнего класса, имеют модификатор inner class
sealed class:
добавление модификатора sealed к суперклассу ограничивает возможность создания подклассов. Все прямые подклассы должны быть вложены в суперкласс. Запечатанный класс не может иметь наследников, объявленных вне класса. По умолчанию запечатанный класс открыт и модификатор open не требуется. Запечатанные классы немного напоминают enum. В изолированном классе можно создать столько подклассов, сколько необходимо для покрытия каждой ситуации. Помимо этого каждый подкласс может иметь несколько экземпляров, каждый из которых будет нести в себе свое собственное состояние. Каждый подкласс изолированного класса имеет свой конструктор со своими индивидуальными свойствами. Изолированные классы — это enum с суперсилой. У изолированного класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Классы, которые расширяют наследников изолированного класса могут находиться где угодно.
sealed class StringSource { data class Text(val text: String) : StringSource() data class Res(val resId: Int) : StringSource() } val text = StringSource.Text("123") val textRes = StringSource.Res(123)
Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты. Конструктор изолированного класса всегда приватен, и это нельзя изменить. Изолированные классы нельзя инициализировать. Наследники изолированного класса могут быть классами любого типа: классом данных, объектом, обычным классом или даже другим изолированным классом.
Также нужно помнить что:
Все переменные в интерфейсах по умолчанию являются final
При наследовании и перезаписи можно расширять уровень доступа default -> protected -> public
При наследовании и перезаписи можно сужать уровень ошибок например Exception -> RuntimeException -> ArithmeticException
➤ Что такое Default method
Default method в Java
Default method (или метод по умолчанию) — это метод с реализацией, который может быть объявлен в интерфейсе. В Java возможность создания методов по умолчанию появилась с версии Java 8.
Основные особенности default method:
Метод с реализацией:
В отличие от обычных методов интерфейса, которые должны быть реализованы в классах, реализующих этот интерфейс, default method уже содержит свою реализацию.
Не нарушает совместимость:
Добавление новых методов в интерфейсы до Java 8 приводило бы к необходимости изменять все классы, которые этот интерфейс реализуют. Теперь с помощью методов по умолчанию можно добавлять новые методы в интерфейс без необходимости переписывать все существующие классы.
Наследование:
Классы, которые реализуют интерфейс, могут переопределять метод по умолчанию, если требуется особая логика. Если они этого не делают, используется реализация по умолчанию, указанная в интерфейсе.
Повторное использование кода:
Общую функциональность можно поместить в метод по умолчанию, чтобы избежать дублирования кода в различных классах.
Расширение функциональности:
Default methods позволяют расширять интерфейсы, добавляя новые методы, сохраняя при этом возможность использовать их в старом коде.
interface MyInterface { // Обычный абстрактный метод void regularMethod(); // Метод по умолчанию с реализацией default void defaultMethod() { System.out.println("This is the default method implementation."); } } class MyClass implements MyInterface { @Override public void regularMethod() { System.out.println("Implemented regular method."); } // Можно переопределить метод по умолчанию, если нужно @Override public void defaultMethod() { System.out.println("Overridden default method."); } } public class Main { public static void main(String[] args) { MyClass obj = new MyClass(); obj.regularMethod(); // Output: Implemented regular method. obj.defaultMethod(); // Output: Overridden default method. } }
Default method в Kotlin
Kotlin концепция default methods в интерфейсах реализована немного иначе, чем в Java, но функционально она присутствует через методы с реализацией в интерфейсах.
В Kotlin интерфейсы могут содержать реализацию методов, и классы, реализующие эти интерфейсы, могут использовать эту реализацию или переопределять методы при необходимости. Это аналогично методам по умолчанию в Java.
interface MyInterface { // Обычный метод, который нужно реализовать в классе fun regularMethod() // Метод с реализацией по умолчанию fun defaultMethod() { println("This is the default method implementation.") } } class MyClass : MyInterface { override fun regularMethod() { println("Implemented regular method.") } // Можно переопределить метод по умолчанию, если нужно override fun defaultMethod() { println("Overridden default method.") } } fun main() { val myClass = MyClass() myClass.regularMethod() // Output: Implemented regular method. myClass.defaultMethod() // Output: Overridden default method. }
default method в Java или методы с реализацией в интерфейсах Kotlin не могут иметь состояние напрямую, так как интерфейсы сами по себе не могут хранить состояние.
Интерфейсы не могут иметь экземплярные переменные (поля), которые могли бы хранить состояние между вызовами методов. Это связано с тем, что интерфейсы не предназначены для хранения состояния — их основная роль заключается в предоставлении контракта (набора методов), который классы должны реализовать.
Однако, методы по умолчанию могут обращаться к статическим переменным интерфейса или к данным, переданным в качестве аргументов. Но они не могут иметь собственных переменных экземпляра, которые могли бы хранить состояние.
Чем Default method отличается от статического метода:
Default method и статический метод в интерфейсах Java (и аналогично в Kotlin) различаются по нескольким важным характеристикам: их применению, контексту вызова и взаимодействию с реализациями интерфейса. Вот основные различия:
Способ вызова:
Default method:
Метод по умолчанию вызывается через экземпляр класса, который реализует интерфейс.
То есть, он может быть вызван на объекте, который реализует интерфейс
Статический метод:
Статический метод вызывается не через объект, а через сам интерфейс. Он привязан непосредственно к интерфейсу и не зависит от его реализации в классах.
Привязка к объекту:
Default method:
Привязан к объекту класса, который реализует интерфейс.
Он может использовать состояние объекта (например, поля класса) или вызывать другие методы объекта.
Статический метод:
Не имеет доступа к экземпляру объекта и, следовательно, не может использовать состояние объекта или вызывать нестатические методы.
Он может оперировать только данными, которые переданы ему как параметры, или работать со статическими полями (если такие есть).
Наследование и переопределение:
Default method:
Класс, реализующий интерфейс, может переопределить метод по умолчанию, если ему нужно предоставить свою собственную реализацию.
Статический метод:
Статические методы не могут быть переопределены в классах, реализующих интерфейс. Они принадлежат интерфейсу и не наследуются классами.
Если статический метод с тем же именем объявлен в классе, это просто другой метод.
Доступ к статическим методам интерфейса из его реализации:
Default method:
Методы по умолчанию могут использовать статические методы интерфейса, но для этого нужно явно вызывать их через имя интерфейса.
Статический метод:
Статические методы могут вызывать только другие статические методы интерфейса или работать с данными, переданными в параметры.
Основное назначение:
Default method:
Основная цель — предоставить возможность добавления новых методов в интерфейс без нарушения обратной совместимости. То есть, если в интерфейсе объявляется новый метод по умолчанию, старые реализации интерфейса не будут обязаны реализовать его.
Статический метод:
Статический метод чаще всего используется для выполнения операций, которые не зависят от экземпляра класса. Это утилитарные методы, которые имеют доступ только к статическим данным и не требуют состояния объекта.
Чем интерфейс с Default method отличается от абстрактного класса:
нтерфейсы с default methods и абстрактные классы в Java/Kotlin имеют много общего, поскольку оба могут предоставлять частичную реализацию методов. Однако между ними есть несколько ключевых различий, которые связаны с архитектурными и функциональными аспектами. Давайте рассмотрим основные различия между ними.
Наследование и множественная реализация:
Интерфейс с default methods:
В Java и Kotlin класс может реализовывать несколько интерфейсов. Это позволяет классу наследовать поведение от нескольких источников.
Интерфейс может содержать default methods — методы с реализацией по умолчанию, которые классы могут переопределить при необходимости.
Интерфейс может определять контракт (набор методов), который класс должен реализовать, и предоставлять общую реализацию некоторых методов.
interface InterfaceA { default void printMessage() { System.out.println("Message from InterfaceA"); } } interface InterfaceB { default void printMessage() { System.out.println("Message from InterfaceB"); } } class MyClass implements InterfaceA, InterfaceB { // Разрешаем конфликт между реализациями @Override public void printMessage() { InterfaceA.super.printMessage(); // Вызываем реализацию InterfaceA } }
В этом примере класс MyClass может реализовывать сразу два интерфейса и при необходимости выбрать, какую реализацию использовать.
Абстрактный класс:
Класс может наследоваться только от одного абстрактного класса. Это называется одноуровневое наследование, и это ограничивает гибкость по сравнению с интерфейсами.
Абстрактный класс может содержать как абстрактные методы (без реализации), так и конкретные методы (с реализацией). В отличие от интерфейсов, абстрактные классы могут иметь поля и конструкторы.
abstract class AbstractClass { abstract void printMessage(); void showMessage() { System.out.println("Message from AbstractClass"); } } class MyClass extends AbstractClass { @Override void printMessage() { System.out.println("Overridden message"); } }
Поддержка состояния:
Интерфейс с default methods:
Интерфейс не может хранить состояние напрямую, то есть он не может иметь нестатические поля или конструкторы. Все переменные в интерфейсе должны быть статическими и финальными (константами).
Default methods могут только использовать методы и состояние из классов, которые реализуют интерфейс. Это делает интерфейсы менее подходящими для случаев, когда требуется хранение состояния.
interface MyInterface { default void defaultMethod() { System.out.println("Default method"); } } class MyClass implements MyInterface { private int value = 10; // Состояние в классе public void showValue() { System.out.println("Value: " + value); } }
Абстрактный класс:
Абстрактные классы могут хранить состояние — они могут содержать поля, которые могут быть инициализированы через конструктор и использоваться в методах класса. Это делает абстрактные классы более гибкими для создания классов с общим состоянием и поведением.
Абстрактные классы могут иметь как статические, так и нестатические поля.
abstract class AbstractClass { protected int value; AbstractClass(int value) { this.value = value; } abstract void printValue(); } class MyClass extends AbstractClass { MyClass(int value) { super(value); } @Override void printValue() { System.out.println("Value: " + value); } }
Множественное наследование поведения:
Интерфейс с default methods:
Интерфейсы с методами по умолчанию могут быть полезны для реализации множественного наследования поведения. Класс может реализовывать несколько интерфейсов, каждый из которых предоставляет default method. Это позволяет избегать проблем, связанных с традиционным множественным наследованием (например, «алмазная проблема» в C++).
Если у двух интерфейсов есть методы с одинаковыми сигнатурами, класс, который реализует эти интерфейсы, должен явно указать, какую реализацию использовать или переопределить метод.
Абстрактный класс:
В Java абстрактный класс не поддерживает множественное наследование. Класс может наследоваться только от одного абстрактного класса. Это одно из основных ограничений по сравнению с интерфейсами.
Конструкторы и создание объектов:
Интерфейс с default methods:
Интерфейсы не могут иметь конструкторы и, следовательно, не могут создавать экземпляры самих себя. Конструкторы предназначены только для классов. Интерфейс не может инкапсулировать логику создания объектов или инициализацию состояния.
Абстрактный класс:
Абстрактные классы могут иметь конструкторы, которые могут вызываться подклассами при создании объекта. Это позволяет абстрактным классам передавать состояние и логику инициализации своим подклассам.
abstract class AbstractClass { int value; AbstractClass(int value) { this.value = value; } } class MyClass extends AbstractClass { MyClass(int value) { super(value); } }
Модификаторы доступа:
Интерфейс с default methods:
Методы в интерфейсе могут быть только public (по умолчанию), а также private (начиная с Java 9). Но интерфейс не может иметь методы с модификатором доступа protected или default (package-private).
Поля интерфейса всегда public, static, и final.
Абстрактный класс:
Абстрактный класс может содержать методы с любыми модификаторами доступа: public, protected, private, и default (package-private). Это даёт больше гибкости в управлении доступом к методам и состоянию.
Поля в абстрактном классе также могут иметь любые модификаторы доступа.
Когда использовать интерфейс с default methods vs абстрактный класс:
Используйте интерфейсы с default methods, если:
Вам нужно предоставить контракт, который классы должны реализовать, с возможностью включения базовой реализации.
Вам нужно, чтобы класс мог реализовывать несколько интерфейсов одновременно.
Вам не нужно хранить состояние в интерфейсе.
Используйте абстрактный класс, если:
Вам нужно хранить состояние и логику инициализации (через конструкторы).
Вам требуется общая реализация методов и возможность наследования её в подклассах.
Вам нужно больше контроля над модификаторами доступа.
➤ Какие вы знаете Java Collection
interface Collection:
базовый интерфейс для коллекций и других интерфейсов коллекций.
interface List:
упорядоченный список, в котором у каждого элемента есть индекс, дубликаты значений допускаются. Наследуется от Collection.
interface Set:
неупорядоченное множество уникальных элементов. Наследуется от Collection
interface Map:
состоит из пар «ключ-значение». Ключи уникальны, а значения могут повторяться. Порядок элементов не гарантирован. Map позволяет искать объекты (значения) по ключу. Содержит put вместо add. Содержит методы entrySet для преобразования пары ключ-значение в Set, метод keySet для преобразования ключей в Set, метод values для преобразования значений в Collection.
Не путайте интерфейс Collection и фреймворк Collections. Map не наследуется от интерфейса Collection, не наследуется не от чего, но входит в состав фреймворка Collections.
interface Queue:
очередь. В таком списке элементы можно добавлять только в хвост, а удалять — только из начала- так реализуется концепция first in, first out. Наследуется от Collection
interface Deque:
может выступать и как очередь, и как стек. Это значит, что элементы можно добавлять как в её начало, так и в конец. То же относится к удалению. Наследуется от Queue
class Stack:
наследуется от класса Vector, который в свую очередь имплементит List, который реализует простой механизм типа last in, first out.
interface Iterator:
позволяет пробегаться по коллекциям за исключением Map, cодержит методы hasNext, next, remove, также forEachRemaining
Классы:
List:
AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector
Set:
AbstractSet, ConcurrentHashMap.KeySetView, ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, JobStateReasons, LinkedHashSet, TreeSet
Map:
AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap
Queue:
AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, LinkedTransferQueue, PriorityBlockingQueue, PriorityQueue, SynchronousQueue
Deque:
ArrayDeque, ConcurrentLinkedDeque, LinkedBlockingDeque, LinkedList
Наиболее известные коллекции:
ArrayList:
список на основе массива.
LinkedList:
связанный двунаправленный список.
Vector:
это то же, что и ArrayList, но все методы синхронизированы
PriorityQueue:
сортированная компаратором очередь на основе массива, для сложных классов можно передать компаратор в конструктор
SortedSet:
сортированные элементы
NavigableSet:
позволяет извлекать элементы на основании их значений
TreeSet:
все объекты хранятся в отсортированном виде по возрастанию, работает на основе TreeMap
HashSet:
то же что и HashMap но ключем является сам объект, работает на основе HashMap
LinkedHashSet:
соблюдает порядок вставки объектов, работает на основе LinkedHashMap
HashMap:
массив ключ значение, до коллизии представляет из себя массив, после коллизии однонаправленный список, при достижении определенного порога нода из однонаправленного списка (хранит следующий элемент) трансформируется элемент красно черного дерева TreeNode
Hashtable:
синхронизированный вариант HashMap
LinkedHashMap:
HashMap который запоминает порядок добавления элементов, он сохраняется при вытаскивании с помощью entrySet или других методов. В Entry помимо ноды хранится ссылка на следующий и предыдущий элемент.
TreeMap:
сортированная мапа на основе красно черного дерева
Также нужно помнить что:
В Kotlin коллекции разделяются на изменяемые (mutable) и неизменяемые (immutable).
➤ Какие уровни сложности для операций с коллекциями
Списки (Lists):
ArrayList:
Добавление в конец: O(1) амортизированное
Добавление в середину или начало: O(n)
Удаление с конца: O(1)
Удаление с середины или начала: O(n)
Доступ по индексу: O(1)
Поиск: O(n)
LinkedList:
Добавление в начало или конец: O(1)
Добавление в середину: O(1) (при наличии ссылки на узел) или O(n) (поиск узла)
Удаление с начала или конца: O(1)
Удаление из середины: O(1) (при наличии ссылки на узел) или O(n) (поиск узла)
Доступ по индексу: O(n)
Поиск: O(n)
Множества (Sets):
HashSet:
Добавление: O(1) амортизированное
Удаление: O(1) амортизированное
Поиск: O(1) амортизированное
LinkedHashSet:
Добавление: O(1) амортизированное
Удаление: O(1) амортизированное
Поиск: O(1) амортизированное
Итерация: O(n) (сохраняет порядок вставки)
TreeSet:
Добавление: O(log n)
Удаление: O(log n)
Поиск: O(log n)
Карты (Maps):
HashMap:
Добавление: O(1) амортизированное
Удаление: O(1) амортизированное
Поиск: O(1) амортизированное
LinkedHashMap:
Добавление: O(1) амортизированное
Удаление: O(1) амортизированное
Поиск: O(1) амортизированное
Итерация: O(n) (сохраняет порядок вставки)
TreeMap:
Добавление: O(log n)
Удаление: O(log n)
Поиск: O(log n)
Очереди (Queues и Deques):
PriorityQueue:
Добавление: O(log n)
Удаление: O(log n)
Поиск: O(n)
ArrayDeque:
Добавление в начало или конец: O(1)
Удаление с начала или конца: O(1)
Поиск: O(n)
LinkedList (используется как очередь или дек):
Добавление в начало или конец: O(1)
Удаление с начала или конца: O(1)
Поиск: O(n)
Таблицы (Tables):
ConcurrentHashMap:
Добавление: O(1) амортизированное
Удаление: O(1) амортизированное
Поиск: O(1) амортизированное
➤ Что такое HashMap
HashMap использует хеш-таблицу для хранения карточки, обеспечивая быстрое время выполнения запросов get() и put(). Хеши хранятся в Bucket. Нужно переопределить equals() и hashCode(). Hash представляет собой Integer, в которое преобразовывается ключ. Сначала сравнивается хеш, если он одинаковый, то сравниваются ключи, так как возможна коллизия, когда хеши одинаковые, а ключи- нет.
DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16 DEFAULT_LOAD_FACTOR = 0.75f; MAXIMUM_CAPACITY = 1 << 30 // 1_073_741_824 или половина max int
класс для хранения данных
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; // здесь хранится следующая нода если хеш совпадает Node<K,V> next; }
Если в
transient Node<K,V>[] table;
нет элементов с переданным в метод
final V putVal( int hash, K key, V value, boolean onlyIfAbsent, boolean evict )
хешем, выполняется
tab[i] = newNode(hash, key, value, null);
а если есть- то в Node<K,V> next класса Node складывается следующий элемент при помощи
p.next = newNode(hash, key, value, null);
Если размера не хватает, выполняется
newCap = oldCap << 1 // или умножение на 2
Полученный хеш-код может быть огромным числовым значением, а исходный массив условно рассчитан только на 16 элементов. Поэтому хеш-код нужно трансформировать в значения от 0 до 15 (если размер массива 16), для этого используются дополнительные преобразования.
Java 8 после достижения определенного порога вместо связанных списков используются сбалансированные красно черные деревья TreeNodes (работает аналогично TreeMap).
Это означает, что HashMap в начале сохраняет объекты в связанном списке, но после того, как количество элементов в хэше достигает определенного порога происходит переход к сбалансированным деревьям, а затем может преобразовываться обратно в обычную ноду.
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; }
В Kotlin также можно задавать с помощью to
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
если Hash в HashMap совпадают, r элементу добавляется новая запись, ссылающееся на старую запись, для просмотра записей в этом элементе необходимо будет пройти связанный список записей. Это называется Метод цепочек
➤ Как отсортировать коллекцию, содержащую сложный класс, Comparator и Comparable
Comparable:
класс должен заимплементить интерфейс Comparable, вписать в качестве дженерика класс (implements Comparable), реализовать метод compareTo
Comparator:
Класс Comparator является функциональным интерфейсом, который используется для сравнения объектов. В языке Kotlin можно использовать лямбда-выражения для создания экземпляров класса Comparator.
Например, если у нас есть класс Person со свойствами name и age, мы можем создать Comparator для сортировки списка Person по возрасту следующим образом:
Set set = new TreeSet<String>(new Comparator<String>() { public int compare(String i1, String i2) { return i2.compareTo(i1); } }); // или Comparator<String> comparator = (o1, o2) -> o1.compareTo(o2); Set set = new TreeSet<String>(comparator); // или Collections.sort(list, comparator);
data class Person(val name: String, val age: Int) fun main() { val people = listOf( Person("Alice", 25), Person("Bob", 30), Person("Charlie", 20) ) val ageComparator = Comparator<Person> { p1, p2 -> p1.age - p2.age } val sortedByAge = people.sortedWith(ageComparator) println(sortedByAge) }
Пример сортировки для BigDecimal:
val one = BigDecimal(1) val two = BigDecimal(1.0) val setOne = TreeSet<BigDecimal>() val setTwo = HashSet<BigDecimal>() setOne.add(one) setOne.add(two) setTwo.add(one) setTwo.add(two) println(setOne.size) // Результат: 1 println(setTwo.size) // Результат: 2
Поведение BigDecimal:
Класс BigDecimal реализует интерфейс Comparable<BigDecimal>.
BigDecimal(1) и BigDecimal(1.0) имеют одно и то же числовое значение (1), но разные представления в плане точности:
BigDecimal(1) имеет точность 0 (целое число).
BigDecimal(1.0) имеет точность 1 (с дробной частью).
Эти два объекта считаются разными при использовании методов equals() и hashCode(), так как они учитывают не только значение, но и точность.
TreeSet:
TreeSet в Java/Kotlin — это множество, которое сортирует элементы на основе их естественного порядка или по заданному компаратору.
Для класса BigDecimal порядок определяется методом compareTo(), который сравнивает только числовое значение и игнорирует точность.
Поскольку числовое значение BigDecimal(1) и BigDecimal(1.0) одинаково, метод compareTo() вернёт 0, что заставит TreeSet считать эти объекты одинаковыми.
Поэтому в TreeSet будет добавлен только один элемент (либо BigDecimal(1), либо BigDecimal(1.0)).
HashSet:
HashSet использует методы equals() и hashCode() для сравнения объектов.
Как мы уже обсуждали, BigDecimal(1) и BigDecimal(1.0) считаются разными объектами, потому что они имеют одинаковое числовое значение, но разную точность.
В результате оба объекта будут добавлены в HashSet.
➤ В чем разница между ArrayList и LinkedList
ArrayList:
список на основе массива.
LinkedList:
связанный двунаправленный список.
Если часто добавляются и удаляются элементы, особенно из середины или начала листа- то лучше LinkedList, в остальных случаях- ArrayList
При создании в ArrayList DEFAULT_CAPACITY = 10 либо в конструктор можно передать количество, при достижении массив увеличивается в 1.5 раза с помощью метода grow
int newCapacity = oldCapacity + (oldCapacity >> 1)
В LinkedList информация хранится во внутреннем статическом классе Node. Имплементит Queue и его удобно использовать как очередь и соответственно есть методы очередей- peek (вытащить и не удалить), pool (вытащить и удалить)
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
Механизм итерации по LinkedList:
Когда вы начинаете итерацию по LinkedList, механизм проходит по узлам списка один за другим, начиная с головы (head) и двигаясь к хвосту (tail). Каждый узел хранит ссылки на следующий и предыдущий узлы, что позволяет передвигаться по цепочке узлов.
ListIterator предоставляет больше возможностей, таких как перемещение назад (к предыдущему узлу).
private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { next = (index == size) ? null : node(index); nextIndex = index; } public boolean hasNext() { return nextIndex < size; } public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public boolean hasPrevious() { return nextIndex > 0; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; } // ... }
Механизм итерации по ArrayList:
Итерация по ArrayList с помощью Iterator основана на прямом доступе к элементам массива по их индексу. Проход по элементам выполняется линейно, но доступ к каждому элементу имеет сложность O(1).
ListIterator предоставляет дополнительные возможности, такие как перемещение по элементам как вперёд, так и назад, изменение элементов, получение индекса текущего элемента и т.д.
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } // ... } private class ListItr extends Itr implements ListIterator<E> { ListItr(int index) { super(); cursor = index; } public boolean hasPrevious() { return cursor != 0; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor - 1; } @SuppressWarnings("unchecked") public E previous() { checkForComodification(); int i = cursor - 1; if (i < 0) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i; return (E) elementData[lastRet = i]; } public void set(E e) { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.set(lastRet, e); } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } public void add(E e) { checkForComodification(); try { int i = cursor; ArrayList.this.add(i, e); cursor = i + 1; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } }
➤ Как работает TreeMap
TreeMap в Java — это реализация интерфейса NavigableMap, которая хранит ключи в отсортированном порядке с использованием красно-чёрного дерева. Красно-чёрное дерево — это самобалансирующееся двоичное дерево поиска, которое поддерживает упорядоченность элементов и обеспечивает эффективное выполнение основных операций, таких как добавление, удаление и поиск.
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; }
Отсортированный порядок:
Все элементы (пары «ключ-значение») в TreeMap хранятся в отсортированном порядке по ключам. Ключи сортируются в зависимости от их естественного порядка (через интерфейс Comparable) или с помощью внешнего компаратора (через интерфейс Comparator), если он был передан при создании карты.
Самобалансирующееся дерево:
TreeMap использует красно-чёрное дерево для хранения данных. Это самобалансирующееся двоичное дерево поиска, которое обеспечивает следующие временные характеристики для операций:
Вставка (put): O(log n)
Удаление (remove): O(log n)
Поиск (get): O(log n)
Уникальные ключи:
Как и другие реализации интерфейса Map, TreeMap не допускает дубликатов ключей. Если вы пытаетесь вставить элемент с ключом, который уже существует в карте, то новое значение заменит старое.
Внутреннее устройство TreeMap:
Ключи и значения: TreeMap хранит пары «ключ-значение» в узлах красно-чёрного дерева. Каждый узел дерева содержит ключ, значение, ссылку на левое поддерево, ссылку на правое поддерево и цвет узла (красный или чёрный).
Интерфейсы Comparable и Comparator:
Если ключи реализуют интерфейс Comparable, то они будут сравниваться друг с другом на основе их естественного порядка (например, для чисел это порядок возрастания).
Вы также можете передать Comparator при создании TreeMap, чтобы контролировать порядок сортировки ключей. В этом случае элементы будут отсортированы по этому компаратору.
Вставка элементов (метод put):
Когда вы добавляете новую пару «ключ-значение» в TreeMap, сначала происходит сравнение ключа с другими ключами, которые уже находятся в дереве.
Алгоритм двоичного поиска использует метод compareTo() (если ключи реализуют Comparable) или переданный компаратор (Comparator) для определения, куда вставить новый ключ.
Если ключ уже существует, то значение обновляется, но ключ остаётся на месте.
Поиск элемента (метод get):
Поиск элемента по ключу выполняется с помощью двоичного поиска. Алгоритм идёт по дереву, начиная с корня, и на каждом узле решает, идти влево (если искомый ключ меньше текущего) или вправо (если ключ больше текущего).
Операция поиска в TreeMap имеет сложность O(log n).
Удаление элементов (метод remove):
Удаление элемента из TreeMap также происходит с балансировкой дерева. Если удаляемый узел имеет два потомка, дерево найдёт наименьший элемент в правом поддереве и заменит удаляемый узел им, чтобы сохранить порядок.
Операция удаления также выполняется за O(log n).
Поддержание баланса дерева:
Красно-чёрное дерево поддерживает баланс путём перекрашивания узлов и выполнения вращений (левое или правое вращение) при вставке или удалении узлов. Это гарантирует, что высота дерева всегда остаётся логарифмической от числа элементов, что позволяет эффективно выполнять операции.
Преимущества и недостатки TreeMap:
Преимущества:
Упорядоченность:
Все ключи хранятся в отсортированном порядке.
Логарифмическая сложность:
Вставка, удаление и поиск выполняются за O(log n), что делает TreeMap эффективным для хранения больших объемов данных с необходимостью поиска в отсортированном порядке.
Поддержка диапазонов:
TreeMap позволяет легко получать элементы в диапазоне ключей (например, метод subMap).
Недостатки:
Медленнее, чем HashMap:
Вставка и поиск в TreeMap выполняются медленнее, чем в HashMap, где эти операции имеют амортизированную сложность O(1).
Большие накладные расходы на память:
Красно-чёрное дерево использует дополнительные ссылки и поля для балансировки, что требует больше памяти, чем структуры на основе хешей.
➤ Какие правила нейминга в Java
Нейминг очень важен для понимания кода, не ленитесь называть классы, методы и переменные правильно в соответствии с тем, что они значат и что делают
Классы и интерфейсы:
пишутся в CamelCase с большой буквы
Методы и переменные:
пишутся в camesCase c маленькой буквы, кроме констант
Константы:
пишутся в SNAKE_CASE большими буквами
Переменная может начинаться на _ $ или букву, но не может начинаться с цифры, но может содержать цифры
Реализации интерфейсов часто именуются Имя интерфейса + Impl, например InterfaceNameImpl
Названия переменных начиная с m и интерфейсов начиная с I устарели
Правила нейминга описаны в Java Code Conventions.
Для перевода кейсов один в другой кейсов в IDEA есть удобный плагин “String Manipulation”
➤ Что такое статические классы в Java
Статическим классом может быть только внутренний класс, он можно наследовать, как и он может наследоваться от любого другого класса и имплементировать интерфейс.
Статический вложенный класс ничем не отличается от обычного внутреннего класса за исключением того, что его объект не содержит ссылку на создавший его объект внешнего класса. Для использования статических методов/переменных/класса нам не нужно создавать объект данного класса. В обычных внутренних классах без экземпляра внешнего класса мы не можем создать экземпляр внутреннего.
public class Vehicle { public static class Car { public int km; } } Vehicle.Car car = new Vehicle.Car(); car.km = 90;
➤ Какая разница между StringBuffer и StringBuilder
StringBuffer и StringBuilder служат для операций с текстовыми данными, StringBuffer синхронизированный и потокобезопасный
➤ Что такое рефлексия в Java
Рефлексия (reflection) это механизм, который позволяет программам исследовать и модифицировать свою структуру и поведение во время выполнения.
С помощью рефлексии можно получить доступ к информации о классах, интерфейсах, полях, методах и конструкторах во время выполнения программы. Это может быть полезно в тех случаях, когда вы не знаете структуру класса заранее, но хотите обработать ее динамически.
Например, с помощью рефлексии можно создавать новые объекты, вызывать методы, устанавливать значения полей и даже загружать новые классы во время выполнения программы. Однако, необходимо быть осторожным при использовании рефлексии, так как это может привести к снижению производительности и ухудшению читаемости кода.
public class Main { public static void main(String[] args) throws Exception { ClosedClass closedClassNewInstance = new ClosedClass(); Class closedClass = closedClassNewInstance.getClass(); closedClass = ClosedClass.class; closedClass = Class.forName("com.company.ClosedClass"); String className = closedClass.getName(); closedClassNewInstance = (ClosedClass) closedClass.newInstance(); Constructor[] constructors = closedClass.getConstructors(); constructors = closedClass.getDeclaredConstructors(); Method[] methods = closedClass.getMethods(); Parameter[] parameters = constructors[0].getParameters(); String name = parameters[0].getName(); String type = parameters[0].getType().getName(); Method method = closedClass.getMethod("method"); method.invoke(closedClass); Field field = closedClass.getField("variable"); field.setAccessible(true); field.setInt(closedClass, 1); } } class ClosedClass { int variable; void method(){} }
➤ В каких ситуациях возникает Memory leak
из-за статических полей, через незакрытые ресурсы, неверные реализации equals() и hashCode(), внутренние классы, которые ссылаются на внешние классы, не правильного использования lazy в Kotlin, утечки контекста и тд
Пример кода, в котором может произойти утечка памяти:
class MyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_my) val button = findViewById<Button>(R.id.my_button) var data: String = "Initial data" button.setOnClickListener { // Здесь лямбда захватывает ссылку на data // и будет продолжать её использовать даже после того, // как data была уничтожена. Toast.makeText(this, data, Toast.LENGTH_SHORT).show() } // Предположим, что мы хотим освободить data из памяти, // чтобы не произошла утечка памяти. // Мы делаем это, установив data в null. data = null // Это не удалит ссылку на data из лямбды. } }
➤ Что такое Generic (Дженерики)
С их помощью можно объявлять классы, интерфейсы и методы, где тип данных указан в виде параметра, например , в этом случае не нужно делать приведение типов (Integer). Если использовать в качестве дженерика Object в Java или Any в Kotlin, можно использовать любой тип данных, но затем необходимо делать каст до нужного с помощью скобок в Java или “as” в Kotlin.
В старом коде Java дженерики не используются.
Дженерики видны только на этапе компиляции, дальше осуществляется каст до типа указанного в дженерике.
<? extends Object>:
Также дженерики могут быть масками, например Collection или List
<T>:
Также дженериками могут быть буквы. В данном примере мы ограничили Т наследниками класса NewClass и интерфейса Comparable, соответственно у T будут те же свойства.
class NewClass<T extends NewClass & Comparable> { T variable; NewClass method(T variable) { // метод Comparable variable.compareTo(1); // метод NewClass variable.method(new NewClass()); List<T> list = new ArrayList<>(); List<NewClass> list2 = new ArrayList<>(); list.add(variable); list2.add(variable); return list.get(0); } }
В Kotlin также есть:
Source<out T>:
указывает, что тип T является ковариантным, то есть тип List является подтипом типа List. Проекция типа позволяет использовать тип T только в качестве возвращаемого типа метода или типа значения свойства, но не в качестве типа параметра метода или типа значения свойства.
interface Producer<out T> { fun produce(): T }
Source<in T>:
указывает, что тип T является контравариантным, то есть тип List является подтипом типа List. Проекция типа позволяет использовать тип T только в качестве параметра метода или типа значения свойства, но не в качестве возвращаемого типа метода или типа значения свойства.
interface Consumer<in T> { fun consume(item: T) }
<reified T>:
Обычно, во время выполнения программы, информация о типах аргументов удаляется, и вы не можете получить доступ к типам, используемым в качестве аргументов обобщенной функции. Однако, используя ключевое слово reified, вы можете получить доступ к типу аргумента во время выполнения.
inline fun <reified T> getTypeName() = T::class.simpleName fun main() { println(getTypeName<String>()) // prints "String" println(getTypeName<Int>()) // prints "Int" }
В этом примере мы определили обобщенную функцию getTypeName(), которая использует ключевое слово reified для получения имени класса типа T во время выполнения. Мы вызываем эту функцию с различными типами, и она возвращает имя класса для каждого типа.
Ключевое слово reified может использоваться только с обобщенными функциями, не с классами или интерфейсами. Кроме того, обобщенный тип, объявленный с использованием ключевого слова reified, можно использовать только в контексте, где тип явно указан, например, при вызове другой функции.
<*>:
называется Звездная проекция или Star projection
используется для указания неопределенного типа аргумента при использовании обобщенных типов.
Когда вы используете обобщенный тип в коде Kotlin, вы можете указать тип аргумента в угловых скобках. Но иногда вы можете хотеть использовать обобщенный тип, но не указывать конкретный тип аргумента, например, если вы хотите использовать тип, который может содержать объекты разных типов.
В этом случае вы можете использовать символ <*> вместо типа аргумента. Например:
val list: List<*> = listOf("foo", 42, Any())
В этом примере мы создали список list типа List<>, который может содержать объекты любого типа. Мы добавили в список три объекта разных типов: строку “foo”, число 42 и объект типа Any(). Теперь list содержит все три объекта. Символ <> также может использоваться вместо типа аргумента при создании объекта обобщенного класса, например:
val map: Map<String, *> = mapOf("foo" to 42, "bar" to "baz")
Function<*, String>: означает Function<in Nothing, String>; Function<Int, *>: означает Function<Int, out Any?>; Function<*, *>: означает Function<in Nothing, out Any?>
➤ Какие методы есть у класса Object в Java и Any в Kotlin
wait(), notify(), notifyAll():
три метода из набора для многопоточности
getClass():
получить класс объекта во время выполнения. В основном используется для рефлексии.
clone():
получить точную копию объекта. Не рекомендуется использовать. Чаще советуют использовать конструктор копирования.
equals():
сравнивает два объекта.
hashcode():
числовое представление объекта. Значение по-умолчанию — целочисленный адрес в памяти.
toString():
возвращает представление объекта в виде строки. По-умолчанию возвращает имя_класса@hashcode в 16-ричной системе.
Если hashcode не переопределен, то вернут значение по-умолчанию.
finalize():
Если объект взаимодействует с какими-то ресурсами, например открывает поток вывода и читает из него, то такой поток необходимо закрыть перед удалением объекта из памяти. Для этого в языке Java достаточно переопределить метод finalize(), который вызывается в исполняющей среде Java непосредственно перед удалением объекта данного класса. В теле метода finalize() нужно указать те действия, которые должны быть выполнены перед уничтожением объекта. Метод finalize() вызывается лишь непосредственно перед сборкой “мусора”
У класса Any в Kotlin есть методы:
equals(), hashCode(), toString()
➤ Что такое Enum в Java
Специальный класс, представляющий из себя перечисление
В простейшей реализации выглядит так
enum Size { SMALL, MEDIUM, LARGE }
его функционал можно расширить
enum Size { SMALL("small"), MEDIUM("medium"), LARGE("large") { @Override String getSize() { return size + ", this is maximum size"; } }; String size; Size(String size) { this.size = size; } String getSize() { return size; }
➤ Какой в Java приоритет при преобразовании примитивных типов
byte ➜ short ➜ int ➜ long ➜ float ➜ double ➜ Byte() ➜ Object() ➜ Byte()… ➜ byte… ➜ short… ➜ int… ➜ long… ➜ float… ➜ double… ➜ Object()…
➤ Что такое приведение типов / преобразование типов / каст
Бывает явное и неявное преобразование
Возможно автоматические расширяющее преобразование (widening):
byte -> short -> int -> long
int -> double
short -> float -> double
char -> int
Возможно преобразование с потерей информации:
int -> float
long -> float
long -> double
Явное преобразование с потерей данных:
int a = 258; byte b = (byte) a; // 2 double a = 56.9898; int b = (int) a; // 56
Преобразования при операциях:
если один из операндов операции относится к типу double, то и второй операнд преобразуется к типу double
если предыдущее условие не соблюдено, а один из операндов операции относится к типу float, то и второй операнд преобразуется к типу float
если предыдущие условия не соблюдены, один из операндов операции относится к типу long, то и второй операнд преобразуется к типу long
иначе все операнды операции преобразуются к типу int
В Kotlin есть:
as:
обычное не безопасное приведение типов
as?:
безопасное приведение типов, возвращает null в случае неудачи
is:
является этим типом
!is:
не является этим типом
➤ Для чего нужны и как создавать аннотации в Java
Аннотации в Java являются метками в коде, описывающими метаданные для функции/класса/пакета.
@interface AnnotationName { int getResult() default 1; String value(); } @AnnotationName("value") public class Main { @AnnotationName("value") String text; @AnnotationName(value = "value", getResult = 1) public static void main(String[] args) { } public static void testAnnotation(Object object) throws Exception { boolean haveAnnotation = !object.getClass().isAnnotationPresent(AnnotationName.class); Class<Main> demoClass = Main.class; AnnotatedElement annotatedElement = demoClass; Annotation[] annotations = annotatedElement.getAnnotations(); Method method = demoClass.getMethod("testAnnotation"); annotations = method.getAnnotations(); } }
Аннотации бывают по типу хранения:
SOURCE: используется только при написании кода и игнорируется компилятором
CLASS: сохраняется после компиляции, однако игнорируется JVM
RUNTIME: сохраняется после компиляции и подгружается JVM
Тип объекта над которым указывается:
ANNOTATION_TYPE: другая аннотация
CONSTRUCTOR: конструктор класса
FIELD: поле класса
LOCAL_VARIABLE: локальная переменная
METHOD: метод класса
PACKAGE: описание пакета package
PARAMETER: параметр метода public void hello(@Annontation String param){}
TYPE: указывается над классом
Java SE 1.8 стандартная библиотека языка предоставляет нам 10 аннотаций
@Override
Retention: SOURCE; Target: METHOD.
Показывает, что метод над котором она прописана, наследован у родительского класса.
@Deprecated
Retention: RUNTIME; Target: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE.
Указывает на методы, классы или переменные, которые является “устаревшими” и могут быть убраны в последующих версиях продукта.
@SuppressWarnings
Retention: SOURCE; Target: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
Отключает вывод предупреждений компилятора, которые касаются элемента над которым она указана. Является SOURCE аннотацией указываемой над полями, методами, классами.
@Retention
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Задает “тип хранения” аннотации над которой она указана. Да эта аннотация используется даже для самой себя.
@Target
Retention: RUNTIME; Target: ANNOTATION_TYPE;
Задает тип объекта над которым может указываться создаваемая нами аннотация. Да и она тоже используется для себя же.
В Java аннотации создаютья с помощью @interface, в Kotlin с помощью annotation class
➤ Что такое Wrappers (обертки) в Java
Обертки (wrappers) в Java — это классы, которые оборачивают примитивные типы данных, такие как int, char, boolean и т.д., чтобы они могли быть использованы как объекты. Эти классы находятся в пакете java.lang и имеют следующие имена:
Integer() для int
Character() для char
Boolean() для boolean
Short() для short
Long() для long
Double() для double
Float() для float
Byte() для byte
Каждый из этих классов содержит методы и поля, позволяющие работать с примитивными типами данных как с объектами. Например, класс Integer содержит методы для преобразования чисел в строки, сравнения чисел и т.д.
Обертки также используются для передачи примитивных типов данных в качестве параметров в методы, которые ожидают объекты. Например, если метод ожидает параметр типа Object, а вы хотите передать ему целое число, вы можете создать объект класса Integer, обернуть в него число, и передать этот объект в метод.
Еще одним преимуществом оберток является возможность использования null-значений. Примитивные типы не могут быть null, но обертки могут. Если вы не уверены, будет ли у вас значение для переменной или нет, вы можете использовать обертку, чтобы избежать ошибок.
В Kotlin обертки не используются, вместо этого компилятор сам решает, когда использовать примитивный типа, а когда ссылочный
➤ Какие есть операторы перехода
return:
по умолчанию производит возврат из ближайшей окружающей его функции или анонимной функции
break:
завершает выполнение цикла
continue:
продолжает выполнение цикла со следующего его шага, без обработки оставшегося кода текущей итерации
Любое выражение в Kotlin может быть помечено меткой label:
loop@ for (i in 1..100) { for (j in 1..100) { if (true) break@loop } } fun foo() { listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit print(it) } } fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@forEach print(it) } } return@a 1 // верни 1 в метке @a, а не верни выражение с меткой (@a 1)
➤ Что такое Stack (стек) и Heap (куча) в Java
Для оптимальной работы приложения JVM делит память на область стека (stack) и область кучи (heap). Стек работает по схеме LIFO (последним вошел, первым вышел). Всякий раз, когда вызывается новый метод, содержащий примитивные значения или ссылки на объекты, то на вершине стека под них выделяется блок памяти. Куча используется для объектов и классов. Новые объекты всегда создаются в куче, а ссылки на них хранятся в стеке. Примитивное поле экземпляра класса хранится в куче. Куча используется всеми частями приложения в то время как стек используется только одним потоком исполнения программы.
Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче. Объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков. Если память стека полностью занята, то Java Runtime бросает java.lang.StackOverflowError, а если память кучи заполнена, то бросается исключение java.lang.OutOfMemoryError: Java Heap Space.
Каждый поток, работающий в виртуальной машине Java, имеет свой собственный стек. Стек содержит информацию о том, какие методы вызвал поток. Как только поток выполняет свой код, стек вызовов изменяется. Стек потока содержит все локальные переменные для каждого выполняемого метода. Поток может получить доступ только к своему стеку.
Когда метод вызывается, JVM создает новый блок в стеке, называемый “frame” или “stack frame”. В этом блоке хранятся локальные переменные метода, а также ссылки на объекты, переданные методу в качестве аргументов.
Каждый новый вызов метода добавляет новый frame в стек, а завершение метода удаляет последний frame из стека. Этот механизм называется “stack machine”.
Кроме того, стек используется для хранения информации о возврате из методов. Когда метод вызывается, адрес возврата (return address) помещается в стек, чтобы JVM знала, куда возвращаться после завершения метода.
Стек ограничен по размеру и может вызвать ошибку переполнения стека (stack overflow), если в стеке создается слишком много frame’ов. Это может произойти, например, если метод рекурсивно вызывает сам себя слишком много раз. В стандартной конфигурации JVM размер стека может составлять от нескольких мегабайт до нескольких десятков мегабайт в зависимости от версии JVM и операционной системы. Однако размер стека можно изменить при запуске JVM с помощью параметра -Xss. Например, чтобы установить размер стека в 1 мегабайт, необходимо запустить JVM с параметром -Xss1m.
В стандартной конфигурации JVM размер кучи может составлять от нескольких сотен мегабайт до нескольких гигабайт в зависимости от версии JVM и операционной системы. Однако размер кучи можно изменить при запуске JVM с помощью параметров -Xms и -Xmx. Параметр -Xms устанавливает начальный размер кучи, а параметр -Xmx устанавливает максимальный размер кучи. Например, чтобы установить начальный размер кучи в 256 мегабайт и максимальный размер кучи в 1 гигабайт, необходимо запустить JVM с параметрами -Xms256m -Xmx1g.
Локальные переменные, невидимы для всех других потоков, кроме потока, который их создал. Даже если два потока выполняют один и тот же код, они всё равно будут создавать локальные переменные этого кода в своих собственных стеках. Таким образом, каждый поток имеет свою версию каждой локальной переменной.
Все локальные переменные примитивных типов (boolean, byte, short, char, int, long, float, double) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.
Куча содержит все объекты, созданные в вашем приложении, независимо от того, какой поток создал объект. К этому относятся и версии объектов примитивных типов (например, Byte, Integer, Long и т.д.). Неважно, был ли объект создан и присвоен локальной переменной или создан как переменная-член другого объекта, он хранится в куче.
Локальная переменная может быть примитивного типа, в этом случае она полностью хранится в стеке потока.
Локальная переменная также может быть ссылкой на объект. В этом случае ссылка (локальная переменная) хранится в стеке потоков, но сам объект хранится в куче.
Объект может содержать методы, и эти методы могут содержать локальные переменные. Эти локальные переменные также хранятся в стеке потоков, даже если объект, которому принадлежит метод, хранится в куче.
Переменные-члены объекта хранятся в куче вместе с самим объектом. Это верно как в случае, когда переменная-член имеет примитивный тип, так и в том случае, если она является ссылкой на объект.
Статические переменные класса также хранятся в куче вместе с определением класса.
К объектам в куче могут обращаться все потоки, имеющие ссылку на объект. Когда поток имеет доступ к объекту, он также может получить доступ к переменным-членам этого объекта. Если два потока вызывают метод для одного и того же объекта одновременно, они оба будут иметь доступ к переменным-членам объекта, но каждый поток будет иметь свою собственную копию локальных переменных.
public static void main(String[] args) { int x = 10; // переменная типа int сохраняется в стеке String str = "Hello World!"; // объект типа String сохраняется в куче System.out.println(str); }
В этом примере переменная x типа int сохраняется в стеке, так как это примитивный тип данных. Переменная str типа String является объектом и сохраняется в куче.
При выполнении программы метод main помещается в стек, создается переменная x и ей присваивается значение 10. Затем создается объект String, содержащий строку “Hello World!”. Ссылка на этот объект сохраняется в переменной str. При вызове метода println значение ссылки на объект str передается в метод и он выводит строку “Hello World!” на консоль.
После выполнения метода main все переменные удаляются из стека, а объект String продолжает существовать в куче, пока на него есть ссылки из других частей программы.
Кроме стека и кучи, в Java существует область хранения постоянных данных (PermGen или Metaspace в зависимости от версии Java). В эту область помещаются метаданные классов, информация о методах, переменных и прочие данные, связанные с самой структурой программы. В Java 8 и выше, PermGen заменена на Metaspace.
Код программы компилируется в байт-код, который сохраняется в файле .class. При запуске программы, байт-код загружается в память и интерпретируется виртуальной машиной Java (JVM).
➤ Что такое JDK, JRE и JVM
JDK (Java Development Kit):
это набор инструментов, который разработчики используют для создания Java-приложений. JDK включает в себя компилятор Java, библиотеки классов Java, утилиты для разработки и отладки Java-приложений и другие инструменты. JDK является полной установкой для разработки Java-приложений.
JRE (Java Runtime Environment):
это окружение выполнения Java, которое используется для запуска Java-приложений. JRE включает в себя виртуальную машину Java (JVM), библиотеки классов Java и другие компоненты, необходимые для выполнения Java-приложений. JRE не содержит инструментов для разработки Java-приложений.
JVM (Java Virtual Machine):
это виртуальная машина, которая обеспечивает выполнение Java-кода на компьютере. JVM переводит байт-код Java в машинный код, который может выполняться на конкретной платформе. JVM является ключевым компонентом Java-платформы, так как он обеспечивает возможность написания Java-кода один раз и его запуска на любой платформе, где установлена JVM.
Каждый компонент является важной частью Java-платформы, и они взаимодействуют между собой. Разработчики используют JDK для создания Java-приложений, а затем JRE используется для запуска этих приложений, используя JVM для выполнения Java-кода.
➤ Какими значениями инициализируются переменные по умолчанию
Значение по умолчанию зависит от типа переменной:
Для числовых типов (byte, short, int, long, float, double) значение по умолчанию равно 0.
Для типа char значение по умолчанию равно ‘\u0000’ (символ ‘NUL’).
Для типа boolean значение по умолчанию равно false.
Для ссылочных типов (любой класс, интерфейс, массив) значение по умолчанию равно null.
➤ Возможно ли сузить уровень доступа/тип возвращаемого значения при переопределении метода
нет, т.к. это приведёт к нарушению принципа подстановки Барбары Лисков. Расширение уровня доступа возможно.
➤ Какие существуют типы ссылок в Java
Strong Reference:
это ссылка на объект, которая гарантирует, что объект не будет удален из памяти, пока на него есть хотя бы одна такая ссылка. Когда объект находится под управлением strong reference, его жизненный цикл определяется жизненным циклом ссылки. Если ссылка не используется, то объект также не удаляется из памяти, что может привести к утечке памяти.
String title = “hello”;
SoftReference:
объекты, созданные через SoftReference, будут собраны в случае, если JVM требует память. То есть имеется гарантия, что все soft reference объекты будут собраны перед тем, как JVM выбросит OutOfMemoryError. SoftReference часто используется для кешей, потребляющих большое количество памяти.
class MyClass { private var softRef: SoftReference<HeavyObject>? = null fun doSomething() { var heavyObj = softRef?.get() if (heavyObj == null) { heavyObj = HeavyObject() softRef = SoftReference(heavyObj) } // use heavyObj here } } class HeavyObject { // ... }
В этом примере SoftReference используется для хранения ссылки на объект HeavyObject, который может занимать большой объем памяти. В методе doSomething() проверяется, существует ли уже SoftReference и сохраняется ли объект HeavyObject. Если объект еще не был создан или уже был удален из памяти, создается новый объект и сохраняется ссылка на него с помощью SoftReference. В противном случае, если объект HeavyObject уже существует в памяти и есть ссылка на него через SoftReference, то ссылка на него возвращается для дальнейшего использования.
Важно отметить, что SoftReference не гарантирует, что объект всегда будет доступен в памяти. Если система заполнит память и не будет достаточно места для хранения HeavyObject, то объект может быть удален, и ссылка через SoftReference будет автоматически очищена. При следующей попытке доступа к объекту создастся новый объект и сохранится ссылка на него в SoftReference.
WeakReference:
слабей SoftReference, не спасает объект от финализации, даже если имеется достаточное количество свободной памяти. Как только на объект не останется strong и soft ссылок, он может быть финализирован. Используется для кешей и для создания цепей связанных между собой объектов.
private var activityNavController: WeakReference<NavController>? = null // или var person: Person? = Person("John") val weakReference = WeakReference(person) println("Before garbage collection: $weakReference") person = null System.gc() println("After garbage collection: $weakReference")
В этом примере создается экземпляр класса Person и затем создается слабая ссылка на него с помощью WeakReference. После этого ссылка на оригинальный объект удаляется, и вызывается сборка мусора с помощью метода System.gc(). В конце выведется информация о ссылке на объект Person до и после сборки мусора.
Обратите внимание, что после сборки мусора ссылка на объект Person становится null, поскольку оригинальный объект был удален из памяти. Ссылка на объект через WeakReference также становится null, что означает, что объект был удален.
PhantomReference:
объекты, созданные через PhantomReference, уничтожаются тогда, когда GC определяют, что ссылаемые объекты могут быть освобождены. Этот тип ссылок используется как альтернатива финализации для более гибкого освобождения ресурсов.
class MyObject { // ресурсы, связанные с объектом } val phantomReferenceQueue = ReferenceQueue<MyObject>() val myObject = MyObject() val phantomReference = PhantomReference(myObject, phantomReferenceQueue) // выполняем какие-то действия // проверяем, удалился ли объект из памяти if (phantomReferenceQueue.poll() != null) { // освобождаем ресурсы, связанные с объектом }
Здесь мы создали объект MyObject и поместили его в PhantomReference. Затем мы выполняем какие-то действия, и, если объект был удален из памяти, освобождаем ресурсы, связанные с ним. Обратите внимание, что мы создали ReferenceQueue, чтобы отслеживать удаление объекта.
➤ В чем разница между map и flatMap
map:
это функция, которая принимает коллекцию и функцию, которая применяется к каждому элементу коллекции. В результате создается новая коллекция с тем же количеством элементов, что и в исходной коллекции, но с измененными значениями.
val numbers = listOf(1, 2, 3, 4) val doubledNumbers = numbers.map { it * 2 } println(doubledNumbers) // [2, 4, 6, 8]
flatMap:
это функция, которая принимает коллекцию и функцию, которая возвращает коллекцию. Затем flatMap объединяет все полученные коллекции в одну коллекцию.
val numbers = listOf(1, 2, 3) val nestedNumbers = numbers.flatMap { listOf(it, it * 2) } println(nestedNumbers) // [1, 2, 2, 4, 3, 6]
Разница между map и flatMap заключается в том, что map возвращает коллекцию элементов, полученных после применения функции к каждому элементу исходной коллекции, а flatMap возвращает коллекцию элементов, полученных после применения функции, которая возвращает коллекцию, к каждому элементу исходной коллекции. Кроме того, flatMap может быть полезен, когда нужно «развернуть» вложенные коллекции в одну большую коллекцию.
Применение в Java Stream API:
map и flatMap часто используются в контексте Stream API, который был введен в Java 8. Map преобразует элементы коллекции, не изменяя уровень вложенности, тогда как flatMap объединяет несколько потоков в один плоский поток.
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class MapExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4); List<Integer> doubledNumbers = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList()); System.out.println(doubledNumbers); // [2, 4, 6, 8] } }
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class FlatMapExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3); List<Integer> flatMappedNumbers = numbers.stream() .flatMap(n -> Arrays.stream(new Integer[]{n, n * 2})) .collect(Collectors.toList()); System.out.println(flatMappedNumbers); // [1, 2, 2, 4, 3, 6] } }
Применение в Kotlin Flow:
Когда мы говорим о Kotlin Flow, map и flatMap имеют похожие концепции, но применяются к потокам данных.
➤ Какие есть типы ошибок в Java
Все ошибки наследуются от класса Throwable
Exception:
ошибки при выполнении программы
Error:
системные ошибки при работе JVM, не нужно отлавливать за редким исключением, так как это серьезная ошибка и она приведет к падению программы
Exception бывают:
Checked:
их обязательно отлавливать и проверяет компилятор, причина возникновения- потенциальные ошибки в методах
примеры: Throwable, Exception, IOException, ReflectiveOperationException
Unchecked:
их не обязательно отлавливать и не проверяет компилятор, причина возникновения- ошибки в коде
примеры: RuntimeException, IndexOutOfBoundsException
➤ Как работает Garbage Collector и как организована память в JVM
Сборщик мусора, бывают (в HotSpot Oracle JVM):
Serial Garbage Collection:
последовательная сборка молодого и старого поколения.
Parallel Garbage Collection:
сборщик по умолчанию в Java 8, работает также как и Serial GC, но с использованием многопоточности
CMS Garbage Collection:
Concurrent Mark-and-Sweep. Делает две краткие паузы с полной остановкой всех потоков, эти паузы в сумме меньше, чем общий цикл фоновой сборки. По возможности, осуществляет сборку мусора в фоновом режиме. Первая пауза называется initial mark, в это время анализируется stack, после чего в фоновом режиме происходит обход heap-а, начинается mark фаза. После этого надо снова остановить приложение и провести remark — удостовериться, что пока мы делали в фоновом режиме ничего не изменилось. И только после этого просиходит sweep в фоновом режиме — очистка уже ненужных участков.
G1 Garbage Collection:
сборщик по умолчанию начиная с Java 9, идея, лежащая в основе G1, называется pause goal “желательная пауза”. Этот параметр показывает, на какое время программа может прервать работу во время исполнения ради сборки мусора, например, на 20 мс один раз в 5 минут. Сборщик мусора не гарантирует, что будет работать именно так, но будет стараться работать с заданными желательными паузами. Это коренным образом отличает его от всех сборщиков мусора, с которыми мы сталкивались раньше. Разработчик может гораздо более гибко контролировать процесс сборки мусора. G1 делит heap на области (регионы) равного размера, например, по 1 мегабайту. Далее динамически выбирается набор таких регионов, которые называются молодым поколением, при этом сохраняется понятие Eden и Survivor. Но выбор происходит динамически.
Области памяти:
Eden:
область динамической памяти, в которой изначально создаются объекты. Многие объекты никогда не покидают этой области памяти, так как быстро становятся мусором.
Когда мы пишем что-то в виде new Object() мы создаем объект именно в Eden.
Относится к young generation.
Survivor:
в памяти присутствует две области уцелевших. Или же можно считать, что область уцелевших обычно делится пополам. Именно в нее попадают объекты, пережившие “изгнание из Эдема” (отсюда и ее название). Иногда два этих пространства называются From Space и To Space. Одна из этих областей всегда пустует, если только не происходит процесс сбора.
Из From Space объекты либо удаляются GC, либо перекочевывают в To Space — последнее место перед тем, как стать совсем старыми и перейти в Tenured.
Относится к young generation.
Tenured:
область, где оказываются уцелевшие объекты, которые признаются “достаточно старыми” (таким образом, они покидают область Survivor).
Хранилище не очищается в ходе молодой сборки.
Обычно по умолчанию сюда помещаются объекты, пережившие 8 сборок мусора.
Относится к old generation.
Постоянное поколение памяти Metaspace и PermGen:
До Java 8 существовал специальный раздел: PermGen, здесь выделялось место для внутренних структур, например для определений классов. PermGen не входило в состав динамической памяти, обычные объекты сюда никогда не попадали.
Тут хранились метаданные, классы, интернированные строки, и т.д — это была специальная область памяти у JVM.
Так как достаточно трудно понять необходимый размер этой области, до Java 8 можно было часто наблюдать ошибку java.lang.OutOfMemoryError. Происходило это потому, что эта область переполнялась, если только вы не выставили достаточное количество памяти для нее, а определить достаточно памяти или нет можно было только научным методом “тыка”.
Поэтому, начиная с Java 8, было принято вообще убрать эту область и вся информация, которая там хранилась либо переносится в heap, например интернированные строки, либо выносится в область metaspace, в native memory. Максимальный Metaspace по умолчанию не ограничен ничем кроме предела объёма нативной памяти. Но его можно по желанию ограничить параметром MaxMetaspaceSize, аналогичным по сути к MaxPermSize у предка PermGen. Metaspace не очищается GC и в случае необходимости его можно чистить вручную.
String Pool:
область памяти, где хранятся строки, ее смысл в том, что строки могут повторяться, а этом случае записывается одна строка с разными ссылками на нее.
String text = "text"; // создастся в String Pool String text = new String("text"); // создастся вне String Pool в Heap String text = new String("text").intern(); // создастся в String Pool за счет метода intern
Cуществует несколько типов сборок мусора:
minor, major и full.
minor сборка мусора:
очищается Eden и Survivor (young generation)
major сборка мусора:
очищается old generation
full сборка мусора:
очищается все
Записывать и читать данные в native memory можно при помощи ByteBuffer и Unsafe.
➤ Что такое Java NIO (New Input/Output)
Java NIO (New Input/Output) — это набор API, включенный в Java начиная с версии 1.4, который предоставляет более эффективные и масштабируемые методы работы с вводом/выводом по сравнению с классическим IO (Input/Output) API. NIO API предназначен для разработки высокопроизводительных приложений, которые работают с большими объемами данных или обрабатывают множество соединений одновременно.
Основные особенности Java NIO:
Каналы (Channels):
Каналы представляют собой абстракцию над потоками данных. Они могут быть асинхронными и неблокирующими.
Примеры: FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel.
Буферы (Buffers):
Буферы используются для хранения данных, читаемых из канала или записываемых в канал.
Примеры: ByteBuffer, CharBuffer, IntBuffer, FloatBuffer.
Селекторы (Selectors):
Селекторы позволяют одному потоку управлять несколькими каналами. Это полезно для разработки серверов, которые обрабатывают множество соединений одновременно.
Примеры: Selector.
Неблокирующий режим (Non-blocking mode):
Каналы могут работать в неблокирующем режиме, что позволяет не блокировать поток при выполнении операций чтения или записи.
➤ Какой порядок выполнения Java класса
инициализация статических переменных суперкласса (при первом обращении к классу)
статический инициализатор суперкласса (при первом обращении к классу)
инициализация статических переменных (при первом обращении к классу)
статический инициализатор (при первом обращении к классу)
конструктор суперкласса (если нет суперкласса, то конструктор класса Object,
если в конструкторе суперкласса есть параметры, их нужно передать во время вызова конструктора вызываемого класса с помощью
super(параметры) )
обычный инициализатор
инициализация обычных переменных
конструктор класса
➤ Может ли абстрактный класс наследоваться от обычного
Да, также может перезаписывать методы, делая их абстрактными, интерфейс не может
class One{ void one(){} } abstract class Two extends One { @Override abstract void one(); } class Three extends Two { @Override void one() {} }
➤ Какие бывают циклы в Java и Kotlin
Java:
for (int i = 0; i < 10; i++){ } for ( ; ; ) { } for (String text : array) { } do { } while (true) while (true){ } list.stream().forEach((k) -> { }) list.stream().forEach(System.out::println);
Kotlin:
for (value in array){ } for (x in 1..5){ } for (x in 9 downTo 0 step 3) { } list.forEach { print(it) } val items = setOf("one", "two", "three") when { "one" in items -> println("one") "two" in items -> println("two") } // проверка элемента в коллекции выведет one
➤ Что такое ClassLoader
В Java существует три стандартных загрузчика, каждый из которых осуществляет загрузку класса из определенного места:
Bootstrap:
базовый загрузчик, также называется Primordial ClassLoader. Загружает стандартные классы JDK из архива rt.jar
Extension ClassLoader:
загрузчик расширений. Загружает классы расширений, которые по умолчанию находятся в каталоге jre/lib/ext, но могут быть заданы системным свойством java.ext.dirs
System ClassLoader:
системный загрузчик. Загружает классы приложения, определенные в переменной среды окружения CLASSPATH
Каждый загрузчик, за исключением базового, является потомком абстрактного класса java.lang.ClassLoader. Например, реализацией загрузчика расширений является класс sun.misc.Launcher$ExtClassLoader, а системного загрузчика — sun.misc.Launcher$AppClassLoader. Базовый загрузчик является нативным и его реализация включена в JVM.
Также можно реализовать свой ClassLoader, для этого нужно заекстендить ClassLoader и переопределить его методы
public abstract class ClassLoader { public Class<?> loadClass(String name); protected Class<?> loadClass(String name, boolean resolve); protected final Class<?> findLoadedClass(String name); public final ClassLoader getParent(); protected Class<?> findClass(String name); protected final void resolveClass(Class<?> c); }
При запуске класса он проходит верификацию, чтобы в байткоде при помощи hex редактора нельзя было поменять значения, сломав этим логику.
Если класс не проходит верификацию, выкидывается ошибка java.lang.VerifyError
➤ Что такое SecurityManager
Класс java.lang.SecurityManager позволяет приложениям реализовывать политику безопасности. Без SecurityManager права не ограничены.
SecurityManager позволяет приложению определить перед выполнением, возможно, небезопасной или чувствительной операции, что это за операция и предпринимается ли она в контексте безопасности, который позволяет выполнить операцию. Приложение может разрешить или запретить операцию.
У SecurityManager есть много методов check… для проверки.
Например: checkAccept генерирует исключение SecurityException, если вызывающему потоку не разрешено принимать сокет-соединение от указанного хоста и номера порта. Обычно пермишены находятся в файле java.policy или default.policy
SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkAccept (String host, int port); }
Можно создавать свой файл с пермишенами, а затем либо в vm options прописать -Djava.security.policy=src/…/my.policy либо
System.setProperty("java.security.policy", "src/…/my.policy"); System.setSecurityManager(new SecurityManager());
➤ Что такое JavaCompiler
Интерфейс, который позволяет компилировать код из программы
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); int resultCode = javaCompiler.run(null, null, null ,”path/test.java”);
➤ Как запустить JavaScript из Java
Перед запуском проверить, установлен ли движок JavaScript
ScriptEngine engine = new ScriptEngineManager(null).getEngineByName("JavaScript"); // или engine = new ScriptEngineManager(null).getEngineByName("js"); // или engine = new ScriptEngineManager(null).getEngineByName("nashorn"); String code = "var q = 0; q + 1"; Object o = engine.eval(code); System.out.println(o);
➤ Что такое Optional в Java и Kotlin
Optional в Java это концепция, которая была добавлена в Java 8 для обработки возможных null значений.
Optional<String> text = Optional.empty(); text = Optional.of("123"); if(text.isEmpty()) { System.out.println(text.get()); }
В Kotlin, вместо Optional, используется конструкция “Nullable”.
В Kotlin, Nullable используется для обозначения переменных, которые могут содержать null значения. Операции с Nullable переменными должны использовать операторы безопасной навигации (safe call operator — ?. и elvis operator — ?:) для избежания исключений при попытке доступа к null значению.
Optional в Kotlin имеет также специальный тип данных, называемый “Optional” или “Maybe”, который можно использовать в некоторых ситуациях, когда вы хотите явно обозначить, что переменная может содержать null или не-null значение. Тип Optional создается с использованием ключевого слова “Nullable” в качестве модификатора
override fun getData(): Optional<Data> { val file = getFile() return if (!file.exists() || !file.isFile) { Optional.empty() } else { Optional.of(file) } }
➤ Какие есть побитовые (поразрядные) операции в Java и Kotlin
В Java:
Для записи чисел со знаком в Java применяется дополнительный код (two’s complement), при котором старший разряд является знаковым. Если его значение равно 0, то число положительное, и его двоичное представление не отличается от представления беззнакового числа. Например, 0000 0001 в десятичной системе 1. Если старший разряд равен 1, то мы имеем дело с отрицательным числом. Например, 1111 1111 в десятичной системе представляет -1. Соответственно, 1111 0011 представляет -13.
byte b = 7; // 0000 0111 short s = 7; // 0000 0000 0000 0111
&:
логическое умножение
int a1 = 2; //010 int b1 = 5; //101 System.out.println(a1 & b1); // результат 0 int a2 = 4; //100 int b2 = 5; //101 System.out.println(a2 & b2); // результат 4
|:
логическое сложение
int a1 = 2; //010 int b1 = 5; //101 System.out.println(a1 | b1); // результат 7–111 int a2 = 4; //100 int b2 = 5; //101 System.out.println(a2 | b2); // результат 5–101
^:
логическое исключающее ИЛИ
~:
логическое отрицание
a<<b:
сдвигает число a влево на b разрядов. Например, выражение 4<<1 сдвигает число 4 (которое в двоичном представлении 100) на один разряд влево, в результате получается число 1000 или число 8 в десятичном представлении.
a>>b:
смещает число a вправо на b разрядов. Например, 16>>1 сдвигает число 16 (которое в двоичной системе 10000) на один разряд вправо, то есть в итоге получается 1000 или число 8 в десятичном представлении.
a>>>b:
в отличие от предыдущих типов сдвигов данная операция представляет беззнаковый сдвиг — сдвигает число a вправо на b разрядов. Например, выражение -8>>>2 будет равно 1073741822.
В Kotlin:
shl(bits): сдвиг влево с учётом знака (<< в Java) shr(bits): сдвиг вправо с учётом знака (>> в Java)
ushr(bits): сдвиг вправо без учёта знака (>>> в Java)
and(bits): побитовое И
or(bits): побитовое ИЛИ
xor(bits): побитовое исключающее ИЛИ
inv(): побитовое отрицание
➤ Что такое ByteBuffer и Unsafe
Класс, с помощью которого можно записывать или читать данные из нативной памяти (не heap, более быстрая). Эта память не очищается Garbage Collector и нужно очищать самому.
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // размер в байтах byteBuffer.put(new byte[]{1, 2, 3}); // запись в память byteBuffer.flip(); // удалить все не заполненные (все что после 1 2 3) byteBuffer.position(0); // переместить курсор на начальную позицию byte[] bytes = new byte[byteBuffer.remaining()]; // размер == 3 после flip или == 1024 без него byteBuffer.duplicate().get(bytes); // получить содержимое byteBuffer.clear(); // не забыть очистить // теперь bytes == 1 2 3
Unsafe позволяет работать с памятью напрямую
public class Main { public static void main(String[] args) throws Exception { Field field = Unsafe.class.getDeclaredField("theUnsafe"); // получаем с помощью рефлексии филд field.setAccessible(true); // разрешаем доступ Unsafe unsafe = (Unsafe) field.get(null); // получаем поле и кастим до класса // попробуем записать int long startAddressInDDR = unsafe.allocateMemory(1024L); // выделяем память в ddr в байтах unsafe.putInt(startAddressInDDR, 123); System.out.println(unsafe.getInt(startAddressInDDR)); unsafe.freeMemory(startAddressInDDR); // очищаем адрес // далее новый экземпляр класса)) Example example = (Example) unsafe.allocateInstance(Example.class); System.out.println(example.name); // null хотя у класса есть конструктор, куда нужно передать имя, и имя инициализировано example.name = "name"; System.out.println(example.name); // name } } class Example{ Example(String name){ this.name = name; } public String name = "1"; } // Kotlin fun main(args: Array<String>) { val field: Field = Unsafe::class.java.getDeclaredField("theUnsafe") field.isAccessible = true val unsafe: Unsafe = field.get(null) as Unsafe val example: Example = unsafe.allocateInstance(Example::class.java) as Example val name: String = example.name // по синтаксису здесь не может быть null println(name) // но здесь null example.name = "name" println(example.name) // name } data class Example(var name: String = "1")
➤ Что такое лямбда (lambda) в Java
Представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы.
Лямбда-выражение не выполняется само по себе, а образует реализацию метода, определенного в интерфейсе.
Интерфейс должен содержать только один метод без реализации.
interface Interface { String method(String parameter); } public static void main(String[] args) { // выполняет в анонимном классе метод "method", который принимает на вход параметр, печатает его и возвращает другой параметр Interface one = (String parameter) -> { System.out.print(parameter); return "2"; }; System.out.print(one.method("1 ")); } // 1 2
➤ Как в Java загрузить исполняемый код из сети
Для загрузки исполняемого кода используется механизм динамической загрузки классов. Для этого можно использовать класс java.net.URLClassLoader, который позволяет загружать классы из различных источников, включая файлы в локальной файловой системе и удаленные файлы по URL-адресу.
import java.net.URL; import java.net.URLClassLoader; public class RemoteClassLoader { public static void main(String[] args) throws Exception { URL url = new URL("http://example.com/classes/MyClass.class"); URLClassLoader classLoader = new URLClassLoader(new URL[] { url }); // Загрузка класса из удаленного источника Class<?> clazz = classLoader.loadClass("MyClass"); // Создание экземпляра класса Object obj = clazz.newInstance(); // Вызов метода clazz.getMethod("someMethod").invoke(obj); } }
Здесь мы создаем экземпляр URLClassLoader с массивом URL-адресов, содержащих путь к загружаемому классу. Затем мы загружаем класс методом loadClass() и создаем экземпляр класса методом newInstance(). Далее можно вызывать методы загруженного класса, используя рефлексию.
➤ Что такое AsynchronousServerSocketChannel
класс из пакета java.nio.channels в Java NIO, который предоставляет асинхронный способ работы с серверными сокетами. Этот класс позволяет создавать серверные сокеты, которые могут принимать соединения от клиентов без блокирования выполнения потока.
Основные особенности AsynchronousServerSocketChannel:
Асинхронность:
Методы этого класса не блокируют поток выполнения, позволяя эффективно управлять большим числом соединений.
Функциональные колбэки:
Используются для обработки завершения операций асинхронно.
Поддержка каналов:
Интеграция с другими каналами и буферами из Java NIO.
Пример использования AsynchronousServerSocketChannel
Рассмотрим пример создания простого асинхронного сервера, который принимает соединения и читает данные от клиентов.
import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.AsynchronousServerSocketChannel import java.nio.channels.AsynchronousSocketChannel import java.nio.channels.CompletionHandler import java.nio.charset.StandardCharsets import java.util.concurrent.Future import java.util.concurrent.TimeUnit fun main() { val serverAddress = InetSocketAddress("localhost", 8080) val serverChannel = AsynchronousServerSocketChannel.open().bind(serverAddress) println("Server listening on port 8080") serverChannel.accept(null, object : CompletionHandler<AsynchronousSocketChannel, Void?> { override fun completed(clientChannel: AsynchronousSocketChannel, attachment: Void?) { // Принимаем следующее соединение serverChannel.accept(null, this) // Обработка текущего соединения handleClient(clientChannel) } override fun failed(exc: Throwable, attachment: Void?) { println("Failed to accept a connection") exc.printStackTrace() } }) // Блокируем основной поток, чтобы сервер продолжал работать Thread.currentThread().join() } fun handleClient(clientChannel: AsynchronousSocketChannel) { val buffer = ByteBuffer.allocate(1024) clientChannel.read(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int, attachment: ByteBuffer) { attachment.flip() val message = StandardCharsets.UTF_8.decode(attachment).toString() println("Received message: $message") attachment.clear() // Отправка ответа клиенту val response = ByteBuffer.wrap("Message received".toByteArray(StandardCharsets.UTF_8)) clientChannel.write(response, response, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int, buffer: ByteBuffer) { if (buffer.hasRemaining()) { clientChannel.write(buffer, buffer, this) } else { buffer.clear() clientChannel.read(buffer, buffer, this) } } override fun failed(exc: Throwable, buffer: ByteBuffer) { println("Failed to write to client") exc.printStackTrace() try { clientChannel.close() } catch (e: Exception) { e.printStackTrace() } } }) } override fun failed(exc: Throwable, attachment: ByteBuffer) { println("Failed to read from client") exc.printStackTrace() try { clientChannel.close() } catch (e: Exception) { e.printStackTrace() } } }) }
Обработка клиента:
В методе handleClient происходит чтение данных от клиента и запись ответа.
clientChannel.read(buffer, buffer, object : CompletionHandler { … }) начинает асинхронное чтение данных.
completed вызывается, когда данные успешно прочитаны.
failed вызывается, если чтение не удалось.
Отправка ответа клиенту:
После чтения данных сервер отправляет ответ клиенту с помощью clientChannel.write(response, response, object : CompletionHandler { … }).
➤ Что такое функциональные интерфейсы и аннотация @FunctionalInterface в Java
Аннотация @FunctionalInterface в Java применяется к интерфейсам и служит для указания того, что интерфейс является функциональным (может иметь только один абстрактный метод).
Это значит, что интерфейс предназначен для реализации с помощью лямбда-выражений или ссылок на методы.
Зачем нужна аннотация?
Явное указание:
Документирует, что интерфейс является функциональным.
Проверка компилятором:
Если случайно добавить второй абстрактный метод, компилятор выдаст ошибку.
Правила функционального интерфейса:
Должен содержать ровно один абстрактный метод.
Может содержать любое количество методов по умолчанию (default) или статических методов (static).
Может переопределять методы из Object (например, equals()).
Пример функционального интерфейса:
@FunctionalInterface interface MyInterface { // единственный абстрактный метод void doSomething(); // методы по умолчанию разрешены default void defaultMethod() { System.out.println("Default method"); } // статические методы также разрешены static void staticMethod() { System.out.println("Static method"); } } public class Main { public static void main(String[] args) { // реализация функционального интерфейса с помощью лямбда-выражения MyInterface action = () -> System.out.println("Doing something..."); action.doSomething(); // Doing something... action.defaultMethod(); // Default method MyInterface.staticMethod(); // Static method } }
Такой же код на Kotlin
fun interface MyInterface { // Единственный абстрактный метод fun doSomething() // Методы по умолчанию разрешены fun defaultMethod() { println("Default method") } // Статические методы в Kotlin обычно выносятся в companion object companion object { fun staticMethod() { println("Static method") } } } fun main() { // Использование интерфейса через лямбду val action = MyInterface { println("Doing something...") } action.doSomething() // Doing something... action.defaultMethod() // Default method MyInterface.staticMethod() // Static method }
Что произойдет, если нарушить правило:
Если вы случайно добавите второй абстрактный метод, получите ошибку на этапе компиляции:
«Multiple non-overriding abstract methods found in interface BrokenInterface»
➤ Какие есть встроенные функциональные интерфейсы в Java
Function<T, R>:
Принимает один аргумент и возвращает результат
Function<String, Integer> length = String::length; int len = length.apply("Hello"); // 5
Predicate<T>:
Принимает аргумент и возвращает boolean
Predicate<Integer> isPositive = n -> n > 0; boolean result = isPositive.test(10); // true
Consumer<T>:
Принимает аргумент и ничего не возвращает
Consumer<String> print = System.out::println; print.accept("Hello!"); // Hello!
Supplier<T>:
Ничего не принимает, возвращает значение
Supplier<Double> random = Math::random; double value = random.get(); // случайное число
UnaryOperator<T>:
Принимает один аргумент типа T и возвращает результат такого же типа
UnaryOperator<Integer> square = x -> x * x; int result = square.apply(4); // 16
BinaryOperator<T>:
Принимает два аргумента типа T, возвращает результат такого же типа
BinaryOperator<Integer> sum = Integer::sum; int result = sum.apply(2, 3); // 5
BiFunction<T, U, R>:
Принимает два аргумента разных типов и возвращает результат
BiFunction<String, String, Integer> totalLength = (a, b) -> a.length() + b.length(); int length = totalLength.apply("Hello", "Java"); // 9
BiConsumer<T, U>:
Принимает два аргумента, ничего не возвращает
BiConsumer<String, Integer> printNameAge = (name, age) -> System.out.println(name + " is " + age + " years old."); printNameAge.accept("Alice", 30); // Alice is 30 years old.
BiPredicate<T, U>:
Принимает два аргумента и возвращает логическое значение
BiPredicate<String, String> equalsIgnoreCase = String::equalsIgnoreCase; boolean res = equalsIgnoreCase.test("java", "JAVA"); // true
Runnable:
Ничего не принимает и не возвращает
Runnable task = () -> System.out.println("Running!"); new Thread(task).start(); // Running!
Callable<V>:
Ничего не принимает, возвращает значение типа V, может бросить исключение.
Callable<Integer> callable = () -> { Thread.sleep(1000); return 42; }; ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(callable); int result = future.get(); // 42 (через 1 секунду) executor.shutdown();
Специализированные интерфейсы для примитивов:
IntPredicate, LongPredicate, DoublePredicate
IntConsumer, LongConsumer, DoubleConsumer
IntSupplier, LongSupplier, DoubleSupplier
IntFunction, LongFunction, DoubleFunction
ToIntFunction, ToLongFunction, ToDoubleFunction
IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
Пример использования IntPredicate:
IntPredicate isEven = n -> n % 2 == 0; boolean result = isEven.test(10); // true
➤ Какие есть типовые параметры в Java
Стандартные обозначения по соглашению:
<T>:
сокращение от Type (тип). Используется для любого типа данных
Примеры:
List<T>
Optional<T>
<E>:
сокращение от Element (элемент). Обычно используется для элементов коллекций
Примеры:
Collection<E>
Set<E>
<K>:
сокращение от Key (ключ). Используется для ключей, например, в структурах типа Map
Примеры:
Map<K, V>
<V>:
сокращение от Value (значение). Используется для значений в структурах типа Map
Примеры:
Map<K, V>
<R>:
сокращение от Result (результат). Обычно используется для возвращаемых значений
Примеры:
Function<T, R>
<U>:
дополнительный второй типовой параметр, если необходимо указать более одного общего типа
Примеры:
BiFunction<T, U, R>
<N>:
сокращение от Number (число). Используется редко, но может использоваться для числовых типов
Примеры:
Comparator<N> (если нужен Comparator для чисел)
<S>:
дополнительный типовой параметр, редко используется. Обычно для дополнительных вспомогательных типов при множественной параметризации.
➤ Какой механизм работы у volatile переменных в Java
С точки зрения JVM и Java Memory Model (JMM), volatile задействует более низкоуровневые механизмы, обеспечивая управление памятью и барьеры видимости.
Memory Barriers (Мемори-барьеры):
При доступе к volatile-переменной JVM вставляет memory barriers (барьеры памяти), которые управляют порядком операций чтения/записи. Это:
LoadLoad barrier
LoadStore barrier
StoreLoad barrier
StoreStore barrier
volatile boolean flag = false;
При чтении flag:
JVM вставляет LoadLoad + LoadStore — всё, что происходит после чтения, не может быть «перенесено» до него.
При записи в flag:
JVM вставляет StoreStore + StoreLoad — все изменения до записи должны быть завершены, и никакие чтения после неё не могут быть выполнены раньше.
На практике это означает, что volatile предотвращает агрессивные оптимизации компилятора и CPU, которые могут нарушить порядок выполнения кода в многопоточке.
Работа с кешами и регистрами CPU:
Без volatile, потоки могут кэшировать значение переменной в регистрах или L1/L2 кешах. С volatile, JVM гарантирует, что:
Поток всегда читает значение из основной памяти (main memory).
При записи — значение сразу сбрасывается в основную память.
Happens-before правило:
JMM определяет «happens-before» отношения между операциями. Для volatile:
Запись в volatile переменную happens-before любой чтения этой же переменной из другого потока.
Это одна из фундаментальных гарантий JMM и основа безопасности видимости в многопоточности.
JVM-инструкции:
На уровне байткода:
Чтение volatile → volatileLoad
Запись в volatile → volatileStore
JIT-компилятор может также вставить lock или mfence инструкции для x86, или dmb (data memory barrier) для ARM.
volatile в JVM:
Гарантирует видимость между потоками через memory barriers.
Управляет порядком исполнения операций.
Запрещает переупорядочивание.
Поддерживает happens-before отношение.
Использует специфические инструкции для flush/load из main memory.
➤ Как в JVM работают стеки потоков, с точки зрения взаимодействия с процессором
Что такое стек потока в JVM:
Для каждого потока JVM создаёт отдельный стек (Thread Stack), который используется для хранения:
фреймов вызовов методов (Stack Frames)
локальных переменных
операндов (для операций JVM-инструкций)
Каждый метод при вызове получает свой фрейм. После завершения метода фрейм убирается со стека.
Как это связано с процессором:
Процессор сам по себе оперирует регистрами, кешами и оперативной памятью. JVM работает в пользовательском пространстве, и стек потока JVM находится в оперативной памяти (RAM).
На уровне взаимодействия:
Создание потока:
JVM вызывает через ОС создание нативного потока (например, через pthread_create в Linux).
ОС выделяет блок памяти под стек (обычно 1–2 МБ по умолчанию).
JVM инициализирует структуру JVM стека внутри этого блока памяти.
Вызов метода = работа со стеком:
Когда поток выполняет метод, JVM добавляет новый Stack Frame.
Все вычисления (например, a + b) происходят на стеке — операнды кладутся туда, инструкции JVM работают с ними (через операндный стек фрейма).
JVM сама интерпретирует инструкции или передаёт их в JIT-компилированный код, который работает напрямую с регистром и стеком CPU.
JIT-компиляция и взаимодействие с CPU:
Когда HotSpot JIT-компилирует метод, он генерирует машинный код, который:
использует регистры CPU (например, для хранения локальных переменных, аргументов, временных значений);
управляет своим стеком вызовов (в стеке процесса);
обращается к данным JVM-стека, если нужно, через указатели и смещения.
Как JVM и стек потока используют кеш:
теперь добавим это к нашему стеку потока:
Работа с локальными переменными и операндами:
Когда JVM (или JIT-код) обращается к локальным переменным или операндам в стеке, он фактически обращается к памяти.
Эта память сначала попадает в CPU кеш:
Если данные есть в кеш-памяти (L1/L2), доступ очень быстрый (несколько наносекунд).
Если нет — CPU делает кеш-промах (cache miss) и подтягивает данные из RAM.
При активной работе метода, данные, связанные со стеком (например, int a = 42), почти всегда попадают в кеш — это называется кеш-локальность (cache locality).
Что такое кеш CPU:
Кеш процессора — это многоуровневая быстрая память, встроенная в процессор. Её задача — уменьшить количество обращений к оперативной памяти (RAM), потому что RAM работает медленно по сравнению с CPU.
Основные уровни:
L1 (очень быстрый, маленький — на ядро)
L2 (чуть медленнее, но больше — тоже на ядро)
L3 (ещё больше, медленнее — общий на все ядра)
L4 (редко используется, как правило, в серверных CPU)
JIT и оптимизация под кеш:
JIT-компилятор HotSpot (C2) генерирует машинный код, который:
Использует регистры CPU максимально для «горячих» переменных (чтобы не лезть в память вообще).
Устраивает layout переменных так, чтобы часто используемые данные находились рядом в памяти → это повышает пространственную локальность, что помогает кешу.
Кеш и переключение потоков:
При контекстном переключении, кеши могут быть частично инвалидированы (например, L1 кеш другого потока/ядра может не содержать нужные данные).
Это делает частые переключения между потоками менее эффективными, если они работают с разными частями памяти.
Поэтому важно держать данные «близко» — это одна из причин, почему горячие функции в JIT-интерпретации работают гораздо быстрее.
False sharing и кеш-линии:
Кеш работает с кеш-линиями (обычно 64 байта).
Если два потока пишут в разные переменные, которые находятся в одной кеш-линии, возникает false sharing — и процессор вынужден синхронизировать кеши между ядрами.
JVM старается это избегать, например, используя @Contended аннотацию (внутренняя оптимизация для конкурентных структур).
Стек потока (локальные переменные) -> Зачастую в L1 кеше
Операнды JVM -> Часто помещаются в регистры/кеш
JIT-код -> Генерируется с учётом кеш-локальности
Часто вызываемые методы -> Их код и данные остаются в кешах
Переключение потоков -> Может вызвать кеш-промахи
Контекстное переключение:
Когда ОС переключает контекст (смена потока), она сохраняет регистры CPU, включая указатель стека (stack pointer).
При возврате к потоку, стек продолжает использоваться, как будто ничего не произошло.
Пример визуализации (упрощённо):
fun foo(a: Int): Int { return bar(a * 2) } fun bar(x: Int): Int { return x + 1 }
foo вызывается → стек JVM получает фрейм foo.
bar вызывается → стек JVM получает фрейм bar.
Выполняется x + 1 → в стек bar кладётся x, потом результат.
bar завершён → его фрейм удаляется.
Возвращаемся к foo, который тоже завершён → его фрейм удаляется.