Статические методы
Выбери формат для чтения
Загружаем конспект в формате docx
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Практическая работа 10 (по материалам Лекции)
Принципы ООП
Задание:
1) Выполнить и отладить коды, приведенные на страницах Лекции.
2) Реэудльтаты обосновать.
3) Выполнить задания для самопроверки стр.7, 9, 15
4) Выполнение отчета по практической работе
Статические методы
Статическим методом называется фрагмент программы, которому присвоено некоторое уникальное имя, и который по этому имени можно вызывать из остальных частей программы. В момент, когда происходит вызов, выполняются действия, перечисленные внутри метода (в его описании или теле).
В объектно-ориентированном программировании основная задача методов заключается в том, чтобы изменять текущее состояние объекта, но до тех пор, когда в программе объекты ещё не используются, методы уже могут вводиться. Метод, который описан внутри некоторого класса, но вызывается без приложения к конкретному объекту этого класса, называется статическим.
Кроме имени и описания, о которых сказано выше, у метода есть ряд других характеристик:
1. Набор модификаторов.
2. Тип возвращаемого значения.
3. Набор аргументов (параметров).
Модификаторы метода
Для того чтобы создать статический метод, перед его именем надо указать модификатор static. Если этого не сделать, то метод можно будет вызывать только в приложении к конкретному объекту данного класса (будет нестатическим).
Модификатор public отвечает за уровень доступа к описываемому методу. Вместо public могут указываться уровни доступа private или protect, а также может не указываться ничего, тогда будет действовать уровень доступа по умолчанию.
С уровнями доступа мы познакомимся подробнее, когда будем создавать свои классы, а пока отметим, что доступ по умолчанию разрешает обращаться к методу из любой части того пакета, в котором метод описан. А уровень public открывает доступ к методу откуда угодно. В том числе и из других пакетов и программ.
Метод main обязан иметь уровень доступа public как раз потому, что к нему обращается виртуальная машина Java, не являющаяся частью какого-либо пакета.
Кроме этого, существуют другие модификаторы, которые, например, позволяют регулировать работу методов в процессе параллельных (многопоточных) вычислений.
Тип возвращаемого значения
Методы в Java условно можно разделить на 2 группы: функции и процедуры. К первой группе относятся методы, очень похожие на функции в математическом смысле. В результате своей работы такие методы возвращают в то место программы, из которого они были вызваны, некоторый конкретный результат существующего типа, то есть это может быть целое или вещественное число или логическое значение (int, double, boolean), массив (ссылка на него), объект (ссылка на него). Возвращаемое значение должно присваиваться переменной подходящего типа или же передаваться какому-либо другому методу в роли аргумента.
В отличие от функций, методы процедурного типа производят какие-либо полезные действия, но не дают законченного результата, который мог бы выражаться в одном конкретном значении или объекте.
Примеры:
double r = Math.random();
/* random относится к функциям */
System.out.println(r);
/* println относится к процедурам */
Если бы мы создали метод, который так же, как и println, печатал бы текст на экран, но при этом подсчитывал бы количество пробелов в тексте, и возвращал бы этот результат, то получили бы функцию. При этом функция продолжала бы выполнять полезные действия, характерные для процедуры println. Соответственно, функция более универсальна, чем процедура, но не всегда необходима.
При создании метода в первую очередь надо определить, будет ли он функцией или процедурой. Для промежуточных вычислений, как правило, используются функции. Для сокращения однотипных фрагментов кода могут подходить и процедуры.
После модификаторов, но также слева от имени метода, указывается тип возвращаемого им значения (если метод является функцией, например: int[] или double) или же слово void (если метод является процедурой).
Если метод является функцией, то в нём обязательно должна встречаться команда return после которой через пробел указывается то выражение, значение которого должно быть возвращено в качестве результата работы метода.
Все команды, указанные в описании метода после return, выполняться уже не будут, return без аргумента можно использовать внутри процедур. Он будет просто досрочно завершать процедуру (аналог break для цикла).
Аргументы (параметры)
При вызове метода в него из основной программы может передаваться набор некоторых значений. Для того чтобы научить метод их принимать (и внутри метода их обрабатывать), в круглых скобках после имени метода должны быть перечислены пары вида: тип_аргумента имя_аргумента через запятую.
Тогда при вызове метода можно будет указать набор значений, соответствующих по типам, описанным аргументам.
Значение, которые передаются методу в момент вызова, называются фактическими параметрами, а имена аргументов, которые фигурируют в описании метода — формальными параметрами.
Каждый формальный параметр является внутри метода локальной переменной, то есть он недоступен за пределами метода (вне блока его описания). В момент вызова метода фактическое значение копируется в формальный параметр.
В частности, это означает, что, передавая какую-либо переменную базового типа как параметр методу при его вызове, мы не сможем изменить значение этой переменной в основной программе. Если в метод через аргумент передаётся какого-либо объекта или массива, то внутрь метода копируется только ссылка на объект или массив (т. е. их адрес в памяти). Действия, которые мы совершим с массивом или объектом внутри метода, отразятся на состоянии этого массива или объекта в основной программе даже после того, как метод завершит свою работу. Внутри метода мы обращались по тому же адресу и работали с теми же данными в памяти, что доступны в основной программе.
Если имя фактического параметра совпадает с именем формального параметра, то это не влечёт никакой проблемы: внутри метода имеется локальная переменная, в которую при вызове копируется значение одноимённой глобальной переменной. Обращаясь по этому имени внутри метода, будем попадать на локальную переменную и никак не сможем добраться до глобальной.
Описание метода
Метод должен описываться внутри класса, но при этом один метод не описывают внутри другого, то есть метод должен вкладываться непосредственно в блок класса.
Общая схема описания метода:
модификаторы тип_возвращаемого_значения имя_метода (формальные аргументы) {
// действия, выполняемые методом
// возможно, return
}
Имя метода по традиции должно начинаться с маленькой буквы. Если оно состоит из нескольких слов, каждое следующее слово начинают с заглавной буквы. Имя для метода выбирают так, чтобы было понятно, что он делает.
Рассмотрим несколько примеров:
Пример 1.
public static double kvadk (double) {
double t;
t = Math.pow(a, 0.5);
return t;
}
Теперь внутри метода main мы сможем использовать наш метод. Например, так:
int a = 25;
System.out.println(kvadk(a));
// 5.0
System.out.println(a)
// 25
При передаче фактических параметров в метод действует автоприведение. Если аргумент фактический не соответствует типу формального, то Java пробует привести фактический аргумент к более универсальному типу (в данном случае int был приведён к double).
Перегрузка методов
Сигнатурой метода называется совокупность его имени и набора формальных параметров.
Java позволяет создавать несколько методов с одинаковыми именами, но разными сигнатурами. Создание метода с тем же именем, но с другим набором параметров называется перегрузкой. Какой из перегруженных методов должен выполняться при вызове, Java определяет на основе фактических параметров.
void pr( double a) {
System.out.println(a);
}
void pr (String a) {
System.out.println(a);
}
void pr(int[] a) {
for (int i=0; i 0) {
if(isPrime(u)) {
System.out.println("Вы ввели простое число");
} else {
System.out.print("Простые делители числа: ");
for(int i = (int)Math.sqrt(u); i >= 2 ; i--) {
if(u%i == 0 && isPrime(i)) {
System.out.print(i+" ");
}
}
System.out.println();
}
} else {
System.out.println("Вы ввели не положительное число");
}
} else {
System.out.println("Вы ввели не целое число");
}
}
}
В следующем примере за счёт перегрузки будет создано несколько одноимённых методов.
Первый вариант метода будет просто переводить строку, т. е. фактически являться боле коротким синонимом встроенного метода System.out.println(). Параметров у этого варианта не будет.
Второй вариант метода (его первая перегрузка), проверяет, есть ли у числового аргумента дробная часть, если её нет, то аргумент приводится к целым и выводится на экран без нулевой дробной части (3 вместо 3.0). В этот метод смогут в качестве единственного аргумента передаваться не только переменные типа double, но и переменные любого другого типа, для которого возможно автоприведение к double (например, любые целочисленные переменные).
Третий метод с одним параметром просто вызывает четвёртый метод, передавая в качестве параметров ему полученный массив, а также пробел в качестве второго параметра. Обратите внимание, что мы вызываем метод, который будет описан далее по ходу программу, это вполне допустимо.
Четвёртый метод выводит числовой массив, обрабатывая каждый элемент уже существующим методом. После каждого выведенного элемента добавляется переданный в параметре разделитель.
public class Main {
public static void pr() {
System.out.println();
}
public static void pr(double d) {
if((int)d == d) {
System.out.print((int)d);
} else {
System.out.print(d);
}
}
public static void pr(double[] m) {
pr(m, " ");
}
public static void pr(double[] m, String s) {
for(int i = 0; i < m.length; i++) {
pr(m[i]);
System.out.print(s);
}
}
public static void main(String[] args) {
double[] arrn = {1, 2.71, 3.14, 15, -5, 92, 0.5};
double p = 3.0;
int k = 13;
pr(p); // вывод числа, без дробной части при возможности
pr(); // переводит строку
pr(arrn); // вывод числового массива в строку
pr(); // переводит строку
pr(arrn,", "); // вывод числового массива в строку через запятую
pr(); // переводит строку
pr(k); // вывод целого числа через автоприведение
}
}
В результате работы программы на экран будет выведено:
3
1 2.71 3.14 15 -5 92 0.5
1, 2.71, 3.14, 15, -5, 92, 0.5,
1
Задачи для самопроверки
1. Создать статический метод, который будет иметь два целочисленных параметра a и b, и в качестве своего значения возвращать случайное целое число из отрезка [a;b]. C помощью данного метода заполнить массив из 20 целых чисел и вывести его на экран.
2. Создать метод, который будет выводить указанный массив на экран в строку. С помощью созданного метода и метода из предыдущей задачи заполнить 5 массивов из 10 элементов каждый случайными числами и вывести все 5 массивов на экран, каждый на отдельной строке.
3. Создать метод, который будет сортировать указанный массив по возрастанию любым известным вам способом.
4. В массиве хранится 7 явно заданных текстовых строк. Создать программу, которая отсортирует и выведет на экран строки в алфавитном порядке. Например, если были даны такие строки:
Пушкин
Лермонтов
Некрасов
Толстой Л. Н.
Толстой А. Н.
Есенин
Паустовский
Программа должна вывести на экран:
Есенин
Лермонтов
Некрасов
Паустовский
Пушкин
Толстой А. Н.
Толстой Л. Н.
Указание: прежде всего надо создать метод, устанавливающий отношения порядка для двух строк, переданных методу через аргументы.
Рекурсия
Рекурсией называется метод (функция), которая внутри своего тела вызывает сама себя.
Рассмотрим пример — вычисление факториала. Для того чтобы вычислить n!, достаточно знать и перемножить между собой (n-1)! и n.
Создадим метод, реализующий описанный способ.
static int fact (int n) {
if (n==1) {
return 1;
} else if (n==2) {
return 2;
} else {
return fact(n-1) * n;
}
}
Указанный метод вычисляет факториал натурального числа.
Рассмотрим пример, вычисляющий через рекурсию n-ое число Фибоначчи.
Напомним, как выглядят первые элементы этого ряда: 1 1 2 3 5 8 13 …
static int fib (int n) {
if (n==1 || n == 2) {
return 1;
}
return fib (n-2) + fib (n-1);
}
Обратите внимание, что в этом методе второй return не помещён в блок else для первого условного оператора. Это допустимо, ведь если выполнится условие и сработает первый return, то произойдёт выход из метода, до второго return исполнение программы дойдёт только в случае невыполнения условия.
Рекурсивные вычисления часто приводят к необходимости повторять одни и те же действия, что существенно замедляет работу программы.
Задачи для самопроверки:
1. Выясните экспериментальном путём, начиная с какого элемента последовательности Фибоначчи, вычисление с использованием рекурсии становится неприемлемым (занимает более минуты по времени).
2. Создайте гибридный метод, для небольших n вычисляющий n-ое число Фибоначчи с помощью рекурсии, а для значений, превышающих выясненное вами в предыдущей задаче пороговое n, вычисляющий n-ое число Фибоначчи с помощью итерационного алгоритма (цикла, в рамках которого будут сохраняться значения двух предыдущих элементов последовательности).
3. Подсчитайте, сколько раз потребуется повторно вычислить четвёртый элементы последовательности Фибоначчи для вычисления пятнадцатого элемента.
Стек вызовов
В общем случае в текущий момент времени может исполняться только один единственный метод из всей программы. Это значит, что, если метод а устроен таким образом, что в своём теле он вызывает метод b, а сам а вызывается в main, то при запуске программы управление сначала будет передано методу main, затем методу а, затем методу b. Метод b вернёт результат и управление в а, а вернет результат управления в main, и только потом будут выполняться основные команды, указанные в методе main на остальных строках после вызова a.
Вся иерархия (кто кого вызывал) хранится в специальной области памяти, называемой стеком вызовов. Элементы в этот фрагмент памяти добавляются по следующему принципу: последний добавленный элемент должен быть извлечён первым. Когда работает метод b, получается, что под ним в стеке оказываются метод a и метод main.
В связи с этим в процессе рекурсии существует опасность переполнения стека вызовов.
Существует так называемая сложная рекурсия, при которой метод а вызывает метод b, b вызывает с, а с вызывает а.
Создание собственных классов в Java
Полями создаваемого класса могут быть не только переменные встроенных типов, но и объекты любых других доступных классов.
Соответственно, удобно использовать ранее созданные классы в новых. При этом задействовав методы существующих классов можно значительно упростить создание нового класса.
Рассмотрим пример, в котором класс окружностей создаётся с использованием класса точек (одним из полей класса окружностей является объект-точка):
import java.util.Scanner;
class Point {
public double x; // абсцисса точки
public double y; // ордината точки
// возвращает строку с описанием точки
public String toString() {
return "("+x+";"+y+")";
}
// выводит на экран описание точки
public void print() {
System.out.print(this.toString());
}
// метод перемещает точку на указанный вектор
public void move(double a, double b) {
x = x + a;
y = y + b;
}
// метод изменяет координаты точки на указанные
public void set(double a, double b) {
x = a;
y = b;
}
// конструктор по умолчанию, создающий точку с указанными пользователем координатами
public Point() {
boolean err;
do {
err = false;
System.out.print("Введите абсциссу точки: ");
Scanner scan = new Scanner(System.in);
if(scan.hasNextDouble()) {
x = scan.nextDouble();
} else {
System.out.println("Вы ввели не число, попробуйте снова");
err = true;
}
} while (err);
do {
err = false;
Scanner scan = new Scanner(System.in);
System.out.print("Введите ординату точки: ");
if(scan.hasNextDouble()) {
y = scan.nextDouble();
} else {
System.out.println("Вы ввели не число, попробуйте снова");
err = true;
}
} while (err);
}
// конструктор, создающий точку с указанными координатами
public Point(double a, double b) {
x = a;
y = b;
}
// метод вычисляющий расстояние между точками
public double length(Point p) {
return Math.sqrt( Math.pow(p.x-x,2) + Math.pow(p.y-y,2) );
}
// метод проверяющий совпадают ли точки
public boolean equalsPoint(Point p) {
if(this.x == p.x && this.y == p.y) {
return true;
} else {
return false;
}
}
}
class Circle {
public double r; // радиус
public Point c; // центр
// возвращает строку с описанием окружности
public String toString() {
return "Окружность с центром в точке " + c + " и радиусом " + r;
}
// выводит на экран описание окружности
public void print() {
System.out.print(this.toString());
}
// метод перемещает центр окружности на указанный вектор
public void move(double a, double b) {
c.move(a, b);
}
// метод изменяет окружность, перемещая центр в указанные координаты и меняя радиус
public void set(double a, double b, double m) {
c.set(a, b);
r = m;
}
// метод изменяет окружность, перемещая центр в указанную точку и меняя радиус
public void set(Point p, double m) {
c.set(p.x, p.y);
r = m;
}
// конструктор по умолчанию, создающий окружность с указанными пользователем параметрами
Circle () {
System.out.println("Задайте центр окружности:");
c = new Point();
boolean err;
do {
err = false;
Scanner scan = new Scanner(System.in);
System.out.print("Задайте радиус: ");
if(scan.hasNextDouble()) {
r = scan.nextDouble();
if (r <= 0) {
System.out.println("Радиус окружности должен быть положительным");
err = true;
}
} else {
System.out.println("Вы ввели не число, попробуйте снова");
err = true;
}
} while (err);
}
Circle (double a, double b, double m) {
c.set(a, b);
r = m;
}
// метод вычисляющий длину окружности
public double length(Point p) {
return 2*Math.PI*r;
}
// метод проверяющий, совпадают ли две окружности
public boolean equalsCircle(Circle o) {
if(this.r == o.r && c.equalsPoint(o.c)) {
return true;
} else {
return false;
}
}
}
public class Main {
public static void main(String[] args) {
Circle o1 = new Circle();
o1.print();
}
}
Обратите внимание на то, что в методах класса окружностей используются методы класса точек, что значительно упрощает построение второго класса.
Полиморфизм
Примечательно также и то, что в классах у нас есть методы с одинаковыми именами. Например, методы print(), set(...), length().
Метод set имеет разные сигнатуры: в первом классе он получает два вещественных числовых аргумента, а во втором классе окружностей существует две реализации этого метода, у первой три вещественных числовых аргумента, а второй аргумента два, но первый это объект-точка, а второй — вещественное число. Соответсвенно, в момент вызова метода set никаких сложностей с тем, чтобы определить из какого класса должен исполняться метод — не возникнет (в силу разных наборов параметров).
А вот методы print() и length() вообще не имеют аргументов. И когда мы будем вызывать их в приложении к какому-то объекту, например, так:
obj.print();
То какой из методов будет выполнен будет исключительно от того, к какому классу относится объект obj. Если к классу точек, то будут выведены координаты точки obj, если к классу окружностей, то параметры окружности obj.
Явление, когда разный программный код связан с одним и тем же именем (в данном примере с именем метода print()) называется — полиморфизмом (одно имя, но много форм).
Полиморфизм позволяет упростить использование создаваемых классов: пользователь любого из двух созданных нами классов будет знать, что для вывода описания объекта на экран надо использовать метод print(), не нужно даже задумываться какой именно это объект (т.е. к какому классу он относится).
С полиморфизмом вы уже сталкивались на примере перегрузки методов, в ООП контекстом для вызова конкретной реализации метода является уже не только набор аргументов, но и класс того объекта, к которому метод применяется.
Инкапсуляция
Представленный выше пример содержит проблемный момент, связанный с тем, что ограничения на допустимые значения полей объектов никак не будут учитываться пользователем класса окружностей (внешней частью программы, где создаются и используются объекты класса). По смыслу моделируемой задачи поле r не должно получать отрицательных значений (ведь это радиус окружности). Мы учли это, например, в конструкторе, но пользователь класса может выполнять примерно такие действие, недопустимые с точки зрения предметной области задачи:
Circle o2 = new Circle();
o2.r = -17.5;
o2.print(); // получим окружность с отрицательным радиусом
Для ограничения подобных действий, а также для сокрытия внутреннего устройства классов от пользователя (в целях безопасности и обеспечения модульнусти) существуют различные уровни доступа к членам класса (определяемые с помощью модификатора, пока нам хорошо знаком только модификатор public):
Модификаторы и зоны доступа
Тело класса
Пакет, содержащий класс
Класс-наследник (подкласс)
Вся остальная часть программы (например, другие пакеты)
public
+
+
+
+
protected
+
+
+
-
default (модификатор не пишется)
+
+
-
-
private
+
-
-
-
Из таблицы следует, что модификатор public предоставляет к полю или методу доступ из любой части программы. Это самый открытый и общедоступный вариант.
Напротив, модификатор private разрешает обращаться напрямую к члену класса только из самого класса. Это самый закрытый вариант.
Соответсвенно, изменив модификатор перед полем r класса окружностей мы можем решить описанную выше проблему:
private double r; // радиус
Теперь попытка обратиться к свойству r из за пределов класса:
o2.r = -17.5;
Будет приводить к ошибке:
Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - r has private access in main.Circle
at main.Main.main(Main.java:142)
Java Result: 1
Но что, если нам всё-таки потребуется обратиться к этому полю, чтобы изменить или прочитать его значения? Для этого можно добавить в класс методы, которые будут отвечать за изменение или чтение поля r, но при этом иметь более широкий уровень доступа (например, public):
public double getR() {
return r;
}
public void setR(double a) {
if(a > 0) {
r = a;
} else {
System.out.println("Радиус окружности должен быть положительным");
}
}
Первый метод просто возвращает в то место, откуда будет вызван, значение поля. Никаких дополнительных проверок при чтении значения поля мы устраивать по смыслу задачи не должны. Зато второй метод, перед тем как изменить значение поля, проверяет допустимо ли новое значение, если оно недопустимо, то поле не изменяется, а на экран выдаётся предупреждение.
Соответсвенно, теперь можно выполнять такой код (ошибки в программе не будет):
Circle o2 = new Circle();
o2.setR(-17.5); // тут будет выведено сообщение о недопустимом значении, но значение поля не изменится
o2.print(); // получим окружность с тем радиусом, что изначально был задан с клавиатуры
Методы, подобные созданным, назваются «геттеры» (от слова get, получать) и «cеттеры» (от слова set, устанавливать). Они являются обёртками для доступа к полям на чтение и запись, соответвенно.(Смотри презентацию от 07.04.2020)
Инкапсуляция позволяет запрещать и контролировать использование любых членов класса за пределами его тела.
Задачи для самопроверки
1. Создайте класс треуголников на координатной плоскости, используя в качестве полей объекты-точки. Реализуйте в классе:
a) конструктор, позволяющий задавать вершины с клавиатуры;
b) метод print() выводящий описание треугольника на экран;
c) методы для вычисления периметра и площади треугольника.
2. Доработайте конструктор таким образом, чтобы нельзя было задать три вершины, лежащие на одной прямой. Это несложно будет сделать с использованием метода из класса точек, который проверяет явлются ли точки коллинеарными, если прежде вы не реализовали этот метод, то сейчас самое время сделать это.
3. Инкапсулируйте поля таким образом, чтобы нельзя изменить значение любого из них так, чтобы вершины оказались на одной прямой.
4. Создайте метод, поворачивающий треугольник вокруг центра тяжести на указанное в аргументе количество градусов