Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатика
ИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханика
ОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторика
СоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансы
ХимияЧерчениеЭкологияЭкономикаЭлектроника

Создание собственных классов в Java (продолжение), инкапсуляция, полиморфизм

Читайте также:
  1. Gt;■ < ' ol" '. 5 ■* создание ряда
  2. II. Создание глоссария по теме занятия
  3. IV Создание ковровой дорожки
  4. Snow Brand Milk не делает выводов из собственных ошибок
  5. А ты мне нет, принцесса, - губы Аниса, что только что касались его собственных, растянулись в довольной и нахальной улыбке.
  6. А. Создание и заполнение базы данных
  7. Априорные и апостериорные вероятности классов объектов

Статические методы в 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 вызывает с, а с вызывает а.

 

 


 

Создание собственных классов в Java: свойства, методы, конструкторы

Создание класса: свойства и методы

Рассмотрим пример создания простейшего класса. Давайте с его помощью смоделируем окружности на координатной плоскости.

Каждая такая окружность, как известно, будет определяться своим центром (т.е. точкой с двумя числовыми координатами) и радиусом (т.е. его длиной, представляемой в виде числа). Таким образом, окружность на координатной плоскости характеризуют 3 вещественных числа. Значит в нашем классе должно быть три соответствующих свойства.

Пока не будем пытаться решать серьёзных задач с помощью класса, а наделим его следующими возможностями: созданную на основе класса окружность должно быть возможно выводить на экран (в виде описания её характеристик), перемещать (т.е. совершать преобразование движения, меняя координаты её центра) и масштабировать (т.е. совершать преобразование подобия, меняя радиус окружности).

// описываем отдельный новый класс
class Circle {
// свойства класса
public double x; // абсцисса центра
public double y; // ордината центра
public double r; // радиус
// методы класса
// выводит на экран параметры окружности
public void printCircle() {
System.out.println("Окружность с центром ("+x+";"+y+") и радиусом "+r);
}
// перемещает центр, движение окружности
public void moveCircle(double a, double b) {
x = x + a;
y = y + b;
}
// масштабируем, выполняем преобразование подобия с коэффициентом k
public void zoomCircle(double k) {
r = r * k;
}
}

// описываем основной класс, содержащий метод main
public class Main {
public static void main(String[] args) {
// Создаём объект (окружность класса Circle), у неё будет нулевой
// радиус и центр в (0.0;0.0), поскольку все свойства получат
// значения по умолчанию
Circle o1 = new Circle();
// выводим на экран параметры окружности
o1.printCircle();
// Меняем абсциссу центра, обращааясь к свойству x
o1.x = 3;
// Меняем радиус, обращааясь к свойству r
o1.r = 12.3;
// выводим на экран обновлённые параметры окружности
o1.printCircle();
// Создаём другой объект того же класса
Circle o2 = new Circle();
o2.r = 3.14;
o2.zoomCircle(1.66);
o2.printCircle(); // Окружность с центром (0.0;0.0) и радиусом 5.2124
}
}

Конструкторы

Когда мы создаём объект командой Circle o1 = new Circle(); используется так называемый конструктор по умолчанию (или конструктор без параметров) — это специальный метод класса, мы его не определяли явно, но даже если его не определить он создаётся автоматически, выполняется при создании каждого нового объекта и присваивает первоначальные значения его свойствам (инициализирует их). Значения по умолчанию для свойств зависят от их типа (0 или 0.0 для чиловых типов, false для логического типа и т.д.).

Конструктор по умолчанию можно описать явно и при этом задать началльные значения для свойств нового объекта, отличные от значений по умолчанию.

От остальных методов конструктор отличается тем, что имеет то же самое имя, что и весь класс, а также не имеет типа возвращаемого значения (по сути, в результате своей работы конструктор возвращает новый объект нужного класса).

class Circle {
public double x; // абсцисса центра
public double y; // ордината центра
public double r; // радиус

public void printCircle() {
System.out.println("Окружность с центром ("+x+";"+y+") и радиусом "+r);
}
public void moveCircle(double a, double b) {
x = x + a;
y = y + b;
}
public void zoomCircle(double k) {
r = r * k;
}
// конструктор по умолчанию, теперь сразу после создания объекта будем
// получать окружность единичного радиуса с центром в начале координат
public Circle() {
x = 0.0;
y = 0.0;
r = 1.0;
}
}

public class Main {
public static void main(String[] args) {
Circle o1 = new Circle();
o1.printCircle(); // Окружность с центром (0.0;0.0) и радиусом 1.0
}
}

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

Описать подобный конструктор можно в дополнение к основному следующим образом:

public Circle(double a, double b, double s) {
x = a;
y = b;
r = s;
}

Теперь при создании объектов можно пользоваться любым конструктором на выбор:

Circle o1 = new Circle();
o1.printCircle(); // Окружность с центром (0.0;0.0) и радиусом 1.0
Circle o2 = new Circle(1,-1,14);
o2.printCircle(); // Окружность с центром (1.0;-1.0) и радиусом 14.0

Нужно учитывать следующий факт: если в классе описан явно хотя бы один конструктор с параметрами, то конструктор по умолчанию (без параметров) создаваться автоматические уже не будет (его в такой ситуации надо описывать явно). Хотя, если вам требуется только конструктор с параметрами (как второй из нашего примера), то можно обойтись и совсем без конструктора по умолчанию (описать в классе только один конструктор с параметрами).

Доступ к членам класса из тела методов

Добавим в наш класс метод, вычисляющий площадь той окружности, к которой метод применён. Метод будет описывать так:

public double squareCircle() {
double s = Math.PI * r * r;
return s;
}

Результат работы метода можно увидеть следующим образом:

System.out.println("Площадь круга o2: "+o2.squareCircle()); //615.75...

Обратите внимание: внутри каждого метода класса доступны свойства того объекта, для которого метод будет вызываться. То есть если мы вызываем метод для объекта o2, то внутри метода при его выполнении мы будем работать именно со свойствами объекта o2 (o2.x будет доступно x, o2.r будет доступно как r и т.д.).

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

Например, можно было бы начать описание метода для масштабирования таким образом:

public void zoomCircle(double r) {…

Как же в таком случае обращаться к свойствам объекта (ведь имена этих свойств перекрываются формальным параметром)?

Решение такой неоднозначности существует: к любому свойству внутри метода можно обращаться не только по имени, но и через ссылку this. То есть внутри метода можно написать x=13;, а можно this.x=13; — эффект будет идентичный. Соответственно, когда имя формального параметра перекрывает имя свойства, к имени свойства нужно обращаться через ссылку this. Тогда метод можно переписать таким образом:

public void zoomCircle(double r) {
this.r = this.r * r;
}

Понятно, что удобнее не допускать перекрывания имён свойств именами локальных параметров в методах. Иногда, впрочем, требуется внутри метода применить какой-то другой метод к текущему объекту, тогда без ссылки this не обойтись.

Добавим в класс метод, проверяющий, совпадают ли две окружности по площади.

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

Метод можно описать так:

public boolean equalsCircle(Circle cir) {
if(this.squareCircle() == cir.squareCircle()) {
return true;
} else {
return false;
}
}

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

if(o1.equalsCircle(o2)) {
System.out.println("Круги o2 и o1 имеют равную площадь");
} else {
System.out.println("Круги o2 и o1 имеют различную площадь");
}

Задачи

1. Создайте в классе Circle метод, вычисляющий длину окружности.

2. Создайте в классе Circle метод, перемещающий центр круга в случайную точку квадрата координатной плоскости с диагональю от [-99;-99] до [99;99]. Обратите внимание на то, что требуется создать обычный метод, применимый к уже существующему объекту, а не конструктор создающий новый объект.

3. Измените в классе Circle конструктор по умолчанию так, чтобы в момент создания объекта с его помощью, координаты центра и радиус окружности пользователь вводил с клавиатуры.

4. Создайте в классе Circle метод, вычисляющий расстояние между центрами двух окружностей.

5. Создайте в классе Circle метод, проверяющий, касаются ли окружности в одной точке. Учтите, что возможен вариант, когда одна окружность содержится внутри другой и при этом всё равно возможно касание в одной точке.

Пример

class Circle {
public double x; // абсцисса центра
public double y; // ордината центра
public double r; // радиус

public void printCircle() {
System.out.println("Окружность с центром ("+x+";"+y+") и радиусом "+r);
}
public void moveCircle(double a, double b) {
x = x + a;
y = y + b;
}
public void zoomCircle(double r) {
this.r = this.r * r;
}
public Circle() {
x = 0.0;
y = 0.0;
r = 1.0;
}
public Circle(double a, double b, double s) {
x = a;
y = b;
r = s;
}
// метод вычисляющий площадь круга
public double squareCircle() {
double s = Math.PI * r * r;
return s;
}
// метод проверяющий равны ли окружности по площадям
public boolean equalsCircle(Circle cir) {
if(this.squareCircle() == cir.squareCircle()) {
return true;
} else {
return false;
}
}
}

public class Main {
public static void main(String[] args) {
Circle o1 = new Circle();
o1.printCircle(); // Окружность с центром (0.0;0.0) и радиусом 1.0
Circle o2 = new Circle(1,-1,14);
o2.printCircle(); // Окружность с центром (1.0;-1.0) и радиусом 14.0
System.out.println("Площадь круга o2: "+o2.squareCircle()); //615.75...
o1.zoomCircle(14);
if(o1.equalsCircle(o2)) {
System.out.println("Круги o2 и o1 имеют равную площадь");
} else {
System.out.println("Круги o2 и o1 имеют различную площадь");
}
}
}

 

 


 

Создание собственных классов в Java (продолжение), класс Object

Рассмотрим пример класса точек на плоскости:

class Point {
public double x; // абсцисса точки
public double y; // ордината точки

// возвращает строку с описанием точки
public String toString() {
return "("+x+";"+y+")";
}
// выводит на экран описание точки
public void printPoint() {
System.out.print(this.toString());
}
// метод перемещает точку на указанный вектор
public void movePoint(double a, double b) {
x = x + a;
y = y + b;
}
// метод изменяет координаты точки на указанные
public void setPoint(double a, double b) {
x = a;
y = b;
}
// конструктор по умолчанию, создающий точку в начале координат
public Point() {
x = 0.0;
y = 0.0;
}
// конструктор, создающий точку с указанными координатами
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;
}
}
}

public class Main {
public static void main(String[] args) {
Point p1 = new Point();
Point p2 = new Point(1,1);
System.out.println("Растстояние между точками "+p1+" и "+p2+" равно "+p1.length(p2));
}
}

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

Например, мы можем пользоваться методом явно:

Point p3 = new Point(3,4.67);
System.out.println("Координаты точки: "+p3.toString());

А можем просто обединять наш объект с другой строкой, провоцируя автоприведение:

Point p4 = new Point(3.2,2.3);
System.out.println("Координаты точки: "+p4);

Результат при этом будем получать идентичный.

Такой особенный метод toString() существует на самом деле для всех объектов в Java. Любой класс в Java является наследниом класса Object (хотя наследование мы явно при создании своего класса никак не используем) и от этого родительского класса получает ряд готовых методов (среди которых toString() тоже присутствует). Теперь в классе Point мы метод toString() перегрузили, сделав для него такую реализацию, которая требуется нам в нашей программе.

Ещё один яркий пример метода, наследуемого от коренного класса Object — это метод equals(Object o) для сравнения объектов — его можно применять к любым двум объектам (даже если они из разных классов), вызывая метод для одного из них, а второй передавая через параметр. Метод будет возвращать истинное значение тогда и только тогда, когда будет вызван для двух ссылок на один и тот же объект. В своих программах с практической точки зрения равными можно считать разные объекты имеющие одинаковый набор текущих значений в полях, поэтому метод equals обычно тоже перегружают. Вместе с ним обязательно перегружать и метод hashCode(), возвращающий некоторое целое число для каждого объекта, являющегося его уникальным числовым идентификатором (хешем). По умолчанию (в той реализации, которая представлена в классе Object) это число строится на основании адреса объекта в памяти, но при перегрузке метода можно придумать свою собственную реализацию, главное, чтобы она удовлетворяла одному важному правилу: если два обекта совпадают в соответствии с методом equals, то у них должны быть одинаковые хеши, возвращаемые методом hashCode(), при этом обратного не требуется. Например, для нашего класса Point мы могли бы в качестве хеша возвращать произведение координат точки.

Не требуется, но рекомендуется для своих классов перегружать перечисленные выше методы. В примере это сделано для метода toString, но не сделано для equals и hashCode.

Задачи

1. Создайте в классе метод, который будет выводить на экран сообщение о том, в какой координатной четверти лежит точка.

2. Создайте в классе метод, проверяющий, являются ли две точки симметричными относительно начала отсчёта.

3. Измените в классе конструктор по умолчанию таким образом, чтобы начальные координаты точки при её создании пользователь задавал с клавиатуры.

4. Создайте в классе метод, проверяющий, являются ли три точки коллинеарными (т.е. лежащими на одной прямой).

 

Взаимное расположение точек и прямых на плоскости

Три точки , и лежат на одной прямой тогда и только тогда, когда выполняется условие

 

 


 

Создание собственных классов в 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, устанавливать). Они являются обёртками для доступа к полям на чтение и запись, соответвенно.

Инкапсуляция позволяет запрещать и контролировать использование любых членов класса за пределами его тела.

Задачи

1. Создайте класс треугольников на координатной плоскости, используя в качестве полей объекты-точки. Реализуйте в классе:
a) конструктор, позволяющий задавать вершины с клавиатуры;
b) метод print() выводящий описание треугольника на экран;
c) методы для вычисления периметра и площади треугольника.

2. Доработайте конструктор таким образом, чтобы нельзя было задать три вершины, лежащие на одной прямой. Это несложно будет сделать с использованием метода из класса точек, который проверяет являются ли точки коллинеарными, если прежде вы не реализовали этот метод, то сейчас самое время сделать это.

3. Инкапсулируйте поля таким образом, чтобы нельзя изменить значение любого из них так, чтобы вершины оказались на одной прямой.

4. Создайте метод, поворачивающий треугольник в его плоскости вокруг центра тяжести на указанное в аргументе количество градусов.

У треугольника как у части однородной плоскости центром тяжести является точка пересечения медиан


 


Дата добавления: 2015-10-29; просмотров: 361 | Нарушение авторских прав


<== предыдущая страница | следующая страница ==>
Принципы создания пользовательского интерфейса. Контейнеры, панели. Основные компоненты библиотеки Swing.| Код программы

mybiblioteka.su - 2015-2024 год. (0.048 сек.)