Абстракция - это выделение свойств (полей) и действий (методов) описываемой модели необходимых для решения данной задачи и опускание других свойств/действий.
Инкапсуляция - это сокрытие сложностей внутренней реализации модели и предоставление простого и понятного интерфейса пользователю.
class SomeComplexClass {
private Object field1;
private Object field2;
// ...
private Object fieldN;
private void someComplexMethod1(/*args*/) {
// complex logic
}
private void someComplexMethod2(/*args*/) {
// complex logic
}
// ...
private void someComplexMethodN(/*args*/) {
// complex logic
}
// Сложности реализации объекта скрываются, а пользователю предоставляется простой интерфейс
public void smartMethod(Object obj){
someComplexMethod1(/*args*/);
someComplexMethod2(/*args*/);
// ...
someComplexMethodN(/*args*/);
}
}Наследование - это свойство перенимать состояние (поля) и поведение (методы) родительского класса.
class Animal {
int age;
int weight;
public void eat(){}
}
class Person extends Animal {
String name;
public void introduceAndEat(){
System.out.println("Hello, my name is " + name + ", my age is " + age + ", my weight is " + weigth +
" and now I am going to eat!");
eat();
}
}Полиморфизм: способность метода обрабатывать параметры разных типов, взаимодействуя с ними по родительскому интерфейсу.
public class Clazz {
public static void main(String[]args){
List immutableList = Arrays.asList("a", "b", "c");
List arrayList = new ArrayList();
List linkedList = new LinkedList();
// независимо от объекта, которым является параметр метода,
// если он реализует интерфейс List, метод его примет и корректно обработает
method(immutableList);
method(arrayList);
method(linkedList);
}
// также к полиморфизму можно отнести перегрузку методов
public static void method(List list) {}
public static void method(List list, int max) {}
public static void method(List list, int max, int min) {}
}byte- целое число от -128 до 127 (8 бит)short- целое число от -32768 до 32767 (16 бит)char- беззнаковое целое число, представляющее собой символ UTF-16 (буквы и цифры, 16 бит)int- целое число от -2147483648 до 2147483647 (32 бита)long- целое число от -9223372036854775808L до 9223372036854775807L (64 бита)
float- действительное число от 1.4e-45f до 3.4e+38f (32 бита)double- действительное число от 4.9e-324 до 1.7e+308 (64 бита)
boolean- true/false (32 бита везде, кроме массивов, там 8 бит)
Все остальные типы, начиная от
Object
Класс - это прототип (шаблон) некой сущности
Объект - это конкретный экземпляр класса
Интерфейс - это контракт, который обязуются соблюдать реализующие его классы.
Реализует взаимоотношение "ведет себя как". Интерфейс может иметь:
- методы (по умолчанию - абстрактные, без реализации)
- final static поля
- начиная с Java 8 - дефолтные методы с реализацией по-умолчанию
- можно реализовать сколько угодно интерфейсов
Абстрактный класс - класс, создание экземпляров которого невозможно.
Реализует взаимоотношение "является" (is-a). Абстрактный класс может иметь:
- поля, в т.ч. нестатические
- методы, в т.ч. абстрактные
- можно наследовать только 1 класс, в том числе абстрактный
Наследование абстрактного класса образует гораздо более сильную связь с родителем, чем реализация интерфейса. То есть при создании класса-наследника под капотом сначала вызывается конструктор класса-родителя, инициализируются все его поля, а потом создается класс-наследник.
Интерфейс же просто регламентирует обязательное поведение классов, которые его реализуют.
Таким образом, для поддержания более низкой связности системы рекомендуется использовать реализацию интерфейсов во всех случаях, кроме тех, где нужно выстроить явную иерархию с отношением "является".
Ключевое слово static означает принадлежность поля/метода/класса не к объекту, а к классу.
В случае с полем это означает что существует только 1 экземпляр этого поля на всю программу и можно получить к нему доступ, не создавая объект класса, в котором описано это поле, через имя класса (Class.staticField).
В случае с методом это означает что этот метод может использовать только другие статические методы и можно получить к нему доступ, не создавая объект класса, в котором описан этот метод, через имя класса (Class.staticMethod()).
Класс Object является родителем по-умолчанию для всех классов.
Из-за наследования, у всех классов есть следующие методы, определенные в классе Object:
toString()- возвращает строковое представление объектаgetClass()- возвращает специальный объект типаClass, который описывает текущий класс- методы сравнения объектов
hashCode()- возвращает число типа int, неким образом характеризующее объектequals(Object obj)- возвращает true/false в зависимости от результата сравнения объекта с объектом obj. В базовой реализации сравнивает ссылки на объекты:return this == obj
- методы для работы с потоками (могут быть вызваны только внутри
synchronizedблоков)notify()notifyAll()wait(long timeout)wait(long timeout, int nanos)wait()
finalize()- метод, который использует GC для освобождения ресурсов (категорически не надо переопределять, потому что его выполнение ничем не гарантируется)clone()- создает дубликат объекта (в базовой реализации дублируются только примитивные поля)
Класс String является текстовой строкой. Класс String - иммутабельный,
то есть значение объекта класса String нельзя изменить после создания объекта.
В JVM существуют пулы значений для String и классов-обёрток (Integer, Character и т.д.). Для String в пул строк добавляется каждая строка, созданная через оператор литерала "". Другие такие же строки не будут заново созданы и не будут занимать место в памяти, все они будут ссылаться на строку в пуле строк.
Есть 2 способа создать строку:
String s = new String()- использование оператора new не рекомендуется, потому что в таком случае будет создан новый объект, занимающий место в памяти, а не будет использовано значение из пула строк.String s = "abc"- если такого значения еще нет в пуле строк, оно там создастся. Если есть, то будет использовано значение из пула строк, экономя память.
Для строк в Java переопределен оператор "+", действие по сложению строк называется конкатенацией.
При конкатенации строк образуется новая строка, потому что строки иммутабельны
Проблема циклической конкатенации строк:
class StringTest {
public static void main(String[] args) {
String s = "abc";
for (int i = 0; i < Integer.MAX_VALUE; i++) {
s = s + 1; // циклическая конкатенация строки
}
}
}В данном случае, при исполнении цикла, будет создано 2147483647 объектов String, что крайне неэффективно для памяти.
Во избежание подобных ситуаций рекомендуется использовать мутабельный класс StringBuilder и его метод append():
class StringBuilderTest {
public static void main(String[] args) {
String s = "abc";
StringBuilder sb = new StringBuilder(s);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sb.append(1); // циклическая конкатенация строки
}
String result = sb.toString();
}
}-
equals()гарантирует: еслиo1.equals(o2)вернул true, то объекты равны. Если false, то элементы точно не равны друг другу. -
hashCode()гарантирует что если у двух объектов разные хэшкоды - эти объекты точно разные, а если хэшкоды равны то либо объекты равны, либо произошла коллизия.
Для корректной работы, при создании собственного класса, который будет хранить данные, всегда нужно переопределять методы equals() и hashCode().
Иерархия коллекций:
Интерфейс
Mapне является наследником интерфейсаIterable, поэтому просто так пройтись по Map'е нельзя. Но можно использовать вложенный в MapEntrySet, который содержит в себе пары ключ-значение
List<T> - список (массив) элементов.
ArrayList- изменяемый массив, где каждому элементу соответствует его индекс от 0 до n.- Основная коллекция для хранения однотипных данных
- Быстрый поиск по индексу
O(1) - Вставка/удаление
O(n)
LinkedList- двусвязный список, где у каждого элемента есть ссылка на предыдущий и следующий элемент.- Крайне редко используется
- Быстрая вставка/удаление
O(1) - Поиск
O(n)
Map<K, V> - отображение (словарь) элементов в формате key-value.
HashMap- словарь с хранением в хэш-структуре.- Основная коллекция для хранения K-V элементов
- Быстрый поиск
O(1) - Вставка/удаление
O(1)-O(log(n)) - Не сортированный
- Представляет собой массив односвязных списков на n <= 16 и массив красно-черных деревьев при n > 16
- Каждый элемент массива (бакет) соответствует числу от 0 до 15
- При помещении в Hash-структуру элемента высчитывается его hashcode, над ним проводят операцию, отображая его хэшкод в диапазон от 0 до 15, и кладут в соответсвующий бакет.
- Если в бакете уже есть элемент, то новый элемент кладется следующим элементом, образуя структуру односвязный список для n <= 16 красно-черное деревво при n > 16
TreeMap- словарь с хранением в красно-черном дереве.- Редко используется
- Быстрый поиск
O(log(n)) - Вставка/удаление
O(log(n))) - Сортированный, то есть элементы, которые туда кладутся, должны поддерживать
интерфейс
Comparable, или нужно указатьComparatorпри создании данной коллекции - Представляет собой красно-черное дерево:
- Бинарное - у любого элемента может быть только 2 наследника
- Сортированное - бОльший элемент кладется справа, мЕньший - слева
- Самобалансирующеяся - дерево самостоятельно перестраивается, для равномерного распределения длины ветвей
Set<T> - множество уникальных элементов.
Под капотом реализуется как Map<K,V>, у которого вместо Value - объект-заглушка.
Queue<T> - очередь элементов, контролирующая порядок поступления/извлечения элементов.
Deque- двунаправленная очередь, может работать как по принципу LIFO, так и по FIFO.
Исключения - объекты, которые создаются ("выбрасываются") в момент нестандартной ситуации в программе.
Легенда
- Красный:
Errors- критические ошибки в программе, после которых дальнейшее выполнение невозможноStackOverflowError- переполнение стека вызовов методов. Причина: слишком глубокий стек вызовов, в основном из-за некорректного использования рекурсивного вызова методаOutOfMemoryError- переполнение памяти кучи. Причина: вся память, выданная JVM, кончилась, в основном из-за слишком больших объемов данных или слишком низкой настройки максимального значения памяти кучи
- Желтый:
Checked Exceptions(проверяемые) - исключения, которые программист обязан обработать ИЛИ указать в сигнатуре метода для дальнейшего проброса.Exception- общий родитель для всех проверяемых исключений. Используется для создания собственного проверяемого исключения через наследованиеIOException- ошибки ввода/вывода. Возникают, если, например, неправильно указать путь до файла, который нужно прочитать
- Зеленый
Unchecked Exceptions(непроверяемые) - исключения, которые программист не обязан обрабатыватьRuntimeException(RTE) - общий родитель для всех непроверяемых исключений. Используется для создания собственного непроверяемого исключения через наследование.NullPoinerException(NPE) - ошибка, появляющаяся в результате вызова метода у пустого (null) объекта. Бывает крайне тяжело диагностируема.ArithmeticException,ArrayIndexOutOfBoundsExceptionи тд. - ошибки, возникающие при неправильном написании программы (семантические). Например, деление на ноль или выход за границы массива
Способы обработки исключений:
try-catch- попытаться поймать исключение в блокеtryи обработать его в блокеcatch. В блокеcatchчаще всего необходимо после обработки исключения выбрасыватьRuntimeException(e), чтобы исключение не терялось.try-catch-finally- п.1 + блокfinally, который выполнится в любом случае, было исключение или нет.try-with-resources- п.1 + можно в круглых скобках после словаtryсоздать объект, поддерживающий интерфейсAutoCloasble, тогда по окончании блокаtry-catchу этого объекта будет вызван методclose(). Этими объектами зачастую являются потоки ввода/вывода.
В блоках
catchнеобходимо располагать исключения от младшего к старшему по иерархии наследования, чтобы отлавливать наиболее конкретный exception.
Функциональный интерфейс - интерфейс, содержащий 1 метод и аннотацию уровня класса
@FunctionalInterface(необязательно, но желательно).
Основные функциональные интерфейсы:
Function<T, R>: T -> R - описывает метод преобразования аргумента из одного типа в другой (или тот же самый)Predicate<T>: T -> boolean - описывает метод преобразования аргумента в логическое выражениеConsumer<T>: T -> void - описывает метод преобразования аргумента в ничего (потребление аргумента)Supplier<t>: void -> R - описывает метод получения возвращаемого параметра из ничего (производство параметра)
Так же существуют Bi-формы этих интерфейсов,
например BiFunction<T1, T2, R> : T1, T2 -> R или BiPredicate<T1, T2> : T1, T2 -> boolean
Stream API - метод работы с коллекциями как с потоком объектов.
class StreamExample{
public static void main(String[] args) {
AtomicLong cnt = new AtomicLong();
source.stream()
.map(x -> x.squash())
.peek(x -> cnt.incrementAndGet())
.filter(x -> x.getColor() != YELLOW)
.forEach(System.out::println);
}
}Методы Stream API делятся на 3 категории:
- Начинающие:
.stream()- создает stream из коллекции.parallelStream()- создает stream из коллекции, производящий операции многопоточно, используя пулForkJoinPoolStream.generate(Supplier<T>)- генерирует бесконечный stream по описанию отSupplier
- Промежуточные (возвращают
Stream<T>):filter(Predicate<T>)- пропускают далее только элементы, подходящие по условиюPredicatemap(Function<T, R>)- преобразовывает (отображает) элемент из типа T в тип R (T м.б. = R)flatMap(Function<T, Stream<R>>)- преобразует двумерный массив в одномерный.peek(Consumer<T>)- делает действие над элементом, и кладет его назад (удобно для логгирования состояния стрима)limit(long n)- ограничивает стрим n элементамиskip(long n)- пропускает n элементовsorted()- сортирует стрим (не рекомендуется из-за потребления ЦП)distinct()- контролирует уникальность элементов стрима (не рекомендуется из-за потребления ЦП)
- Конечные (терминальные, возвращают НЕ
Stream<T>)collect(Collectors.toList())- собирает стрим обратно в коллекцию (в данном случае вList)collect(Collectors.groupingBy())- собирает стрим в Map- reduce() - "сворачивает" стрим в 1 элемент
- count() - возвращает количество элементов стрима
Stream API использует ленивый тип вычислений. То есть, пока не вызван терминальный оператор, никаких вычислений сделано не будет.
Многопоточность - способность программы исполняться на нескольких ядрах процессора одновременно.
Преимущества:
- Возможность сильно ускорить выполнение программы, за счет того, что каждый следующий процесс не будет ждать окончания предыдущего процесса Недостатки:
- При неправильном использовании может не только не ускорить программу, но и внести серьезные баги и состояния, при которых дальнейшее выполнение невозможно
Алгоритм работы (упрощенный):
- При запуске
psvmметода, JVM выделяет 1 поток (Main Thread) приложению. - При выполнении программы, можно стартовать другие потоки из потока Main или из других потоков.
- Эти потоки могут быть обычными или сервисными ("демонами", от англ. daemon).
- Main thread ждет окончания работы всех остальных НЕ-daemon потоков, затем заканчивает выполнение программы.
- Создать класс-наследник
Thread, переопределить в нем методrun(), где указать всю логику, которую будет выполнять поток. Создать экземпляр этого класса и вызвать методstart()(НЕ run()). - Создать класс, реализующий интерфейс
Runnable, переопределить в нем методrun(), где указать всю логику, которую будет выполнять поток. Положить этот класс в конструктор класса Thread при создании объекта Thread, на созданном объекте вызвать методstart()(НЕ run()). - Создать объект класса
Thread, в конструктор положить анонимный класс, реализующий интерфейсRunnable, переопределить в нем методrun(), где указать всю логику, которую будет выполнять поток, или просто указать лямбду, как в примере ниже. На созданном объекте вызвать методstart().
class ThreadExample{
public static void main(String[] args) {
Thread myThread = new Thread(() -> {
// TODO implement thread logic here
});
myThread.start();
}
}- Используя ExecutorFramework, создать пул потоков и в него отправлять задачи (сабмитить таски).
class ExecutorExample{
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
// TODO implement thread logic here
});
}
}При реализации многопоточности одной из главных проблем является контроль доступа потоков к общим ресурсам.
Например, есть файл, в который одновременно пишет 2 потока, а 3-ий из него читает.
Эта проблема называется Race Condition (состояние "гонки"), при котором становится важно, какой поток каким по
счету использует общий ресурс (файл). Но JVM не гарантирует порядок выполнения потоков!
Для решения подобных проблем существует ключевое слово synchronized, которое гарантирует, что данный метод/блок кода
в одну единицу времени может исполнять только 1 поток.
class SynchronizedExample{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
accessToFile();
});
Thread thread2 = new Thread(() -> {
accessToFile();
});
thread1.start();
thread2.start();
}
// Только 1 поток сможет исполнять этот метод в одну единицу времени.
public synchronized void accessToFile(){
// access to file (create, read, write, etc.)
}
}При реализации многопоточности существует проблема: каждое ядро процессора обладает своим кэшем, куда складывает наиболее часто используемые данные, чтобы уменьшить количество чтений из памяти и увеличить скорость. Но после кэширования, некоторые переменные могут быть изменены другим потоком, а тот поток, который закэшировал эту переменную, ничего об этом не знает.
Решение проблемы - запретить процессору кэшировать некоторые данные.
Ключевое слово volatile означает, что процессору запрещается кэшировать данную переменную,
и нужно обращаться к ней только из общей памяти.
При реализации многопоточности существует проблема: такие операции, как, например,
i++;(инкремент с присвоением) хоть и написаны в одно действие, фактически, процессором выполняются как 2 разных действия. В момент, когда первое действие уже выполнено, а второе еще нет, переменная может быть изменена другими потоками. В таком случае, результат командыi++;не определен.
Решение проблемы - использовать специальные Atomic-классы, любые методы в которых происходят
за одно действие процессора.
class NotAtomicExample{
public static void main(String[] args) {
int i = 0;
int a = 0;
Thread thread1 = new Thread(() -> {
i++; // ЦП выполнит операцию за 2 действия
a = i;
});
Thread thread2 = new Thread(() -> {
i+2; // ЦП выполнит операцию за 2 действия
a = i;
});
thread1.start();
thread2.start();
// в итоге точное значение a не определено
}
}
class AtomicExample{
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
int a = 0;
Thread thread1 = new Thread(() -> {
i.incrementAndGet(); // ЦП выполнит операцию за 1 действие
a = i.get();
});
Thread thread2 = new Thread(() -> {
i.addAndGet(2); // ЦП выполнит операцию за 1 действие
a = i.get();
});
thread1.start();
thread2.start();
// в итоге точное значение a определено
}
}Kotlin - многоцелевой язык, который может компилироваться в java, javascript и в нативный код
var- ключевое слово для обозначения поля, ссылку которого можно изменять после инициализации поля (англ. variable). Аналог обычного поля в Java.val- ключевое слово для обозначения поля, ссылку которого нельзя изменять после инициализации поля (англ. value). Аналог final поля в Java.
Всегда рекомендуется использовать val, кроме тех случаев, когда необходимо использовать var.
companion object - аналог static в Java. Зачастую используется как блок кода, в котором определяются константы.
class SomeClass {
companion object {
const val BASE_RATE_PERCENT = 13
}
}
Чтобы сделать функцию, которая для Java будет как статическая, можно объявить её на уровне файла:
class SomeClass {
// в файле объявлен класс (может быть и не объявлен)
}
fun someFun() {
// на верхнем уровне файла объявлена функция, которая при компиляции будет считаться статической
// функцией класса ИмяФайлаKt
}
Extension-функция (функция-расширение) - функция, которая создается с указанием класса и имеющая область видимости этого класса внутри себя. Эту функцию можно использовать так, будто это функция объявлена в самом классе.
fun String.isEmail(): Boolean {
return this.matches(".*@.*\..*")
}
В Java Extension-функция компилируется как класс
(ClassName)ExtensionKtсо статическим методом с n+1 аргументом, где n - число аргументов extension-функции, а +1 -this
class StringExtensionKt {
public static Boolean isEmail(String obj){
return obj.matches(".*@.*\..*");
}
}Null-safety - функционал языка Kotlin, который делает проверку на null обязательной для всех типов. Грамотное
использование null-safety может избавить программу от одной из самых неприятных ошибок - NullPointerException
Основные операторы null-safety:
val str: String?- объект может хранить в себе ссылкуnullval str: String- объект не может хранить в себе ссылкуnullstr?.toUpperCase()- вызов метода произойдет только еслиstr != nullstr!!.toUpperCase()- вызов метода произойдет в любом случае, в т.ч. еслиstr == null. Крайне не рекомендуетсяstr?.toUpperCase() ?: "empty"- Элвис-оператор - еслиstr == null, то выражение вернет"empty"String!- Kotlin не знает ничего о нуллабельности этого объекта. Например, при вызове метода, написанного на Java, аргументы которого не помечены аннотацией@NotNull
Иммутабельность ("неизменчивость") - свойство коллекции, которое запрещает изменять её после создания. Это свойство делает использование коллекции более безопасным, особенно в многопоточной среде (несколько потоков не смогут изменять эту коллекцию одновременно, только читать)
По умолчанию все коллекции в Kotlin иммутабельные. Чтобы создать mutable-коллекцию, надо явно указать при создании тип с приставкой
Mutable
val list = listOf("a", "b") // immutable
val mutableList = MutableListOf("a", "b") // mutable
val list: List<String> // immutable
val mutableList: MutableList<String> // mutable
Sequence - аналог Java Stream API, использует ленивые вычисления (пока не вызовется терминальный оператор,
никаких операций произведено не будет)
В Kotlin есть возможность прямо на коллекциях вызывать методы вроде
map{},filter{}и тд. Но в таком случае вычисления будут жадными, то есть на каждый вызов такого оператора будет создана новая коллекция. Для оптимизации вычислений необходимо вместо длинной цепочки вызовов этих методов сначала вызвать методasSequence(), а в конце метод, собирающий в коллекцию, напримерtoList()
Any- аналог java-типаObject, является родителем для всех классов.Unit- аналог java-типаvoid. Явно можно не указывать.Nothing- используется как возвращаемое значение в тех случаях, когда метод не закончится никогда, например, бросая исключение. Является наследником для всех классов.
with- принимает объект, далее внутри {} можно обращаться к полям и методам объекта без указания его имени.let- выполняет блок кода в {}. Удобно использовать как "при не null сделай что-то", для этого нужно использовать конструкцию?.let{}.run- то же, что иwith, только не принимает объект, а вызывается на нем, какlet.apply- вызывается на объекте используя его область видимости в {}, возвращает этот же объект.
Data-класс, сущность в kotlin, которая используется преимущественно для хранения данных.
Свойства Data-классов:
toString()метод переопределен корректноequals() & hashCode()так же переопределеныcomponentN()- функция, способная "разобрать" объект на его поля, напримерval (name, age, height) = personcopy()- метод, копирующий объект. В аргументы принимает те поля, которые хочешь изменить в полученном объекте
Требования:
- Ключевое слово data в начале объявления класса
- Обязательно указывать primary-конструктор
- Все параметры primary-конструктор должны быть помечены
val/var - Не могут быть
open(нельзя наследоваться),abstract(абстрактными),sealed(быть с фиксированными наследниками),inner(вложенными)
Не рекомендуется использовать data-классы как JPA Entity (хотя всё равно все так делают) по следующим причинам:
- data class не может быть open, а для JPA должен (решается подключением плагина all-open, при создании через spring initializr подключается автоматически)
equals()&hashCode()определяются при создании объекта, а hibernate может установить@Idобъекта после его создания, в момент помещения в бд. Можно обойти, используя в качестве@Idне суррогатный ключ, а бизнес-ключ.
В Kotlin можно создавать/возвращать из метода анонимный объект, используя ключевое слово object
// Создание анонимного объекта
val helloWorld = object { // Можно указывать супер-типы через `:` для наследования
val hello = "Hello"
val world = "World"
// object наследует Any, поэтому слово `override` необходимо для переопределения `toString()`
override fun toString() = "$hello $world"
}
// Возвращение из метода
private fun getObject() = object {
val x: String = "x"
}В Kotlin есть поддержка паттерна Singleton прямо из коробки, используя слово object:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
}
// использование:
DataProviderManager.registerDataProvider(/**/)
Данный singleton является потокобезопасным
- Внедрение и управление зависимостями (Dependency injection).
- Инверсия управления (IoC) - Сущность не сама создает свои зависимости, а зависимости поставляются ей извне
Слово "зависимость" может употребляться в 2-х значениях в контексте разработки на Spring:
- Maven-зависимость проекта (тэг
dependencyв pom.xml)- Зависимость сущностей внутри проекта (В бин внедрен другой бин, например, через
@Autowired)
<dependency>
<!-- Зависимость в контексте maven-->
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>@Component
class SomeComponent {
@Autowired
AnotherComponent dependencyComponent; // зависимость во всех остальных смыслах, кроме maven
}- Через конструктор (основной). Преимущества:
- Не нужно писать
@Autowired - Обнаружение циклических зависимостей на этапе старта приложения
- Не нужно писать
@InjectMocksдля unit-тестов
- Не нужно писать
- Через поле (не рекомендуется). Когда зависимость внедряется через поле, то Spring использует механизм рефлексии "расковыривая" объект во время выполнения и находя места для внедрения зависимостей. Не рекомендуется из-за большой нагрузки во время рефлексии.
- Через сеттер (устарело)
Bean - это "спринговский объект", то есть обычный экземпляр java-класса, который положили в контекст спринга
SpringApplicationContext - контейнер бинов (Map<String, Object>, где key - id бина, value - экземпляр бина).
- Через XML (устарело)
- Через java-код + XML (устарело)
- Через аннотации (основной):
@Component- аннотация уровня класса, указывает спрингу, что от данного класса нужно создать бин@Bean- вешается над методом, возвращаемое значение которого будет являться бином. Используется в классах помеченных@Configuration
Пример с @Component:
@Component //Здесь мог бы быть @Service, @Repository, @Controller, @RestController
class SomeComponent(
val dependencyComponent: AnotherComponent //внедряется зависимость
) {
fun someFun(){
// логика
}
}
Пример с @Bean & @Configuration:
@Configuration //класс-конфигурация помечается аннотацией @Configuration
class EmailConfig {
@Bean //только в классе @Configuration можно над методом вешать аннотацию @Bean
// Суть метода: Создать, правильно сконфигурировать объект и вернуть его, чтобы Spring из него создал бин
fun javaMailSender(): JavaMailSender {
val mailSender = JavaMailSenderImpl()
mailSender.host = nopsEmailProperties.host
mailSender.port = nopsEmailProperties.port
mailSender.username = nopsEmailProperties.username
mailSender.password = nopsEmailProperties.password
return mailSender
}
}
Популярный вопрос:
@Beanvs.@Componentvs.@ComponentScanvs.@Configurationvs.@Autowired
@Component- аннотация уровня класса, которая указывает спрингу создать бин из этого класса. Используется для написания собственных бинов@Bean- аннотация уровня метода в классе, помеченном@Configuration, которая указывает спрингу, как создавать и конфигурировать бин. Чаще используется для конфигурирования и создания бинов из уже существующих классов@Configuration- аннотация уровня класса, которая указывает спрингу искать в этом классе методы, помеченные@Beanдля создания бинов@ComponentScan- аннотация уровня класса, которая указывает спрингу, что этот пакет и все подпакеты следует сканировать на предмет аннотаций@Componentи@Configuration@Autowired- аннотация уровня поля (не рекомендуется) или конструктора (можно не указывать) в классе, помеченном@Component, которая указывает спрингу, что сюда нужно внедрить зависимость (бин)
- init-метод вызывается сразу после внедрения в бин всех необходимых зависимостей.
- destroy-метод вызывается после остановки контекста
@Component
class SomeComponent(
val dependencyComponent: AnotherComponent //внедряется зависимость
) {
@PostConstruct //метод вызывается сразу после внедрения в бин всех необходимых зависимостей.
fun init(){
println("I am created!:)")
}
@PreDestroy //метод вызывается после остановки контекста только для scope = singleton
fun destroy(){
println("I am going to be destroyed:(")
}
}post-destroy у бинов со скоупом prototype не вызывается
- Singleton (по умолчанию). Бин с данным именем создаётся 1 раз в Spring Application Context и каждый раз при вызове getBean() вызывается тот самый бин. Используется, когда у бина нет изменяемых состояний (stateless)
- Prototype. При вызове getBean() каждый раз будет создаваться новый бин в Spring Application Context. Используется, когда у бина есть изменяемые состояния (statefull)
- Request. Создаётся один экземпляр бина на каждый HTTP запрос. Касается исключительно ApplicationContext.
- Session. Создаётся один экземпляр бина на каждую HTTP сессию. Касается исключительно ApplicationContext.
- Global-session. Создаётся один экземпляр бина на каждую глобальную HTTP сессию. Используется только с портлетами. Касается исключительно ApplicationContext.
@ComponentScanвместе с аннотацией@Configuration, чтобы указать пакеты, которые мы хотим сканировать.@ComponentScanбез аргументов указывает Spring сканировать текущий пакет и все его подпакеты.@Autowiredвнедряет ссылку на существующий бин в поле, над которым висит@Autowired. Если scope бина “prototype”, то при@Autowiredполю присваивается ссылка на новый бин.
Внедрять зависимости через
@Autowiredследует через конструктор, тк через конструктор зависимости могут быть final, а значит потокобезопасными. Также при внедрении через конструктор можно не писать @InjectMocks при написании юнит-тестов.
@Service,@Repository- аналоги@Component(синтаксический сахар)@Controller(@RestController) говорит спрингу исследовать данный класс на наличие аннотации@RequestMapping. Другие стереотипные аннотации такого не делают@RestController=@Controller+@ResponseBody@Qualifier. Используется для указания id конкретного бина для внедрения в другой бин при условии, что есть несколько бинов одного типа@Primary. Определяет предпочтение, когда присутствует несколько bean-компонентов одного типа. Компонент, связанный с аннотацией @Primary, будет использоваться, если не указано иное.@Transactional. Делает методы над которыми висит транзакционными (обладающими свойствами транзакции)
Транзакционность - свойство, при котором выполняемые методы поддерживают правила транзакционности (ACID):
- A - атомарность - действие неделимое, то есть может быть только либо полностью выполнено, либо нет
- C - согласованность - выполняемое действие должно быть корректно с точки зрения бизнес-логики (в БД в колонку "дата рождения" нельзя сохранить "имя")
- I - изолированность - транзакции не должны мешать друг другу. Для нахождения компромисса между потреблением
ресурсов и изолированностью существует 4 уровня изолированности транзакции:
READ UNCOMMITTED- Если несколько параллельных транзакций пытаются изменять одну и ту же строку таблицы, то в окончательном варианте строка будет иметь значение, определенное всем набором успешно выполненных транзакцийREAD COMMITTED- (основной) - защита от чернового, «грязного» чтения, тем не менее, в процессе работы одной транзакции другая может быть успешно завершена и сделанные ею изменения зафиксированыREPEATABLE READ- читающая транзакция «не видит» изменения данных, которые были ею ранее прочитаны. При этом никакая другая транзакция не может изменять данные, читаемые текущей транзакцией, пока та не окончена.SERIALIZABLE- транзакции полностью изолируются друг от друга, каждая выполняется так, как будто параллельных транзакций не существует
- D - долговечность - результат транзакции остается навсегда и не зависит от внешних событий (отключение питания БД)
Все операции с БД необходимо делать транзакционными. Это значит, что на все методы, в которых используется объект для доступа к бд (repository), должны быть помечены аннотацией
@TransactionalДля реализации транзакционности (как и для все остальной Spring-магии) используется механизм проксирования - то есть логику выполняет не оригинальный объект, а его прокси (заместитель), который содержит всю Spring-логику И требуемую логику
public class MyServiceImpl {
@Transactional
public void method1() {
//do something
method2();
}
@Transactional
public void method2() {
//do something
}
}Ответ: Для поддержки транзакций через аннотации используется Spring AOP, в момент вызова method1() на самом деле вызывается метод прокси объекта. Создается новая транзакция и далее происходит вызов method1() класса MyServiceImpl. А когда из method1() вызовем method2(), обращения к прокси нет, вызывается уже сразу метод нашего класса и, соответственно, никаких новых транзакций создаваться не будет
Решение::
- Рефакторинг. Создание новой абстракции (например
Facade), которая будет атомарно вызывать методы сервиса, помеченные@Transactional. Таким образом избегается перекрестный вызов транзакционных методов друг-другом - Self-inject класса и вызов метода через него (
@Autowiredсамого в себя). Работает только для бинов со scopesingleton, дляprototypeпри старте приложения упадёт ошибка циклического внедрения бина самого в себя.
@Service
@Transactional
class Clazz {
@Autowired
Clazz clazz;
void method1(){//транзакция создается
clazz.method2();//транзакция создается
}
void method2(){
}
}- Использовать
TransactionTemplateдля полного управления транзакциями:
class TransactionTempalateExample{
public static void main(String[] args) {
transactionTemplate.execute(new TransactionCallback()
{
// код в этом методе выполняется в транзакционном контексте
public Object doInTransaction(TransactionStatus status)
{
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
Spring Data JPA (Java Persistence API) - модуль Spring, реализующий взаимодействие с БД.
Spring Data JPA - является оберткой над самой популярной Java ORM - Hibernate
Hibernate является ORM (Object-relational mapping, объектно-реляционное отображение) - "мощной оберткой" над JDBC
JDBC (Java Database Connectivity) - протокол взаимодействия Java с реляционными БД
Преимущества:
- Сильное уменьшение количества boilerplate-кода в отличие от обычного JDBC
- Сильное уменьшение конфигураций, в отличие от Hibernate
- Простота использование - минимум кода, максимум функциональности
- Реализация транзакционности
- Интеграция с другими модулями Spring
Недостатки:
- Часто формируются очень не оптимальные запросы к БД
- Медленнее JDBC
- Чем сложнее функционал, тем сложнее и глубже настройка
PagingAndSortingRepository<E, ID>JpaRepository<E, ID>CrudRepository<E, ID>(содержитJpaRepositoryиPagingAndSortingRepository) - основной
https://yannbriancon.com/blog/eliminate-hibernate-n-plus-one-queries/
Проблема N+1 это при @FetchType.EAGER подгружается вся структура сущности + ее наследники (которых N штук).
Каждая из дочерних сущностей подгружается отдельным запросом. Итого идут запросы на получение N
(всех связанных элементов) + 1(основной элемент).
Подход Spring Data:
@FetchType.LAZY
User one2many Role
public interface UserRepository extends CrudRepository<User, Long> {
// Проблема
List<User> findAllBy(); // происходит проблема N+1
// Решение 1
@Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")
List<User> findWithoutNPlusOne(); // используя LEFT JOIN, мы решаем проблему N + 1
// Решение 2
@EntityGraph(attributePaths = {"roles"})
List<User> findAll(); //используя attributePaths, Spring Data JPA позволяет избежать проблемы N + 1
}Lombok - библиотека, позволяющая избежать java boilerplate (большое количество "обслуживающего" кода, который ничего толком не делает)
Основные аннотации:
@AllArgsConstructor- конструктор со всеми параметрами (не рекомендуется для Spring)@NoArgsConstructor- конструктор по-умолчанию, без параметров - необходим для JPA@RequiredArgsConstructor- конструктор только с final-полями (рекомендуется для Spring)@Getter- гет-методы для всех полей@Setter- сет-методы для всех полей@EqualsAndHashCode- методыequals() & hashCode()в их эталонных реализациях@EqualsAndHashCode.Exclude- исключить поле изequals() & hashCode(), например, для служебных полей или для избежания зацикливания в коллекциях с отношением ManyToOne и ManyToMany
@ToString- методtoString()@ToString.Exclude- исключить поле изequals() & hashCode(), иногда нужно для избежания зацикливания в коллекциях с отношением ManyToOne и ManyToMany
@Accessors(fluent = true)- сет-методы для всех полей, возвращающие this (то есть их можно вызвать через цепочку)@Builder- реализует паттерн Builder, который делает создание объектов более лаконичным, черезMyObject.builder().someSetters().build()@Data- аккумулирует аннотации@ToString,@EqualsAndHashCode,@Getterна все поля,@Setterна все не-final поля, и@RequiredArgsConstructor@Slf4j- добавляет объект-логгер в класс.
HTTP (Hypertext transition protocol) - протокол взаимодействия по сети.
Основной состав HTTP-запроса:
- метод (
GET,POST,PUT,DELETEи тд.) - url - адрес веб-сайта (
http://vk.com) - path variable - переменная пути (
/application/123456- 123456 является path variable) - request params - параметры запроса (
http://vk.com?id=123456&view=mobile- ?id=123456&view=mobile - параметры запроса, начинаются с?, значение через=идут через&) - request body - тело запроса (
{ "age" : 25, "name" = "Alex" }- в формате JSON) - headers - заголовки - key-value хранилище заголовков и их значений. Метаинформация о запросе.
Основной состав HTTP-ответа:
- response status - статус ответа (
200) - response body - тело запроса (
{ "age" : 25, "name" = "Alex" }- в формате JSON)
200- успешные статусы300- статусы перенаправления400- статусы ошибок клиента500- статусы ошибок сервера
GETЦель – получение данных с сервера. Запрос может содержать параметры и переменные пути. Тело запроса пустое. Может передавать только пары ключ-значение. GET-запрос идемпотентен.POSTЦель – создать данные на сервере. Все параметры хранятся в теле запроса. Тип данных (ключ-значение, JSON, XML, и т.д.).PUTЦель – обновить данные на сервере. PUT-запрос идемпотентен. Может содержать как тело, так и параметры запроса с переменными пути.DELETEЦель – удалить данные. DELETE-запрос идемпотентен. Может содержать параметры и переменные пути.
Метод HTTP является идемпотентным, если повторный идентичный запрос, сделанный один или несколько раз подряд, имеет один и тот же эффект, не изменяющий состояние сервера.
Другими словами, идемпотентный метод не должен иметь никаких побочных эффектов (side-effects), кроме сбора статистики или подобных операций. Корректно реализованные методы GET, HEAD, PUT и DELETE идемпотентны, но не метод POST. Также все безопасные методы (GET, HEAD или OPTIONS) являются идемпотентными
REST - representative state transfer
- client-server. Отделение потребности интерфейса клиента от потребностей сервера, хранящего данные, повышает переносимость кода клиентского интерфейса на другие платформы, а упрощение серверной части улучшает масштабируемость.
- stateless. в период между запросами клиента никакая информация о состоянии клиента на сервере не хранится
- cacheability. Возможеость сервера кешировать ответы (при этом клиент должен знать, какой ответ был отправлен сервером из кеша)
- layerd-system. Клиент не способен определить, взаимодействует он напрямую с сервером или же с промежуточным узлом, в связи с иерархической структурой сетей. Применение промежуточных серверов способно повысить масштабируемость за счёт балансировки нагрузки и распределённого кэширования
- Код по требованию. REST может позволить расширить функциональность клиента за счёт загрузки кода с сервера
- Единообразие интерфейсов. Унифицированные интерфейсы позволяют каждому из сервисов развиваться независимо
- Использовать
WebClientвместо устаревшегоRestTemplate - Сначала делается запрос и в ответе приходит id запроса. Далее с периодичностью в некоторое время делается запрос с этим айдишником к серверу и когда данные по айдишнику появляются, при следующем запросе они отдаются в ответ. (аналог с circuit breaker)
Kafka - асинхронный брокер сообщений, реализующий модель издатель-подписчик. Основная суть взаимодействия по Kafka: издатель может ничего не знать о подписчиках, он просто посылает сообщения в топик, в то время как подписчик может подписываться на топик, получая все сообщения от издателя асинхронно, не взаимодействуя с издателем напрямую.
- Модель publisher-subscriber
- У каждого подписчика есть свой offset - номер первого непрочитанного сообщения
- Подписчики, которые читают сообща, находятся в одной consumer-группе, у них общие offset'ы
- Сообщения распределяются по партициям ключу партицирования. От каждого сообщения считается hash-функция и высчитывается остаток от деления на число партиций
- Для отказоустойчивости используются брокеры, которые содержат в себе копии сообщений. Обычно брокера 3
- Удаляются сообщения согласно 2м стратегиям: по времени (default- неделя) или по размеру очереди
- Чтение обычно происходит пачками. Настройка
max.pool.records. После изучения новой пачки фиксируется новый offset. - Время чтения одной пачки сообщений настраивается через
max.pool.intervals.ms. Если consumer не успевает прочитать сообщения за отведенное время, его отстраняют, заменяя следующим, группу перебалансируют.
- Асинхронное взаимодействие. Нам не важен ответ; Последующее действие не зависит от результата асинхронной операции
- Амортизация нагрузки. Отправителю не нужно ждать, когда принимающая сторона обработает всю инфу.
- Потоковая обработка данных. Возможность обрабатывать большое количество сообщений в секунду.
- Репликация данных. Мастер-система пушит в кафку каждое изменение данных, Системы считывают изменения и актуализируют собственные копии мастер-данных.
- Event-driven architecture.
-
S single responsibility principle- принцип единственности ответственности. У каждого класса должен быть только один мотив для изменений. "Один класс - одна задача"
-
O open/closed principle – принцип открытости\закрытости. Класс должен быть открыт для расширения и закрыт для изменения. Нужно стараться расширять (extend) классы, а не изменять их (Не ломать уже существующий код)
-
L Принцип подстановки Лисков. Подклассы должны дополнять, а не замещать поведение базового класса. (Тип ПАРАМЕТРОВ МЕТОДА подкласса должен совпадать или быть более абстрактным, чем типы параметров базового класса, и наоборот тип ВОЗВРАЩАЕМОГО ЗНАЧЕНИЯ метода подкласса должен совпадать или быть подтипом возвращаемого значения базового класса)
-
I interface segregation principle. Принцип разделения интерфейса. Клиенты не должны зависеть от методов, которые они не используют. (нужно стараться делать узкоспециализированные интерфейсы, чтобы не приходилось реализовывать избыточное поведение)
-
D Принцип инверсии зависимости. Классы верхних уровней не должны зависеть от классов нижних уровней. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций. (Зависимости должны быть не напрямую (сильными), а через абстракции(слабыми)). Прим.: Зависимости через интерфейсы, а не через наследование
- Декоратор. Композиция. Один класс внедрён в другой класс как поле.
class CoolClass {
public void doCoolStuff() {
// cool logic
}
}
class CoolClassDecorator {
private CoolClass coolClass; // один класс содержит другой как поле (композиция), расширяя и дополняя его функционал
public void doCoolerStuff() {
// cooler logic
coolClass.doCoolStuff();
// cooler logic
}
}- Фасад. Инкапсуляция. сложная реализация спрятана за удобными методами.
class ServiceA {
// some complex logic
}
class ServiceB {
// some complex logic
}
class ServiceC {
// some complex logic
}
class ABCFacade() {
private ServiceA serviceA;
private ServiceB serviceB;
private ServiceC serviceC;
public void smartMethod() { // инкапсуляция сложной логики за простым и понятным API (интерфейсом взаимодействия)
serviceA.doComplexLogic();
serviceB.doComplexLogic();
serviceC.doComplexLogic();
}
}- Фабрика. Очень похож на Builder. Специальный класс, которому делегируем создание экземпляра другого класса. Те же функции берет на себя и фабричный метод.
import java.util.List;
class Car {
Engine engine;
List<Wheel> wheels;
Transmission transmission;
// Внутри основного класса создается статический класс-билдер, у которого все те
// же поля, но есть сэттеры без приставки "set" и возвращающие this (CarBuilder),
// чтобы их моно было вызывать через цепочку вызовов через "."
public static class CarBuilder {
Engine engine;
List<Wheel> wheels;
Transmission transmission;
public CarBuilder engine(Engine engine) {
this.engine = engine;
return this;
}
public CarBuilder wheels(List<Wheel> wheels) {
this.wheels = wheels;
return this;
}
public CarBuilder transmission(Transmission transmission) {
this.transmission = transmission;
return this;
}
public Car build() {
return new Car(this);
}
}
public static CarBuilder builder() {
return new CarBuilder();
}
private Car(CarBuilder builder) {
this.engine = builder.engine;
this.wheels = builder.wheels;
this.transmission = builder.transmission;
}
}
// пример использования
public class BuilderTest {
public static void main(String[] args) {
Car car = Car.builder() // сначала вызывается статический метод build()
.engine(new Engine()) // потом сэттятся все поля
.wheels(List.of(new Wheel(), new Wheel(), new Wheel(), new Wheel()))
.transmission(new Transmission())
.build(); // в конце вызывается метод build(), который возвращает созданный объект
}
}- Singleton. Разрешает создавать не более одного экземпляра класса.
public class Singleton {
private static Singleton instance; // создается единственное статическое поле instance
private Singleton(){} // конструктор - приватный
// synchronized чтобы несколько потоков одновременно не создали несколько экземпляров Singleton'a
public static synchronized Singleton getInstance() { // создается static метод
if (instance == null) { // если поле instance == null, значит синглтон еще не создавался
instance = new Singleton(); // поэтому, создаем синглтон
}
return instance; // возвращаем instance (либо только что созданный, либо уже существующий)
}
}- Strategy. В зависимости от состояния выполнить то или иное действие (switch/case, if/else)
public interface Navigation { // есть базовый интерфейс для построения маршрута
void buildRoute();
}
public class CarNavigation implements Navigation { // реализация построения маршрута для автомобиля
@Override
void buildRoute(){
// build route for car
}
}
public class BusNavigation implements Navigation { // реализация построения маршрута для автобуса
@Override
void buildRoute(){
// build route for bus
}
}
public class BikeNavigation implements Navigation { // реализация построения маршрута для велосипеда
@Override
void buildRoute(){
// build route for bike
}
}
public enum RouteType { // тип маршрута
CAR, BUS, BIKE
}
public class NavigationStrategy { // стратегия выбора маршрута
private CarNavigation carNavigation; //содержит в себе все варианты составителей маршрутов
private BusNavigation busNavigation;
private BikeNavigation bikeNavigation;
public buildRouteByType(RouteType type) { // по типу маршрута выбирает подходящий класс-исполнитель
Navigation navigation;
switch (type) {
CAR -> navigation = carNavigation;
BUS -> navigation = busNavigation;
BIKE -> navigation = bikeNavigation;
}
navigation.buildRoute(); // выполняет целевой метод (благодаря полиморфизму)
}
}Бинарный поиск - метод поиска элемента в отсортированном массиве, при котором сложность поиска будет O(log n), потому каждую следующую итерацию отсекается одна половина исходных данных
class BinarySearcher {
public int runBinarySearchRecursively(
int[] sortedArray/*сортированный массив*/,
int key /*искомое число*/,
int low/*индекс наименьшего элемента в массиве*/,
int high/*индекс наибольшего элемента в массиве*/) {
int middle = low + ((high - low) / 2);// находим середину
if (high < low) {
return -1;
}
if (key == sortedArray[middle]) { // если ключ - середина, возвращаем его
return middle;
} else if (key < sortedArray[middle]) { // если ключ меньше центрального элемента, выполняем рекурсивный вызов с параметрами low, middle - 1
return runBinarySearchRecursively(
sortedArray, key, low, middle - 1);
} else {
return runBinarySearchRecursively( // если ключ меньше середины, выполняем рекурсивный вызов с параметрами middle + 1, high
sortedArray, key, middle + 1, high);
}
}
}Сортировка пузырьком - алгоритм сортировки сложности O(n^2) (плохая), основная идея которого - попарное сравнение элементов и если левый элемент больше правого, то их меняют местами.
public class BubbleSortExample {
static void bubbleSort(int[] arr) {
int n = arr.length;
int temp = 0;//временное поле для запоминания
for (int i = 0; i < n; i++) {
for (int j = 1; j < (n - i); j++) { // второй цикл сужается каждую итерацию
if (arr[j - 1] > arr[j]) { // сравниваем левый элемент с правым
//swap elements
temp = arr[j - 1]; //
arr[j - 1] = arr[j]; // если левый - больше, меняем местами
arr[j] = temp; //
}
}
}
}
}public class GreatestOne {
public int find(int[] array) {
int max = Integer.MIN_VALUE; // хранилище максимального элемента объявляем так, чтобы любое число было не меньше него
for (int i = 0; i < array.length; i++) {
if (max < array[i]) { // если элемент больше, чем max, присваиваем его значение переменной max
max = array[i];
}
}
return max;
}
}Нахождение большего элемента в убывающей или возрастающей или возрастающей, а затем убывающей последовательности за минимальное число итераций
public class GreatestOne {
public int find(int[] array) {
if (array[0] > array[1]) {
return array[0]; // Если п-ть убывающая (второй эл-т меньше первого), то бОльший - первый элемент.
}
for (int i = 1; i < array.length; i++) {
if (array[i-1] > array[i]) { // если предыдущий элемент больше текущего - это пик
return array[i-1];
}
}
return array[array.length - 1]; // если в цикле пика найдено не было - значит бОльший элемент - последний,
// потому что последовательность возрастающая
}
}С использованием StringBuilder:
public class StringFormatter {
public static String reverseString(String str) {
StringBuilder sb = new StringBuilder(str);
sb.reverse();
return sb.toString(); // можно в одну строку, соединив вызовы методов
}
} Без использования StringBuilder:
public class StringFormatter {
public static String reverseString(String str) {
char ch[] = str.toCharArray(); // преобразуем строку в массив char
String rev = "";
for (int i = ch.length - 1; i >= 0; i--) { // идем обратно по массиву char'ов
rev += ch[i]; // циклически конкатенируем строку с элементами (так делать плохо, можно заменить
// на создание char[] аналогичной длины и записывание туда элементов сначала,
// а потом преобразовать char[] в строку)
}
return rev;
}
} 



