Подготовка за бекенд интервю по Java

Въпроси и отговори за интервю по Java 2026

Набор от 30 въпроса за интервю по Java за бекенд разработчици, от основите на езика и поведението на JVM до колекции, Stream API и многонишковост.

Започни пробно интервю по JavaНе се изисква кредитна карта. Налична е 1 безплатна сесия.
Практика за техническо интервю на английски езикРежим, в който хора, за които английският не е роден език, могат да се упражняват за преминаване на технически интервюта.

Java и JVM

1Какво е Java и защо се смята за независима от платформата?

Java е статично типизиран език с общо предназначение. Изходният код се компилира до байткод, а байткодът се изпълнява на JVM (Java Virtual Machine). Същият .class или .jar файл обикновено може да се изпълнява на различни операционни системи без прекомпилация, ако е налична съвместима JVM. Това не е абсолютна независимост: нейтив библиотеки, специфични за платформата пътища, системни команди и поведение на операционната система все пак могат да направят приложението зависимо от платформата.

Java source -> javac -> bytecode -> JVM -> machine code
Опитайте да отговорите на този въпрос с треньор с ИИ

2Каква е разликата между JVM (Java Virtual Machine), JRE (Java Runtime Environment) и JDK (Java Development Kit)?

JVM (Java Virtual Machine) зарежда и изпълнява байткод и управлява паметта, нишките и сметосъбирането. Исторически JRE (Java Runtime Environment) означаваше JVM плюс библиотеките, необходими за изпълнение на приложенията. JDK (Java Development Kit) включва средата за изпълнение и инструментите за разработка като `javac`, `java`, дебъгер, `javadoc`, `jar` и инструменти за диагностика. В модерните дистрибуции отделно JRE е по-рядко срещано, тъй като приложенията често се разпространяват със собствен персонализиран образ на средата за изпълнение.

JDK = development tools + runtime
Runtime = JVM + standard libraries
Опитайте да отговорите на този въпрос с треньор с ИИ

3Как JVM (Java Virtual Machine) изпълнява Java код и защо е необходим JIT (Just-In-Time)?

`javac` компилира изходния код до байткод. JVM зарежда класовете и започва да интерпретира или изпълнява инструкциите от байткода. Често изпълняваните критични за производителността части от кода се засичат по време на изпълнение и се компилират от JIT компилатора в оптимизиран машинен код. JIT може да вгражда методи (inlining), да оптимизира цикли, да прилага анализ на изплъзването (escape analysis) и да премахва някои заделяния на памет. Това е причината Java приложенията да стават по-бързи след първоначално загряване и защо микробенчмарковете без JMH често са подвеждащи.

Опитайте да отговорите на този въпрос с треньор с ИИ

4Как работи `ClassLoader`?

Даден `ClassLoader` зарежда дефиниции на класове в JVM (Java Virtual Machine). Основните видове зареждащи класове са `Bootstrap`, `Platform`, `Application` и потребителски имплементации. Всеки клас се идентифицира не само по своето пълно квалифицирано име, но и по инстанцията на `ClassLoader`, която го е заредила, така че два класа с едно и също име от различни зареждащи класове могат да бъдат различни типове. Java обикновено използва делегиране към родителя: зареждащият клас първо пита своя родител, за да намали риска от подмяна на платформени класове.

Loading
Linking: verification, preparation, resolution
Initialization
Опитайте да отговорите на този въпрос с треньор с ИИ

5Как е организирана паметта на JVM (Java Virtual Machine)?

Ключовите области в паметта на JVM включват купа за обекти и масиви, Java стековете с рамки на извикване за всяка нишка, `Metaspace` за метаданни на класовете, кеша за код за JIT (Just-In-Time) компилиран код, PC регистъра и системните стекове. Избягвайте прекаленото опростяване, че примитивните типове винаги живеят в стека, а обектите винаги живеят в купа: поле от примитивен тип се съхранява вътре в своя обект, а JIT анализът на изтичането (escape analysis) може да премахне заделянето на памет или да промени представянето на обекта.

Опитайте да отговорите на този въпрос с треньор с ИИ

6Как работи GC (Garbage Collector)?

GC (Garbage Collector) освобождава паметта, заета от обекти, които вече не са достъпни от корените на сметосъбирането (GC Roots). Корените включват локални променливи на активни методи, статични полета, активни нишки и JNI (Java Native Interface) референции. Много сметосъбирачи разчитат на наблюдението, че повечето нови обекти имат кратък живот, затова динамичната памет (heap) често се разделя на млада и стара генерация. GC не затваря детерминирано файлове или мрежови връзки; външните ресурси трябва да се затварят с конструкцията `try-with-resources`.

try (var stream = Files.newInputStream(path)) {
    // use stream
}
Опитайте да отговорите на този въпрос с треньор с ИИ

ООП и типове

7Кои четири принципа на ООП (Обектно-ориентирано програмиране) поддържа Java?

Java поддържа капсулация, абстракция, наследяване и полиморфизъм. Капсулацията скрива вътрешното състояние зад публичен API. Абстракцията се фокусира върху същественото поведение. Наследяването създава нов тип въз основа на съществуващ тип. Полиморфизмът позволява на кода да работи с различни имплементации чрез споделен договор. На практика композицията често е за предпочитане пред наследяването, тъй като създава по-слаба свързаност.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}
Опитайте да отговорите на този въпрос с треньор с ИИ

8Каква е разликата между претоварване (overloading) и припокриване (overriding) на методи?

Претоварването означава, че няколко метода имат едно и също име, но различни параметри; компилаторът избира метода въз основа на статичните типове на аргументите. Припокриването означава, че даден подклас заменя реализацията на метод на инстанция от родителския клас; изборът на метод по време на изпълнение се базира на действителния тип на обекта. Статичните методи се скриват, а не се припокриват, а частните методи не се припокриват, тъй като не се наследяват като достъпни членове.

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}
Опитайте да отговорите на този въпрос с треньор с ИИ

9Каква е разликата между интерфейс и абстрактен клас?

Интерфейсът дефинира договор и може да съдържа абстрактни методи, методи по подразбиране, статични методи, константи и помощни частни методи. Абстрактният клас може допълнително да има състояние на инстанцията, конструктори, защитени полета, споделена имплементация и абстрактни или конкретни методи. Един клас може да наследи само един клас, но може да имплементира множество интерфейси. Използвайте интерфейс за независим договор, а абстрактен клас – за тясно свързани типове със споделена имплементация и състояние.

interface Repository<T> {
    Optional<T> findById(long id);
}
Опитайте да отговорите на този въпрос с треньор с ИИ

10Какво означава ключовата дума final?

На променлива final може да бъде присвоена стойност само веднъж, но референция final не прави реферирания обект неизменяем. Метод final не може да бъде препокрит. Клас final не може да бъде наследен. Ключовата дума final съобщава намерението и ограничава промените в референциите или наследяването, но само по себе си не осигурява пълен модел за неизменяемост или безопасност при работа с нишки.

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}
Опитайте да отговорите на този въпрос с треньор с ИИ

11Какво представляват `records` в Java?

`record` е компактен синтаксис за неизменяеми носители на данни. Компилаторът генерира `private final` полета, конструктор, методи за достъп, `equals`, `hashCode` и `toString`. `records` са полезни за DTO (Data Transfer Object), обекти-стойности и резултати от методи. Те не са заместител на всяка домейн същност: в компактен конструктор може да се добави валидация, но сложно изменяемо поведение, мързеливо зареждане и модели, базирани на наследяване, обикновено не са подходящи за `records`.

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
Опитайте да отговорите на този въпрос с треньор с ИИ

12Как се създава неизменяем обект в Java?

Направете класа `final` или внимателно ограничете наследяването, направете полетата `private final`, избягвайте сетъри и извършвайте валидация в конструктора. За изменяеми входни данни създавайте защитни копия и връщайте копия или немодифицируеми изгледи. Дълбоката неизменяемост изисква всички вложени състояния също да бъдат неизменяеми. Полетата `final` помагат за безопасното публикуване, но един обект все още може да бъде логически изменяем, ако излага изменяема колекция или изменяем вложен обект.

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}
Опитайте да отговорите на този въпрос с треньор с ИИ

Референции, низове и сравняване

13Java предава аргументите по стойност или по референция?

Java винаги предава аргументите по стойност. За примитивните типове се копира самата стойност. За обектите се копира стойността на референцията. Даден метод може да променя обекта чрез копираната референция, но не може да замени референцията на извикващия с друг обект. Това разграничение обяснява защо промяната на дадено поле може да бъде видима за извикващия, докато присвояването на параметъра към нов обект не е.

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}
Опитайте да отговорите на този въпрос с треньор с ИИ

14Каква е разликата между `==` и `equals()`?

При примитивните типове `==` сравнява стойностите. При обектните референции `==` проверява дали и двете референции сочат към един и същ обект. `equals()` е метод, който трябва да сравнява логическото равенство, ако класът го имплементира правилно. `String`, `Integer`, `LocalDate` и много класове, базирани на стойности, припокриват `equals`. За потребителски домейн обекти `equals` трябва да отразява предвидената семантика за идентичност или стойност.

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true
Опитайте да отговорите на този въпрос с треньор с ИИ

15Какъв е договорът между методите `equals` и `hashCode`?

Ако два обектa са равни според метода `equals`, те задължително трябва да връщат един и същ `hashCode`. Обратното не е изискване: различни обекти могат да имат еднакъв `hashCode`. Методът `equals` трябва да бъде рефлексивен, симетричен, транзитивен, консистентен и да връща `false` при подаден `null`. Нарушаването на този договор компрометира правилното поведение на `HashMap`, `HashSet` и други хеш-базирани колекции: намирането на даден обект може да стане невъзможно, въпреки че той присъства в колекцията.

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}
Опитайте да отговорите на този въпрос с треньор с ИИ

16Защо `String` е неизменяем?

`String` е неизменяем заради сигурността, предвидимостта и производителността. Неизменяемостта позволява интерниране на низове, безопасно споделяне между нишки, надеждно кеширане на `hashCode` и сигурно използване в пътища, URL адреси, имена на класове и стойности, чувствителни към сигурността. Операции като `concat`, `replace` и `substring` създават нови низове. От Java 9 насам `String` вътрешно може да използва `byte[]` с компактно представяне, но публичният договор за неизменяемост остава същият.

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning
Опитайте да отговорите на този въпрос с треньор с ИИ

17Каква е разликата между String, StringBuilder и StringBuffer?

String е неизменяем, така че многократното конкатениране в цикли може да създаде много междинни обекти, въпреки че компилаторът може да оптимизира прости случаи. StringBuilder е изменяем и е предпочитан за изграждане на низове в една нишка. StringBuffer е подобен, но е синхронизиран, така че обикновено е по-бавен и се използва главно за по-стари API интерфейси, изискващи безопасност при работа с нишки. За форматиране, String.format или API интерфейси, базирани на шаблони, може да са по-ясни, но не винаги са по-бързи.

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;
Опитайте да отговорите на този въпрос с треньор с ИИ

Генерични типове и колекции

18Какво представляват генеричните типове и какво е изтриване на типовете?

Генеричните типове позволяват на кода да изразява типово безопасни контейнери и програмни интерфейси, като например `List<String>`. По време на компилация компилаторът проверява типовете и вмъква преобразувания там, където е необходимо. По време на изпълнение по-голямата част от информацията за генеричния тип се изтрива, така че `List<String>` и `List<Integer>` споделят един и същ клас. Това е причината да не можете директно да създавате `new T()`, да използвате примитивни типове като параметри или надеждно да проверявате с `instanceof List<String>`.

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;
Опитайте да отговорите на този въпрос с треньор с ИИ

19Какво представлява Java Collections Framework?

Collections Framework е набор от интерфейси и имплементации за съхранение и обработка на групи от обекти. Основните интерфейси включват List, Set, Queue, Deque и Map. Имплементациите включват ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap и PriorityQueue. Изборът на колекция трябва да се прави въз основа на модела на достъп, изискванията за подредба, уникалността на елементите, сложността на търсенето, допълнителния разход на памет и нуждите при паралелно изпълнение.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
Опитайте да отговорите на този въпрос с треньор с ИИ

20Как работи HashMap?

HashMap съхранява записи в кофи, избрани чрез хеш на ключа. За търсене изчислява хеша, намира съответната кофа и сравнява ключовете чрез метода equals. Колизите се обработват вътре в кофата; в съвременните версии на Java дългите вериги могат да бъдат преобразувани в балансирано дърво, когато са изпълнени определени условия. HashMap позволява един ключ със стойност null и множество null стойности, не е безопасен за многонишково изпълнение и може да промени размера си, когато се надвиши прагът на фактора на запълване.

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());
Опитайте да отговорите на този въпрос с треньор с ИИ

21Каква е разликата между HashMap, LinkedHashMap, TreeMap и ConcurrentHashMap?

`HashMap` осигурява бърз неподреден достъп и не е безопасен при работа в многонишкова среда. `LinkedHashMap` запазва реда на вмъкване или реда на достъп и е полезен за кешове от типа LRU. `TreeMap` поддържа ключовете сортирани според естествения им ред или чрез `Comparator` и изпълнява операциите за O(log n) време. `ConcurrentHashMap` е предназначен за паралелен достъп чрез сегментирана или вътрешна синхронизация, не позволява `null` ключове или стойности и се предпочита пред `synchronizedMap` при високо натоварване.

map.computeIfAbsent(key, ignored -> loadValue());
Опитайте да отговорите на този въпрос с треньор с ИИ

22Каква е разликата между ArrayList и LinkedList?

`ArrayList` е реализиран чрез масив. Той осигурява бърз достъп по индекс, компактно разположение в паметта и ефективно итериране, но вмъкването в средата на масива изисква изместване на елементи. `LinkedList` е двойно свързан списък. Вмъкването е по-бързо, ако възелът вече е известен, но достъпът по индекс отнема O(n) време, допълнителният разход на памет е по-голям и локалността на кеша е по-лоша. В много реални приложения `ArrayList` е по-бърз, дори когато се налагат някои вмъквания.

Опитайте да отговорите на този въпрос с треньор с ИИ

23Каква е разликата между Comparable и Comparator?

`Comparable` дефинира естествения ред вътре в класа чрез метода `compareTo`. `Comparator` е отделен обект или ламбда израз, който дефинира външно подреждане. `Comparable` е полезен, когато даден тип има един очевиден ред по подразбиране. `Comparator` е по-подходящ, когато съществуват множество начини за подреждане, например по име, дата или приоритет. Методите `compare` и `compareTo` трябва да бъдат последователни и за предпочитане съвместими с `equals`, когато се използват в сортирани множества или асоциативни масиви.

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);
Опитайте да отговорите на този въпрос с треньор с ИИ

Изключения и ресурси

24Каква е разликата между проверявани (checked) и непроверявани (unchecked) изключения?

Проверяваните изключения наследяват `Exception`, но не и `RuntimeException`, и компилаторът изисква извикващият код да ги обработи или декларира. Непроверяваните изключения наследяват `RuntimeException` и не се изискват в сигнатурите на методите. Проверяваните изключения са подходящи за ситуации на границите на API, от които е възможно възстановяване. Непроверяваните изключения са подходящи за програмни грешки, невалидно състояние и нарушения на програмния договор. В съвременния код на услугите екипите често избягват прекомерното използване на проверявани изключения, тъй като те създават излишен шум при преминаването си през слоевете.

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");
Опитайте да отговорите на този въпрос с треньор с ИИ

25Каква е разликата между throw, throws и try-with-resources?

`throw` незабавно хвърля изключение. `throws` декларира възможно изключение в сигнатурата на метод. `try-with-resources` автоматично затваря `AutoCloseable` ресурси в обратен ред. Ако и главният блок, и методът за затваряне хвърлят изключения, второто се запазва като потиснато изключение (suppressed exception). Това е по-безопасно и по-ясно от ръчното използване на блокове `finally`, когато се работи с няколко ресурса.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
Опитайте да отговорите на този въпрос с треньор с ИИ

Stream API и функционален стил

26Какво представлява Stream API?

Stream API позволява на кода да обработва поредици от данни декларативно. Междинните операции като filter, map и sorted се изпълняват отложено; терминалните операции като toList, collect, reduce и forEach стартират изпълнението. Потокът не е колекция, обикновено се използва само веднъж и работи най-добре с чисти функции без странични ефекти. parallelStream не гарантира по-бързо изпълнение и може да навреди на производителността или коректността.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();
Опитайте да отговорите на този въпрос с треньор с ИИ

27Какво е Optional и кога трябва да се използва?

Optional<T> изрично представя липсата на резултат и работи добре като тип на връщана стойност. Полезните методи включват map, flatMap, filter, orElse, orElseGet и orElseThrow. orElse изчислява аргумента си незабавно, докато Supplier функцията в orElseGet се изпълнява само когато стойността липсва. Optional обикновено не се препоръчва за поля на същности, параметри на методи или сериализирани DTO (Data Transfer Object) обекти, освен ако няма основателна причина.

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());
Опитайте да отговорите на този въпрос с треньор с ИИ

Многонишковост

28Какво гарантират synchronized, volatile и атомарните класове?

synchronized осигурява взаимно изключване и връзка от типа happens-before. volatile гарантира видимост на последните записи и ограничава пренареждането на инструкциите, но не прави съставните операции атомарни. Атомарните класове предоставят индивидуални атомарни операции като incrementAndGet. Използвайте ключалки или synchronized за няколко свързани промени, volatile за прости флагове на състоянието и AtomicInteger или LongAdder за броячи при висока конкуренция за достъп.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Опитайте да отговорите на този въпрос с треньор с ИИ

29Какво представляват състезателното условие, мъртвата схватка и безопасността на нишките?

Състезателно условие възниква, когато резултатът зависи от непредвидимия ред на изпълнение на нишките. Мъртва схватка се получава, когато нишките чакат вечно за ресурси, държани една от друга. За да постигнете безопасност на нишките, намалете споделеното изменяемо състояние, поддържайте последователен ред на заключване, съкратете критичните секции, избягвайте извикването на външен код докато държите ключалка и използвайте неизменяеми обекти, конкурентни колекции, таймаути или предаване на съобщения. Безопасността на нишките е свойство на целия протокол за достъп до състоянието.

Опитайте да отговорите на този въпрос с треньор с ИИ

30Каква е разликата между ExecutorService, CompletableFuture и виртуалните нишки?

ExecutorService управлява пул от нишки и изпълнението на задачи. CompletableFuture помага за композирането на асинхронни операции. Виртуалните нишки са леки нишки на JVM (Java Virtual Machine), които са удобни за голям брой блокиращи I/O задачи. Те опростяват модела 'нишка за всяка заявка', но не ускоряват задачите, които натоварват силно процесора, не премахват ограниченията на базите данни или външните ресурси и не правят автоматично изменяемото състояние безопасно за нишките.

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}
Опитайте да отговорите на този въпрос с треньор с ИИ