Определение собственных классов

From AsIsWiki
Revision as of 10:06, 4 April 2015 by Alex (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
Форум

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


Contents

Класс Employee

Простейшее определение класса в Java имеет следующий вид:

class ИмяКласса {
    конструктор_1
    конструктор_2
    ...
    метод_1
    метод_2
    ...
    поле_1
    поле_2
    ...
}

В данном стиле сначала указываются методы, а в конце - поля класса. Это сделано для привлечения основного внимания к интерфейсу, а не к реализации класса.

Класс Employee предназначен для создания платежной ведомости:

import java.util.*;

public class EmployeeTest {

    // Constructor
    public static void main(String[] args) {

        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = 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 {

    // Constructor
    public Employee(String n, double s, int year, int month, int day) {
        name = n;
        salary = s;
        // GregorianCalendar uses 0 for January
        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);

        hireDay = calendar.getTime();
    }

    // Methods
    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public Date getHireDay() {
        return hireDay;
    }

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

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

Программа состоит из двух классов: Employee и EmployeeTest. Класс EmployeeTest объявлен как public и содержит метод main().
Из всего множества классов, используемых в программе, только в одном из них должен быть метод main().

Исходный текст программы находится в файле EmployeeTest.java (имя файла должно совпадать с именем общедоступного класса).
В исходнике может быть только один public-класс, и любое количество прочих классов, в объявлении которых public отсутствует.

Компилятор создает два файла классов: EmployeeTest.class и Employee.class.

Программа стартует, когда интерпретатор получает имя класса, содержащего метод main():

java EmployeeTest

В результате выполнения метода main(), создается три новых объекта Employee, и отображается их состояние.


Использование нескольких исходных файлов

Приведенная выше программа содержит два класса в одном исходном файле. Многие программисты предпочитают помещать каждый класс в отдельный файл. Следуя этой логике можно разнести классы по двум файлам: Employee.java и EmployeeTest.java.

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

javac Employee*.java

Можно запустить компиляцию public-класса, содержащего метод main():

javac EmployeeTest.java

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

В компиляторе Java реализованы возможности утилиты make.


Анализ класса Employee

В классе Employee реализованы один конструктор и четыре метода:

public Employee(String n, double s, int year, int month, int day)

public String getName()

public double getSalary()

public Date getHireDay()

public void raiseSalary(double byPercent)

Все методы объявлены как public, т.е. доступ к ним открыт из любого класса.

В классе есть три поля для хранения данных:

private String name;

private double salary;

private Date hireDay;

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

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

Поля name и hireDay являются ссылками на экземпляры классов String и Date. Это довольно распространенное явление: классы часто содержат экземпляры других классов.


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

Рассмотрим конструктор класса Employee:

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

Имя конструктора совпадает с именем класса. Конструктор срабатывает при создании объекта Employee, заполняя поля экземпляра заданными значениями:

new Employee("James Bond", 100000, 1950, 1, 1);

в результате поля экземпляра будут заполнены так:

name = "James Bond"
salary = 100000;
hireDay = January 1, 1950;

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

james.Employee("James Bond", 250000, 1950, 1, 1);  // Error!

Обобщая информацию о конструкторах, можно отметить следующее:

  • Имя конструктора совпадает с именем класса.
  • Класс может иметь несколько конструкторов.
  • Конструктор может иметь один или несколько параметров либо не иметь их совсем.
  • Конструктор не возвращает никакого значения.
  • Конструктор всегда вызывается совместно с оператором new.

Конструкторы Java и C++ работают подобно. Однако, все объекты в Java размещаются в динамической памяти и конструкторы вызываются только вместе с оператором new. Программисты, имеющие опыт работы на C++, часто допускают такую ошибку:

Employee number007("James Bond", 10000, 1950, 1, 1);  // C++

Это выражение работает в C++, а в Java - нет.

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

public Employee(String n, double s, ... ) {
    String name = n;    // Error!
    double salary = s;  // Error!
}

В конструкторе объявляются локальные переменные name и salary. Доступ к ним возможен только внутри конструктора. Они маскируют поля класса с аналогичными именами.


Явные и неявные параметры

Методы класса имеют доступ ко всем его полям. Рассмотрим следующий пример:

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

Метод устанавливает новое значение поля salary, но ничего не возвращает:

number007.raiseSalary(5);

Вызов данного метода увеличивает значение поля salary в объекте number007 на 5%. При этом выполняется два выражения:

double raise = number007.salary * 5 / 100;

number007.salary += raise;

Метод raiseSalary имеет два параметра:

  • Неявный - объект типа Employee, который указывается перед именем метода.
  • Явный - число, указанное в скобках после имени метода.

Явные параметры перечисляются в объявлении метода, например: double byPercent. Неявный параметр в объявлении метода не приводится.

В каждом методе ключевое слово this ссылается на неявный параметр. Например, метод raiseSalary может выглядеть так:

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

Такой стиль позволяет более четко различать поля экземпляра и локальные переменные.

В C++ методы определяются вне класса:

void Employee::raiseSalary(double byPercent) {
    // C++
}

Если определить метод внутри класса, он автоматически станет подставляемым (inline):

class Employee {
    int getName() {  // подставляемая функция в C++
        return name;
    } 
}

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


Преимущества инкапсуляции

Рассмотри методы getName(), getSalary() и getHireDay():

public String getName() {
    return name;
}

public double getSalary() {
    return salary;
}

public Date getHireDay() {
    return hireDay;
}

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

В данном примере, поле name доступно только для чтения. После инициализации конструктором, ни один метод не сможет изменить значения этого поля. Значит есть гарантия, что информация этого поля не будет искажена.

Поле salary допускает запись, но изменить его значение может только метод raiseSalary(). Если окажется, что поле содержит неверное значение, то отладке подлежит только один метод. Если бы поле было открытым, причина ошибки могла бы находиться где угодно.

Если требуется читать и модифицировать содержимое поля, то в классе необходимо реализовать следующие элементы:

private type fieldName  // закрытое поле данных

public type getName()   // общедоступный метод доступа

public type setName()   // общедоступный модифицирующий метод

Это сложнее, чем просто открыть доступ к полю, но при этом программист получает существенные преимущества:

  1. Внутреннюю реализацию класса можно изменять независимо от других классов.
    Предположим, что имя и фамилия сотрудника хранятся отдельно:
    String firstName;
    String lastName;
    

    Тогда метод getName должен формировать возвращаемое значение так:

    firstName + " " + lastName
    

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

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

При создании методов доступа, возвращающих ссылки на изменяемый объект, следует быть крайне аккуратным. В классе Employee это правило нарушено: метод getHireDay() возвращает объект Date:

class Employee {
    ...
    public Date getHire() {
        return hireDay;
    }
    ...
    private Date hireDay;
}

Это не соответствует принципу инкапсуляции. Пример проблемного кода:

Employee harry = ... ;

Date d = harry.getHireDay();

double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;

d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);  // значение объекта harry изменено

Обе ссылки d и harry.hireDay относятся к одному и тому же объекту. Применение модифицирующего метода к объекту d автоматически изменят состояние объекта harry.

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

Исправленный код:

class Employee {
    ...
    public Date getHireDay() {
        return (Date) hireDay.clone();
    }
    ...
}

Метод clone() используется для копирования изменяемого поля данных.


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

Метод имеет доступ к любым данным объекта, которому он принадлежит. Кроме того, он может обращаться к закрытым данным всех объектов своего класса. Рассмотрим метод equals, сравнивающий между собой два экземпляра класса Employee:

class Employee {
    ...
    boolean equals(Employee other) {
        return name.equals(other.name);
    }
}

Вызов этого метода выглядит так:

if (harry.equals(boss)) ...

Метод equals имеет доступ к закрытым полям как объекта harry, так и объекта boss. Это вполне объяснимо, поскольку оба объекта относятся к одному классу Employee.

В C++ действует аналогичное правило: метод имеет доступ к переменным и функциям любого объекта своего класса.


Закрытые методы

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

Совершенно не обязательно сообщать пользователям о существовании закрытых методов. Использовать их будет только разработчик класса. А вот открытые методы следует декларировать, т.к. другие части программы могут к ним обращаться.


Неизменяемые поля экземпляра

Поля экземпляра можно объявить с помощью ключевого слова final. Такое поле инициализируется при создании объекта, далее его значение изменить нельзя. Например, поле name класса Employee можно объявить неизменяемым, так как после создания объекта оно никогда не изменяется - метода setName() не существует:

class Employee {
    ...
    private final String name;
}

Модификатор final удобно применять при объявлении полей простых типов, либо полей, типы которых задаются неизменяемыми классами. Класс, методы которого не позволяют изменить состояние объекта, называется неизменяемым. Класс String является примером неизменяемого класса. Если класс допускает изменения, то ключевое слово final может стать источником недоразумений, например:

private final Date hireDate;

Выражение означает, что переменная hireDate не изменяется после создания объекта. Но это не означает, что состояние объекта, на который ссылается переменная, остается неизменным. В любой момент можно вызвать метод setTime().



Форум

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

Personal tools
Namespaces

Variants
Actions
Navigation
Tools