Статические методы в Java, перегрузка методов, рекурсия

Статические методы

Статическим методом называется фрагмент программы, которому присвоено некоторое уникальное имя, и который по этому имени можно вызывать из остальных частей программы. В момент, когда происходит вызов, выполняются действия, перечисленные внутри метода (в его описании или теле).
В объектно-ориентированном программировании основная задача методов заключается в том, чтобы изменять текущее состояние объекта, но до тех пор, когда в программе объекты ещё не используются, методы уже могут вводиться. Метод, который описан внутри некоторого класса, но вызывается без приложения к конкретному объекту этого класса, называется статическим.

Кроме имени и описания, о которых сказано выше, у метода есть ряд других характеристик:

  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<a.length; i++) {
    System.out.print(a[i]+" ")
  }
  System.out.println();
}

Пример использования метода.

int a = 5;
int [] m = {1, 2, 8, 3}
String s = "Мир";
pr (a) //работает исходный метод
pr (a+s); // 5 мир, работает первая перегрузка
pr (m); // 1 2 8 3
pr (m+a); // ошибка

Переменная а не относится к типу double, но её обрабатывает исходный метод, поскольку возможно автоприведение из int в double. В обратном направлении оно невозможно. Если бы метод имел аргумент типа int, то с его помощью вещественные числа выводить не получилось бы.

Перегрузка методов реализует такое важное свойство в программировании, как полиморфизм. Полиморфным называется программный код, который связан с одним общим именем, но имеет разные реализации. Какая из реализаций будет работать, выбирается на основе контекста, в котором имя было упомянуто. Конкретно для методов полиморфными являются их перегрузки, а выбор исполняемой перегрузки происходит по параметрам.

Полиморфизм: одно имя, много форм.

Примеры использования методов

Следующая программа ищет и выводит на экран все простые нетривиальные делители числа, введённого пользователем с клавиатуры, начиная с наибольшего из делителей, либо сообщает, что введённое число — является простым.

import java.util.Scanner;
public class Main {
     public static boolean isPrime(int n) {
        for(int i = 2; i <= Math.sqrt(n) ; i++) {
            if(n%i == 0) {
                return false;
            }
        }
        return true;
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Введите натуральное число: ");
        if(sc.hasNextInt()) {
            int u = sc.nextInt();
            if(u > 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 вызывает с, а с вызывает а.