Хабр, привет! Меня зовут Даниил Пилипенко, я программный директор факультета backend-разработки направления «Программирование» Skillbox, директор центра подбора IT-специалистов SymbioWay. Сегодня разберём практические задачи, с которыми могут столкнуться Java-разработчики junior-уровня на собеседовании. Такого рода задачи мы часто видим на собеседованиях наших клиентов и коллег, а также сами даём соискателям.
Чего хочет работодатель
В процессе подбора кандидатов работодатель хочет понять, насколько ему подходит тот или иной соискатель — насколько он комфортен в общении и работе, обладает необходимым опытом и, что самое важное, техническими навыками для предстоящей работы. Ранее я рассказывал о пяти навыках, которые часто проверяются при приёме на работу у начинающих Java-разработчиков. А в этой статье я приведу примеры конкретных задач, которые могут давать на собеседованиях, и разберу, на что смотрит работодатель при их решении.
Примеры задач
Задача 1. Написать код, выполняющий какую-то несложную задачу. Здесь может быть, например, классический FizzBuzz, задача на сжатие или переворачивание строки. Работодателю здесь важно понять, как соискатель владеет основами синтаксиса языка и может ли писать код сразу чисто. Удивительно, но многие кандидаты испытывают значительные сложности при выполнении задач такого рода.
Одна из задач, которую мы даём соискателям: написать код, который выводит числа от 0 до 1000, которые делятся на 3, но не делятся на 5, и сумма цифр в которых меньше десяти. Задача часто вызывает совершенно немыслимые трудности у людей, утверждающих, что их уровень middle или даже senior. Вот несколько примеров решений такой задачи кандидатами на собеседованиях:
//Пример 1
public class TestClass {
public static final int MAX_LIMIT = 1000;
public static void main(String[] args) {
for(int i = 0; i < MAX_LIMIT; i++) {
boolean enabled = true;
if (i % 3 != 0) {
enabled = false;
}
if (i % 5 == 0) {
enabled = false;
}
if (!testSum(i)) {
enabled = false;
}
if (enabled == true) {
System.out.println("Число: " + i);
}
}
}
public static boolean testSum(int in) {
int res = 0;
while (in > 0) {
res += in % 10;
in = in / 10;
}
return res < 10;
}
}
//Пример 2
public class Test {
public static void main(String[] args) {
for (int i = 0; i<1000; i ++) {
if ( i%3 == 0 && i%5!= 0) {
}
}
}
public int returnNumber( int i) {
int t;
if (i >0){
returnNumber(i/10);
}
}
}
//Пример 3
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class TestTask {
public static List<Integer> getForNumber() {
List<Integer> list = IntStream.range(0, 1000).boxed()
.filter(n -> n % 3 == 0 && n % 5 != 0)
.collect(Collectors.toList());
return list;
}
public static int sumOfNubers(List<Integer> list) {
for (Integer n : list) {
int summ;
while(n>1) {
summ += n % 10;
n /= 10;
}
}
return 0;
}
}
Напишите нам в комментариях, как вы оцениваете каждое из этих решений и почему. Какое из них считаете лучшим, а какое худшим. Сразу отмечу, что решение, которое мы в SymbioWay считаем идеальным, здесь не приводим. Можете также попробовать написать его в комментариях.
С помощью задач такого типа мы проверяем уровень соискателя: как много он писал код сам, а также косвенно — сталкивался ли с «грязным» кодом и рефакторил ли его. По такому заданию можно косвенно увидеть, как быстро и «чисто» (понятно и поддерживаемо) специалист будет писать собственный код.
Задача 2. Создать веб-приложение на фреймворке Spring, которое будет соответствовать определённой спецификации. Спецификация может быть дана в текстовом виде или в виде отдельного Swagger-файла. Например, реализовать backend для анонимного онлайн-чата со следующей спецификацией:
GET /init — запрос инициализации, который по ID сессии отвечает, авторизован ли текущий пользователь или нет.
Запрос без параметров
-
Формат ответа:
{ "result": true }
POST /auth — запрос, регистрирующий и авторизующий пользователя. Он добавляет пользователя в базу данных и привязывает к текущей сессии.
Формат запроса:
name — имя пользователя
sex — пол пользователя, M или F
-
Формат ответа:
{ "result": true }
GET /users — запрос, возвращающий список пользователей в чате в порядке от самого свежего, который зарегистрировался недавно, до самого старого.
Запрос без параметров
-
Формат ответа:
{ "result": true, "data": [ { "id": 567, "name": "Alex Kurnikov", "male": "M" }, ... ] }
GET /messages — запрос, возвращающий ленту сообщений чата от самого нового до самого старого с постраничной навигацией.
Формат запроса:
offset — сдвиг от самого последнего до самого раннего, от 0, по умолчанию равен 0
limit — количество сообщений, которые нужно вывести, по умолчанию — 100
-
Формат ответа:
{ "result": true, "count": 100, "data": [ { "id": 46273, "time": "18:30 30.01.2022", "authorId": 567, "message": "Some HTML text, may me <b>tagged</b>" }, ... ] }
POST /messages — запрос, создающий новое сообщение от имени текущего пользователя. Форматы запроса и ответа спроектируйте самостоятельно на ваше усмотрение на основе имеющейся документации.
С помощью таких задач мы и работодатели проверяют сразу несколько навыков:
умение правильно проектировать и создавать веб-приложения на фреймворке Spring в соответствии с принятой структурой;
понимание принципов клиент-серверного взаимодействия (знание HTTP и стандарта REST);
умение работать с ORM-системой (обычно Hibernate) и базой данной, умение проектировать структуру этой базы;
умение писать код на Java и владение базовыми навыками при работе с этим языком программирования, в частности, знание ООП, умение работать со строками и коллекциями, лямбда-выражениями и Stream API, привычку их применять при необходимости.
Задача 3. Написать SQL-запрос. Например, задача может быть такой. У вас есть две таблицы:
employee — сотрудники компании
id
department_id
work_start_date
name
salary
department — отделы, в которых работают сотрудники
id
name
lead_id — ID руководителя отдела
Нужно написать такой SQL-запрос, который выведет всех сотрудников, работающих в компании с лета 2021 года, не привязанных к отделам и получающих зарплату меньше 100 000 рублей.
Верным ответом на такую задачу будет запрос:
SELECT
e.id,
e.name
FROM employee e
LEFT JOIN department d ON d.id = e.department_id
WHERE
e.work_start_date >= '2021-06-01' AND
e.salary < 100000 AND
d.id IS NULL
С помощью задач такого плана можно понять, насколько кандидат владеет языком запросов SQL, если это важно в данном проекте.
Вместо задач на написание SQL-запросов, могут предложить написать код с использованием Hibernate. Конечно, владение SQL — фундаментальный навык, который никогда не будет лишним. Но в некоторых проектах предпочитают писать не на чистом SQL.
Задача № 4. Решить заданную проблему устно. Например, рассказать, как предотвращать взаимные блокировки (deadlock) в многопоточных приложениях, или объяснить, как код, работающий с коллекцией, сделать потокобезопасным.
Здесь обычно требуется сначала рассказать о самой проблеме — в чём она состоит и почему её вообще следует решать, а затем объяснить, как именно это можно сделать. И чем понятнее будет ваш рассказ и чем логичнее будут ваши рассуждения, тем лучше.
При этом не обязательно решать задачу до конца идеально. Того, что вы думаете в правильном направлении, будет вполне достаточно для засчитывания ответа как верного. Ведь в реальности невозможно знать и помнить всё, гораздо важнее умение находить решения, понимать, куда «копать», и ставить эксперименты.
Задача № 5. Задача или серия вопросов на понимание принципов и паттернов ООП. Например, создать класс, объект которого может быть представлен только в единственном экземпляре (по сути, реализовать паттерн Singleton).
Тут могут не только попросить в итоге написать код для многопоточного режима работы, но и объяснить, зачем вообще такое может быть нужно, или рассказать, почему Singleton не используется во фреймворке Spring и чем он там фактически заменяется.
Мы сами сталкиваемся с тем, что на вопросы вроде «для чего в Java используется ключевое слово final» или «может ли статический метод переопределяться при наследовании» отвечают менее 20% кандидатов, по резюме имеющих уровень middle или выше.
Как интерпретируют результаты
Вы, возможно, будете удивлены, но во время решения практических задач работодатель не всегда ждёт полного и чёткого ответа по каждой из них. Он рассчитывает скорее на правильный вектор рассуждения и понимание той или иной темы.
Конечно, когда мы просим написать код, то кандидату лучше бы справиться с этой задачей. Так мы понимаем, что он действительно умеет писать код и делает это сразу чисто. Кроме того, здесь часто бывает важно ваше упорство: умение довести задачу до конца — важнейший навык, с которым у многих в наше время возникают проблемы.
Третье, что также важно увидеть в кандидате, — это понимание базовых, фундаментальных концепций. Если вы не знаете, что такое HTTP, как делать простейшие SQL-запросы, для чего вообще нужна многопоточность и что такое потокобезопасность, вам, скорее всего, откажут.
Когда мы подбираем специалистов уровня junior в нашу команду или кому-то из работодателей, мы проверяем три блока навыков:
Понимание базовых концепций (синтаксис, коллекции, чистота кода, ООП, создание приложений на Spring, HTTP, REST и SQL).
Умение быстро, самостоятельно и грамотно решать возникающие проблемы, в том числе находить нужную информацию.
Уровень обучаемости — скорость и качество запоминания и дальнейшего внедрения в свою работу best practices и рекомендаций, которые мы даём новичку.
После приёма на работу начинающего специалиста важно не перегрузить его слишком сложными задачами. Если он хорошо выполняет свою работу и близок к тому, чтобы начать скучать, то постепенно ему доверяют всё более сложные и интересные проекты, которые обеспечивают дальнейший рост.
Комментарии (34)
WASD1
20.09.2022 15:30+4Но вторая задача же по объёму больше всех остальных вместе взятых.
В этом случае её следует вынести в отдельное design-interview (ну или в отдельную дизайн-сессию интервью).
Semenych
20.09.2022 16:45блин, вторую задачу я так наизусть без IDE и доки по спрингу решать реально не просто. Есть ряд вещей которые вот так на поверхности памяти не лежат
Filex
20.09.2022 18:09+1Предложу такое решение 1й задачи.
import java.util.List; import java.util.stream.IntStream; public class Main { private static final int MIN = 0; private static final int MAX = 1000; private static final int GOOD_DIV = 3; private static final int BAD_DIV = 5; private static final int DIGIT_SUM_CONDITION = 10; public static void main(String[] args) { List<Integer> numList = IntStream.range(MIN, MAX).boxed() .filter(i -> (i % GOOD_DIV == 0 && i % BAD_DIV != 0 && checkSumDigits(i))) .toList(); System.out.println(numList); } private static boolean checkSumDigits(int num) { int val = 0; while (num > 0) { val += num % 10; num = num / 10; if (val >= DIGIT_SUM_CONDITION) { return false; } } return true; } }
s_f1
20.09.2022 18:42Чтобы вывести числа, кратные 3, цикл должен быть с шагом 3, а тут, судя по первому условию .filter (в Java я не силён, поправьте, если не так) шаг цикла 1. Вместо деления вторым условием можно смотреть последнюю цифру числа, раз уж вы её потом всё равно вычисляете.
Germanjon
21.09.2022 07:20Чтобы избавиться от "дорогих" if-ов, можно сделать массив [3,6,9,12] и в цикле прибавлять его к 15*Х
pin2t
21.09.2022 14:23-1Вот за это я и не люблю стримы, люди на ровном месте для решения простейшей задачи изобретают супер сложные конструкции без всякой причины. Проще надо быть
import static java.lang.System.out; public class Main { public static void main(String[] args) { for (int i = 0; i < 1000; i++) { if (i % 3 == 0 && i % 5 != 0 && (i / 100 + (i / 10) % 10 + i % 10) < 10) { out.println(i); } } out.print(1000); } }
Результат такой же, но гораздо проще для понимания и поддержки, потому что программист оперирует 6-ю словами всего (for, int, i, if, out, print)
вместо 16-ти (List, Integer, numList, IntStream, range, MIN, MAX, boxed, filter, i, GOOD_DIV, BAD_DIV, toList, System, out, print)
valery1707
21.09.2022 19:21Данное решение
в дальнейшем нужно будет поддерживать
Sonar
всё равно возмутится хардкоду значений в коде - и нужно будет выносить константыразвёрнутое вычисление суммы цифр это быстро, но работает только для
1000
- если максимум увеличится в размере всего на пару цифр, то развёрнутая формула вычисления суммы станет совсем не читаемой1000
не делится на3
и должно отсутствовать в выводе, а оно присутствует
Это, конечно, не повод использовать стримы, но и пропускать такой код через ревью я бы не стал.
pin2t
22.09.2022 16:04Вы придумали несуществующие требования
Это решение ненадо поддерживать, это просто тестовое задание на собеседовании. Его никто не будет ревьюить, его выкинут сразу же после окончания собеседования
Никто не будет запускать Sonar на этом коде
Решение работает для конкретного заданного случая и заданного диапазона, естественно не является универсальным
Да, с 1000 я облажался :-(. Тут согласен.
valery1707
22.09.2022 18:51Вы придумали несуществующие требования
Выдержка из постановки к задаче 1:
Работодателю здесь важно понять, как соискатель владеет основами синтаксиса языка и может ли писать код сразу чисто.
С помощью задач такого типа мы проверяем уровень соискателя: как много он писал код сам, а также косвенно — сталкивался ли с «грязным» кодом и рефакторил ли его. По такому заданию можно косвенно увидеть, как быстро и «чисто» (понятно и поддерживаемо) специалист будет писать собственный код.Из этого следует что код должен быть достаточно "опрятным" и "поддерживаемым".
Да,Sonar
тут не будет прогоняться и код, действительно, будет "выброшен", но именно по этому коду идёт оценка того что кандидат будет "творить" в дальнейшем - можно писать "лишь бы работало", но это явно не покажет кандидата с лучшей стороны.Да и вы сами пишете:
Результат такой же, но гораздо проще для понимания и поддержки
, хотя проверка суммы цифр явно таковой не является даже в текущем варианте (суммирующем всего 3 позиции) и только ухудшится при изменении условий (что часто происходит с таким кодом на собесах - типа "а что будет если диапазон будет не до
1000
, а до20000
?" - см. упоминание рефакторинга).Решение работает для конкретного заданного случая и заданного диапазона, естественно не является универсальным
Вычисление суммы цифр легко превращается в функцию, которая
работает для любых значений
повышает читаемость и поддерживаемость
не снижает производительности
повышает тестируемость и модульность
igolikov
21.09.2022 00:23+1public class FizzBuz1 { public static void main(String[] args) { for (int i = 3; i <= 1000; i += 3) { if (i % 5 != 0 && check(i)) { System.out.println(i); } } } private static boolean check(int i) { int r = 0; while (i > 0 && r < 10) { r += i % 10; i = i / 10; } return r < 10; } }
stanislavskijvlad
21.09.2022 11:42один товарищ изложил свою версию, чем джун отличается от мидла. первый напишет fizzbuzz через if-ы или case. а опытный кодер знает, что проект может меняться. и вместо фиксированного количества множителей мы напишем рекурсивную функцию, которая принимает от нуля аргументов и далее. поэтому шаг в цикле точно будет 1. пишу по памяти, сам не советчик здесь ) Кажется, смысл в том, чтоб программа была легко изменяемой через классы и интерфейсы.
WASD1
21.09.2022 14:10+1После знакомства с ФП понимаешь, что здесь лучше всего вернуть filter (map(list)).
И потом делай с ним всё что хочешь.
DenisPantushev
22.09.2022 07:58Чет какие-то сложные решения в комментариях. Предлагаю следующее:
IntStream.range(0, 1000) .filter( ((IntPredicate) e -> (e % 3) == 0) .and(e -> (e % 5) != 0) .and( e -> { int sd = Integer.valueOf(e) .toString() .chars() .map(e1 -> Character.getNumericValue(e1)) .reduce(Integer::sum) .orElse(0); return sd < 10; }) ) .forEach(System.out::println);
CooL-A-TiGeR
22.09.2022 10:39-1Предложу свой вариант первой задачи (если не создавать переменные по условиям задачи):
public class Task01 { /* * написать код, который выводит числа от 0 до 1000, которые делятся на 3, но не * делятся на 5, и сумма цифр в которых меньше десяти */ public static void main(String[] args) { for (int i = 0; i < 1000; i++) { if (i % 3 == 0 & i % 5 != 0 & sumFigure(i) < 10) { System.out.println(i); } } } static int sumFigure(int num) { return num % 10 + num / 10 + num / 100 + num / 1000; }
}
aleksandy
22.09.2022 13:01assertThat(sumFigure(102)).isEqualTo(3);
Код невалидный, падает на первом же трёхзначном числе, делящемся на 3.
cartonworld
Зачем JOIN, если d.id IS NULL? ???????? Так себе верность
p-oleg
Тоже не понял зачем тут JOIN
достаточно запроса к таблице employee: ... Where department_id is null and ...
Chronas
Подозреваю, что смысл тут в возможном отсутствии в department записи соответсвующей employee.department_id.
Отдел пропал, а сотрудник остался )
aleksandy
Тогда exists(), но вообще такого не должна допускать сама СУБД, т.к. внешний ключ. А если ключа нет, то проектировщик схемы даже не джун.
LaRN
Вероятно тут попытка определить существует ли в таблице отделов отдел с идентификатором, который храниться в поле department_id таблицы сотрудников.
А в этом поле может быть или NULL, или 0, или ещё какое-то значение, которые показывает что у сотрудника нет отдела.
Поэтому самое надёжное чекнуть таблицу отделов.
p-oleg
Выше уже написали. Внешний ключ не даст вставить id несуществующего отдела.
LaRN
Ну тут же вопрос на знание Sql, да и кроме этого внешние ключи есть не всегда. В некоторых случаях для оптимизации их могут не добавлять.
awk795
Критикуя - предлагай. Как еще найти не привязанных к департаментам? e.id not in (select id from department)? Сомнительное решение..
valery1707
Проверка
e.id not in (select id from department)
не просто "сомнительна" она в принципе не корректна, так какselect id from department
выберет идентификаторы департаментов, тогда какe.id
это идентификаторы сотрудников и они не связаны между собой и в том числе могут и пересекатьсяВ комментариях уже предложили решение:
e.department_id is null
. Правда оно рассчитано на то что структура БД корректна иe.department_id
имеетFK
на таблицуdepartment
awk795
Да я ошибся не e.id, а e.
department_id
.А если department_id = 0?
valery1707
Если в БД существует нормальный
FK
, то вe.department_id
не будет ерунды, в том числе нуля и других значений которых нет в таблицеdepartment
.Если же
FK
нет (что как раз и позволяет хранить вe.department_id
значения навроде0
) тогдаJOIN
будет верятно всё же более адекватным вариантом чем проверки на уровне записей.Я считаю что изначально нужно рассчитывать на
FK
, если другое не сказано явно и уже после уточнения вносить правки в запрос на основе новых сведений, а додумывать что-то дело не благодарное - иначе можно дойти до того что "не привязанных к отделам" можно интепретировать как "работник может быть приязан к нескольким отделам", а значит вdepartment_id
будет лежать списокid
отделов и это нужно учитывать.P.S.
И это тоже обсуждалось.