JavaRush /Java блог /Архив info.javarush /Особенности Java 8 – максимальное руководство (часть 1)
0xFF
9 уровень
Донецк

Особенности Java 8 – максимальное руководство (часть 1)

Статья из группы Архив info.javarush
Первая часть перевода статьи Java 8 Features – The ULTIMATE Guide. Вторая часть тут (ссылка может поменяться). Особенности Java 8 – максимальное руководство (часть 1) - 1 От редакции: статья опубликована в то время как Java 8 была доступна общественности и все указывает на то, что это действительно major-версия. Здесь мы в изобилии предоставили руководства Java Code Geeks, такие как Играем с Java 8 – Лямбды и Параллелизм, Java 8 руководство по API даты и времени: LocalDateTime и Абстрактный Класс против Интерфейса в эру Java 8. Мы также ссылаемся на 15 необходимых к прочтению руководств по Java 8 из других источников. Конечно мы рассматриваем некоторые из недостатков, например, Темная сторона Java 8. Итак, пришло время собрать все основные особенности Java 8 в одном месте для вашего удобства. Наслаждайтесь!

1. Введение

Без сомнений релиз Java 8 величайшее событие со времен Java 5 (выпущена довольно давно, в 2004-м). Он принес множество новых особенностей в Java как в язык, так и в компилятор, библиотеки, инструменты и JVM (виртуальная машина Java). В этом руководстве мы собираемся взглянуть на эти изменения и продемонстрировать различные сценарии использования на реальных примерах. Руководство состоит из нескольких частей каждая из которой затрагивает конкретную сторону платформы:
  • Язык
  • Компилятор
  • Библиотеки
  • Инструменты
  • Среда выполнения (JVM)

2. Новые особенности в языке Java 8

В любом случае Java 8 – это крупный релиз. Можно сказать, что это заняло так много времени из-за реализации возможностей, которые искал каждый Java-разработчик. В этом разделе мы собираемся охватить большинство из них.

2.1. Лямбды и Функциональные интерфейсы

Лямбды (также известные как закрытые или анонимные методы) наиболее большое и наиболее ожидаемое изменение языка во всем релизе Java 8. Они позволяют нам задавать функциональность как аргумент метода (объявляя функцию вокруг), или задавать код как данные: понятия с которыми знаком каждый разработчик функционального программирования. Много языков на платформе JVM (Groovy, Scala, …) имели лямбды с первого дня, но у разработчиков Java не было выбора кроме как представлять лямбды через анонимные классы. Обсуждение дизайна лямбд заняли много времени и усилий общественности. Но в конце концов компромиссы были найдены, что привело к появлению новых кратких конструкций. В своей простейшей форме лямбда может быть представлена в виде разделенных запятыми списка параметров, символа –> и тела. Например:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) )
Обратите внимание, что тип аргумента e определен компилятором. Кроме того вы можете явно указать тип параметра обернув параметр в скобки. Например:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
В случае, если тело лямбды более сложное, оно может быть обернуто в фигурные скобки подобно определению обычной функции в Java. Например:

Arrays.asList( "a", "b", "d" ).forEach( e -< {
    System.out.print( e );
    System.out.print( e );
} );
Лямбда может ссылаться на члены класса и локальные переменные (неявно делает обращение эффективным независимо от того обращается к final полю или нет). Например, эти 2 фрагмента эквиваленты:

String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
    ( String e ) -> System.out.print( e + separator ) );
И:

final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
    ( String e ) -> System.out.print( e + separator ) );
Лямбды могут возвращать значение. Тип возвращаемого значения будет определен компилятором. Объявление return не требуется, если тело лямбды состоит из одной строки. Два фрагмента кода ниже эквивалентны:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
И:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );
Разработчики языка долго думали как сделать уже существующие функции лямбда-дружелюбными. В результате появилось понятие функционального интерфейса. Функциональный интерфейс – это интерфейс с только одним методом. В результате он может быть неявно преобразован в лямбда-выражение. java.lang.Runnable и java.util.concurrent.Callable два замечательных примера функциональных интерфейсов. На практике функциональные интерфейсы очень хрупки: если кто-то добавит хотя бы один другой метод в определение интерфейса, он не будет больше функциональным и процесс компиляции не завершится. Чтобы избежать этой хрупкости и явно определить намерения интерфейса как функционального в Java 8 была добавлена специальная аннотация @FunctionalInterface (все существующие интерфейсы в библиотеке Java получили аннотацию @FunctionalInterface). Давайте посмотрим на это простое определение функционального интерфейса:

@FunctionalInterface
public interface Functional {
    void method();
}
Есть одна вещь, которую нужно иметь в виду: методы по умолчанию и статические методы не нарушают принцип функционального интерфейса и могут быть объявлены:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
         
    default void defaultMethod() {           
    }       
}
Лямбды наиболее популярный пункт Java 8. Они имеют весь потенциал для привлечения большего количества разработчиков к этой прекрасной платформе и обеспечить умелую поддержку функциональным особенностям в чистой Java. Для более детальной информации обратитесь к официальной документации.

2.2. Интерфейсы по умолчанию и статические методы

В Java 8 было расширено определение интерфейсов двумя новыми концепциями: метод по умолчанию и статический метод. Методы по умолчанию делают интерфейсы несколько похожими на трейты, но служат немного другой цели. Они позволяют добавлять новые методы к существующим интерфейсам не нарушая обратную совместимость для ранее написанных версий этих интерфейсов. Разница между методами по умолчанию и абстрактными методами в том, что абстрактные методы должны быть реализованы, а методы по умолчанию нет. Вместо этого каждый интерфейс должен предоставить так называемую реализацию по умолчанию, и все наследники будут получать ее по умолчанию (с возможностью переопределить эту реализацию по умолчанию при необходимости). Давайте посмотрим на пример ниже.

private interface Defaulable {
    // Интерфейсы теперь разрешают методы по умолчанию, 
    // клиент может реализовывать  (переопределять)
    // или не реализовывать его
    default String notRequired() {
        return "Default implementation";
    }       
}
	         
private static class DefaultableImpl implements Defaulable {
}
	     
private static class OverridableImpl implements Defaulable {
    @Override
    public String notRequired() {
        return "Overridden implementation";
    }
}
Интерфейс Defaulable объявил метод по умолчанию notRequired() используя ключевое слово default как часть определения метода. Один из классов, DefaultableImpl, реализует этот интерфейс оставляя метод по умолчанию как есть. Другой класс, OverridableImpl, переопределяет реализацию по умолчанию и предоставляет свою собственную. Другая интересная особенность, представленная в Java 8 – это то, что интерфейсы могут объявить (и предложить реализацию) статических методов. Вот пример:

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier<Defaulable> supplier ) {
        return supplier.get();
    }
}
Небольшой фрагмент кода объединяет метод по умолчанию и статический метод из примера выше:

public static void main( String[] args ) {
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );
	         
    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}
Вывод на консоль этой программы выглядит так:

Default implementation
Overridden implementation
Реализация методов по умолчанию в JVM является очень эффективной и вызов метода поддерживается инструкциями байт-кода. Методы по умолчанию позволили существующим Java интерфейсам развиваться не нарушая процесс компиляции. Хорошие примеры – множество добавленных методов в интерфейс java.util.Collection: stream(), parallelStream(), forEach(), removeIf(), … Хотя будучи мощными, методы по умолчанию следует использовать с осторожностью: прежде, чем объявить метод по умолчанию стоит подумать дважды действительно ли это необходимо так как это может привести к неоднозначности компиляции и ошибках в сложных иерархиях. Для получения более детальной информации обращайтесь к документации.

2.3. Ссылочные методы

Ссылочные методы внедряют полезный синтаксис, чтобы ссылаться на существующие методы или конструкторы Java-классов или объектов (экземпляров). Совместно с лямбда-выражениями, ссылочные методы делают языковые конструкции компактными и лаконичными, делая его шаблонным. Ниже представлен класс Car как пример различных определений методов, давайте выделим четыре поддерживаемых типа ссылочных методов:

public static class Car {
    public static Car create( final Supplier<Car> supplier ) {
        return supplier.get();
    }             
	         
    public static void collide( final Car car ) {
        System.out.println( "Collided " + car.toString() );
    }
	         
    public void follow( final Car another ) {
        System.out.println( "Following the " + another.toString() );
    }
	         
    public void repair() {  
        System.out.println( "Repaired " + this.toString() );
    }
}
Первый ссылочный метод – ссылка на конструктор с синтаксисом Class::new или альтернативный для дженериков (generics) Class< T >::new. Обратите внимание, что конструктор не имеет аргументов.

final Car car = Car.create( Car::new );
final List<Car> cars = Arrays.asList( car );
Второй вариант это ссылка на статический метод с синтаксисом Class::static_method. Обратите внимание, что метод принимает ровно один параметр типа Car.

cars.forEach( Car::collide );
Третий тип – ссылка на метод экземпляра произвольного объекта определенного типа с синтаксисом Class::method. Обратите внимание, что никакие аргументы не принимаются методом.

cars.forEach( Car::repair );
И последний, четвертый тип – ссылка на метод экземпляра определенного класса с синтаксисом instance::method. Обратите внимание, что метод принимает только один параметр типа Car.

final Car police = Car.create( Car::new );
cars.forEach( police::follow );
Запуск всех этих примеров как Java-программы производит следующий вывод на консоль (ссылка на класс Car может отличаться):

Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Для более детальной информации и деталей ссылочных методов обращайтесь к официальной документации.

2.4. Повторяющиеся аннотации

С тех пор как в Java 5 была введена поддержка аннотаций, эта возможность стала очень популярной и очень широко используемой. Тем не менее, одним из ограничений использования аннотаций был тот факт, что одна и та же аннотация не может быть объявлена более одного раза в одном месте. Java 8 нарушает это правило и представляет повторяющиеся аннотации. Это позволяет тем же аннотациям повторяться несколько раз в том месте где они объявлены. Повторяющиеся аннотации следует аннотировать себя с использованием аннотации @Repeatable. На самом деле, это не сколько изменение языка сколько трюк компилятора, в то время как техника остается той же. Давайте посмотрим на простой пример:

package com.javacodegeeks.java8.repeatable.annotations;
	 
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
	 
public class RepeatingAnnotations {
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    public @interface Filters {
        Filter[] value();
    }
	     
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    @Repeatable( Filters.class )
    public @interface Filter {
        String value();
    };
	     
    @Filter( "filter1" )
    @Filter( "filter2" )
    public interface Filterable {       
    }
	     
    public static void main(String[] args) {
        for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
            System.out.println( filter.value() );
        }
    }
}
Как мы можем видеть, класс Filter аннотирован с помощью @Repeatable( Filters.class ). Filters просто владелец аннотаций Filter, но компилятор Java старается скрыть их присутствие от разработчиков. Таким образом, интерфейс Filterable содержит аннотации Filter, которые объявлены дважды (без упоминания Filters). Также Reflection API предоставляет новый метод getAnnotationsByType() для возвращения повторяющихся аннотаций некоторого типа (помните, что Filterable.class.getAnnotation( Filters.class ) вернет экземпляр Filters введенный компилятором). Вывод программы будет выглядеть следующим образом:

filter1
filter2
За более детальной информацией обратитесь к официальной документации.

2.5. Улучшенное выведение типов

Компилятор Java 8 получил много улучшений выведения типов. Во многих случаях явные параметры типов могут быть определены компилятором, тем самым делая код чище. Давайте взглянем на один из примеров:

package com.javacodegeeks.java8.type.inference;
	 
public class Value<T> {
    public static<T> T defaultValue() {
        return null;
    }
	     
    public T getOrDefault( T value, T defaultValue ) {
        return ( value != null ) ? value : defaultValue;
    }
}
А вот использование с типом Value<String>:

package com.javacodegeeks.java8.type.inference;
	 
public class TypeInference {
    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault( "22", Value.defaultValue() );
    }
}
Параметр типа Value.defaultValue() определеляется автоматически и не должен быть представлен явно. В Java 7 тот же пример не будет компилироваться и должен быть переписан в виде <NOBR>Value.<String>defaultValue()</NOBR>.

2.6. Расширенная поддержка аннотаций

Java 8 расширяет контекст где могут быть использованы аннотации. Сейчас аннотацию может иметь почти все: локальные переменные, обобщенные типы, суперклассы и реализуемые интерфейсы, даже исключения методов. Несколько примеров представлено ниже:

package com.javacodegeeks.java8.annotations;
	 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
	 
public class Annotations {
    @Retention( RetentionPolicy.RUNTIME )
    @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
    public @interface NonEmpty {       
    }
	         
    public static class Holder<@NonEmpty T> extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {          
        }
    }
	         
    @SuppressWarnings( "unused" )
    public static void main(String[] args) {
        final Holder<String> holder = new @NonEmpty Holder<String>();      
        @NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();      
    }
}
ElementType.TYPE_USE и ElementType.TYPE_PARAMETER два новых типа элементов, чтобы описать соответствующий контекст аннотаций. Annotation Processing API также потерпело незначительные изменения, чтобы распознать новые типы аннотаций в Java.

3. Новые возможности в компиляторе Java

3.1. Имена параметров

На протяжении всего времени разработчики Java изобретали разнообразные способы сохранения имен параметров метода в Java байт-коде, чтобы сделать их доступными во время выполнения (например, библиотека Paranamer). И наконец, Java 8 создает эту нелегкую функцию в языке (используя Reflection API и метод Parameter.getName() ) и байт-коде (с помощью нового аргумента компилятора javac: –parameters).

package com.javacodegeeks.java8.parameter.names;
	 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}
Если вы скомпилируете этот класс без использования аргумента –parameters и затем запустите программу, вы увидите что-то типа этого:

Parameter: arg0
С параметром –parameters переданным компилятору, вывод программы будет отличаться (будет показано фактическое имя параметра):

Parameter: args
Для опытных пользователей Maven аргумент –parameters может быть добавлен в компиляцию используя секцию maven-compiler-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
    <compilerArgument>-parameters</compilerArgument>
    <source>1.8</source>
    <target>1.8</target>
    </configuration>
</plugin>
Для проверки доступности имен параметров есть удобный isNamePresent() метод предоставленный классом Parameter.

4. Новые инструменты Java

Java 8 поставляется с новым набором инструментов командной строки. В этом разделе мы рассмотрим самые интересные из них.

4.1. Движок Nashorn: jjs

jjs – автономный движок Nashorn, который основан на командной строке. Он принимает список JavaScript файлов исходного кода и запускает их. Например, давайте создадим файл func.js следующего содержания:

function f() {
     return 1;
};
	 
print( f() + 1 );
Чтобы запустить этот файл давайте передадим его как аргумент в jjs:

jjs func.js
Вывод на консоль будет таким:

2
Для более подробной информации смотрите документацию.

4.2. Анализатор зависимостей класса: jdeps

jdeps действительно отличный инструмент командной строки. Он показывает зависимости на уровне пакета или класса для Java классов. Он принимает .class файл, папку или JAR-файл на вход. По умолчанию jdeps выводит зависимости в стандартную систему вывода (консоль). В качестве примера давайте посмотрим на отчет зависимостей популярной библиотеки Spring Framework. Чтоб сделать пример коротким давайте посмотрим зависимости только для JAR-файла org.springframework.core-3.0.5.RELEASE.jar.

jdeps org.springframework.core-3.0.5.RELEASE.jar
Эта команда выводит довольно много, поэтому мы будем анализировать только часть вывода. Зависимости сгруппированы по пакетам. Если зависимостей нет – будет выведено not found.

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io                                           
      -> java.lang                                         
      -> java.lang.annotation                              
      -> java.lang.ref                                     
      -> java.lang.reflect                                 
      -> java.util                                         
      -> java.util.concurrent                              
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang                                         
      -> java.lang.annotation                              
      -> java.lang.reflect                                 
      -> java.util
Для более детальной информации обращайтесь к официальной документации.
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Kir Уровень 4
23 февраля 2021
Как я понимаю, тут описка? (e -<) Arrays.asList( "a", "b", "d" ).forEach( e -< { System.out.print( e ); System.out.print( e ); } );
Eugene Semenov Уровень 23
22 апреля 2020
Спасибо за статью. есть вопрос (возможно еще не дошел в изучении, а может пропустил). почему в 8 джаве пишут некоторые строки с точки как здесь:

List<String> result  = stream
               .filter(line -> line.startsWith("Как"))
               .map(String::toUpperCase)
               .collect(Collectors.toList());