02-edits, 03-continue

This commit is contained in:
Ivan I. Ovchinnikov 2022-10-05 01:11:41 +03:00
parent 0419b1f1b6
commit 74f09b6169
4 changed files with 782 additions and 71 deletions

View File

@ -105,6 +105,60 @@ public class Cat {
}
\end{lstlisting}
- Также, хотелось бы отметить, что мы можем использовать -Xms и -Xmx опции JVM, чтобы определить начальный и максимальный размер памяти в куче. Для стека определить размер памяти можно с помощью опции -Xss;
\begin{lstlisting}[language=Java,style=JCodeStyle]
public class Cat {
private String name;
private String color;
private int age;
public Cat() {
System.out.println("constructor is constructing...");
name = "Barsik";
color = "White";
age = 2;
}
}
\end{lstlisting}
\begin{lstlisting}[language=Java,style=JCodeStyle]
public class Cat {
private String name;
private String color;
private int age;
Cat(String n, String c, int a) {
System.out.println("constructor is constructing...");
name = n;
color = c;
age = a;
}
}
\end{lstlisting}
\begin{lstlisting}[language=Java,style=JCodeStyle]
Cat cat1 = new Cat("Barsik", "White", 4);
Cat cat2 = new Cat("Murzik", "Black", 6);
\end{lstlisting}
\begin{lstlisting}[language=C,style=CCodeStyle]
public class Cat {
private String name;
private String color;
private int age;
public Cat(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
}
\end{lstlisting}
ключевое слово this в Java используется только в составе экземпляра класса. Но неявно ключевое слово this передается во все методы, кроме статических (поэтому this часто называют неявным параметром) и может быть использовано для обращения к объекту, вызвавшему метод.
\printnomenclature[40mm]
\end{document}

View File

@ -84,6 +84,8 @@
\label{tab:types}
\end{table}
% TODO По умолчанию, создавая примитивную переменную ей из-за примитивности данных, присваивается начальное значение - ноль для числовых и ложь для булева. Что ещё раз доказывает, что мы храним там просто числа в двоичной системе счисления, мы не можем туда положить пустоту, а ноль - это тоже значение.
Шесть из восьми типов имеет диапазон значений, а значит основное их отличие в объёме занимаемой памяти. У \code{double} и \code{float} тоже есть диапазоны, но они заключаются в точности представления дробной части. Диапазоны означают, что если попытаться положить в переменную меньшего типа большее значение, произойдёт «переполнение переменной».
\subsubsection{Переполнение целочисленных переменных}
@ -444,6 +446,8 @@ Constare - (лат. стоять твёрдо). Константность эт
array2 = {1, 2, 3, 4, 5}; // <@%\lh{dkgreen}{<-- здесь недопустима инициализация}@> <@ \label{codeline:arr-invalid} @>
\end{lstlisting}
% TODO Если мы не определяем переменную, понятно, данные мы хранить не планируем. Если определяем примитивную, помним, она инициализируется нулём, а вот если мы определяем ссылочный идентификатор, он имеет начальное значение null, то есть явно демонстрирует, что не ссылается ни на какой объект. Нулевой указатель - это гораздо более серьёзное явление, чем просто временное отсутствие объекта по идентификатору, очень часто это не инициализированные объекты и попытки вызова невозможных методов, но о том, почему NullPointerException это ваш лучший друг мы поговорим позже \\ \hline
\begin{frm} \excl Никак и никогда нельзя присвоить идентификатору целый готовый массив в процессе работы, нельзя стандартными средствами переприсвоить ряд значений части массива (так называемые слайсы или срезы). \end{frm}
Массивы бывают как одномерные, так и многомерные. Многомерный массив - это всегда массив из массивов меньшего размера: двумерный массив - это массив одномерных, трёхмерный - массив двумерных и так далее. Правила инициализации у них не отличаются. Преобразовать тип массива нельзя никогда, но можно преобразовать тип каждого отдельного элемента при чтении. Это связано с тем, что под массивы сразу выделяется непрерывная область памяти, а со сменой типа всех значений массива эту область нужно будет или значительно расширять или значительно сужать.
@ -656,7 +660,7 @@ Constare - (лат. стоять твёрдо). Константность эт
\textbf{Функция} - это исполняемый блок кода. Функция, принадлежащая классу называется \textbf{методом}.
\begin{lstlisting}[language=Java,style=JCodeStyle]
void int method(int param1, int param2) {
void method(int param1, int param2) {
//function body
}
@ -674,6 +678,9 @@ Constare - (лат. стоять твёрдо). Константность эт
Все аргументы передаются копированием, не важно, копирование это числовой константы, числового значения переменной или хранимой в переменной ссылке на массив. Сам объект в метод не копируется, а копируется только его ссылка.
% TODO void от англ пустота & Возвращаемого значения у метода может и не быть, в некоторых языках такие функции и методы называются процедурами, а в джаве для них просто придумали специальное возвращаемое значение - войд. войд от англ пустота, причём это не какое-то там банальное эмптинесс, а серьёзное масштабное космическое войд, чтобы подчеркнуть, что метод прям совсем ничего точно не возвращает, ох и затейники эти авторы языков программирования. \\ \hline
Возвращаемые из методов значения появляются в том месте, где метод был вызван. Если будет вызвано несколько методов, то весь контекст исполнения первого метода сохраняется, кладётся (на стек) в стопку уже вызванных методов и процессор идёт выполнять только что вызванный второй метод. По завершении вызванного второго метода, мы снимаем со стека лежащий там контекст первого метода, кладём в него вернувшееся из второго метода значение, если оно есть, и продолжаем исполнять первый метод.
\textbf{Вызов метода} - это, по смыслу, тоже самое, что подставить в код сразу его возвращаемое значение.

View File

@ -25,7 +25,7 @@ Java является языком со \textbf{строгой} (также мо
Термин явная типизация говорит нам о том, что при создании переменной мы должны ей обязательно присвоить какой-то тип, явно написав это в коде. В более поздних версиях языка (с 9й) стало возможным инициализировать переменные типа \code{var}, обозначающий нужный тип тогда, когда его возможно однозначно вывести из значения справа. Бывают языки с неявной типизацией, например, Python, там можно как указать тип, так его и не указывать, язык сам попробует по контексту догадаться, что вы имели ввиду; \\ \hline
лайд
cлайд
%Java - Статическая | Сильная | Явная
%C# - Статическая | Сильная | Явная
%Java9 - Статическая | Сильная | Неявная
@ -50,6 +50,8 @@ Java является языком со \textbf{строгой} (также мо
Плюсом такого подхода является возможность легко корректировать значения переменных в достаточно больших проектах. Представьте, что в вашем коде несколько тысяч строк, а какое-то число, скажем, возраст совершеннолетия, число 18, использовалось несколько десятков раз. При использовании приложения в стране, где совершеннолетием считается 21 год вы должны будете перечитывать весь код в поисках магических "18" и править их на "21". Да ещё и не запутаться, те ли это 18, которые про совершеннолетие, а не про количество карманов в жилетке Вассермана, хоть мы и знаем, что их 26. В случае с константой изменить число нужно в одном месте. \\ \hline
Слайд начальное значение & По умолчанию, создавая примитивную переменную ей из-за примитивности данных, присваивается начальное значение - ноль для числовых и ложь для булева. Что ещё раз доказывает, что мы храним там просто числа в двоичной системе счисления, мы не можем туда положить пустоту, а ноль - это тоже значение. \\ \hline
таблица «Основные типы данных» & Примитивных типов всего восемь и это, наверное, первое, что спрашивают на джуниорском собеседовании, это байт, шорт, инт, лонг, флоут, дабл, чар и булин. как вы можете заметить в этой таблице, шесть из восьми типов имеет диапазон значений, а значит основное их отличие в объёме занимаемой памяти. На самом деле у дабла и флоута тоже есть диапазоны, просто они заключаются в другом и их довольно сложно отобразить в простой таблице. Что значат эти диапазоны? они значат, что если мы попытаемся положить в переменную меньшего типа какое-то большее значение, произойдёт неприятность, которая носит название «переполнение переменной». \\ \hline
переполнение переменной если презы умеют в гифки, нужна вода, льющаяся в переполненный стакан & Интересное явление, рассмотрев его мы рассмотрим одни из самых трудноуловимых ошибок в программах, написанных на строго типизированных языках. С переполнением переменных есть одна неприятность: их не распознаёт компилятор. Итак, переполнение переменной - это ситуация, в которой как и было только что сказано, мы пытаемся положить большее значение в переменную меньшего типа. чем именно чревато переполнение переменной легче показать на примере (тут забавно будет вставить в слайд пару-тройку картинок из вот этого описания расследования крушения ракеты из-за переполнения переменной https://habr.com/ru/company/pvs-studio/blog/306748/) \\ \hline
@ -173,6 +175,8 @@ byte b = (byte)i ; & для явного преобразования типов
Слайд & Ссылочные типы отличаются от примитивных местом хранения информации. в примитивах данные хранятся прямо там, где существует переменная и где написан её идентификатор, а по идентификатору ссылочного типа внезапно хранится не значение, а ссылка. Эту ссылку можно представить как ярлык у вас на рабочем столе, то есть очевидно, что непосредственная информация хранится не там, где написан идентификатор. Такое явное разделение идентификатора переменной и данных, которые по нему можно найти, нам будет важно помнить и понимать при работе с ООП на дальнейших занятиях. \\ \hline
02-туалетка & Если мы не определяем переменную, понятно, данные мы хранить не планируем. Если определяем примитивную, помним, она инициализируется нулём, а вот если мы определяем ссылочный идентификатор, он имеет начальное значение null, то есть явно демонстрирует, что не ссылается ни на какой объект. Нулевой указатель - это гораздо более серьёзное явление, чем просто временное отсутствие объекта по идентификатору, очень часто это не инициализированные объекты и попытки вызова невозможных методов, но о том, почему NullPointerException это ваш лучший друг мы поговорим позже \\ \hline
Слайд & в идентификаторе массива, фактически, находится адрес его первого элемента. не лишним будет напомнить, что массив - это единая сплошная область данных, в связи с чем в массивах возможно осуществление доступа по индексу. Самый младший индекс любого массива - ноль, поскольку индекс - это значение смещения по элементам относительно начального адреса массива. То есть, для получения самого первого элемента нужно сместиться на ноль шагов. Очевидно, что самый последний элемент в массиве из, скажем, десяти значений, будет храниться по девятому индексу. \\ \hline
Лайвкод & Массивы в джава создают несколькими способами. В общем виде объявление это тип квадратные скобки как обозначение того, что это будет массив из переменных этого типа, идентификатор. Инициализировать массив можно либо ссылкой на другой массив, пустым массивом или заранее заданными значениями, записанными через запятую в фигурных скобках. Присвоить в процессе работы идентификатору возможно только значение ссылки из другого идентификатора или новый пустой массив. \\ \hline
@ -247,14 +251,15 @@ $ & Резюмируем, ... читаем слайд \\ \hline
Слайд & Методы обособлены и их параметры локальны, то есть никакой параметр никакой функции не виден другой функции. Из этого свойства есть очевидное следствие - нельзя писать функции внутри функций. Одна из наиболее частых ошибок новичка - невнимательно посмотреть на открывающие/закрывающие скобки и сильно удивляться тому, что компилятор ругается на синтаксически верно написанную функцию. \\ \hline
Слайд & Все аргументы в джава передаются копированием, не важно, копирование это числовой константы, числового значения переменной или хранимой в переменной ссылке например на массив. Обратите внимание, что сам объект в метод не копируется, а копируется только его ссылка. Через несколько лекций мы поговорим о том, что в метод можно передать и другой метод, но пока что оставим эту магию в стороне. Важно, что порядок записи аргументов должен строго соответствовать порядку записи параметров функции. \\ \hline
Слайд & Все аргументы в джава передаются копированием, не важно, копирование это числовой константы, числового значения переменной или хранимой в переменной ссылке например на массив. Обратите внимание, что сам объект в метод не копируется, а копируется только его ссылка. Через несколько лекций мы поговорим о том, что в метод можно передать и другой метод, но пока что оставим эту магию в стороне. Важно, что порядок записи аргументов должен строго соответствовать порядку записи параметров функции. Возвращаемые из методов значения возникают ровно в том месте, где метод был вызван, это обусловлено архитектурой компьютеров общего назначения и так называемым стеком вызовов. Если мы вызываем несколько методов, а именно это мы чаще всего и делаем в наших программах, то весь контекст исполнения первого метода сохраняется, кладётся (на стек) в стопку уже вызванных методов и процессор идёт выполнять только что вызванный второй метод. по завершении вызванного второго метода мы снимаем со стека лежащий там контекст первого метода, кладём в него вернувшееся из второго метода значение, если оно есть, и продолжаем исполнять первый метод. \\ \hline
Вот тут может мне какой-то реквизит понадобится вроде одноразовых тарелок, буду стек вызовов показывать & Возвращаемые из методов значения возникают ровно в том месте, где метод был вызван, это обусловлено архитектурой компьютеров общего назначения и так называемым стеком вызовов. Если мы вызываем несколько методов, а именно это мы чаще всего и делаем в наших программах, то весь контекст исполнения первого метода сохраняется, кладётся (на стек) в стопку уже вызванных методов и процессор идёт выполнять только что вызванный второй метод. по завершении вызванного второго метода мы снимаем со стека лежащий там контекст первого метода, кладём в него вернувшееся из второго метода значение, если оно есть, и продолжаем исполнять первый метод. \\ \hline
void от англ пустота & Возвращаемого значения у метода может и не быть, в некоторых языках такие функции и методы называются процедурами, а в джаве для них просто придумали специальное возвращаемое значение - войд. войд от англ пустота, причём это не какое-то там банальное эмптинесс, а серьёзное масштабное космическое войд, чтобы подчеркнуть, что метод прям совсем ничего точно не возвращает, ох и затейники эти авторы языков программирования. \\ \hline
Слайд & В дажва нет вечно запутывающих понятий из С++, поэтому не буду загружать вам голову всякими rvalue, lvalue, xvalue, хоть возвращаемые значения и являются rvalue, главное, запомнить, что вызов метода это по смыслу тоже самое, что подставить в код сразу его возвращаемое значение. Посмотрим внимательно на так хорошо нам знакомую функцию мейн - она в качестве параметра принимает массив строк, а в качестве возвращаемого значения у неё войд. Это кстати отличает джаву от классического С++, где функция мейн возвращает так называемый код завершения программы - целое число, оповещающее ОС о том, штатно ли завершилось приложение\\ \hline
Слайд & Методы бывают разные, писать их тоже можно по разному, но важно помнить, что джава - это регистрозависимый язык. Что это значит, и почему сейчас? Это значит, что большие буквы это не тоже самое, что маленькие буквы. В методах это особенно важно. \\ \hline
Слайд & Сигнатура метода — это имя метода и его параметры. В сигнатуру метода не входит возвращаемое значение. Так вот, никак и никогда нельзя написать два метода с одинаковой сигнатурой. А вот с разными - пожалуйста. И здесь мы сталкиваемся с интересным механизмом, который будем обсуждать на некоторых следующих уроках: перегрузка методов. \\ \hline
Слайд & Перегрузка методов - это механизм языка, позволяющий написать методы с одинаковыми названиями и разными оставшимися частями сигнатуры, чтобы получить некоторое единообразие при вызове семантически схожих методов с разнотипными данными. проще говоря можно назвать методы одинаково, но манипулировать при этом разными типами данных. \\ \hline

View File

@ -85,7 +85,6 @@
лайвкод 03-статическое-поле-ошибка & Посмотрим на опасность. Мы видим, что у каждого кота есть имя, и помним, что коты хранят значение своего имени каждый сам у себя. А знают экземпляры о названии поля потому что знают, какого класса они экземпляры. Но что если мы по невнимательности добавим свойство статичности к имени кота? \\ \hline
03-статическое-поле-признак & Создав тех же самых котов, которых мы создавали весь урок, мы получим двух мурзиков и ни одного барсика. Почему это произошло? По факту переменная у нас одна на всех, и значение тоже одно, а значит каждый раз мы меняем именно его, а все остальные коты ничего не подозревая смотрят на значение общей переменной и бодро его возвращают. Поэтому, чтобы не запутаться, к статическим переменным, как правило, обращаются не по ссылке на объект — cat1.name, а по имени класса — Cat.name. \\ \hline
03-статические-поля & К слову, статические переменные — редкость в Java. Вместо них применяют статические константы. Они определяются ключевыми словами static final и по соглашению о внешнем виде кода пишутся в верхнем регистре. \\ \hline
@ -134,105 +133,751 @@ Permanent Generation — эта область содержит метаинфо
\end{itemize} \\ \hline
Сборщик мусора & Вроде разобрались, разделили память так, как её разделяет ЖВМ. Предлагаю взглянуть на то, как ЖВМ осуществляет управление неиспользуемыми объектами \\ \hline
& \\ \hline
& \\ \hline
Но, прежде чем углубиться в детали, давайте сначала упомянем несколько вещей:
список справа & Но, прежде чем углубиться в детали, давайте сначала упомянем несколько вещей:
\begin{itemize}
\item Этот процесс запускается автоматически Java, и Java решает, запускать или нет этот процесс;
\item На самом деле это дорогостоящий процесс. При запуске сборщика мусора все потоки в вашем приложении приостанавливаются (в зависимости от типа GC, который будет обсуждаться позже);
\item На самом деле это более сложный процесс, чем просто сбор мусора и освобождение памяти.
\end{itemize}
Несмотря на то, что Java решает, когда запускать сборщик мусора, вы можете явно вызвать System.gc() и ожидать, что сборщик мусора будет запускаться при выполнении этой строки кода, верно?
\item На самом деле это дорогостоящий процесс. При запуске сборщика мусора все потоки в вашем приложении приостанавливаются (в зависимости от типа GC);
\item На самом деле это гораздо более сложный процесс, чем просто сбор мусора и освобождение памяти.
\end{itemize} \\ \hline
Это ошибочное предположение.
03-System.gc(); & Несмотря на то, что Java решает, когда запускать сборщик мусора, нам доступен метод класса System, который мы можем явно вызвать и ожидать, что сборщик мусора будет запускаться при выполнении этой строки кода, верно? Это ошибочное предположение. Вы только как бы просите Java запустить сборщик мусора, но, опять же, Java решать, делать это или нет. В любом случае явно вызывать System.gc() не рекомендуется. \\ \hline
Вы только как бы просите Java запустить сборщик мусора, но, опять же, Java решать, делать это или нет. В любом случае явно вызывать System.gc() не рекомендуется.
03-устройство памяти & Поскольку это довольно сложный процесс и может повлиять на производительность всего приложения, он реализован весьма разумно. Для этого используется так называемый процесс «Mark and Sweep» (отмечай и подметай). Java анализирует переменные из стека и «отмечает» все объекты, которые необходимо поддерживать в рабочем состоянии. Затем все неиспользуемые объекты очищаются. Фактически, чем больше мусора и чем меньше объектов помечены как живые, тем быстрее идет процесс. Чтобы сделать это еще более оптимизированным, память кучи состоит из нескольких частей, как показано на слайде. \\ \hline
Поскольку это довольно сложный процесс и может повлиять на вашу производительность, он реализован разумно. Для этого используется так называемый процесс «Mark and Sweep». Java анализирует переменные из стека и «отмечает» все объекты, которые необходимо поддерживать в рабочем состоянии. Затем все неиспользуемые объекты очищаются.
03-молодое-поколение & Все новые объекты начинаются с молодого поколения. Как только они выделены в коде Java, они попадают в этот подраздел, называемый eden space. В конце концов пространство эдема заполняется объектами. На этом этапе происходит незначительная сборка мусора, так называемая minor collection. Некоторые объекты (те, на которые есть ссылки) помечаются, а некоторые (те, на которые нет ссылок) - нет. Те, которые были отмечены, затем переходят в другой подраздел молодого поколения под названием S0 пространства выживших (обратите внимание, что само пространство выживших разделено на две части, S0 и S1). Те, которые остались немаркированными, удаляются автоматической сборкой мусора. \\ \hline
Фактически, чем больше мусора и чем меньше объектов помечены как живые, тем быстрее идет процесс. Чтобы сделать это еще более оптимизированным, память кучи на состоит из нескольких частей, как было показано на картинке с организацией памяти в Java.
03-выжившее-поколение & Так будет продолжаться до тех пор, пока пространство eden снова не заполнится; на этом этапе начинается новый цикл. События minor collection повторяются, но в этом цикле немного иначе. S0 был заполнен, и поэтому все отмеченные объекты, которые выживают как из пространства eden, так и из S0, фактически попадают во вторую часть пространства survivor, называемую S1. На приведенной на слайде диаграмме видно, что они помечены как из пространства выживших и в пространство выживших соответственно. \\ \hline
Давайте пройдёмся по поколениям.
03-третье-поколение & Следует отметить одну очень важную вещь: любые объекты, попадающие в пространство выживших, помечаются счетчиком возраста. Алгоритм проверит это, чтобы увидеть, соответствует ли он пороговому значению для перехода в старое поколение.
### Сборщик мусора. Молодое поколение.
С высокого уровня все новые объекты начинаются с молодого поколения. Как только они выделены в коде Java, они попадают конкретно в этот подраздел, называемый eden space. В конце концов пространство эдема заполняется предметами. На этом этапе происходит незначительное событие сборки мусора. Некоторые объекты (те, на которые есть ссылки) помечены, а некоторые (те, на которые нет ссылок) - нет. Те, которые были отмечены, затем переходят в другой подраздел молодого поколения под названием S0 пространства выживших (обратите внимание, что само пространство выживших разделено на две части, S0 и S1). Те, которые остались немаркированными, удаляются автоматической сборкой мусора Java.
При следующей второстепенной сборке повторяется тот же процесс. Однако на этот раз пространства выживших переключаются. Объекты, на которые даны ссылки, перемещаются в S0. Главная мысль в том, что объекты не обязательно переходят из S0 в S1 пространства выживших. На самом деле, они просто чередуются с тем, куда они переключаются при каждой minor сборке мусора.
![jc-03-gc01](uploads/6ed12a38aeaa5648bb55a34d86b7e2b2/jc-03-gc01.png)
Если эти процессы обобщить, то по существу все новые объекты начинаются в пространстве eden, а затем в конечном итоге попадают в пространство survivor, поскольку они переживают несколько циклов сборки мусора. \\ \hline
Так будет продолжаться до тех пор, пока пространство eden снова не заполнится; на этом этапе начинается новый цикл. События предыдущего абзаца повторяются, но в этом цикле все немного по-другому. S0 был заполнен, и поэтому все отмеченные объекты, которые выживают как из пространства eden, так и из S0, фактически попадают во вторую часть пространства survivor, называемую S1. На приведенной ниже диаграмме мы видим, что они помечены как **из пространства выживших** и **в пространство выживших** соответственно.
03-старое-поколение & Старое поколение можно рассматривать как место, где лежат долгоживущие объекты. По сути, если объекты достигают определенного возрастного порога после нескольких событий сборки мусора в молодом поколении, то затем они могут быть перемещены в старое поколение. Когда объекты собирают мусор из старого поколения, происходит крупное событие сборки мусора.
![jc-03-gc02](uploads/6ed12a38aeaa5648bb55a34d86b7e2b2/jc-03-gc02.png)
Предлагаю рассмотреть, как выглядит продвижение из пространства выживших молодого поколения в старое поколение. В приведенном выше примере все выжившие объекты, достигшие возрастного порога в 8 циклов — и это всего лишь пример, поэтому специально не запоминайте это число — перемещаются алгоритмом в старое поколение.
Следует отметить одну очень важную вещь: любые объекты, попадающие в пространство выживших, помечаются счетчиком возраста. Алгоритм проверит это, чтобы увидеть, соответствует ли он пороговому значению для перехода к следующему этапу: старое поколение (чуть позже доберёмся до этого).
Старое поколение состоит только из одной секции, называемой постоянным поколением. \\ \hline
При следующем второстепенном GC повторяется тот же процесс. Однако на этот раз пространства выживших переключаются. Объекты, на которые даны ссылки, перемещаются в S0.
Постоянное поколение & Обратите внимание, что постоянное поколение не заполняется, когда объекты старого поколения достигают определенного порога, а затем перемещаются (повышаются) в постоянное поколение. Скорее, постоянное поколение немедленно заполняется JVM метаданными, которые представляют классы и методы приложений во время выполнения. JVM иногда может следовать определенным правилам для очистки постоянного поколения, и когда это происходит, это называется полной сборкой мусора major collection.
![jc-03-gc03](uploads/6ed12a38aeaa5648bb55a34d86b7e2b2/jc-03-gc03.png)
Также, хотелось бы ещё раз упомянуть событие под названием остановить мир. Когда происходит небольшая сборка мусора (для молодого поколения) или крупная сборка мусора (для старого поколения), мир останавливается; другими словами, все потоки приложений полностью останавливаются и должны ждать завершения события сборки мусора. \\ \hline
Главная мысль в том, что объекты не обязательно переходят из S0 в S1 пространства выживших. На самом деле, они просто чередуются с тем, куда они переключаются при каждом незначительном событии сборки мусора.
Если эти процессы обобщить, то по существу все новые объекты начинаются в пространстве eden, а затем в конечном итоге попадают в пространство survivor, поскольку они переживают циклы сборки мусора.
### Сборщик мусора. Старое поколение.
Старое поколение можно рассматривать как место, где лежат долгоживущие объекты. По сути, если объекты достигают определенного возрастного порога после нескольких событий сборки мусора в молодом поколении, то затем они могут быть перемещены в старое поколение.
Когда объекты собирают мусор из старого поколения, происходит крупное событие сборки мусора.
Предлагаю рассмотреть, как выглядит продвижение из пространства выживших молодого поколения в старое поколение.
![jc-03-gc04](uploads/6ed12a38aeaa5648bb55a34d86b7e2b2/jc-03-gc04.png)
В приведенном выше примере все выжившие объекты, достигшие возрастного порога в 8 циклов — и это всего лишь пример, поэтому специально не запоминайте это число — перемещаются алгоритмом в старое поколение.
Старое поколение состоит только из одной секции, называемой постоянным поколением.
### Сборщик мусора. Постоянное поколение.
Внимание! Постоянное поколение **не** заполняется, когда объекты старого поколения достигают определенного порога, а затем перемещаются (повышаются) в постоянное поколение.
Скорее, постоянное поколение немедленно заполняется JVM метаданными, которые представляют классы и методы приложений во время выполнения.
JVM иногда может следовать определенным правилам для очистки постоянного поколения, и когда это происходит, это называется **полной сборкой мусора**.
Также, хотелось бы упомянуть событие под названием *остановить мир*. Когда происходит небольшая сборка мусора (для молодого поколения) или крупная сборка мусора (для старого поколения), мир останавливается; другими словами, все потоки приложений полностью останавливаются и должны ждать завершения события сборки мусора.
### Сборщик мусора. Реализации
Стоит упомянуть, что JVM имеет пять типов реализаций GC:
Сборщик мусора. Реализации
\begin{itemize}
\item последовательный
\item параллельный
\item CMS
\item G1
\item ZGC
\end{itemize} & Стоит упомянуть, что JVM имеет пять типов реализаций GC:
- Последовательный сборщик мусора. Это самая простая реализация GC, поскольку она в основном работает с одним потоком. В результате эта реализация GC замораживает все потоки приложения при запуске. Поэтому не рекомендуется использовать его в многопоточных приложениях, таких как серверные среды;
- Параллельный сборщик мусора. Это GC по умолчанию для JVM, который иногда называют сборщиками пропускной способности. В отличие от последовательного сборщика мусора, он использует несколько потоков для управления пространством кучи, но также замораживает другие потоки приложений во время выполнения GC. Если мы используем этот GC, мы можем указать максимальные потоки сборки мусора и время паузы, пропускную способность и занимаемую площадь (размер кучи);
- Сборщик мусора CMS. Реализация Concurrent Mark Sweep (CMS) использует несколько потоков сборщика мусора для сбора мусора. Он предназначен для приложений, которые предпочитают более короткие паузы при сборке мусора и могут позволить себе совместно использовать ресурсы процессора со сборщиком мусора во время работы приложения. Проще говоря, приложения, использующие этот тип GC, в среднем реагируют медленнее, но не перестают отвечать, чтобы выполнить сборку мусора. Здесь следует отметить, что, поскольку этот GC является параллельным, вызов явной сборки мусора, такой как использование System.gc() во время работы параллельного процесса, приведет к сбою / прерыванию параллельного режима;
- Сборщик мусора CMS. Реализация Concurrent Mark Sweep (CMS) использует несколько потоков сборщика мусора для сбора мусора. Он предназначен для приложений, которые требуют более коротких пауз при сборке мусора и могут позволить себе совместно использовать ресурсы процессора со сборщиком мусора во время работы приложения. Проще говоря, приложения, использующие этот тип GC, в среднем работают медленнее, но не перестают отвечать, чтобы выполнить сборку мусора. Здесь следует отметить, что, поскольку этот GC является параллельным, вызов явной сборки мусора, такой как использование System.gc() во время работы параллельного процесса, приведет к сбою или прерыванию параллельного режима;
- Сборщик мусора G1. Сборщик мусора G1 (Garbage First) предназначен для приложений, работающих на многопроцессорных компьютерах с большим объемом памяти. Он доступен с обновления 4 JDK7 и в более поздних версиях. Сборщик G1 заменит сборщик CMS, поскольку он более эффективен;
- Z сборщик мусора. ZGC (Z Garbage Collector) - это масштабируемый сборщик мусора с низкой задержкой, который дебютировал в Java 11 в качестве экспериментального варианта для Linux. JDK 14 представил ZGC под операционными системами Windows и macOS. ZGC получил статус production начиная с Java 15. ZGC выполняет всю дорогостоящую работу одновременно, не останавливая выполнение потоков приложений более чем на 10 мс, что делает его подходящим для приложений, требующих низкой задержки.
Теперь, пройдя часть со сборщиком мусора, давайте немного подытожим разницу между Стеком и Кучей:
- Z сборщик мусора. ZGC (Z Garbage Collector) - это масштабируемый сборщик мусора с низкой задержкой, который дебютировал в Java 11 в качестве экспериментального варианта для Linux. JDK 14 представил ZGC под операционными системами Windows и macOS. ZGC получил статус production начиная с Java 15. ZGC выполняет всю дорогостоящую работу одновременно, не останавливая выполнение потоков приложений более чем на 10 мс, что делает его подходящим для приложений, требующих низкой задержки.\\ \hline
Итоги рассмотрения устройства памяти
\begin{itemize}
\item куча доступна везде, объекты доступны отовсюду
\item все объекты хранятся в куче, все локальные переменные хранятся на стеке
\item стек недолговечен
\item и стек и куча могут быть переполнены
\item куча много больше стека, но стек гораздо быстрее
\end{itemize}
&
- Куча используется всеми частями приложения в то время как стек используется только одним потоком исполнения программы;
- Всякий раз, когда создается объект, он всегда хранится в куче, а в памяти стека содержится ссылка на него. Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче;
- Объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков;
- Управление памятью в стеке осуществляется по схеме LIFO;
- Всякий раз, когда создается объект, он всегда хранится в куче, а в памяти стека содержится ссылка на него. Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче;
- Стековая память существует лишь какое-то время работы программы, а память в куче живет с самого начала до конца работы программы;
- Также, хотелось бы отметить, что мы можем использовать -Xms и -Xmx опции JVM, чтобы определить начальный и максимальный размер памяти в куче. Для стека определить размер памяти можно с помощью опции -Xss;
- Если память стека полностью занята, то Java Runtime бросает java.lang.StackOverflowError, а если память кучи заполнена, то бросается исключение java.lang.OutOfMemoryError: Java Heap Space;
- Размер памяти стека намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.
& \\ \hline
& \\ \hline
- Размер памяти стека намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи. \\ \hline
КОНСТРУКТОР & После такого, копания в шестерёнках, время расслабиться и перейти к разбору конструкторов Java. Вернёмся к нашим барсикам. \\ \hline
03-два-кота & Чтобы создать объект мы тратим одну строку кода (Cat cat1 = new Cat()). Поля этого объекта заполнятся автоматически значениями по-умолчанию (целочисленные - 0, логические - false, ссылочные - null и т.д.). Нам бы хотелось дать коту какое-то имя, указать его возраст и цвет, поэтому мы пишем ещё три строки кода. В таком подходе есть несколько недостатков: во-первых, мы напрямую обращаемся к полям объекта (чего не стоит делать, в соответствии с принципами инкапсуляции, о которых речь пойдет чуть позже), а во-вторых, если полей у класса будет намного больше, то для создания всего лишь одного объекта будет уходить 5-10-15 строк кода, что очень громоздко и утомительно. Было бы неплохо иметь возможность сразу, при создании объекта указывать значения его полей. \\ \hline
03-плохой-конструктор лайвкод & Для инициализации объектов при создании в Java предназначены конструкторы. Конструктор - это частный случай метода в том смысле, что он тоже выполняет какие-то действия. Имя конструктора обязательно должно совпадать с именем класса, возвращаемое значение не пишется. Если создать конструктор класса Cat как показано на слайде, он автоматически будет вызываться при создании объекта. Теперь, при создании объектов класса Cat, все коты будут иметь одинаковые имена, цвет и возраст (это будут белые двухлетние Барсики). \\ \hline
03-параметризированный-конструктор лайвкод & При использовании конструктора из предыдущего примера, все созданные коты будут одинаковыми, пока мы вручную не поменяем значения их полей. Чтобы можно было указывать начальные значения полей наших объектов необходимо создать параметризированный конструктор. В приведенном на слайде примере, в параметрах конструктора используется первая буква от названия поля, это сделано для упрощения понимания логики заполнения полей объекта. И очень скоро будет заменено на более корректное использование ключевого слова this. \\ \hline
03-вызов-параметризированного лайвкод & При такой форме конструктора, когда мы будем создавать в программе кота, необходимо будет обязательно указать его имя, цвет и возраст. Набор полей, которые будут заполнены через конструктор, вы определяете сами, то есть вы не обязаны заполнять все поля, которые есть в классе записывать в параметры конструктора, но при вызове обязаны заполнить аргументами все, что есть в параметрах, как при вызове метода.
Наборы значений имён, цветов и возрастов будут переданы в качестве аргументов конструктора (n, c, a), а конструктор уже перезапишет полученные значения в поля объект (name, color, age). То есть начальные значения полей каждого из объектов будет определяться тем, что мы передадим ему в конструкторе. Как видите, теперь нам нет необходимости обращаться напрямую к полям объектов, и мы в одну строку можем инициализировать наш новый объект. \\ \hline
03-перегрузка-конструктора лайвкод & Мы можем как не объявлять ни одного конструктора, так и объявить их несколько. Также как и при перегрузке методов, имеет значение набор аргументов, не может быть нескольких конструкторов с одинаковым набором аргументов. В наш пример мы допишем конструктор, принимающий только один параметр - имя, а значит цвет будет неизвестен, а возраст, например, равен единице. \\ \hline
03-вызов-перегрузки лайвкод & В этом случае допустимы будут следующие варианты создания объектов - с полным набором параметров и только с именем. Соответствующий перегружаемый конструктор вызывается в зависимости от аргументов, указываемых при выполнении оператора new. Не лишним будет напомнить что у классов всегда есть или лучше сказать всегда должен быть конструктор, даже если вы не пропишите свою реализацию конструктора. \\ \hline
03-конструктор-по-умолчанию лайвкод & Пока мы недалеко ушли от вызовов конструкторов, важно упомянуть, что как только вы создали в классе свою реализацию конструктора, пустой конструктор, называемый также конструктором по-умолчанию автоматически создаваться не будет. И если вам понадобится такая форма конструктора (в которой нет параметров, и которая ничего не делает), необходимо будет создать его вручную: public Cat() {}. То есть компилятор как-бы думает, что если вы не описали конструкторы, значит вам не важно, как будет создаваться объект, а значит, хватит пустого конструктора, ну а если конструктор написан, значит выкручивайтесь сами. (здесь можно показать конструкторы в скомпилированных класс-файлах) \\ \hline
ключевое слово this
this - это указатель на текущий экземпляр класса.
нужен когда одинаковые имена полей и параметров
нужен чтобы вызвать конструктор из конструктора & Рассмотрим ещё один момент. Пару слайдов назад я упомянул о ключевом слове this, значит посмотрим, на часть ситуаций, в которых его используют. В контексте конструкторов, применять this нужно в двух случаях:
\begin{itemize}
\item Когда у переменной экземпляра класса и переменной метода/конструктора одинаковые имена;
\item Когда нужно вызвать конструктор одного типа (например, конструктор по умолчанию или параметризированный) из другого. Это еще называется явным вызовом конструктора.
\end{itemize}
на самом деле, это ключевое слово используется чаще, но в основном с этими целями, не так много, — всего два случая, в контексте конструкторов. Рассмотрим обе ситуации на примерах. \\ \hline
03-параметризированный-конструктор & Вспоминаем наш конструктор, видим, что переменные в параметрах называются не так, как проименованы поля класса. Многим программистом казалось это странным — вводить переменную с новым именем, если в итоге речь идет об одном и том же. Об имени, цвете или возрасте в классе Cat. Поэтому, разработчики языка задумались о том, чтобы удобно сделать использование одного имени переменной. Другими словами, зачем иметь два имени для переменной, обозначающей одно и то же.\\ \hline
03-параметризированный-конструктор-2 & хотелось бы сделать как-то так. Но в этом случае возникает проблема. У нас теперь две переменные, которые называются одинаково. Один String name принадлежит классу Cat, а другой String name находится в локальной видимости конструктора. А жвм как и любой другой электрический прибор всегда идёт по пути наименьшего сопротивления, когда не знает, какую переменную вы имеете ввиду. То есть, когда пишете строку name = name; Java берёт самую близкую name из конструктора и для левой и для правой части оператора присваивания, и получается, что вы просто присваиваете значение переменной name из конструктора, ей же. Что не имеет никакого смысла. Для решения этой проблемы и некоторых других было введено ключевое слово this, которое в данном случае укажет, что нужно вызывать переменную не конструктора, а класса Cat. \\ \hline
03-правильный-конструктор & То есть this сошлется на вызвавший объект, как было сказано в начале. В результате чего имя котика через конструктор будет установлено создаваемому объекту. Таким образом, здесь this позволяет не вводить новые переменные для обозначения одного и того же, что позволяет сделать код менее перегруженным дополнительными переменными. \\ \hline
03-один-из-другого & Второй случай частого использования this с конструкторами - вызов одного конструктора из другого. это может пригодиться когда у вас (как ни странно) несколько конструкторов и вам не хочется в новом конструкторе переписывать код инициализации, приведенный в конструкторе ранее. Все не так страшно как кажется. Напишем немного синтетический пример, который, тем не менее, должен многое объяснить. Здесь вызывается обычный конструктор с тремя параметрами, который принимает имя цвет и возраст, но, допустим, когда котята рождаются возраст им задавать смысла немного, поэтому нам может пригодиться и конструктор просто с именем и цветом, а зачем писать присваивание имени и цвета несколько раз, если можно вызвать соответствующий конструктор? Но на такой вызов есть ограничение, конструктор из конструктора можно вызвать только один раз и только на первой строке конструктора. \\ \hline
public Cat (Cat cat) {
this(cat.name, cat.color, cat.age);
} & Есть еще один вид конструктора - это конструктор копирования. Чтобы создать конструктор копирования, мы можем объявить конструктор, который принимает объект того же типа, в нашем случае котика, в качестве параметра, а в самом конструкторе аналогично конструктору, заполняющему все параметры, заполнить каждое поле входного объекта в новый экземпляр. Но у нас уже есть конструктор, который заполняет все поля, зачем нам очень похожий, спросите вы и будете правы
(лайвкод) благодаря имеющемуся у нас конструктору со всеми нужными параметрами, с помощью ключевого слова this явно вызвать конструктор заполняющий все поля кота, значениями из переданного объекта, фактически, его копирующий.
То, что мы имеем здесь, это неглубокая копия, что удобно и довольно просто, поскольку все наши поля int и два String в данном случае являются либо примитивными, либо неизменяемыми типами. Если класс Java имеет изменяемые поля, например, массивы, то мы можем вместо простой сделать глубокую копию внутри его конструктора копирования. При глубокой копии вновь созданный объект не должен зависеть от исходного, потому что мы создаем отдельную копию каждого изменяемого объекта, а значит, например, просто скопировать ссылку на массив будет недостаточно. Такая вложенность может быть любая и определяется поставленной задачей. \\ \hline
Инкапсуляция & Получше узнав о классах и объектах в джава, добавив для себя понимания организации этого добра в памяти, можно приступить к полному погружению в ООП. Начнём с Инкапсуляции.\\ \hline
Инкапсуляция - определение (от лат...) & Инкапсуляция связывает данные с манипулирующим ими кодом и позволяет управлять доступом к членам класса из отдельных частей программы, предоставляя доступ только с помощью определенного ряда методов, что позволяет предотвратить злоупотребление этими данными. То есть класс должен представлять собой "черный ящик", которым возможно пользоваться, но его внутренний механизм защищен от повреждений. \\ \hline
чёрный ящик, 03-useless-box & Как работает той или иной поисковик, которым вы пользуетесь? Как именно он ищет информацию по тем или иным словам, которые вы вводите? Почему одни результаты находятся сверху в результатах поиска, а не какие-то другие? Хотя мы и используем поисковые средства каждый день, но, мало кто знает ответы на эти вопросы. Но это и не важно. Ведь это никому не нужно знать, когда поисковики выполняются свою задачу - ведь только это и важно. Все это возможно благодаря одному из главных принципов объектно-ориентированного программирования — инкапсуляции. Изначальное значение слова «инкапсуляция» в программировании — объединение данных и методов работы с этими данными в одной упаковке ("капсуле"). \\ \hline
кот в мешке & В Java в роли упаковки-капсулы выступает класс. Класс содержит в себе и данные (поля класса), и действия (методы класса) для работы с этими данными. Также, можно встретить такое понятие как "сокрытие". Оно реализует два направления: сокрытие реализации и сокрытие данных. Хорошо, но с помощью чего достигается сокрытие? Все члены класса в языке Java - поля и методы - имеют модификаторы доступа. Мы уже сталкивались с модификатором public, создавая публичные классы и публичную точку входа в программу. public означает доступность отовсюду, обычно используется для методов. Модификаторы доступа позволяют задать допустимую область видимости для членов класса, то есть контекст, в котором можно употреблять данную переменную или метод. \\ \hline
03-МД-без-протекта-и-привата & Есть два модификатора, которые нам уже известны, это паблик и пекеж-приват, также известный как дефолтный, пакетный или отсутствующий модификатор. Что это значит? это значит, что вообще всё что мы пишем в джаве имеет уровень доступа и если мы не определили этот уровень явно, то джава отнесёт наши данные к уровню доступности внутри пакета. Про пакеты мы говорили в самом начале, и вот, если публичные классы находятся в одном пакете - им доступны поля и методы друг друга, имеющие модификатор пекеж-прайват.\\ \hline
03-МД-без-протекта & модификатор private определяет доступность только внутри класса, и предпочтительнее всех. Вообще, хорошая практика предоставлять минимальный доступ к переменным в своём классе, чтобы те, кто им пользуются не внесли какие-то неожиданные изменения в его работу. Возникает законный вопрос, как так получается, что мы пользуемся всякими сложными механизмами без особенно долгого осмысливания, как они устроены и на чем основана их работа? Как мы можем дёргать за все эти ниточки ЖРЕ, если максимальная приватность - это очень хорошо? Все просто: создатели всех библиотек предоставляют простой и удобный интерфейс к своим разработкам. \\ \hline
продублировать 3-26 (поля класса кот) лайвкод & Внимательно осмотрев класс кота мы приходим к выводу, что хранить возраст котов очень неудобно, потому что каждый год нужно будет обновлять это значение для каждого объекта кота в программе, а это может оказаться утомительно. Выходом может оказаться хранение не возраста, а неизменяемого параметра даты рождения и подсчёт возрастка каждый раз, когда его запрашивают. Ведь человеку, который запрашивает возраст котика, не интересно, каким образом получено значение, прочитано из поля или вычислено, ему важен конечный результат. Вот это и есть сокрытие реализации. \\ \hline
cat.name = "";
cat.color = "GREEEEEN";
cat.age = -12345;
лайвкод & Внимательно рассмотрим класс котика, хотя, конечно, мы его писали, что может пойти не так? А не так здесь может быть очень многое. Например, кто-то может создать хорошего кота, а потом его переименовать, перекрасить в зелёный цвет или сделать ему отрицательный возраст. Коту такое явно не понравится. \\ \hline
& \\ \hline
& \\ \hline
Какой ужас, взгляните на эти значения. Данный класс котика позволяет присваивать полям безумные значение. В результате в программе находятся объекты с некорректным состоянием, типа вот этого котика с зелёным цветом и с возрастом -12345 лет.
Какая же тут была допущена ошибка? Все поля находятся в публичном доступе. К ним можно обратиться в любом месте программы: достаточно просто создать объект Cat — и все, у любого программиста есть доступ к его данным напрямую через оператор “.”
Нам нужно как-то защитить наши данные от некорректного вмешательства извне.
Во-первых, все переменные экземпляра (поля) необходимо помечать модификатором private. Private — самый строгий модификатор доступа в Java. Если ты его используешь, поля класса Cat не будут доступны за его пределами.
Теперь поля вроде как защищены. Но получается, что доступ к ним закрыт “намертво”: в программе нельзя даже получить вес существующей кошки, если это понадобится. Это тоже не вариант: в таком виде наш класс практически нельзя использовать.
Что же, нам нужно решить вопросы с получением и изменением значений полей. На помощь к нам приходят геттеры и сеттеры.
Название происходит от английского “get” — “получать” (т.е. “метод для получения значения поля”) и set — “устанавливать”
Предлагаю посмотреть на класс со стороны применения этих методов:
public class Cat {
private String name;
private String color;
private int age;
public Cat(String name, String color, int age) {
this.name = name;
this.color = color;
this.weight = age;
}
public Cat() {
}
void voice() {
System.out.println(name + " meows");
}
void jump() {
if (this.age < 5) System.out.println(name + " jumps");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Теперь у нас есть возможность получать данные и устанавливать их. Например, с помощью getColor ьы можем получить значение окраса котика.
Давайте посмотрим пример создания такого котика:
public class Main {
public static void main(String[] args) {
Cat murka = new Cat("Мурка", "Чёрный", 4);
String murkaName = murka.getName();
String murkaColor = murka.getColor();
int murkaAge = murka.getAge();
System.out.println("Имя кота: " + murkaName);
System.out.println("Окрас кота: " + murkaColor);
System.out.println("Возраст кота: " + murkaAge);
}
}
Вывод в консоле:
Имя кота: Мурка
Окрас кота: Чёрный
Возраст кота: 5
Теперь из другого класса (Main) есть доступ к полям Cat, но только через геттеры. Обрати внимание — у геттеров стоит модификатор доступа public, то есть они доступны из любой точки программы.
А что, если нам хочется обновить возраст котика? Не проблема, тут спокойно можно использовать геттер:
public class Main {
public static void main(String[] args) {
Cat murka = new Cat("Мурка", "Чёрный", 4);
System.out.println("Изначальный возраст кота — " + murka.getAge());
barsik.setName(5);
System.out.println("Новый возраст кота — " + murka.getAge());
}
}
Вывод в консоль:
Изначальный возраст кота — 4
Новый возраст кота — Василий - 5
Сейчас у многих мог возникнуть вопрос, а в чём разница? Что сеттер — это полноценный метод. А в метод, в отличие от поля, ты можешь заложить необходимую тебе логику проверки, чтобы не допустить неприемлемых значений. Например, можно легко не позволить назначение отрицательного числа в качестве возраста:
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
System.out.println("Ошибка! Возраст не может быть отрицательным числом!");
}
}
Так вот это является одним из трёх китов ООП - Инкапсуляцией.
Еще один момент хотелось бы немного затронуть - это Enum. Кроме отдельных примитивных типов данных и классов в Java есть такой тип как enum или перечисление. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора enum, после которого идет название перечисления. Затем идет список элементов перечисления через запятую:
enum Season { WINTER, SPRING, SUMMER, AUTUMN }
Перечисление фактически представляет новый тип, поэтому мы можем определить переменную данного типа и использовать ее:
public class Main{
public static void main(String[] args) {
Season current = Season.SPRING;
System.out.println(current);
}
}
Выведет SPRING.
Каждое перечисление имеет статический метод values(). Он возвращает массив всех констант перечисления:
public class Main{
public static void main(String[] args) {
Season[] seasons = Season.values();
for (Season s : seasons) { System.out.println(s); }
}
}
А метод ordinal() возвращает порядковый номер определенной константы (нумерация начинается с 0).
Перечисления, как и обычные классы, могут определять конструкторы, поля и методы. Например:
\begin{verbatim}
public class Main{
public static void main(String[] args) {
System.out.println(Color.RED.getCode());
System.out.println(Color.GREEN.getCode());
}
}
enum Color{
RED("#FF0000"), BLUE("#0000FF"), GREEN("#00FF00");
private String code;
Color(String code){
this.code = code;
}
public String getCode(){ return code;}
}
\end{verbatim}
Перечисление Color определяет приватное поле code для хранения кода цвета, а с помощью метода getCode оно возвращается. Через конструктор передается для него значение. Следует отметить, что конструктор по умолчанию приватный, то есть имеет модификатор private. Любой другой модификатор будет считаться ошибкой. Поэтому создать константы перечисления с помощью конструктора мы можем только внутри перечисления.
Это было введение в первое часть ООП, дальше переходим ко второму - Наследование.
Наследование:
extends;
Object (глобальное наследование);
protected;
конструкторы в наследовании. Кто кого вызывает - авокадо
конструкторы по умолчанию при наследовании и что делать при ситуации, когда отсутствует у родителя - эксепшн
super, опираясь на this;
преобразование типов (скрины вспомнить. С ссылочными тыры-пыры);
final (переопределение - не надо, а предотвращение наследования);
абстракт
Давайте представим, что мы хотим создать помимо класса котиков, но и класс для собачек. Данный класса будет выглядеть так:
public class Dog {
public String name;
public String color;
public int age;
public Dog(String name, String color, int age) {
this.name = name;
this.color = color;
this.weight = age;
}
public Dog() {
}
void voice() {
System.out.println(name + " grrrr");
}
void jump() {
if (this.age < 5) System.out.println(name + " jumps");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Что же, вот и описание класса для собачек. Вроде выглядит неплохо, пока мы не сталкиваемся с необходимостью описать классы для целого зоопарка. Мы видим, что в наших классах есть очень много одинаковых полей и методов.
А как мы помним из занятия по циклам - если что-то повторяется в коде несколько раз - это очень плохо и надо срочно от этого избавиться, чтобы не плодить сотни строк кода, в которых даже разбираться никто не будет. Тут нам на помощь приходит наследование. И кот и пёс являются животными, и у всех описываемых нами животных есть имя, возраст, окрас. И все описываемые нами животные могут бегать, прыгать, и откликаться на имя. Создадим так называемый родительский, он же базовый, класс, животное. И поместим в него общие поля.
public class Animal {
private String name;
private String color;
private int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public Animal() {
}
void jump() {
if (this.age < 5) System.out.println(name + " jumps");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Пока не очень ясно зачем мы так делаем, ну есть у нас класс животное и хорошо, всё отлично работает, можно больше ничего не писать. Но, не тут то было, несмотря на все свои сходства, наши животные, например, издают разные звуки, поэтому, унаследовав от родительского класса основные свойства и поведение, мы в классах наследниках описываем разные методы например, реакции на оклик.
Так что же нужно, чтобы унаследовать какой-то класс? Чтобы унаследовать один класс от другого, нужно после объявления нашего класса указать ключевое слово extends и написать имя родительского класса. Реализуем котика и собачку:
class Cat extends Аnimal {
void voice() {
System.out.println(getname() + " meows");
}
}
class Dog extends Аnimal {
void voice() {
System.out.println(name + " grrrr"); //name fail
}
}
У всей этой прекрасной объектной ориентированности есть одно ограничение: для каждого создаваемого подкласса можно указать только один суперкласс. В Java не поддерживается множественное наследование от нескольких суперклассов в одном подклассе. Если класс-родитель не указан, таковым считается класс Object. Также, мы не смогли бы определить поле имя, как показано в примере с собачкой, хотя могла получить доступ к методу геттеру получения значения имени.
Что же с ним не так? Вспоминаем информацию о модификаторах доступа и находим несостыковку. Модификатор private определяет область видимости только внутри класса, а если нам нужно чтобы переменную было видно ещё и в классах-наследниках, нужен модификатор доступа по умолчанию. А если мы создаём класс наследник в каком-то другом пакете, то и вовсе protected. Это просто нужно запомнить, что если мы хотим создавать наследников от нашего класса, то те поля, которые будут наследоваться, должны быть не приватными, а защищёнными.
Всё же, давайте чуток подробнее рассмотрим модификаторы доступа в наследовании.
К членам данных и методам класса можно применять следующие модификаторы доступа:
private. В этом случае член данных класса или метод класса доступен только из методов данного класса. Из методов подклассов и объектов (экземпляров) данного класса доступа нет;
protected (защищенный доступ). В этом случае есть доступ из данного класса, объекта этого класса а также подкласса. Тем не менее, нет доступа из объекта подкласса;
public. В этом случае есть доступ из данного класса, подкласса, объекта данного класса а также объекта подкласса.
Также, стоит упомянуть об особенностях каждого из модификаторов.
Если члены данных или методы класса объявлены с модификатором доступа private, тогда они считаются:
доступными из методов класса, в котором они объявлены;
недоступными для всех других методов любых классов (подклассов), экземпляров (объектов) любых классов.
Если члены данных или методы класса объявлены с модификатором доступа protected, тогда они считаются:
доступными из методов класса, в котором они объявлены;
доступными из методов подкласса. Это касается и случая, когда унаследованный класс объявляется в другом пакете;
доступными из экземпляров (объектов) данного класса;
доступными из экземпляров (объектов) подкласса.
Модификатор доступа public применяется если нужно получить доступ к члену данных или методу класса из любой точки программы. Доступ к public членам данных и методам класса имеют:
методы данного класса;
методы подкласса;
объекты данного класса;
объекты подкласса;
объекты, которые объявлены в методах классов, которые находятся в других пакетах.
И последний модификатор доступа - это default. Он не обозначается ключевым словом, поскольку установлен в Java по умолчанию для всех полей и методов.
Случаи его применения ограничены, как и у модификатора protected. Чаще всего default-доступ используется в пакете, где есть какие-то классы-утилиты, не реализующие функциональность всех остальных классов в этом пакете.
А теперь пришло время вернуться к конструкторам. Надо же разобраться какой и когда вызывается и после кого.
Будем снова рассматривать пример с животными.
Начнём мы с того, что при создании объекта в первую очередь вызывается конструктор его базового класса, а только потом — конструктор самого класса, объект которого мы создаем.
То есть при создании объекта Cat сначала отработает конструктор класса Animal, а только потом конструктор Cat.
Чтобы убедиться в этом — упростим классы и добавим в конструкторы Cat и Animal вывод в консоль.
public class Animal {
public Animal() {
System.out.println("Ctor Animal");
}
}
public class Cat extends Animal {
public Cat() {
System.out.println("Ctor Cat");
}
public static void main(String[] args) {
Cat cat = new Cat();
}
}
Вывод в консоль:
Ctor Animal
Ctor Cat
Мы можем явно вызвать конструктор базового класса в конструкторе класса-потомка. Базовый класс еще называют “суперклассом”, поэтому в Java для его обозначения используется ключевое слово super.
Давайте сделаем это с классами энимэл и котика. Возьмём старый класс энимал и модифицируем класс котика.
Класс энимэл с геттерами и сеттерами:
public class Animal {
private String name;
private String color;
private int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public Animal() {
}
// getters and setters
// ...
}
И модифицированный класс котика:
public class Cat extends Animal {
private String voice;
public Cat(String name, String color, int age, String voice){
super(name, color, age);
this.voice = voice;
}
void voiceCat() {
System.out.println(getName() + " " + voice);
}
}
После, вызвать:
public class Main {
public static void main(String[] args) {
Cat cat = new Cat("Murka", "Black", 4, "muuuur");
cat.voiceCat();
}
}
Вывод в консоль:
Murka muuuur
В конструкторе Cat мы вызвали конструктор Animal и передали в него три поля. Нам осталось явно проинициализировать только одно поле — voice, которого в Animal нет.
Ранее, мы говорили о том, что при создании объекта в первую очередь вызывается конструктор класса-родителя. Так вот, именно поэтому слово super() всегда должно стоять в конструкторе первым!
Иначе логика работы конструкторов будет нарушена и программа выдаст ошибку.
Компилятор знает, что при создании объекта класса-потомка сначала вызывается конструктор базового класса. И если будет выполнена попытка вручную изменить это поведение - он не позволит этого сделать.
Перед продолжением, хотелось бы сделать небольшое отступление, раз уж мы заговорили о super(), нужно бы и пояснить разницу между им и this.
this и super - это два специальных ключевых слова в Java, которые представляют соответственно текущий экземпляр класса и его суперкласса. Java-программисты часто путают эти слова и обнаруживают слабую осведомленность об их специальных свойствах, о которых нередко спрашивают на интервью по Java Сore. Вот, например, пара вопросов, из того, что сразу приходит на ум, о this и super, Можно ли присвоить другое значение ключевому слову this в Java? и какая разница между ключевыми словами this и super в Java.
Так вот, как я уже сказал, главное отличие между this и super в Java в том, что this представляет текущий экземпляр класса, в то время как super - текущий экземпляр родительского класса. Вот один из примеров использования переменных this и super — мы уже разбирали примеры вызовов конструкторов одного из другого, т.н. вызовы конструкторов по цепочке, это возможно благодаря использованию ключевых слов this и super. Внутри класса для вызова своего конструктора без аргументов используется this(), тогда как super() используется для вызова конструктора без аргументов, или как его ещё называют, конструктора по умолчанию родительского класса. Ну или как мы сделали ранее, вообще любой другой конструктор, передав ему соответствующие параметры.
Ещё this и super в Java используются для обращения к переменным экземпляра класса и его родителя. Вообще-то, к ним можно обращаться и без префиксов super и this, но только если в текущем блоке такие переменные не перекрываются другими переменными, т.е. если в нем нет локальных переменных с такими же именами, в противном же случае использовать имена с префиксами придется обязательно, но это не беда, т.к. в таком виде они даже более читабельны. Классическим примером такого подхода является использование this внутри конструктора, который принимает параметр с таким же именем, как и у переменной экземпляра.
Теперь вернёмся к нашим котикам и собачкам с наследованием.
В этой иерархии классов можно проследить следующую цепь наследования: Object (все классы неявно наследуются от типа Object) -> Animal -> Cat|Dog.
Давайте рассмотрим картинку ниже и попробуем вспомнить о преобразовании типов.
Суперклассы обычно размещаются выше подклассов, поэтому на вершине наследования находится класс Object, а в самом низу Cat и Dog.
Объект подкласса также представляет объект суперкласса. Поэтому в программе мы можем написать следующим образом:
public class Main {
public static void main(String[] args) {
Object animal = new Animal("Cat", "Black", 3);
Object cat = new Cat("Murka", "Black", 4, "muuuur");
Object dog = new Dog("Bobik", "White", 2, "woof");
Animal dogAnimal = new Dog("Manya", "Grey", 3, "woof");
Animal catAnimal = new Cat("Marusya", "Orange", 1, "myau");
}
}
Это так называемое восходящее преобразование (от подкласса внизу к суперклассу вверху иерархии) или upcasting. Такое преобразование осуществляется автоматически.
Обратное не всегда верно. Например, объект Animal не всегда является объектом Cat или Dog. Поэтому нисходящее преобразование или downcasting от суперкласса к подклассу автоматически не выполняется. В этом случае нам надо использовать операцию преобразования типов.
Object animal = new Cat("Murka", "Black", 4, "muuuur");
Cat cat = (Cat)animal;
cat.voiceCat();
В данном случае переменная animal приводится к типу Cat. И затем через объект cat мы можем обратиться к функционалу объекта Cat.
Еще один момент из этой темы - это оператор instanceof.
Нередко данные приходят извне, и мы можем точно не знать, какой именно объект эти данные представляют. Соответственно возникает большая вероятность столкнуться с ошибкой. И перед тем, как провести преобразование типов, мы можем проверить, а можем ли мы выполнить приведение с помощью оператора instanceof:
Object cat = new Cat("Murka", "Black", 4, "muuuur");
if(cat instanceof Dog){
Dog dogIsCat = (Dog) cat;
dogIsCat.jump();
}
else{
System.out.println("Conversion is invalid");
}
Выражение cat instanceof Dog проверяет, является ли переменная cat объектом типа Dog. Но так как в данном случае явно не является, то такая проверка вернет значение false, и преобразование не сработает.
Вывод консоли:
Conversion is invalid
А вот выражение cat instanceof Animal выдало бы true и вывело бы Murka jumps:
Object cat = new Cat("Murka", "Black", 4, "muuuur");
if(cat instanceof Animal){
Animal catAnimal = (Animal) cat;
catAnimal.jump();
}
else{
System.out.println("Conversion is invalid");
}
Как хорошо, когда у нас есть возможность наследоваться от класса, но что, если нам стало нужно, чтобы от нашего класса никто больше не смог унаследоваться?
В java есть ключевое слово final. Оно может применяться к классам, методам, переменным (в том числе аргументам методов).
Нам на данный момент интересен момент с классом, как можно запретить наследование.
Если мы взглянем на класс String, то увидим, что он объявлен как final. И унаследоваться от него нельзя:
public final class String{
//...
}
class SubString extends String{ //Ошибка компиляции
}
То есть, если мы логически понимаем, что у нас от котиков уже нельзя наследоваться, то мы их можем пометить этим ключевым словом:
public final class Cat extends Animal {
private String voice;
public Cat(String name, String color, int age, String voice){
super(name, color, age);
this.voice = voice;
}
void voiceCat() {
System.out.println(getName() + " " + voice);
}
}
И тогда, если мы в коде начнём писать класс и укажем, что наследуемся от кота, то у нас выведется Cannot inherit from final 'org.example.Cat.
Хорошо, мы разобрались с наследованием в целом, с поведением конструкторов, с преобразованием типов и т.д. Осталось разобрать момент, который называется абстракцией.
В ООП это означает, что при проектировании классов и создании объектов необходимо выделять только главные свойства сущности, и отбрасывать второстепенные.
Так вот, абстрактный класс — это максимально абстрактная, о-о-о-чень приблизительная «заготовка» для группы будущих классов. Эту заготовку нельзя использовать в готовом виде — слишком «сырая». Но она описывает некое общее состояние и поведение, которым будут обладать будущие классы — наследники абстрактного класса.
Рассмотрим пример абстрактного класса энимэл:
public abstract class Animal {
private String name;
private String color;
private int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public Animal() {
}
public abstract void jump() ;
public abstract void voice();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Таким образом выглядит абстрактный класс энимал. Что в нём такого особенного? Прежде всего, он максимально абстрактно описывает нужную нам сущность — животное. Слово abstract находится здесь недаром. В мире не существует «просто животных». Есть губки, иглокожие, хордовые и т.д.
Данный абстрактный класс просто является чертежом по которому будут создаваться дальнейшие классы животных. Допишем для пример класс Dog:
public class Dog extends Animal{
private String voice;
public Dog(String name, String color, int age, String voice){
super(name, color, age);
this.voice = voice;
}
@Override
public void jump() {
if (getAge() < 5) System.out.println(getName() + " jumps");
}
@Override
public void voice() {
System.out.println(getName() + " " + voice);
}
}
Использование:
Animal dogAnimal = new Dog("Manya", "Grey", 3, "woof");
dogAnimal.jump();
dogAnimal.voice();
Вывод:
Manya jumps
Manya woof
Это во многом похоже на то, о чем мы говорили в наследовании. Только там у нас класс Animal и его методы не были абстрактными. Но у такого решения есть целый ряд минусов, которые в абстрактных классах исправлены.
Первое и главное — экземпляр абстрактного класса создать нельзя.
Тот же пример можно привести с животными. Представьте, если бы в программе появились объекты Animal — «просто животное». Какого оно вида, к какому семейству относится, какие у него характеристики — непонятно. Было бы странно увидеть его в программе. Никаких «просто животных» в природе не существует. Только собаки, кошки, лисы, кроты и другие.
\end{longtable}