У мене вийшла досить об'ємна добірка поширених запитань про ООП, Java Syntax, винятки в Java, колекції та багатопотоковість, яку для зручності я розіб'ю на кілька частин.
Важливо: ми розглядатимемо лише версії Java до 8. Усі нововведення, починаючи з Java 9, тут не розглядатимуться.
Будь-які ідеї чи зауваження щодо того, як можна покращити відповіді, вітаються. Приємного читання!Java-співбесіда: запитання з ООП
1. Які особливості має Java?
Відповідь:-
Концепції ООП:
- об'єктно-орієнтоване програмування;
- успадкування;
- інкапсуляція;
- поліморфізм;
- абстракція.
-
Кросплатформеність: програму, написану на Java, можна запускати на будь-якій платформі без змін. Єдина вимога — наявність встановленої JVM (Java Virtual Machine).
-
Висока продуктивність: JIT-компілятор (Just-In-Time Compiler) забезпечує високу продуктивність. Він перетворює байт-код на машинний код, після чого JVM виконує програму.
-
Багатопотоковість: потік виконання представлений класом
Thread. JVM автоматично створює потікmain. Програміст може створювати додаткові потоки, успадковуючи класThreadабо реалізуючи інтерфейсRunnable.
2. Що таке успадкування?
Під успадкуванням мається на увазі, що один клас може успадковувати (extends) інший клас. Таким чином можна повторно використовувати код класу, від якого успадковуються. Існуючий клас відомий якsuperclass, а створюваний — subclass. Також ще кажуть parent і child.
public class Animal {
private int age;
}
public class Dog extends Animal {
}
де Animal — це parent, а Dog — child.3. Що таке інкапсуляція?
Таке запитання часто трапляється на співбесіді Java-розробника. Інкапсуляція — це приховування реалізації за допомогою модифікаторів доступу, а також гетерів і сетерів. Це робиться для того, щоб закрити доступ для зовнішнього використання в тих місцях, де розробники вважають це за потрібне. Доступний приклад із життя — автомобіль. Ми не маємо прямого доступу до роботи двигуна. Для нас робота полягає в тому, щоб вставити ключ у замок запалювання та запустити двигун. А які процеси відбуватимуться під капотом — не наша справа. Ба більше, наше втручання в цю діяльність може призвести до непередбачуваної ситуації, через яку можна і машину зламати, і нашкодити собі. Так само відбувається і в програмуванні. Добре описано у Вікіпедії. Стаття про інкапсуляцію є і на JavaRush.4. Що таке поліморфізм?
Поліморфізм — це здатність програми однаково використовувати об'єкти з однаковим інтерфейсом без інформації про конкретний тип цього об'єкта. Як кажуть, один інтерфейс — безліч реалізацій. За допомогою поліморфізму можна об'єднувати та використовувати різні типи об'єктів відповідно до їхньої спільної поведінки. Наприклад, є клас Animal, який має двох нащадків — Dog і Cat. Спільний клас Animal має загальну поведінку для всіх — видавати звук. У випадку, коли потрібно зібрати разом усіх нащадків класу Animal і викликати метод «видавати звук», використовують можливості поліморфізму. Ось як це виглядатиме:
List<Animal> animals = Arrays.asList(new Cat(), new Dog(), new Cat());
animals.forEach(animal -> animal.makeSound());
Отже, поліморфізм допомагає нам. Причому це стосується і поліморфних (перевантажених) методів.
Запитання на співбесіді — Java Syntax
5. Що таке конструктор у Java?
Наступні характеристики є правильними:- Коли створюється новий об'єкт, програма використовує відповідний конструктор.
- Конструктор нагадує метод. Його особливість полягає в тому, що він не має типу, що повертається (зокрема й
void), а його ім'я збігається з ім'ям класу. - Якщо не визначено жодного конструктора, автоматично буде створено конструктор без параметрів.
- Конструктор може бути перевантажений.
- Якщо було створено конструктор із параметрами, а потрібен ще й конструктор без параметрів, його необхідно створити окремо, оскільки автоматично він уже не генеруватиметься.
6. Які два класи не успадковуються від Object?
Не ведіться на провокацію — таких класів немає. Усі класи прямо або через своїх предків успадковуються від класу Object!7. Що таке Local Variable?
Ще одне з популярних запитань на співбесіді Java-розробника. Local Variable (локальна змінна) — це змінна, яка оголошена всередині методу й існує доти, доки виконується цей метод. Щойно виконання методу завершується, локальна змінна перестає існувати. Ось програма, яка використовує локальну зміннуhelloMessage у методі main():
public static void main(String[] args) {
String helloMessage;
helloMessage = "Hello, World!";
System.out.println(helloMessage);
}
8. Що таке Instance Variable?
Instance Variable — це змінна, яка оголошена всередині класу, і вона існує доти, доки існує об'єкт. Приклад — класBee, у якому є дві змінні: nectarCapacity та maxNectarCapacity:
public class Bee {
/**
* Current nectar capacity
*/
private double nectarCapacity;
/**
* Maximal nectar that can take bee.
*/
private double maxNectarCapacity = 20.0;
...
}
9. Що таке модифікатори доступу?
Модифікатори доступу — це інструмент, за допомогою якого можна налаштувати доступ до класів, методів і змінних. Існують такі модифікатори, упорядковані за зростанням рівня доступу:private— використовується для методів, полів і конструкторів. Рівень доступу — лише клас, у якому його оголошено.package-private (default)— може використовуватися для класів. Доступ є лише в межах пакета (package), у якому оголошено клас, метод, змінну або конструктор.protected— такий самий рівень доступу, як іpackage-private, але також доступ надається класам, які успадковуються від класу з модифікаторомprotected.public— використовується також для класів. Надає повний доступ у всьому застосунку.

10. Що таке перевизначення методів?
Перевизначення методів відбувається тоді, коли дочірній клас хоче змінити поведінку методу батьківського класу. Якщо потрібно, щоб усе ж виконалася реалізація методу батьківського класу, у дочірньому класі можна використати конструкцію видуsuper.methodName(), яка виконає метод батьківського класу, після чого можна додати власну логіку.
Вимоги, яких потрібно дотримуватися:
- сигнатура методу має бути однаковою;
- тип значення, що повертається, має бути таким самим.
11. Що таке сигнатура методу?
Сигнатура методу — це сукупність назви методу та аргументів, які він приймає. Сигнатура методу є унікальним ідентифікатором методу під час перевантаження методів.
12. Що таке перевантаження методів?
Перевантаження методів — це властивість поліморфізму, за якої за допомогою зміни сигнатури методу можна створити різні методи для виконання одних і тих самих дій:- одна й та сама назва методу;
- різні аргументи;
- може бути різний тип значення, що повертається.
add() у ArrayList може бути перевантажений таким чином і виконуватиме додавання різними способами залежно від вхідних аргументів:
add(Object o)— просто додає об'єкт;add(int index, Object o)— додає об'єкт за вказаним індексом;add(Collection<Object> c)— додає колекцію об'єктів;add(int index, Collection<Object> c)— додає колекцію об'єктів, починаючи із зазначеного індексу.
13. Що таке Interface?
Множинне успадкування не реалізоване в Java, тому для вирішення цієї проблеми були додані інтерфейси в тому вигляді, у якому ми їх знаємо ;) Довгий час інтерфейси містили лише методи без їхньої реалізації. У межах цієї відповіді говоритимемо саме про них. Наприклад:
public interface Animal {
void makeSound();
void eat();
void sleep();
}
Із цього випливають деякі особливості:
- усі методи в інтерфейсі є
publicіabstract; - усі змінні —
public static final; - класи не успадковують інтерфейси (
extends), а реалізують їх (implements). При цьому клас може реалізовувати будь-яку кількість інтерфейсів; - класи, що реалізують інтерфейс, повинні надати реалізацію всіх методів, оголошених в інтерфейсі.
public class Cat implements Animal {
public void makeSound() {
// реалізація методу
}
public void eat() {
// реалізація
}
public void sleep() {
// реалізація
}
}
14. Що таке default method в Interface?
Тепер поговоримо про дефолтні методи. Навіщо вони потрібні? Ці методи додали, щоб, як то кажуть, «і вовки ситі, і вівці цілі». Про що йдеться? З одного боку, потрібно було додати нову функціональність: лямбда-вирази та Stream API. З іншого боку, необхідно було зберегти те, чим славиться Java, — зворотну сумісність. Для цього довелося додати вже готову реалізацію методів до інтерфейсів. Так і з'явилися дефолтні методи. Отже, дефолтний метод — це реалізований метод в інтерфейсі, який має ключове словоdefault.
Наприклад, усім відомий метод stream() в інтерфейсі Collection. Перевірте — цей інтерфейс зовсім не такий простий, як здається :). Або ще один не менш відомий метод forEach() з інтерфейсу Iterable. Його також не існувало, доки не були додані дефолтні методи.
До речі, про це також можна почитати на JavaRush.15. А як тоді успадкувати два однакові дефолтні методи?
Виходячи з попередньої відповіді на те, що таке дефолтний метод, можна поставити інше питання. Якщо можна реалізувати методи в інтерфейсах, теоретично можна реалізувати два інтерфейси з однаковим методом, і як таке робити? Є два різні інтерфейси з однаковим методом:
interface A {
default void foo() {
System.out.println("Foo A");
}
}
interface B {
default void foo() {
System.out.println("Foo B");
}
}
І є клас, який реалізує ці два інтерфейси. Щоб не було невизначеності й код скомпілювався, нам потрібно перевизначити метод foo() у класі C, причому можна просто викликати в ньому метод foo() будь-якого з інтерфейсів — A або B.
Але як вибрати конкретний метод інтерфейсу A чи B? Для цього є конструкція A.super.foo().
public class C implements A, B {
@Override
public void foo() {
A.super.foo();
}
}
или:
public class C implements A, B {
@Override
public void foo() {
B.super.foo();
}
}
Таким чином, метод foo() класу C використовуватиме або дефолтний метод foo() з інтерфейсу A, або метод foo() з інтерфейсу B.16. Що таке абстрактні методи та класи?
У Java є зарезервоване словоabstract, яке використовується для позначення абстрактних класів і методів.
Для початку — визначення. Абстрактним методом називається метод, оголошений без реалізації за допомогою ключового слова abstract в абстрактному класі. Тобто це метод, аналогічний методу в інтерфейсі, але з додаванням ключового слова abstract, наприклад:
public abstract void foo();
Абстрактним класом називається клас, який має також abstractслово:
public abstract class A {
}
Абстрактний клас має кілька особливостей:
- на його основі не можна створити об'єкт;
- може містити абстрактні методи;
- може не містити абстрактних методів.
17. Яка різниця між String, StringBuilder і StringBuffer?
ЗначенняString зберігаються в пулі рядків (constant string pool). Щойно буде створено рядок, він з'явиться в цьому пулі. Видалити його звідти не можна.
Наприклад:
String name = "book";
...змінна буде посилатися на рядок у пулі рядків (Constant String Pool)
Якщо задати змінній name інше значення, вийде наступне:
name = "pen";
Constant string pool
Таким чином, ці два значення так і залишаться там.StringBuffer:
- значення
StringBufferзберігається в купі (Heap). Якщо значення змінюється, нове значення замінює попереднє без створення нового об'єкта; StringBufferє синхронізованим, тому він потокобезпечний;- через синхронізацію працює повільніше, ніж
StringBuilder.
StringBuffer name = "book";
Як тільки значення name зміниться, в стеку зміниться значення:
StringBuilder майже такий самий, як і StringBuffer, тільки він не є потокобезпечним. Саме тому його швидкодія вища, ніж у StringBuffer.18. Яка різниця між абстрактним класом та інтерфейсом?
Абстрактний клас:- абстрактні класи мають конструктор за замовчуванням; він викликається щоразу, коли створюється нащадок цього абстрактного класу;
- може містити як абстрактні, так і неабстрактні методи. Ба більше, він може взагалі не містити абстрактних методів, але при цьому залишатися абстрактним класом;
- клас, що успадковує абстрактний клас, повинен реалізувати лише його абстрактні методи;
- абстрактний клас може містити instance-змінні (див. питання №8).
- не має конструктора й не може бути ініціалізований;
- до Java 8 міг містити лише абстрактні методи (починаючи з Java 8 з'явилися
defaultтаstaticметоди); - класи, що реалізують інтерфейс, повинні реалізувати всі його абстрактні методи (крім
defaultmethods); - інтерфейси можуть містити лише константи.
19. Чому доступ до елемента масиву має складність O(1)?
Це запитання мені буквально поставили на останній співбесіді. Як я дізнався пізніше, його ставлять, щоб зрозуміти, як мислить кандидат. Звісно, практична користь від цього знання невелика — достатньо просто знати цей факт. Спочатку потрібно уточнити, що O(1) — це позначення часової складності алгоритму, коли операція виконується за сталий час. Тобто це позначення найшвидшого можливого виконання. Щоб відповісти на це запитання, потрібно зрозуміти, що ми знаємо про масиви. Щоб створити масивint, потрібно написати таке:
int[] intArray = new int[100];
З цього запису можна зробити кілька висновків:- Під час створення масиву відомий його тип. Якщо тип відомий, то зрозуміло, який розмір матиме кожна комірка масиву.
- Відомий розмір масиву.
А як виходить O(1) під час доступу до об'єктів ArrayList?
Це запитання логічно випливає з попереднього. Адже справді, коли ми працюємо з масивом, у якому зберігаються примітивні типи, ми заздалегідь знаємо розмір кожного елемента. Але що робити, якщо маємо таку схему, як на зображенні:
і ми хочемо створити колекцію з елементами, які мають тип A, і додати різні реалізації — B, C, D:
List<A> list = new ArrayList();
list.add(new B());
list.add(new C());
list.add(new D());
list.add(new B());
Як у цій ситуації зрозуміти, яким буде розмір кожної комірки, адже кожен об’єкт може бути різним і мати різні додаткові поля або взагалі бути повністю іншим? Що робити?
Тут питання поставлене так, щоб заплутати й спантеличити. Ми ж знаємо, що насправді в колекціях зберігаються не самі об’єкти, а лише посилання на ці об’єкти. А всі посилання мають однаковий розмір, і він відомий. Тому підрахунок місця тут працює так само, як і в попередньому питанні.
21. Автоупаковка (autoboxing) та авторозпаковка (unboxing)
Історична довідка: автоупаковка та авторозпаковка — одне з головних нововведень JDK 5. Автоупаковка (autoboxing) — це процес автоматичного перетворення примітивного типу у відповідний клас-обгортку. Авторозпаковка (unboxing) робить протилежне до автоупаковки — перетворює клас-обгортку на примітив. А от якщо значення обгортки дорівнюєnull, то під час розпаковки буде викинуто виняток NullPointerException.Відповідність: примітив — обгортка
| Примітив | Клас-обгортка |
|---|---|
| boolean | Boolean |
| int | Integer |
| byte | Byte |
| char | Character |
| float | Float |
| long | Long |
| short | Short |
| double | Double |
Автоупаковка відбувається:
-
коли примітивному значенню присвоюють посилання на клас-обгортку:
До Java 5:
// Ручна упаковка, або як це було до Java 5. public void boxingBeforeJava5() { Boolean booleanBox = new Boolean(true); Integer intBox = new Integer(3); // і так далі для інших типів }Після Java 5:
// Автоматична упаковка, або як це почало працювати в Java 5. public void boxingJava5() { Boolean booleanBox = true; Integer intBox = 3; // і так далі для інших типів } -
коли примітивне значення передають як аргумент методу, який очікує клас-обгортку:
public void exampleOfAutoboxing() { long age = 3; setAge(age); } public void setAge(Long age) { this.age = age; }
Авторозпаковка відбувається:
-
коли об'єкт класу-обгортки присвоюють примітивній змінній:
// До Java 5: int intValue = new Integer(4).intValue(); double doubleValue = new Double(2.3).doubleValue(); char c = new Character((char) 3).charValue(); boolean b = Boolean.TRUE.booleanValue(); // Починаючи з Java 5: int intValue = new Integer(4); double doubleValue = new Double(2.3); char c = new Character((char) 3); boolean b = Boolean.TRUE; -
У випадках із арифметичними операціями. Вони застосовуються тільки до примітивних типів, для цього потрібно розпаковувати примітив.
// До Java 5 Integer integerBox1 = new Integer(1); Integer integerBox2 = new Integer(2); // для порівняння потрібно було писати так: integerBox1.intValue() > integerBox2.intValue() // Починаючи з Java 5 integerBox1 > integerBox2 -
коли передають в обгортку метод, який приймає відповідний примітив:
public void exampleOfAutoboxing() { Long age = new Long(3); setAge(age); } public void setAge(long age) { this.age = age; }
22. Що таке ключове слово final і де його використовують?
Ключове словоfinal можна використовувати для змінних, методів і класів.
finalзмінну не можна перепризначити іншому об'єкту.finalклас не можна успадковувати.finalметод не можна перевизначити в класі-нащадку.
final змінні
Java надає два способи створити змінну та присвоїти їй значення:- Можна оголосити змінну, а ініціалізувати її пізніше.
- Можна оголосити змінну й одразу її ініціалізувати.
final змінних у цих випадках:
public class FinalExample {
//статическая переменная final, которая сразу же инициализируется:
final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
//final переменная, которая не инициализирована, но работать будет только если
//инициализировать это в конструкторе:
final long creationTime;
public FinalExample() {
this.creationTime = System.currentTimeMillis();
}
public static void main(String[] args) {
FinalExample finalExample = new FinalExample();
System.out.println(finalExample.creationTime);
// final поле FinalExample.FINAL_EXAMPLE_NAME не может быть заасайнено
// FinalExample.FINAL_EXAMPLE_NAME = "Not you're not!";
// final поле Config.creationTime не может быть заасайнено
// finalExample.creationTime = 1L;
}
}
Чи можна вважати final змінну константою?
Оскількиfinal змінній не можна присвоїти нове значення, може здатися, що будь-яка final змінна є константою. Але це не зовсім так.
Якщо тип даних, на який посилається змінна, є immutable (незмінним), тоді так — таку змінну можна вважати константою.
Якщо ж тип даних є mutable (змінним), то за допомогою методів цього об'єкта можна змінити його внутрішній стан. Тобто, хоча final змінну не можна перепризначити іншому об'єкту, сам об'єкт, на який вона посилається, може змінюватися. У такому разі називати таку змінну константою не можна.
Як видно з прикладу, лише частина final змінних є справжніми константами, тоді як інші посилаються на змінні об'єкти, стан яких можна змінювати.
public class FinalExample {
// final змінні, що посилаються на незмінні об'єкти:
final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
final static Integer FINAL_EXAMPLE_COUNT = 10;
// final змінні, що посилаються на змінні об'єкти:
final List<String> addresses = new ArrayList();
final StringBuilder finalStringBuilder = new StringBuilder("constant?");
}
Локальні final-змінні
Колиfinal змінну створюють усередині методу, її називають локальною final змінною:
public class FinalExample {
public static void main(String[] args) {
// Так можна
final int minAgeForDriveCar = 18;
// А можна й так — у циклі foreach:
for (final String arg : args) {
System.out.println(arg);
}
}
}
Ми можемо використовувати ключове слово final у розширеному циклі for, оскільки після завершення кожної ітерації цього циклу щоразу створюється нова змінна.
Проте це не стосується звичайного циклу for. Тому наведений нижче код призведе до помилки компіляції.
// Локальній final змінній i не можна присвоїти нове значення
for (final int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
final клас
Не можна успадковувати клас, оголошений якfinal. Простіше кажучи, жоден клас не може бути нащадком такого класу.
Чудовим прикладом final класу в JDK є String. Один із перших кроків під час створення незмінного (immutable) класу — оголосити його як final. Тоді жоден інший клас не зможе успадковуватися від нього:
public final class FinalExample {
}
// Тут виникне помилка компіляції
class WantsToInheritFinalClass extends FinalExample {
}
final методи
Коли метод оголошено з модифікаторомfinal, його називають final методом. Такий метод не можна перевизначити в класі-нащадку.
До речі, хорошим прикладом final методу в класі Object є getClass(). Оскільки він оголошений як final, його не можна перевизначити.
public class FinalExample {
public final String generateAddress() {
return "Some address";
}
}
class ChildOfFinalExample extends FinalExample {
// Тут виникне помилка компіляції
@Override
public String generateAddress() {
return "My OWN Address";
}
}
Як і де використовувати final у Java
- використовуйте ключове слово
finalдля оголошення констант рівня класу; - використовуйте
finalзмінні для об'єктів, якщо не хочете, щоб їх можна було перепризначити. Наприклад, для властивостей об'єкта, які використовуються під час логування; - якщо не потрібно, щоб клас можна було успадковувати, оголосіть його як
final; - якщо потрібно створити
immutableклас, одним із перших кроків є оголосити його якfinal; - якщо потрібно, щоб реалізація методу не змінювалася в класах-нащадках, оголосіть цей метод як
final. Це гарантує, що його реалізація не буде перевизначена.
23. Що таке mutable та immutable?
Mutable
Mutable називають об'єкти, стан яких можна змінити після створення. Тобто після створення такого об'єкта можна змінювати його поля або внутрішній стан без створення нового об'єкта.
Прикладами mutable класів є StringBuilder та StringBuffer. Приклад:
public class MutableExample {
private String address;
public MutableExample(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
// Цей сетер може змінити поле address
public void setAddress(String address) {
this.address = address;
}
public static void main(String[] args) {
MutableExample obj = new MutableExample("first address");
System.out.println(obj.getAddress());
// Оновлюємо поле address, отже це mutable об'єкт
obj.setAddress("Updated address");
System.out.println(obj.getAddress());
}
}
Immutable
Immutable називають об'єкти, стан яких не можна змінити після створення. Будь-яка спроба змінити такий об'єкт призводить до створення нового об'єкта, а не до зміни існуючого.
Саме тому immutable об'єкти чудово підходять для використання як ключі в HashMap.
Прикладами immutable класів є String, Integer, Double та інші. Приклад:
// Зробимо цей клас final, щоб його не можна було успадкувати
public final class ImmutableExample {
private String address;
ImmutableExample(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
// Видаляємо сетер
public static void main(String[] args) {
ImmutableExample obj = new ImmutableExample("old address");
System.out.println(obj.getAddress());
// Тому це поле неможливо змінити, отже це immutable об'єкт
// obj.setAddress("new address");
// System.out.println(obj.getAddress());
}
}
24. Як написати immutable клас?
Після того як ми з'ясували, що такеmutable та immutable об'єкти, закономірно виникає наступне запитання: як написати immutable клас?
Щоб створити immutable клас, потрібно дотримуватися таких правил:
- оголосити клас як
final; - зробити всі поля
privateі надати до них лише гетери. Сетери створювати не потрібно; - зробити всі поля
final, щоб їх можна було ініціалізувати лише один раз; - ініціалізувати всі поля через конструктор. Якщо поле є
mutable, слід виконати глибоке копіювання (тобто скопіювати сам об'єкт, його поля, вкладені об'єкти тощо); - якщо гетери повертають
mutableоб'єкти, необхідно повертати їх копії, а не посилання на самі об'єкти.
/**
* Приклад створення immutable об'єкта.
*/
public final class FinalClassExample {
private final int age;
private final String name;
private final HashMap<String, String> addresses;
public int getAge() {
return age;
}
public String getName() {
return name;
}
/**
* Створюємо копію об'єкта перед тим, як повернути його.
*/
public HashMap<String, String> getAddresses() {
return (HashMap<String, String>) addresses.clone();
}
/**
* У конструкторі виконуємо глибоке копіювання для mutable об'єктів.
*/
public FinalClassExample(int age, String name, HashMap<String, String> addresses) {
System.out.println("Виконуємо глибоке копіювання в конструкторі");
this.age = age;
this.name = name;
HashMap<String, String> temporaryMap = new HashMap<>();
String key;
Iterator<String> iterator = addresses.keySet().iterator();
while (iterator.hasNext()) {
key = iterator.next();
temporaryMap.put(key, addresses.get(key));
}
this.addresses = temporaryMap;
}
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ