Это класс, который содержит в себе нереализованные методы (называются абстрактными методами), которые наследники должны реализовать.
Под ‘нереализованные’ имеется ввиду, что такие методы только объявляются в абстрактном классе, то есть у них нет тела, то есть кода внутри {}.
Абстрактные методы помечаются ключевым словом abstract.
Приведем пример абстрактного класса. Например птица. Птица это общий концепт для любой птицы, будь-то воробей или попугай.
Абстрактный класс птицы может иметь не только абстрактные методы,а и методы с реализацией.
Например.
Любая птица имеет крылья. Махание крыльями примерно ОДИНАКОВОЕу всех птиц (ну по крайней мере представим так). Соответственно можно создать в абстрактном классе не абстрактный, а обычный метод с реализацией для махания крыльями и все классы птиц, которые наследуют от абстрактного класса Птица будут по умолчанию пользоваться методом для махания крыльями из абстрактного класса.
Абстрактный же метод в абстрактном классе Птицабудет иметь смысл определить в таком случае:
Любая птица издает какие-то звуки. Но в этом уже случае почти все птицы издают РАЗНЫЕ звуки. Значит в абстрактном классе Птица должен быть абстрактный метод для издавания звуков, который наследующие от этого абстрактного класса птицы обязаны переопределить.
То есть суть в том, что если мы знаем, что у класса, который мы создаем будут наследники и у всех них будет метод, который будет выполнять одно и то же действие (например, издавание звуков у птиц), но результат этого метода у всех наследников будет разным (разные звуки), то есть смысл объявить создаваемый нами класс абстрактным и объявить этот метод абстрактным.
Из примеров абстрактных классов можно еще привести, например, Форма (форму реализует например куб и треугольник), средство передвижения (реализует машина или велосипед)
Воспринимайте абстрактный класс как недопиленный класс, который наследники должны допилить по своему посредством реализации абстрактных методов этого абстрактного класса. Недопиленный он потому что в нем всё же присутствуют реализации (пример метода махания крыльями), но в нем и хватает абстрактных методов, которые должны допилить наследники.
Поясним на примере:
abstract class Bird {
//поля и абстрактные методы
private String species;
private int age;
//Метод махания крыльями с реализацией по умолчанию.
//Махание крыльями примерно одинаковое у птиц
//(ну по крайней мере представим так)
//поэтому для всех наследников класса Bird
//пусть будет один общий метод,
//которым они будут пользоваться.
//То есть большинству наследников не придеться
//переопределять wingsFlapMechanic
public void wingsFlapFlap() {
System.out.println(“Flap Flap Flap …”);
}
//не реализованный метод, который
//обязан реализовать наследники
//поскольку все птицы издают разные звуки
//(ну почти все наверное).
public abstract void pirdVoice();
//То есть если мы точно знаем что
//у всех наследников будет
//своя реализация метода
//делаем его абстрактным.
//просто геттеры сеттеры.
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Parrot extends Bird {
//реализуем абстрактный метод
public void pirdVoice() {
System.out.println(“RRAAAR!”);
}
}
class Sparrow extends Bird {
//реализуем абстрактный метод
public void pirdVoice() {
System.out.println(“Chick-Chirik”);
}
}
public class AbstractExample {
public static void main(String[] args) {
Parrot parrot = new Parrot();
Sparrow sparrow = new Sparrow();
//Пускай теперь эти разные
//птицы помахают крыльями.
parrot.wingsFlapFlap();
sparrow.wingsFlapFlap();
//Махают одинаково как видим
//теперь пусть споют
parrot.pirdVoice();
sparrow.pirdVoice();
//как видим у них разный голос
}
}
Полиморфизм — возможность использовать одно и то же имя для разных реализаций методов или конструкторов.
В Java полиморфизм реализуется с помощью перегрузки (overloading) и переопределения (overriding).
Перегрузка позволяет в пределах одного класса создавать несколько методов (или конструкторов) с одинаковым именем, а отличаться они будут колличеством или типом параметров.
Возникает закономерный вопрос: когда мы воспользуемся одним из таких методов с одинаковым именем, как компилятор определяет, какую реализацию выбрать? Ответ прост — он делает это на основании числа и типов переданных аргументов.
Рассмотрим пример перегрузки конструкторов:
class Animal {
private String eats;
private int noOfLegs;
//Конструктор без параметров
//(называется конструктором по умолчанию).
public Animal(){}
//Конструктор с одним параметром
//задает значение только полю eats.
public Animal(String food){
this.eats = food;
}
//Конструктор с несколькими параметрами
//задает значения обоим полям.
public Animal(String food, int legs){
this.eats = food;
this.noOfLegs = legs;
}
//сеттеры и геттеры
public String getEats() {
return eats;
}
public void setEats(String eats) {
this.eats = eats;
}
public int getNoOfLegs() {
return noOfLegs;
}
public void setNoOfLegs(int noOfLegs) {
this.noOfLegs = noOfLegs;
}
}
class Polimorphism {
public static void main(String[] args) {
//Какой же из конструкторов
//Animal, которые мы определили будет использован
//при создании объекта животного?
//Ведь у них же всех одинаковые имена.
//На самом деле, можно просто выбрать
//нужный конструктор передав в него
//нужное количество параметров.
//То есть, как видим, в строке кода ниже
//вызывается конструктор с одним параметром.
Animal animal = new Animal(“Grass”);
//Значит будет использован конструктор
//public Animal(String food), ведь в конструктор
//передаётся всего один параметр.
//В выводе увидим, что строка Grass успешно
//записалась в поле объекта animal.
System.out.println(animal.getEats());
}
}
Вывод:
Даже несмотря на то, что у всех конструкторов одинаковое имя Animal, компилятор различает их по параметрам. Это и есть пример перегрузки конструктора, которая является одной из форм полиморфизма.
Однако помним, что полиморфизм может применяться не только к конструкторам, но и к методам.
Переопределение метода родителя в классе-наследнике
Под полиморфизмом также понимается переопределение метода родительского класса в классе-наследнике.
Например, ниже в классе Animal (Животное) определён метод move() (Движение), который просто сообщает, что животное может двигаться. Его реализация — вывод строки "The animal moves." — является универсальной и подходит для любого животного. Такой метод вполне может использоваться без изменений в классах-наследниках.
Однако в некоторых случаях желательно уточнить поведение метода. Например, если мы создадим класс Cat (Кот), который наследуется от Animal, то он унаследует и метод move(). Но в контексте кошки может быть полезно уточнить поведение этого метода, поскольку, разумеется, куда лучше, если move() будет описывать именно движения, характерные для кошки — например, "The cat gracefully walks and jumps." — а не просто общее "The animal moves."
В таких случаях метод move() можно переопределить в классе-наследнике, чтобы сделать поведение более конкретным и реалистичным.
Это и есть проявление полиморфизма: один и тот же метод, но разная реализация в разных классах.
Рассмотрим пример:
class Animal {
private String eats;
private int noOfLegs;
public Animal(){}
public Animal(String food){
this.eats = food;
}
public Animal(String food, int legs){
this.eats = food;
this.noOfLegs = legs;
}
public String getEats() {
return eats;
}
public void setEats(String eats) {
this.eats = eats;
}
public int getNoOfLegs() {
return noOfLegs;
}
public void setNoOfLegs(int noOfLegs) {
this.noOfLegs = noOfLegs;
}
// Метод с базовой реализацией, который,
// желательно, чтобы наследники переопределили.
public void move() {
System.out.println(“The animal moves.”);
}
}
class Cat extends Animal{
private String name;
private String color;
Cat(){}
Cat (String catName, String catColor) {
name = catName;
color = catColor;
System.out.println(color);
}
public String getName(){
return name;
}
public void setName(String catName){
name = catName;
}
public String getColor(){
return color;
}
public void setColor(String catColor){
color = catColor;
}
public void move(){
//Переопределяем метод move.
//Теперь движения кошки будут выводится как:
//”The cat gracefully walks and jumps.”.
System.out.println(“The cat gracefully walks and jumps.”);
}
}
class Polimorphism2 {
public static void main(String[] args) {
Cat cat = new Cat();
// Воспользуемся методом move.
cat.move();
// Если бы метод move не был переопределён,
// на консоль вывелось бы “The animal moves.”
// Но для кошки корректнее — “The cat gracefully
// walks and jumps.”
}
}
Вывод:
Каждое животное, наследующее класс Animal, может по-своему переопределить метод move(). Так, у птицы это может быть “летает”, у собаки — “бежит”, и так далее.
Переопределение методов — мощный механизм, позволяющий делать поведение объектов более точным и соответствующим их конкретному типу, оставаясь при этом в рамках единой абстракции.
Наследование дает возможность одному классу получить доступ к полям и методам другого класса. Однако между этими классами должна быть логическая связь: от общего к частному.
Например, в программе ниже создаётся класс Animal (Животное).
Затем создаётся класс Cat (Кот), который с помощью ключевого слова extends наследует Animal, потому что кот — это частный случай животного, то есть кот является животным.
Животными, разумеется, являются и собака, и птица, и кенгуру и т.д., поэтому они тоже могут наследовать от Animal. То есть класс Animal общий в том плане, что от него могут наследовать классы разных конкретных животных.
При наследовании класса Animal, класс Cat получает доступ напрямуюко всем полям и методам класса Animal.
Например, если у класса Animal есть поля: количество ног, тип питания и т.д., то класс Cat, наследующий Animal, также будет обладать этими полями. И если мы создадим объект кота в методе main, то сможем обращаться к этим полям через объект кота — как будто они определены непосредственно в классе Cat. Таким образом, можно сказать, что создаётся единый объект, состоящий из данных класса Animal и класса Cat.
Рассмотрим пример:
// Это класс Животное
class Animal {
// У него два поля: тип питания и количество ног
private String eats;
private int noOfLegs;
public Animal(){}
public Animal(String food, int legs){
this.eats = food;
this.noOfLegs = legs;
}
//геттеры и сеттеры
public String getEats() {
return eats;
}
public void setEats(String eats) {
this.eats = eats;
}
public int getNoOfLegs() {
return noOfLegs;
}
public void setNoOfLegs(int noOfLegs) {
this.noOfLegs = noOfLegs;
}
}
//Наследование реализуется с помощью
//ключевого слова extends.
//Cat наследует Animal.
class Cat extends Animal{
private String name;
private String color;
Cat(){}
Cat (String catName, String catColor) {
name = catName;
color = catColor;
System.out.println(color);
}
public String getName(){
return name;
}
public void setName(String catName){
name = catName;
}
public String getColor(){
return color;
}
public void setColor(String catColor){
color = catColor;
}
void sayMeow(){
System.out.println(“Meow”);
}
}
class Inheritance {
public static void main(String[] args){
Cat somecat = new Cat();
//Через объект класса Cat
//имеем доступ к методам
//setNoOfLegs и getNoOfLegs класса Animal
//благодаря наследованию.
somecat.setNoOfLegs(4);
System.out.println(somecat.getNoOfLegs());
}
}
Вывод:
Основное преимущество наследования очевидно — уменьшение количества кода за счет того, что довольно большая часть кода может быть вынесена в класс родитель, а наследники этот код могут просто использовать, так сказать, “подключаясь” с помощью extends к классу родителю.
Кроме того, наследование помогает логично организовать структуру программы — от общего к частному — и упрощает сопровождение: изменения в коде родителя автоматически отражаются во всех наследниках.
В Java поля класса никогда не следует определять как public, то есть они не должны быть доступны напрямую. Вместо этого поля должны быть private, а доступ к ним должен обеспечиваться с помощью специальных методов – геттеров и сеттеров.
геттер – это public метод через который мы получаем значение private поля.
сеттер – это public метод через который мы задаем значение private полю.
То есть мы работаем с private полем не напрямую (напрямую это так – somecat.name = “Barsik”;), а через public методы класса, то есть методы, которые доступны не только в пределах класса.
Определяется один геттер и один сеттер на каждое private поле класса.
Поясним на примере:
class Cat {
// Пусть оба поля будут private
private String name;
private String color;
Cat(String catName, String catColor) {
name = catName;
color = catColor;
// К color можно обращаться только
// в пределах класса кошки и ниоткуда больше
// например, в конструкторе класса кота
System.out.println(color);
}
// Как видим, для поля name был создан
// геттер и сеттер (два публичных метода ниже).
public String getName() {
return name;
}
public void setName(String catName) {
name = catName;
}
// Для color тоже создаем геттер и сеттер.
public String getColor() {
return color;
}
public void setColor(String catColor) {
color = catColor;
}
void sayMeow() {
System.out.println(“Meow”);
}
}
class Props {
public static void main(String[] args) {
Cat somecat = new Cat(“Kuzya”, “White”);
// Поскольку напрямую к полям мы обратиться
// не можем (вот так somecat.color = “White”),
// используем сеттер setColor для записи значения
// в поле color объекта somecat.
// setColor публичный метод, поэтому
// можем к нему обратиться напрямую.
somecat.setColor(“Black”);
// Геттер тоже публичный метод,
// поэтому тоже используем его напямую
// для извлечения значения приватного поля color
// из объекта somecat.
System.out.println(somecat.getColor());
}
}
Вывод:
Зачем нужны геттеры, сеттеры?
На первый взгляд может показаться, что проще объявить поле public и обращаться к нему напрямую через объект. Зачем усложнять?
Однако на деле, чтобы была возможность при необходимости накладывать ограничения на допустимые значения поля класса, нужно использовать сеттер. Сеттер — это обычный метод, а значит, мы можем добавить в него любую дополнительную логику, которая будет отфильтровывать недопустимые значения и не позволять им попасть в поле. Такой подход помогает контролировать данные, которые записываются в поля объекта, и предотвращает появление нежелательных значений.
Допустим, если в классе Cat есть поле name, которое public, то напрямую через объект этого класса можно задать значение этого поля. Например “З” (вот так – somecat.name = “З”;).
Ну кто называет кошку одним символом? – Никто. Поэтому в сеттере можно задать ограничение, чтобы имя кошки содержало минимум два символа.
Поясним на примере:
class Cat{
private String name;
private String color;
public String getName(){
return name;
}
public void setName(String catName){
//Задаем ограничение.
//Если длина переданного в метод имени
//больше единицы,
if(catName.length()<1) {
//то записываем переданное имя в поле name.
name = catName;
}
else { //В ином случае
//выведем сообщение чтобы ввели
//больше чем один символ
System.out.println("Cat name have to contain"
+ "more than 1 character.");
}
}
public String getColor(){
return color;
}
public void setColor(String catColor){
color = catColor;
}
void sayMeow(){
System.out.println("Meow");
}
}
class Props2 {
public static void main(String[] args) {
Cat somecat = new Cat();
somecat.setName("P");//В name ничего не запишется
//и будет выведено сообщение:
//"Cat name have to contain more than 1 character."
//В результате строчкой кода ниже будет выведено,
//что поле name объекта somecat пустое (null),
//так как у нас не получилось записать туда "P".
System.out.println(somecat.getName());
}
}
В прошлом уроке мы рассмотрели базовые принципы инкапсуляции. Теперь, продолжая эту тему, пора познакомиться с модификаторами доступаpublic и private.
С их помощью можно ограничивать доступ к определённым элементам класса — например, полям и методам.
Если вспомнить пример из предидущего урока, то в классе Cat перед полями String name и String color не было указано никаких модификаторов. Однако, по умолчанию они являются public, что означает открытый доступ.
Благодаря этому мы могли напрямую через объекткота записать значения в его поля name и color вот так – somecat.name = “Barsik” и somecat.color = “White”.
Если мы поставим перед этими полями private, то это уже будет запрещено.
С полями класса помеченными как private можно работать только в пределах класса, то есть, например, в пределах методов или конструкторов этого класса.
Поясним на примере:
class Cat {
// пусть name будет public, а color будет private
public String name;
private String color;
Cat(String catColor) {
color = catColor;
// к color можно обращаться только
// в пределах класса кошки и ниоткуда больше.
// Например, ниже обращаемся к color
// в конструкторе класса кошки для вывода
// содержимого этого приватного поля на консоль.
System.out.println(color);
}
void sayMeow() {
System.out.println(“Meow”);
}
}
class Modifiers {
public static void main(String[] args) {
Cat somecat = new Cat(“White”);
// name помечен как public, поэтому мы можем
// напрямую через объект записать
// в него значение “Barsik”.
somecat.name = “Barsik”;
System.out.println(somecat.name);
// Если мы раскомментируем строчку кода ниже
// то будет ошибка, так как color помечен как private.
// somecat.color = “White”;
// То есть опять-таки, к private полям можно
// обращаться только в пределах класса.
}
}
Вывод:
Все эти модификаторы также можно применять к методам класса, конструкторам и самим классам — с той же целью: ограничить доступ к внутренним элементам класса извне.
ООП основывается на трёх ключевых принципах: – Инкапсуляция, Полиморфизм, Наследование.
Начнем с инкапсуляции.
Инкапсуляция означает сокрытие деталей реализации класса от внешнего кода, который просто использует этот класс.
Это означает, что тому, кто создал объект класса и пользуется им, совсем не обязательно знать его внутреннюю реализацию — достаточно понимания доступных методов и правил их использования. То есть можно работать с полями и методами объекта, даже не видя сам класс, из которого он создан.
Если вспомнить пример из прошлого урока про кошку, то тому кто создал и пользуется классом кошки не обязательно видеть класс кошки.
Также инкапсуляция значит защиту внутренних данных класса от другого кода, никак не относящемуся к данному классу.
То есть, например, класс кошки не должен иметь доступ к полям класса собаки, это разные животные, поэтому то мы и отделили их в разные классы, чтобы у них был ограниченный доступ друг к другу.
Простейший пример базовой инкапсуляции:
class Cat{
String name;
String color;
Cat (String catName, String catColor) {
name = catName;
color = catColor;
}
void sayMeow(){
System.out.println(“Meow”);
}
}
class Dog {
//Отсюда классу Dog не доступны поля кошки name и color,
//а также метод sayMeow. То есть в этом классе нельзя
//использовать содержимое класса Cat.
String name;
String color;
Dog (String dogName, String dogColor) {
name = dogName;
color = dogColor;
}
void sayWuf(){
System.out.println(“Wuf”);
}
}
class Incapsulation {
public static void main(String[] args) {
Cat somecat = new Cat(“Barsik”, “White”);
System.out.println(somecat.name);
}
}
Это был бызовый пример инкапсуляции, но под инкапсуляцией также, конечно, имеют ввиду модификаторы доступа и свойства (геттеры, сеттеры), которые будут рассмотрены в следующих уроках.
У класса может быть специальный метод, который не имеет возвращаемого значения и который имеет то же самое имя, что и класс.
Этот метод называется конструктором.
Он нужен чтобы при создании объектаклассаможно было выполнить какие-то действия — например, сразу при создании объекта присвоить его полям значения.
class Cat{
String name;
String color;
// Видим, что перед Cat ничего не указано —
// это значит, что этот метод это точно констурктор
// и что он не возвращает накакого значения.
// При создании объекта класса
// в конструктор будут передаваться
// два строковых значения — имя кошки и её цвет.
Cat (String catName, String catColor) {
// И эти переданные значения
// присваиваются полям name и color объекта.
name = catName;
color = catColor;
}
void sayMeow(){
System.out.println(“Meow”);
}
}
// Класс с методом main, где создаётся объект кота.
class Constructors{
public static void main(String[] args) {
// Очевидно, что ниже после new вызываеться
// конструктор, который мы определили выше.
// Через него создаваемому объекту кошки
// задаеться имя и цвет.
Cat somecat = new Cat(“Barsik”, “White”);
// Таким образом в конструкторе
// мы можем выполнять какие-либо действия
// сразу при создании объекта.
// Например, присвоить
// полям объекта значения.
// Выведем на консоль установленное
// в конструкторе имя кошки.
System.out.println(somecat.name);
}
}
Вывод:
На самом деле, мы уже сталкивались с конструктором в прошлом уроке когда создавали объект кота вот так:
Cat somecat = new Cat();.
В данном случае это конструктор по умолчанию, который автоматически создаётся компилятором, если в классе не определён ни один другой. Он позволяет создавать объекты без параметров. Однако если мы явно добавим свой конструктор (например, с параметрами), компилятор уже не создаст конструктор по умолчанию — его придётся прописать вручную.
ООП (Объектно-ориентированное программирование) – это способ построения программных продуктов. Такие программные продукты состоят из объектов и вся работа такого программного продукта построена на взаимодействии этих объектов.
Что же такое объект в таком программном продукте?
Да, в общем-то, всё что угодно.
Например, какой-либо элемент интерфейса – скажем, панель с кнопками – может быть обьектом.
Или если, например, программный продукт как-то связан с котами (например, программа для поиска подходящей породы кошки), то в ней точно будет объект кота.
И таких разных объектов в продукте может быть очень много и все они будут тем или иным образом взаимодействовать друг с другом.
То есть, как уже можно было догадаться, объект – это программная модель какой-либо сущности реального мира.
Сущность на основе которой строиться модель может иметь какие-либо характеристики и может проявлять какое-либо поведение. Соответственно модель тоже должна содержать эти характеристики и уметь проявлять поведение этой сущности.
Характеристики в объекте правильно называть свойствами, а какое-либо поведение объект проявляет с помощью методов(функций) в нем.
Например, есть сущность реального мира кот. Кот имеет характеристики – имя кота, цвет кота и т.д.
Кот может проявлять какое-либо поведение – например сказать Мяу.
Если представить кота, как его модель в виде объекта, то свойствами этого объекта будут, например, кот по имени Барсик серого цвета, и он будет говорить мяу с помощью метода в нем.
Как видим, объект кота уже имеет конкретное имя Барсик и имеет конкретный цветсерый. То есть объект – это уже конкретный кот, а не просто кот в общем.
А вот кот в общем – это КЛАСС.
Кот в общем – это шаблонпо которому был создан объект кота.
То есть классзадает, что у кота должно быть имя и цвет (это называется ПОЛЯМИ класса) и он должен уметь говорить мяу (в классеописывается то, как он должен говорить мяу).
А уже по этому шаблонусоздается объект кота с конкретным именем, цветом и он уже, собственно, говорит мяу, как ему было задано говорить мяу в классе кота.
Класс кота и объект этого класса на примере.
Ниже приведен класс кота и создание его объекта с подробным пояснением.
// Это класс кота (кот “в общем” или шаблон кота).
class Cat {
// Это поля класса. То есть любой кот, который будет создан по этому шаблону, будет иметь
// имя и цвет. Когда по шаблону кота будет создан конкретный объект кота,
// этот объект будет иметь конкретное значение в name и конкретное значение в color.
String name;
String color;
// Любой кот должен уметь говорить “мяу”.
// Здесь в методе sayMeow мы описываем, как наш кот будет говорить “мяу”.
void sayMeow() {
System.out.println(“Meow”);
}
}
class JavaOOP {
public static void main(String[] args) {
// В main мы создаем объект кошки по классу кошки.
// То есть мы создаем конкретного кота.
// С помощью new выделяем память под объект этого кота.
Cat somecat = new Cat();
// Внизу мы даем этому конкретному коту имя и цвет.
somecat.name = “Barsik”;
somecat.color = “White”;
// И теперь давайте же наш только что появившийся кот скажет “мяу”.
somecat.sayMeow();
}
}
После изучения функций, условий и циклов стоит поговорить об области видимости переменных — то есть, где переменные “видны” и доступны для использования.
В примере программы ниже можно увидеть, что внутри if была определена строковая переменная d и после блока if совершается попытка вывести ее содержимое на консоль. Однако, она недоступна вне блока if.
Этот код не скомпилируется, так как все переменные, которые создаются внутри блока if НЕ ВИДНЫ внешнему блоку, то есть функции main.
Это касается не только if, а и for, switch, else и тому подобное.
class Test {
static int averageOf3Nums(int a, int b, int c) {
int sum = a + b + c;
int resultAverage = sum / 3;
return resultAverage;
}
public static void main(String[] args) {
String b = “Some text info!”;
String c = “Some text info!”;
if (b.equals(c)) {
String d = b + c;
}
System.out.println(d);
}
}
Вывод:
Чтобы переменная была доступна вне блока, её нужно объявить заранее, до входа в if:
class Test
{
static int averageOf3Nums(int a, int b, int c) {
int sum = a + b + c;
int resultAverage = sum / 3;
return resultAverage;
}
public static void main(String[] args)
{
String b = “Some text info!”;
String c = “Some text info!”;
// Создаем переменную заранее
String d = “”;
if (b.equals(c)) {
// Записываем результирующую строку
// в уже созданную ранее переменную
d = b + c;
}
// Выводим ее на консоль
System.out.println(d);
}
}
В этом уроке мы познакомимся с понятием функций, которые в Java все же корректнее называть методами. Несмотря на это, термин “функция” широко используется и в Java, поэтому для удобства будем придерживаться именно его.
До этого мы уже употребляли некоторые функции — например, println() для вывода строки в консоль или replace() для замены символов в строке и т.д.
Говоря просто, функция — это единица кода, которая выполняет определенное действие над переданными ей данными.
Важно понимать, что все процессы, которые выполняют это действие (то есть код, который реализует процессы выполняющие это действие) скрыты от наших глаз — они происходят “за кулисами”.
То есть в программе мы просто написали название функции, передав ей какие-либо аргументы (аргументы это то, что передается функции в круглых скобочках () ) и она выполняет то или иное действие.
И эту функцию можно таким образом вызывать много раз по ходу программы.
К примеру, представьте, что мы вызвали функцию equals много раз по ходу программы.
А теперь представьте если бы для сравнения двух строк на каждом месте где вызывается эта функция мы бы писали не вызов equals, а то, что внутри equals, то есть код, который реализует процессы сравнения двух строк и собственно выполняет сравнение двух строк, то есть код, которыйпод капотом у equals, а этот код это не одна строчка кода, и очевидно, что на каждом месте где у нас раньше был вызов equals, эти строчки кода из equals будут повторяться. Что очевидно не есть хорошо. Вызов функции это одна строчка кода, а то что у нее под капотом это много строчек кода. Лучше повторять одну строчку кода, чем повторять много строчек кода.
И здесь очевиден смысл функций!!
Вынося код в функцию, а не многократно повторяя его по ходу программы мы уменьшаем количество повторяющегося кода, что ясное дело делает его намного читабельнее.
Более наглядно на примере программы:
class Test
{
public static void main(String []args)
{
String b = “Some text info!”;
String c = “Some text info!”;
String d = “Some text inrwgf!”;
if(b.equals(c)) { //Если бы здесь,
System.out.println(“b and c are equal strings!”);
}
System.out.println(b.equals(c));// здесь,
System.out.println(c.equals(d));// и здесь,
//для сравнения строк вместо вызова equals был бы
//код, который под капотом у функции equals
//то очевидно было бы много повторяющегося кода кода.
}
}
Создание функции
Создадим функцию, которая вычисляет среднее арифметическое трёх чисел: складывает их, делит сумму на три и выводит результат на консоль. Ниже приведен пример создания этой функции.
Пока не обращайте внимание на слова static и void, их разберем позже.
averageOf3Nums – это имя нашей функции (по аналогии с функцией equals, где equals – это имя функции).
По этому имени будет вызываться наша функция для нахождения ср. арифметического.
В скобочках через запятую определяються аргументы, которые будут передаваться в функцию – какого они типа и сколько их. Как видим, это будут три числовых значения.
Внутри же этой функции находиться код который реализует процессы нахождения среднего арифметического трех чисел: он выполняет действия над переданными аргументами и выводит результат.
Пример создания функции:
class Test
{
static void averageOf3Nums(int a, int b, int c) {
int sum = a + b + c; // сложить 3 числа
int resultAverage = sum / 3; // поделить на их количество
System.out.println(resultAverage); // вывести на консоль ср. ар.
}
public static void main(String[] args)
{
// В main используем функцию, определенную выше.
// Ниже вместо a будет подставлено 6,
// вместо b будет подставлено 9,
// а вместо c будет подставлено 34.
// И все действия, которые выполняются с a, b, c
// в функции выше, будут выполняться с 6, 9, 34.
// В переменной resultAverage окажется
// среднее арифметическое этих чисел,
// и оно будет выведено на консоль.
averageOf3Nums(6, 9, 34);
// Вызовем еще пару раз с другими числами.
averageOf3Nums(9, 44, 11);
averageOf3Nums(3, 12, 4);
}
}
Вывод:
Код выше можно было бы переписать без функции.
class Test
{
public static void main(String []args)
{
int sum, resultAverage;
sum = 6+9+34; // сложить 3 числа
resultAverage = sum/3; // поделить на их количество
System.out.println(resultAverage); // вывести на консоль
sum = 9+44+11;
resultAverage = sum/3;
System.out.println(resultAverage);
sum = 3+12+4;
resultAverage = sum/3;
System.out.println(resultAverage);
}
}
Вывод:
То, о чем мы говорили: много повторяющегося кода. Это неудобно и неэффективно, так как вместо одной строчки приходиться каждый раз писать три.
Возвращаемое значение функции
Если вспомнить функцию equals, мы помним, что она возвращала true или false.
То есть результатом выполнения функции equals является booleanзначение.
Наша же функция averageOf3Nums возвращала void(это ключевое слово можно увидеть перед именем).
Это ключевое слово значит, что функция не должна возвращать ничего. И наша функция ничего и не возвращала, она просто выполняла некоторые вычисления и потом вызывала функцию println для вывода результата вычислений на консоль.
Таким образом, ключевое слово перед именем функции – это тип возвращаемого функцией значения.
Пример функции, которая возвращает значение:
class Test {
// Теперь наша функция уже не void и должна
// возвращать число типа int (int перед именем)
static int averageOf3Nums(int a, int b, int c) {
int sum = a + b + c;
int resultAverage = sum / 3;
// Действительно, resultAverage — это число типа int.
// Поэтому ключевым словом return можем вернуть
// это число как результат функции
return resultAverage;
}
public static void main(String[] args) {
// Нам уже незачем просто вызывать функцию, как в строке кода ниже —
// это бессмысленно, так как результат функции никуда не сохраняется.
// Число, которое возвращает функция, должно быть как-то использовано.
averageOf3Nums(6, 9, 34);
// Например, можно записать возвращаемое значение в переменную:
int average = averageOf3Nums(6, 9, 34);
// А затем передать её в функцию для вывода на консоль:
System.out.println(average);
// В результате на консоли увидим среднее арифметическое
}
}