Готовимся к Oracle Certified Java 8 Programmer

Всем привет! В этом году Oracle зарезилил свои экзамены по Java 8 — для сдачи стали доступны Associate (1Z0-808) и Professional (1Z0-809). В этой статье я хочу поделиться опытом прохождения новых экзаменов и подробнее раскрыть встречающиеся темы из восьмой версии. Большая часть будет посвящена Professional, так как он наиболее интересен. Также я не буду поднимать философские вопросы о том, надо ли это вообще — поговорим о технической стороне и о том, как подготовиться к сертификации.

О процедуре заказа уже написано много статей, подробно останавливаться на этом месте смысла не вижу. Регистрируемся на PearsonVUE и Oracle CertView, связываем аккаунты, заказываем, оплачиваем и идем сдавать. Сертификационных центров в Киеве хватает (около десятка), и расписание очень гибкое.

Есть приятный бонус. В этом году Java празднует свое 20-летие, и поэтому во всем мире до конца 2015 года действует скидка 20% на все Java-экзамены. Просто введите промокод «Java20» при оплате на PearsonVUE. Судя по всему, есть возможность заказывать со скидкой на январь 2016.

Oracle Certified Associate (1Z0-808)

Associate — это начальный уровень. Здесь проверяют базовые знания языка. На странице экзамена доступен список тем.

Также можно ознакомиться со списком отличий 7-й и 8-й версий. Если сказать в целом, экзамен по 8 — это такой экзамен по 7, где вас дополнительно спросят о лямбдах и new Date and Time API.

Что есть для подготовки:
— Первая часть книги 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. Список тем лежит на странице экзамена. Есть и список отличий 7-й и 8-й версий.

Экзамен появился лишь в августе. В связи с этим есть нюансы — study guide’ы на момент написания статьи отсутствуют. Ближайший релиз ожидается 21-го декабря: OCP: Java SE 8 Programmer II Study Guide by Jeanne Boyarsky & Scott Selikoff.

Как готовиться:
— Материалы по 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 — около 15-ти;
— При подготовке посвящайте больше времени практике — это даст куда больше зазубривания книг. Открывайте IDE и пробуйте любые непонятные вещи.

Удачи!

Похожие статьи:
DOU продовжує серію коротких Zoom-інтерв’ю з керівниками українських IT-компаній, щоби дізнатися, як сьогодні почувається IT-ринок,...
  Две тайваньские компании Asus и Aser все еще продолжают бороться с трудностями, мешающими им занять свою нишу на мировом рынке...
Как мы открывали IT школу, часть 5. Проблемы золотоискателей и успехи девушек-моделей [Об авторе: Дима Малеев — работает...
Особенность и уникальность тренинга в том, что он выходит за рамки какой-то одной профессии, он посвящен налаживанию...
Початок повномасштабної війни став важким випробуванням для «Нової пошти». Багато відділень і співробітників...
Яндекс.Метрика