Подробнее о первой программе; строки, массивы, конструкторы

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

C увеличением  размеров программы и числа работающих над ней программистов, увеличивается и количество ошибок, и время, затрачиваемое на поиск каждой. Рассмотрим, например, класс целых чисел Int:

public class Int {
  public int myValue = 0;
  ...
}

Заметим, что программист, использующий этот класс, может беспрепятственно изменить значение поля myValue. В случае с классом целых чисел это, скорее всего, не приведёт к сбою в работе программы, но если мы бы имели дело с более сложным классом, поля которого были бы нетривиально взаимосвязаны друг с другом, это могло бы полностью разрушить структуру класса и привести к весьма печальным последствиям. Хотелось бы оградить создаваемые классы от подобного бесцеремонного вмешательства, любые изменения значений полей объекта данного класса должны производиться только "изнутри", с помощью средств самого класса - методов. Для этой цели существуют модификаторы прав доступа.

Модификатор прав доступа ставится в начале объявления поля, метода или класса. В Java существует четыре таких модификатора, но в данной лекции будут рассмотрены только два: public и private. Модификатор public разрешает доступ к данным как внутри класса, так и вне его, из других классов, а private – доступ только внутри класса. Таким образом, стоит только написать:

public class Int {
    private int myValue = 0;
    int smth;
}

и использование поля myValue везде, кроме методов класса, станет невозможным (компилятор выдаст ошибку).

Следует заметить, что модификатор прав доступа относится только к одному элементу класса, перед которым он поставлен, то есть в нашем примере модификатор private относится только к полю myValue, но не к полю smth. Поле smth по умолчанию не private и не public, на самом деле, оно "почти public", и мы будем считать его public (о том, что же происходит на самом деле, мы поговорим в одной из дальнейших лекций).

Статические поля

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

Для того чтобы задать поле класса как статическое, необходимо в его определении добавить слово static:

public class PiOwner{
    public static double Pi = 3.1415926;
}

Что же такое статическое поле? Статическое поле – это собственность не конкретного объекта, а класса в целом. Это аналог глобальной переменной ( в Java, как известно, глобальных переменных нет). Статические поля создаются в момент загрузки класса, к которому относятся (о том, что это значит, будет рассказано в одной из следующих лекций), и доступны, даже если ни один из объектов данного класса не был создан. При этом они не являются константными, то есть их можно изменять по ходу программы (как сделать поля константами будет рассказано в одной из ближайших лекций).

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

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

PiOwner.Pi

Теперь можно окончательно объяснить строчку:

System.out.println(Float.MIN_VALUE);

Здесь:

Примером статического метода может служить функция Float.isNaN(float v), которая возвращает true, если параметр – не число, и false в противном случае.

Строки

В одной из предыдущих лекций были перечислены 8 стандартных типов в Java. Всё остальное в Java – это классы. Строки в Java – это один из классов стандартной библиотеки.

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

 String s = new String("Hello");

Возможен также более короткий вариант:

 String s = "Hello";

Это несколько отличается от обычного создания объекта. Но так писать тоже можно. Так как при написании программ строки встречаются достаточно часто, для них сделано исключение. Имеет место неявный вызов new (). Java рассчитана на то, что ей будут пользоваться, а не на то, чтобы быть настоящим объектно-ориентированным языком.

Чтобы узнать ещё одну важную особенность строк в Java, рассмотрим следующий пример. При исполнении кода:

 String t = s + "!";

получаем новый объект класса String, на который ссылается t.

Кроме того, если:

 s += "?";

то создаётся очередной новый объект, и на него теперь ссылается s. А что же стало со строкой "Hello", на которую s ссылалась прежде? Если на неё есть другие ссылки, то она продолжает своё существование, а если же ссылок больше нет, то скоро её "съест" garbage collector.

Таким образом, необходимо запомнить одно важное свойство объектов String в Java: их нельзя менять. При любой попытке добавить что-либо к строке создаётся новый объект класса String.

String - ссылочный тип. Если s и t - произвольные ссылки на объекты класса String, то:

 if (s == t) {...}

проверяет, указывают ли ссылки s и t на один и тот же объект. Если же необходимо сравнить сами строки, а не ссылки на них, то следует использовать метод equals():

if (s.equals(t)) {...}

После создания двух объектов:

 String s = "Hello"
 String s1 = "Hel";	
 String t = s1 + "lo";

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

Например, нельзя утверждать, что после создания некоторой строки, s == s.intern(). Всё зависит от того, как и когда эта строка была создана. Но после оператора:

 s = s.intern();

строка точно будет находиться в пуле, а s, очевидно, будет равняться s.intern().

Таким образом, после вызова:

 s = s.intern();
 t = t.intern();

s и t гарантировано указывают на один и тот же объект, размещённый в пуле строк.

Поместив строку в пул, мы достигаем важного преимущества: у равных проинтернированных строк - равные ссылки. Теперь мы имеем возможность сравнивать строки с помощью оператора ==. Сравнение ссылок происходит за константное время, следовательно такое сравнение значительно выгоднее, чем использование метода equals(), в котором сравнение строк происходит за линейное от длины строки время. Кроме того, интернирование экономит память: все одинаковые строки хранятся в одном и том же объекте, помещённом в пул.

Из приведённого выше ясно, что строки можно складывать. Кроме того, что к строке можно прибавить другую строку, к строке можно прибавить практически всё, что угодно. Например:

 int n;
 String s = "";
 s += n;

 s += "" + n;

В двух предыдущих случаях в строку будет записана десятичная запись числа n.

Таким образом, возможен подобный оператор:

 System.out.println(a + "*" + b + "=" + c);

Но, если написать:

 System.out.println(7 + 7 + "==" + 1 + 4);

то в результате на экран выведется 14 == 14, а не 77 == 14.
Кроме того, во избежание ошибок не допускаются присваивания вида:

  s = n; // это не скомпилируется!

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

class Int {
    public int myValue = 0;
    ...
    // заголовок метода должен быть именно таким!
    public String toString() {
        return "" + myValue;
    }
}

Когда в коде встретится:

 String s = "" + n;

или

 System.out.println(n);

то неявно будет вызван именно метод toString(), с помощью которого объект будет преобразован в строку.

Следует заметить, что сложение строк в Java достаточно дорогостоящая операция (одно сложение, нам, конечно, не повредит, а вот если к строке надо будет "приклеить" много кусочков, то на создание при каждом сложении нового объекта может уйти много времени). Так, например, рассмотрим код:

 String s = "";
 for (int i = 0; i < N; i++) {
     s += 'A';
 }

Рассмотрим i-й шаг этого цикла: текущая строка содержит i-1 символ. Создаётся новая строка, в которую копируются i-1 символ из текущей и добавляется один новый символ. Таким образом, новая строка содержит i символов:

Наш цикл на каждой итерации требует создания новой строки, копирования в неё i-1 элемента и добавления одного нового:

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

0 + 1 + 2 + 3 + ... + (N - 1) = N*(N - 1) / 2

копирований символов. Кроме того, на каждом шаге создаётся новый объект (всего N шагов). Следовательно, затрачиваемое время

T = С1 * N2 + C2 * N,

где С2 довольно велико, так как на создание объектов тратится существенное время.

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

 StringBuffer sb = new StringBuffer();
 for (int i = 0; i < N; i++) {
     sb.append (i);
 }
 s = sb.toString();

В случае с использованием буфера время будет пропорционально N, так как на каждой итерации производится единственная операция - добавление элемента в буфер.

Ещё два полезных метода, определённых для класса String:

s.length(); // возвращает длину строки
s.charAt(i); // доступ к i–тому символу строки (отсчёт
с нуля)

Массивы

Массив в языке Java – это тоже класс.

Задание и инициализация массива:

int [] B = new int [100]; // массив ста целых чисел
int [] C = new int [N]; // массив целых чисел длины N
			// если N < 0, произойдёт ошибка при исполнении

int [] D = {1, 2, 3, 4, 5} // массив из пяти элементов
int [] E; 
if (...) { //размер массива определяется истинностью или ложностью условия
	E = new int [10];
} else {
	E = new int [20];
}

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

Однажды заданная, длина массива не может измениться. Её можно получить следующим образом:

A.length;

Следует отметить, что length не метод, а public (позволяет просмотр длины вне класса), final (смысл этого модификатора будет пояснён в ближайших лекциях) поле.

Многомерных массивов в Java нет. Можно задать массивы массивов, например:

int [][] M; // массив массивов целых чисел

Инициализировать массив массивов можно так:

M = new int [10][20]; 

или

M = new int [10][];

for (int i = 0; i < 10; i++){
    M[i] = new int [20];
}

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

M = new int [10][];

for (int i = 0; i < 10; i++){
    M[i] = new int [i+1];
}

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

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

public class Int {
    private int myValue;

    // конструктор
    public Int(int value) {
        myValue = value;
    }
}

Конструкторы не возвращают никакого значения (даже типа void), а название конструктора должно совпадать с названием класса.

Создаваться объекты класса Int будут следующим образом:

Int i = new Int(1000);

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

public class Int {
    private int myValue;

    // конструктор №1
    public Int(int value){
        myValue = value;
    }

    // конструктор №2
    public Int(){
    }
}

...

Int n = new Int(1000); // вызовется первый конструктор

Int m = new Int(); // вызовется второй конструктор

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

Заметим, что во втором конструкторе нигде не присвоено значение полю myValue. Дело в том, что при создании объекта класса его поля инициализируются значениями по умолчанию. Для целочисленных типов (byte, short, int, long) и типов float и double это значение 0, поля типа char иниициализируются символом, код которого 0, а поля логического типа boolean - значением false. О том, как инициализируются поля – объекты, будет рассказано в одной из следующих лекций.