Топ-50 Java Core вопросов и ответов на собеседовании. Часть 1
Всем привет, дамы и господа Software Engineers! Давайте поговорим о вопросах на собеседовании. О том, к чему нужно готовиться и что нужно знать. Это отличный повод для того, чтобы повторить или же изучить с нуля эти моменты.
У меня получилась довольно объемная подборка часто задаваемых вопросов об ООП, Java Syntax, исключениях в Java, коллекциях и многопоточности, которую для удобства я разобью на несколько частей. Важно: мы будем говорить только о версии Java до 8. Все нововведения с 9, 10, 11, 12, 13 не будут учитываться здесь. Любые идеи/замечания, как улучшить ответы, приветствуются. Приятного прочтения, поехали!
Java собеседование: вопросы по ООП
1. Какие особенности есть у Java?
Ответ:
- ООП концепты:
- объектная ориентированность;
- наследование;
- инкапсуляция;
- полиморфизм;
- абстракция.
- Кроссплатформенность: программа на Java может быть запущена на любой платформе без каких-либо изменений. Единственное, что нужно — установленная JVM (java virtual machine).
- Высокая производительность: JIT(Just In Time compiler) позволяет высокую производительность. JIT конвертирует байт-код в машинный код и потом JVM стартует выполнение.
- Мультипоточность: поток выполнения, известный как
Thread
. JVM создает thread, который называетсяmain thread
. Программист может создать несколько потоков наследованием от класса 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. Что такое переопределение (overriding) методов?
Переопределение методов происходит, когда child хочет изменить поведение parent класса. Если нужно, чтоб выполнилось-таки то, что есть в методе parent, можно использовать в child конструкцию вида super.methodName(), что выполнит работу parent метода, а уже потом добавить логику. Требования, которые нужно соблюдать:
- сигнатура метода должна быть такая же;
- возвращаемое значение должно быть таким же.
11. Что такое сигнатура метода?
Сигнатура метода — это набор из названия метода и аргументов, какие принимает метод. Сигнатура метода является уникальным идентификатором для метода при перегрузке методов.
12. Что такое перегрузка методов?
Перегрузка методов — это свойство полиморфизма, в котором при помощи изменения сигнатуры метода можно создать разные методы для одних действий:
- одно и то же имя метода;
- разные аргументы;
- может быть разный возвращаемый тип.
Например, один и тот же add()
из ArrayList
может быть перегружен следующим образом и будет выполнять добавление разным способом, в зависимости от входящих аргументов:
add(Object o)
— просто добавляет объект;add(int index, Object o)
— добавляет объект в определенный индекс;add(Collection<Object> c)
— добавляет список объектов;add(int index, Collection<Object> c)
— добавляет список объектов, начиная с определенного индекса.
13. Что такое Interface?
Множественное наследование не реализовано в джаве, поэтому чтобы преодолеть эту проблему, были добавлены интерфейсы в том виде, в котором мы их знаем 😉 Долгое время у интерфейсов были только методы без их реализации. В рамках этого ответа поговорим именно о них. Например:
public interface Animal {
void makeSound();
void eat();
void sleep();
}
Из этого вытекают некоторые нюансы:
- все методы в интерфейсе — публичные и абстрактные;
- все переменные — 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, с другой стороны, нужно было оставить то, чем славится джава — обратную совместимость. Для этого нужно было ввести уже готовые решения в интерфейсы. Так к нам и пришли дефолтные методы. То есть, дефолтный метод — это реализованный метод в интерфейсе, у которого есть ключевое слово 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”);
}
}
И есть класс, который реализует эти два интерфейса. Но только как выбрать специфический метод интерфейса А или В? Для этого есть конструкция такого вида: A.super.foo()
:
public class C implements A, B {
public void fooA() {
A.super.foo();
}
public void fooB() {
B.super.foo();
}
}
Таким образом, метод fooA()
будет использовать дефолтный метод foo()
из интерфейса A
, а метод fooB()
, соответственно, метод foo()
из интерфейса B
.
16. Что такое абстрактные методы и классы?
В джава есть зарезервированное слово abstract
, которое используется для обозначения абстрактных классов и методов. Для начала — определения. Абстрактным методом называется метод, который создан без реализации с ключевым словом abstract
в абстрактном классе. То есть, это метод как в интерфейсе, только с добавкой ключевого слова, например:
public abstract void foo();
Абстрактным классом называется класс, который имеет также abstract
слово:
public abstract class A {
}
У абстрактного класса есть несколько особенностей:
- на его основе нельзя создать объект;
- он может иметь абстрактные методы;
- он может и не иметь абстрактные методы.
Абстрактные классы нужны для обобщения какой-то абстракции (сорян за тавтологию), которой в реальной жизни нет, но она содержит множество общих поведений и состояний (то есть, методов и переменных). Примеров из жизни — хоть отбавляй. Всё вокруг нас. Это может быть “животное”, “машина”, “геометрическая фигура” и так далее.
17. Какая разница между String, String Builder и String Buffer?
Значения String
хранятся в пуле стрингов (constant string pool). Как только будет создана строка, она появится в этом пуле. И удалить ее будет нельзя. Например:
String name = "book";
…переменная будет ссылаться на стринг пул Constant string pool
Если задать переменной name другое значение, получится следующее:
name = "pen";
Constant string pool
Таким образом, эти два значения так и останутся там. String Buffer:
- значения
String
хранятся в стеке(Stack). Если значение изменено, значит новое значение будет заменено на старое; String Buffer
синхронизирован, и поэтому он потокобезопасный;- из-за потокобезопасности скорость работы оставляет желать лучшего.
Пример:
StringBuffer name = “book”;
Как только значение name сменится, в стеке измениться значение:
StringBuilder Точно такой же, как и StringBuffer
, только он не потокобезопасный. Поэтому скорость его явно выше, чем в StringBuffer
.
18. Какая разница между абстрактным классом и интерфейсом?
Абстрактный класс:
- абстрактные классы имеют дефолтный конструктор; он вызывается каждый раз, когда создается потомок этого абстрактного класса;
- содержит как абстрактные методы, так и не абстрактные. По большому счету может и не содержать абстрактных методов, но все равно быть абстрактным классом;
- класс, который наследуется от абстрактного, должен реализовать только абстрактные методы;
- абстрактный класс может содержать Instance Variable(смотри вопрос №5).
Интерфейс:
- не имеет никакого конструктора и не может быть инициализирован;
- только абстрактные методы должны быть добавлены (не считая default methods);
- классы, реализующие интерфейс, должны реализовать все методы (не считая default methods);
- интерфейсы могут содержать только константы.
19. Почему доступ по элементу в массиве происходит за O(1)?
Это вопрос буквально с последнего собеседования. Как я узнал позже, это вопрос задается для того, чтобы увидеть, как человек мыслит. Ясно, что практического смысла в этих знаниях немного: хватает только лишь знания этого факта. Для начала нужно уточнить, что O(1) — это обозначение временной сложности алгоритма, когда операция проходит за константное время. То есть это обозначение самого быстрого выполнения. Чтобы ответить на этот вопрос, нужно понять, что мы знаем о массивах? Чтоб создать массив int
, мы должны написать следующее:
int[] intArray = new int[100];
Из этой записи можно сделать несколько выводов:
- При создании массива известен его тип.Если известен тип, то понятно, какого размера будет каждая ячейка массива.
- Известно, какого размера будет массив.
Из этого следует: чтобы понять, в какую ячейку записать, нужно просто вычислить, в какую область памяти записать. Для машины это проще простого. У машины есть начало выделенной памяти, количество элементов и размер одной ячейки. Из этого понятно, что место для записи будет равно начальному месту массива + размер ячейки, умноженный на ее размер.
А как получается О(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(); //и после JDK 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 переменной, кажется, что это переменные константы. Но это только на первый взгляд. Если тип данных, на который ссылается переменная — immutable
, то да, это константа. А если тип данных mutable
, то есть изменяемый, при помощи методов и переменных можно будет изменить значение объекта, на который ссылается final
переменная, и в таком случае назвать ее константой нельзя. Так вот, на примере видно, что часть финальных переменных действительно константы, а часть — нет, и их можно изменить.
public class FinalExample {
//неизменяемые финальные переменные:
final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
final static Integer FINAL_EXAMPLE_COUNT = 10;
//изменяемые фильнаные переменные
final List<String> addresses = new ArrayList();
final StringBuilder finalStringBuilder = new StringBuilder("constant?");
}
Local final переменные
Когда final
переменная создается внутри метода, ее называют local 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
каждый раз создается новая переменная. Только это все не относится к нормальному циклу for, поэтому приведенный ниже код выдаст ошибку времени компиляции.
// final local переиенная j не может быть назначена
for (final int i = 0; i < args.length; i ++) {
System.out.println(args[i]);
}
Final класс
Нельзя расширять класс, объявленный как final
. Проще говоря, никакой класс не может наследоваться от данного. Прекрасным примером final
класса в JDK является String
. Первый шаг к созданию неизменяемого класса — пометить его как final
, и тогда нельзя будет его расширить:
public final class FinalExample {
}
// Здесь будет ошибка компиляции
class WantsToInheritFinalClass extends FinalExample {
}
Final методы
Когда метод маркирован как final, его называют final метод (логично, правда?). Final метод нельзя переопределять у класса наследника. К слову, методы в классе Object — wait() и notify() — это 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 переменные для объектов, когда вы не хотите, чтобы они были изменены. Например, специфичные для объекта свойства, которые мы можем использовать для целей логирования;
- если не нужно, чтобы класс был расширен, отметить его как окончательный;
- если нужно создать immutable< класс, нужно сделать его финальным;
- если нужно, чтоб реализация метода не менялась в наследниках, обозначить метод как
final
. Это очень важно, чтобы быть уверенным, что реализация не изменится.
23. Что такое mutable immutable?
Mutable
Mutable называются объекты, чьи состояния и переменные можно изменить после создания. Например такие классы, как StringBuilder, StringBuffer. Пример:
public class MutableExample {
private String address;
public MutableExample(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
// этот сеттер может изменить поле name
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());
// обновляем поле name, значит это mutable объект
obj.setAddress("Updated address");
System.out.println(obj.getAddress());
}
}
Immutable
Immutable называются объекты, состояния и переменные которых нельзя изменить после создания объекта. Чем не отличный ключ для HashMap, да?) Например, String, Integer, Double и так далее. Пример:
// сделаем этот класс финальным, чтобы никто не мог его изменить
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.setName("new address");
// System.out.println(obj.getName());
}
}
24. Как написать immutable класс?
После того, как выясните, что такое mutable и immutable объекты, следующий вопрос будет закономерный — как написать его? Чтоб написать immutable неизменяемый класс, нужно следовать простым пунктам:
- сделать класс финальным.
- сделать все поля приватными и создать только геттеры к ним. Сеттеры, разумеется, не нужно.
- Сделать все mutable поля final, чтобы установить значение можно было только один раз.
- инициализировать все поля через конструктор, выполняя глубокое копирование (то есть, копируя и сам объект, и его переменные, и переменные переменных, и так далее)
- клонировать объекты 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;
}
}