Готовимся к Oracle Certified Java 8 Programmer
Всем привет! В этом году Oracle зарезилил свои экзамены по Java 8 — для сдачи стали доступны Associate (1Z0-808) и Professional (1Z0-809). В этой статье я хочу поделиться опытом прохождения новых экзаменов и подробнее раскрыть встречающиеся темы из восьмой версии. Большая часть будет посвящена Professional, так как он наиболее интересен. Также я не буду поднимать философские вопросы о том, надо ли это вообще — поговорим о технической стороне и о том, как подготовиться к сертификации.
О процедуре заказа уже написано много статей, подробно останавливаться на этом месте смысла не вижу. Регистрируемся на PearsonVUE и Oracle CertView, связываем аккаунты, заказываем, оплачиваем и идем сдавать. Сертификационных центров в Киеве хватает (около десятка), и расписание очень гибкое.
Есть приятный бонус. В этом году Java празднует свое
Oracle Certified Associate (1Z0-808)
Associate — это начальный уровень. Здесь проверяют базовые знания языка. На странице экзамена доступен список тем.
Также можно ознакомиться со списком отличий
Что есть для подготовки:
— Первая часть книги OCA/OCP Java SE 7 Programmer I & II Study Guide by Kathy Sierra & Bert Bates;
— OCA: Java SE 8 Programmer I Study Guide by Jeanne Boyarsky & Scott Selikoff;
— Очень полезный материал Maurice Naftalin’s Lambda FAQ;
— Java 8 Date and Time.
В практических тестах можно потренироваться на Quizful или выбрать что-нибудь отсюда.
Date and Time
Oracle обожает новый API. Вас ожидают вопросы по основным сущностям пакета java.time.
По факту нужно помнить, что все основные классы являются immutable, и не попадаться на глупых вопросах:
LocalDate localDate = LocalDate.now();
localDate.plus(1, ChronoUnit.DAYS);
В данном случае c оригинальным объектом ничего не произойдет.
То же самое касается Period, ZonedDateTime и других. Никаких родственных связей у этих классов нет (но методы преобразования присутствуют).
LocalDateTime localDateTime = LocalDate.now(); //ошибка компиляции
Ничего суперсложного не будет, просто хорошо почитайте материал и потренируйтесь в IDE.
Лямбды
Здесь также будут базовые вопросы: что это такое, в чём фишка, замените лямбду анонимным классом, перепишите кусок кода с использованием лямбд и так далее. Стоит иметь представление о базовых интерфейсах пакета java.util.function (Consumer, Supplier, Function, Predicate, UnaryOperator).
Также будут вопросы о видимости переменных — в Java 8 появился термин effectively final variable (local variables referenced from a lambda expression must be final or effectively final). Пример:
List<String> list = new ArrayList<>(); list.add("Hi"); list.add("Toast"); list.add("Beer"); int count = 2; list.removeIf((String s) -> s.length() <= count); //1 list.forEach((s) -> System.out.println(s)); //2 if (list.size() == 2) { count = 5; //3 } else { System.out.println("Hello!"); }
В данном случае мы получаем ошибку компиляции в строке 1, так как переменная count изменяется в блоке if и перестает быть effectively final. Если убрать строку 3, всё будет окей. Обратите внимание, что изменение происходит после обращения, но компилятор отслеживает такие вещи. String s в строке 1 не имеет никакого отношения к s в строке 2 — это локальные имена аргументов и разные способы объявления.
Сам экзамен довольно прост. Просто будьте внимательны.
Oracle Certified Professional (1Z0-809)
Он же бывший SCJP. Список тем лежит на странице экзамена. Есть и список отличий
Экзамен появился лишь в августе. В связи с этим есть нюансы — study guide’ы на момент написания статьи отсутствуют. Ближайший релиз ожидается
Как готовиться:
— Материалы по OCA;
— Вторая часть OCA/OCP Java SE 7 Programmer I & II Study Guide by Kathy Sierra & Bert Bates;
— Java SE 8 for the Really Impatient by Cay Horstmann;
— Летом был замечательный курс Oracle JDK 8 Lambdas and Streams. Материалы доступны на YouTube;
— А если и этого мало — Java 8 Stream tutorial;
— Любые mock-тесты. Например, Enthuware 1Z0-809.
Что будет на экзамене? Для начала все классические темы, вопросы по которым «проапгрейджены» с использованием нового синтаксиса и приёмов. Ну и, конечно же, Java 8 (особенно Stream API). О новых темах, которые мне встретились, я и хочу написать ниже.
Optional
Одно из нововведений. Помимо собственной функциональности, активно используется Stream API (reduce, max, min, findAny, etc.). Где искать подвоха и что важно понимать? Optional призван избавить нас от NullPointerException. Так ли это на самом деле?
Optional<String> opt = Optional.of(null);
System.out.println(opt); //и получаем NPE :)
Для избавления существует метод ofNullable. Но и тут есть свой нюанс:
Optional<String> opt = Optional.ofNullable(null); //вернёт Optional.empty
System.out.println(opt.get()); //NoSuchElementException
Почему так было сделано и почему существует of и ofNullable — я нагуглил здесь.
Нельзя изменить контент Optional после создания. Любые манипуляции возвращают новый объект.
Optional<String> optional = Optional.empty(); if (optional.isPresent()) { System.out.println("Yes"); } else { optional.of("Java"); } System.out.println(optional.orElse("Unknown"));
Здесь будет выведено «Unknown» — optional.of("Java") вернул новый объект, но никуда не присвоил.
Interfaces / Functional Interfaces
Начнем с default/static методов. Есть хороший раздел из Maurice Naftalin’s Lambda FAQ. Со static следует помнить, что в отличии от статических методов класса, статические методы интерфейса не могут быть вызваны через ссылку на объект:
interface One { static void foo() {} } class Alpha implements One { } public static void main(String[] args) { One.foo(); //ок One obj = new Alpha(); obj.foo(); //ошибка компиляции }
Дефолтные методы можно переопределять, но нельзя переопределять статические методы дефолтными и обратно:
interface One { default void foo() { System.out.println("One"); } } interface Two extends One { default void foo() { System.out.println("Two"); } static void foo() { //ошибка компиляции System.out.println("Static Two"); } }
Не стоит пугаться количества функциональных интерфейсов в java.util.function. Основных там не много, и они достаточно хорошо описаны в литературе или документации. Все остальные — это специализации (Bi, Int, Double, Long, etc.). Если почитаете и потренируетесь в IDE, проблем возникнуть не должно.
Если быть невнимательным, можно попасться на чем-то таком:
Function<String> f = name::toUpperCase;
Данный код не скомпилируется, потому что Function<T, R> принимает аргумент и возвращаемый тип. Один аргумент может принимать специализация, например, IntFunction<R> (принимает int, возвращает R).
Еще один пример:
Stream<Double> stream = DoubleStream.of(1, 2, 3).boxed(); UnaryOperator<Integer> unaryOperator = x -> x * 2; stream.map(unaryOperator).forEach(System.out::println);
Получаем ошибку компиляции, потому что map ожидает UnaryOperator<Double>
. Но помимо нормального решения, её можно обойти некоторыми извращенными способами. Например, заменив вызов map(unaryOperator) на map(x -> unaryOperator.apply(Integer.valueOf(x.intValue()))) (не делайте так никогда).
Method / Constructor References
По ссылкам на методы в дополнение к основным материалам также хороший материал есть на Oracle docs.
Гораздо больше можно запутаться в ссылках на конструкторы. Базовое объяснение довольно простое:
Supplier<String> supplier = () -> new String();
превращается в
Supplier<String> supplier = String::new;
Ну и вызываем:
String str = supplier.get();
Другого синтаксиса, например, String():new, String::new("test«>) быть не может. Это вызовет ошибку компиляции. Но что, если в конструктор требуется передать аргументы? Supplier нам уже не подойдет, его метод T get() ничего не принимает.
Создаем свой (также можно воспользоваться Function):
interface SupplierWithArg<T, U> { T get(U arg); } SupplierWithArg<String, String> supplier = String::new; String str = supplier.get(“Java 8");
В данном случае синтаксис ссылки на конструктор никак не поменялся. Компилятор сам определил, какой конструктор класса String вызвать. В случае отсутствия подходящего конструктора, конечно же, будет ошибка компиляции.
А если аргумент параметризован? Например, у нас есть класс:
class Student { List<String> grades; public Student(List<String> grades) { this.grades = grades; } }
И функциональный интерфейс:
interface SupplierWithParamArg<T, U> { T get(List<U> arg); }
В данном случае Student::new не прокатит, компилятору нужно указать тип. Это можно сделать так:
List<String> grades = Arrays.asList("A", "B", “C"); SupplierWithParamArg<Student, String> supplier = Student::<String>new; Student student = supplier.get(grades);
Stream API
There are 95 methods in 23 classes that return a Stream
Many of them, though are intermediate operations in the Stream interface
71 methods in 15 classes can be used as practical Stream sources
(JDK 8 MOOC Lambdas and Streams)
Самое важное нововведение. Более половины вопросов будет именно об операциях со стримами. И также они будут фигурировать в вопросах на общие темы. Ключевым интерфейсом является Stream<T>
- он содержит практически все методы, которые будут упомянуты ниже.
Теперь о частых ошибках:
— Операции со стримами делятся на intermediate и terminal (в документации всегда можно увидеть, к какому типу относится метод);
— Для отработки стрима необходимы две вещи — source и terminal operation;
— Intermediate-операции «are lazy when possible». Они не выполняются, пока не потребуется результат.
Эти три пункта ведут к следующему:
List<StringBuilder> list = Arrays.asList(new StringBuilder("Java"), new StringBuilder(“Hello")); list.stream().map((x) -> x.append(" World”)); list.forEach(System.out::println);
Выведет:
Java
Hello
Не произошло абсолютно ничего, потому что map является intermediate-операцией, которая добавила преобразование и вернула новый стрим. Но без вызова terminal-операции мы просто «вяжем» свои вычисления до финального результата.
Стоит добавить любую terminal-операцию:
list.stream().map((x) -> x.append(" World”)).count(); // count возвращает кол-во элементов стрима.
и стрим отработает. Объекты листа будут изменены в:
Java World
Hello World
Стрим нельзя использовать повторно, если на нем отработала terminal-операция:
List<StringBuilder> list = Arrays.asList(new StringBuilder("Java"), new StringBuilder("Hello")); Stream<StringBuilder> stream = list.stream().map((x) -> x.append(" World")); long count = stream.count(); Object[] array = stream.toArray(); // java.lang.IllegalStateException
Обратите внимание, что метод close для интерфейса Stream не является terminal-операцией. Он идет от интерфейса AutoCloseable, который наследует BaseStream.
Специализированные версии стримов (DoubleStream, IntStream, LongStream) позволяют уйти от создания лишних объектов и autoboxing/unboxing. Мы работаем напрямую с примитивами. У интерфейса Stream есть соответствующие методы для преобразования — mapToXXX / flatMapToXXX. У специализированных версий метод boxed делает обратное — возвращает Stream<xxx>. Ещё у IntStream и LongStream есть интересные методы range и rangeClosed, генерирующие последовательность значений с шагом 1.
Интересную подборку частых ошибок при работе со стримами можно увидеть здесь.
Например, порядок операций и методы skip, limit:
IntStream.iterate(0, i -> i + 1) .limit(10) .skip(5) .forEach((x) -> System.out.print(x + " "));
Выведет: 5 6 7 8 9.
Меняем местами:
IntStream.iterate(0, i -> i + 1) .skip(5) .limit(10) .forEach((x) -> System.out.print(x + " "));
Получаем: 5 6 7 8 9 10 11 12 13 14.
Short-circuit operations
О чем стоит помнить на экзамене? Методы anyMatch, allMatch, noneMatch принимают Predicate и возвращают boolean. Также в названиях методов заложена механика их работы.
Stream<Integer> values = IntStream.rangeClosed(0, 10).boxed(); values.peek(System.out::println).anyMatch(x -> x == 5);
Будет выведена последовательность от 0 до 5. При x = 5 предикат вернет true, и стрим закончит работу.
Методы findFirst, findAny не принимают аргументов и возвращают Optional (потому что результата может и не быть).
C findAny не всё так просто:
Optional<Integer> result = IntStream.rangeClosed(10, 15) .boxed() .filter(x -> x > 12) .findAny(); System.out.println(result);
Казалось бы, данный код всегда выведет Optional с 13 внутри. Однако, findAny не гарантирует последовательности, он может выбрать любой элемент. Особенно это касается parallel-стримов (для производительности которых он был и создан). Для стабильного результата существует findFirst.
Reduction / Mutable Reduction
Самое простое по этой теме: методы min и max принимают Comparator и возвращают Optional.
За агрегирование результата отвечает reduce. Простейшая его версия принимает BinaryOperator (два аргумента — полученное на предыдущем шаге значение и текущее значение). Возвращает всегда одно значение для стрима, завернутое в Optional.
Например, max может быть заменен на:
.reduce((x, y) -> x > y ? x : y)
Или еще проще
.reduce(Integer::max)
Версия с корнем (identity) аккумулирует вычисление на основе типа корня:
T reduce(T identity, BinaryOperator<T> accumulator))
int sum = IntStream.range(0, 9).reduce(0, (x, y) -> x + y);
Mutable reduction позволяет не просто выдать результат, но и завернуть его в какой-нибудь контейнер (например, коллекцию). Для этого у стрима есть методы collect.
Стоит помнить, что collect в простейшей его версии принимает три аргумента:
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
Supplier — возвращает новые инстансы целевого контейнера на текущем шаге.
Accumulator — собирает элементы в него.
Combiner — сливает контейнеры воедино.
Пример collect для ArrayList в роли контейнера:
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
Также был добавлен класс Collectors с множеством уже реализованных удобных операций. Рассмотрите его методы и хорошенько поиграйтесь в IDE. Например, по groupingBy вопросов будет достаточно. Пример:
List<String> list = Arrays.asList("Dave", "Kathy", "Ed", "John", "Fred"); Map<Integer, Long> data = list.stream() .collect(Collectors.groupingBy(String::length, Collectors.counting())); System.out.println(data);
Напоминает GROUP BY из SQL. Метод группирует значения по длине строки и данная его версия возвращает Map. Получаем вывод: {2=1, 4=3, 5=1}.
Ещё есть интересный метод partitioningBy. Он организует элементы согласно предикату в Map<Boolean, List<T>>
:
Stream<Integer> values = IntStream.rangeClosed(0, 10).boxed(); Object obj = values.collect(Collectors.partitioningBy(x -> x % 2 == 0)); System.out.println(obj);
Вывод:{false=[1, 3, 5, 7, 9], true=[0, 2, 4, 6, 8, 10]}
Parallel streams
По этой теме вопросов откровенно мало. Не волнуйтесь, вас и так будут спрашивать по многопоточности и Fork/Join Framework. По последнему может начаться настоящий трeш и угар. От простых вопросов: «Что это такое? Преимущества?» и «RecursiveTask vs RecursiveAction» до огромных полотен кода с сортировками массивов.
Под капотом параллельных стримов как раз и работает ForkJoinPool.
Для начала — методы parallel / sequential являются intermediate-операциями. С их помощью можно определять тип операций. Стрим может переходить из sequential в parallel и обратно. По-дефолту Collection.stream возвращает sequential-стрим.
List<Integer> list = IntStream.range(0, 256).boxed().collect(Collectors.toList()); int sum = list.stream() .filter(x -> x > 253) .parallel() .map(x -> x + 1) .sequential() .reduce(Integer::sum).orElse(0);
forEachOrdered наряду с forEach существует не просто так.
Например:
IntStream.range(0, 9).parallel().forEach(System.out::println)
Будут выведены числа от 0 до 8 в непредсказуемом порядке. Метод forEachOrdered заставит вывести их в натуральном порядке. Но он не сортирует данные (if the stream has a defined encounter order © JavaDoc).
Еще нюанс — далеко не факт, что параллельный стрим всегда заставит вычисления обрабатываться в разных потоках:
List<String> values = Arrays.asList("a", "b"); String join = values.parallelStream() .reduce("-", (x, y) -> x.concat(y)); System.out.println(join);
Если тут случится распараллеливание, и результат будет обрабатываться в двух разных потоках, на выходе получим —a-b. Каждый элемент по отдельности сольется с корнем, а затем всё сольется воедино. Но этого может и не произойти, тогда на первом шаге получим -a, а финальным результатом будет -ab.
Collections
Наиболее видимые изменения: Iterable и Map получили forEach. Обратите внимание, что для Map он принимает BiConsumer (два аргумента для ключа и значения):
Map<Integer, String> map = new HashMap<>(); map.put(1, "Joe"); map.put(2, "Bill"); map.put(3, "Kathy"); map.forEach((x, y) -> System.out.println(x + " " + y));
Кстати, можно вполне себе выводить, например, только значения. Не обязательно использовать все аргументы в выражении — map.forEach((x, y) -> System.out.println(y));
Далее, Collection получил stream() и parallelStream().
Могут попасться теоретические вопросы на тему работы HashMap. HashMap, LinkedHashMap, ConcurrentHashMap ведут себя иначе в Java 8. Грубо говоря, когда с хэшами беда и количество элементов связного списка в корзине переваливает за определенное значение, то список превращается в сбалансированное дерево. Об этом можно почитать, например, здесь или здесь.
Date and Time
Здесь придется углубиться в функционал новых классов. Duration манипулирует датами в разрезе часов/минут/секунд. Period использует дни/месяцы/годы. Где будет видна эта разница больше всего? При смещении временных диапазонов и переходом на летнее/зимнее время.
Например, в этом году в Украине переход на зимнее время состоялся 25 октября в 4 часа ночи (на час назад):
LocalDateTime ldt = LocalDateTime.of(2015, Month.OCTOBER, 25, 3, 0); ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.of("EET")); zdt = zdt.plus(Duration.ofDays(1)); System.out.println(zdt); zdt = ZonedDateTime.of(ldt, ZoneId.of("EET")); zdt = zdt.plus(Period.ofDays(1)); System.out.println(zdt);
Данный код выведет:
2015-10-26T02:00+02:00[EET]
2015-10-26T03:00+02:00[EET]
Что произошло? Duration добавил к дате конкретные 24 часа (потому что длительность одного дня всегда 24 часа). При переходе получаем на час меньше. Period же добавил конкретный день, и локальное время сохранилось.
I/O, NIO 2
Касаемо общих вопросов, стоит хорошо ориентироваться в операциях Path — normalize, relativize, resolve. А также StandardOpenOption. Но об этом будет достаточно в рекомендуемой литературе.
Теперь о Java 8. Вас могут ожидать не очень сложные вопросы про чтение и обработку текстового файла. В Files и BufferedReader появился метод lines(), возвращающий стрим, который состоит из строк выбранного файла.
Например, вывести строки:
Files.lines(Paths.get(“file.txt”)).forEach(System.out::println);
Или — разбить на слова, убрать дубликаты, перевести в нижний регистр и отсортировать по длине:
try (BufferedReader reader = Files.newBufferedReader( Paths.get("file.txt"), StandardCharsets.UTF_8)) { List<String> list = reader.lines() .flatMap(line -> Stream.of(line.split("[- .:,]+”))) .map(String::toLowerCase) .distinct() .sorted((w1, w2) -> w1.length() - w2.length()) .collect(Collectors.toList()); }
Кроме этого, в классе Files стримы также используют методы list, find и walk.
Заключение
Надеюсь, я смог немного раскрыть встречающиеся темы. К сожалению, исключительно из личного экзаменационного опыта нельзя описать всё возможное.
Еще несколько общих советов:
— Не нервничайте:). Вопросы в целом адекватные и без извращений. У Oracle нет цели завалить вас кривыми тестами;
— Также стоит следить за временем. На оба экзамена дается 2,5 часа. Лично у меня на OCA еще оставалось минут 40 на перепроверку, а вот в случае с OCP — около
— При подготовке посвящайте больше времени практике — это даст куда больше зазубривания книг. Открывайте IDE и пробуйте любые непонятные вещи.
Удачи!