Продолжаем рассказ про API, которые появились в новых версиях Java.
1. Files.mismatch()
Появился в: Java 12
На практике довольно часто возникает необходимость проверить, являются ли два файла в точности одинаковыми или нет. С помощью метода Files.mismatch()
, появившегося в Java 12, это наконец-то можно сделать. Этот метод возвращает позицию первого несовпадающего байта в двух файлах или -1
, если файлы идентичны.
Это может быть полезно, например, когда синхронизируешь содержимое двух директорий. Чтобы не перезаписывать файл при копировании тем же самым содержимым и лишний раз не нагружать диск, можно сначала проверить, идентичны файлы или нет:
public static void syncDirs(Path srcDir, Path dstDir)
throws IOException {
// Для простоты демонстрации считаем, что поддиректорий нет
List<Path> srcFiles = Files.list(srcDir).collect(toList());
for (Path src : srcFiles) {
Path dst = dstDir.resolve(src.getFileName());
if (!Files.exists(dst)) {
System.out.println("Copying file " + dst);
Files.copy(src, dst);
} else if (Files.mismatch(src, dst) >= 0) {
System.out.println("Overwriting file " + dst);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
}
}
}
(Кстати, когда уже наконец Stream
отнаследуют от Iterable
? Хочется просто писать for (Path file : Files.list(dir))
, а не возиться с промежуточными списками.)
2. Новые методы в java.time
Появились в: Java 9
В Java почти 20 лет не было нормального API для работы с датами и временем. Эту проблему решили лишь в Java 8, когда ввели новый пакет java.time
под руководством небезызвестного Стивена Колборна, создателя библиотеки Joda Time. А в девятой версии java.time
добавили множество интересных методов.
В Java 8 Duration
нельзя просто разбить на составляющие (например, прошло 2 дня, 7 часов, 15 минут, 12 секунд). В Java 9 для этого появились методы toDaysPart()
, toHoursPart()
, toMinutesPart()
, toSecondsPart()
и т.д. Пример:
public static String modifiedAgo(Path path) throws IOException {
FileTime time = Files.getLastModifiedTime(path);
Instant to = Instant.now();
Instant from = time.toInstant();
Duration d = Duration.between(from, to);
return String.format(
"Файл был изменён %d дней, %d часов, %d минут, %d секунд назад",
d.toDaysPart(), d.toHoursPart(),
d.toMinutesPart(), d.toSecondsPart());
}
А что если нам надо узнать, сколько месяцев назад был изменён файл? Элегантного способа на Java 8, насколько мне известно, нет. А в Java 9 для этого можно использовать новый метод Duration.dividedBy()
:
public static long modifiedAgo(Path path, ChronoUnit unit)
throws IOException {
FileTime time = Files.getLastModifiedTime(path);
Instant to = Instant.now();
Instant from = time.toInstant();
Duration d = Duration.between(from, to);
return d.dividedBy(unit.getDuration());
}
public static void main(String[] args) throws Exception {
Path path = ...
System.out.printf("Файл был изменён %d месяцев назад%n",
modifiedAgo(path, ChronoUnit.MONTHS));
}
Нововведения коснулись также класса LocalDate
. С помощью метода LocalDate.ofInstant()
можно сконвертировать Instant
в LocalDate
:
LocalDate date = LocalDate.ofInstant(
Instant.now(), ZoneId.systemDefault());
System.out.println(date);
А используя новый метод LocalDate.datesUntil()
, наконец-то можно легко получить Stream
всех дат в интервале между двумя датами:
LocalDate from = LocalDate.of(2020, 1, 1);
LocalDate to = LocalDate.of(2020, 1, 9);
from.datesUntil(to)
.forEach(System.out::println);
Вывод:
2020-01-01
2020-01-02
2020-01-03
2020-01-04
2020-01-05
2020-01-06
2020-01-07
2020-01-08
Также есть перегрузка, где можно указать период:
LocalDate from = LocalDate.of(2020, 1, 1);
LocalDate to = LocalDate.of(2020, 1, 31);
from.datesUntil(to, Period.ofWeeks(1))
.forEach(System.out::println);
Вывод:
2020-01-01
2020-01-08
2020-01-15
2020-01-22
2020-01-29
Остальные методы:
Clock.tickMillis()
Duration.truncatedTo()
LocalDate.toEpochSecond()
LocalTime.ofInstant()
LocalTime.toEpochSecond()
OffsetTime.toEpochSecond()
Chronology.epochSecond()
DateTimeFormatterBuilder.appendGenericZoneText()
3. Collection.toArray()
с функцией-генератором
Появился в: Java 11
С конвертацией коллекций в массивы у Java была непростая история. С момента появления Collection
в Java 1.2 было два способа создания массива на основе коллекции:
- Использовать метод
Collection.toArray()
, который возвращаетObject[]
. - Использовать метод
Collection.toArray(Object[])
, который принимает уже созданный массив и заполняет его. Если переданный массив недостаточной длины, то создаётся новый массив нужной длины того же типа и возвращается. С появлением дженериков в Java 1.5 метод логичным образом поменял свою сигнатуру наCollection.toArray(T[])
.
Загвоздка в том, что если нужен массив конкретного типа (допустим String[]
), второй метод можно использовать двумя способами:
- Использовать конструкцию
collection.toArray(new String[0])
. Тем самым, мы сознательно почти всегда отбрасываем массив, а передаём его туда, чтобы метод узнал тип массива. - Использовать конструкцию
collection.toArray(new String[collection.size()])
. В этом случае массив передаётся нужной длины, а значит ничего зря не отбрасывается, и код по идее работает быстрее. К тому же здесь не нужен рефлективный вызов.
Таким образом, второй вариант долгое время считался основным, и в IntelliJ IDEA даже была инспекция, которая подсвечивала первый вариант и предлагала конвертировать его во второй, более эффективный.
Однако в 2016 году вышла статья Алексея Шипилёва, где он решил досконально разобраться в этом вопросе и пришёл к выводу, что не-а: первый вариант всё-таки быстрее (по крайней мере в версиях JDK 6+). Эта статья получила большой резонанс, и в IDEA решили изменить инспекцию, сделав у неё три опции: предпочитать пустой массив (default), предпочитать преаллоцированный массив или предпочитать то или иное в зависимости от версии Java.
Но история на этом не закончилась, потому что некоторые программисты принципиально не желали использовать эти хаки с пустыми массивами и хотели писать код «элегантно». Поэтому они вспомнили про Stream.toArray(IntFunction[])
и стали писать collection.stream().toArray(String[]::new)
. Медленно? Ну и что, зато красиво.
Программисты из Oracle посмотрели на всё это безобразие и подумали: а давайте уже сделаем один нормальный способ, который и будет рекомендованным? И в Java 11 добавили долгожданный метод Collection.toArray(IntFunction[])
, тем самым запутав людей ещё сильнее.
Но на самом деле никакой путаницы нет. Да, теперь есть 4 варианта, но если вы не выжимаете такты из своего процессора, то вам следует просто использовать новый метод:
List<Integer> list = ...;
Integer[] array = list.toArray(Integer[]::new);
4. Методы InputStream
: readNBytes()
, readAllBytes()
, transferTo()
Появились в: Java 9 / Java 11
Ещё одно неудобство, которое существовало в Java долгие годы – отсутствие стандартного короткого способа считать все данные из InputStream
. Если не прибегать к библиотекам, то в Java 8 решить такую задачу довольно нетривиально: нужно завести список буферов, заполнять их, пока данные не кончатся, потом слить в один большой массив, учесть, что последний буфер заполнен лишь частично и т.д. Короче, нюансов хватает.
В Java 9 добавили метод InputStream.readAllBytes()
, который берёт всю эту работу на себя и возвращает заполненный массив байтов точной длины. Например, прочитать stdout
/stderr
процесса теперь очень легко:
Process proc = Runtime.getRuntime().exec("java -version");
try (InputStream inputStream = proc.getErrorStream()) {
byte[] bytes = inputStream.readAllBytes();
System.out.print(new String(bytes));
}
Вывод:
openjdk version "14-ea" 2020-03-17
OpenJDK Runtime Environment (build 14-ea+33-1439)
OpenJDK 64-Bit Server VM (build 14-ea+33-1439, mixed mode, sharing)
Также если надо прочитать только N
байтов, то можно использовать метод из Java 11 InputStream.readNBytes()
.
Если же надо легко и эффективно (без промежуточного массива) перенаправить InputStream
в OutputStream
, то можно использовать InputStream.transferTo()
. Например, для вывода версии Java в файл код будет выглядеть примерно так:
Process proc = Runtime.getRuntime().exec("java -version");
Path path = Path.of("out.txt");
try (InputStream inputStream = proc.getErrorStream();
OutputStream outputStream = Files.newOutputStream(path)) {
inputStream.transferTo(outputStream);
}
Кстати, перенаправить Reader
во Writer
теперь тоже можно: с помощью метода Reader.transferTo()
, появившегося в Java 10.
5. Collectors.teeing()
Появился в: Java 12
При использовании Stream
часто возникает необходимость собрать элементы в два коллектора. Допустим, у нас есть Stream
из Employee
, и нужно узнать:
- Сколько всего сотрудников в Stream.
- Сколько сотрудников, у которых есть телефонный номер.
Как это сделать в Java 8? Первое, что приходит в голову: сначала позвать Stream.count()
, а потом Stream.filter()
и Stream.count()
. Однако это не сработает, потому что Stream
является одноразовым и второй вызов выбросит исключение.
Второй вариант – завести два счётчика и увеличивать их внутри Stream.forEach()
:
Stream<Employee> employees = ...
int[] countWithPhoneAndTotal = {0, 0};
employees
.forEach(emp -> {
if (emp.getPhoneNumber() != null) {
countWithPhoneAndTotal[0]++;
}
countWithPhoneAndTotal[1]++;
});
System.out.println("Employees with phone number: "
+ countWithPhoneAndTotal[0]);
System.out.println("Total employees: "
+ countWithPhoneAndTotal[1]);
В принципе, это работает, но это императивный подход, который плохо переносится на другие виды коллекторов. Stream.peek()
плох по той же причине.
Ещё есть идея использовать Stream.reduce()
:
class CountWithPhoneAndTotal {
final int withPhone;
final int total;
CountWithPhoneAndTotal(int withPhone, int total) {
this.withPhone = withPhone;
this.total = total;
}
}
CountWithPhoneAndTotal countWithPhoneAndTotal = employees
.reduce(
new CountWithPhoneAndTotal(0, 0),
(count, employee) -> new CountWithPhoneAndTotal(
employee.getPhoneNumber() != null
? count.withPhone + 1
: count.withPhone,
count.total + 1),
(count1, count2) -> new CountWithPhoneAndTotal(
count1.withPhone + count2.withPhone,
count1.total + count2.total));
System.out.println("Employees with phone number: "
+ countWithPhoneAndTotal.withPhone);
System.out.println("Total employees: "
+ countWithPhoneAndTotal.total);
Этот вариант, конечно же, кошмар. Во-первых, он слишком огромный, во-вторых, неэффективный, так как на каждом шагу создаётся новый экземпляр CountWithPhoneAndTotal
. Если когда-нибудь доделают Валгаллу, то можно будет пометить класс CountWithPhoneAndTotal
как inline
, но первая проблема всё равно останется.
На этом мои идеи закончились. Если вдруг кто-то придумает, как сделать такой подсчёт в Java 8 коротким и эффективным, то напишите в комментариях. А я расскажу, как это можно сделать в Java 12 с помощью метода Collectors.teeing()
:
Entry<Long, Long> countWithPhoneAndTotal = employees
.collect(teeing(
filtering(employee -> employee.getPhoneNumber() != null, counting()),
counting(),
Map::entry
));
И всё.
С методом Collectors.teeing()
была очень интересная история: когда ему придумывали имя, то долго не могли прийти к консенсусу из-за огромного количество предложенных вариантов. Чего там только не было: toBoth
, collectingToBoth
, collectingToBothAndThen
, pairing
, bifurcate
, distributing
, unzipping
, forking
,… В итоге его назвали teeing
от английского слова tee, которое само произошло от буквы T, напоминающую по форме раздваиватель. В этом и есть суть имени метода: он раздваивает поток на две части.
6. Runtime.version()
Появился в: Java 9
Иногда нужно узнать версию Java во время выполнения. Помните ли вы, как это сделать? Скорее всего, вы полезете искать название нужного свойства в интернете. Возможно некоторые вспомнят, что оно называется java.version
. А ещё вроде бы есть java.specification.version
… На самом деле, таких свойств как минимум пять:
for (String key : Arrays.asList(
"java.version",
"java.runtime.version",
"java.specification.version",
"java.vm.version",
"java.vm.specification.version")) {
System.out.println(key + " = " + System.getProperty(key));
}
Если запустить код на Java 8, то он выведет примерно следующее:
java.version = 1.8.0_192
java.runtime.version = 1.8.0_192-b12
java.specification.version = 1.8
java.vm.version = 25.192-b12
java.vm.specification.version = 1.8
Как отсюда вытащить цифру 8? Наверное, надо взять java.specification.version
, отбросить 1.
, потом сконвертировать строку в число… Но не торопитесь, потому что на Java 9 это всё сломается:
java.version = 9.0.1
java.runtime.version = 9.0.1+11
java.specification.version = 9
java.vm.version = 9.0.1+11
java.vm.specification.version = 9
Однако не печальтесь, потому что в Java 9 появилось нормальное API для получения версий и было немного допилено в Java 10. С этим API больше не нужно ничего «вытаскивать» и парсить, а можно просто позвать метод Runtime.version()
. Этот метод возвращает объект типа Runtime.Version
, у которого можно запросить все нужные части версии:
Runtime.Version version = Runtime.version();
System.out.println("Feature = " + version.feature());
System.out.println("Interim = " + version.interim());
System.out.println("Update = " + version.update());
System.out.println("Patch = " + version.patch());
Например, вот что он вернёт, если его позвать на JDK 11.0.5:
Feature = 11
Interim = 0
Update = 5
Patch = 0
7. Optional.isEmpty()
Появился в: Java 11
Я не стану утверждать, что этот метод изменит вашу жизнь радикальным образом, но всё же в некоторых случаях он сможет избавить вас от ненужных отрицаний:
if (!stream.findAny().isPresent()) {
System.out.println("Stream is empty");
}
Используя метод Optional.isEmpty()
, код можно немножко упростить:
if (stream.findAny().isEmpty()) {
System.out.println("Stream is empty");
}
Также этот метод позволяет заменить лямбды на ссылки на методы в некоторых случаях:
Stream<Optional<Integer>> stream = Stream.of(
Optional.of(1),
Optional.empty(),
Optional.of(2));
long emptyCount = stream
.filter(Optional::isEmpty) // Было opt -> !opt.isPresent()
.count();
8. HTTP-клиент
Появился в: Java 11
Долгое время единственным API для клиентского HTTP был класс HttpURLConnection
, который существовал в Java практически с момента её появления. Спустя два десятилетия стало очевидно, что он больше не отвечает современным требованиям: он неудобен в использовании, не поддерживает HTTP/2 и веб-сокеты, работает только в блокирующем режиме, а ещё его очень трудно поддерживать. Поэтому было принятое решение создать новый HTTP Client, который попал в Java 9 в качестве инкубационного модуля, а позже был стандартизован в Java 11.
Новый клиент находится в модуле java.net.http
, и его использование осуществляется через главный класс HttpClient
. Приведём пример, как можно сделать простой HTTP-запрос с сайта и получить содержимое страницы с кодом ответа:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest
.newBuilder(new URI("https://minijug.ru"))
.build();
HttpResponse<Stream<String>> response = client.send(request,
HttpResponse.BodyHandlers.ofLines());
System.out.println("Status code = " + response.statusCode());
System.out.println("Body = ");
// Первые 4 строки
response.body().limit(4).forEach(System.out::println);
Вывод:
Status code = 200
Body =
<!doctype html>
<html>
<head>
<title>miniJUG</title>
В модуле java.net.http
большое количество возможностей, и на их описание уйдёт много времени, поэтому сегодня мы ограничимся только примером выше.
9. Lookup.defineClass()
Появился в: Java 9
Приходилось ли вам загружать классы во время выполнения? Если да, вы наверняка знаете, что в Java 8 без нового загрузчика класса это сделать нельзя. Ну или ещё можно использовать Unsafe.defineClass()
или Unsafe.defineAnonymousClass()
, но это нестандартное API, которое крайне не рекомендуется использовать.
Однако есть хорошая новость: если вам нужно загрузить класс в том же пакете, не создавая новый загрузчик класса, то для этого можно использовать стандартный метод MethodHandles.Lookup.defineClass()
, который появился в Java 9. Этому методу достаточно передать массив байтов класса:
// Main.java
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Path.of("Temp.class"));
Class<?> clazz = MethodHandles.lookup().defineClass(bytes);
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println(obj);
}
}
// Temp.java
class Temp {
@Override
public String toString() {
return "Hello from Temp!";
}
}
Теперь скомпилируем класс Temp
, а затем скомпилируем и запустим класс Main
> javac Temp.java
> javac Main.java
> java Main
Hello from Temp!
Повторюсь, чтобы это сработало, класс Temp
и класс Main
должны находиться в одном пакете (в данном случае они оба находятся в дефолтном пакете, поэтому всё хорошо). Если класс Temp
будет находиться в другом пакете, то понадобится завести специальный класс-делегат в том же пакете, что и Temp
, и осуществлять загрузку через него.
Да, пример выше совсем простой, но это сделано исключительно для краткости и простоты демонстрации. Так как defineClass()
принимает массив байтов, то загружать класс можно откуда угодно, а не только с файловой системы. Можно даже загрузить класс, скомпилированный в память во время исполнения. Для этого можно использовать ToolProvider.getSystemJavaCompiler()
, который находится в модуле java.compiler
(конкретную реализацию я оставлю в качестве упражнения для читателя).
10. ByteArrayOutputStream.writeBytes()
Появился в: Java 11
Метод ByteArrayOutputStream.writeBytes()
– это дублёр метода ByteArrayOutputStream.write()
с одним важным отличием: в сигнатуре write()
есть throws IOException
, а в сигнатуре writeBytes()
– нету (IOException
есть во write()
, потому что этот метод наследуется от OutputStream
). Это значит, что начиная с Java 11, использование ByteArrayOutputStream
становится немножко проще:
private static byte[] concat(Stream<byte[]> stream) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
// stream.forEach(out::write); (Не скомпилируется)
stream.forEach(out::writeBytes);
return out.toByteArray();
}
Бонус: конструктор IndexOutOfBoundsException(int)
Появился в: Java 9
Сегодняшний рассказ хочу завершить мелким улучшением в Java 9: если вам надо выбросить IndexOutOfBoundsException
с указанием неправильного индекса, то теперь можно просто передать этот индекс в конструктор, и он сам сгенерирует сообщение:
private static void doAtIndex(int index) {
if (index < 0) {
throw new IndexOutOfBoundsException(index);
}
// ...
}
public static void main(String[] args) {
// java.lang.IndexOutOfBoundsException: Index out of range: -1
doAtIndex(-1);
}
Заключение
Итак, мы рассмотрели ещё 10 (+1) новых API, которые появились в новых версиях Java. Всё ещё не хотите обновляться? Если нет, то тогда ждите следующую часть.
Автор: Zheka Kozlov