- PVSM.RU - https://www.pvsm.ru -
На сегодняшний день Java 8 является самой популярной версией Java и ещё довольно долго будет ей оставаться. Однако с тех пор уже выпущено пять новых версий Java (9, 10, 11, 12, 13), и совсем скоро выйдет ещё одна, Java 14. В этих новых версиях появилось гигантское количество новых возможностей. Например, если считать в JEP'ах, то в сумме их было реализовано 141:
Однако в этом цикле статей не будет никакого сухого перечисления JEP'ов. Вместо этого я хочу просто рассказать об интересных API, которые появились в новых версиях. Каждая статья будет содержать по 10 API. В выборе и порядке этих API не будет какой-то определённой логики и закономерности. Это будет просто 10 случайных API, не ТОП 10 и без сортировки от наиболее важного API к наименее важному. Давайте начнём.
Objects.requireNonNullElse()
и Objects.requireNonNullElseGet()
Начнём мы наш список с двух очень простеньких, но очень полезных методов в классе java.util.Objects
: requireNonNullElse() [7]
и requireNonNullElseGet() [8]
. Эти методы позволяют вернуть передаваемый объект, если он не null
, а если он null
, то вернуть объект по умолчанию. Например:
class MyCoder {
private final Charset charset;
MyCoder(Charset charset) {
this.charset = Objects.requireNonNullElse(
charset, StandardCharsets.UTF_8);
}
}
requireNonNullElseGet()
– это не что иное, как просто ленивая версия requireNonNullElse()
. Она может пригодиться, если вычисление аргумента по умолчанию является затратным:
class MyCoder {
private final Charset charset;
MyCoder(Charset charset) {
this.charset = Objects.requireNonNullElseGet(
charset, MyCoder::defaultCharset);
}
private static Charset defaultCharset() {
// long operation...
}
}
Да, конечно же в обоих случаях можно было бы легко обойтись и без этих функций, например, использовать обычный тернарный оператор или Optional
, но всё же использование специальной функции делает код немножко короче и чище. А если использовать статический импорт и писать просто requireNonNullElse()
вместо Objects.requireNonNullElse()
, то код код можно сократить ещё сильнее.
Если предыдущие два метода – это просто косметика, то статические методы-фабрики коллекций позволяют действительно сильно сократить код и даже улучшить его безопасность. Речь о следующих методах, появившихся [9] в Java 9:
List.of(E... elements) [10]
(и перегрузки)Set.of(E... elements) [11]
(и перегрузки)Map.of(K k1, V v1, K k2, V v2, ...) [12]
(и перегрузки)Map.ofEntries(Entry<? extends K, ? extends V>... entries) [13]
К этому же списку можно добавить сопутствующий метод Map.entry(K k, V v) [14]
, создающий Entry
из ключа и значения, а также методы копирования коллекций, которые появились [15] в Java 10:
List.copyOf(Collection<? extends E> coll) [16]
Set.copyOf(Collection<? extends E> coll) [17]
Map.copyOf(Map<? extends K,? extends V> map) [18]
Статические методы-фабрики позволяют создать неизменяемую коллекцию и инициализировать её в одно действие:
List<String> imageExtensions = List.of("bmp", "jpg", "png", "gif");
Если не пользоваться сторонними библиотеками, то аналогичный код на Java 8 выглядит гораздо более громоздким:
List<String> imageExtensions = Collections.unmodifiableList(
Arrays.asList("bmp", "jpg", "png", "gif"));
А в случае с Set
или Map
всё ещё печальнее, потому что аналогов Arrays.asList()
для Set
и Map
не существует.
Такая громоздкость провоцирует многих людей, пишуших на Java 8, вообще отказываться от неизменяемых коллекций и всегда использовать обычные ArrayList
, HashSet
и HashMap
, причём даже там, где по смыслу нужны неизменяемые коллекции. В результате это ломает концепцию immutable-by-default и снижает безопасность кода.
Если же наконец обновиться с Java 8, то работать с неизменяемыми коллекциями становится намного проще и приятнее благодаря методам-фабрикам.
Files.readString()
и Files.writeString()
Java всегда была известна своей неспешностью вводить готовые методы для частых операций. Например, для одной из самых востребованных операций в программировании, чтения файла, очень долго не было готового метода. Лишь спустя 15 лет после выхода Java 1.0 появилось NIO, где был введён метод Files.readAllBytes() [19]
для чтения метода в массив байтов.
Но этого всё ещё не хватало, потому что людям часто приходится работать с текстовыми файлами и для этого нужно читать из файла строки, а не байты. Поэтому в Java 8 добавили метод Files.readAllLines() [20]
, возвращающий List<String>
.
Однако и этого было недостаточно, так как люди спрашивали, как просто прочитать весь файл в виде одной строки. В итоге, для полноты картины в Java 11 добавили долгожданный метод Files.readString() [21]
, тем самым окончательно закрыв этот вопрос. Удивительно, что если аналогичный метод присутствовал во многих других языках с самого начала, то Java для этого потребовалось больше 20 лет.
Вместе с readString()
конечно же ввели и симметричный метод writeString() [22]
. Также у этих методов есть перегрузки, позволяющие указать Charset
. В совокупности всё это делает работу с текстовыми файлами чрезвычайно удобной. Пример:
/** Перекодировать файл из одной кодировки в другую */
private void reencodeFile(Path path,
Charset from,
Charset to) throws IOException {
String content = Files.readString(path, from);
Files.writeString(path, content, to);
}
Optional.ifPresentOrElse()
и Optional.stream()
Когда Optional
появился в Java 8, для него не сделали удобного способа выполнить два разных действия в зависимости от того, есть ли в нём значение или нет. В итоге людям приходится прибегать к обычной цепочке isPresent()
и get()
:
Optional<String> opt = ...
if (opt.isPresent()) {
log.info("Value = " + opt.get());
} else {
log.error("Empty");
}
Либо можно извернуться ещё таким образом:
Optional<String> opt = ...
opt.ifPresent(str ->
log.info("Value = " + str));
if (opt.isEmpty()) {
log.error("Empty");
}
Оба варианта не идеальны. Но, начиная с Java 9, такое можно сделать элегантно с помощью метода Optional.ifPresentOrElse() [23]
:
Optional<String> opt = ...
opt.ifPresentOrElse(
str -> log.info("Value = " + str),
() -> log.error("Empty"));
Ещё одним новым интересным методом в Java 9 стал Optional.stream() [24]
, который возвращает Stream
из одного элемента, если значение присутствует, и пустой Stream
, если отсутствует. Такой метод может быть очень полезен в цепочках с flatMap()
. Например, в этом примере очень просто получить список всех телефонных номеров компании:
class Employee {
Optional<String> getPhoneNumber() { ... }
}
class Department {
List<Employee> getEmployees() { ... }
}
class Company {
List<Department> getDepartments() { ... }
Set<String> getAllPhoneNumbers() {
return getDepartments()
.stream()
.flatMap(d -> d.getEmployees().stream())
.flatMap(e -> e.getPhoneNumber().stream())
.collect(Collectors.toSet());
}
}
В Java 8 пришлось бы писать что-нибудь вроде:
e -> e.getPhoneNumber().map(Stream::of).orElse(Stream.empty())
Это выглядит громоздко и не очень читабельно.
Process.pid()
, Process.info()
и ProcessHandle
Если без предыдущих API обойтись худо-бедно ещё можно, то вот замену метода Process.pid() [25]
в Java 8 найти будет довольно проблематично, особенно кроссплатформенную. Этот метод возвращает нативный ID процесса:
Process process = Runtime.getRuntime().exec("java -version");
System.out.println(process.pid());
Также с помощью метода Process.info() [26]
можно узнать дополнительную полезную информацию о процессе. Он возвращает объект типа ProcessHandle.Info [27]
. Давайте посмотрим, что он вернёт нам для процесса выше:
Process process = Runtime.getRuntime().exec("java -version");
ProcessHandle.Info info = process.info();
System.out.println("PID = " + process.pid());
System.out.println("User = " + info.user());
System.out.println("Command = " + info.command());
System.out.println("Args = " + info.arguments().map(Arrays::toString));
System.out.println("Command Line = " + info.commandLine());
System.out.println("Start Time = " + info.startInstant());
System.out.println("Total Time = " + info.totalCpuDuration());
Вывод:
PID = 174
User = Optional[orionll]
Command = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java]
Args = Optional[[-version]]
Command Line = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java -version]
Start Time = Optional[2020-01-24T05:54:25.680Z]
Total Time = Optional[PT0.01S]
Что делать, если процесс был запущен не из текущего Java-процесса? Для этого на помощь приходит ProcessHandle [28]
. Например, давайте достанем всю ту же самую информацию для текущего процесса с помощью метода ProcessHandle.current() [29]
:
ProcessHandle handle = ProcessHandle.current();
ProcessHandle.Info info = handle.info();
System.out.println("PID = " + handle.pid());
System.out.println("User = " + info.user());
System.out.println("Command = " + info.command());
System.out.println("Args = " + info.arguments().map(Arrays::toString));
System.out.println("Command Line = " + info.commandLine());
System.out.println("Start Time = " + info.startInstant());
System.out.println("Total Time = " + info.totalCpuDuration());
Вывод:
PID = 191
User = Optional[orionll]
Command = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java]
Args = Optional[[Main.java]]
Command Line = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java Main.java]
Start Time = Optional[2020-01-24T05:59:17.060Z]
Total Time = Optional[PT1.56S]
Чтобы получить ProcessHandle
для любого процесса по его PID, можно использовать метод ProcessHandle.of() [30]
(он вернёт Optional.empty
, если процесса не существует).
Также в ProcessHandle
есть много других интересных методов, например, ProcessHandle.allProcesses() [31]
.
String
: isBlank()
, strip()
, stripLeading()
, stripTrailing()
, repeat()
и lines()
Целая гора полезных методов для строк появилась в Java 11.
Метод String.isBlank() [32]
позволяет узнать, является ли строка состоящей исключительно из whitespace:
System.out.println(" nrt".isBlank()); // true
Методы String.stripLeading() [33]
, String.stripTrailing() [34]
и String.strip() [35]
удаляют символы whitespace в начале строки, в конце строки или с обоих концов:
String str = " tHello, world!tn";
String str1 = str.stripLeading(); // "Hello, world!tn"
String str2 = str.stripTrailing(); // " tHello, world!"
String str3 = str.strip(); // "Hello, world!"
Заметьте, что String.strip()
не то же самое, что String.trim() [36]
: второй удаляет только символы, чей код меньше или равен U+0020, а первый удаляет также пробелы из Юникода:
System.out.println("stru2000".strip()); // "str"
System.out.println("stru2000".trim()); // "stru2000"
Метод String.repeat() [37]
конкатенирует строку саму с собой n
раз:
System.out.print("Hello, world!n".repeat(3));
Вывод:
Hello, world!
Hello, world!
Hello, world!
Наконец, метод String.lines() [38]
разбивает строку на линии. До свидания String.split()
, с которым люди постоянно путают, какой аргумент для него использовать, то ли "n"
, то ли "r"
то ли "nr"
(на самом деле, лучше всего использовать регулярное выражение "R"
, которое покрывает все комбинации). Кроме того, String.lines()
зачастую может быть более эффективен, поскольку он возвращает линии лениво.
System.out.println("line1nline2nline3n"
.lines()
.map(String::toUpperCase)
.collect(Collectors.joining("n")));
Вывод:
LINE1
LINE2
LINE3
String.indent()
Давайте разбавим наш рассказ чем-нибудь свежим, что появилось совсем недавно. Встречайте: метод String.indent() [39]
, который увеличивает (или уменьшает) отступ каждой линии в данной строке на указанную величину. Например:
String body = "<h1>Title</h1>n" +
"<p>Hello, world!</p>";
System.out.println("<html>n" +
" <body>n" +
body.indent(4) +
" </body>n" +
"</html>");
Вывод:
<html>
<body>
<h1>Title</h1>
<p>Hello, world!</p>
</body>
</html>
Заметьте, что для последней линии String.indent()
сам вставил перевод строки, поэтому нам не пришлось добавлять 'n'
после body.indent(4)
.
Конечно, наибольшый интерес такой метод будет представлять в сочетании с блоками текста [40], когда они станут стабильными, но ничто не мешает использовать его уже прямо сейчас без всяких блоков текста.
Stream
: takeWhile()
, dropWhile()
, iterate()
с предикатом и ofNullable()
Stream.takeWhile() [41]
похож на Stream.limit()
, но ограничивает Stream
не по количеству, а по предикату. Такая необходимость в программировании возникает очень часто. Например, если нам надо получить все записи в дневнике за текущий год:
[
{ "date" : "2020-01-27", "text" : "..." },
{ "date" : "2020-01-25", "text" : "..." },
{ "date" : "2020-01-22", "text" : "..." },
{ "date" : "2020-01-17", "text" : "..." },
{ "date" : "2020-01-11", "text" : "..." },
{ "date" : "2020-01-02", "text" : "..." },
{ "date" : "2019-12-30", "text" : "..." },
{ "date" : "2019-12-27", "text" : "..." },
...
]
Stream
записей является почти бесконечным, поэтому filter()
использовать не получится. Тогда на помощь приходит takeWhile()
:
getNotesStream()
.takeWhile(note -> note.getDate().getYear() == 2020);
А если мы хотим получить записи за 2019 год, то можно использовать dropWhile() [42]
:
getNotesStream()
.dropWhile(note -> note.getDate().getYear() == 2020)
.takeWhile(note -> note.getDate().getYear() == 2019);
В Java 8 Stream.iterate()
мог генерировать только бесконечный Stream
. Но в Java 9 у этого метода появилась перегрузка [43]
, которая принимает предикат. Благодаря этому многие циклы for
теперь можно заменить на Stream
:
// Java 8
for (int i = 1; i < 100; i *= 2) {
System.out.println(i);
}
// Java 9+
IntStream
.iterate(1, i -> i < 100, i -> i * 2)
.forEach(System.out::println);
Обе этих версии печатают все степени двойки, которые не превышают 100
:
1
2
4
8
16
32
64
Кстати, последний код можно было бы переписать с использованием takeWhile()
:
IntStream
.iterate(1, i -> i * 2)
.takeWhile(i -> i < 100)
.forEach(System.out::println);
Однако вариант с трёхаргументным iterate()
всё-таки чище (и IntelliJ IDEA предлагает [44] его исправить обратно).
Наконец, Stream.ofNullable() [45]
возвращает Stream
с одним элементом, если он не null
, и пустой Stream
, если он null
. Этот метод отлично подойдёт в примере выше с телефонами компании, если getPhoneNumber()
будет возвращать nullable String
вместо Optional<String>
:
class Employee {
String getPhoneNumber() { ... }
}
class Department {
List<Employee> getEmployees() { ... }
}
class Company {
List<Department> getDepartments() { ... }
Set<String> getAllPhoneNumbers() {
return getDepartments()
.stream()
.flatMap(d -> d.getEmployees().stream())
.flatMap(e -> Stream.ofNullable(e.getPhoneNumber()))
.collect(Collectors.toSet());
}
}
Predicate.not()
Этот метод не вносит ничего принципиально нового и носит скорее косметический, нежели фундаментальный характер. И всё же возможность немного подсократить код всегда очень приятна. С помощью Predicate.not() [46]
лямбды, в которых есть отрицание, можно заменить на ссылки на методы:
Files.lines(path)
.filter(str -> !str.isEmpty())
.forEach(System.out::println);
А теперь используя not()
:
Files.lines(path)
.filter(not(String::isEmpty))
.forEach(System.out::println);
Да, экономия не такая уж и огромная, а если использовать s -> !s.isEmpty()
, то количество символов, наоборот, становится больше. Но даже в этом случае я всё равно предпочту второй вариант, так как он более декларативен и в нём не используется переменная, а значит не захламляется пространство имён.
Сегодняшний рассказ я хочу завершить новым интересным API, появившимся в Java 9 и служащим для очистки ресурсов перед их утилизацией сборщиком мусора. Cleaner [47]
является безопасной заменой метода Object.finalize()
, который сам стал deprecated в Java 9.
С помощью Cleaner
можно зарегистрировать очистку ресурса, которая произойдёт, если её забыли сделать явно (например, забыли вызвать метод close()
или не использовали try-with-resources
). Вот пример абстрактного ресурса, для которого в конструкторе регистрируется очищающее действие:
public class Resource implements Closeable {
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
cleanable = CLEANER.register(this, () -> {
// Очищающее действие
// (например, закрытие соединения)
});
}
@Override
public void close() {
cleanable.clean();
}
}
По-хорошему, такой ресурс пользователи должны создавать в блоке try
:
try (var resource = new Resource()) {
// Используем ресурс
}
Однако могут найтись пользователи, которые забудут это делать и будут писать просто var resource = new Resource()
. В таких случаях очистка выполнится не сразу, а позовётся позже в одном из следующих циклов сборки мусора. Это всё же лучше, чем ничего.
Если вы хотите изучить Cleaner
получше и узнать, почему никогда не стоит использовать finalize()
, то рекомендую вам послушать мой доклад [48] на эту тему.
Java не стоит на месте и постепенно развивается. Пока вы сидите на Java 8, с каждым релизом появляется всё больше и больше новых интересных API. Сегодня мы рассмотрели 10 таких API. И вы сможете использовать их все, если наконец решитесь мигрировать с Java 8.
В следующий раз мы рассмотрим ещё 10 новых API.
Если вы не хотите пропустить следующую часть, то рекомендую вам подписаться на мой Телеграм-канал [49], где я также публикую новости Java.
Автор: Zheka Kozlov
Источник [50]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/344698
Ссылки в тексте:
[1] JDK 9: https://openjdk.java.net/projects/jdk9/
[2] JDK 10: https://openjdk.java.net/projects/jdk/10/
[3] JDK 11: https://openjdk.java.net/projects/jdk/11/
[4] JDK 12: https://openjdk.java.net/projects/jdk/12/
[5] JDK 13: https://openjdk.java.net/projects/jdk/13/
[6] JDK 14: https://openjdk.java.net/projects/jdk/14/
[7] requireNonNullElse(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Objects.html#requireNonNullElse(T,T)
[8] requireNonNullElseGet(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Objects.html#requireNonNullElseGet(T,java.util.function.Supplier)
[9] появившихся: https://openjdk.java.net/jeps/269
[10] List.of(E... elements): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/List.html#of(E...)
[11] Set.of(E... elements): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Set.html#of(E...)
[12] Map.of(K k1, V v1, K k2, V v2, ...): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Map.html#of(K,V,K,V)
[13] Map.ofEntries(Entry<? extends K, ? extends V>... entries): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Map.html#ofEntries(java.util.Map.Entry...)
[14] Map.entry(K k, V v): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Map.html#entry(K,V)
[15] появились: http://unmodifiable_collections_enhancements_in_java_10.html
[16] List.copyOf(Collection<? extends E> coll): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/List.html#copyOf(java.util.Collection)
[17] Set.copyOf(Collection<? extends E> coll): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Set.html#copyOf(java.util.Collection)
[18] Map.copyOf(Map<? extends K,? extends V> map): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Map.html#copyOf(java.util.Map)
[19] Files.readAllBytes(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/file/Files.html#readAllBytes(java.nio.file.Path)
[20] Files.readAllLines(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/file/Files.html#readAllLines(java.nio.file.Path)
[21] Files.readString(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/file/Files.html#readString(java.nio.file.Path)
[22] writeString(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/nio/file/Files.html#writeString(java.nio.file.Path,java.lang.CharSequence,java.nio.file.OpenOption...)
[23] Optional.ifPresentOrElse(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Optional.html#ifPresentOrElse(java.util.function.Consumer,java.lang.Runnable)
[24] Optional.stream(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Optional.html#stream()
[25] Process.pid(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/Process.html#pid()
[26] Process.info(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/Process.html#info()
[27] ProcessHandle.Info: https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.Info.html
[28] ProcessHandle: https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.html
[29] ProcessHandle.current(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.html#current()
[30] ProcessHandle.of(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.html#of(long)
[31] ProcessHandle.allProcesses(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.html#allProcesses()
[32] String.isBlank(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#isBlank()
[33] String.stripLeading(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#stripLeading()
[34] String.stripTrailing(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#stripTrailing()
[35] String.strip(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#strip()
[36] String.trim(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#trim()
[37] String.repeat(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#repeat(int)
[38] String.lines(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#lines()
[39] String.indent(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/String.html#indent(int)
[40] блоками текста: https://minijug.ru/text_blocks.html
[41] Stream.takeWhile(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/stream/Stream.html#takeWhile(java.util.function.Predicate)
[42] dropWhile(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/stream/Stream.html#dropWhile(java.util.function.Predicate)
[43] перегрузка: https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/stream/Stream.html#iterate(T,java.util.function.Predicate,java.util.function.UnaryOperator)
[44] предлагает: https://youtrack.jetbrains.com/issue/IDEA-214960
[45] Stream.ofNullable(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/stream/Stream.html#ofNullable(T)
[46] Predicate.not(): https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/function/Predicate.html#not(java.util.function.Predicate)
[47] Cleaner: https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ref/Cleaner.html
[48] мой доклад: https://www.youtube.com/watch?v=K5IctLPem0c
[49] мой Телеграм-канал: https://tlg.name/miniJUG
[50] Источник: https://habr.com/ru/post/485750/?utm_source=habrahabr&utm_medium=rss&utm_campaign=485750
Нажмите здесь для печати.