Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
Часть I. Язык в котором значение переменной а на 10 больше значения этой переменной в вызывающем объекте Возвращение объекта class Test { int а i) { a = i; } Test incrByTenO { Test temp = new Test(a+10) return temp; } } class RetOb { public static void main(String a r g s []) { Test obi = new T e s t (2); Test ob2; ob2 = o b i .incrByTen(); System.out.println("obi.a : " + obi.a); System.out.println("ob2.a : " + ob2.a); ob2 = o b 2 а после второго увеличения значения + o b 2 а Эта программа создает следующий вывода 2 оЬ2.а: 12 оЬ2.а после второго увеличения значения Как видите, при каждом вызове метода incrByTen () программа создает новый объект и возвращает ссылку на него вызывающей процедуре. Приведенная программа иллюстрирует еще один важный момент поскольку память для всех объектов резервируется динамически с помощью оператора new, программисту ненужно беспокоиться о том, чтобы объект не вышел за пределы области видимости, так как выполнение метода, в котором он был создан, прекращается. Объект будет существовать до тех пор, пока где-либо в программе будет существовать ссылка на него. При отсутствии какой-либо ссылки на него объект будет уничтожен вовремя следующего сбора “мусора”. Рекурсия В языке Java поддерживается рекурсия Рекурсия — это процесс определения чего-либо в терминах самого себя. Применительно к программированию на языке Java, рекурсия — это атрибут, который позволяет методу вызывать самого себя. Такой метод называют рекурсивным. Классический пример рекурсии — вычисление факториала числа. Факториал числа N — это произведение всех целых чисел от 1 до N. Например, факториал 3 равен 1 х 2 х 3 х, или 6. Вот как можно вычислить факториал, используя рекурсивный метод Глава 7. Более пристальный взгляд на методы и классы 7 5 // Простой пример рекурсии class Factorial { // это рекурсивный метод int fact (int n) { int result; if(n==l) return 1; result = fact(n-l) * n; return result; } } class Recursion { public static void main(String a r g s []) { Factorial f = new Факториал 3 равен " + Факториал 4 равен " + Факториал 5 равен " + Вывод этой программы таков. Факториал 3 равен Факториал 4 равен Факториал 5 равен 12 Для тех, кто незнаком с рекурсивными методами, работа метода f a c t () может быть не совсем понятна. Вот как работает этот метод. При вызове метода f a c t () с аргументом, равным 1, функция возвращает 1. В противном случае она возвращает произведение f a c t (п) п. Для вычисления этого выражения программа вызывает метод f a c t () с аргументом 2. Это приведет к третьему вызову метода с аргументом, равным 1. Затем этот вызов возвратит значение 1, которое будет умножено назначение п во втором вызове метода. Этот результат (равный 2) возвращается исходному вызову метода f a c t () и умножается на 3 (исходное значение п. В результате мы получаем ответ, равный 6. В метод f a c t () можно было бы вставить вызовы метода p r i n t I n (), которые будут отображать уровень каждого вызова и промежуточные результаты. Когда метод вызывает самого себя, новым локальным переменными параметрам выделяется место в стеке и код метода выполняется с этими новыми начальными значениями. При каждом возврате из рекурсивного вызова старые локальные переменные и параметры удаляются из стека, и выполнение продолжается с момента вызова внутри метода. Рекурсивные методы выполняют действия, подобные выдвиганию и складыванию телескопа. Из-за дополнительной перегрузки ресурсов, связанной с дополнительными вызовами функций, рекурсивные версии многих подпрограмм могут выполняться несколько медленнее их итерационных аналогов. Большое количество обращений к методу может вызвать переполнение стека. Поскольку параметры и локальные переменные сохраняются в стеке, а каждый новый вызов создает новые копии этих значений, это может привести к переполнению стека. В этом случае система времени выполнения Java будет передавать исключение. Однако, вероятно, об этом можно не беспокоиться, если только рекурсивная подпрограмма не начинает себя вести странным образом. Основное преимущество применения рекурсивных методов состоит в том, что их можно использовать для создания версий некоторых алгоритмов, которые проще и понятнее их аналогов с использованием итерации. Например, алгоритм бы 1 7 Часть I. Язык строй сортировки достаточно трудно реализовать итерационным методом. Ане которые типы алгоритмов, связанных с искусственным интеллектом, легче всего реализовать именно с помощью рекурсивных решений. При использовании рекурсивных методов нужно позаботиться о том, чтобы где-либо в программе присутствовал оператор i f , осуществляющий возврат из рекурсивного метода без выполнения рекурсивного вызова. В противном случае, будучи вызванным, метод никогда не выполнит возврат. Эта ошибка очень часто встречается при работе с рекурсией. Поэтому вовремя разработки советуем как можно чаще использовать вызовы метода p r i n t I n (), чтобы можно было следить за происходящими прервать выполнение в случае ошибки. Рассмотрим еще один пример рекурсии. Рекурсивный метод p r i n t A r r a y () выводит первые i элементов массива v a l u e s . // Еще один пример рекурсии RecTest { int va l u e s []; RecTest(int i) { values = new int[i]; } // рекурсивное отображение элементов массива void printArray(int i) { if(i==0) return; else printArray(i-1); System.out.println("[" + (i-1) + "] " + values[i-1]); } } class Recursion2 { public static void main(String a r g s []) { RecTest ob = new RecTest(10); int i ; for(i=0; i<10; i++) ob.values[i] = i; o b Эта программа создает следующий вывод 0] об Введение в управление доступом Как вы уже знаете, инкапсуляция связывает данные с манипулирующим ими кодом. Однако инкапсуляция предоставляет еще один важный атрибут управление доступом Инкапсуляция позволяет управлять тем, какие части программы могут Глава 7. Более пристальный взгляд на методы и классы 7 получать доступ к членам класса. Управление доступом позволяет предотвращать злоупотребления. Например, предоставляя доступ к данным только при помощи четко определенного набора методов, можно предотвратить злоупотребление этими данными. Таким образом, если класс реализован правильно, он создает черный ящик, который можно использовать, но внутренний механизм которого защищен от повреждения. Однако представленные ранее классы не полностью соответствуют этой цели. Рассмотрим, например, класс Stack, представленный в конце главы 6. Хотя методы push () и pop () действительно предоставляют управляемый интерфейс стека, этот интерфейс необязателен для использования. То есть другая часть программы может обойти эти методы и обратиться к стеку непосредственно. Понятно, что в плохих руках эта возможность может приводить к проблемам. В этом разделе мы представим механизм, с помощью которого можно строго управлять доступом к различным членам класса. Способ доступа к члену класса определяется модификатором доступа, присутствующим в его объявлении. В языке Java определен обширный набор модификаторов доступа. Некоторые аспекты управления доступом связаны, главным образом, с наследованием и пакетами. (Пакет — это, по сути, группировка классов) Эти составляющие механизма управления доступом Java будут рассмотрены в следующих разделах. А пока начнем с рассмотрения применения управления доступом к отдельному классу. Когда основы управления доступом станут понятны, освоение других аспектов не представит особой сложности. Модификаторами доступа Java являются public открытый, private закрытый) и protected защищенный. Java определяет также уровень доступа, предоставляемый по умолчанию. Модификатор защищенный) применяется только при использовании наследования. Остальные модификаторы доступа описаны далее в этой главе. Начнем с определения модификаторов public и private. Когда к члену класса применен модификатор доступа public, он становится доступным для любого другого кода. Когда член класса указан как private, он доступен только другим членам этого же класса. Теперь вам должно быть понятно, почему методу main () всегда предшествует модификатор public. Этот метод вызывается кодом, расположенным вне данной программы, те. системой времени выполнения Java. При отсутствии модификатора доступа по умолчанию член класса считается открытым внутри своего собственного пакета, но недоступным для кода, расположенного вне этого пакета. (Пакеты рассматриваются в следующей главе.) В уже разработанных нами классах все члены класса использовали режим доступа, определенный по умолчанию, который, по сути, является открытым. Однако, как правило, это не будет соответствовать реальным требованиям. Обычно доступ к данным класса необходимо ограничить, предлагая доступ только через методы. Кроме того, в ряде случаев придется определять закрытые методы класса. М одификатор доступа предшествует остальной спецификации типа члена. То есть оператор объявления члена должен начинаться с модификатора доступа int i; private double j ; private int myMethod(int a, char b) { Чтобы влияние использования открытого и закрытого доступа было понятно, рассмотрим следующую программу Эта программа демонстрирует различие между модификаторами и private. */ 1 7 Часть I. Язык Java class Test { int a; // доступ, определенный по умолчанию int b; // открытый доступ int с // закрытый доступ методы доступа к с setc(int i) { // установка значения переменной с с = i ; } int g e t c () { // получение значения переменной с с AccessTest { public static void main(String a r g s []) { Test ob = new T e s t (); // Эти операторы правильны, аи доступны непосредственно ob.a = 10; ob.b = 20; // Этот оператор неверен и может вызвать ошибку // ob.c = 100; // Ошибка Доступ к объекту с должен осуществляться с // использованием методов его класса с (100); // ОК System.out.println("а, b, и с " + ob.a + " + ob.b + " " + Как видите, внутри класса Test использован метод доступа, заданный по умолчанию, что в данном примере равносильно указанию доступа public. Объект b явно указан как public. Объект с указан как закрытый. Это означает, что он недоступен для кода вне его класса. Поэтому внутри класса AccessTest объект сне может применяться непосредственно. Доступ к нему должен осуществляться с использованием его открытых методов setc ( ) и getc () . Удаление символа комментария изначала строки ob.c = 100; // Ошибка! сделало бы компиляцию этой программы невозможной из-за нарушений правил доступа. В качестве более реального примера применения управления доступом рассмотрим следующую усовершенствованную версию класса Stack, код которого был приведен в конце главы 6. // Этот класс определяет целочисленный стек, который может // содержать 10 значений class Stack { /* Теперь переменные stck и tos являются закрытыми. Это означает что они не могут быть случайно или намеренно изменены так, чтобы повредить стек int stck[] = new i n t [10]; private int tos; // Инициализация верхушки стека Глава 7. Более пристальный взгляд на методы и классы 7 9 Stack() { tos = -1; } // Проталкивание элемента в стек void push(int item) { if (Стек полон = item; } // Выталкивание элемента из стека int p o p () { if(tos < 0) Стек не загружен return 0; } else return Как видите, теперь обе переменные s t c k , содержащая стеки, содержащая индекс верхушки стека, указаны как p r i v a t e . Это означает, что обращение к ним или их изменение могут осуществляться только через методы p u s h () и pop (). Например, указание переменной t o s как закрытой препятствует случайной установке другими частями программы ее значения выходящим за пределы конца массива s t c k Следующая программа — усовершенствованная версия класса S t a c k . Чтобы убедиться в том, что члены класса s t c k n t o s действительно недоступны, попытайтесь удалить символы комментария из строк операторов TestStack { public static void main(String a r g s []) { Stack mystackl = new S t a c k O ; Stack mystack2 = new Stack(); // проталкивание чисел в стек i=0; i<10; i++) mystackl.p u s h (i ); for(int i=10; i<20; i++) mystack2.push(i ); // выталкивание этих чисел из стека Стек в mystackl:"); for(int i=0; i<10; Стек в mystack2:"); for(int i=0; i<10; i++) System.out.println(mystack2.p o p ()); // эти операторы недопустимы // mystackl.tos = -2; // mystack2.s t c k [3] = Обычно методы будут обеспечивать доступ к данным, которые определены классом, но это необязательно. Переменная экземпляра вполне может быть от 1 8 Часть I. Язык крытой, если на то имеются веские причины. Например, для простоты переменные экземпляров в большинстве простых классов, созданных в этой книге, определены как открытые. Однако в большинстве классов, применяемых в реальных программах, манипулирование данными должно будет выполняться только с использованием методов. В следующей главе вернемся к теме управления доступом. Вы убедитесь, что управление доступом особеннр важно при использовании на следования. Что такое s t a t i В некоторых случаях желательно определить член класса, который будет использоваться независимо от любого объекта этого класса. Обычно обращение к члену класса должно осуществляться только в сочетании с объектом его класса. Однако можно создать член класса, который может использоваться самостоятельно, без ссылки на конкретный экземпляр. Чтобы создать такой член, в начало его объявления нужно поместить ключевое слово s t a t i c . Когда член класса объявлен как s t a t i c (статический, он доступен до создания каких-либо объектов его класса и без ссылки на какой-либо объект. Статическими могут быть объявлены как методы, таки переменные. Наиболее распространенный пример статического члена — метод ma i n (). Этот метод объявляют как s t a t i c , поскольку он должен быть объявлен до создания любых объектов. Переменные экземпляров, объявленные как s t a t i c , по существу являются глобальными переменными. При объявлении объектов их класса программа не создает никаких копий статической переменной. Вместо этого все экземпляры класса совместно используют одну и туже статическую переменную. На методы, объявленные как s t a t i c , накладывается ряд ограничений. • Они могут непосредственно вызывать только другие статические методы. • Они могут непосредственно осуществлять доступ только к статическим пе ременным. • Они никоим образом не могут ссылаться на члены типа t h i s или s u p e r . Ключевое слово s u p e r связано с наследованием и описывается в следующей главе.) Если для инициализации статических переменных нужно выполнить вычисления, можно объявить статический блок, который будет выполняться только один раз при первой загрузке класса. В следующем примере показан класс, который содержит статический метод, несколько статических переменных и статический блок инициализации Демонстрация статических переменных, методов и блоков class UseStatic { static int a = 3; static int b; static void meth(int x) { System.out.printIn("x = " + x ) ; System.out.println("a = " + a); System.out.p r intln("b = " + b ) ; } static Статический блок инициализирован Глава 7. Более пристальный взгляд на методы и классы 8 Ь = а * 4; } public static void main(String a r g s []) { m e t h (Сразу после загрузки класса UseStatic программа выполняет все статические операторы. Вначале значение переменной а устанавливается равным 3, затем программа выполняет статический блок, который выводит сообщение, и инициализирует переменную b значением а, или 12. После этого программа вызывает метод main () , который обращается к методу meth () , передавая параметру х значение 42. Три вызова метода println () ссылаются на две статические переменные аи Ь, а также на локальную переменную х. Вывод этой программы таков. Статический блок инициализирован ха За пределами класса, в котором они определены, статические методы и переменные могут использоваться независимо от какого-либо объекта. Для этого достаточно указать имя их класса, за которым должен следовать точечный оператор. Например, если статический метод нужно вызвать извне его класса, это можно сделать используя следующую общую форму. имя класса. метод Здесь имя класса имя класса, в котором объявлен статический метод. Как видите, этот формат аналогичен применяемому для вызова нестатических методов через переменные объектных ссылок. Статическая переменная доступна аналогичным образом — с использованием точечного оператора, следующей за именем класса. Так в языке Java реализованы управляемые версии глобальных методов и переменных. Приведем пример. Внутри метода main () обращение к статическому методу callme () и статической переменной b осуществляется с использованием имени их класса StaticDemo. class StaticDemo { static int a = 42; static int b = 99; static void callme() { System.out.println("a = " + a) ; } } class StaticByName { public static void main(String a r g s []) { StaticDemo.callme(); System.out.println("b = " + StaticDemo.b ) Вывод этой программы выглядит следующим образом. а - 42 b = 99 } 1 8 Часть I. Язык Знакомство с ключевым словом f i n a Поле может быть объявлено как final финальное. Это позволяет предотвратить изменение содержимого переменной, сделав ее, по сути, константой. Это означает, что финальное поле должно быть инициализировано вовремя его объявления. Значение можно также присвоить в пределах конструктора, но первый подход более распространен int FILE_NEW = 1; final int FILE_OPEN = 2; final int FILE_SAVE = 3; final int FILE_SAVEAS = 4; final int FILE_QUIT = Теперь все последующие части программы могут пользоваться переменной FILE_ OPEN и прочими так, как если бы они были константами, без риска изменения их значений. В практике программирования Hajava принято идентификаторы всех финальных полей записывать прописными буквами, как в приведенном выше примере. Кроме полей, как final могут быть объявлены параметры метода и локальные переменные. Объявление параметра как final препятствует его изменению в пределах метода. Объявление как final локальной переменной препятствует присвоению ей значения более одного раза. Ключевое слово f inal можно применять также к методам, нов этом случае его значение существенно отличается от применяемого к переменным. Это дополнительное применение ключевого слова final описано в следующей главе, посвященной наследованию. Повторное рассмотрение массивов Массивы были представлены ранее в этой книге до того, как мы рассмотрели классы. Теперь, имея представление о классах, можно сделать важный вывод относительно массивов все они реализованы как объекты. В связи с этим существует специальный атрибут массива, который наверняка пригодится. В частности, размер массива, те. количество элементов, которые может содержать массив, хранится в его переменной экземпляра length. Все массивы обладают этой переменной, которая всегда будет содержать размер массива. Ниже приведен пример программы, которая демонстрирует это свойство Эта программа демонстрирует член длины массива class Length { public static void main(String a r g s []) { int a l [] = new i n t [10]; int a2[] = {3, 5, 7, 1, 8, 99, 44, -10}; int a3[] = {4, 3, 2, длина al равна " + длина a2 равна " + длина аЗ равна " + Эта программа создает следующий вывод. длина al равна 10 длина а равна 8 длина аЗ равна 4 Глава 7. Более пристальный взгляд на методы и классы 8 Как видите, программа отображает размер каждого массива. Имейте ввиду, что значение переменной l e n g t h никак не связано с количеством действительно используемых элементов. Оно отражает лишь то количество элементов, которое может содержать массив. Член l e n g t h может находить применение во множестве ситуаций. Например, ниже показана усовершенствованная версия класса S t a c k . Как вы, возможно, помните, предшествующие версии этого класса всегда создавали элементный стек. Следующая версия позволяет создавать стеки любого размера. Значение s t c k . l e n g t h служит для предотвращения переполнения стека Усовершенствованный класс Stack, в котором использован // член длины массива class Stack { private int stck[]; private int tos; // резервирование и инициализация стека Stack(int size) { stck = new i n t [size]; tos = -1; } // Проталкивание элемента в стек void push(int item) { if(tos==stck.length-1) // использование члена длины массива Стек полон = item; } // Выталкивание элемента из стека int p o p () { if(tos < 0) Стек не загружен return 0; } else return stck[tos--]; } } class TestStack2 { public static void main(String a r g s []) { Stack mystackl = new S t a c k (5); Stack mystack2 = new Sta c k (8); // проталкивание чисел в стек i=0; i<5; i++) mystackl.p u s h (i ); for(int i=0; i<8; i++) mystack2.p u s h (i ); // выталкивание этих чисел из стека Стек в mystackl:"); for(int i=0; i<5; Стек в mystack2:"); for(int i=0; i<8; i++) System.out.println(mystack2.p o p ()); } } 1 8 Часть I. Язык Обратите внимание на то, что программа создает два стека один глубиной в пять элементов, а второй — в шесть. Как видите, то, что массивы поддерживают информацию о своей длине, упрощает создание стеков любого размера. Представление вложенных и внутренних классов Язык Java позволяет определять класс внутри другого класса. Такие классы называют вложенными классами. Область видимости вложенного класса ограничена областью видимости внешнего класса. Таким образом, если класс В определен внутри класса А, класс Вне может существовать независимо от класса А. Вложенный класс имеет доступ к членам (в том числе закрытым) класса, в который он вложен. Однако внешний класс не имеет доступа к членам вложенного класса. Вложенный класс, который объявлен непосредственно внутри области видимости своего внешнего класса, является его членом. Можно также объявлять вложенные классы, являющиеся локальными для блока. Существует два типа вложенных классов статические и нестатические. Статический вложенный класс — класс, к которому применен модификатор s t a t i c . Поскольку он является статическим, должен обращаться к нестатическим членам своего внешнего класса при помощи объекта. То есть он не может непосредственно ссылаться на нестатические члены своего внешнего класса. Из-за этого ограничения статические вложенные классы используются редко. Наиболее важный тип вложенного класса — внутренний класс. Внутренний класс — это нестатический вложенный класс. Он имеет доступ ко всем переменными методам своего внешнего класса и может непосредственно ссылаться на них также, как это делают остальные нестатические члены внешнего класса. Следующая программа иллюстрирует определение и использование внутреннего класса. Класс Outer содержит одну переменную экземпляра outer_x, один метод экземпляра test ( ) и определяет один внутренний класс Inner. // Демонстрация использования внутреннего класса, class Outer { int oater_x = 100; void t e s t () { Inner inner = new I n ner(); inner.display(); } // это внутренний класс class Inner { void display() t System.o u t .print I n (вывод outer_x = " + outer_x) ; } } } class InnerClassDemo { public static void main(String a r g s []) { Outer outer = new O u t e r (); ou ter.t e s t (); } } Глава 7. Более пристальный взгляд на методы и классы 8 Это приложение создает следующий вывод. вывод: outer_x = В этой программе внутренний класс Inner определен в области видимости класса Outer. Поэтому любой код в классе Inner может непосредственно обращаться к переменной outer_x. Метод экземпляра display () определен внутри класса Inner. Этот метод отображает значение переменной outer_x в стандартном выходном потоке. Метод main () экземпляра InnerClassDemo создает экземпляр класса Outer и вызывает его метод test (). Этот метод создает экземпляр класса Inner и вызывает метод display (Важно понимать, что экземпляр класса Inner может быть создан только внутри области видимости класса Outer. Компилятор Java создает сообщение об ошибке, если любой код вне класса Outer пытается инициализировать класс Inner. В общем случае экземпляр внутреннего класса должен создаваться содержащей его областью. Как уже было сказано, внутренний класс имеет доступ ко всем элементам своего внешнего класса, ноне наоборот. Члены внутреннего класса известны только внутри области видимости внутреннего класса и не могут быть использованы внешним классом Компиляция этой программы будет невозможна class Outer { int outer_x = 100; void t e s t () { Inner inner = new Inner(); inner.display(); } // это внутренний классу у - локальная переменная класса Inner void display() выводу ошибка, здесь переменная у неизвестна (В этом примере переменная у объявлена как переменная экземпляра класса Inner. Поэтому она неизвестна за пределами класса и не может использоваться методом showy (Хотя мы уделили основное внимание внутренним классам, определенным в качестве членов внутри области видимости внешнего класса, внутренние классы можно определять внутри области видимости любого блока. Например, вложенный класс можно определить внутри блока, определенного методом, или даже внутри тела цикла for, как показано в следующем примере 1 8 Часть I. Язык Java // Определение внутреннего класса внутри цикла for. class Outer { int outer_x = 100; void t e s t () { for(int i=0; i<10; i++) { class Inner { void display() вывод outer_x = " + outer_x); } } Inner inner = new Inner(); inner.display(); } } } class InnerClassDemo { public static void main(String a r g s []) { Outer outer = new O u t e r (); outer.t e s t (Вывод, создаваемый этой версией программы, показан ниже. вывод: outer._x = вывод outer._x = вывод = вывод = вывод outer._x = вывод = вывод = вывод = вывод = вывод outer._x = Хотя вложенные классы применимы не во всех ситуациях, они особенно удобны при обработке событий. Мы вернемся к теме вложенных классов в главе 22. В ней представлены внутренние классы, которые можно использовать для упрощения кода, предназначенного для обработки определенных типов событий. Читатели ознакомятся также с анонимными внутренними классами, являющимися внутренними классами без имен. И последнее первоначальная спецификация Java версии 1.0 не допускала использования вложенных классов. Они появились в версии Java Описание класса s t r i n Хотя класс String подробно будет рассмотрен в части II этой книги, здесь уместно кратко ознакомить с ним читателей, поскольку мы будем использовать строки в некоторых последующих примерах части I. Вероятно, String — наиболее часто используемый класс из библиотеки классов Java. Очевидная причина этого в том, что строки — исключительно важный элемент программирования. Во-первых, следует уяснить, что любая создаваемая строка в действительности представляет собой объект класса String. Даже строковые константы в действительности являются объектами класса String. Например, в операторе Глава 7. Более пристальный взгляд на методы и классы 8 Это - также объект строка "Это - также объект String" — объект класса String. Во-вторых, объекты класса String являются неизменяемыми. После того как он создан, его содержимое не может изменяться. Хотя это может показаться серьезным ограничением, на самом деле это не так по двум причинам. • Если нужно изменить строку, всегда можно создать новую строку, содержащую все изменения. • В Java определен класс StringBuffer, равноправный классу String, допускающий изменение строк, что позволяет выполнять в Java все обычные манипуляции строками. (Класс StringBuffer описан в части Существует множество способов создания строк. Простейший из них — воспользоваться оператором вроде следующего myString = "тестовая строка"; Как только объект класса String создан, его можно использовать во всех ситуациях, в которых допустимо использование строк. Например, следующий оператор отображает содержимое объекта Для объектов класса String в Java определен один оператор, +, который служит для объединения двух строк. Например, оператор myString = "Мне" + " нравится " + "приводит к тому, что содержимым переменной myString становится строка "Мне нравится Следующая программа иллюстрирует описанные концепции Демонстрация применения строк class StringDemo { public static void main(String args[]) { String strObl = "Первая строка str0b2 = "Вторая строка str0b3 = strObl + " и " + Эта программа создает следующий вывод. Первая строка Вторая строка Первая строка и вторая строка Класс String содержит несколько методов, которые можно использовать. Опишем некоторые из них. С помощью метода equals () можно проверять равенство двух строк. Метод length () позволяет выяснить длину строки. Вызывая метод charAt () , можно получить символ с указанным индексом. Ниже приведены общие формы этих трех методов equals {втораяСтр) int length() char charAt (индекс) Следующая программа демонстрирует применение этих методов 1 8 Часть I. Язык Java // Демонстрация некоторых методов класса String, class StringDemo2 { public static void main(String a r g s []) { String strObl = "Первая строка strOb2 = "Вторая строка str0b3 = strObl; System.ou Длина strObl: " + Символ с индексом 3 в strObl: " + strObl.charAt(3)); i f (strObl.equals(strOb2)) System.out.println("strObl == str0b2"); else System.out.pr intln("strObl != strOb2"); if(strObl.equals(strOb3)) System.out.pr intln("strObl == strOb3"); else System.out.println("strObl i= Эта программа создает следующий вывод. Длина strObl: Символ с индексом 3 в strObl: s strObl != strOb2 strObl = = Конечно, подобно тому, как могут существовать массивы любого другого типа объектов, могут существовать и массивы строк Демонстрация использования массивов объектов типа String, class St n n g D e m o 3 { public static void main(String a r g s []) { String str[] = { "один, "два, "три" }; for(int i=0; i S y s t e m .out. p r i n t l n ("s t r [" + i + "]: " + s t r [ i ] Вывод этой программы таков t r [0]: один s t r [1]: два s t r [2]: три Как вы убедитесь из следующего раздела, строковые массивы играют важную роль во многих программах Использование аргументов командной строки Иногда необходимо передать определенную информацию программе вовремя ее запуска. Для этого используют аргументы командной строки метода m ain (). Аргумент командной строки — это информация, которую вовремя запуска программы задают в командной строке непосредственно после ее имени. Доступ кар Глава 7. Более пристальный взгляд на методы и классы 8 9 гументам командной строки внутри программы Java не представляет сложности — они хранятся в виде строк в массиве типа String, переданного методу main ( Первый аргумент командной строки хранится в элементе массива args [ 0 ] , второй — в элементе args [ 1 ] и т.д. Например, следующая программа отображает все аргументы командной строки, с которыми она вызывается Отображение всех аргументов командной строки class Comman dLi ne { publi c static voi d ma i n( St rin g a r g s []) { for(int i=0; i < a r g s .length; i + +) S y s t e m . o u t . p r i n t l n (" a r g s [" + i + "]: " + a r g s [ i ] Попытайтесь выполнить эту программу, введя следующую строку, java CommandLine this is a test 100 -1 В результате отобразится следующий вывод r g s [0] this a r g s [1] is a r g s [2] a a r g s [3] test a r g s [4] 100 a r g s Помните Все аргументы командной строки передаются как строки. Численные значения нужно вручную преобразовать в их внутренние представления, как поясняется в главе Список аргументов переменной длины J D K 5 была добавлена новая функциональная возможность, которая упрощает создание методов, принимающих переменное количество аргументов. Это средство получило название vararg (сокращение от variable-length arguments — список аргументов переменной длины. Метод, который принимает переменное количество аргументов, называют методом с переменным количеством аргументов. Ситуации, в которых методу нужно передавать переменное количество аргументов, встречаются не так уж редко. Например, метод, который открывает подключение к Интернету, может принимать имя пользователя, пароль, имя файла, протокол и тому подобное, но применять значения, заданные по умолчанию, если какие-либо из этих сведений опущены. В этой ситуации было бы удобно передавать только те аргументы, для которых заданные по умолчанию значения неприменимы. Еще один пример — метод printf ( ), входящий в состав библиотеки ввода- вывода Java. Как будет показано в главе 19, он принимает переменное количество аргументов, которые форматирует, а затем выводит. До версии J2SE 5 обработка списка аргументов переменной длины могла выполняться двумя способами, ни один из которых не был особенно удобен. Во-первых, если максимальное количество аргументов было небольшими известным, можно было создавать перегруженные версии метода — по одной для каждого возможного способа вызова метода. Хотя этот способ и приемлем, но применим только в редких случаях. Во-вторых, когда максимальное количество возможных аргументов было большим или неизвестным, применялся подход, при котором аргументы сначала помещались в массива затем массив передавался методу. Следующая программа иллюстрирует этот подход 1 9 Часть I. Язык Java // Использование массива для передачи методу переменного // количества аргументов. Это старый стиль подхода // к обработке списка аргументов переменной длины class PassArray { static void vaTest(int v[]) Количество аргументов " + v.length + " Содержимое ") ; for(int x : v) System.out.print(x + " "); System.out.println(); } public static void main(String a r g s []) { // Обратите внимание на способ создания массива для хранения аргументов n l [] = { 10 } ; int п 2 [] = { 1, 2 , 3 }; int п З [] = { } ; vaTest(nl); // 1 аргумент // 3 аргумента // без аргументов } } Эта программа создает следующий вывод. Количество аргументов 1 Содержимое Количество аргументов 3 Содержимое 1 2 Количество аргументов 0 Содержимое: В программе методу v a T e s t () аргументы передаются через массив v. Этот старый подход к обработке списка аргументов переменной длины позволяет методу v a T e s t () принимать любое количество аргументов. Однако он требует, чтобы эти аргументы были вручную помещены в массив до вызова метода v a T e s t (). Создание массива при каждом вызове метода v a T e s t () — задача не только трудоемкая, но и чревата ошибками. Возможность использования методов с переменным количеством аргументов обеспечивает более простой и эффективный подход. Для указания списка аргументов переменной длины используют три точки ( . . .). Например, вот как метод vaTest () можно записать с использованием списка аргументов переменной длины void vaTest(int ... v) Эта синтаксическая конструкция указывает компилятору, что метод vaTest () может вызываться без аргументов или с несколькими аргументами. В результате массив v неявно объявляется как массив типа int [ ] . Таким образом, внутри метода vaTest () доступ к массиву v осуществляется с использованием синтаксиса обычного массива. Предыдущая программа с применением метода с переменным количеством аргументов приобретает следующий вид Демонстрация использования списка аргументов переменной длины class VarArgs { // теперь vaT e s t () использует список аргументов переменной длины static void vaTest(int ... v) Количество аргументов " + v.length + " Содержимое "); for(int x : v) Sy s t em .o ut. pri nt( x + " "); Глава 7. Более пристальный взгляд на методы и классы 9 1 System.out.println(); } public static void main(String a r g s []) { // Обратите внимание на возможные способы вызова // v a Test() с переменным количеством аргументов vaTest(lO); // 1 аргумент a T e s t (1, 2, 3); // 3 аргумента v a T e s t (); // без аргументов } } Вывод этой программы совпадает с выводом исходной версии. Отметим две важные особенности этой программы. Во-первых, как уже было сказано, внутри метода vaTest () переменная v действует как массив. Это обусловлено тем, что переменная v является массивом. Синтаксическая конструкция . . . просто указывает компилятору, что метод будет использовать переменное количество аргументов и что эти аргументы будут храниться в массиве, на который ссылается переменная v. Во-вторых, в методе main () метод vaTest () вызывается с различным количеством аргументов, в том числе и вовсе без аргументов. Аргументы автоматически помещаются в массив и передаются переменной v. В случае отсутствия аргументов длина массива равна нулю. Наряду с параметром с переменным количеством аргументов массив может содержать нормальные параметры. Однако параметр с переменным количеством аргументов должен быть последним параметром, объявленным методом. Например, следующее объявление метода вполне допустимо dolt(int a, int b, double с, int ... vals) В данном случае первые три аргумента, указанные в обращении к методу dolt () , соответствуют первым трем параметрам. Все остальные аргументы считаются принадлежащими параметру Помните, что параметр с переменным количеством аргументов должен быть последним. Например, следующее объявление записано неправильно dolt(int a, int b, double с ... vals, boolean stopFlag) { 11 Ошибка! В этом примере предпринимается попытка объявления обычного параметра после параметра с переменным количеством аргументов, что недопустимо. Существует еще одно ограничение, о котором следует знать метод должен содержать только одни параметр с переменным количеством аргументов. Например, следующее объявление также неверно dolt(int a, int b, double с ... vals, double ... morevals) { // Ошибка! Попытка объявления второго параметра с переменным количеством аргументов недопустима. Рассмотрим измененную версию метода vaTest () , которая принимает обычный аргумент и список аргументов переменной длины Использование списка аргументов переменной длины совместно / / со стандартными аргументами class VarArgs2 { / / В этом примере msg — обычный параметр a v — параметр vararg. 1 9 Часть I. Язык Java static voi d v a T e s t (String msg, int ... v) { S y s t e m .ou t.p r i n t(msg + v .l en gth + " Содержимое r i n t (x + " ") ; S y s t e m .o u t .p r i n t l n () ; } public static vo id mai n( St r in g a r g s []) { v a T e s t ( "Один параметр "Три параметра "Без параметров "Вывод этой программы таков. Один параметр v a r a r g : 1 Содержимое Три параметра v a r a r g : 3 Содержимое Без параметров vararg: 0 Содержимое Перегрузка методов с переменным количеством аргументов Метод, который принимает список аргументов переменной длины, можно перегружать Параметры и перегрузка class VarArgs3 { static vo id v a T e s t ( int ... v) { S y s t e m .ou t.p r i n t("v a T e s t (int . . .) : " +Количество аргументов " + v. len gth + " Содержимое "); for(int x : v) S y s t e m .out.p r i n t(x + " " ) ; S y s t e m .out. p r i n t l n () ; } static void v a T e s t(boole an ... v) { S y s t e m .out. p r i n t("v a T e s t(boo lea n ...) " +Количество аргументов " + v. le ng t h + " Содержимое t(x + " ") ; S y s t e m .out. p r i n t l n (); } static void v a T e s t (String msg, int ... v) { S y s t e m .out. p r i n t("v a T e s t( String, int .. . ): " + msg + v .l en gth + " Содержимое ") ; for(int x : v) Syst em .ou t.p rin t(x + " "); Глава 7. Более пристальный взгляд на методы и классы 9 3 S y s t e m .o u t .p r in t ln ()? } public static void m ain(String a r g s []) { v a T e s t (1, 2 , 3 ) ; v a T e s t ( "Проверка ", 10, 2 0 ); v a T e st(tru e, fa ls e , f a l s e ) Эта программа создает следующий вывод ...): Количество аргументов 3 Содержимое 1 2 3 vaTest(String, int ...): Проверка 2 Содержимое 10 20 vaTest(boolean ...) Количество аргументов 3 Содержимое true false Приведенная программа иллюстрирует два возможных способа перегрузки метода с переменным количеством аргументов. Первый способ — типы его параметра с переменным количеством аргументов могут быть различными. Именно это имеет место в вариантах vaRest (int . . . ) и vaTest (boolean. . . ). Помните, что конструкция . . . вынуждает компилятор обрабатывать параметр как массив указанного типа. Поэтому, подобно тому как можно выполнять перегрузку методов, используя различные типы параметров массива, можно выполнять перегрузку методов с переменным количеством аргументов, используя различные типы списков аргументов переменной длины. В этом случае система Java использует различие в типах для определения нужного варианта перегруженного метода. Второй способ перегрузки метода с переменным количеством аргументов — добавление одного или нескольких обычных параметров. Именно это было сделано для метода vaTest (String , int . . . ) . В данном случае для определения нужного метода система Java использует и количество аргументов, и их тип. На заметку Метод, поддерживающий переменное количество аргументов, может быть перегружен также методом, который не поддерживает эту возможность. Например, в приведенной ранее программе метод vaTest () может быть перегружен методом vaTest (int х ) . Эта специализированная версия вызывается только при наличии аргумента int. В случае передачи методу двух и более аргументов типа int программа будет использовать версию метода vaTest (int. v) с переменным количеством аргументов. Переменное количество аргументов и неопределенность П ри перегрузке метода, принимающего список аргументов переменной длины, могут случаться непредвиденные ошибки. Они связаны с неопределенностью, которая может возникать при вызове перегруженного метода со списком аргументов переменной длины. Например, рассмотрим следующую программу / Список аргументов переменной длины, перегрузка и неопределенность / / / Эта программа содержит ошибку, и ее компиляция / / будет невозможна class VarArgs4 { static void v aT e st(in t . . . v) { S y s t e m .o u t .p r in t ( "v aT es t(in t . . . ) : " +Количество аргументов " + v .le n g th + 1 9 4 Часть I. Язык Java " Содержимое ") ; for(int x : v) System.out.print(x + " "); System.out.println(); } static void vaTest(boolean ... v) { System.out.print("vaTest(boolean ...) " +Количество аргументов " + v.length + " Содержимое "); for(boolean x : v) System.out.print(x + " "); System.out.println(); } public static void main(String a r g s []) { v a T e s t (1, 2 , 3 ) ; / / O K vaTest(true, false, false); // OK va T e s t (); // Ошибка неопределенность! } } В этой программе перегрузка метода vaTest () выполняется вполне корректно. Однако ее компиляция будет невозможна из-за следующего вызова a T e s t (); // Ошибка неопределенность! Поскольку параметр с переменным количеством аргументов может быть пустым, этот вызов может быть преобразован в обращение к методу vaTest (int... ) или vaTest (boolean. . . ) . Оба варианта допустимы. Поэтому вызов принципиально неоднозначен. Рассмотрим еще один пример неопределенности. Следующие перегруженные версии метода vaTest () изначально неоднозначны, несмотря на то что одна из них принимает обычный параметр void vaTest(int ... v) { // ... static void vaTest(int n, int ... v) { // Хотя списки параметров метода vaTest () различны, компилятор не имеет возможности разрешения следующего вызова a T e s t (Должен ли он быть преобразован в обращение к методу vaTest (int. ) с переменным количеством аргументов или в обращение к методу vaTest (int, int . . . ) без переменного количества аргументов Компилятор не имеет возможности ответить на этот вопрос. Таким образом ситуация неоднозначна. Из-за ошибок неопределенности, подобных описанным, в некоторых случаях придется пренебрегать перегрузкой и просто использовать два различных имени метода. Кроме того, в некоторых случаях ошибки неопределенности служат признаком концептуальных изъянов программы, которые можно устранить за счет более тщательного построения решения задачи |