- •Основы классов
- •Общая форма класса
- •Простой класс
- •Объявление объектов
- •Операция new
- •Представление методов
- •Добавление метода к классу Box
- •Возврат значений
- •Добавление метода с параметрами
- •Конструкторы
- •Параметризованные конструкторы
- •Ключевое слово this
- •Скрытие переменной экземпляра
- •Сборка "мусора"
- •Метод finalize()
- •Класс Stack
- •Перегрузка конструкторов
- •Использование объектов в качестве параметров
- •Передача аргументов
- •Возврат объектов
- •Рекурсия
- •Управление доступом
- •Статические элементы
- •Спецификатор final
- •Ревизия массивов
- •Вложенные и внутренние классы
- •Класс String
- •Использование аргументов командной строки
- •Наследование
- •Основы наследования
- •11 Тело класса }
- •Доступ к элементам и наследование
- •Практический пример
- •Переменная суперкласса может ссылаться на объект подкласса
- •Использование ключевого слова super
- •Вызов конструктора суперкласса с помощью первой формы super
- •Создание многоуровневой иерархии
- •II построить клон объекта
- •Когда вызываются конструкторы
- •Переопределение методов
- •Динамическая диспетчеризация методов
- •Зачем нужны переопределенные методы?
- •Применение переопределения методов
- •Использование абстрактных классов
- •Void meth() { // ошибка! Нельзя переопределять.
- •Класс Object
Класс Stack
Хотя класс box полезен для иллюстрации существенных элементов класса, он имеет небольшое практическое значение. Чтобы показать действительную мощь классов, данную главу закончим более сложным примером. Как вы помните из обсуждения объектно-ориентированного программирования (OOP), представленного в главе 2, одним из наиболее важных его преимуществ является инкапсуляция данных и кода, который манипулирует этими данными. Механизмом, с помощью которого достигается инкапсуляция, является класс. Создавая класс, вы организуете новый тип данных, который определяет как характер данных, так и подпрограммы, используемые для манипулирования этими данными. Непротиворечивый и управляемый интерфейс с данными класса определяют методы. Таким образом, вы можете использовать класс через его методы, не беспокоясь о деталях его реализации или о том, как данные фактически управляются внутри класса. В некотором смысле, класс подобен "машине данных". Чтобы использовать машину через ее органы управления, никаких знаний о том, что происходит внутри машины, не требуется. Фактически, поскольку подробности скрыты, ее внутренняя работа может быть изменена так, как это необходимо. Пока ваш код использует класс через его методы, внутренние подробности могут изменяться, не вызывая побочных эффектов вне класса.
Чтобы получить практическое приложение предшествующего обсуждения, давайте разработаем один из типичных примеров инкапсуляции – стек. Стек хранит данные, используя очередь типа LIFO ("Last-In, First-Out") – последним вошел, первым вышел. То есть стек подобен стопке тарелок на столе – последняя тарелка, поставленная на стопку, снимается со стопки первой. Стеки управляются через две операции, традиционно называемые push (поместить) и pop (извлечь, вытолкнуть). Чтобы поместить элемент в вершину стека, нужно использовать операцию push. Чтобы извлечь элемент из стека, нужно использовать операцию pop. Заметим, что инкапсуляция полного механизма стека – довольно простая задача.
В следующей программе класс с именем stack реализует стек целых чисел:
// Этот класс определяет целый стек для хранения 10 значений.
class Stack {
int stck[] = new int[10];
int tos;
// инициализировать вершину стека Stack () {
tos = -1; }
// поместить элемент в стек
void push(int item) {
if (tos=9)
System.out.println("Стек заполнен.");
else
stck[++tos] = item;
}
// Извлечь элемент из стека
int pop() {
if tos < 0) {
System.out.println("Стек пуст.");
return 0;
}
else
return stck[tos–];
}
}
Нетрудно видеть, что класс stack определяет два элемента данных и три метода. Стек целых чисел содержится в массиве stck. Этот массив индексирован переменной tos, которая всегда содержит индекс вершины стека. Конструктор stack() инициализирует tos значением – 1, которое указывает, что стек пуст. Метод push () помещает элемент в стек. Чтобы извлечь элемент, вызовите метод pop(). Так как доступ к стеку выполняется через push() и pop о, тот факт, что стек содержится в массиве, не мешает использованию стека. Например, стек мог бы храниться в более сложной структуре данных, скажем, типа связного списка, а интерфейс, определенный методами push() и pop(), остался бы тем же самым.
Показанный ниже класс Teststack, демонстрирует работу с классом stack. Он создает два целочисленных стека, помещает некоторые значения в каждый и затем выталкивает их.
class TestStack {
public static void main(String args[]) {
Stack mystackl = new Stack ();
Stack mystack2 = new Stack ();
// поместить несколько чисел в стек
for(int i=0; i<10; i++)
mystackl. push (i);
for(int i=10; i<20; i++)
mystack2.push (i);
// вытолкнуть эти числа из стека
System.out.println("Стек в mystackl:"};
for(int 1=0; i<10; i++)
System.out.println(mystackl.pop());
System.out.println("Стек в mystack2:"),
for(int 1=0; i<10; i++)
System.out.println(mystack2.pop());
}
}
Эта программа генерирует следующий вывод:
Стек в mystack1:
9
8
7
6 . - .
5
4
3
2
1
о
Стек в mystack2:
19
18
17
16
15
14
13
12
11
10
Нетрудно заметить, что содержимое каждого стека различно.
Наконец, последнее замечание относительно класса stack: в данной реализации возможно изменение массива stck, который содержит стек, кодом, находящимся вне класса stack, оставляя его открытым для неправильного использования или повреждений. В следующей главе вы увидите, как исправить эту ситуацию.
Методы и классы
Эта глава продолжает обсуждение методов и классов, начатое в предшествующей главе. В ней рассматривается несколько тем, касающихся методов, включая перегрузку, передачу параметров и рекурсию. Затем изложение возвращается к классам, обсуждая управление доступом, использование ключевого слова static и один из наиболее важных встроенных Java-классов String.
Перегрузка методов
В языке Java в пределах одного класса можно определить два или более методов, которые совместно используют одно и то же имя, но имеют разное количество параметров. Когда это имеет место, методы называют перегруженными, а о процессе говорят как о перегрузке метода. Перегрузка методов – один из способов, с помощью которого Java реализует полиморфизм. Если вы никогда не пользовались языком, допускающим перегрузку методов, то концепция может сначала показаться странной. Но, как вы увидите, перегрузка метода – одна из наиболее захватывающих и полезных особенностей языка Java.
Чтобы определить при вызове, какую версию перегруженного метода в действительности вызывать, Java руководствуется типом и/или числом его параметров. Таким образом, перегруженные методы должны отличаться по типу и/или числу их параметров. Хотя такие методы могут иметь различные типы возвращаемого значения, однако одного его недостаточно, чтобы различить две версии метода. Когда Java сталкивается с вызовом перегруженного метода, он просто выполняет его (метод) версию, чьи параметры соответствуют параметрам, используемым в вызове.
Простой пример, который иллюстрирует перегруженный метод:
// Демонстрация перегруженного метода,
class OverloadDemo {
void test() {
System.out.println("Параметры отсутствуют");
}
// Перегруженный метод test с одним int-параметром.
void test(int a) j
System.out.println("a: " + a);
}
// Перегруженный метод test с двумя int-параметрами.
void test(int a, int b) {
System.out.println("а и Ь: " + a + " " + b);
}
// Перегруженный метод test с double-параметром,
double test(double a) {
System.out.println("Вещественное двойной точности а: " + a),
return a*a;
}
}
class Overload {
public static void main(String arcfs[]) {
OverloadDemo ob = new OverloadDemo();
double result;
// вызвать все версии
testf) ob.test();
ob.test(10);
ob.test(10, 20);
result = ob.test(123.2);
System.out.println("Результат ob.test(123.2): " + result)
}
}
Эта программа генерирует следующий вывод:
Параметры отсутствуют
а: 10
а и Ь: 10 20
Вещественное двойной точности а: 123.2
Результат ob.test(123.2): 15178.24
Как можно видеть, test() перегружен четыре раза. Первая версия не имеет никаких параметров, вторая имеет один параметр целого типа, третья – два целочисленных параметра, а четвертая – один double-параметр. Тот факт, что четвертая версия test() еще и возвращает значение, не имеет никакого отношения к перегрузке, так как типы возвращаемых значений не играют никакой роли для выбора перегруженных методов.
Когда вызывается перегруженный метод, Java ищет соответствие между аргументами вызова метода и его параметрами. Однако это соответствие не всегда может быть точным. В некоторых случаях определенную роль в выборе перегруженного метода могут сыграть автоматические преобразования типов Java. Например, рассмотрим следующую программу:
// Автоматическое преобразование типов в применении к перегрузке,
class OverloadDemo {
void test() {
System.out.println("Параметры отсутствуют");
}
// Перегруженный test с двумя int-параметрами.
void test(int a, int b) {
System.out.println("а и b: " + a + " " + b);
}
// Перегруженный test с double-параметром и возвращаемым типом,
void test(double a) {
System.out.println("Внутри test(double) a: " + a);
}
}
class Overload {
public static void main(String args[]) {
OverloadDemo ob = new OverloadDemo ();
int i = 88;
ob.testO;
ob.test(10, 20);
ob.test(i); // здесь будет вызван test(double)
ob.test(123.2); // здесь будет вызван test(double)
}
}
Эта программа генерирует следующий вывод:
Параметры отсутствуют
а и Ь: 10 20
Внутри test (double) a: 88
Внутри test (double) a: 123.2
Эта версия overioadDemo не определяет test(int) с одним целым параметром. Поэтому, когда test() вызывается с целым аргументом внутри класса overload, никакого согласованного метода не находится. Однако Java может автоматически преобразовывать int в double, и это преобразование можно использовать для разрешения вызова. Поэтому, после того, как test(int) не находится, Java расширяет i до double и затем вызывает test (double). Конечно, если бы test(int) был определен, то он вызывался бы вместо test (double). Java использует эти автоматические преобразования типов только тогда, когда никакого точного соответствия не находится.
Перегрузка методов поддерживает полиморфизм, потому что это один из способов, с помощью которых Java реализует парадигму "один интерфейс, множество методов". Чтобы понять, как это делается, приведем следующие рассуждения. На языках, которые не поддерживают перегрузку методов, каждому методу необходимо давать уникальное имя. Однако часто нужно реализовать, по существу, один и тот же метод для различных типов данных. Рассмотрим функцию абсолютного значения. На языках, которые не поддерживают перегрузку, существует обычно три или более версий этой функции, каждая со слегка отличающимся именем. Например, в С, функция abs() возвращает абсолютное значение целого числа, labs() возвращает абсолютное значение длинного целого числа, a fabs() – абсолютное значение числа с плавающей точкой. Так как С не поддерживает перегрузку, каждая функция должна иметь свое собственное имя, даже при том, что все три функции выполняют, по существу, одно и то же. Это делает ситуацию более сложной, чем она фактически есть на самом деле. Хотя основная концепция каждой функции одна и та же, вам все еще нужно помнить три разных имени. Подобная ситуация отсутствует в Java, потому что метод получения абсолютного значения един для всех типов данных. Действительно, библиотека стандартных классов Java включает метод абсолютного значения, с именем abs(). Этот метод перегружен в Math-классе Java, чтобы обрабатывать все числовые типы. Java определяет, какую версию abs() вызывать, основываясь на типе аргумента. Значение перегрузки заключается в том, что она позволяет осуществлять доступ к связанным методам при помощи общего имени. Таким образом, имя abs представляет общее выполняемое действие. Право же выбирать правильную специфическую версию для конкретного обстоятельства предоставлено компилятору. Вы же, как программист, должны только помнить общую выполняемую операцию. При использовании полиморфизма несколько имен были сокращены до одного. Хотя этот пример довольно прост, но если расширить концепцию, то можно увидеть, как перегрузка может помочь вам управлять большей сложностью.
Когда вы перегружаете метод, каждая версия этого метода может выполнять любое действие, какое вы пожелаете. Нет никакого правила, заявляющего, как перегруженные методы должны быть связаны друг с другом. Однако, со стилистической точки зрения, перегрузка метода подразумевает некоторую их взаимосвязь. Таким образом, хотя можно использовать то же самое имя,
чтобы перезагрузить несвязанные методы, но этого делать не нужно. Например, можно использовать имя sqr, чтобы создать методы, возвращающие квадрат целого числа и квадратный корень числа с плавающей точкой. Но эти две операции совершенно различны. Такой способ применения перегрузки метода противоречит его первоначальной цели. Практически, следует перегружать только тесно связанные операции.
