Привет! Ты уже освоил создание собственных классов, с полями и методами. Сегодня мы как раз подробно поговорим про методы.
Мы, конечно, уже не раз делали в наших лекциях, но мы говорили в основном об общих моментах. Сегодня же мы буквально разберем методы “по частям” — узнаем из чего они состоят, какие варианты их создания существуют и как всем этим можно управлять:) Поехали!

Сигнатура метода

Весь код, который описывает метод, называется сигнатурой метода. Общий вид сигнатуры можно описать так:
модификатор доступа, тип возвращаемого значения, имя метода(список параметров) {
    // тело метода
}
Рассмотрим для примера сигнатуры нескольких методов класса Dog.
public class Dog {

   String name;

   public Dog(String name) {
       this.name = name;
   }

   public static void main(String[] args) {
       Dog max = new Dog("Макс");
       max.woof();

   }

   public void woof() {
       System.out.println("Собака по имени " + name + " говорит \"Гав-гав!\"");
   }

   public void run(int meters) {
       System.out.println("Собака по имени " + name + " пробежала " + meters + " метров!");
   }

   public String getName() {
       return name;
   }
}

1. Модификатор доступа

Модификатор доступа всегда указывается первым. Все методы класса Dog обозначены модификатором public. То есть мы можем вызвать их из любого другого класса:
public class Main {

   public static void main(String[] args) {

       Dog butch = new Dog("Бутч");
       butch.run(100);
   }

}
Методы класса Dog, как видишь, легко доступны в классе Main. Это возможно именно благодаря модификатору public. В Java есть и другие модификаторы, и не все из них позволят использовать метод внутри другого класса. О них мы поговорим в других лекциях. Главное, запомни за что отвечает модификатор: за доступность/недоступность метода в других классах:)

2. Ключевое слово static

Один из методов Dog, а именно main() обозначен ключевым словом static. Оно тоже входит в сигнатуру метода, а его значение мы уже знаем. Мы не указали его в схеме сигнатуры в начале лекции, поскольку оно присутствует там далеко не всегда. Если же оно есть — то указать его необходимо после модификатора доступа. Помнишь, в прошлых лекциях мы говорили о статических переменных класса? Применительно к методам это слово имеет примерно тот же смысл. Если метод указан как static — это означает, что он может использоваться без ссылки на конкретный объект класса. И действительно — чтобы запустить статический метод main() в классе Dog тебе не нужно создавать экземпляр Dog, он запускается и без этого. Если бы этот метод не был статическим — то для его использования нам понадобилось бы сперва создать объект.

3. Возвращаемое значение.

Если наш метод должен что-то вернуть, то далее мы указываем тип возвращаемого значения. Это видно на примере геттера getName():
public String getName() {
   return name;
}
Он возвращает объект типа String. Если же метод ничего не возвращает — вместо типа указывается ключевое слово void, как в методе woof():
public void woof() {
   System.out.println("Собака по имени " + name + " говорит \"Гав-гав!\"");
}

Методы с одинаковыми именами

Бывают ситуации, когда в нашей программе нужно несколько вариантов работы метода. Почему бы нам не создать свой собственный искусственный интеллект? У Amazon есть Alexa, у Яндекса — Алиса, так чем мы хуже?:) В фильме про Железного Человека Тони Старк создал собственный выдающийся искусственный интеллект — J.A.R.V.I.S. Отдадим должное прекрасному персонажу и назовем наш ИИ в его честь:) Первое, чему мы должны научить Джарвиса — здороваться с людьми, которые заходят в комнату (будет странно, если столь великий интеллект окажется невежливым).
public class Jarvis {

   public void sayHi(String name) {
       System.out.println("Добрый вечер, " + name + ", как ваши дела?");
   }

   public static void main(String[] args) {
       Jarvis jarvis = new Jarvis();
       jarvis.sayHi("Тони Старк");
   }
}
Вывод в консоль: Добрый вечер, Тони Старк, как ваши дела? Отлично! Джарвис умеет приветствовать вошедшего. Чаще всего, конечно, это будет его хозяин — Тони Старк. Но ведь он может прийти не один! А наш метод sayHi() принимает на вход только один аргумент. И, соответственно, сможет поприветствовать только одного из пришедших, а другого проигнорирует. Не очень-то вежливо, согласен?:/ В этом случае чтобы решить проблему мы можем просто написать в классе 2 метода с одинаковым названием, но с разными параметрами:
public class Jarvis {

   public void sayHi(String firstGuest) {
       System.out.println("Добрый вечер, " + firstGuest + ", как ваши дела?");
   }

   public void sayHi(String firstGuest, String secondGuest) {
       System.out.println("Добрый вечер, " + firstGuest + ", " + secondGuest + ", как ваши дела?");
   }

}
Это называется перегрузкой методов. Перегрузка позволяет нашей программе быть более гибкой и учитывать различные варианты работы. Проверим как это работает:
public class Jarvis {

   public void sayHi(String firstGuest) {
       System.out.println("Добрый вечер, " + firstGuest + ", как ваши дела?");
   }

   public void sayHi(String firstGuest, String secondGuest) {
       System.out.println("Добрый вечер, " + firstGuest + ", " + secondGuest + ", как ваши дела?");
   }

   public static void main(String[] args) {
       Jarvis jarvis = new Jarvis();
       jarvis.sayHi("Тони Старк");
       jarvis.sayHi("Тони Старк", "Капитан Америка");
   }
}
Вывод в консоль: Добрый вечер, Тони Старк, как ваши дела? Добрый вечер, Тони Старк, Капитан Америка, как ваши дела? Отлично, оба варианта сработали:) Тем не менее, проблему мы не решили! Что, если гостей будет трое? Конечно, мы можем еще раз перегрузить метод sayHi(), чтобы он принимал имена трех гостей. Но их ведь может быть и 4, и 5. И так до бесконечности. Нет ли другого способа научить Джарвиса работать с любым количеством имен, без миллиона перегрузок метода sayHi()? :/ Конечно, есть! Иначе была бы разве Java самым популярным в мире языком программирования? ;)
public class Jarvis {

   public void sayHi(String...names) {

       for (String name: names) {
           System.out.println("Добрый вечер, " + name + ", как ваши дела?");
       }
   }

   public static void main(String[] args) {
       Jarvis jarvis = new Jarvis();
       jarvis.sayHi("Тони Старк");
       System.out.println();
       jarvis.sayHi("Тони Старк", "Капитан Америка");
   }
}
Запись (String...names) переданная в качестве параметра позволяет нам указать, что в метод передается какое-то количество строк. Мы не оговариваем заранее сколько их должно быть, поэтому работа нашего метода становится теперь намного более гибкой:
public class Jarvis {

   public void sayHi(String...names) {

       for (String name: names) {
           System.out.println("Добрый вечер, " + name + ", как ваши дела?");
       }
   }

   public static void main(String[] args) {
       Jarvis jarvis = new Jarvis();
       jarvis.sayHi("Тони Старк", "Капитан Америка", "Черная Вдова", "Халк");
   }
}
Вывод в консоль: Добрый вечер, Тони Старк, как ваши дела? Добрый вечер, Капитан Америка, как ваши дела? Добрый вечер, Черная Вдова, как ваши дела? Добрый вечер, Халк, как ваши дела? Внутри метода мы в числе перебираем все аргументы и выводим готовые фразы с именами на консоль. Здесь мы применяем упрощенный цикл for-each (ты уже с ним сталкивался). Он отлично подходит, потому что запись String...names — на самом деле означает, что все переданные параметры помещаются компилятором в массив. Поэтому с переменной names можно работать как с массивом, в том числе — перебирать в цикле. При этом он сработает при любом количестве переданных строк! Две, десять, хоть тысяча — метод будет стабильно работать с любым количеством гостей. Намного удобнее, чем делать перегрузки для всех возможных вариантов, согласен?:) Приведем еще один пример перегрузки метода. Добавим Джарвису метод printInfoFromDatabase(). Он будет печатать в консоль информацию о человеке из базы данных. Если в базе данных указано, что человек является супергероем или суперзлодеем — эта информация также будет выведена на экран:
public class Jarvis {

   public  void printInfoFromDatabase (String bio) {

       System.out.println(bio);
   }

   public void printInfoFromDatabase(String bio, boolean isEvil, String nickname) {

       System.out.println(bio);
       if (!isEvil) {
           System.out.println("Также известен как супергерой " + nickname);
       } else {
           System.out.println("Также известен как суперзлодей " + nickname);
       }
   }

   public static void main(String[] args) {
       Jarvis jarvis = new Jarvis();
       jarvis.printInfoFromDatabase("Лора Палмер. Дата рождения - 22 июля 1972, город Твин Пикс, штат Вашингтон");
       System.out.println();
       jarvis.printInfoFromDatabase("Макс Эйзенхарт. Рост 188см, вес 86 кг.", true, "Магнето");
   }
}
Вывод: Лора Палмер. Дата рождения - 22 июля 1972, город Твин Пикс, штат Вашингтон Макс Эйзенхарт. Рост 188см, вес 86 кг Также известен как суперзлодей Магнето Вот так, наш метод работает в зависимости от данных которые мы в него передаем. Еще один важный момент: порядок следования аргументов имеет значение! Допустим, наш метод принимает на вход строку и число:
ublic class Man {

   public static void sayYourAge(String greeting, int age) {
       System.out.println(greeting + " " + age);
   }

   public static void main(String[] args) {

       sayYourAge("Мой возраст - ", 33);
       sayYourAge(33, "Мой возраст - "); //ошибка!
   }
}
Если метод sayYourAge() класса Man принимает на вход строку и число — значит именно в таком порядке их нужно передавать в программе! Если мы передадим их в другом порядке — компилятор выдаст ошибку и человек не сможет назвать свой возраст. Кстати, конструкторы, которые мы проходили в прошлой лекции, тоже являются методами! Их тоже можно перегружать (создавать несколько конструкторов с разным набором аргументов) и для них тоже принципиально важен порядок передачи аргументов. Настоящие методы!:)

Как вызывать методы с похожими параметрами

Как ты знаешь, в Java есть такое слово как null. При работе с ним очень важно понимать, что null не является ни объектом, ни типом данных. Представь себе, что у нас есть класс Man и метод introduce(), который объявляет имя человека и его возраст. При этом возраст можно передать в форме текста, а можно - числом.
public class Man {

   public void introduce(String name, String age) {
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public void introduce(String name, Integer age) {
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public static void main(String[] args) {

       Man sasha = new Man();
       sasha.introduce("Саша", "двадцать один");

       Man masha = new Man();
       masha.introduce("Мария", 32);
   }
}
С перегрузкой мы уже знакомы, поэтому знаем, что метод оба раза отработает как надо: Меня зовут Саша, мой возраст - двадцать один Меня зовут Мария, мой возраст - 32 Но что будет, если в качестве второго параметра мы передаем не строку и не число, а null?
public static void main(String[] args) {

   Man victor = new Man();
   victor.introduce("Виктор", null);//Ambiguous method call!
}
Мы получим ошибку компиляции! Ошибка “Ambiguous method call” переводится как “двусмысленный вызов метода”. Из-за чего она могла возникнуть и в чем заключается “двусмысленность”? На самом деле все просто. Дело в том, у нас есть два варианта метода: со String и с Integer в качестве второго аргумента. Но и String, и Integer могу быть null! Для обоих типов (поскольку они ссылочные) null является значением по умолчанию. Именно поэтому компилятор в данной ситуации не может разобраться, какой вариант метода он должен вызвать. Решить эту проблему достаточно просто. Дело в том, что null можно явно преобразовать к конкретному ссылочному типу. Поэтому при вызове метода ты можешь указать в скобках нужный тебе тип данных для второго аргумента! Компилятор поймет твой “намек” и вызовет нужный метод:
public class Man {

   public void introduce(String name, String age) {
       System.out.println("Метод с двумя строками!");
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public void introduce(String name, Integer age) {
       System.out.println("Метод со строкой и числом!");
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public static void main(String[] args) {

       Man victor = new Man();
       victor.introduce("Виктор", (String) null);
   }
}
Вывод: Метод с двумя строками! Меня зовут Виктор, мой возраст - null А вот если бы числовой параметр был примитивом int, а не объектом ссылочного типа Integer — такой ошибки не возникло бы.
public class Man {

   public void introduce(String name, String age) {
       System.out.println("Метод с двумя строками!");
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public void introduce(String name, int age) {
       System.out.println("Метод со строкой и числом!!");
       System.out.println("Меня зовут " + name + ", мой возраст - " + age);
   }

   public static void main(String[] args) {

       Man victor = new Man();
       victor.introduce("Виктор", null);
   }
}
Догадался почему? Если догадался — молодец:) Потому что примитивы не могу быть равны null. Теперь у компилятора остался только один вариант вызова метода introduce() — с двумя строками. Именно этот вариант метода и будет отрабатывать каждый раз при вызове метода.