Продолжаем рассказ про API, которые появились в новых версиях Java.
1. Files.mismatch()
Появился в: Java 12
На практике довольно часто возникает необходимость проверить, являются ли два файла в точности одинаковыми или нет. С помощью метода Files.mismatch()
, появившегося в Java 12, это наконец-то можно сделать. Этот метод возвращает позицию первого несовпадающего байта в двух файлах или -1
, если файлы идентичны.
Это может быть полезно, например, когда синхронизируешь содержимое двух директорий. Чтобы не перезаписывать файл при копировании тем же самым содержимым и лишний раз не нагружать диск, можно сначала проверить, идентичны файлы или нет:
public static void syncDirs(Path srcDir, Path dstDir) throws IOException { // Для простоты демонстрации считаем, что поддиректорий нет try (Stream<Path> stream = Files.list(srcDir)) { for (Path src : stream.collect(toList())) { 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 : stream)
, а не возиться с промежуточными списками.)
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. Всё ещё не хотите обновляться? Если нет, то тогда ждите следующую часть.