Функции Java 8 с примерами

Java 8 была выпущена 18 марта 2014 года. Это было давно, но до сих пор многие проекты работают на Java 8. Это потому, что это был крупный выпуск с множеством новых функций. Давайте рассмотрим все захватывающие и основные функции Java 8 с примерами кода.

Краткий обзор функций Java 8

Некоторые из важных функций Java 8 включают;

  1. Метод forEach() в интерфейсе Iterable
  2. Методы по умолчанию и статические методы в интерфейсах
  3. Функциональные интерфейсы и лямбда-выражения
  4. Java Stream API для операций с массивами данных в коллекциях
  5. API времени в Java
  6. Улучшения в API коллекций
  7. Улучшения в API параллелизма
  8. Улучшения в IO в Java

Давайте кратко рассмотрим эти функции Java 8. Я предоставлю несколько фрагментов кода для лучшего понимания функций в простом виде.

1. Метод forEach() в интерфейсе Iterable

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

В Java 8 был введен метод forEach в интерфейсе java.lang.Iterable, чтобы при написании кода мы сосредотачивались на бизнес-логике. Метод forEach принимает объект java.util.function.Consumer в качестве аргумента, поэтому он помогает разместить нашу бизнес-логику в отдельном месте, которое мы можем повторно использовать. Давайте рассмотрим использование forEach на простом примере.

package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		//создание образца коллекции
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		//прохождение с использованием итератора
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		//прохождение через метод forEach интерфейса Iterable с использованием анонимного класса
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		//прохождение с реализацией интерфейса Consumer
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

//Реализация Consumer, которая может быть повторно использована
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}
}

Количество строк может увеличиться, но метод forEach помогает разместить логику итерации и бизнес-логику в отдельном месте, что приводит к более высокому разделению забот и более чистому коду.

2. Методы по умолчанию и статические методы в интерфейсах

Если вы внимательно прочтете детали метода forEach, вы заметите, что он определен в интерфейсе Iterable, но мы знаем, что интерфейсы не могут иметь тела метода. Начиная с Java 8, интерфейсы усовершенствованы для того, чтобы иметь метод с реализацией. Мы можем использовать ключевые слова default и static для создания интерфейсов с реализацией метода. Реализация метода forEach в интерфейсе Iterable:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

Мы знаем, что Java не предоставляет множественное наследование в классах, потому что это приводит к Проблеме алмаза. Как теперь это будет обрабатываться с интерфейсами, если интерфейсы теперь похожи на абстрактные классы?

Решение в том, что компилятор выдаст исключение в этом сценарии, и нам придется предоставить логику реализации в классе, реализующем интерфейсы.

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	//попытка переопределения метода Object вызывает ошибку времени компиляции как
	//"Метод по умолчанию не может переопределить метод из java.lang.Object"
	
//	default String toString(){
//		return "i1";
//	}
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

Обратите внимание, что у обоих интерфейсов есть общий метод log() с логикой реализации.

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	// MyClass не будет компилироваться без собственной реализации log()
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

Как видите, Interface1 имеет статическую реализацию метода, которая используется в реализации метода MyClass.log(). В Java 8 широко используются методы по умолчанию и статические методы в API коллекций, а методы по умолчанию добавляются для обеспечения обратной совместимости нашего кода.

Если в иерархии какого-либо класса есть метод с тем же именем, методы по умолчанию становятся неактуальными. Объект является базовым классом, поэтому, если у нас есть методы по умолчанию equals() и hashCode() в интерфейсе, они станут неактуальными. Поэтому для большей ясности интерфейсам не разрешается иметь методы по умолчанию Object.

Для полной информации об изменениях интерфейса в Java 8, пожалуйста, прочтите Изменения интерфейса в Java 8.

3. Функциональные интерфейсы и лямбда-выражения

Если вы обратите внимание на приведенный выше код интерфейса, вы заметите аннотацию @FunctionalInterface. Функциональные интерфейсы – это новая концепция, введенная в Java 8. Интерфейс с ровно одним абстрактным методом становится функциональным интерфейсом. Нам не нужно использовать аннотацию @FunctionalInterface для обозначения интерфейса как функционального.

@FunctionalInterface аннотация – это средство избежать случайного добавления абстрактных методов в функциональные интерфейсы. Вы можете рассматривать ее как аннотацию @Override, и лучше всего использовать ее. java.lang.Runnable с единственным абстрактным методом run() – отличный пример функционального интерфейса.

Одно из основных преимуществ функционального интерфейса – возможность использования лямбда-выражений для их создания. Мы можем создать экземпляр интерфейса с анонимным классом, но код выглядит громоздким.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Поскольку у функциональных интерфейсов есть только один метод, лямбда-выражения могут легко предоставить реализацию метода. Мы просто должны предоставить аргументы метода и бизнес-логику. Например, мы можем написать вышеприведенную реализацию, используя лямбда-выражение, как:

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

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

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

Лямбда-выражения – это средство для создания анонимных классов функциональных интерфейсов с легкостью. Использование лямбда-выражений не приносит преимуществ во время выполнения, поэтому я буду использовать их осторожно, потому что мне не важно написать несколько дополнительных строк кода.

A new package java.util.function has been added with bunch of functional interfaces to provide target types for lambda expressions and method references. Lambda expressions are a huge topic, I will write a separate article on that in the future.

Вы можете ознакомиться с полным руководством по ссылке Руководство по лямбда-выражениям в Java 8.

4. Java Stream API для массовых операций над коллекциями

A new java.util.stream has been added in Java 8 to perform filter/map/reduce like operations with the collection. Stream API will allow sequential as well as parallel execution. This is one of the best features for me because I work a lot with Collections and usually with Big Data, we need to filter out them based on some conditions.

Интерфейс Collection был расширен методами stream() и parallelStream() по умолчанию для получения потока для последовательного и параллельного выполнения. Давайте посмотрим на их использование на простом примере.

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		//последовательный поток
		Stream<Integer> sequentialStream = myList.stream();
		
		//параллельный поток
		Stream<Integer> parallelStream = myList.parallelStream();
		
		//использование лямбда-выражения с API Stream, пример фильтрации
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		//использование лямбда-выражения в forEach
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

Если вы запустите приведенный выше пример кода, вы получите вывод, похожий на следующий:

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

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

В этом сообщении невозможно охватить все аспекты Stream API, вы можете прочитать обо всём, что касается Stream API, на Примере руководства по использованию Java 8 Stream API.

5. Java Time API

Работа с датой, временем и часовыми поясами в Java всегда была сложной задачей. Не было стандартного подхода или API в Java для работы с датой и временем. Одним из хороших дополнений в Java 8 является пакет java.time, который упростит процесс работы со временем в Java.

Просматривая пакеты Java Time API, я чувствую, что они будут очень легки в использовании. Он имеет некоторые подпакеты, такие как java.time.format, который предоставляет классы для вывода и разбора дат и времени, и java.time.zone, который обеспечивает поддержку часовых поясов и их правил.

В новом API времени предпочтение отдается перечислениям перед целочисленными константами для месяцев и дней недели. Одним из полезных классов является DateTimeFormatter для преобразования объектов DateTime в строки. Для полного руководства перейдите на Пример использования Java Date Time API.

6. Улучшения в API коллекций

Мы уже видели метод forEach() и API Stream для коллекций. Некоторые новые методы, добавленные в API Collection, включают:

  • Iterator метод по умолчанию forEachRemaining(Consumer action) для выполнения заданного действия для каждого оставшегося элемента, пока все элементы не будут обработаны или пока действие не вызовет исключение.
  • Collection метод по умолчанию removeIf(Predicate filter) для удаления всех элементов этой коллекции, удовлетворяющих заданному предикату.
  • Collection метод spliterator(), возвращающий экземпляр Spliterator, который можно использовать для последовательного или параллельного обхода элементов.
  • Map методы replaceAll()compute()merge().
  • Улучшение производительности для класса HashMap при коллизиях ключей

7. Улучшения API параллелизма

Некоторые важные улучшения API параллелизма включают:

  • ConcurrentHashMap методы compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() и search().
  • CompletableFuture, который может быть явно завершен (установка его значения и статуса).
  • Executors метод newWorkStealingPool() для создания пула потоков с алгоритмом украденной работы, используя все доступные процессоры в качестве целевого уровня параллелизма.

8. Улучшения в Java IO

Некоторые из улучшений IO, о которых мне известно:

  • Files.list(Path dir), возвращающий лениво заполняемый поток, элементы которого – это записи в каталоге.
  • Files.lines(Path path), читающий все строки из файла как поток.
  • Files.find(), возвращающий лениво заполняемый поток Path, выполняющий поиск файлов в файловом дереве, начиная с заданного файла.
  • BufferedReader.lines(), возвращающий поток, элементами которого являются строки, прочитанные из этого BufferedReader.

Различные улучшения ядра Java 8 API

Некоторые другие улучшения API, которые могут пригодиться:

  1. Статический метод ThreadLocal с методом withInitial(Supplier supplier) для упрощения создания экземпляров.
  2. Интерфейс Comparator был расширен большим количеством методов по умолчанию и статических методов для естественного упорядочения, обратного порядка и т. д.
  3. Методы min(), max() и sum() в оболочках Integer, Long и Double.
  4. Методы logicalAnd(), logicalOr() и logicalXor() в классе Boolean.
  5. Метод stream() в классе ZipFile для получения упорядоченного потока элементов ZIP-файла. Элементы появляются в потоке в том же порядке, в котором они находятся в центральном каталоге ZIP-файла.
  6. Несколько вспомогательных методов в классе Math.
  7. jjs команда добавлена для вызова движка Nashorn.
  8. jdeps команда добавлена для анализа файлов классов.
  9. Мост JDBC-ODBC был удален.
  10. Пространство памяти PermGen было удалено.

Это все особенности Java 8 с примерами программ. Если я упустил какие-то важные особенности Java 8, пожалуйста, дайте знать через комментарии.

Source:
https://www.digitalocean.com/community/tutorials/java-8-features-with-examples