До сих пор мы использовали только
существующие методы классов. Например, для вывода информации в консоль мы часто
применяли методы:
System.out.println() и System.out.print()
объекта out. Вместе с тем
существующих методов языка Java конечно же недостаточно и возникает
необходимость создания своих собственных. В связи с этим нужно понимать, в каких
случаях целесообразно их
создавать.
Часто это делается, чтобы избавить себя много раз писать
один и тот же текст в программе.
Например, в программах
при работе со строками
часто требуется
сравнивать
одну строку с другой, значит, такую операцию
лучше определить в виде метода и использовать его по мере необходимости. Вот почему
такой метод уже существует и называется equal(). Но на все случаи
жизни методы не придумаешь, да это и не нужно, поэтому в программе можно
объявить свой, используя
следующий синтаксис:
[модификаторы] <тип>
<имя метода> ([аргументы]) { <тело метода>
}
Модификаторы –
это такие определения как
public или static
именно такие
модификаторы записаны перед объявлением метода main, существующего
в каждой программе:
public
static void main(String[] args) {…}
Далее, тип метода – это
тот тип данных, который будет он возвращать. Затем, имя метода служит для его
вызова в программе (оно
придумывается программистом подобно именам переменных). Список аргументов необходим для
передачи методу каких-либо данных при его вызове. Тело метода – это набор
операторов, которые выполняются при его вызове.
Рассмотрим
пример создания метода, который вычисляет периметр прямоугольника. Запишем его
внутри класса Main, чтобы мы могли
его вызывать в методе main. Наш метод будет выглядеть так:
float perimetr(float a, float b) {
float res = 2 * (a + b);
return res;
}
Здесь в нем
указаны два вещественных аргумента – это ширина и длина прямоугольника,
возвращаемый тип также имеет вещественный тип float. Причем, перед
каждым аргументом обязательно прописывать его тип. То есть, писать вот так:
float a, b
нельзя. Далее, внутри
самого метода создается временная переменная res, которой
присваивается результат вычисления периметра. А оператор return показывает, что
будет возвращать метод.
Метод main нашей программы
запишем так:
public static void main(String[] args) {
int w = 5;
float h = 3.4f;
float P1 = perimetr(w, h);
float P2 = perimetr(5, 4);
System.out.println(P1);
System.out.println(P2);
}
Смотрите, мы
здесь сначала определили две переменные и, затем, вызываем наш метод perimeter сначала с
передачей ему переменных в качестве аргументов, а затем, передаем обычные
числа. Однако интегрированная среда нам здесь говорит о некой ошибке. Дело в
том, что метод main объявлен как статический (модификатор static). Что это такое
мы поговорим позже, а сейчас заметим, что из статических методов можно вызывать
только другие статические методы. Поэтому перед определением нашего метода
пропишем модификатор static.
Теперь ошибок
никаких нет и мы можем посмотреть на результат работы нашей программы. Как это
все работает? Во-первых, метод вызывается по своему имени, за которым идут
круглые скобки и в них при необходимости указываются аргументы. Если аргументы
не нужны, то пишем просто круглые скобки. Далее, переданные аргументы
копируются в переменные a и b, метод
вычисляет периметр и возвращаемое значение присваивается переменным P1 и P2.
А теперь давайте
добавим в метод, например, такую строчку после оператора return:
Смотрите,
интегрированная среда сообщает о какой-то ошибке. Дело в том, что как только в
методе встречается оператор return, его работа заканчивается и все
что идет дальше не может быть выполнено. Именно об этом здесь и говорит
интегрированная среда. А вот если перенести эту строчку до оператора return, то все будет
нормально.
В дополнение к
рассмотренным ранее примитивным типам переменных у методов часто используется
еще один тип данных – void, означающий, что метод ничего не
возвращает. Например, мы хотим написать метод, который бы выводил значения
массива на экран. Это можно сделать так:
static void show_ar(byte[] ar) {
for (byte val: ar)
System.out.print(val+" ");
}
и, затем,
вызвать его в методе main:
byte array[] = {1, 3, 0, -2, 7, 9};
show_ar(array);
Так как здесь
использован тип void, то оператор return писать не
обязательно.
Теперь смотрите,
в методах Java можно указывать
произвольное число аргументов определенного типа. Это записывается так:
static void show_ar(int ...ar) {
for (int val: ar)
System.out.print(val+" ");
}
то есть,
указывается тип аргументов, затем, через пробел ставятся три точки и сразу
записывается имя массива, в котором будут храниться все переданные аргументы. И
вызов такой функции теперь будет выглядеть так:
show_ar(1, 3, 0, -2, 7, 9);
В результате,
выведем те же значения в консоль. Вот так можно передавать произвольное число
аргументов.
Перегрузка методов
В языке Java существует такая
вещь как перегрузка методов. Перегрузка позволяет создавать несколько методов с
одинаковыми именами, но разными типами (или числом) аргументов. Например, мы
хотим записать метод вычисления модуля числа как для целочисленных значений,
так и для вещественных. Это лучше всего сделать через перегрузку таким образом.
static int modul(int x) {
if (x < 0) x = -x;
return x;
}
static double modul(double x) {
return (x < 0) ? -x : x;
}
и вызвать их так:
int a1 = modul(-3);
int a2 = (int)modul(-3.5);
double a3 = modul(-3);
double a4 = modul(-3.5);
System.out.print(a1+"\n"+a2+"\n"+a3+"\n"+a4);
Смотрите, здесь
записаны два метода modul с разными типами аргумента x. Затем, в main идет их вызов.
Как вы думаете, как компилятор определяет вызов каких из двух методов
подставить в эти строчки? Это делается по типу входных аргументов. То есть,
если вот здесь записано -3, значит, будет вызван метод modul с целочисленным
аргументом. Если же стоит вещественное число -3.5, то вызовется метод с
вещественным типом аргумента. При этом вот эти типы не играют никакой роли в
выборе методов. Имеют значение только входные типы данных.
Вы также можете
задаться вопросом: зачем вообще делать перегрузку, давайте зададим один метод с
вещественным типом и он будет работать и с целочисленными значениями? Все
верно, но работать это будет дольше, так как обработка целочисленных данных как
вещественных требует большего машинного времени. А один из критериев создания
программ звучит: программа должна работать как можно быстрее. Поэтому здесь
лучше использовать перегрузку.
Рекурсивные методы
И последнее, что
мы рассмотрим на этом занятии – это рекурсивные методы, то есть, методы,
которые вызывают сами себя. Да, да, так тоже можно делать. Давайте я сначала
приведу пример работы рекурсивного метода, а, затем, мы разберемся как это
работает.
static void up_and_down(int n) {
System.out.println("Уровень вниз " + n);
if (n < 4) up_and_down(n + 1);
System.out.println("Уровень вверх " + n);
}
и вызовем его:
Запустим эту программу,
и увидим следующее:
Уровень
вниз 1
Уровень
вниз 2
Уровень
вниз 3
Уровень
вниз 4
Уровень
вверх 4
Уровень
вверх 3
Уровень
вверх 2
Уровень
вверх 1
Почему все так
получилось? Давайте разберемся.
Стек вызова
функций
Смотрите, когда
вызывается какой-либо метод, он помещается в стек вызова методов (чтобы знать,
какие методы в каком порядке были вызваны). Сначала в этом стеке находится
метод main(), далее, из
него вызывается метод up_and_down(1). Что
происходит дальше? Этот метод выводит на экран строку «Уровень вниз 1» и
проверяет условие 1<4 – да, и идет вызов такого же метода, но уже с
аргументом n+1=2. Причем
работа первого метода up_and_down(1) и второго up_and_down(2) совершенно
независимы друг от друга! Второй метод также выводит строку «Уровень вниз 2» и
проверяет условие 2<4. Оно срабатывает и идет вызов следующего метода up_and_down(3). Здесь
происходит все то же самое. Наконец, когда мы достигаем up_and_down(4), условие 4<4
не срабатывает и дальнейшего вызова метода up_and_down не происходит и
выполняется следующая строчка метода, которая выводит строку «Уровень вверх 4».
Что будет дальше? Дальше метод up_and_down(4) завершается
и вызов передается методу up_and_down(3) в
соответствии со стеком вызова. Но этот метод уже выполнил первые две строчки,
поэтому выполняется третья, которая выводит «Уровень вверх 3». Он также
завершается, вызов переходит к методу up_and_down(2), он по
аналогии выводит третью строку «Уровень вверх 2», передает управление up_and_down(1), он выводит
«Уровень вверх 1» и на этом работа рекурсии завершается. Вот так работают
рекурсивные методы.
Кстати, обратите
внимание, если бы у нас не было вот этого условия, то рекурсия ушла бы вниз,
пока стек методов не переполнился бы (возникла бы ошибка stack overflow), поэтому при
реализации рекурсивных методов нужно внимательно относиться к глубине их вызова
– она не должна быть большой.