- Конструкторы, наследование и инициализация в Java
- Наследование, интерфейсы и абстрактные классы
- Перегрузка методов, форматирование строк, сравнение объектов в Java
- Интерфейсы Comparable и Comparator, введение в Java Collections
- Коллекции в Java: LinkedList, HashSet и HashMap
- Неизменяемые коллекции, итераторы и циклы в Java
- Дженерики в Java
- Исключения в Java
- Annotations
- Работа с потоками ввода/вывода (Input/Output Streams) в Java
- Лямбда-выражения и Stream Api в Java
- Модель памяти в Java
- Введение в многопоточность
- Базы данных
- Работа с JDBC: подключение к базе данных, создание таблиц и строк
Классы-обертки предоставляют объектные представления примитивных типов в Java.
- Примеры:
- Integer для int
- Boolean для boolean
- Double для double
Основные характеристики:
- Позволяют использовать null, в отличие от примитивов с их значениями по умолчанию (0, false и т.д.)
- Поддерживают автоупаковку (autoboxing) и распаковку (unboxing)
- Используются, когда необходимо представить отсутствие значения через null
Применение:
- Финансовые системы
- Критически важные системы, где важно различать отсутствие значения и значение по умолчанию
Блоки инициализации - это участки кода, окруженные фигурными скобками, используемые для инициализации полей класса.
- Выполняются при загрузке класса
- Запускаются до любых статических методов или конструкторов
- Обозначаются ключевым словом
static { }
- Выполняются при создании объекта
- Запускаются перед конструкторами
- Просто заключаются в фигурные скобки
{ }
Порядок выполнения:
- Статические поля и блоки
- Нестатические поля и блоки
- Конструкторы
Примечание: В современной разработке блоки инициализации используются реже, но важно знать о их существовании и порядке выполнения.
Конструкторы - это специальные методы для создания и инициализации объектов класса.
Основные характеристики:
- Имя конструктора совпадает с именем класса
- Не имеют возвращаемого типа (даже void)
- Могут быть перегружены (иметь разные списки параметров)
Пример:
public class Cat {
private String name;
// Конструктор без параметров
public Cat() {
this.name = "Безымянный";
}
// Конструктор с параметром
public Cat(String name) {
this.name = name;
}
}Важные моменты:
- Если не определен ни один конструктор, Java предоставляет конструктор по умолчанию без аргументов
- При определении любого конструктора, конструктор по умолчанию больше не предоставляется автоматически
this используется для обращения к текущему объекту внутри метода или конструктора.
Применение:
- Различение полей класса и параметров с одинаковыми именами
- Вызов другого конструктора того же класса
Пример:
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name; // this.name относится к полю класса
this.age = age; // age - параметр конструктора
}
}Наследование - это механизм, позволяющий создавать новые классы на основе существующих.
- Ключевое слово:
extends - Подкласс (дочерний класс) наследует поля и методы суперкласса (родительского класса)
- Пример:
class Cat extends Animal
super используется в конструкторах подклассов для вызова конструктора суперкласса.
Важные аспекты:
- Обеспечивает правильную инициализацию унаследованных полей
- Должен быть первым оператором в конструкторе подкласса
Пример:
public class Animal {
protected String environment;
protected String food;
public Animal(String environment, String food) {
this.environment = environment;
this.food = food;
}
}
public class Cat extends Animal {
private String name;
public Cat(String environment, String food, String name) {
super(environment, food); // Вызов конструктора Animal
this.name = name;
}
}- Статические поля и блоки суперкласса
- Статические поля и блоки подкласса
- Нестатические поля и блоки суперкласса
- Конструктор суперкласса
- Нестатические поля и блоки подкласса
- Конструктор подкласса
- Класс (например, Cat.java) определяет шаблон для объектов
- Содержит описание полей, методов и конструкторов
- Объекты создаются с использованием ключевого слова
new - Каждый объект имеет свое уникальное состояние
- Пример:
Cat vasya = new Cat("Домашний", "Рыба", "Вася");
- Обычно размещается в отдельном классе (часто называемом Runner)
- Используется для создания и использования объектов
- Пример:
public class Runner { public static void main(String[] args) { Cat vasya = new Cat("Домашний", "Рыба", "Вася"); Cat murzik = new Cat("Домашний", "Мясо", "Мурзик"); } }
Понимание конструкторов, наследования и порядка инициализации критически важно для эффективного программирования на Java. Эти концепции лежат в основе объектно-ориентированного дизайна и позволяют создавать гибкие и расширяемые программы.
Ключевые моменты для запоминания:
- Порядок инициализации (статические → нестатические → конструкторы)
- Использование
superв конструкторах подклассов - Различие между определением класса и созданием объекта
- Применение
thisдля разрешения конфликтов имен
Наследование - это один из ключевых принципов объектно-ориентированного программирования (ООП) в Java. Оно позволяет создавать иерархии классов, где дочерние классы наследуют поля и методы от родительских классов.
Основные аспекты наследования:
- Синтаксис:
class Дочерний extends Родительский - Позволяет переиспользовать код родительского класса
- Дочерние классы могут переопределять (override) методы родителя
- Порядок инициализации при наследовании:
- Статические поля и блоки родителя
- Статические поля и блоки потомка
- Нестатические поля и блоки родителя
- Нестатические поля и блоки потомка
- Конструктор родителя
- Конструктор потомка
Пример:
class Vehicle {
int maxSpeed;
void horn() {
System.out.println("Бип");
}
}
class Car extends Vehicle {
@Override
void horn() {
System.out.println("Бип-бип");
}
}Модификаторы доступа определяют видимость полей и методов класса:
- Default (пакетный доступ): доступно внутри пакета
- private: доступно только внутри класса
- protected: доступно внутри пакета и в подклассах
- public: доступно везде
Пример:
public class Vehicle {
private int maxSpeed;
protected String fuelType;
public void horn() { ... }
}Интерфейсы определяют контракт, которому должны следовать реализующие их классы.
Ключевые особенности:
- Содержат только абстрактные методы (до Java 8) и константы
- Класс может реализовывать несколько интерфейсов
- Синтаксис:
class МойКласс implements Интерфейс1, Интерфейс2
Пример:
interface Remote {
void turnOn();
void turnOff();
}
class TVRemote implements Remote {
@Override
public void turnOn() { ... }
@Override
public void turnOff() { ... }
}Абстрактные классы представляют собой "чертеж" для других классов, сочетая конкретные и абстрактные методы.
Особенности:
- Не могут быть напрямую инстанцированы
- Подклассы должны реализовать все абстрактные методы
- Синтаксис:
abstract class МойАбстрактныйКласс - Могут иметь конструкторы, в отличие от интерфейсов
Пример:
abstract class TVRemote implements Remote {
abstract void chooseInput();
void channelUp() {
System.out.println("Канал +");
}
}
class SamsungTVRemote extends TVRemote {
@Override
void chooseInput() { ... }
}Методы по умолчанию позволяют добавлять новую функциональность в интерфейсы без нарушения существующих реализаций.
Особенности:
- Предоставляют реализацию по умолчанию, которую можно переопределить
- Помогают избежать необходимости обновления всех реализующих классов при добавлении новых методов
Пример:
interface Remote {
void turnOn();
void turnOff();
default void changeDimension() {
throw new UnsupportedOperationException("Метод не реализован");
}
}Для демонстрации концепций был создан пример с иерархией пультов управления:
interface Remote {
void turnOn();
void turnOff();
}
abstract class TVRemote implements Remote {
abstract void chooseInput();
}
class SamsungTVRemote extends TVRemote {
@Override
public void turnOn() { ... }
@Override
public void turnOff() { ... }
@Override
void chooseInput() { ... }
}
class LGTVRemote extends TVRemote implements MagicRemote {
// ... реализация методов ...
}
interface MagicRemote {
void showCursor();
}Этот пример демонстрирует:
- Использование интерфейсов для определения общего поведения (Remote)
- Абстрактные классы как промежуточные "чертежи" (TVRemote)
- Наследование и реализацию интерфейсов в конкретных классах (SamsungTVRemote, LGTVRemote)
- Множественную реализацию интерфейсов (LGTVRemote реализует Remote и MagicRemote)
- Наследование позволяет создавать иерархии классов и переиспользовать код
- Интерфейсы определяют контракты, которым должны следовать реализующие классы
- Абстрактные классы предоставляют смесь конкретных и абстрактных методов как "чертеж" для подклассов
- Методы по умолчанию в интерфейсах позволяют добавлять методы без нарушения существующих реализаций
- Правильный выбор между интерфейсами и абстрактными классами зависит от конкретной задачи и требований к дизайну
-
Определение:
- Возможность создания нескольких методов с одинаковым именем, но разными параметрами
- Все перегруженные методы должны иметь одинаковое имя и возвращаемый тип
-
Пример: класс DatePrinter
public class DatePrinter { public static void printDate(int day, int month, int year) { ... } public static void printDate(String dateString) { ... } public static void printDate(long milliseconds) { ... } }
-
Преимущества:
- Упрощает использование методов для разработчиков
- Позволяет обрабатывать разные форматы входных данных
- Улучшает читаемость кода
-
Правила перегрузки:
- Методы должны отличаться списком параметров
- Нельзя создать два метода с одинаковыми параметрами, но разными возвращаемыми типами
-
Практическое применение:
- Обработка данных из разных API с различными форматами
- Создание универсальных утилитных методов
-
Проблема конкатенации:
String result = "Hello, " + name + "! You are " + age + " years old.";
- Неудобно читать и поддерживать
- Prone to errors
-
Решение: String.format()
String result = String.format("Hello, %s! You are %d years old.", name, age);
- %s - для строк
- %d - для целых чисел
- Порядок аргументов соответствует порядку плейсхолдеров
-
Текстовые блоки (с Java 15+):
String text = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. """;
- Удобны для многострочного текста
- Не требуют экранирования символов новой строки
-
Преимущества форматирования:
- Улучшает читаемость кода
- Уменьшает вероятность ошибок
- Упрощает работу с многострочным текстом
-
Проблема сравнения объектов:
- Оператор == сравнивает ссылки, а не содержимое объектов
Cat cat1 = new Cat("Vasya", "Street", 2); Cat cat2 = new Cat("Vasya", "Street", 2); System.out.println(cat1 == cat2); // false
-
Метод equals():
- Должен быть переопределен для корректного сравнения объектов
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Cat cat = (Cat) obj; return age == cat.age && Objects.equals(name, cat.name) && Objects.equals(breed, cat.breed); }
-
Метод hashCode():
- Должен быть переопределен вместе с equals()
- Обеспечивает уникальный числовой идентификатор объекта
@Override public int hashCode() { return Objects.hash(name, breed, age); }
-
Правила переопределения equals() и hashCode():
- Если два объекта равны по equals(), их hashCode() должен быть одинаковым
- Если hashCode() двух объектов различается, они гарантированно не равны
- Если hashCode() одинаковый, объекты могут быть равны или не равны
-
Коллизии хэш-кодов:
- Ситуация, когда разные объекты имеют одинаковый хэш-код
- Может влиять на производительность хэш-основанных коллекций (например, HashMap)
-
Автоматическая генерация в IDE:
- Большинство IDE могут автоматически генерировать корректные реализации equals() и hashCode()
-
Получение данных из внешних API:
- Данные часто приходят в формате JSON
- Пример JSON:
{ "name": "Vasya", "breed": "Street", "age": 2 }
-
Парсинг JSON в Java-объекты:
- Используются специальные библиотеки (например, Jackson, Gson)
- Поля JSON сопоставляются с полями Java-класса
-
Data Transfer Objects (DTO):
- Классы для передачи данных между слоями приложения
- Обычно соответствуют структуре JSON
-
Пример работы с API:
public class CatService { public Cat getCatFromAPI() { String jsonResponse = apiClient.get("/cat"); CatDTO catDTO = jsonParser.parse(jsonResponse, CatDTO.class); return new Cat(catDTO.getName(), catDTO.getBreed(), catDTO.getAge()); } }
- Делает класс самосравнимым
- Реализуется внутри класса
- Использует метод compareTo()
public class Cat implements Comparable<Cat> {
private String name;
private int age;
@Override
public int compareTo(Cat other) {
return this.name.compareTo(other.name);
}
}- Внешний компаратор
- Может быть реализован отдельно от класса
- Использует метод compare()
public class CatComparator implements Comparator<Cat> {
@Override
public int compare(Cat cat1, Cat cat2) {
return cat1.getName().compareTo(cat2.getName());
}
}- Comparable используется для "естественного" порядка сортировки
- Comparator позволяет определить множество способов сортировки
- Comparable реализуется в самом классе, Comparator - отдельно
- И Comparable, и Comparator являются функциональными интерфейсами
- Позволяют использовать лямбда-выражения для краткой записи
- Динамические структуры данных, построенные на основе массивов
- Автоматически расширяются при необходимости
- Предоставляют удобные методы для работы с данными
- Начальная емкость - 10 элементов
- Увеличивается на 50% при заполнении
- Пример создания:
List<String> stringList = new ArrayList<>();- add() - добавление элемента
- get() - получение элемента по индексу
- remove() - удаление элемента
- contains() - проверка наличия элемента
- addAll() - объединение коллекций
- sort() - сортировка (если элементы реализуют Comparable)
- Хранят только объекты, не примитивы
- Для примитивов используются классы-обертки (Integer, Double и т.д.)
- Традиционный цикл for с индексом
for (int i = 0; i < stringList.size(); i++) {
System.out.println(stringList.get(i));
}- Цикл for-each
for (String s : stringList) {
System.out.println(s);
}- Метод forEach() с лямбда-выражением
stringList.forEach(s -> System.out.println(s));- Stream API с лямбда-выражениями
stringList.stream().forEach(fl -> fl);- Краткая запись для вызова методов при итерации
stringList.forEach(System.out::println);- Использование метода filter() в Stream API
stringList.stream()
.filter(Objects::nonNull)
.forEach(System.out::println);- Использование синтаксиса
- Обеспечивает типобезопасность на этапе компиляции
List<Cat> catList = new ArrayList<>();- Символ ? для обозначения неизвестного типа
- Позволяет создавать более гибкие методы
public void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}- Можно ограничить тип элементов определенной иерархией
List<? extends Animal> animalList = new ArrayList<>();- При достижении предела емкости создается новый массив
- Элементы копируются в новый массив
- Размер увеличивается на 50%
- Проверяет текущий размер
- Если нужно, вызывает метод grow()
- Копирует элементы в новый, увеличенный массив
- Проверяет, содержит ли одна коллекция все элементы другой
- Учитывает порядок элементов
- Использование метода Collections.sort()
- Работает с Comparable объектами или с отдельным Comparator
- Коллекции предоставляют удобные инструменты для работы с наборами данных
- Выбор типа коллекции зависит от конкретной задачи
- Интерфейсы Comparable и Comparator позволяют гибко настраивать сортировку
- Generics обеспечивают типобезопасность при работе с коллекциями
- LinkedList - это двунаправленный связанный список
- Каждый элемент (нода) содержит:
- Само значение (item) Ссылку на предыдущий элемент Ссылку на следующий элемент
- Элементы могут быть разбросаны по памяти
- Первый элемент имеет null в качестве ссылки на предыдущий
- Последний элемент имеет null в качестве ссылки на следующий
- Константное время (O(1)) для вставки и удаления элементов в любой позиции
- Эффективен при частых операциях вставки/удаления в середине списка
- Медленнее в итерации по сравнению с ArrayList Занимает больше памяти из-за хранения дополнительных ссылок
- ArrayList быстрее для операций доступа по индексу и итерации
- ArrayList медленнее для вставки/удаления в середине (требует сдвига элементов)
- Использует массив "корзин" (buckets) для хранения элементов
- Элементы распределяются по корзинам на основе их хэш-кода
- При добавлении элемента вызывается его метод hashCode()
- На основе полученного хэш-кода вычисляется номер корзины
- Элемент помещается в соответствующую корзину
- Хранит только уникальные элементы
- Не гарантирует порядок элементов
- Обеспечивает быстрый доступ и вставку (в среднем O(1))
- Коллизия возникает, когда два разных элемента имеют одинаковый хэш-код
- Решение коллизий:
- Создание связанного списка внутри корзины
- При большом количестве коллизий преобразование в бинарное дерево
- Разные объекты должны иметь разные хэш-коды
- Одинаковые объекты должны иметь одинаковые хэш-коды
- Метод equals() должен быть согласован с hashCode()
- Похожа на HashSet, но хранит пары ключ-значение
- Использует хэш-код ключа для определения корзины
- При добавлении пары вызывается hashCode() ключа
- Вычисляется номер корзины на основе хэш-кода
- Пара ключ-значение помещается в соответствующую корзину
- put(key, value): добавление или обновление пары
- get(key): получение значения по ключу
- remove(key): удаление пары по ключу
- containsKey(key): проверка наличия ключа
- containsValue(value): проверка наличия значения
- Ключи должны быть уникальными
- Значения могут повторяться
- Null может быть использован как ключ (только один раз) и как значение
- HashSet внутренне реализован с использованием HashMap
- В HashSet значения используются как ключи в HashMap, а в качестве значений используется dummy object
- ArrayList: для частого доступа по индексу и итерации
- LinkedList: для частых вставок/удалений в середине списка
- HashSet: для хранения уникальных элементов с быстрым доступом
- HashMap: для хранения пар ключ-значение с быстрым доступом по ключу
- HashMap для хранения информации о версиях мобильных платформ:
Map<String, String> platformVersions = new HashMap<>(); platformVersions.put("iOS", "14.5"); platformVersions.put("Android", "11"); platformVersions.put("Huawei", "EMUI 11");
- На небольших объемах данных (до 500 элементов) разница в производительности может быть незаметна
- На больших объемах данных правильный выбор коллекции критичен для производительности
- ArrayList и HashMap используются в 97% случаев на практике
- LinkedList и HashSet имеют более узкие области применения, но важны для понимания
- Правильный выбор коллекции зависит от конкретной задачи и ожидаемых операций
- Неизменяемые коллекции (Immutable collections)
1.1. Создание неизменяемых коллекций
- Используются методы
of()для List, Set и Map - Пример:
List<String> immutableList = List.of("элемент1", "элемент2", "элемент3");
1.2. Особенности неизменяемых коллекций
- После инициализации нельзя добавлять, удалять или изменять элементы
- При попытке модификации выбрасывается UnsupportedOperationException
- Пример:
List<String> immutableList = List.of("Hello", "World"); immutableList.add("New"); // Выбросит UnsupportedOperationException
1.3. Применение неизменяемых коллекций
- Полезны для создания коллекций с значениями по умолчанию, которые не должны изменяться
- Обеспечивают безопасность и предсказуемость в многопоточной среде
1.4. Важное замечание
- Хотя саму коллекцию нельзя изменить, объекты внутри неё могут быть изменяемыми
- Пример:
List<StringBuilder> list = List.of(new StringBuilder("Hello")); list.get(0).append(" World"); // Это допустимо
- Итерация и модификация коллекций
2.1. Стандартный цикл for
- Позволяет безопасно модифицировать коллекцию во время итерации
- Пример:
for (int i = 0; i < list.size(); i++) { if (condition) { list.remove(i); i--; // Важно уменьшить счетчик после удаления } }
2.2. Цикл for-each (enhanced for loop)
- Эффективен для итерации, но не позволяет модифицировать коллекцию
- При попытке модификации выбрасывает ConcurrentModificationException
- Пример:
for (String item : list) { if (condition) { list.remove(item); // Выбросит ConcurrentModificationException } }
2.3. Использование итераторов
- Обеспечивают безопасный способ модификации коллекции во время итерации
- Пример:
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (condition) { iterator.remove(); } }
2.4. Аналогия с комнатой (для понимания работы for-each)
- Преподаватель сравнивает for-each с входом в комнату: "Представьте, что вы зашли в комнату, точно зная расположение всех предметов. Если кто-то удалит дверь, пока вы внутри, возникнет проблема. Так же работает for-each: он "входит" в коллекцию, зная все её элементы, и не ожидает изменений во время работы."
- Современные методы модификации коллекций
3.1. Метод removeIf()
- Предоставляет более лаконичный способ удаления элементов по условию
- Построен на принципах работы итератора, но с более читаемым синтаксисом
- Пример:
list.removeIf(item -> item.equals("value"));
3.2. Сравнение с традиционными подходами
- Более краткий и читаемый код
- Меньше вероятность ошибок при написании
- Многопоточные коллекции (краткое упоминание)
4.1. ConcurrentHashMap
- Предназначен для безопасной работы в многопоточной средe
- Обеспечивает потокобезопасную модификацию при одновременном доступе из разных потоков
4.2. Пример использования (концептуально):
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Безопасно для использования в разных потоках
map.put("key", 1);
map.get("key");- Практические рекомендации
5.1. Выбор правильного цикла
- Для простого перебора без модификации: for-each Для модификации во время итерации: стандартный for или итератор
5.2. Использование современных методов
- Предпочтительно использовать removeIf() и другие современные методы там, где это возможно
- Они более читаемы и менее подвержены ошибкам
- Заключение
- Неизменяемые коллекции полезны для создания коллекций с фиксированным содержимым
- При работе с коллекциями важно учитывать особенности различных циклов и методов итерации
- Итераторы предоставляют гибкий способ модификации коллекций во время обхода
- Современные методы Java (например, removeIf()) упрощают работу с коллекциями, сохраняя безопасность операций
Дженерики (обобщения) в Java - это механизм, позволяющий создавать классы, интерфейсы и методы, которые могут работать с различными типами данных, сохраняя при этом типобезопасность.
- Угловые скобки
<>используются для объявления дженериков - Буквы
T,Eи другие часто используются как параметры типа - Пример:
List<String>- список, содержащий только строки
- Уменьшение дублирования кода
- Обеспечение типобезопасности на этапе компиляции
- Создание универсальных классов и методов
Рассмотрим пример создания класса Box<T>:
public class Box<T> {
private T[] array;
public Box(int size) {
this.array = (T[]) new Object[size];
}
public void add(T item) {
for (int i = 0; i < array.length; i++) {
if (array[i] == null) {
array[i] = item;
return;
}
}
throw new RuntimeException("Нет места в массиве");
}
public T getFirst() {
for (T item : array) {
if (item != null) {
return item;
}
}
return null;
}
}Этот класс позволяет создавать "коробки" для хранения элементов любого типа:
Box<Integer>- для целых чиселBox<String>- для строкBox<CustomObject>- для пользовательских объектов
Методы также могут быть обобщенными:
public <T extends Number> List<T> fromArrayToList(T[] array) {
return Arrays.asList(array);
}Этот метод работает с массивами любого подкласса Number.
Символ ? представляет неизвестный тип.
List<? extends Vehicle> vehiclesПринимает Vehicle или его подклассы.
List<? super Vehicle> vehiclesПринимает Vehicle или его суперклассы.
extendsчасто используется для чтения (read-only)superпозволяет и чтение, и запись
При выполнении программы информация о дженерик-типах стирается:
- Компилятор заменяет обобщенные типы на
Objectили ограниченный тип - Это обеспечивает обратную совместимость с кодом, написанным до введения дженериков
Пример метода, который может работать с "коробками" любого типа:
public static void printBoxes(Box<?>... boxes) {
for (Box<?> box : boxes) {
System.out.println(box.getFirst());
}
}Использование varargs (...) позволяет передавать несколько аргументов.
- Нельзя использовать примитивные типы напрямую (используйте классы-обертки)
- Нельзя создавать экземпляры параметров типа
- Нельзя создавать массивы параметризованных типов
- Используйте дженерики для создания универсальных утилитных классов и методов
- При работе с коллекциями всегда указывайте тип элементов
- Используйте ограничения типов для более точного контроля над дженериками
Дженерики - мощный инструмент в Java, позволяющий создавать более гибкий и типобезопасный код. Они широко используются в стандартной библиотеке Java и являются важной частью современной разработки на Java.
- Исключения (exceptions) – это механизм обработки ошибок в Java, возникающих во время выполнения программы (runtime).
- Исключительные ситуации могут возникать по разным причинам: ошибки ввода данных, неправильные вычисления, проблемы с доступом к файлам и т.д.
- Все исключения в Java наследуются от класса
Throwable. Существует два основных типа исключений:- Checked Exceptions:
- Эти исключения компилятор проверяет во время компиляции кода.
- Если метод может выбросить checked exception, то он должен либо объявить это в своей сигнатуре с помощью ключевого слова
throws, либо обработать исключение внутри блокаtry-catch. - Пример:
IOException(возникает при работе с файлами).
- Unchecked Exceptions (RuntimeException):
- Эти исключения не проверяются компилятором.
- Программа не обязана обрабатывать эти исключения, но может это делать по своему усмотрению.
- Пример:
ArrayIndexOutOfBoundsException(возникает при выходе за границы массива),ArithmeticException(возникает при делении на ноль).
- Checked Exceptions:
try { ... }: Внутри этого блока помещается код, который потенциально может выбросить исключение.catch (ТипИсключения имяПеременной) { ... }: Этот блок обрабатывает исключение определенного типа.ТипИсключениядолжен соответствовать типу ожидаемого исключения, аимяПеременной– это имя переменной, которая будет содержать объект исключения. Внутри блокаcatchможно выполнить действия по обработке ошибки, например, вывести сообщение об ошибке, записать информацию в лог-файл или предпринять другие действия для восстановления работы программы.- Порядок блоков
catch: Если несколько блоковcatchловят исключения, находящиеся в одной иерархии наследования (например,ArithmeticExceptionиRuntimeException), то блокcatchдля потомка должен стоять перед блокомcatchдля родителя. В противном случае, блокcatchдля родителя перехватит все исключения этого типа, и блокcatchдля потомка никогда не будет выполнен.
finally { ... }: Этот блок выполняется всегда, независимо от того, было ли выброшено исключение или нет. Обычно в блокеfinallyвыполняется закрытие ресурсов, таких как файлы или потоки ввода-вывода, чтобы предотвратить утечки ресурсов.
- Можно создавать собственные классы исключений, расширяя
RuntimeExceptionилиException. - Пример:
public class ArtemException extends RuntimeException { ... }.
int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
try {
for (int i = 0; i <= array.length; i++) {
System.out.println(array[i]);
}
} catch (ArrayIndexOutOfBoundsException | ArithmeticException exception) {
System.err.println("Out of array range");
} catch (Exception exception) {
System.err.println("EXCEPTION");
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.err.println(e.getMessage());
}
try (Scanner scanner = new Scanner(System.in);
FileInputStream fis = new FileInputStream("some.txt")) {
divideByZero();
} catch (ArithmeticException | ArtemException e) {
System.err.println(e.getMessage());
} catch (Exception e) {
System.err.println("Some issue");
} finally {
System.out.println("FINALLY");
scanner.close();
}
public static void divideByZero() {
Scanner scanner = new Scanner(System.in);
int x = scanner.nextInt();
if (x == 0) {
throw new ArithmeticException("Divide by zero");
} else {
System.out.println(1/x);
}
scanner.close();
}
public static void someMethod() {
throw new ArtemException("Some issue");
}- Иерархия исключений:
Throwable->Exception->CheckedилиUnchecked. RuntimeExceptionявляется подклассомException.Error- это критичные ошибки, с которыми невозможно работать.Finallyблок выполняется всегда, независимо от результата блокаtry.Try-catchможно использовать в любом методе, где потенциально может возникнуть исключение.- Можно создавать свои исключения, наследуясь от
RuntimeExceptionилиException.
- Мощный инструмент для пометки классов и методов, чтобы те выполняли определённую логику.
- Позволяют задать метаданные к элементам кода, не изменяя их основное назначение.
- Применяются к классам, полям, методам, параметрам, конструкторам, переменным.
- Позволяют проводить проверку и фильтрацию на любом этапе обработки данных, в том числе на этапе написания кода, компиляции и выполнения программы.
- Работают с помощью аннотационного процессора, который сам разработчик может создать.
- Target: Указывает цель или область действия аннотации. Определяет, где именно может быть поставлена аннотация.
- Retention: Определяет, насколько долго хранятся аннотации, доступные для использования. Возможные значения:
- SOURCE - только на этапе написания кода
- CLASS - во время компиляции и выполнения программы
- RUNTIME - доступна на всех этапах, используется для изменения поведения класса или метода.
Предположим, есть класс Phone с атрибутом locked и методами takePhoto и unlock(). Нужно создать аннотацию, которая будет проверять статус телефона (заблокирован или разблокирован) перед выполнением определённых действий.
@Target(ElementType.TYPE)
@Retention(ElementType.RUNTIME)
public @interface IsLockePhone {
boolean locked() default false;
}@IsLockePhone(locked = true)
public class Phone {
private String name;
private boolean locked = true;
public Phone(String name) {
this.name = name;
}
public void takePhoto() {
if (isLocked()) {
System.out.println("Phone is locked!");
} else {
System.out.println("CLICK CLIK!");
}
}
@PhoneGenerallyAvailable
public void unlock() {
if (!locked) {
System.out.println("Phone unlocked successfully!");
locked = false;
} else {
System.out.println("Phone already unlocked!");
}
}
}public static void annotationProcessor(Class<?> clazz) {
boolean isLockedPhoneAnnotationPresent = clazz.isAnnotationPresent(IsLockedPhone.class);
if (isLockedPhoneAnnotationPresent) {
IsLockedPhone isLockedPhone = clazz.getAnnotation(IsLockedPhone.class);
for (Method method : clazz.getDeclaredMethods()) {
boolean isPhoneGenerallyAvailableAnnotationPresent = method.isAnnotationPresent(PhoneGenerallyAvailable.class);
if (isPhoneGenerallyAvailableAnnotationPresent) {
PhoneGenerallyAvailable annotation = method.getAnnotation(PhoneGenerallyAvailable.class);
Constructor<?> constructor = clazz.getConstructor(Phone.class);
constructor.setAccessible(true);
method.invoke(constructor);
System.out.println("Phone is not ready yet!");
} else {
System.out.println("Phone is not ready yet!");
}
}
if (isLockedPhone.locked()) {
System.out.println("Phone is fully locked!");
} else {
System.out.println("Phone is not present!");
}
}
else {
System.out.println("Is not present!");
}
}Таким образом, данный код выполнит проверку, заблокирован ли телефон перед выполнением методов и выведет соответствующие сообщения.
В данной лекции рассматривается пакет java.io, отвечающий за ввод и вывод данных в Java. Этот пакет содержит классы для работы с различными типами ввода и вывода, включая файлы, консоль и сетевые потоки.
Основное внимание уделяется работе с байтовыми потоками (InputStream и OutputStream) и их базовым реализациям, которые являются надстройками над байтовыми потоками, потому что в конечном итоге данные передаются как байты.
Что такое поток? Поток - это абстрактное представление последовательности данных. Можно представить поток как реку, из которой можно черпать воду (читать данные) или сливать воду обратно (записывать данные). Существуют разные способы взаимодействия с потоком (аналогия с черпанием воды ложкой или ведром).
Виды потоков:
В Java есть два основных типа потоков:
-
InputStream: для чтения данных. Аналогично System.in (стандартный ввод), который тоже является реализацией InputStream.
-
OutputStream: для записи данных. Аналогично System.out.println(), который использует OutputStream для вывода на консоль.
Это базовые, низкоуровневые реализации InputStream и OutputStream соответственно. Они работают с массивами байтов (byte array).
- String.getBytes() преобразует строку в массив байтов.
- ByteArrayInputStream(byte[] data): Этот конструктор создает поток ввода данных (InputStream), основанный на переданном массиве байтов. Метод read() считывает байты из потока по одному. Если байты закончились, метод возвращает -1.
- ByteArrayOutputStream(): запись в массив байтов. Каждый байт записывается в ByteArrayOutputStream с помощью метода write().
- baos.toByteArray() возвращает массив байтов,
- baos.toString() – возвращает строковое представление байтов, содержащихся в ByteArrayOutputStream. По умолчанию интерпретируется, как строка в кодировке UTF-8 (или другой, если передать кодировку).
Каждый символ имеет свой байтовый код. Для корректной работы с разными языками (например, кириллицей) важно использовать правильную кодировку.
Пример: Если текст на кириллице записать в ByteArrayOutputStream и затем прочитать из ByteArrayInputStream побайтно, то без правильной кодировки результат будет некорректным ("абракадабра"). Кириллические символы занимают два байта, поэтому интерпретатор может неправильно их прочитать.
UTF-8: Одна из распространённых кодировок, которая позволяет корректно работать с кириллицей и другими символами, требующими больше одного байта.
Base64: Способ кодирования бинарных данных в текстовый формат. Используется для передачи данных через каналы, не поддерживающие бинарные данные (например, email, HTML). Пример: Преобразование изображения в Base64.
Позволяют записывать/читать примитивные типы данных (int, double, boolean, String). Например, данные которые сохраняются в файл, сохраняются последовательно, в том же порядке, в котором были записаны. При чтении данных важно соблюдать ту же последовательность типов, что и при записи. Если нарушить порядок, возникнет ошибка.
Позволяют сериализовать (сохранять) и десериализовать (восстанавливать) объекты Java в файл. Сериализация: Процесс преобразования объекта в последовательность байтов для хранения или передачи. Десериализация: Процесс восстановления объекта из последовательности байтов. Пример: Сохранение объекта класса Cat в файл с помощью ObjectOutputStream и последующее его чтение с помощью ObjectInputStream. vasikSerialized.dat
FileInputStream и FileOutputStream - это реализации InputStream и OutputStream для работы с файлами. Читаем данные из FileInputStream побайтно с помощью read() и записываем в FileOutputStream с помощью write().
BufferedReader и BufferedWriter: Для повышения эффективности работы с файлами (чтение/запись по блокам данных, а не по одному байту), лучше использовать BufferedReader и BufferedWriter, благодаря его методу readLine(). Он читает одну строку (до символа конца строки), возвращает ее как String, или null, если достигнут конец файла.
Важно: Все потоки (InputStream, OutputStream, Reader, Writer) должны быть закрыты после использования для освобождения ресурсов. try-with-resources: Рекомендуемый способ работы с ресурсами, гарантирующий их закрытие даже в случае возникновения ошибок. Альтернатива – блок finally.
Лямбда-выражения, которые появились в Java 8, представляют собой анонимные функции. Они позволяют сократить объем кода и упростить работу с функциональными интерфейсами.
Это блок кода, который может быть передан для выполнения. Формат: (параметры) -> тело.
(a) -> "hello";Предположим, нужно найти в коллекции строку и что-то с ней сделать (выделить или записать в другую коллекцию). Раньше для таких операций использовали циклы (for или for-each). Если в коллекции будут какие-то заполненные объекты, то потребуется делать дополнительные проверки и условия для проверки значений (if, else и т.д.).
Когда в Java появилась восьмая версия, в ней добавили стримы. Коллекция преобразуется в стрим, и в нем выполняются нужные действия. В одном потоке можно выполнить много операций (например, использовать много фильтров, применять функции и т.д.).
Стрим позволяет выполнять последовательные операции над данными, например, фильтрацию, трансформацию, сборку.
List<String> strings = List.of("hello", "world", "!");
strings.stream();
Для защиты от NullPointerException требуется использовать дополнительную проверку, например filter(ft -> Objects.isNull(ft)). Objects - класс-утилита для работы со всеми объектами, метод isNull проверяет значение на равенство null.
- filter: фильтрует данные по условию.
strings.stream().filter(f -> "world".equals(f));
- findFirst: возвращает первый элемент, удовлетворяющий условию.
- orElse: возвращает значение по умолчанию, если результат пуст.
Optional<String> result = strings.stream()
.filter(f -> "world".equals(f))
.findFirst();
.orElse("NOT FOUND");
- Для предотвращения ошибок типа NullPointerException, можно добавить проверку:
.filter(Objects::nonNull)
Если элемент равен null, он будет исключён из дальнейшей обработки.
- Создание списка карт:
List<Card> cards = new ArrayList<>();
cards.add(new Card("Jane Doe", "5600000000000002", null));
cards.add(new Card("John Doe", "4400000000000000", null));
- Фильтрация карт с номерами длиной 16 символов:
List<Card> filteredCards = cards.stream()
.filter(card -> Objects.nonNull(card.getCardNumber()))
.filter(card -> card.getCardNumber().length() == 16)
.toList();
Определение платёжной системы по первой цифре:
List<Card> processedCards = cards.stream()
.map(card -> {
Card commonCard = new Card(card.getHolderName(), card.getCardNumber(), null);
switch (card.getCardNumber().substring(0, 1)) {
case "4" -> commonCard.setPaySystem(PaySystem.VISA);
case "5" -> commonCard.setPaySystem(PaySystem.MASTERCAR);
case "6" -> commonCard.setPaySystem(PaySystem.DISCOVER);
}
return commonCard;
})
.toList();
- Завершают работу потока, возвращая результат вычислений.
- Выполняют действия на основе всего потока.
- После вызова терминального метода стрим становится недоступным для дальнейших операций.
Примеры:
toList()— собирает элементы потока в список:
List<String> result = strings.stream()
.filter(s -> s.length() > 3)
.toList();- Возвращают новый стрим, позволяя цепочку вызовов.
- Не завершают поток; они подготавливают данные для терминальной операции.
Примеры:
filter(Predicate)— фильтрует элементы, оставляя только те, которые соответствуют предикату.
strings.stream().filter(s -> s.equals("world"));map(Function)— применяет функцию к каждому элементу, возвращая преобразованный поток.
strings.stream().map(String::toUpperCase);-
Принимает: объект типа
Predicate<T>(предикат — функциональный интерфейс, принимающий объект и возвращающийboolean). -
Возвращает: новый поток с элементами, соответствующими предикату.
List<String> strings = List.of("hello", "world", "java"); List<String> filtered = strings.stream() .filter(s -> s.startsWith("h")) .toList();
Принимает: объект типа Function<T, R> (функция, преобразующая объект из типа T в тип R). Возвращает: поток преобразованных данных.
List<String> upperCaseStrings = strings.stream()
.map(String::toUpperCase)
.toList();Принимает: объект типа Consumer (операция, принимающая элемент потока и не возвращающая значения). Возвращает: ничего; выполняет действие для каждого элемента.
strings.stream().forEach(System.out::println);
Принимают: входные параметры, определяемые пользователем. Возвращают: вычисленное значение или void, если указан консюмер.
strings.stream()
.filter(s -> s.length() > 4) // Лямбда возвращает boolean
.map(s -> s.toUpperCase()) // Лямбда возвращает преобразованную строку
.forEach(s -> System.out.println(s)); // Лямбда выполняет действие
- Predicate: Описывает условие. Метод: boolean test(T t).
- Function<T, R>: Преобразует данные. Метод: R apply(T t).
- Consumer: Принимает данные и выполняет действие. Метод: void accept(T t).
- Supplier: Поставляет данные. Метод: T get().
Двойное двоеточие (::) — это сокращённая форма для ссылки на методы и конструкторы, которая используется в Java 8. Оно облегчает запись, делая код более компактным и читаемым. Виды ссылок с двойным двоеточием
- Ссылка на статический метод ClassName::staticMethodName
- Ссылка на метод экземпляра конкретного объекта instance::methodName
- Ссылка на метод экземпляра произвольного объекта типа ClassName::methodName
- Ссылка на конструктор ClassName::new
names.stream()
.map(String::toUpperCase) // Ссылка на метод toUpperCase
.forEach(System.out::println); // Ссылка на метод println
public class DoubleColonExample {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// Используем кастомный предикат
Predicate<String> isLongerThanThree = MyStringUtils::isLongerThanThree;
names.stream()
.filter(isLongerThanThree) // Фильтрация по кастомному предикату
.forEach(System.out::println);
}
}
// Утилитарный класс с методом
class MyStringUtils {
// Метод, проверяющий длину строки
public static boolean isLongerThanThree(String s) {
return s.length() > 3;
}
}
Принцип LIFO (last in, first out) - последний пришел, первый ушел.
В стэке хранится:
-
Последовательность вызова методов, начиная с метода main.
-
Имена переменных.
-
Имена методов.
Пример:
В методе main вызывается другой метод.
Из этого метода вызывается ещё один метод.
Эта цепочка может продолжаться.
Выполнение методов происходит по принципу LIFO.
После завершения последнего метода, возвращаемся к предыдущему, затем к предыдущему, и так далее, до метода main.
-
При объявлении объекта Object obj = new Object() в методе, создаётся ссылка на конкретный участок памяти в куче (heap).
-
Когда метод завершает работу, ссылки на объекты удаляются, но сами объекты остаются.
Как удаляются объекты из памяти? С помощью сборщика мусора (Garbage Collector).
- В куче хранятся сами объекты.
- Размер памяти, выделяемый под объект, определяется при его создании, исходя из размеров полей и данных в объекте.
- Сборщик мусора удаляет объекты, на которые нет ссылок.
Куча разделена на области:
-
Young Generation (молодое поколение) - для новых объектов :
Eden (Эдем) - сюда попадают новые объекты. Survivor Space S0 (Пространство выживших 0) - здесь хранятся объекты, пережившие первую очистку сборщика мусора. Survivor Space S1 (Пространство выживших 1) - аналогично S0, но после нескольких очисток в Эдеме. -
Old Generation (старое поколение) - для долгоживущих объектов.
Permanent Space/Metaspace (перманентное пространство/метапространство) - для метаданных классов и строк.
- При первом запуске программы и работе сборщика мусора, объекты со ссылками копируются в любой свободный Survivor space (S0 или S1).
- Объекты без ссылок удаляются.
- При следующем запуске сборщика мусора проверяются объекты в Eden и занятом Survivor Space.
- Объекты со ссылками копируются в свободный Survivor Space.
- Eden и занятый Survivor Space очищаются.
- Объекты, пережившие несколько очисток, перемещаются в Old Generation.
- В Java < 8 - это Permanent Space. В Java >= 8 - это Metaspace.
Раньше были проблемы с Исключением OutOfMemoryError, которое возникает при превышении лимита физической памяти.
- До Java 8 размер памяти был фиксированным, после Java 8 - динамическим.
- Хранит метаданные классов и строки.
- Очистка происходит реже, чем Young Generation.
- Можно вызвать вручную, используя System.gc(). Не рекомендуется.
- Сборщик мусора запускается в случайный момент, по своему алгоритму.
Многопоточность (Multithreading) — это техника параллельного выполнения программы, позволяющая одной программе обрабатывать несколько задач одновременно. С точки зрения кода, это означает, что в одну единицу времени параллельно могут выполняться какие-то задачи.
Многопоточность используется для повышения производительности программы, то есть ускорения работы программы. Благодаря ей процессы могут выполняться параллельно, а не последовательно один за другим.
Представьте, что вы можете одновременно рисовать в Paint, слушать музыку, смотреть видео и писать код в IntelliJ IDEA, и все эти задачи выполняются параллельно в одно и то же время. Это и есть многозадачность.
Современные процессоры могут иметь много ядер, что позволяет физически выполнять несколько задач одновременно.
Многопоточность в Java работает даже на одноядерных процессорах. В этом случае процессор быстро переключается между задачами. Это переключение настолько быстрое, что кажется, что задачи выполняются одновременно.
Пример:
Если есть две задачи, T1 и T2, и процессор имеет только одно ядро (Single core), то процессор переключается между задачами, выполняя их по частям, например, 0.002 секунды для задачи 1 и 0.004 секунды для задачи 2. Этот процесс называется виртуальным параллелизмом (Virtual Parallelism).
-
Стэк (Stack): Для каждого потока создаётся свой стэк вызова методов (Stack main, Stack 1, Stack 2, Stack 3...).
-
Куча (Heap): Куча памяти одна для всех потоков.
Пример:
Если несколько потоков работают с одной коллекцией (Collection) в куче, то они конкурируют за доступ к ней. Например, один поток читает данные, другой добавляет, а третий удаляет. При этом каждый поток работает со своим стэком.
Потокобезопасные коллекции:
В многопоточной среде возникают проблемы с консистентностью данных, если несколько потоков работают с одной коллекцией одновременно. Например, если один поток удаляет элемент с индексом 10, а другой добавляет элемент в этот же индекс. В таком случае возникнет ошибка.
Синхронизация:
Чтобы предотвратить проблемы с консистентностью, необходимо синхронизировать потоки. Это гарантирует, что каждый поток завершает работу с коллекцией, прежде чем другой поток сможет к ней получить доступ.
Синхронизированные коллекции:
В Java существуют потокобезопасные коллекции (например, ConcurrentHashMap), которые обеспечивают синхронизацию и предотвращают проблемы с консистентностью данных.
При отсутствии потокобезопасных коллекций можно синхронизировать потоки вручную, используя механизмы синхронизации (например, ключевое слово synchronized).
Создайте класс, который наследуется от класса Thread. Переопределите метод run(), в котором опишите логику потока. Создайте экземпляр этого класса и вызовите метод start(), чтобы запустить поток.
class ThreadStarter extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread: " + Thread.currentThread().getName() + ": " + i);
}
}
}
public class Threads {
public static void main(String[] args) throws InterruptedException {
ThreadStarter threadStarter1 = new ThreadStarter();
threadStarter1.setName("ThreadStarter1");
threadStarter1.start();
ThreadStarter threadStarter2 = new ThreadStarter();
threadStarter2.setName("ThreadStarter2");
threadStarter2.start();
System.out.println("MAIN THREAD FINISHED");
}
}
Создайте класс, который реализует интерфейс Runnable. Переопределите метод run(), в котором опишите логику потока. Создайте экземпляр класса Thread и передайте в него экземпляр вашего класса, реализующего Runnable. Вызовите метод start() для запуска потока.
class RunnableStarter implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread: " + Thread.currentThread().getName() + ": " + i);
}
}
}
public class Threads {
public static void main(String[] args) throws InterruptedException {
Runnable runnable1 = new RunnableStarter();
Thread thread = new Thread(runnable1);
thread.setName("RUNNABLE1");
thread.start();
Runnable runnable2 = new RunnableStarter();
Thread thread2 = new Thread(runnable2);
thread2.setName("RUNNABLE2");
thread2.start();
System.out.println("MAIN THREAD FINISHED");
}
Используйте лямбда-выражения для создания анонимных классов, реализующих Runnable, внутри метода main().
public class Threads {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread: " + Thread.currentThread().getName() + ": " + i);
}
});
thread.setName("ANONYMOUS-THREAD");
thread.start();
System.out.println("MAIN THREAD FINISHED");
}
}
Метод start(): Важно запускать потоки с помощью метода start(), а не run().
Потоки-демоны не могут существовать без потока-родителя. Если поток-родитель завершает работу, то все его потоки-демоны также завершаются, даже если они не закончили свою работу.
public class Threads {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread: " + Thread.currentThread().getName() + ": " + i);
}
});
thread.setDaemon(true);
thread.setName("DEMON");
thread.start();
System.out.println("MAIN THREAD FINISHED");
}
}
Напишите программу, которая моделирует работу двух потоков: один поток выводит чётные числа от 1 до 100, а другой — нечётные числа. Каждый поток должен выводить числа с задержкой в 500 миллисекунд. Условия задачи:
- Используйте два потока: один для вывода чётных чисел, другой — для нечётных.
- Потоки должны работать параллельно.
- В главном потоке программа должна завершаться только после завершения работы обоих потоков.
-
Базы данных делятся на две группы: NoSQL и SQL.
-
Наиболее интересной для рассмотрения в этой лекции является структура SQL базы данных.
-
SQL расшифровывается как Structured Query Language (на русском их называют реляционными).
NoSQL базы данных построены по принципу «ключ-значение».
Типичные NoSQL базы данных: Mongo, Cassandra, Redis.
Структура NoSQL базы данных:
- ключ
- значение
Типичные SQL базы данных: Oracle SQL, MySQL, PostgreSQL, MS SQL.
База данных (БД)
Таблицы
Колонки
Строки (записи) с различными типами данных
Аналогия с Excel таблицей:
Представим, что таблица в базе данных похожа на таблицу в Excel. Колонки в таблице базы данных похожи на колонки в Excel таблице. Строки в таблице базы данных похожи на строки в Excel таблице.
id (уникальный идентификатор записи) - INT (целое число)
title (название книги) - VARCHAR(30) (строка длиной до 30 символов)
genre (жанр книги) - VARCHAR(20) (строка длиной до 20 символов)
author (автор книги) - VARCHAR(50) (строка длиной до 50 символов)
quantity (количество книг) - INT (целое число)
Реляционные таблицы могут быть связаны между собой.
Ссылка на скачивание PostgreSQL: https://www.postgresql.org/download/
В зависимости от платформы выбирается нужный установщик.
В установщике для Windows устанавливаются:
PostgreSQL server
pgAdmin
Stack Builder
Command Line Tools
Для pgAdmin superuser, как правило, устанавливается логин postgres и пароль root.
С помощью pgAdmin можно создать новую базу данных (New Database) и таблицы внутри неё (Create New Table).
Для работы с базами данных удобно использовать программу DBeaver.
Ссылка на скачивание DBeaver Community: https://dbeaver.io/download/
Для подключения к базе данных в DBeaver нужно указать:
Host name/address (для локального сервера - localhost или 127.0.0.1)
Port (для PostgreSQL по умолчанию - 5432)
Database (имя созданной базы данных)
Username (postgres)
Password (root)
В DBeaver создать новую таблицу можно через меню New Table или SQL-скрипт.
Команда для создания таблицы user_profile в SQL:
create table user_profile(
id SERIAL primary key,
first_name VARCHAR(30) not null,
last_name VARCHAR(50),
email VARCHAR(50) unique,
date_birth timestamp,
status VARCHAR(10) default 'ACTIVE',
create_date TIMESTAMP default CURRENT_TIMESTAMP
);
Команда для вставки данных в таблицу user_profile:
insert into user_profile(first_name, last_name, email, date_birth)
values
('John','Doe','john.doe@gmail.com',1980-11-11T04:00:00Z),
('Jane','Doe','jane.doe@gmail.com',1982-12-12T05:00:00Z);
Команда для получения данных из таблицы user_profile:
select * from user_profile;
Команда для получения определённых колонок из таблицы user_profile:
select first_name,last_name from user_profile;
Команда для получения данных из таблицы user_profile с условием:
select first_name from user_profile where first_name like '%J%';
Команда для обновления данных в таблице user_profile:
update user_profile set first_name = 'Jane' where first_name like '%J%';
Создайте таблицу для хранения информации о книгах в библиотеке и выполните несколько операций с данными:
- Создайте таблицу books, которая будет содержать следующие поля:
id— уникальный идентификатор книги (целое число, автоинкремент),title— название книги (строка до 100 символов),author— автор книги (строка до 50 символов),genre— жанр книги (строка до 30 символов),quantity— количество экземпляров книги (целое число)- Вставьте в таблицу несколько известных книг, например:
- "1984" Джордж Оруэлл, жанр — антиутопия,
- "To Kill a Mockingbird" Харпер Ли, жанр — драма,
- "The Great Gatsby" Фрэнсис Скотт Фицджеральд, жанр — трагедия,
- "The Catcher in the Rye" Джером Д. Сэлинджер, жанр — философский роман,
- "Moby-Dick" Герман Мелвилл, жанр — приключенческий роман
- Выполните следующие SQL-запросы:
- Получите все книги, автор которых — "Harper Lee".
- Обновите количество экземпляров книги "1984" до 15.
- Удалите книгу "Moby-Dick" из таблицы.
В этом уроке мы рассмотрим работу с базами данных с помощью JDBC (Java Database Connectivity). Мы изучим:
- Как подключаться к базе данных.
- Как создавать таблицы в базе данных.
- Как добавлять записи в таблицу.
- Как выбирать записи из таблицы.
- Как обновлять записи в таблице.
JDBC — это Java API для подключения и выполнения SQL-запросов к базе данных.
Для подключения к базе данных нам нужно определить URL (который можно найти например в DBeaver), имя пользователя и пароль для подключения к базе данных.
private String dbUser = "postgres";
private String dbPassword = "root";
private String dbUrl = "jdbc:postgresql://127.0.0.1:5432/pasv_db";Для создания соединения с базой данных мы импортируем из пакета java.sql класс DriverManager и используем его метод getConnection().
Этот метод принимает три параметра: URL базы данных, имя пользователя и пароль и возвращает объект типа Connection, если соединение установилось.
try (Connection connection = DriverManager.getConnection(url, userName, password)) {
...
}Используем
tryс ресурсами, чтобы соединение не оставлять открытым после использования, альтернатива :finally
Можно попыться попытаетесь распечатать распечатать этот объект в консоли, чтобы проверить установилось ли соединение
System.out.println(connection); // выведет примерно так:: com.mysql.cj.jdbc.ConnectionImpl@1a2b3c4dВ случае ошибки «No suitable driver found for postgres», необходимо скачать и добавить JDBC драйвер для PostgresSQL. В данном случае, рекомендуется использовать последнюю версию драйвера для используемой версии Java (для Java 8 и выше – 42.2.15). Для добавления драйвера в IntelliJ IDEA:
- File -> Project Structure
- Перейти в раздел "SDKs"
- Нажать "+" и выбрать скачанный файл драйвера
- Нажать "Apply" и "OK"
Перед тем как начать работать с таблицой, нам нужно использовать метод createStatement() объекта Connection. Этот метод возвращает объект типа Statement.
Statement statement = connection.createStatement();Обьект Statement можно использовать для отправки SQL-запросов к базе данных.
Например благодаря его методу executeUpdate() мы можем создать таблицу в базе данных. Этот метод принимает SQL-запрос как параметр в виде строки.
statement.executeUpdate("CREATE TABLE user_profile ("
+ "id SERIAL PRIMARY KEY, "
+ "first_name VARCHAR(30) NOT NULL, "
+ "last_name VARCHAR(50), "
+ "email VARCHAR(50) UNIQUE, "
+ "date_birth TIMESTAMP, "
+ "status VARCHAR(10) DEFAULT 'ACTIVE', "
+ "create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
+ ");");Используем конкатенацию для удобства, чтобы не писать все в одну строку.
statement.executeUpdate("Insert into user_profile(first_name, last_name, email, date_birth) " +
"values('John', 'Doe', 'johndoe@com.com', '12-12-1992');");Для обработки результатов запроса SELECT используется метод statement.executeQuery(), который возвращает объект ResultSet.
Для перебора результатов используется его метод next(), который возвращает true, если есть следующая запись, и false – если записи закончились.
Для получения значения из колонки используется его метод getXXX(), где XXX – тип данных колонки. Например, getString(), getInt(), getDate().
List<UserProfile> userProfileList = new ArrayList<>();
while (resultSet.next()) {
UserProfile userProfile = new UserProfile();
userProfile.setId(resultSet.getInt("id"));
userProfile.setFirstName(resultSet.getString("first_name"));
userProfileList.add(userProfile);
}- Для каждой строки данных, возвращенной запросом, создается новый объект UserProfile.
- Столбцы id и first_name из строки результата запроса сохраняются в соответствующие поля объекта.
- Каждый объект добавляется в список userProfileList, который в конце будет содержать всех пользователей, извлеченных из базы данных.
Создание класса
UserProfileи сохранение в нем данных возвращенных запросом, позволяет структурировать данные, полученные из базы данных, и работать с ними более удобно, сохраняя объектно-ориентированный подход в коде, вместо того чтобы просто работать с результатами в виде строк.
Для предотвращения SQL инъекций, рекомендуется использовать метод prepareStatement() обьекта Connection. Он возвращает обьект PreparedStatement, благодаря его методу как например setString() параметры запроса передаются отдельно.
SQL инъекция — это техника, с помощью которой злоумышленник вставляет или "инъецирует" вредоносный SQL-код в запросы, отправляемые на сервер базы данных.
Метод setString() принимает два аргумента:
- Индекс параметра (нумерация начинается с 1) — позиция в SQL-запросе, где будет вставлено значение.
- Значение, которое необходимо подставить в запрос.
PreparedStatement preparedStatement = connection.prepareStatement("update user_profile set first_name = ? where first_name = ?");
preparedStatement.setString(1, "Jane");
preparedStatement.setString(2, "John");Части SQL-запроса заменены на плейсхолдеры в виде знаков вопроса (?). Эти плейсхолдеры затем заменяются на реальные значения в момент выполнения запроса с использованием методов объекта PreparedStatement.
Создайте таблицу
orders, которая будет содержать следующие поля:
order_id— уникальный идентификатор заказа (целое число, автоинкремент),customer_name— имя покупателя (строка до 100 символов),product_name— название товара (строка до 100 символов),quantity— количество заказанных товаров (целое число),order_date— дата и время создания заказа (тип TIMESTAMP, значение по умолчанию — текущая дата и время),status— статус заказа (значения: "PENDING", "SHIPPED", "DELIVERED", "CANCELLED").Вставьте в таблицу следующие заказы:
- Покупатель:
Alice Johnson, Товар:Laptop, Количество:1, Статус:PENDING- Покупатель:
Bob Smith, Товар:Smartphone, Количество:2, Статус:SHIPPED- Покупатель:
Charlie Brown, Товар:Headphones, Количество:1, Статус:DELIVERED
- Выполните следующие SQL-запросы с помощью JDBC:
- Выберите все заказы со статусом
PENDING.- Обновите статус заказа с именем покупателя
Alice JohnsonнаSHIPPED.- Удалите все заказы, у которых статус
CANCELLED.
Реализуйте защиту от SQL-инъекций, используя
PreparedStatementдля выполнения запросов.Создайте Java-класс
Orderдля представления записей из таблицы. Извлеките данные из таблицыordersс использованием JDBC и сохраните их в список объектов этого класса.









