понедельник, 15 февраля 2016 г.

Как работают грейдеры наподобиe Codility, Hackerrank. Демистификация

Думаю вам не раз на этапе собеседования приходилось проходить онлайн тест на одном из популярных ресурсов: Codility, Hackerrank.

Методика простая - ознакомившись с условием задачи, вписать свой решение в соответствующей textarea, предварительно выбрав язык. И если вы выбираете javascript, то ваш код обработается сразу браузером.

A как обрабатывается ваш java код? Это нужно сделать на стороне сервера. Средствами javax.tools.

Идея динамической компиляции не нова и используется в Java EE, когда наши jsp - страницы компилируются в сервлеты и загружаеются в сервлет-контейнер. Но писать этот код самому не приходится.

Легким гуглением я нашел пример динамической компиляции "в лоб", который немного приоткрыл завесу тайны. Здесь уже видны основные принципы и API, с которым нужно работать.


        DiagnosticCollector diagnostics = new DiagnosticCollector();
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);

        // This sets up the class path that the compiler will use.
        // I've added the .jar file that contains the DoStuff interface within in it...
        List optionList = new ArrayList();
        optionList.add("-classpath");
        optionList.add(System.getProperty("java.class.path") + ";dist/InlineCompiler.jar");

        Iterable compilationUnit
                = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(helloWorldJava));
        JavaCompiler.CompilationTask task = compiler.getTask(
            null, 
            fileManager, 
            diagnostics, 
            optionList, 
            null, 
            compilationUnit);

  • Предупреждения и ошибки - они у нас помещаются в DiagnosticCollector diagnostics
  • StandardJavaFileManager - файловый менеджер, берем "стандарт" из compiler.getStandardFileManager(diagnostics, null, null)
  • compilationUnit - набор файлов для компиляции, которые нам предоставляет наш файловый менеджер(в примере файл мы сами записали предварительно через io)
  • optionList co всеми любимым classpath, но можно передавать любые параметры, в том числе версию java
  • JavaCompiler, который мы получаем из ToolProvider.getSystemJavaCompiler(), скармливаем ему все перечисленное выше - получаем JavaCompiler.CompilationTask, вызываем call() для компиляции и получаем скомпилированный класс либо же ошики в DiagnosticCollector

Не очень изящный, но вполне рабочий пример. Из недостатков, мы физически создаем *.java файл, и если там код, который нескомпилируется, то у нас будут проблемы с компиляцией всего приложения пока мы этот файл опять таки физически не удалим

Потом я нашел более гибкое решение. Отличная статья.

Самый главный момент здесь - это имплементация FileManagerImpl расширяющая ForwardingJavaFileManager. FileManagerImpl отвечает за привязку имен классов к их исходному либо уже скомпилированному коду. Делается это таким образом - используется кастомная имплементация SimpleJavaFileObject - JavaFileObjectImpl. В JavaFileObjectImpl находится ресурс - исходник или байткод и помечается это флажком Kind.SOURCE или же Kind.CLASS.


А теперь давайте рассмотрим конкретную ситуацию

Есть задача: мы получаем число и нужно его перевести в двоичный вид и найти наибольшее колиство 0 между 1. Нам показан пустой пример, куда нужно вписать имплементацию:


class SolutionImpl implements Solution {
    public int solution(int N) {
        // write your code in Java SE 8
    }
}

Сделаем следующие поправки, для упрощения. Программист, решая задачу не использует дополнительных библиотек. И таким образом наши входные данные - это только тело метода.

Пример тела метода:


String code = "    String binary = Integer.toBinaryString(N);\n"
                + "        int max = 0;\n"
                + "        int temp = 0;\n"
                + "        for (char c : binary.toCharArray()) {\n"
                + "            if (c == '0') {\n"
                + "                temp++;\n"
                + "\n"
                + "            } else {\n"
                + "                max = Math.max(max, temp);\n"
                + "                temp = 0;\n"
                + "            }\n"
                + "        }\n"
                + "        return max;\n";

На нашей стороне в свою очередь уже имеется интерфейс Solution, шаблон будущего кода класса, куда подставляется тело метода (можно также пакет и имя класса).


package $packageName;

public class $className
       implements com.getman.grader.Solution {

   public int solution(int N) {
      $expression
   }
}

    private String fillTemplate(String packageName, String className, String expression)
            throws IOException {
        if (template == null)
            template = readTemplate();
        // simplest "template processor":
        String source = template.replace("$packageName", packageName)//
                .replace("$className", className)//
                .replace("$expression", expression);
        return source;
    }

Далее наш String отправляется на компиляцию, если проходит успешно мы получаем Class<Solution>, вызываем newInstance() и отправляем экземляр нашего класса на тестирование и получаем результаты. В своем примере я вывожу их просто на экран


0 -> correct
1 -> correct
2 -> correct
1041 -> correct
601 -> correct
600 -> correct

Теперь работа грейдером для меня более понятна. Если есть вопросы - оставляйте комментарии, с радостью отвечу :) Рабочий пример можно скачать здесь

.

1 комментарий: