Skip to content

Latest commit

 

History

History
1597 lines (1346 loc) · 123 KB

c6.md

File metadata and controls

1597 lines (1346 loc) · 123 KB

Глава 6

Дополнительные сведения о методах и классах

Основные навыки и понятия

  • Управление доступом к членам классов
  • Передача объектов при вызове методов
  • Возврат объектов из методов
  • Перегрузка методов
  • Перегрузка конструкторов
  • Применение рекурсии
  • Использование ключевого слова static
  • Применение внутренних классов
  • Использование аргументов переменной длины

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

Управление доступом к членам класса

Поддержка свойства инкапсуляции в классе дает два главных преимущества. Во-первых, класс связывает данные с кодом. Это преимущество использовалось в предыдущих примерах программ, начиная с главы 4. И во-вторых, класс предоставляет средства для управления доступом к его членам. Именно эта, вторая преимущественная особенность и будет рассмотрена ниже.

В языке Java, по существу, имеются два типа членов класса: открытые (public) и закрытые (private), хотя в действительности дело обстоит немного сложнее. Доступ к открытому члену свободно осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рассматривавшихся до сих пор примерах программ. А закрытый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организуется управление доступом.

Ограничение доступа к членам класса является основополагающей частью объектно-ориентированного программирования, поскольку оно позволяет исключить неверное использование объекта. Разрешая доступ к закрытым данным только с помощью строго определенного ряда методов, можно предупредить присваивание неверных значений этим данным, выполняя, например, проверку диапазона представления чисел. Для закрытого члена класса нельзя задать значение непосредственно в коде за пределами класса. Но в то же время можно полностью управлять тем, как и когда данные используются в объекте. Следовательно, правильно реализованный класс образует некий “черный ящик”, которым можно пользоваться, но внутренний механизм «его действия закрыт для вмешательства извне.

В рассмотренных ранее примерах программ не уделялось никакого внимания управлению доступом, поскольку в Java члены класса по умолчанию доступны из остальных частей программы. (Иными словами, они открыты для доступа по умолчанию.) Это удобно для создания небольших программ (в том числе и тех, что служат примерами в данной книге), но недопустимо в большинстве реальных условий эксплуатации программного обеспечения. Ниже будет показано, какими языковыми средствами Java можно пользоваться для управления доступом.

Модификаторы доступа в Java

Управление доступом к членам класса в Java осуществляется с помощью трех модификаторов доступа (называемых также спецификаторами): public, private и protected. Если модификатор не указан, то принимается тип доступа по умолчанию. В этой главе будут рассмотрены модификаторы public и private. Модификатор protected непосредственно связан с наследованием, и поэтому он будет обсуждаться в главе 8.

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

Если все классы в программе относятся к одному пакету, то отсутствие модификатора доступа равнозначно указанию модификатора public по умолчанию. Пакет представляет собой группу классов, предназначенных как для организации классов, так и для управления доступом. Рассмотрение пакетов откладывается до главы 8, а для примеров программ, представленных в этой и предыдущих главах, тип доступа по умолчанию не отличается от public.

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

public String errMsg;
private accountBalance bal;

private boolean isError(byte status) { // ...

Для того чтобы стал понятнее эффект от применения модификаторов доступа public и private, рассмотрим следующий пример программы:

// Открытый и закрытый доступ к членам класса,
class MyClass {
    private int alpha; // закрытый доступ
    public int beta; // открытый доступ
    int gamma; // тип доступа по умолчанию (по существу, открытый)

    /* Методы доступа к переменной alpha. Члены класса могут
       обращаться к закрытым членам того же класса.
    */
    void setAlpha(int а) {
        alpha = а;
    }

    int getAlpha() {
        return alpha;
    }
}

class AccessDemo {
    public static void main(String args[])  {
        MyClass ob = new MyClass();

        /* Доступ к переменной alpha возможен только с помощью
           специально предназначенных для этой цели методов. */
        ob.setAlpha(-99);
        System.out.println("ob.alpha is " + ob.getAlpha());

        // Обратиться к переменной alpha так, как показано ниже, нельзя.
        // ob.alpha =10; // Ошибка! alpha - закрытая переменная!

        // Следующие обращения вполне допустимы, так как
        // переменные beta и gamma являются открытыми,
        ob.beta = 88;
        ob.gamma = 99;
    }
}

Нетрудно заметить, что в классе MyClass переменная alpha определена как private, переменная beta — как public, а перед переменной gamma модификатор доступа отсутствует, т.е. в данном примере она ведет себя как открытый член класса, которому по умолчанию присваивается модификатор доступа public. Переменная alpha закрыта, и поэтому к ней невозможно обратиться за пределами ее класса. Следовательно, в классе AccessDemo нельзя пользоваться переменной alpha непосредственно. Доступ к ней организуется с помощью открытых методов доступа setAlpha() и getAlpha(), определенных в одном с ней классе. Если удалить комментарии в начале следующей строки кода, то скомпилировать рассматриваемую здесь программу не удастся:

// ob.alpha = 10; // Ошибка! alpha - закрытая переменная!

Компилятор выдаст сообщение об ошибке, связанной с нарушением правил доступа. Несмотря на то что переменная alpha недоступна для кода за пределами класса MyClass, пользоваться ею можно с помощью открытых методов доступа setAlpha() и getAlpha().

Таким образом, закрытые переменные могут быть свободно использованы другими членами класса, но недоступны за пределами этого класса.

Рассмотрим практическое применение средств управления доступом на примере приведенной ниже программы. Во время ее выполнения предотвращается возникновение ошибок нарушения границ отказоустойчивого целочисленного массива. Это достигается следующим образом. Массив объявляется как закрытый член класса, а доступ к нему осуществляется с помощью специально предназначенных для этой цели методов. Эти методы отслеживают попытки обращения к элементам, не входящим в массив, и вместо аварийной остановки программы возвращают сообщение об ошибке. Массив определяется в классе FailSof tArray, код которого приведен ниже.

/* В этом классе реализуется отказоустойчивый массив,
   предотвращающий ошибки при выполнении программы.
*/
class FailSoftArray {
    private int a[];    //  Ссылка на массив.
    private int errval; //  Значение, возвращаемое в    том случае, если
                        // в методе get() будет обнаружена ошибка,
    public int length;  //  Переменная length   открыта.

    /* Конструктору данного класса передается   размер  массива и   значение,
       которое должен возвращать метод get() при обнаружении ошибки. */
    public FailSoftArray(int size, int errv) {
        a = new int[size];
        errval = errv;
        length = size;
    }

    // возвратить значение элемента массива по заданному индексу
    public int get(int index) {
        // Отслеживание попытки обращения за границы массива.
        if(ok(index)) return a[index];
        return errval;
    }

    // установить значение элемента no заданному индексу,
    // если возникнет ошибка, возвратить логическое значение false
    public boolean put(int index, int val) {
        // Отслеживание попытки обращения эа границы массива.
        if(ok(index)) {
            a[index] = val;
            return true;
        }
        return false;
    }

    // возвратить логическое значение true, если индекс
    // не выходит за границы массива
    private boolean ok(int index) {
        if(index >= 0 & index < length) return true;
        return false;
    }
}
// продемонстрировать обращение к отказоустойчивому массиву
class FSDemo {
    public static void main(String args[]) {
        FailSoftArray fs = new FailSoftArray(5, -1);
        int x;
        // выявить скрытые сбои при обращении к массиву
        System.out.println("Fail quietly.") ;
        for(int i=0; i < (fs.length * 2); i++)
            // Доступ к массиву должен осуществляться с помощью
            // специально предназначенных для этого методов,
            fs.put(i, i*10);

        for (int i=0; i < (fs.length * 2); i++) {
            // Доступ к массиву должен осуществляться с помощью
            // специально предназначенных для этого методов.
            х = fs.get (i);
            if(x != -1) System.out.print(x + " ");
        }

        System.out.println ("") ;

        // а теперь обработать сбои и вывести сообщения об ошибках
        System.out.println("\nFail with error reports.");
        for (int i=0; i < (fs.length * 2); i++)
            if (!fs.put(i, i*10))
                System.out.println("Index " + i + " out-of-bounds");

        for(int i=0; i < (fs.length * 2); i++)  {
            x = fs.get (i);
            if(x != -1) System.out.print(x + " ") ;
            else
                System.out.println("Index " + i + " out-of-bounds");
        }
    }
}

Выполнение этой программы дает следующий результат:

Fail quietly.
0 10 20 30 40

Fail with error reports.
Index 5 out-of-bounds
Index 6 out-of-bounds
Index 7 out-of-bounds
Index 8 out-of-bounds
Index 9 out-of-bounds
0 10 20 30 40 Index 5 out-of-bounds
Index 6 out-of-bounds
Index 7 out-of-bounds
Index 8 out-of-bounds
Index 9 out-of-bounds

Рассмотрим подробнее приведенный выше пример программы. В классе Fail So ft Array определены три закрытых члена. Первым из них является перемен¬ ная а, в которой содержится ссылка на массив, предназначенный для хранения данных. Вторым членом является переменная errval, в которой хранится значение, возвращае¬ мое вызывающей части программы в том случае, если вызов метода get() оказывает¬ ся неудачным. И третьим членом является метод ok(), в котором определяется, нахо¬ дится ли индекс в границах массива. Эти три члена могут быть использованы только другими членами класса FailSof tArray. Остальные члены данного класса объявлены открытыми и могут быть вызваны из любой части программы, где используется класс FailSoftArray.

При построении объекта типа FailSof tArray следует указать размер массива и значение, которое должно быть возвращено, если вызов get() окажется неудачным. Ошибочное значение должно отличаться от тех значений, которые могут храниться в массиве. Конкретный массив, обращение к которому осуществляется по ссылке в переменной а, а также ошибочное значение в переменной errval не могут быть непосредственно доступны пользователям построенного объекта типа FailSoftArray, и благодаря этому неправильное их употребление исключается. В частности, пользователь не может непосредственно обратиться к массиву по ссылке в переменной а, указав индекс нужного элемента и не нарушив, возможно, при этом границы массива. Это можно сделать только с помощью методов get() и put().

Метод ok() объявлен как закрытый главным образом для того, чтобы проиллюстрировать управление доступом. Даже если бы он и был открытым, это не представляло бы никакой опасности, поскольку он не видоизменяет объект. Но этот метод используется только членами класса FailSoftArray, поэтому он и объявлен закрытым.

Обратите внимание на то, что переменная экземпляра length открыта. Это согласуется с правилами реализации массивов в Java. Для того чтобы получить данные о длине массива типа FailSoftArray, достаточно прочитать значение переменной экземпляра length.

Для сохранения данных в массиве типа FailSoftArray по указанному индексу вызывается метод put(), тогда как метод get() извлекает содержимое элемента этого массива по заданному индексу. Если указанный индекс оказывается вне границ массива, метбд put() возвращает логическое значение false, а метод get() — значение errval. Ради простоты в большинстве примеров программ, представленных в этой книге, на члены класса будет в основном распространяться тип доступа по умолчанию. Но не следует забывать, что в реальных объектно-ориентированных программах очень важно ограничивать доступ к членам класса, и в особенности к переменным. Как будет показано в главе 7, при использовании наследования роль средств управления доступом еще более возрастает.

Пример для опробования 6.1. Усовершенствование класса Queue

Модификатор доступа private можно использовать для усовершенствования класса Queue, разработанного в примере для опробования 5.2 из главы 5. В текущей версии этого класса используется тип доступа по умолчанию, который, по существу, делает все члены этого класса открытыми. Это означает, что другие классы могут непосредственно обращаться к элементам базового массива — и даже вне очереди. А поскольку назначение класса, реализующего очередь, состоит в том, чтобы обеспечить принцип доступа “первым пришел — первым обслужен”, то возможность произвольного обращения к элементам массива явно неуместна. В частности, это давало бы возможность недобросовестным программистам изменять индексы в переменных putloc и getloc, искажая тем самым организацию очереди. Подобные недостатки нетрудно устранить с помощью модификатора доступа private.

Последовательность действий

  1. Создайте новый файл Queue. j ava.
  2. Добавьте к массиву q, а также к переменным putloc и getloc модификатор доступа private в классе Queue. В результате код этого класса должен выглядеть так, как показано ниже.
    // Усовершенствованный класс очереди, предназначенной
    // для хранения символьных значений,
    class Queue {
        // Следующие члены класса теперь являются закрытыми,
        private char q[]; // Массив для хранения элементов очереди
        private int putloc, getloc; // Индексы размещения и извлечения
                                    // элементов очереди
    
        Queue(int size) {
            q = new char[size+1];   //  выделить память для очереди
            putloc = getloc = 0;
        }
    
        // поместить символ в очередь
        void put(char ch) {
            if(putloc==q.length-1) {
                System.out.println(" - Queue is full.");
                return;
            }
            putloc++;
            q[putloc] = ch;
        }
    
        // извлечь символ из очереди
        char get() {
            if(getloc == putloc) {
                System.out.println(" - Queue is empty.");
                return (char) 0;
            }
            getloc++;
            return q[getloc];
        }
    }
    
  3. Изменение типа доступа к массиву q и переменным putloc и getloc с выбираемого по умолчанию на закрытый (private) никак не скажется на работе тех программ, где класс Queue используется правильно. В частности, этот класс будет по-прежнему взаимодействовать с классом QDemo, созданным в примере для опробования 5.2. В то же время неправильное обращение к классу Queue станет невозможным. Например, следующий фрагмент кода недопустим:
Queue test = new Queue(lO);
test.q[0] =99;  // Ошибка!
test.putloc = -100; //  He пройдет!
  1. Теперь, когда массив q и переменные putloc и getloc стали закрытыми, класс Queue строго следует принципу “первым пришел — первым обслужен”, по которому действует очередь.

Передача объектов методам

В приведенных ранее примерах программ в качестве параметров методам передавались лишь простые типы. Но параметрами могут быть и объекты. Например, в привеское значение true только в том случае, если все три размера обоих параллелепипедов совпадают. А в методе same Volume() сравниваются лишь объемы двух параллелепипедов. Но в обоих случаях параметр ob имеет тип Block. Несмотря на то что Block — это класс, параметры данного типа используются таким же образом, как и параметры встроенных в Java типов данных.

Способы передачи аргументов методу

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

Первым способом является вызов по значению. В этом случае значение аргумента копируется в формальный параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент, используемый для вызова. А вторым способом передачи аргумента является вызов по ссылке. В данном случае параметру метода передается ссылка на аргумент, а не значение аргумента. В методе эта ссылка используется для доступа к конкретному аргументу, указываемому при вызове. Это означает, что изменения, вносимые в параметр, будут оказывать влияние на аргу¬ мент, используемый для вызова метода. Как будет показано далее, в Java используются оба способа. А выбор конкретного способа зависит от того, что именно передается.

Если методу передается простой тип, например int или double, он передается по значению. При этом создается копия аргумента, а то, что происходит с параметром, принимающим аргумент, не распространяется за пределы метода. Рассмотрим в качестве примера следующую программу:

// Простые типы данных передаются методам по значению,
class Test {
    /* Этот метод не может изменить значения аргументов,
    передаваемых ему при вызове. */
    void noChange(int i, int j) {
        i   = i + j;
        j = -j;
    }
}
class CallByValue {
    public static void main (String args.[]) {
        Test ob = new Test();

        int a = 15, b = 20;

        System.out.println("a and b before call: " +
                            a + " " + b);

        ob.noChange(a, b);

        System.out.println("a and b after call: " +
                            a + " " + b);
    }
}

Ниже приведен результат выполнения данной программы.

a and b before call: 15 20
a and b after call: 15 20

Нетрудно заметить, что действия, выполняемые в теле метода noChange(), не оказывают никакого влияния на значения переменных а и b в вызывающем методе.

Если методу передается объект, то ситуация меняется коренным образом, поскольку объекты передаются неявно по ссылке. Не следует забывать, что создание переменной типа класса, по существу, означает формирование ссылки на объект этого класса. И методу на самом деле передается только ссылка, а не сам объект. Поэтому при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается аргумент. Это означает, что и аргумент, и параметр ссылается на один и тот же объект и что объекты, по существу, передаются методам по ссылке. Таким образом, объект в методе будет оказывать влияние на объект, используемый в качестве аргумента. Для примера рассмотрим следующую программу:

// Объекты передаются методам по ссылке,
class Test {
    int a, b;

    Test(int i, int j) {
        a = i;
        b = j;
    }

    /* Передача объекта методу. Теперь переменные ob.a b и ob.b
    из передаваемого объекта можно изменить. */
    void change(Test ob) {
    ob.a = ob.a + ob.b;
    ob.b = -ob.b;
    }
}
class CallByRef {
    public static void main(String args[]) {
        Test ob = new Test(15, 20);
        System.out.println("ob.a and ob.b before call: " +
                            ob.a + " " + ob.b);
        ob.change(ob);
        System.out.println("ob.a and ob.b after call: " +
                            ob.a + " " + ob.b);
    }
}

Выполнение этой программы дает следующий результат:

ob.a and ob.b before call: 15 20
ob.a and ob.b after call: 35 -20

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

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

Возврат объектов

Метод может возвращать данные любого типа, включая и типы классов. Например, объект приведенного ниже класса ErrorMsg может быть использован для сообщения об ошибке. В этом классе имеется метод getErrorMsg(), который возвращает объект типа String, описывающий ошибку. Объект типа String строится на основании кода ошибки, переданного методу.

// Возврат объекта типа String,
class ErrorMsg {
    String msgs[] = {
        "Output Error",
        "Input Error",
        "Disk Full",
        "Index Out-Of-Bounds"
    };

    // возвратить объект типа String в виде сообщения об ошибке
    String getErrorMsg(int i) {
        if(i >=0 & i < msgs.length)
            return msgs[i];
        else
            return "Invalid Error Code";
    }
}
class ErrMsg {
    public static void main(String args[]) {
        ErrorMsg err = new ErrorMsg();

        System.out.println(err.getErrorMsg(2));
        System.out.println(err.getErrorMsg(19));
    }
}

Выполнение этой программы дает следующий результат:

Disk Full
Invalid Error Code

Разумеется, возвращать можно и объекты создаваемых классов. Например, приведенный ниже фрагмент кода представляет собой переработанную версию предыдущей программы, где создаются два класса формирования ошибок Err и Error Inf о. В классе Err, помимо кода ошибки, инкапсулируется символьная строка описания ошибки. А в классе Errorlnf о содержится метод getErrorlnf о (), возвращающий объект типа Err.

// Возврат объекта, определяемого разработчиком программы,
class Err {
    String msg; // Сообщение об ошибке
    int severity; // Код, определяющий серьезность ошибки

    Err(String m, int s) {
        msg = m;
        severity = s;
    }
}

class Errorlnfo {
    String msgs[] = {
        "Output Error",
        "Input Error",
        "Disk Full",
        "Index Out-Of-Bounds"
    };
    int howbad[] = { 3, 3, 2, 4 };

    // Возврат объекта типа Err.
    Err getErrorlnfo(int i) {
        if(i >=0 & i < msgs.length)
            return new Err(msgs[i], howbad[i]);
        else
            return new Err("Invalid Error Code", 0) ;
    }
}

class Errlnfo {
    public static void main(String args[]) {
        Errorlnfo err = new Errorlnfo();
        Err e;

        e = err.getErrorlnfo (2);
        System.out.println(e.msg + " severity: " + e.severity);

        e = err.getErrorInfo.(19) ;
        System.out.println(e.msg + " severity: " + e.severity);
    }
}
Disk Full severity: 2
Invalid Error Code severity: 0

При каждом вызове метода getErrorlnfo() создается новый объект типа Err и ссылка на него возвращается вызывающему методу. Этот объект затем используется методом main() для отображения кода серьезности ошибки и текстового сообщения.

Объект, возвращенный методом, существует до тех пор, пока на него имеется хотя бы одна ссылка. Если ссылки на объект отсутствуют, он уничтожается системой “сборки мусора”. Поэтому при выполнении программы не возникает ситуация, когда объект разрушается лишь потому, что метод, в котором он был создан, завершился.

Перегрузка методов

В этом разделе речь пойдет об одном из самых интересных языковых средств Java — перегрузке методов. Несколько методов одного класса могут иметь одно и то же имя, отличаясь лишь набором параметров. Перегрузка методов является одним из способов реализации принципа полиморфизма в Java.

Для того чтобы перегрузить метод, достаточно объявить его новый вариант, отличающийся от уже существующих, а все остальное сделает компилятор. Нужно лишь соблюсти одно условие: тип и/или число параметров в каждом из перегружаемых методов должны быть разными. Некоторые считают, что два метода могут отличаться лишь типом возвращаемого значения, но это заблуждение. Возвращаемый тип не предоставляет достаточных сведений для принятия решения о том, какой именно метод должен быть использован. Конечно, перегружаемые методы могут иметь разные возвращаемые типы, но при вызове метода выполняется лишь тот его вариант, в котором параметры соответствуют передаваемым аргументам.

Ниже приведен простой пример программы, демонстрирующий перегрузку методов.

// Перегрузка методов,
class Overload {
    // Первый вариант метода.
    void ovlDemo() {
        System.out.println("No parameters");
    }

    // перегрузить метод ovlDemo с одним параметром типа int.
    // Второй вариант метода.
    void ovlDemo(int а) {
        System.out.println("One parameter: " + a);
    }

    // перегрузить метод ovlDemo с двумя параметрами типа int.
    // Третий вариант метода.
    int ovlDemo(int a, int b) {
        System.out.println("Two parameters: " + a + " " + b) ;
        return a + b;
    }

    // перегрузить метод ovlDemo с двумя параметрами типа double.
    // Четвертый вариант метода.
    double ovlDemo(double a, double b) {
        System.out.println("Two double parameters: " +
                            a + " "+ b);
        return a + b;
    }
}

class OverloadDemo {
    public static void main(String args[])  {
        Overload ob = new Overload();
        int resl;
        double resD;

        // вызвать все варианты метода ovlDemo()
        ob.ovlDemo();
        System.out.println();

        ob.ovlDemo(2) ;
        System.out.println();

        resl = ob.ovlDemo(4, 6) ;
        System.out.println("Result of ob.ovlDemo(4, 6): " +
                            resl);
        System.out.println();

        resD = ob.ovlDemo(1.1, 2.32);
        System.out.println("Result of ob.ovlDemo(1.1, 2.32): " +
                            resD);
    }
}

Как видите, метод ovlDemo() перегружается четырежды. В первом его варианте параметры не предусмотрены, во втором — определен один целочисленный параметр, в третьем — два целочисленных параметра, в четвертом — два параметра типа double. Обратите внимание на то, что первые два варианта метода ovlDemo() имеют тип void, а два другие возвращают значение. Как пояснялось ранее, тип возвращаемого значения не учитывается при перегрузке методов. Следовательно, попытка определить два варианта метода ovlDemo() так, как показано ниже, приводит к ошибке.

// Возможен лишь один вариант метода ovlDemo (int).
// Возвращаемое значение нельзя использовать
// для различения перегружаемых методов.
void ovlDemo(int а) {
    System.out.println("One parameter: " + a);
}
/* Ошибка! Два варианта метода ovlDemo(int) не могут существовать,
   даже если типы возвращаемых ими значений отличаются.
*/
int ovlDemo(int а) {
    System.out.println("One parameter: " + a);
    return a * a;
}

Как поясняется в комментариях к приведенному выше фрагменту кода, отличия возвращаемых типов недостаточно для перегрузки методов.

Как следует из главы 2, в Java производится автоматическое приведение типов. Это приведение распространяется и на типы параметров перегружаемых методов. В качестве примера рассмотрим следующий фрагмент кода:

/* Автоматическое преобразование типов может оказывать влияние
   на выбор перегружаемого метода.
*/
class 0verload2 {
    void f(int x) {
        System.out.println("Inside f(int): " + x) ;
    }

    void f(double x) {
        System.out.println("Inside f(double): " + x) ;
    }
}

class TypeConv {
    public static void main(String args[])  {
        overload2 ob = new 0verload2();

        int i = 10;
        double d = 10.1;
        byte b = 99;
        short s = 10;
        float f = 11.5F;

        ob.f(i); // Вызов метода оb.f(int)
        ob.f(d); // Вызов метода ob.f(double)
        ob.f(b); // Вызов метода oh.f(int) с преобразованием типов
        ob.f(s); // Вызов метода ob.f(int) с преобразованием типов
        ob.f(f); // Вызов метода ob.f(double) с преобразованием типов
    }
}

Выполнение этого фрагмента кода дает следующий результат:

Inside f (int) : 10
Inside f(double): 10.1
Inside f (int): 99
Inside f(int): 10
Inside f(double): 11.5

В данном примере определены только два варианта метода f(): один принимает параметр типа int, а второй — параметр типа double. Но передать методу f() можно также значение типа byte, short или float. Значения типа byte и short исполняющая система Java автоматически преобразует в тип int. В результате будет вызван вариант метода f (int). А если параметр имеет значение типа float, то оно преобразуется в тип double и далее вызывается вариант метода f (double).

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

// Добавление варианта метода f(byte).
class 0verload2 {
    void f(byte x) {
        System.out.println("Inside f(byte): " + x) ;
    }

    void f(int x) {
        System.out.println("Inside f(int) : " + x);
    }

    void f(double x) {
        System.out.printlnpinside f(double): " + x);
    }
}

class TypeConv {
    public static void main(String args[]) {
        0verload2 ob = new 0verload2();

        int i = 10;
        double d = 10.1;

        byte b = 99;
        short s = 10;
        float f = 11.5F;

        ob.f(i); // Вызов метода ob.f(int)
        ob.f(d); // Вызов метода ob.f(double)

        ob.f(b); // Вызов метода ob.f(byte) без преобразования типов

        ob.f(s) ; // Вызов метода ob.f (int) с преобразованием (типов
        ob.f(f) ; // Вызов метода ob.f(double) с преобразованием типов
    }
}

Выполнение этой версии программы дает следующий результат:

Inside f(int): 10
Inside f(double): 10.1
Inside f(byte): 99
Inside f(int): 10
Inside f(double): 11.5

В данной версии программы используется вариант метода f() с параметром типа byte. Так, если при вызове метода f() ему передается значение типа byte, вызывается вариант метода f (byte) и автоматическое приведение к типу int не производится.

Перегрузка методов представляет собой механизм воплощения полиморфизма, т.е. способ реализации в Java принципа “один интерфейс — множество методов”. Для того чтобы стёбю понятнее, как и для чего это делается, необходимо принять во внимание следующее соображение: в языках программирования, не поддерживающих перегрузку методов, каждый метод должен иметь уникальное имя. Но в ряде случаев требуется выполнять одну и ту же последовательность операций над разными типами данных. В качестве примера рассмотрим функцию, определяющую абсолютное значение. В языках, не поддерживающих перегрузку методов, приходится создавать три или более варианта данной функции с именами, отличающимися хотя бы одним символом. Например, в языке С функция abs() возвращает абсолютное значение числа типа int, функция labs() — абсолютное значение числа типа long, а функция f abs() применяется к значению с плавающей точкой. А поскольку в С не поддерживается перегрузка, то каждая из функций должна иметь свое собственное имя, несмотря на то, что все они выполняют одинаковые действия. Это приводит к неоправданному усложнению процесса написания программ. Разработчику приходится не только представлять себе действия, выполняемые функциями, но и помнить все три их имени. Такая ситуация не возникает в Java, потому что все методы, вычисляющие абсолютное значение, имеют одно и то же имя. В стандартной библиотеке Java для вычисления абсолютного значения предусмотрен метод abs(). Его перегрузка осуществляется в классе Math для обработки значений всех числовых типов. Решение о том, какой именно вариант метода abs() должен быть вызван, исполняющая система Java принимает, исходя из типа аргумента.

Главная ценность перегрузки заключается в том, что она обеспечивает доступ к связанным вместе методам по общему имени. Следовательно, имя abs обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с более сложными ситуациями в программировании.

Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не существует какого-то твердого правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имя sqr можно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции. Такое применение перегрузки методов противоречит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции.

Перегрузка конструкторов

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

// Демонстрация перегрузки конструкторов,
class MyClass {
    int х;

    // Конструкторы перегружаются разными способами.
    MyClass()   {
        System.out.println("Inside MyClass().");
        x = 0 ;
    }

    MyClass(int i) {
        System.out.println("Inside MyClass(int) . ") ;
        x = i;
    }

    MyClass(double d) {
        System.out.println("Inside MyClass(double).");
        x = (int) d;
    }

    MyClass(int i, int j) {
        System.out.println("Inside MyClass(int, int).");
        x = i * j;
    }
}

class OverloadConsDemo {
    public static void main(String args[]) {
        MyClass tl  =   new MyClass();
        MyClass t2  =   new MyClass(88);
        MyClass t3  =   new MyClass(17.23);
        MyClass t4  =   new MyClass(2,  4);

        System.out.println("tl.x: " + tl.x);
        System.out.println("t2.x: " + t2.x);
        System.out.println("t3.x: " + t3.x);
        System.out.println("t4.x: " + t4.x);
    }
}

В результате выполнения этой программы получается следующий результат:

Inside MyClass().
Inside MyClass(int).
Inside MyClass(double).
Inside MyClass(int, int).
tl.x: 0
t2.x: 88
t3.x: 17
t4.x: 8

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

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

// Инициализация одного объекта посредством другого,
class Summation {
    int sum;

    // построить объект из целочисленного значения
    Summation(int num) {
        sum = 0;
        for(int i=l; i <= num; i++)
        sum += i;
    }

    // Построение одного объекта иэ другого.
    Summation(Summation ob) {
        sum = ob.sum;
    }
}

class SumDemo {
    public static void main(String args[]) {
        Summation si = new Summation(5);
        Summation s2 = new Summation(si);

        System.out.println("si.sum: " + si.sum);
        System.out.println("s2.sum: " + s2.sum);
    }
}

Выполнение этой программы дает следующий результат:

si.sum: 15
s2.sum: 15

Как следует из приведенного выше примера, использование одного объекта при инициализации другого нередко оказывается вполне оправданным. В данном случае при конструировании объекта s2 нет необходимости вычислять сумму. Даже если подобная инициализация не повышает быстродействие программы, зачастую удобно иметь конструктор, создающий копию объекта.

Пример для опробования 6.2. Перегрузка конструктора класса Queue

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

Последовательность действий

  1. Создайте новый файл QDemo2 . j ava и скопируйте в него код класса Queue, созданный в примере для опробования 6.1.

  2. Добавьте сначала в этот класс приведенный ниже конструктор, который будет строить одну очередь на основании другой.

    // Конструктор, строящий один объект типа Queue на основании другого.
    Queue(Queue ob) {
        putloc = ob.putloc;
        getloc = ob.getloc;
        q = new char[ob.q.length];
    
        // копировать элементы очереди
        for(int i=getloc+l; i <= putloc; i++)
            q[i] = ob.q[i];
    }
    

    Внимательно проанализируем этот конструктор. Сначала переменные putloc и getloc инициализируются в нем значениями, содержащимися в объекте ob, который передается ему в качестве параметра. Затем в нем организуется новый массив для хранения элементов очереди, которые далее копируются из объекта ob в этот массив. Вновь созданная копия очереди будет идентична оригиналу, хотя они и являются совершенно отдельными объектами.

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

    // Конструирование и инициализация объекта типа Queue.
    Queue(char а [ ])   {
        putloc = 0;
        getloc = 0;
        q = new char[a.length+1];
    
        for(int i = 0; i < a.length; i++) put(a[i]);
    }
    

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

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

    // Класс, реализующий очередь для хранения символов,
    class Queue {
        private char q[]; // Массив для хранения элементов очереди,
        private int putloc, getloc; // Индексы размещения и извлечения
                                    // элементов очереди.
        // сконструировать пустую очередь заданного размера
        Queue(int size) {
            q = new char[size+1]; // выделить память для очереди
            putloc = getloc = 0;
        }
    
        // сконструировать очередь из существующего объекта типа Queue
        Queue(Queue ob) {
            putloc = ob.putloc;
            getloc = ob.getloc;
            q = new char[ob.q.length];
    
            // копировать элементы в очередь
            for (int i=getloc+l; i <= putloc; i++)
                q[i] = ob.q[i];
        }
    
        // сконструировать очередь из массива исходных значений
        Queue(char а[]) {
            putloc = 0;
            getloc = 0;
            q = new char[a.length+1];
    
            for(int i = 0; i < a.length; i++) put(a[i]);
        }
    
        // поместить символ в очередь
        void put(char ch) {
            if(putloc==q.length-1) {
                System.out.println(" - Queue is full.");
                return;
            }
    
            putloc++;
            q[putloc] = ch;
        }
    
        // извлечь символ из очереди
        char get()  {
            if(getloc == putloc) {
                System.out.println(" - Queue is empty.");
                return (char) 0;
            }
    
            getloc++;
            return q[getloc];
        }
    }
    
    // продемонстрировать класс Queue в действии
    class QDemo2 {
        public static void main(String args[]) {
            // построить пустую очередь для хранения 10 элементов
            Queue ql = new Queue(10);
            char name[] = {'Т', 'o', 'm'};
            // построить очередь из массива
            Queue q2 = new Queue(name);
    
            char ch;
            int i;
    
            // поместить ряд символов в очередь ql
            for(i=0; i < 10; i++)
                ql.put((char) ('A1 + i));
    
            // построить одну очередь из другой очереди
            Queue q3 = new Queue(ql);
    
            // показать очереди
            System.out.print("Contents of ql: ");
            for(i=0; i < 10; i++)   {
                ch = ql.get();
                System.out.print(ch);
            }
    
            System.out.println("\n");
    
            System.out.print("Contents of q2: ");
            for(i=0; i < 3; i++) {
                ch = q2.get();
                System.out.print(ch);
            }
    
            System.out.println("\n");
    
            System.out.print("Contents of q3: ");
            for(i=0; i < 10; i++)   {
                ch = q3.get();
                System.out.print(ch);
            }
        }
    }
    

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

    Contents of ql: ABCDEFGHIJ
    
    Contents of q2: Tom
    
    Contents of q3: ABCDEFGHIJ
    

Рекурсия

В Java допускается, чтобы метод вызывал самого себя. Этот процесс называется рекурсией, а метод, вызывающий самого себя, — рекурсивным. Вообще говоря, рекурсия представляет собой процесс, в ходе которого нечто определяет самое себя. В этом отношении она чем-то напоминает циклическое определение. Рекурсивный метод отличается в основном тем, что он содержит оператор, в котором этот метод вызывает самого себя. Рекурсия является эффективным механизмом управления программой.

Классическим примером рекурсии служит вычисление факториала числа. Факториал числа N представляет собой произведение всех целых чисел от 1 до N. Например, факториал числа 3 равен 1x2x3, или 6. В приведенном ниже примере программы демонстрируется рекурсивный способ вычисления факториала числа. Для сравнения в эту программу включен также нерекурсивный вариант вычисления факториала числа.

// Простой пример рекурсии,
class Factorial {
    // Рекурсивный метод,
    int factR(int n) {
        int result;

        if(n==l) return 1;
        // Рекурсивный вызов метода factRO .
        result = factR(n-l) * n;
        return result;
    }

    // Вариант программы, вычисляющий факториал итеративным способом,
    int factl(int n) {
        int t, result;

        result = 1;
        for(t=l; t <= n; t++) result *= t;
        return result;
    }
}

class Recursion {
    public static void main(String args[])  {
        Factorial f = new Factorial();

        System.out.println("Factorials using recursive method.");
        System.out.println("Factorial of    3   is  "   +   f.factR(3));
        System.out.println("Factorial of    4   is  "   +   f.factR(4));
        System.out.println("Factorial of    5   is  "   +   f.factR(5));
        System.out.println();

        System.out.println("Factorials using iterative method.");
        System.out.println("Factorial of    3   is  "   +   f.factl(3));
        System.out.println("Factorial of    4   is  "   +   f.factl(4));
        System.out.println("Factorial of    5   is  "   +   f.factl(5));
    }
}

Ниже приведен результат выполнения данной программы.

Factorials using recursive method.
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120

Factorials using iterative method.
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120

Действия нерекурсивного метода fact I() не требуют особых пояснений. В нем используется цикл, в котором числа, начиная с 1, последовательно умножаются друг на друга, постепенно образуя произведение, дающее факториал.

Рекурсивный метод f actR() действует несколько сложнее. Когда метод factR() вызывается с аргументом, равным 1, он возвращает 1, а иначе —произведение, определяемое из выражения factR(n-l)*n. Для вычисления этого выражения вызывается метод factR() с аргументом п-1. Этот процесс повторяется до тех пор, пока значение переменной п не окажется равным 1, после чего из предыдущих вызовов данного метода начнут возвращаться полученные значения. Например, при вычислении факториала 2 первый вызов метода factR() повлечет за собой второй вызов того же самого метода, но с аргументом 1. В результате метод возвратит значение 1, которое затем умножается на 2 (т.е. исходное значение переменной п). В результате всех этих вычислений будет получен факториал, равный 2. По желанию в тело метода factR() можно ввести операторы println(), чтобы сообщать, на каком именно уровне осуществляется очередной вызов, а также отображать промежуточные результаты вычислений.

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

Рекурсивные варианты многих процедур могут выполняться немного медленнее, чем их итерационные эквиваленты, из-за дополнительных затрат системных ресурсов на неоднократные вызовы метода. Если же таких вызовов окажется слишком много, то в конечном итоге может быть переполнен системный стек. А поскольку параметры и локальные переменные рекурсивного метода хранятся в системном стеке и при каждом новом вызове этого метода создается их новая копия, то в какой-то момент стек может оказаться исчерпанным. Если возникнет подобная ситуация, исполняющая система Java сгенерирует исключение. Но в большинстве случаев об этом не стоит особенно беспокоиться. Как правило, переполнение системного стека происходит тогда, когда рекурсивный метод выходит из под контроля.

Главное преимущество рекурсии заключается в том, что она позволяет реализовать некоторые алгоритмы яснее и проще, чем итерационным способом. Например, алгоритм быстрой сортировки довольно трудно реализовать итерационным способом. А некоторые задачи, например искусственного интеллекта, очевидно, требуют именно рекурсивного решения. При написании рекурсивных методов следует непременно указать в соответствующем месте условный оператор, например if, чтобы организовать возврат из метода без рекурсии. В противном случае возврат из вызванного однажды рекурсивного метода может вообще не произойти. Подобного рода ошибка весьма характерна для реализации рекурсии в практике программирования. Поэтому рекомендуется пользоваться операторами, содержащими вызовы метода println(), чтобы следить за происходящим в рекурсивном методе и прервать его выполнение, если в нем обнаружится ошибка.

Применение ключевого слова static

Иногда требуется определить такой член класса, который будет использоваться независимо от всех остальных объектов этого класса. Как правило, доступ к члену класса организуется посредством объекта этого класса, но в то же время можно создать член класса для самостоятельного применения без ссылки на конкретный экземпляр объекта. Для того чтобы создать такой член класса, достаточно указать в самом начале его объявления ключевое слово static. Если член класса объявляется как static, он становится доступным до создания любых объектов своего класса и без ссылки на какой-нибудь объект. С помощью ключевого слова static можно объявлять как переменные, так и методы. Наиболее характерным примером члена типа static служит метод main(), который объявляется таковым потому, что он должен вызываться виртуальной машиной Java в самом начале выполняемой программы.

Для того чтобы воспользоваться членом типа static за пределами класса, достаточно указать имя этого класса с оператором-точкой. Но создавать объект для этого не нужно. В действительности член типа static оказывается доступным не по ссылке на объект, а по имени своего класса. Так, если требуется присвоить значение 10 переменной count типа static, являющейся членом класса Timer, то для этой цели можно воспользоваться следующей строкой кода:

Timer.count = 10;

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

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

// Применение статической переменной,
class StaticDemo {
    int х; // обычная переменная экземпляра
    static int у; // статическая переменная — это одна копия,
                  // совместно используемая всеми объектами.

    // возвратить сумму значений переменной экземпляра х и
    // статической переменной у.
    int sum() {
        return х + у;
    }
}

class SDemo {
    public static void main(String args[]) {
        StaticDemo obi = new StaticDemo();
        StaticDemo ob2 = new StaticDemo();

        // У каждого объекта имеется своя копия переменной экземпляра,
        obl.x = 10;
        ob2.х = 20;
        System.out.println("Of course, obl.x and ob2.x " +
                           "are independent.");
        System.out.println("obi.x: " + obl.x +
                           "\nob2.x: " + ob2.x);
        System.out.println() ;

        // Все объекты совместно пользуются одной общей
        // копией статической переменной.
        System.out.println("The static variable у is shared.");
        StaticDemo.y = 19;
        System.out.println("Set StaticDemo.y to 19.");

        System.out.println("ob1.sum() : " + obl.sum());
        System.out.println("ob2.sum(): " + ob2.sum());
        System.out.println();

        StaticDemo.y = 100;
        System.out.println("Change StaticDemo.y to 100");

        System.out.println("ob1.sum() : " + ob1.sum());
        System.out.println("ob2.sum(): " + ob2.sum());
        System.out.println();
    }
}

Выполнение этой программы дает следующий результат:

Of course, obl.x and ob2.x are independent,
obl.x: 10
ob2.x: 20

The static variable у is shared.
Set StaticDemo.y to 19.
obi.sum(): 29
ob2.sum(): 39

Change StaticDemo.y to 100
obi.sum(): 110
ob2.sum(): 120

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

Метод типа static отличается от обычного метода тем, что его можно вызывать по имени его класса, не создавая экземпляр объекта этого класса. Пример такого вызова уже приводился ранее. Это был метод sqrt() типа static, относящийся к классу Math из стандартной библиотеки классов Java. Ниже приведен пример программы, в которой объявляется статическая переменная и создается метод типа static.

// Применение статического метода,
class StaticMeth {
    static int val = 1024; // статическая переменная

    // Статический метод,
    static int valDiv2() {
        return val/2;
    }

}

class SDemo2 {
    public static void main(String args[]) {

        System.out.println("val is " + StaticMeth.val);
        System.out.println("StaticMeth.valDiv2(): " +
                            StaticMeth.valDiv2());

        StaticMeth.val = 4;
        System.out.println("val is " + StaticMeth.val);
        System.out.println("StaticMeth.valDiv2(): " +
                            StaticMeth.valDiv2());
    }
}

Выполнение этой программы дает следующий результат:

val is 1024
StaticMeth.valDiy2() : 512
val is 4
StaticMeth.valDiv2(): 2

На применение методов типа static накладывается ряд следующих ограничений.

  • В методе типа static допускается непосредственный вызов только других методов типа static.
  • Для метода типа static непосредственно доступными оказываются только другие данные типа static, определенные в его классе.
  • В методе типа static должна отсутствовать ссылка this.

В приведенном ниже классе код статического метода valDivDenom() создан некорректно.

class StaticError {
    int denom =3;   //  обычная переменная экземпляра
    static int val = 1024; // статическая переменная

    /* Ошибка! К нестатическим переменным нельзя обращаться
        из статического метода. */
    static int valDivDenom()    {
        return val/denom; // не подлежит компиляции!
    }
}

Статические блоки

Иногда для подготовки к созданию объектов в классе должны быть выполнены некоторые инициализирующие действия. В частности, может возникнуть потребность установить соединение с удаленным сетевым узлом или задать значения некоторых статических переменных перед тем, как воспользоваться статическими методами класса. Для решения подобных задач в Java предусмотрены статические блоки. Статический блок выполняется при первой загрузке класса, еще до того, как класс будет использован для каких-нибудь других целей. Ниже приведен пример применения статического блока.

// Применение статического блока,
class StaticBlock {
    static double root0f2;
    static double root0f3;

    // Этот блок выполняется при загрузке класса.
    static {
        System.out.println("Inside static block.");
        root0f2 = Math.sqrt(2.0);
        rootOf3 = Math.sqrt(3.0);
    }

    StaticBlock(String msg) {
        System.out.println (msg) ;
    }
}

class SDemo3 {
    public static void main(String args[]) {
        StaticBlock ob = new StaticBlock("Inside Constructor");

        System.out.println("Square root of 2 is " +
                            StaticBlock.rootOf2);
        System.out.println("Square root of 3 is " +
                            StaticBlock.rootOf3) ;
    }
}

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

Inside static block.
Inside Constructor
Square root of 2 is 1.4142135623730951
Square root of 3 is 1.7320508075688772

Как видите, статический блок выполняется еще до того, как будет создан какой-нибудь объект.

Пример для опробования 6.3. Быстрая сортировка

В главе 5 был рассмотрен простой способ так называемой пузырьковой сортировки, а кроме него, вкратце упоминались и более совершенные способы сортировки. В этом проекте предстоит реализовать один из самых лучших способов: быструю сортировку. Алгоритм быстрой сортировки был разработан Ч. Хоаром и назван его именем. На сегодняшний день это самый лучший универсальный алгоритм сортировки. Он не был продемонстрирован в главе 5 лишь потому, что реализовать быструю сортировку лучше всего с помощью рекурсии. В данном проекте будет создана программа для сортировки символьного массива, но демонстрируемый подход может быть применен к сортировке любых объектов.

Быстрая сортировка опирается на принцип разделения. Сначала выбирается опорное значение (так называемый компаранд), и массив разделяется на две части. Все элементы, которые больше или равны разделяемому компаранду, помещаются в одну часть массива, а те элементы, которые меньше компаранда, — в другую часть. Затем процесс рекурсивно повторяется для каждой оставшейся части до тех пор, пока массив не окажется отсортированным. Допустим, имеется массив, содержащий последовательность символов f edacb, а в качестве компаранда выбран символ d. На первом проходе массив будет частично упорядочен следующим образом:

Исходные данные f e d a с b
Проход 1 b с a d e f

Этот процесс повторяется для каждой части: Ьса и def. Как видите, процесс рекурсивен по своей сути, поэтому рекурсивный способ лучше всего подходит для реализации быстрой сортировки.

Компаранд можно выбрать двумя способами: случайно либо вычислив среднее значение части элементов массива. Эффективность сортировки окажется оптимальной в том случае, когда компаранд выбран как раз посредине диапазона значений элементов, содержащихся в массиве, но зачастую выбрать такое значение непросто. Если же выбрать компаранд случайным образом, то вполне возможно, что он окажется на краю диапазона. Но и в этом случае алгоритм быстрой сортировки будет действовать правильно. В том варианте быстрой сортировки, который реализуется в данном проекте, в качестве компаранда выбирается элемент, находящийся посередине массива.

Последовательность действий

  1. Создайте новый файл QSDemo. j ava.
  2. Создайте сначала класс Quicksort, код которого приведен ниже.
    // Пример для опробования 6.3. Простая версия класса Quicksort,
    // реализующего быструю сортировку,
    class Quicksort {
        // организовать вызов конкретного метода быстрой сортировки
        static void qsort(char items[]) {
            qs(items, 0, items.length-1);
        }
    
        // Рекурсивная версия метода быстрой сортировки символов,
        private static void qs(char items[], int left, int right)
        {
            int i, j;
            char x, y;
    
            i = left; j = right;
            x = items[(left+right)/2];
    
            do {
                while((items[i] < x) && (i < right)) i++;
                while((x < items[j]) && (j > left)) j—;
                if(i <= j) {
                    у = items[i];
                    items[i] = items[j];
                    items[j] = y;
                    i++; j—;
                }
            } while (i <= j);
    
            if(left < j) qs(items, left, j);
            if(i < right) qs(items, i, right);
        }
    }
    
    Для упрощения интерфейса в классе Quicksort предоставляется метод qsort(), из которого вызывается метод qs(), фактически выполняющий сортировку. Такой подход позволяет выполнять сортировку, передавая методу лишь имя массива и не осуществляя первоначальное разделение. А поскольку метод qs() используется только в классе, он определяется как private.
  3. Для того чтобы воспользоваться классом Quicksort, достаточно вызвать метод Quicksort. qsort(). Этот метод определен как static, и поэтому для его вызова достаточно указать имя класса, а создавать объект не обязательно. По завершении работы этого метода массив будет отсортирован. Данная версия программы работает только с символьными массивами, но вы можете адаптировать ее для сортировки массивов любого типа.
  4. Ниже приведен весь исходный код программы, демонстрирующей применение класса Quicksort.
    // Пример для опробования 6.3. Простая версия класса Quicksort,
    // реализующего быструю сортировку,
    class Quicksort {
        // организовать вызов конкретного метода быстрой сортировки
        static void qsort(char items[]) {
            qs(items, 0, items.length-1);
        }
    
        // Рекурсивная версия метода быстрой сортировки символов,
        private static void qs(char items[], int left, int right)
        {
            int i, j;
            char x, y;
    
            i = left; j = right;
            x = items[(left+right)/2];
    
            do {
                while((items[i] < x) && (i < right)) i++;
                while((x < items[j]) && (j > left)) j—;
    
                if(i <= j) {
                    у = items[i];
                    items[i] = items[j];
                    items[j] = y;
                    i++; j—;
                }
            } while (i <= j);
    
            if(left < j) qs(items, left, j);
            if(i < right) qs(items, i, right);
        }
    }
    class QSDemo {
        public static void main(String args[]) {
            char a [ ] = { 'd\  'x',    'a',    'r',    'p\ 'j',    'i' };
            int i;
    
            System.out.print("Original array: ");
            for(i=0; i < a.length; i++)
                System.out.print(a[i]) ;
    
            System.out.println();
    
            // отсортировать массив
            Quicksort.qsort(a);
    
            System.out.print("Sorted array: ");
            for(i=0; i < a.length; i++)
                System.out.print(a[i]);
        }
    }
    

Вложенные и внутренние классы

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

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

Существуют два типа вложенных классов. Одни вложенные классы объявляются с помощью модификатора доступа static, а другие — без него. В этой книге будет рассматриваться только нестатический вариант вложенных классов. Классы такого типа называются внутренними. Внутренний класс имеет доступ ко всем переменным и методам внешнего (т.е. объемлющего) класса и может обращаться к ним непосредственно, как и все остальные нестатические члены внешнего класса. Иногда внутренний класс используется для предоставления услуг объемлющему классу. Ниже приведен пример применения внутреннего класса для вычисления различных значений в объемлющем его классе.

// Применение внутреннего класса,
class Outer {
    int nums[];

    Outer(int n[])  {
        nums = n;
    }

    void Analyze() {
        Inner inOb = new Inner();
        System.out.println("Minimum: 11 + inOb.minO);
        System.out.println("Maximum: " + inOb.maxO);
        System.out.println("Average : " + inOb.avgO);
    }

    // Внутренний класс.
    class Inner {
        int min()   {
            int m = nums[0];
            for (int i=l; i < nums.length; i++)
                if(nums[i] < m) m = nums[i];
            return m;
        }

        int max() {
            int m = nums[0];
            for (int i=l; i < r^urns. length; i++)
                if(nums[i] > m) m = nums[i];

            return m;
        }

        int avg() {
            int a = 0;
            for(int i=0; i < nums.length; i++)
                a += nums[i];

            return a / nums.length;
        }
    }
}

class NestedClassDemo {
    public static void main(String args[]) {
        int x[] = { 3, 2, 1, 5, 6, 9, 7, 8 };
        Outer outOb = new Outer(x);

        outOb.Analyze();
    }
}

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

Minimum: 1
Maximum: 9
Average: 5

В данном примере внутренний класс Inner обрабатывает массив nums, являющийся членом класса Outer. Как упоминалось выше, вложенный класс имеет доступ к членам объемлющего класса, и поэтому он может непосредственно обращаться к массиву nums. А вот обратное не справедливо. Так, например, метод analyze() не может непосредственно вызвать метод min(), не создав объект типа Inner.

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

// Применение класса ShowBits в качестве локального,
    public static void main(String args[])  {
    class LocalClassDemo {

        // Внутренний вариант класса ShowBits.
        // Локальный класс, вложенный в метод.
        class ShowBits {
            int numbits;
            ShowBits(int n) {
            numbits = n;
        }

        void show(long val) {
            long mask = 1;

            // сдвинуть влево для установки единицы в нужной позиции
            mask <<= numbits-1;

            int spacer = 0;
            for(; mask != 0; mask >»= 1)    {
                if((val & mask) != 0) System.out.print("1");
                else System, out .pri-nt ("0") ;
                spacer++;

                if((spacer % 8) ==0)  {
                    System.out.print(" ");
                    spacer = 0;
                }
            }
            System.out.println() ;
        }

        }

        for(byte b = 0; b < 10; b++) {
            ShowBits byteval = new ShowBits(8);

            System.out.print(b + " in binary: ");
            byteval.show(b);
        }
    }
}

Выполнение этой программы дает следующий результат:

0 in binary: 00000000
1 in binary: 00000001
2 in binary: 00000010
3 in binary: 00000011
4 in binary: 00000100
5 in binary: 00000101
6 in binary: 00000110
7 in binary: 00000111
8 in binary: 00001000
9 in binary: 00001001

В данном примере класс ShowBits недоступен за пределами метода main(), а следовательно, попытка получить доступ к нему из любого метода, кроме main(), приведет к ошибке.

И последнее замечание: внутренний класс может быть безымянным. Экземпляр безымянного, или анонимного, внутреннего класса создается при объявлении класса с помощью оператора new. Безымянные внутренние классы будут подробнее рассмотрены в главе 15.

Аргументы переменной длины

Иногда оказываются полезными методы, способные принимать переменное число аргументов. Например, методу, устанавливающему соединение с Интернетом, могут понадобиться имя и пароль пользователя, имя файла, протокол и другие параметры. Если при вызове метода какие-нибудь из этих данных не указаны, то должны использоваться значения по умолчанию. В таком случае было бы уместнее передавать только те аргументы, для которых не предусмотрены значения по умолчанию. А для этого требуется метод, поддерживающий список аргументов переменной, не фиксированной длины.

Раньше для поддержки переменного числа аргументов применялись два способа, причем оба были далеки от совершенства. Первый способ состоял в следующем: если максимальное число аргументов невелико и известно заранее, то можно создать разные варианты одного метода. Очевидно, что такой подход применим лишь в отдельных случаях. Если же значений оказывалось слишком много или их максимальное количество не было известно заранее, то применялся второй способ: параметры помещались в массив, который и передавался методу. Обоим этим способам присущи определенные недостатки, и со временем стало ясно, что требуется какой-то другой, более совершенный подход к решению данной задачи.

В версии JDK 5 появились языковые средства Java, упрощающие создание методов, которым требуется переменное число аргументов. Эти средства называются аргументами переменной длины, а метод, принимающий переменное число аргументов,— методом переменной арности, или же методом с аргументами переменной длины. Список параметров, соответствующих аргументам переменной длины, имеет не фиксированную, а переменную длину. Поэтому метод с аргументами переменной длины может принимать произвольно изменяющееся число аргументов.

Общие положения об аргументах переменной длины

Для указания на то, что метод может принимать переменное число аргументов, в его объявление включается многоточие (...)• Ниже приведен пример метода vaTest(), принимающего переменное число аргументов.

// Метод vaTest() с аргументами переменной длины.
// Объявление списка аргументов переменной длины.
static void vaTest(int ... v) {
    System.out.println("Number of args: " + v.length);
    System.out.println("Contents: ");

    for(int i=0; i < v.length; i++)
        System.out.println(" arg " + i + ": " + v[i]);

    System.out.println();
}

Обратите внимание на следующий синтаксис объявления аргумента v:

int ... v

Этот синтаксис сообщает компилятору, что метод vaTest() может вызываться с указанием произвольного числа аргументов, в том числе и совсем без них. Более того, аргумент v неявно объявляется как массив типа int [ ]. А в теле метода vaTest() доступ к аргументу v осуществляется с помощью обычного синтаксиса обращения к массивам.

Ниже приведен весь исходный код примера программы, демонстрирующего метод vaTest() в действии.

// Демонстрация метода с аргументами переменной длины,
class VarArgs {

    // Метод vaTest() с аргументами переменной длины,
    static void vaTest(int ... v) {
        System.out.println("Number of args: " + v.length);
        System.out.println("Contents: ") ;

        for(int i=0; i < v.length; i++)
            System.out.println(" arg " + i + ": " + v[i]);

        System.out.println();
    }

    public static void main(String args[])
    {
        // Метод vaTest()   может вызываться с переменным числом аргументов.
        vaTest(10); //  1 аргумент
        vaTest(l, 2,    3); //  3 аргумента
        vaTest();   //  без аргументов
    }
}

Выполнение этой программы дает следующий результат:

Number of args: 1
Contents:
arg 0: 10

Number of args: 3
Contents:
arg 0: 1
arg 1: 2
arg 2: 3

Number of args: 0
Contents:

В приведенной выше программе обращает на себя внимание следующее. Во-первых, как пояснялось выше, обращение к аргументу v в методе vaTest() осуществляется как к массиву. Дело в том, что он действительно является массивом. Многоточие в объявлении этого метода указывает компилятору на использование переменного числа аргументов и на необходимость поместить их в массив v. Во-вторых, при обращении к методу vaTest() в методе main() указывается разное число аргументов, включая и вызов данного метода вообще без аргументов. Указываемые аргументы автоматически помещаются в массив v. Если же аргументы не указаны, длина этого массива будет равна нулю.

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

int dolt(int a, int b, double с, int ... vals) {

В данном случае первые три аргумента, передаваемые при вызове метода dolt(), будут соответствовать первым трем параметрам. А остальные аргументы будут считаться относящимися к параметру переменной длины vals.

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

// Использование аргументов переменной длины
// вместе с обычными аргументами,
class VarArgs2 {

    // Здесь msg - обычный аргумент,
    // a v - аргумент переменной длины.
    static void vaTest(String msg, int ... v) {
        System.out.println(msg + v.length);
        System.out.println("Contents: ") ;

        for(int i=0; i < v.length; i++)
            System.out.println(" arg " + i + ": " + v[i]);

        System.out.println();
    }

    public static void main(String args[])
    {
        vaTest("One vararg: ", 10);
        vaTest ("Three varargs: ", 1, 2, 3);
        vaTest("No varargs: ");
    }
}

Выполнение этого фрагмента кода дает следующий результат:

One vararg: 1
Contents:
arg 0: 10

Three varargs: 3
Contents:
arg 0: 1
arg 1: 2
arg 2: 3

No varargs: 0
Contents:

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

int dolt(int a, int b, double с, int ... vals,
boolean stopFlag) { // Ошибка!

В данном примере сделана попытка указать обычный аргумент после аргумента переменной длины.

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

int dolt(int a, int b, double с, int ... vals,
double ... morevals) { // Ошибка!

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

Перегрузка методов с аргументами переменной длины

Если требуется, то метод, принимающий переменное число аргументов, можно перегрузить. Например, в следующей программе трижды перегружается метод vaTest():

// Перегрузка метода с аргументами переменной длины,
class VarArgs3 {
    // Первый вариант метода vaTest().
    static void vaTest(int ... v) {
        System.out.println("vaTest (int ...): " +
                           "Number of args: " + v.length);
        System.out.println("Contents: ") ;

        for(int i=0; i < v.length; i++)
            System.out.println(" arg " + i + ": " + v[i]);

        System.out.println();
    }

    // Второй вариант метода vaTest().
    static void vaTest(boolean ... v) {
        System.out.println("vaTest(boolean ...): " +
                           "Number of args: " + v.length);
        System.out.println("Contents: ") ;

        for(int i=0; i < v.length; i++)
            System.out.println(" arg " + i + ": " + v[i]);

        System.out.println();
    }

    // Третий вариант метода vaTest().
    static void vaTest(String msg, int ... v) {
        System.out.println("vaTest(String, int ...): " +
                            msg + v.length);
        System.out.println("Contents: ") ;
        for (int i=0; i < v.length; i++)
            System.out.println(" arg " + i + ": " + v[i]);

        System.out.println();
    }

    public static void main(String args[])
    {
    vaTest(1, 2, 3) ;
    vaTest("Testing: ", 10, 20);
    vaTest(true, false, false);
    }
}

Выполнение этой программы дает следующий результат:

vaTest(int ...): Number of args: 3
Contents:
arg 0: 1
arg 1: 2
arg 2: 3

vaTest(String, int ...): Testing: 2
Contents:
arg 0: 10
arg 1: 20

vaTest(boolean ...): Number of args: 3
Contents:
atg 0: true
arg 1: false
arg 2: false

В приведенном выше примере программы демонстрируются два способа перегрузки методов с аргументами переменной длины. Во-первых, типы параметров аргументов длины у перегружаемых методов могут отличаться. Это демонстрируют варианты метода vaTest (int . . .) и vaTest (boolean . . .). Напомним: многоточие обозначает, что соответствующий аргумент должен рассматриваться как массив указанного типа. Следовательно, как и при перегрузке обычных методов указываются разные типы параметров, так и перегрузке методов с аргументами переменной длины задаются разные типы подобных аргументов. Исполняющая система Java использует эти данные для правильного выбора вызываемого метода.

Второй способ перегрузки методов с аргументами переменной длины состоит в добавлении обьгчных аргументов. Он реализован в варианте метода vaTest (String, int . . .). В этом случае исполняющая система Java использует для выбора нужного варианта метода данные как о числе параметров, так и об их типах.

Аргументы переменной длины и неоднозначность

При перегрузке методов с аргументами переменной длины может возникнуть довольно неожиданная ошибка. А возникает она вследствие неоднозначности в выборе метода. Рассмотрим в качестве примера следующую программу:

// Перегрузка метода с аргументами переменной длины
//и неоднозначность выбора метода.
//
//В этой программе имеется ошибка, и
// поэтому она не будет компилироваться,
class VarArgs4 {
    // Использование аргумента переменной длины типа int.
    static void vaTest(int ... v) {
        // ...
    }

    // Использование аргумента переменной длины типа boolean.
    static void vaTest(boolean ... v) {
        // ...
    }

    public static void main(String args[])
    {
        vaTest(1, 2, 3); // OK
        vaTest(true, false, false); // OK

        vaTest(); // Ошибка вследствие неоднозначности!
    }
}

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

vaTest(); // Ошибка: неоднозначность вызова!

Переменное число аргументов подразумевает в том числе и нулевое их число, и поэтому приведенный выше вызов может быть интерпретирован и как vaTest (int . . .), и как vaTest (boolean . . .). Оба вызова допустимы, и поэтому обращение к данному методу неоднозначно.

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

static void vaTest(int ... v) { // ...
static void vaTest(int n, int ... v) { // ...

И хотя списки аргументов у обоих вариантов метода vaTest() отличаются, компилятор все равно не может правильно выбрать вариант для следующего вызова:

vaTest (1)

В самом деле, не понятно, нужно ли преобразовать этот вызов в vaTest (int . . .) с одним аргументом переменной длины или же в вызов vaTest (int, int . . .) без аргументов переменной длины? В итоге возникает неоднозначная ситуация.

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

Упражнение для самопроверки по материалу главы 6

  1. Допустим, имеется следующий фрагмент кода:
    class X {
        private int count;
    
    Является ли допустимым приведенный ниже фрагмент кода?
    class Y {
        public static void main(String args[]) {
            X ob = new X();
            ob.count = 10;.
    
  2. Модификатор доступа должен ________ объявлению члена класса.
  3. Помимо очереди, в программах часто используется структура данных, которая называется стеком. Обращение к стеку осуществляется по принципу “первым пришел — последним обслужен44. Стек можно сравнить со стопкой тарелок, стоящих на столе.Последней берется тарелка, поставленная на стол первой. Создайте класс Stack, реализующий стек для хранения символов. Используйте методы push() и pop() для манипулирования содержимым стека. Пользователь класса Stack должен иметь возможность задавать размер стека при его создании. Все члены класса Stack, кроме методов push() и pop(), должны быть объявлены как private. (.Подсказка: в качестве заготовки можете воспользоваться классом Queue, изменив в нем лишь способ доступа к данным.)
  4. Допустим, имеется следующий класс:
    class Test {
        int а;
        Test(int i) { a = i; }
    }
    
    Напишите метод swap(), реализующий обмен содержимым между двумя объектами типа Test, на которые ссылаются две переменные данного типа.
  5. Правильно ли написан следующий фрагмент кода?
    class X {
    int meth(int a, int b) { ... }
    String meth(int a, int b) { ... }
    
  6. Напишите рекурсивный метод, отображающий строку задом наперед.
  7. Допустим, все объекты класса должны совместно пользоваться одной и той же переменной. Как объявить такую переменную?
  8. Для чего может понадобиться статический блок?
  9. Что такое внутренний класс?
  10. Допустим, требуется член класса, к которому могут обращаться только другие члены этого же класса. Какой модификатор доступа следует использовать в его объявлении?
  11. Имя метода и список его параметров вместе составляют ________ метода.
  12. Если методу передается значение типа int, то в этом случае используется передача параметра по __________.
  13. Создайте метод sum() с аргументами переменной длины для суммирования передаваемых ему значений типа int. Метод должен возвращать результат суммирования. Продемонстрируйте его в действии.
  14. Можно ли перегружать метод с аргументами переменной длины?
  15. Приведите пример неоднозначного вызова перегружаемого метода с переменным числом аргументов.