Newinstance java что это
Иногда это может быть требуемым, чтобы получить внутреннее состояние от объекта, который только устанавливается после конструкции. Рассмотрите сценарий, где необходимо получить внутренний набор символов, используемый java.io.Console . ( Console набор символов сохранен в частном поле и является не обязательно тем же самым как набором символов значения по умолчанию виртуальной машины Java, возвращенным java.nio.charset.Charset.defaultCharset() ). ConsoleCharset пример показывает, как это могло бы быть достигнуто:
import java.io.Console; import java.nio.charset.Charset; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import static java.lang.System.out; public class ConsoleCharset < public static void main(String. args) < Constructor[] ctors = Console.class.getDeclaredConstructors(); Constructor ctor = null; for (int i = 0; i < ctors.length; i++) < ctor = ctors[i]; if (ctor.getGenericParameterTypes().length == 0) break; >try < ctor.setAccessible(true); Console c = (Console)ctor.newInstance(); Field f = c.getClass().getDeclaredField("cs"); f.setAccessible(true); out.format("Console charset : %s%n", f.get(c)); out.format("Charset.defaultCharset(): %s%n", Charset.defaultCharset()); // production code should handle these exceptions more gracefully >catch (InstantiationException x) < x.printStackTrace(); >catch (InvocationTargetException x) < x.printStackTrace(); >catch (IllegalAccessException x) < x.printStackTrace(); >catch (NoSuchFieldException x) < x.printStackTrace(); >> >
Отметьте:
Class.newInstance() только успешно выполнится, если конструктор будет, имеет нулевые параметры и уже доступен. Иначе, необходимо использовать Constructor.newInstance() как в вышеупомянутом примере.
Пример выводил для системы Unix:
$ java ConsoleCharset Console charset : ISO-8859-1 Charset.defaultCharset() : ISO-8859-1
Пример выводил для системы Windows:
C:\> java ConsoleCharset Console charset : IBM437 Charset.defaultCharset() : windows-1252
Другое общее применение Constructor.newInstance() должен вызвать конструкторов, которые берут параметры. RestoreAliases пример находит определенного конструктора единственного параметра и вызывает это:
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.Set; import static java.lang.System.out; class EmailAliases < private Setaliases; private EmailAliases(HashMap h) < aliases = h.keySet(); >public void printKeys() < out.format("Mail keys:%n"); for (String k : aliases) out.format(" %s%n", k); >> public class RestoreAliases < private static MapdefaultAliases = new HashMap(); static < defaultAliases.put("Duke", "duke@i-love-java"); defaultAliases.put("Fang", "fang@evil-jealous-twin"); >public static void main(String. args) < try < Constructor ctor = EmailAliases.class.getDeclaredConstructor(HashMap.class); ctor.setAccessible(true); EmailAliases email = (EmailAliases)ctor.newInstance(defaultAliases); email.printKeys(); // production code should handle these exceptions more gracefully >catch (InstantiationException x) < x.printStackTrace(); >catch (IllegalAccessException x) < x.printStackTrace(); >catch (InvocationTargetException x) < x.printStackTrace(); >catch (NoSuchMethodException x) < x.printStackTrace(); >> >
Этот пример использование Class.getDeclaredConstructor() найти конструктора с единственным параметром типа java.util.HashMap . Отметьте, что достаточно передать HashMap.class начиная с параметра любому get*Constructor() метод требует class только в целях типа. Должный ввести стирание , следующее выражение оценивает к true :
HashMap.class == defaultAliases.getClass()
Пример тогда создает новый экземпляр class, используя этого конструктора с Constructor.newInstance() .
$ java RestoreAliases Mail keys: Duke Fang
Что происходит в методе newInstance?
В классе Router фреймворка vert.x есть такой конструктор и метод:
public class Router < final io.vertx.ext.web.Router delegate; public Router(io.vertx.ext.web.Router delegate) < this.delegate = delegate; >public Object getDelegate() < return delegate; >/** * Create a router * @param vertx the Vert.x instance * @return the router */ public static Router router(Vertx vertx)
в строчке непонятно что происходит:
newInstance(io.vertx.ext.web.Router.router((io.vertx.core.Vertx) vertx.getDelegate()));
вернее, результат примерно ясен-он берет инстанс-делегат vertex.но как это происходит и какой именно инстанс он берет?
io.vertx.ext.web.Router io.vertx.core.Vertx
эти строки-подключение пакетов-классов vert.x core и vert.x web. я с таким синтаксисом раньше не сталкивался(чтобы это прописывалось в теле метода.обычно все подключается в верхней части кода, например так эти строки-подключение пакетов-классов vert.x core и vert.x web. я с таким синтаксисом раньше не сталкивался,обычно все подключается в верхней части кода,например так import io.vertx.rxjava.core.Vertx;).Объясните пожалуйста как понимать данный синтаксис?и что по порядку происходит в методе newInstance()
Newinstance java что это
Как сообщает Википедия, рефлексия — это процесс во время которого программа может отслеживать и модифицировать свою структуру во время выполнения. Можно сказать, что программа «сама пишет» часть кода себя, прямо во время ее выполнения. Рефлексия — один из подвидов метапрограммирования — создание программ, которые в процессе своего выполнения порождают другие программы. В данном случае, какая-то часть (ну или вся) программы проектируется так, чтобы программа сама создавала во время выполнения некоторую логику, которую при «обычном» подходе создает программист. Для чего? Все очень просто — иногда некоторые данные, которые жизненно необходимы для создания алгоритма, недоступны на этапе разработки программы. Например: известно, что нужно будет вызывать определенные методы объекта, но какие именно — будет понятно только во время выполнения. Вот здесь на помощь приходит рефлексия. Впрочем, я хотел бы привести несколько самых простых примеров реализации рефлексии в Java. Давайте посмотрим на них — станет более ясно, когда можно использовать рефлексию для какой-то конкретной ситуации.
Пример 1: создание объекта.
Допустим нужно создать копию заданного объекта. Какого конкретно типа объект не ясно, изначально мы знаем только то, что это высший тип в иерархии объектов Java — java.lang.Object. Если возможные типы объекта известны заранее, можно предусмотреть проверку с помощью оператора instanceof. Но предположим — типов много и предусмотреть проверку для каждого из них не представляется возможным, нужно универсальное решение. После создания копии объекта с ним могут выполняться какие-то действия, опять же с помощью рефлексии. Ведь мы не знаем, объект какого типа был передан — в этом и нет необходимости. Пример очень простой:
//создаем объект непосредственно того типа, //которым является java.lang.Object source Object newObject = source.getClass().newInstance();
Метод Class.newInstance() предполагает, что у класса, экземпляром которого является заданный объект, есть конструктор по умолчанию (без аргументов). В противном случае произойдет ошибка. Разумеется есть возможность создать класс и с помощью конструктора с аргументами. Для этого используется метод java.lang.reflect.Constructor.newInstance(). Здесь я пожалуй переадресую вас к документации Oracle — Creating New Class Instances. Итак, после создания экземпляра объекта, нужно вызвать на нем какой-то метод, который не объявлен и не определен в классе java.lang.Object, но мы то знаем — он существует (кроме того, возможно на этапе написания кода имя метода неизвестно, и определяется только во время выполнения).
Пример 2: вызов метода на объекте.
Посмотрим на метод, который позволяет вызвать на объекте любой заданный по имени метод. Если такой метод не существует, будет сгенерировано исключение NoSuchMethodException. Перехват Exception указан потому что вызов метода на объекте может порождать и другие исключения.
/** * @param object Объект для вызова * @param methodName Имя метода * @param paramsTypes Типы аргументов - необязательно * @param arg Аргументы вызываемого метода - необязательно * @return Объект, который вернул целевой метод */ private Object invokeMethod(Object object, String methodName, Class[] paramsTypes, Object arg) < Object result = null; try < //получаем тип объекта, на кот. вызывается метод Class targetObjectClass = object.getClass(); //получаем метод класса Method method = findMethodAtClass(targetObjectClass, paramsTypes, methodName); //если есть аргумент if (arg != null) result = method.invoke(object, arg); else result = method.invoke(object); >catch (Exception e) < e.printStackTrace(); >return result; >
Как видно, метод позволяет вызвать любой заданный по имени метод — с аргументами или без. Нужно обратить внимание на метод findMethodAtClass. Казалось бы, мы узнали действительный тип объекта, знаем имя метода, что еще нужно?
private Method findMethodAtClass(final Class type, Class[] paramsTypes, final String methodName) throws NoSuchMethodException < ClasscurrentType = type; Method[] methods = type.getMethods(); //поиск метода с именем methodName в //заданном классе и всех его суперклассах while (currentType != null) < for (Method buff : methods) < if (buff.getName().equals(methodName)) < if (paramsTypes != null) < Class[] params = buff.getParameterTypes(); if (Arrays.deepEquals(paramsTypes, params)) return buff; > else if (buff.getParameterTypes().length == 0) return buff; > > currentType = currentType.getSuperclass(); > throw new NoSuchMethodException(); >
Дело в том, что если метод вызывается на объекте с помощью рефлексии — нужно обязательно знать, в каком классе определен этот метод. Допустим — целевой класс является подклассом суперкласса, в котором определен нужный метод, а в самом целевом классе нет такого метода — нам нужно это знать. Получить объект Method из подкласса не получится. В примере ищется определенный метод — выше по иерархии классов, начиная с текущего (тип, которым является заданный объект).
Пример 3: доступ к полям класса.
С помощью рефлексии можно получить доступ к любому полю класса по имени.

Можно прочитать и изменить значения приватных полей, и даже final полей. Если говорить более точно — можно «на лету» сделать подобные поля открытыми для чтения или/и изменения — таким образом можно получить доступ к полю, которое было объявлено как приватное, из любого класса. В качестве примера — класс с приватным final полем name. Считываем значение поля, предварительно разрешив доступ к нему, а потом меняем значение:
Глубокое погружение в Java: рефлексия и загрузчик классов. Часть 1
Парадигмы программирования можно сравнить с территориями. На каждой территории, от императивного до декларативного программирования, установлены строгие законы поведения, диктующие, что можно делать, а что нельзя. Но чаще всего разработчики оказываются на территории, где приходится осуществлять то, что не одобряется или запрещается. В таких случаях начинается поиск лазеек и способов применения системы для решения насущных задач. Иногда обнаруживаются законы, которые разрешают действия, противоречащие основным принципам системы.
В этой статье я покажу, как две уникальные особенности Java позволяют создать полезные антишаблоны на территории Java.
Территория ООП
Объектно-ориентированное программирование (ООП) является одной из наиболее распространенных моделей программирования. Оно предполагает, что логика должна строиться вокруг определенных программой типов данных. Java-разработчики сначала определяют основные типы данных, необходимые в программе, а затем включают логику в классы и определяют способы взаимодействия этих классов друг с другом.
Хотя такой подход обладает общепризнанными преимуществами, статически типизированному языку, каковым является Java, не хватает гибкости. Java требует строгого определения типов данных на этапе компиляции. Это не всегда выполнимо, поскольку часто возникают ситуации, в которых невозможно предугадать на момент разработки программы, с какими именно типами данных она будет иметь дело. Кроме того, Java-программы не обеспечивают эволюционирование классов во время выполнения. Во многих случаях это также может стать трудно разрешимой задачей. Рассмотрим некоторые проблемы проектирования.
Проектирование фреймворков
Говоря простыми словами, фреймворк — это кодовая база, которая облегчает разработку, предоставляя структуру компонентов, в некотором роде скелет, на основе которого пользователи фреймворка могут создавать проекты. Определяемые пользователем типы могут быть подключены к фреймворку для настройки.
Например, инженер-тестировщик может осознать, что чаще всего реализует одни и те же шаги по тестированию API сервиса. Причем различия в коде заключаются в основном в типах данных, представляющих сам сервис. Он понимает, что большая часть его кода пригодна для повторного использования и может быть обобщена в виде фреймворка, который будет использоваться в дальнейшем при тестировании сервиса.
Чистое ООП в данном случае создает проблему, которая заключается в том, что приходится строить логику для неоднозначных типов данных. Полезным является фреймворк, который является достаточно общим, чтобы его можно было использовать для тестирования широкого спектра сервисов. Это в корне противоречит статически типизированным языкам, таким как Java, которые требуют манипулировать типами данных через определенные классы.
Проектирование для горячей замены
Программное обеспечение с возможностью “горячей замены” позволяет изменять компоненты во время выполнения без паузы в исполнении. Например, для предприятия часто бывает затратно останавливать работу приложений для обновления кода. Поэтому оно будет стремиться разрабатывать приложения, API которых остаются модифицируемыми и после развертывания.
Первая трудность, возникающая при решении этой задачи, заключается в том, что, подобно фреймворкам, логика должна строиться на основе изменяемых компонентов и, следовательно, неоднозначных типов данных. Кроме того, эта проблема связана с дополнительной сложностью загрузки нового кода во время выполнения, что представляет собой отдельную дополнительную проблему.
Приведенные выше примеры демонстрируют две сложные задачи объектно-ориентированного программирования на языке Java.
- Возможность проектирования для неоднозначных типов данных, то есть типов данных, классы которых неизвестны и не могут быть определены во время разработки.
- Возможность эволюционирования типов данных с изменением их классов во время выполнения программы.
Посмотрим, как пара взаимосвязанных возможностей Java — рефлексия и загрузчики классов — позволяют решать две вышеперечисленные проблемы и создавать невероятно гибкие Java-программы.
Рефлексия в Java
Пожалуй, самой популярной особенностью Java является то, что это статически типизированный язык.
Статическая типизация и вызов
public class Foo
public void doSomething()
System.out.println("I did something");
>
Обычно метод doSomething() вызывается следующим образом:
Foo foo = new Foo();
foo.doSomething();
Приведенный выше код является примером статической типизации и вызова. Переменная foo считается статически типизированной, поскольку ее тип разрешается в тип Foo во время компиляции. Аналогично, компилятор связывает вызов метода doSomething() с методом экземпляра, определенным в классе Foo. При статической типизации и вызове такие языковые явления, как переменные и вызовы методов, можно рассматривать так, будто они фиксируют определенные типы перед выполнением. Для успешной компиляции необходимо, чтобы определения классов стояли на первом месте.
Естественно, что универсальные фреймворки, предназначенные для работы с любыми типами без ограничения интерфейсов, не могут использовать статическую типизацию и вызов. Вместо этого в Java для создания таких фреймворков используется малоизвестный стиль программирования, называемый рефлексивным программированием. Рефлексивное программирование позволяет нарушить статически типизированную природу Java-программ. Оно переносит присвоение типов на время выполнения, а не на время компиляции, что известно как динамическая типизация.
Рефлексивное программирование
Вот определение из учебника “Java Reflection in Action”:
“Рефлексия — это способность работающей программы исследовать себя и свое программное окружение и изменять свое поведение в зависимости от того, что она обнаружила”.
Основную деятельность разработчика можно условно разделить на две категории: творческий аспект в проектировании надежных сопровождаемых программ и программирование, эквивалентное механическому труду. Например, разработчики часто пишут код, который зависит от внешних модулей или компонентов. При изменении внешнего модуля разработчику нередко приходится копаться в коде и переписывать вызовы методов.
Рефлексивное программирование облегчает разработчику вторую задачу. Вместо того чтобы вручную выполнять такую утомительную работу, как рефакторинг кода, патчинг JAR и модификацию вызовов методов, рефлексия позволяет писать программы, которые могут делать выбор, обычно принимаемый человеком, например выбирать между классом X и классом Y или вызывать новый метод вместо старого.
Чтобы понять это, необходимо разобраться в том, как классы существуют внутри виртуальной машины Java (JVM). JVM — это изолированная среда, в которой выполняются Java-программы. По большей части она не знает о своем хост-устройстве и его файловой системе, то есть не знает о файлах .java и .class, которые создаются в процессе разработки и компиляции (подробнее об этом в разделе “Загрузчики классов”). JVM имеет собственную внутреннюю память, в которой хранит все необходимые данные, требующиеся ей во время выполнения программы, в том числе данные о классах программы. Ее память состоит из нескольких компонентов (область методов, область кучи, область стека, регистры ПК, область нативных методов).
Область кучи
Область кучи JVM (Heap Area) — это динамическое пространство памяти, в котором хранятся текущие объекты программы. Именно здесь объекты живут и существуют как программные единицы. Область кучи делится на три основные части.
- Young Generation (молодое поколение). Часть, где хранятся вновь созданные объекты.
- Old Generation (старшее поколение). Часть, где хранятся долгоживущие объекты, т. е. объекты, которые пережили определенное количество циклов сборки мусора в молодом поколении, прежде чем были переведены в старшее поколение.
- MetaSpace (метапространство). Часть, предназначенная для хранения специальных типов объектов, называемых метаобъектами (представляют метаданные программы). Здесь также хранится байт-код методов.
Чтобы язык Java вел себя ожидаемо, среда JVM должна отслеживать метаданные о программе, что позволяет ей корректно выполняться. Например, для конкретного класса JVM должна хранить такую информацию, как его модификаторы доступа, методы и их типы (статический метод или метод экземпляра). Для каждого метода хранится информация о количестве параметров и их типах, а также о возвращаемом типе метода. JVM хранит метаданные программы наиболее известным ей способом — в объектах, называемых метаобъектами. Создатели Java определили специальный набор классов, используемых JVM внутри программы, которые представляют компоненты программы и предоставляют доступ к ним. Классы, интерфейсы и методы программы моделируются как объекты, которые живут в метапространстве (в Java все является объектами, даже классы).
В пакете java.lang определен класс для типа Class:
public final class Class extends Object implements Serializable, GenericDeclaration, Type, AnnotatedElement
Этот класс определяет метаобъект, представляющий класс. Каждый пользовательский класс будет иметь соответствующий объект Class, инстанцированный для него в JVM. Класс Class определяет множество интересных методов, большинство из которых предоставляют информацию о структуре класса:
public Field[] getFields()
/* Возвращает массив, содержащий объекты Field,
представляющие все доступные публичные поля класса или интерфейса,
представленного данным объектом Class.*/
public Annotation[] getAnnotations()
/* Возвращает аннотации, присутствующие в классе. */
public Method[] getMethods()
/* Возвращает массив, содержащий объекты Method,
представляющие все публичные методы класса или интерфейса,
представленного данным объектом Class. */
public T newInstance()
/* Создает новый экземпляр класса, представленного данным объектом Class. */
Аналогично, в java.lang определен класс Method, который определяет метаобъекты, представляющие метод. Он также содержит несколько API, которые могут предоставить информацию о структуре конкретного метода, моделируемого объектом Method:
public final class Method extends Executable
public Annotation [] getDeclaredAnnotations()
/* Возвращает аннотации, присутствующие в данном методе. */
public Class getDeclaringClass()
/* Возвращает объект Class,
представляющий класс или интерфейс, который объявляет метод,
представленный данным объектом Method */
public Class[] getParameterTypes()
/* Возвращает массив объектов Class, представляющих типы параметров методов */
public Class getReturnType()
/* Возвращает объект Class,
представляющий тип возврата метода,
который представлен данным объектом Method. */
public Object invoke(Object obj, Object. args)
/* Вызывает базовый метод,
представленный данным объектом Method,
используя экземпляр класса метода и массив объектов в качестве аргументов */.
Рекомендую изучить в официальной документации API этих классов и других метаобъектов, таких как Field и Annotation. Совокупность метаобъектов в метапространстве представляет собой саму программу.
Тип Java является живым во время выполнения программы. Это не статическая сущность, а скорее живой дышащий объект в JVM.
Метаобъекты в метапространстве, хотя и имеют выделенную часть кучи, не ограничиваются только внутренним использованием JVM — они доступны для работающих программ. Программа может проводить самообследование, обращаясь к представляющим ее метаобъектам. Более того, метаобъекты открывают доступ к той части программы, которую они представляют. Как было показано выше, класс Class определяет метод newInstance() и с его помощью может вернуть объект-экземпляр класса, который он представляет. Аналогично, класс Method определяет метод invoke() , с помощью которого может быть вызван представляемый им метод.
Вернемся к определению рефлексии.
Рефлексия — это способность выполняющейся программы исследовать себя и свое программное окружение (путем обращения к метаобъектам, таким как объекты Class, представляющие ее классы, и обнаружения их внутренней структуры) и изменять свои действия в зависимости от того, что она обнаружила (поскольку можно выбрать вызов части программы, представленной определенным метаобъектом).
Но как именно это реализуется?
Динамическая типизация и вызов
Как следует из предыдущего раздела, необходимость в рефлексии в основном возникает при динамической типизации и вызове. Вызов метода doSomething() класса Foo может быть выполнен альтернативно с использованием рефлексии следующим образом:
String className ="org.example.Foo";
String methodName = "doSomething";
try // Получаем Class объекта foo (рефлексивно - объект класса Foo)
Class cls = Class.forName(className);
// Рефлексивно создаем новый экземпляр foo
Object obj = cls.getDeclaredConstructor().newInstance();
// Получаем метод doSomething
Method method= cls.getDeclaredMethod(methodName);
// Рефлексивно вызваем метод doSomething
method.invoke(obj);
>catch(Exception e)>
Приведенный выше код выполняет динамическую типизацию и вызов метода. При заданных переменных className и methodName он рефлексивно получает объект Class целевого класса и извлекает объект Method целевого метода, через который вызывает метод. Обратите внимание, что если задать в этих двух переменных разные имена классов и методов, то один и тот же код будет выполнять разные методы, поскольку во время компиляции он не привязан к какому-либо определенному типу.
Практический пример: JUnit
JUnit — это популярный фреймворк для автоматизации тестирования Java-приложений. Он позволяет автоматизировать многие аспекты тестирования, такие как отображение агрегированных отчетов о результатах тестирования или выполнение повторяющихся тестовых примеров.
Преимуществом JUnit является его универсальность: он может вызывать тестовые методы любого пользовательского класса, и пользователям даже не нужно придерживаться какого-либо интерфейса при разработке тестовых классов.
Для настройки JUnit в основном опирается на аннотации. Аннотации — это часть языка Java, которая используется для добавления метаданных или дополнительной информации к Java-программам без непосредственного влияния на аннотируемый код.
JUnit определяет аннотацию @Test для “маркировки” тестовых методов. Это означает, что JUnit знает, какие пользовательские методы следует запускать посредством этой аннотации. Например, пользователь может определить следующий класс:
public class TestsBatch
public int sum (int a, int b) return a+b;
>
@Test
public void test1() <
assertEquals(10, sum(3,7));
>
@Test
public void test2() assertEquals(100, sum(20,80));
>
JUnit запускает методы test1() и test2() , поскольку они снабжены аннотацией @Test . Работать так, как задумано, JUnit позволяет широкое использование рефлексии. С помощью рефлексии пользовательские классы обнаруживаются во время выполнения программы и проверяются на наличие методов с аннотацией @Test . Затем эти методы динамически вызываются. Базовая логика, на которой работает фреймворк, достаточно просто реализуется с помощью API рефлексии. Она заключается в следующем.
- Обнаружить/перебрать все определяемые пользователем классы.
- Для каждого класса получить его методы и проверить их.
- Для каждого метода проверить наличие аннотации @Test . Если она есть, то необходимо вызвать этот метод и обработать его результаты.
Все описанные выше действия показаны в приведенном ниже коде:
File[] files = new File("src/test/java/test").listFiles();
// перебор файлов по пути, в котором существуют определенные пользователем классы
for (File file : files) String fileName = file.getName();
// получение объекта Class, соответствующего текущему файлу
Class c = Class.forName("test." + fileName.substring(0, fileName.indexOf(".")));
// проверка того, что класс не является интерфейсом или перечислением
if (!c.isInterface() && !c.isEnum()) // получение методов, принадлежащих классу
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) // проверка наличия аннотации @test у метода
Annotation annotation= method.getAnnotation(Test.class);
if(annotation!=null) // создание нового экземпляра текущего класса
Object obj = c.getDeclaredConstructor().newInstance();
// вызов текущего метода
Object result = method.invoke(obj);
// выполнение необходимой обработки объекта результата
>
>
>
Примечание: этот пример написан мной и не относится к JUnit. Однако JUnit использует рефлексию в своих операциях.
Изначально воспринимаемая неинтуитивно (и, возможно, порождающая самый уродливый код), рефлексия оказывается очень полезной функцией, когда статически типизированная природа Java накладывает ограничения. Более того, она позволяет разработчикам создавать динамические системы и автоматизировать многие трудоемкие аспекты программирования.
В следующем разделе рассмотрим еще одну интересную особенность JVM — загрузчики классов, которые в паре с рефлексией позволяют создавать принципиально открытые расширяемые системы.
- Java 21: новый подход к созданию строк
- Заменят ли потоки данных циклы в Java?
- Кэширование Redis для максимальной производительности в Spring Boot и Java
Читайте нас в Telegram, VK и Дзен