Хабр, привет! Меня зовут Даниил Пилипенко, я программный директор факультета 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)


  1. cartonworld
    20.09.2022 13:38
    +6

    Верным ответом на такую задачу будет запрос:

    Зачем JOIN, если d.id IS NULL? ????‍???? Так себе верность


    1. p-oleg
      20.09.2022 17:31

      Тоже не понял зачем тут JOIN

      не привязанных к отделам

      достаточно запроса к таблице employee: ... Where department_id is null and ...


      1. Chronas
        21.09.2022 05:51

        Подозреваю, что смысл тут в возможном отсутствии в department записи соответсвующей employee.department_id.
        Отдел пропал, а сотрудник остался )


        1. aleksandy
          21.09.2022 09:03
          +2

          Тогда exists(), но вообще такого не должна допускать сама СУБД, т.к. внешний ключ. А если ключа нет, то проектировщик схемы даже не джун.


    1. LaRN
      21.09.2022 09:48

      Вероятно тут попытка определить существует ли в таблице отделов отдел с идентификатором, который храниться в поле department_id таблицы сотрудников.

      А в этом поле может быть или NULL, или 0, или ещё какое-то значение, которые показывает что у сотрудника нет отдела.

      Поэтому самое надёжное чекнуть таблицу отделов.


      1. p-oleg
        21.09.2022 09:53

        Выше уже написали. Внешний ключ не даст вставить id несуществующего отдела.


        1. LaRN
          21.09.2022 12:23

          Ну тут же вопрос на знание Sql, да и кроме этого внешние ключи есть не всегда. В некоторых случаях для оптимизации их могут не добавлять.


    1. awk795
      22.09.2022 00:44

      Критикуя - предлагай. Как еще найти не привязанных к департаментам? e.id not in (select id from department)? Сомнительное решение..


      1. valery1707
        22.09.2022 09:37

        1. Проверка e.id not in (select id from department) не просто "сомнительна" она в принципе не корректна, так как select id from department выберет идентификаторы департаментов, тогда как e.id это идентификаторы сотрудников и они не связаны между собой и в том числе могут и пересекаться

        2. В комментариях уже предложили решение: e.department_id is null. Правда оно рассчитано на то что структура БД корректна и e.department_id имеет FK на таблицу department


        1. awk795
          22.09.2022 09:47

          1. Да я ошибся не e.id, а e.department_id .

          2. А если department_id  = 0?


          1. valery1707
            22.09.2022 11:02

            Если в БД существует нормальный FK, то в e.department_id не будет ерунды, в том числе нуля и других значений которых нет в таблице department.
            Если же FK нет (что как раз и позволяет хранить в e.department_id значения навроде 0) тогда JOIN будет верятно всё же более адекватным вариантом чем проверки на уровне записей.

            Я считаю что изначально нужно рассчитывать на FK, если другое не сказано явно и уже после уточнения вносить правки в запрос на основе новых сведений, а додумывать что-то дело не благодарное - иначе можно дойти до того что "не привязанных к отделам" можно интепретировать как "работник может быть приязан к нескольким отделам", а значит в department_id будет лежать список id отделов и это нужно учитывать.

            P.S.
            И это тоже обсуждалось.


  1. JordanCpp
    20.09.2022 13:50
    +1

    Мне одному показалось, что мужик на главной картинке, одет как юнит из strongholda?:)


    1. Redrik05
      20.09.2022 13:57
      +1

      Это пегий дудочник)) Странно, что вы не в курсе. Сериал-то прикольный


      1. JordanCpp
        20.09.2022 14:14

        Не смотрел сериал, надо будет исправить это упущение.


  1. WASD1
    20.09.2022 15:30
    +4

    Но вторая задача же по объёму больше всех остальных вместе взятых.

    В этом случае её следует вынести в отдельное design-interview (ну или в отдельную дизайн-сессию интервью).


  1. Semenych
    20.09.2022 16:45

    блин, вторую задачу я так наизусть без IDE и доки по спрингу решать реально не просто. Есть ряд вещей которые вот так на поверхности памяти не лежат


  1. s_f1
    20.09.2022 17:16

    А зачем в третьей задаче джойнить department, поясните?


  1. 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;
        }
    }


    1. s_f1
      20.09.2022 18:42

      Чтобы вывести числа, кратные 3, цикл должен быть с шагом 3, а тут, судя по первому условию .filter (в Java я не силён, поправьте, если не так) шаг цикла 1. Вместо деления вторым условием можно смотреть последнюю цифру числа, раз уж вы её потом всё равно вычисляете.


      1. Germanjon
        21.09.2022 07:20

        Чтобы избавиться от "дорогих" if-ов, можно сделать массив [3,6,9,12] и в цикле прибавлять его к 15*Х


    1. 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)


      1. valery1707
        21.09.2022 19:21

        Данное решение

        • в дальнейшем нужно будет поддерживать

        • Sonar всё равно возмутится хардкоду значений в коде - и нужно будет выносить константы

        • развёрнутое вычисление суммы цифр это быстро, но работает только для 1000 - если максимум увеличится в размере всего на пару цифр, то развёрнутая формула вычисления суммы станет совсем не читаемой

        • 1000 не делится на 3 и должно отсутствовать в выводе, а оно присутствует

        Это, конечно, не повод использовать стримы, но и пропускать такой код через ревью я бы не стал.


        1. pin2t
          22.09.2022 16:04

          Вы придумали несуществующие требования

          • Это решение ненадо поддерживать, это просто тестовое задание на собеседовании. Его никто не будет ревьюить, его выкинут сразу же после окончания собеседования

          • Никто не будет запускать Sonar на этом коде

          • Решение работает для конкретного заданного случая и заданного диапазона, естественно не является универсальным

          • Да, с 1000 я облажался :-(. Тут согласен.


          1. valery1707
            22.09.2022 18:51

            Вы придумали несуществующие требования

            Выдержка из постановки к задаче 1:

            Работодателю здесь важно понять, как соискатель владеет основами синтаксиса языка и может ли писать код сразу чисто.
            С помощью задач такого типа мы проверяем уровень соискателя: как много он писал код сам, а также косвенно — сталкивался ли с «грязным» кодом и рефакторил ли его. По такому заданию можно косвенно увидеть, как быстро и «чисто» (понятно и поддерживаемо) специалист будет писать собственный код.

            Из этого следует что код должен быть достаточно "опрятным" и "поддерживаемым".
            Да, Sonar тут не будет прогоняться и код, действительно, будет "выброшен", но именно по этому коду идёт оценка того что кандидат будет "творить" в дальнейшем - можно писать "лишь бы работало", но это явно не покажет кандидата с лучшей стороны.

            Да и вы сами пишете:

            Результат такой же, но гораздо проще для понимания и поддержки

            , хотя проверка суммы цифр явно таковой не является даже в текущем варианте (суммирующем всего 3 позиции) и только ухудшится при изменении условий (что часто происходит с таким кодом на собесах - типа "а что будет если диапазон будет не до 1000, а до 20000?" - см. упоминание рефакторинга).

            Решение работает для конкретного заданного случая и заданного диапазона, естественно не является универсальным

            Вычисление суммы цифр легко превращается в функцию, которая

            1. работает для любых значений

            2. повышает читаемость и поддерживаемость

            3. не снижает производительности

            4. повышает тестируемость и модульность


  1. Pro-invader
    20.09.2022 19:38

    Во 2-м примере метод returnNumber вообще не вызывается


  1. igolikov
    20.09.2022 23:58

    Во втором примере вы забыли код дописать


  1. igolikov
    21.09.2022 00:23
    +1

    public 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;
        }
    }


    1. Germanjon
      21.09.2022 07:25

      В первой задаче не проверяется условие "сумма цифр меньше десяти"


      1. igolikov
        21.09.2022 11:50

        написать код, который выводит числа от 0 до 1000, которые делятся на 3, но не делятся на 5, и сумма цифр в которых меньше десяти

        все 3 условия проверяются


  1. stanislavskijvlad
    21.09.2022 11:42

    один товарищ изложил свою версию, чем джун отличается от мидла. первый напишет fizzbuzz через if-ы или case. а опытный кодер знает, что проект может меняться. и вместо фиксированного количества множителей мы напишем рекурсивную функцию, которая принимает от нуля аргументов и далее. поэтому шаг в цикле точно будет 1. пишу по памяти, сам не советчик здесь ) Кажется, смысл в том, чтоб программа была легко изменяемой через классы и интерфейсы.


    1. WASD1
      21.09.2022 14:10
      +1

      После знакомства с ФП понимаешь, что здесь лучше всего вернуть filter (map(list)).
      И потом делай с ним всё что хочешь.


  1. 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);
    


  1. 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;
    }
    

    }


    1. aleksandy
      22.09.2022 13:01

      assertThat(sumFigure(102)).isEqualTo(3);

      Код невалидный, падает на первом же трёхзначном числе, делящемся на 3.