Object: глобальный суперкласс

From AsIsWiki
Jump to: navigation, search
Форум

Назад | Оглавление | Дальше


Contents

Object: глобальный суперкласс

Класс Object является предком для всех классов Java:

class Employee extends Object  // явно отражать факт наследования от Object не обязательно

Если суперкласс явно не указан, то им считается класс Object. Поскольку в Java каждый класс расширяет Object, очень важно знать, какими возможностями обладает сам класс Object.

Переменную типа Object можно использовать в качестве ссылки на объект любого типа:

Object obj = new Employee("Harry Hacker", 35000);

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

Employee e = (Employee) obj;

В Java объектами не являются лишь простые типы: числа, символы и логические значения.

Все массивы, независимо от того, содержатся ли в его элементах объекты или простые типы, являются объектами, расширяющими класс Object.

Employee[] staff = new Employee[10];

obj = staff;  // OK

obj = new int[10];  // OK

В C++ аналогичного глобального базового класса нет, хотя любой указатель можно преобразовать в указатель типа void*.


Метод equals()

Метод equals() класса Object проверяет, эквивалентны ли два объекта. Поскольку метод equals() реализован в классе Object, он определяет лишь, ссылаются ли переменные на один и тот же объект. В качестве проверки по умолчанию эти действия вполне оправданы: всякий объект эквивалентен самому себе. Для некоторых классов большего и не требуется. Например, вряд ли кому-то потребуется анализировать два объекта PrintStream и выяснять, отличаются ли они друг от друга. Однако в ряде случаев эквивалентными должны считаться объекты одного типа, имеющие одинаковые состояния.

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

class Employee {
    
    ...

    public boolean equals(Object otherObject) {
        
        // Быстрая проверка идентичности объектов
        if (this == otherObject) {
            return true;
        }

        // Если явный параметр - null, возвращается значение false
        if (otherObject == null) {
            return false;
        }

        // Если классы не совпадают, они не эквивалентны
        if (getClass() != otherObject.getClass()) {
            return false;
        }

        // Теперь мы знаем, что объект otherObject
        // имеет тип Employee и не является нулевым
        Employee other = (Employee) otherObject;

        // Проверим, хранятся ли в полях объектов идентичные значения
        return name.equals(other.name)
                && salary = other.salary
                && hireDay.equals(other.hireDay);
    }
}

Метод getClass() определяет тип объекта. Чтобы объекты были эквивалентны, они как минимум должны быть объектами одного и того же класса.

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

class Manager extends Employee {
    
    ...

    public boolean equals(Object otherObject) {

        if (!super.equals(otherObject)) {
            return false;
        }

        // При выполнении super.equals() проверяется,
        // принадлежит ли otherObject тому же классу
        Manager other = (Manager) otherObject;

        return bonus == other.bonus;
    }
}


Проверка эквивалентности объектов и наследование

Как должен работать метод equals(), если неявные и явные параметры не принадлежат одному и тому же классу? Рекомендуется составлять код так, чтобы метод equals() возвращал значение false, если классы не совпадают. Однако многие программисты используют следующую проверку:

if (!(otherObject instanceof Employee)) {
    return false;
}

При этом остается возможность, что otherObject принадлежит подклассу. Данный подход может привести к возникновению проблем. Спецификация Java требует, чтобы метод equals() обладал следующими характеристиками:

  • Рефлексивность. Для любой ненулевой ссылки x вызов x.equals(x) должен возвращать true.
  • Симметричность. Для любых ссылок x и y вызов x.equals(y) должен возвращать true только тогда, когда y.equals(x) возвращает true.
  • Транзитивность. Для любых ссылок x, y и z, если вызовы x.equals(y) и y.equals(z) возвращают true, то вызов x.equals(z) возвращает true.
  • Непротиворечивость. Если объекты, на которые ссылались переменные x и y не изменяются, то повторный вызов x.equals(y) должен возвращать то же значение.
  • Для любой ненулевой ссылки x вызов x.equals(null) должен возвращать false.

Например, очевидно, что результаты проверки должны зависеть от того, вызывается ли в программе x.equals(y) или y.equals(x).

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

e.equals(m)

Объект e принадлежит классу Employee, а объект m - классу Manager, причем каждый из них содержит одинаковые имена, зарплату и дату приема на работу. Если не выполнить проверку эквивалентности классов, которым принадлежат объекты m и e, метод вернет true. Однако это значит, что и обратный вызов, m.equals(e), также должен возвращать true, - правило 2 не позволяет ему возвращать false или генерировать исключение.

В результате оказывается, что класс Manager неоправданно тесно связан с другими классами. Если метод equals() используется для сравнения с объектом Employee, он не должен принимать во внимание информацию, отличающую менеджера от обычного сотрудника. В данной ситуации оператор instanceof выглядит достаточно привлекательно.

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

Следует признать, что данный пример слишком специфичен. В данном случае имело бы смысл объявить метод AbstractSet.equals() как терминальный, чтобы невозможно было изменить семантику проверки на эквивалентность множеств. На самом деле при объявлении метода ключевое слово final не указано. Разработчики класса оставили возможность для реализации более эффективного алгоритма проверки на эквивалентность.

Таким образом, вырисовываются два сценария:

  • Если проверка эквивалентности реализована в подклассе, правило симметричности требует использования метода getClass().
  • Если проверка производится средствами суперкласса, можно применять оператор instanceof; при этом становится возможной ситуация, когда два объекта разных классов будут признаны эквивалентными.

В примере с сотрудниками и менеджерами мы считаем два объекта эквивалентными, если их поля совпадают. Если в нашем распоряжении есть два объекта Manager с одинаковыми именами, заработной платой и датой приема на работу, но с различными суммами премии, мы должны признать объекты разными. Следовательно, мы должны использовать проверку с помощью метода getClass().

Теперь предположим, что для проверки эквивалентности используется идентификационный номер сотрудника. Такая проверка имеет смысл для всех подклассов. В этом случае мы можем применять оператор instanceof и объявить метод Employee.equals() как final.

Ниже приведены рекомендации для создания метода equals():

  1. Предположим, что явный параметр называется otherObject, - впоследствии его тип нужно будет привести к типу другой переменной, которую назовем other.
  2. Проверить, идентичны ли ссылки this и otherObject:
    if (this == otherObject) {
        return true;
    }
    

    Это выражение используется для оптимизации проверки. Гораздо быстрее проверить идентичность ссылок, чем сравнивать поля объектов.

  3. Выяснить, является ли ссылка otherObject нулевой (null). Если да, вернуть значение false (делать обязательно!):
    if (otherObject == null) {
        return false;
    }
    

    Сравнить классы this и otherObject. Если семантика проверки может измениться в подклассе, использовать метод getClass():

    if (getClass() != otherObject.getClass()) {
        return false;
    }
    

    Если принцип проверки остается справедливым для всех подклассов, использовать оператор instanceof:

    if (!(otherObject instanceof ClassName)) {
        return false;
    }
    
  4. Преобразовать объект otherObject в переменную требуемого класса:
    Имя_класса other = (Имя_класса) otherObject;
    
  5. Теперь нужно сравнить между собой все поля. Для полей простых типов используется оператор ==, для объектных полей - метод equals(). Если все поля двух объектов совпадают друг с другом, возвращается значение true, в противном случае - значение false:
    return поле_1 == other.поле_1
        && поле_2.equals(other.поле_2)
        && ... ;
    

    Если в подклассе вы переопределяете метод equals(), в него надо включить вызов super.equals(other).

Стандартная библиотека Java содержит более 150 реализаций метода equals(). Некоторые из них включают множество операторов instanceof, вызовов метода getClass() и фрагментов кода, предназначенных для обработки исключения ClassCastException, другие не выполняют практически никаких действий. Создается впечатление, что многие программисты не представляют себе всех особенностей метода equals(). Например, класс Rectangle представляет собой подкласс Rectangle2D. В обоих классах реализован метод equals(), причем для проверки используется оператор instanceof. Сравнивая Rectangle2D с Rectangle при условии равенства координат, мы получим значение true, если же поменяем параметры местами, проверка даст значение false.

Реализуя метод equals(), многие программисты допускают стандартную ошибку. Сможете ли вы сказать, какая проблема возникает при выполнении следующего фрагмента кода:

public class Employee {

    public boolean equals(Employee other) {

        return name.equals(other.name)
                && salary == other.salary
                && hireDay.equals(other.hireDay);
    }

    ...
}

При определении метода явный параметр объявлен как Employee. В результате он не переопределяет метод equals() класса Object.

Начиная с JDK 5.0, появилась возможность застраховаться от возникновения подобной ошибки, специальным образом указывая, что разрабатываемый метод призван заместить соответствующий метод суперкласса. Для этой цели используется дескриптор @Override.

@Override
public boolean equals(Object other)

Если при этом вы случайно определите новый метод, компилятор вернет сообщение об ошибке. Предположим, что в классе Employee присутствует приведенная ниже строка кода.

@Override
public boolean equals(Employee other)

Поскольку данный метод не переопределяет метод, определенный в суперклассе Object, будет обнаружена ошибка.

Дескриптор @Override относится к языковым конструкциям, применяемым для работы с метаданными. Механизм метаданных достаточно универсальный и допускает расширение. С помощью метаданных компилятор получает возможность отслеживать различные действия.


Метод hashCode()

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

Строка Хэш-код
Hello 140207504
Harry 140013338
Hacker 884756206

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

int hash = 0;

for (int i = 0; i < length(); i++) {
    hash = 31 * hash + charAt(i);
}

Метод hashCode() определен в классе Object. Поэтому каждый объект имеет хэш-код, определяемый по умолчанию. В классе Object хэш-код вычисляется на основе адреса памяти, занимаемой объектом. Рассмотрим следующий пример:

String s = "Ok";

StringBuffer sb = new StringBuffer(s);

System.out.println(s.hashCode() + " " + sb.hashCode());

String t = new String("Ok");

StringBuffer tb = new StringBuffer(t);

System.out.println(t.hashCode() + " " + tb.hashCode());

Результаты выполнения данного фрагмента кода приведены в таблице:

Объект Хэш-код
s 3030
sb 20526976
t 3030
tb 20527144

Строкам s и t соответствуют одинаковые хэш-коды, так как они вычисляются на основе содержимого объекта. Для буферов sb и tb хэш-коды различаются. Причина в том, что в классе StringBuffer метод hashCode() не определен и используется метод hashCode() класса Object, который определяет хэш-код по адресу занимаемой памяти.

Если вы переопределяете метод equals(), вам также следует переопределить и метод hashCode(). Он может быть использован при включении объектов в хэш-таблицу.

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

Ниже приведен пример метода hashCode() для класса Employee:

class Employee {

    public int hashCode() {

        return 7 * name.hashCode()
            + 11 * new Double(salary).hashCode()
            + 13 * hireDay.hashCode();
    }
    ...
}

Методы equals() и hashCode() должны быть совместимы: если x.equals(y) возвращает значение true, то результаты выполнения x.hashCode() и y.hashCode() также должны совпадать. Например, если в методе Employee.equals() для определения эквивалентности применяется табельный номер сотрудника, то при вычислении хэш-кода также должен использоваться этот номер, а не имя и не адрес памяти, занимаемый объектом.

Метод toString()

Еще одним важным методом класса Object является toString(), возвращающий значение объекта в виде строки. В качестве примера можно привести метод toString() класса Point. Он возвращает строку, подобную приведенной ниже:

java.awt.Point[x=10,y=20]

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

public String toString() {
    return "Employee[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

Этот метод можно усовершенствовать. Не будем встраивать имя класса в метод toString(), а лишь вызовем метод getClass().getName() и получим строку, содержащую имя класса.

public String toString() {
    return getClass().getName()
        + "[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

Теперь метод toString работает и с подклассами.

Разумеется, программист, создающий подкласс, должен определить свой собственный метод toString() и добавить поля подкласса. Если в суперклассе используется вызов getClass().getName(), подкласс просто вызывает метод super.toString(). Ниже приведен пример метода toString() для класса Manager.

class Manager extends Employee {
    ...
    public String toString() {
        return super.toString()
            + "[bonus=" + bonus
            + "]";
    }
}

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

Manager[name...,salary=...,hireDay=...][bonus=...]

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

Point p = new Point(10, 20);

String message = "The current position is = " + p;  // автоматически вызывает метод p.String()

Вместо x.toString() можно использовать выражение " " + x. Конкатенация пустой строки с представлением объекта x в виде строки эквивалентна вызову метода x.toString(). Такое выражение будет корректным, даже если переменная принадлежит к одному из простых типов.

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

System.out.println(x);

В этом случае метод println() вызовет метод x.toString() и выведет строку результата.

Метод toString(), определенный в классе Object, выводит имя класса и адрес объекта. Рассмотрим следующий вызов:

System.out.println(System.out);

После выполнения метода println() отображается следующая строка:

java.io.PrintStream@2f6684

Как видите, разработчики класса PrintStream не позаботились о переопределении метода toString().

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

System.out.println("Current position = " + position);

В следующих разделах мы рассмотрим еще лучшее решение:

Logger.global.info("Current position = " + position);

Мы настоятельно рекомендуем переопределять метод toString() в каждом создаваемом вами классе. Это будет полезно как вам, так и программистам, использующим результаты вашей работы.

Следующий листинг содержит код методов equals() и toString(), реализованных для классов Employee и Manager:

import java.util.*;

public class EqualsTest {

    public static void main(String[] args) {
        Employee alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
        Employee alice2 = alice1;
        Employee alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
        Employee bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);

        System.out.println("alice1 == alice2: " + (alice1 == alice2));
        System.out.println("alice1 == alice3: " + (alice1 == alice3));
        System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));
        System.out.println("alice1.equals(bob): " + alice1.equals(bob));
        System.out.println("bob.toString(): " + bob);

        Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        System.out.println("boss.toString(): " + boss);
        System.out.println("carl.equals(boss): " + carl.equals(boss));
        System.out.println("alice1.hashCode(): " + alice1.hashCode());
        System.out.println("alice3.hashCode(): " + alice3.hashCode());
        System.out.println("bob.hashCode(): " + bob.hashCode());
        System.out.println("carl.hashCode(): " + carl.hashCode());
    }
}

class Employee {

    public Employee(String n, double s, int year, int month, int day) {
        name = n;
        salary = s;
        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
        hireDay = calendar.getTime();
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public Date getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    public boolean equals(Object otherObject) {
        // a quick test to see if the objects are identical
        if (this == otherObject) {
            return true;
        }

        // must return false if the explicit parameter is null
        if (otherObject == null) {
            return false;
        }

        // if the classes don't match, they can't be equal
        if (getClass() != otherObject.getClass()) {
            return false;
        }

        // now we know otherObject is a non-null Employee
        Employee other = (Employee) otherObject;

        // test whether the fields have identical values
        return name.equals(other.name)
                && salary == other.salary
                && hireDay.equals(other.hireDay);
    }

    public int hashCode() {
        return 7 * name.hashCode()
                + 11 * new Double(salary).hashCode()
                + 13 * hireDay.hashCode();
    }

    public String toString() {
        return getClass().getName()
                + "[name=" + name
                + ",salary=" + salary
                + ",hireDay=" + hireDay
                + "]";
    }

    private String name;
    private double salary;
    private Date hireDay;
}

class Manager extends Employee {

    public Manager(String n, double s, int year, int month, int day) {
        super(n, s, year, month, day);
        bonus = 0;
    }

    public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }

    public void setBonus(double b) {
        bonus = b;
    }

    public boolean equals(Object otherObject) {
        if (!super.equals(otherObject)) {
            return false;
        }
        Manager other = (Manager) otherObject;
        // super.equals checked
        // that this and other belong to the same class
        return bonus == other.bonus;
    }

    public int hashCode() {
        return super.hashCode() + 17 * new Double(bonus).hashCode();
    }

    public String toString() {
        return super.toString() + "[bonus=" + bonus + "]";
    }

    private double bonus;
}


Универсальный класс ArrayList

В Java размер массива можно задавать во время выполнения программы:

int actualSize = ... ;

Employee[] staff = new Employee[actualSize];

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

В JDK 5.0 ArrayList был преобразован в универсальный класс с параметром типа. Тип элемента массива помещается в угловые скобки и добавляется к имени класса.

Списочный массив для хранения объектов Employee выглядит так:

ArrayList<Employee> staff = new ArrayList<Employee>();

До появления JDK 5.0 универсальные классы отсутствовали. Вместо них применялся класс ArrayList, который позволял хранить объекты типа Object. Если вы работаете со старыми версиями Java, не добавляйте к имени ArrayList суффикс в угловых скобках. Его поддерживают только версии JDK, начиная с 5.0. ArrayList можно рассматривать как "низкоуровневый тип".

В старых версиях Java для создания динамических массивов использовался класс Vector. Однако класс ArrayList эффективнее.

Для добавления новых элементов в списочный массив используется метод add(). Ниже показано, как заполнить такой массив объектами Employee:

staff.add(new Employee("Harry Hacker", ... ));

staff.add(new Employee("Tony Tester", ... ));

Списочный массив управляет внутренним массивом ссылок на объекты. Если метод add() вызывается при заполненном внутреннем массиве, то ArrayList автоматически создает массив большего размера и копирует в него все объекты.

Если заранее известно, сколько элементов нужно хранить, то перед заполнением списка массивов следует вызвать метод ensureCapacity():

staff.ensureCapacity(100);

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

Количество элементов, которые будут храниться в списке массивов, можно передать конструктору класса ArrayList в качестве параметра:

ArrayList<Employee> staff = new ArrayList<Employee>(100);

Выделение памяти для списочного и обычного массивов происходит по-разному:

new ArrayList<Employee>(100)  // Емкость списочного массива = 100

new Employee[100]             // Размер массива = 100

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

Метод size() возвращает фактическое количество элементов в списочном массиве.

staff.size()  // возвращает текущее количество элементов в массиве staff

Эквивалентное выражение для определения размера обычного массива a[] выглядит так:

a.length

Если вы уверены, что список массивов будет иметь постоянный размер, можете вызвать метод trimToSize(). Этот метод устанавливает размер блока памяти так, чтобы он точно соответствовал количеству элементов, подлежащих хранению. Механизм сборки мусора предотвращает неэффективное использование памяти, освобождая ее излишки.

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

Класс ArrayList ведет себя так же, как и шаблон vector в C++. И ArrayList, и vector являются универсальными типами. Однако в шаблоне vector происходит перегрузка оператора [], что упрощает доступ к элементам. Поскольку в Java перегрузка операторов не предусмотрена, следует явным образом вызывать методы. Кроме того, шаблон vector в C++ передается по значению. Если a и b представляют собой два вектора, то выражение a = b приведет к созданию нового вектора, длина которого равна b. Все элементы b будут скопированы в a. В результате выполнения того же самого выражения в Java переменные a и b будут ссылаться на один и тот же списочный массив.


Доступ к элементам списочных массивов

Удобство, предоставляемое автоматическим регулированием размера списочного массива, компенсируется более сложным синтаксисом, который требуется для доступа к его элементам. По этой причине класс ArrayList не является частью языка Java; это лишь вспомогательный класс, помещенный в стандартную библиотеку.

Вместо квадратных скобок, используемых для доступа к элементу массива, программисты вынуждены применять методы get() и set().

Например, чтобы задать i-й элемент, используется выражение:

staff.set(i, harry);

При работе с обычным массивом a[] те же действия выполнялись бы следующим образом:

a[i] = harry;

Как в обычных, так и в списочных массивах индексы отсчитываются от нуля.

Получить элемент списочного массива можно с помощью метода get().

Employee e = staff.get(i);

Как известно, при работе с обычными массивами указывается имя массива и индекс в квадратных скобках.

Employee e = a[i];

В JDK 5.0 появилась возможность осуществлять перебор элементов списочных массивов с помощью цикла "for each".

for (Employee e : staff) {
    // выполнение действий с переменной e
}

Ранее цикл, предназначенный для обработки всех элементов списочного массива, выглядел так:

for (int i = 0; i < staff.size(); i++) {
    Employee e = (Employee) staff.get(i);
    // выполнение действий с переменной e
}

До появления JDK 5.0 универсальные классы отсутствовали, и единственным вариантом возвращаемого значения метода get() класса ArrayList была ссылка Object. Очевидно, что в вызывающем методе приходилось выполнять приведение типов.

Employee e = (Employee) staff.get(i);

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

staff.set(i, new Date());

Проблемы возникнут лишь тогда, когда вы извлечете объект и попытаетесь преобразовать его в тип Employee. При использовании ArrayList<Employee> ошибка будет выявлена на этапе компиляции.

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

ArrayList<X> list = new ArrayList<X>();
while ( ... ) {
    x = ... ;
    list.add(x);
}

Затем, используя метод toArray(), скопируйте все элементы в массив.

X[] a = new X[list.size()];

list.toArray(a);

Не вызывайте метод list.set(i, x), если размер списочного массива меньше i. Например, следующий код содержит ошибку:

ArrayList<Employee> list = new ArrayList<Employee>(100);  // емкость равна 100, размер равен нулю

list.set(0, x);  // элемента с номером 0 еще нет

Заполняя массив, используйте метод add(), а метод set() применяйте только для замены уже существующего элемента.

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

int n = staff.size() / 2;

staff.add(n, e);

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

Аналогично можно удалить элемент из середины списочного массива:

Employee e = (Employee) staff.remove(n);

Элементы, расположенные после удаленного элемента, сдвигаются влево, а размер списка массивов уменьшается на единицу.

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

В следующем листинге показана модифицированная программа EmployeeTest, приводимая ранее. Массив Employee[] заменен классом ArrayList. Обратите внимание на следующие особенности:

  • Задавать размер массива не нужно.
  • С помощью метода add() можно добавлять сколько угодно элементов.

Если бы вместо цикла "for each" применялся обычный цикл for, то в листинге также были бы видны еще два отличия, характерные для работы со списочным массивом:

  • Вместо length для подсчета количества элементов применялся бы метод size().
  • Вместо выражения a[i] для доступа к элементу массива использовался бы метод (Employee) a.get(i).

Содержимое файла ArrayListTest.java

import java.util.*;

public class ArrayListTest {

    public static void main(String[] args) {
        // fill the staff array list with three Employee objects
        ArrayList<Employee> staff = new ArrayList<Employee>();

        staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15));
        staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1));
        staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15));

        // raise everyone's salary by 5%
        for (Employee e : staff) {
            e.raiseSalary(5);
        }

        // print out information about all Employee objects
        for (Employee e : staff) {
            System.out.println("name=" + e.getName()
                    + ",salary=" + e.getSalary()
                    + ",hireDay=" + e.getHireDay());
        }
    }
}

class Employee {

    public Employee(String n, double s, int year, int month, int day) {
        name = n;
        salary = s;
        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
        hireDay = calendar.getTime();
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public Date getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    private String name;
    private double salary;
    private Date hireDay;
}


Совместимость между типизированными и обычными ссылочными массивами

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

Предположим, что в программе присутствует следующий код:

public class employeeDB {
    public void update(ArrayList list) { ... }
    public ArrayList find(String query) { ... }
}

Вы можете указать в качестве параметра метда update() типизированный ссылочный массив.

ArrayList<Employee> staff = ... ;

employeeDB.update(staff);

Несмотря на то, что компилятор не считает данное выражение ошибкой и даже не выводит предупреждающее сообщение, такой подход нельзя считать полностью безопасным. Метод update() может добавлять в списочный массив элементы, типы которых отличаются от Employee. При извлечении этих элементов будет сгенерировано исключение. Причины такого поведения программы могут быть непонятны, но следует учитывать, что часть кода разрабатывалась для версий JDK, предшествующих 5.0. Совместимость виртуальной машины Java должна быть обеспечена в первую очередь, потому что, хотя уровень безопасности и не снижается, воспользоваться преимуществом проверки типов на этапе компиляции не удается.

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

ArrayList<Employee> result = employeeDB.find(query);  // будет выведено предупреждающее сообщение

Чтобы увидеть текст предупреждающего сообщения, надо указать при вызове компилятора опцию -Xlint:unchecked.

Попытка выполнить приведение типов не исправит ситуацию.

// на этот раз появится другое предупреждающее сообщение
ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);

На этот раз сообщение информирует о несогласованном приведении типов.

Данная ситуация возникает из-за некоторых ограничений языка Java. Для совместимости, компилятор, после проверки соблюдения правил работы с типами, преобразует типизированные списочные массивы в обычные объекты ArrayList. В процессе выполнения программы все списочные массивы одинаковы: виртуальная машина не получает информации о типах. Поэтому приведения типов (ArrayList) и (ArrayList<Employee>) при работе программы обрабатываются одинаково.

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


Интерфейсные классы и преобразование простых типов

Иногда приходится преобразовывать переменные простых типов в объекты. Все простые типы имеют аналоги в виде классов. Например, существует класс Integer, соответствующий типу int. Такие классы принято называть интерфейсными, или классами оболочки - object wrapper. Они имеют очевидные имена: Integer, Long, Float, Double, Short, Byte, Character, Void и Boolean. Первые шесть классов представляют собой подклассы класса Number. Интерфейсные классы являются терминальными. Таким образом, вы не можете, например, переопределить метод toString() в классе Integer, чтобы отобразить число римскими цифрами. Кроме того, изменить значение, хранящееся в объекте интерфейсного класса, также невозможно.

Предположим, мы хотим, чтобы в списочном массиве хранились целые числа. К сожалению, с помощью параметра в угловых скобках невозможно задать простой тип, например, выражение ArrayList<int> недопустимо. Здесь приходит на помощь интерфейсный класс. Можно объявить списочный массив, предназначенный для хранения объектов Integer:

ArrayList<Integer> list = new ArrayList<Integer>();

При использовании объекта ArrayList<Integer> производительность становится меньше, чем при работе с массивом int[]. Причина очевидна: каждое значение инкапсулировано внутри объекта, и для его записи или извлечения необходимо выполнять дополнительные действия. Таким образом, использование интерфейсных классов оправдано в небольших наборах данных, когда удобство работы программиста важнее эффективности работы программы.

С появлением JDK 5.0 стало проще добавлять элементы в массив и извлекать их. Рассмотрим следующую строку кода:

list.add(3);

Она автоматически преобразуется в выражение:

list.add(new Integer(3));

Подобное автоматическое преобразование выполняется для всех простых типов.

Если вы присвоите объект Integer переменной int, целочисленное значение будет автоматически извлечено из объекта. Другими словами, компилятор преобразует строку кода:

int n = list.get(i);

В выражение:

int n = list.get(i).intValue();

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

Integer n = 3;
n++;

Компилятор автоматически извлечет целочисленное значение из объекта, увеличив его на единицу и снова поместит в объект.

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

Integer a = 1000;
Integer b = 1000;
if (a == b) ...

Почему "вероятнее всего", а не "наверняка"? Дело в том, что в некоторых реализациях Java в качестве часто используемого значения применяется один и тот же объект. В этом случае в ходе проверки будет определено, что объекты идентичны. Такая неоднозначность результатов не может приветствоваться разработчиками. Выход - использовать при сравнении интерфейсных объектов метод equals().

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

Интерфейсные классы были реализованы еще в JDK 1.0, но до появления JDK 5.0 приходилось составлять код для преобразования простых типов в объекты и извлекать значения из объектов вручную.

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

Чтобы преобразовать строку (s) в целое число (x), используется выражение, подобное приведенному ниже:

int x = Integer.parseInt(s);

При этом создавать объект Integer не обязательно; parseInt() представляет собой статический метод.

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

public static void triple(int x) {  // неправильно
    x++;  // увеличивает локальную копию
}

Попробуем использовать вместо типа int класс Integer:

public static void triple(Integer x) {  // неправильно
    ...
}

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

Если все же необходимо создать метод, изменяющий свои числовые параметры, используйте один из вспомогательных типов, определенных в пакете org.omg.CORBA. К этим типам относятся IntHolder, BooleanHolder и др. Каждый такой тип содержит общедоступное (!) поле value, через которое можно обращаться к хранящемуся в нем числу.

public static void triple(IntHolder x) {
    x.value++;
}


Методы с переменным числом параметров

До появления JDK 5.0 число параметров любого Java-метода было фиксировано. Теперь есть возможность создавать методы, позволяющие при разных вызовах задавать различное количество параметров.

С одним из таких методов, printf(), вы уже знакомы:

System.out.printf("%d", n);
System.out.printf("%d %s", n, "widgets");

В обеих строках вызывается один и тот же метод, но в первом случае методу передаются два параметра, а во втором случае - три. Определение метода printf() выглядит следующим образом:

public class PrintStream {
    public PrintStream printf(String fmt, Object... args) {
        return format(fmt, args);
    }
}

Конструкция "Object..." - часть Java-кода. Она указывает на то, что в дополнение к параметру fmt можно указать любое число объектов. Если вызывающий метод передает целочисленное значение или значение одного из простых типов, оно автоматически преобразуется в интерфейсный объект. После этого метод решает непростую задачу разбора строки fmt и связывания спецификаторов формата со значениями agrs[i].

В методе printf() тип параметра Object... означает то же самое, что и Object[].

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

System.out.printf("%d %d", new Object[] {new Integer(d), "widgets"});

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

public static double max(double... values) {
    double largest = Double.MIN_VALUE;
    for (double v : values) {
        if (v > largest) {
            largest = v;
        }
    }
    return largest;
}

Вызов функции может выглядеть следующим образом:

double m = max(3.1, 40.4, -5);

Компилятор передает функции max() выражение new double[] {3.1, 40.4, -5}.

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

System.out.printf("%d %s", new Object[] {new Integer(1), "widgets"});

Таким образом, можно преобразовать существующую функцию, последний параметр которой является массивом, в метод с переменным числом параметров, не изменяя существующего кода. Подобным образом в JDK 5.0 был расширен метод MessageFormat.format().


Справочник

java.lang.Object

int hashCode()  // возвращает хэш-код объекта (положительное или отрицательное целое число)
                // эквивалентным объектам должны соответствовать одинаковые хэш-коды

Class getClass()  // возвращает класс объекта
                  // java поддерживает представление классов, инкапсулированное в классе Class

boolean equals(Object otherObject)  // сравнивает два объекта, и возвращает:
                                    // true - если объекты занимают одну и ту же область памяти
                                    // false - в противном случае
                                    // в собственных классах этот метод следует переопределять

String toString()  // возвращает строку, представляющую значение объекта
                   // в собственных классах этот метод следует переопределять

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


java.lang.Class

String getName()  // возвращает имя класса

Class getSuperclass()  // возвращает имя суперкласса данного класса в виде объекта Class


java.util.ArrayList<T>

// создает пустой списочный массив
ArrayList<T>()  

// создает пустой списочный массив с заданной емкостью initialCapacity
ArrayList<T>(int initialCapacity)

// добавляет элемент в конец массива (всегда возвращает true)
boolean add(T obj)  

// возвращает число элементов массива (не путать с емкостью массива)
int size()  

// гарантирует, что список массивов имеет емкость, достаточную для хранения
// заданного количества элементов без изменения внутреннего массива
// capacity - требуемая емкость списочного массива
void ensureCapacity(int capacity)  

// сокращает емкость списочного массива до его текущего размера
void trimToSize()  

// заменяет элемент по указанному индексу
void set(int index, T obj)  

// извлекает значение с указанным индексом
T get(int index)  

// добавляет элемент по указанном индексу и сдвигает элементы, следующие за ним
void add(int Index, T obj)  

// удаляет элемент по указанном индексу и сдвигает элементы, следующие за ним
// возвращает удаленный элемент
T remove(int index)  

java.lang.Integer

int intValue()  // извлекает из объекта Integer число int (переопределяет метод intValue() класса Number)

static String toString(int i)                // преобразует число в строку

static String toString(int i, int radix)     // преобразует число в строку 

static int parseInt(String s)                // преобразует строку в число

static int parseInt(String s, int radix)     // преобразует строку в число

static Integer valueOf(String s)             // преобразует строку в объект Integer

static Integer valueOf(String s, int radix)  // преобразует строку в объект Integer

Все преобразования выполняются только для целых чисел.
radix - система счисления, в которой представлено целое число.
Если система счисления не указана, то операция проводится с целым числом, представленным в десятичном виде.


java.text.NumberFormat

Number parse(String s)  // преобразует строку в число

s - целое десятичное число, представленное в виде строки



Форум

Назад | Оглавление | Дальше

Personal tools
Namespaces

Variants
Actions
Navigation
Tools