Перечисления. Исключения. Использование обобщений

1 Теоретическая часть

1.1 Перечисления

В Java 1.5 и последующих версиях поддерживается новый тип классов - перечисления. Перечисление задает список возможных значений, которые может получать переменная этого типа. В простейшей своей форме перечисления Java аналогичны соответствующим конструкциям C++ и C#.

  enum DayOfWeek {
      SUNDAY,
      MONDAY,
      TUESDAY,
      WEDNESDAY,
      THURSDAY,
      FRIDAY,
      SATURDAY
  }
  ...

  DayOfWeek d = DayOfWeek.WEDNESDAY;

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

Перечисленные константы считаются открытыми. Тип enum, как и класс, может быть открытым или пакетным. Для имен возможных значений используют заглавные буквы, поскольку фактически это константы. С константами связаны целые значения, в следующем примере - соответственно от 0 до 6. Получать эти целые значения с помощью функции ordinal(), имя константы - с помощью метода name(). Например:

  DayOfWeek d = DayOfWeek.WEDNESDAY;
  System.out.println(d.name() + " " + d.ordinal());    

С помощью статической функции values() можно получить массив элементов перечисления:

  for (int i = 0; i < DayOfWeek.values().length; i++) {
      System.out.println(DayOfWeek.values()[i]);
  }

Статическая функция valueOf() позволяет получить элемент перечисления по его имени. Например, нам необходимо получить целое значение, связанное с определенным элементом:

  System.out.println(DayOfWeek.valueOf("FRIDAY").ordinal());

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

  static void printAll() {
      for (DayOfWeek d : values())
          System.out.println(d);
  }

Можно перегрузить вывод перечисления через определение функции toString():

enum Gender {
    MALE, FEMALE;

    @Override
    public String toString() {
        switch (this) {
            case MALE:   
                return "мужской пол";
            case FEMALE:   
                return "женский пол";
        }
        return "что-то невозможное!";
    }

}

public class GenderTest {

    public static void main(String[] args) {
        Gender g = Gender.FEMALE;
        System.out.println(g);
    }

}

Константы можно связать с соответствующими значениями. Например, перечисление "Спутник Марса" содержит поле "Расстояние от центра Марса". В нашем примере необходимо добавить конструктор и дополнительные элементы:

package ua.inf.iwanoff.seventh;

enum MoonOfMars {
    PHOBOS(9377), DEIMOS(23460);

    private double distance;

    private MoonOfMars(double distance) {
        this.distance = distance;
    }

    double getDistance() {
        return distance;
    }

    @Override
    public String toString() {
        return name() + ". " + distance + " km from Mars";
    }

}

public class MoonsOfMarsTest {

    public static void main(String[] args) {
        MoonOfMars m = MoonOfMars.PHOBOS;
        System.out.println(m); // PHOBOS. 9377.0 km from Mars
    }

}

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

1.2 Генерация и обработка исключений

1.2.1 Основные концепции

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

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

По сравнению с C++ и C#, Java реализует наиболее строгий механизм работы с исключениями.

1.2.2 Синтаксис генерации исключений

Для генерации исключения используется утверждение throw. После ключевого слова throw должен следовать объект класса java.lang.Throwable или классов, производных от него. Для программных исключений обычно используется класс java.lang.Exception (производный от Throwable) или классы, произведенные от Exception. Такие производные классы обычно отражают специфику конкретной программы.

class SpecificException extends Exception {
}

Имеется также базовый класс для генерации системных ошибок - класс Error. Классы Exception и Error имеют общий базовый класс - Throwable.

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

void f() . . .
    . . .
    if (/* ошибка */) {
        throw new SpecificException();
    }

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

void f() throws SpecificException, AnotherException {
    . . .
    if (/* ошибка */) 
        throw new SpecificException();
    if (/* другая ошибка */) 
        throw new AnotherException();

В следующем примере функция reciprocal() генерирует исключение в случае деления на ноль.

class DivisionByZero extends Exception {

}

class Test {

    double reciprocal(double x) throws DivisionByZero {
        if (x == 0) {
            throw new DivisionByZero();
        }
        return 1 / x;
    }

}

В отличие от C++, Java не допускает создания исключений примитивных типов. Разрешены только объектные типы, принадлежащие иерархии исключений (потомки Exception или Throwable).

При наследовании для перекрытых функций список исключений должен сохраняться.

1.2.3 Синтаксис обработки исключений

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

double x, y;
. . .
try {
    y = reciprocal(x);
}

После блока try должен следовать один или несколько обработчиков (блоков catch). Каждый такой обработчик соответствует определенному типу исключения:

catch (DivisionByZero d) {
    // обработка исключения
}
catch (Exception ex)  {
    // обработка исключения
}

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

class BaseException extends Exception {
}

class FileException extends BaseException {
}

class FileNotFoundException extends FileException {
}

class WrongFormatException extends FileException {
}

class MathException extends BaseException {
}

class DivisionByZero extends MathException {
}

class WrongArgument extends MathException {
}

Имеется некоторая функция, которая может сгенерировать все типы исключений:

public class Exceptions {

    public static void badFunc() throws BaseException {
        // могут возникнуть различные исключения
    }

}

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

try {
    Exceptions.badFunc();
}
catch (FileNotFoundException ex) {
    // файл не найден
}
catch (WrongFormatException ex) {
    // неправильный формат
}
catch (FileException ex) {
    // прочие ошибки, связанные с файлами
}
catch (MathException ex) {
    // все математические ошибки обрабатываем вместе
}
catch (BaseException ex) {
    // подбираем все оставшиеся исключения функции badFunc()
}
catch (Exception ex) {
    // на всякий случай
}

После последнего блока catch можно поместить блок finally. Этот код всегда выполняется не зависимо от того, возникло исключение или нет, даже если в каком-то из блоков был осуществлен выход из функции.

try {
    openFile();
    // другие действия
}
catch (FileError f) {
    // обработка исключения
}
catch (Exception ex) {
    // обработка исключения
}
finally {
    closeFile();
}

Для перехвата любого исключения используются типы Exception или Throwable:

catch (Exception ex) {
    // Обработка исключения
}

или

catch (Throwable ex) {
    // Обработка исключения
}

Типичная реализация обработчика исключения - вызов метода printStackTrace() класса Throwable:

catch (Throwable ex)  {
    ex.printStackTrace();
}

Этот метод осуществляет вывод информации о трассировке стека в стандартный поток сообщений об ошибках System.err. Ниже приведен пример работы функции printStackTrace():

java.lang.NullPointerException
        at SomeClass.g(SomeClass.java:9)
        at SomeClass.f(SomeClass.java:6)
        at SomeClass.main(SomeClass.java:3)

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

double f(double x) {
    double y = 0;
    try {
        y = (new Test()).reciprocal(x);
    }
    catch (DivisionByZero ex) {
        ex.printStackTrace();
    }
    return y;
}

Неперехваченное исключение может быть передано внешнему обработчику с использованием ключевого слова throws:

double g(double x) throws DivisionByZero {
    double y;
    y = reciprocal(x);
    return y;
}

Это правило обязательно для всех исключений Java кроме объектов класса RuntimeException или его потомков, а также для ошибок типа Error. Генерацию таких исключений не нужно оговаривать в заголовке функции. Программист может обрабатывать или игнорировать такие исключения по своему усмотрению. Типичный класс исключений такого вида - NullPointerException.

В версии Java 7 к синтаксису исключений добавлены новые конструкции, которые делают работу с исключениями более удобной. Например, можно создать обработчик событий различных типов с использованием побитовой операции "ИЛИ":

public void newMultiCatch() {
    try {
        methodThatThrowsThreeExceptions();
    } 
    catch (ExceptionOne | ExceptionTwo | ExceptionThree e) {
        // обработка всех исключений
    }
}

Другие дополнительные возможности связаны с так называемым блоком управления ресурсами ("try-with-resources"). Для объектов классов, реализующих интерфейс java.lang.AutoCloseable можно поместить создание объекта непосредственно после try. Метод close(), предусмотренный этим интерфейсом, будет гарантированно вызван при выходе из блока. (аналогично выполнению кода в finally):

try (ClassThatImplementsAutoCloseable sc) {
    // действия, которые могут привести к исключению
}
catch (Exception f) {
    // обработка исключения
}   // автоматический вызов sc.close()

Чаще всего данную конструкцию используют в сочетании с файловыми потоками:

public void newTry() {
    try (FileOutputStream fos = new FileOutputStream("movies.txt")) {
        // Работа с файловыми потоками
    } 
    catch (IOException e) {
        // сообщение об ошибке
    } // автоматическое закрытие файла
}

Файловые потоки будут гарантированно закрыты.

Среда IntelliJ IDEA позволяет автоматизировать процесс создания блоков перехвата и обработки исключений. Если в тексте функции пометить блок и использовать функцию Code | Surround With... | try / catch, помеченный блок будет расположен в блоке перехвата исключений (try { }), а дальше будут добавлены catch-блоки, содержащие стандартную обработку всех возможных исключений.

1.3 Обобщенное программирование в Java

1.3.1 Концепция обобщенного программирования

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

Парадигма обобщенного программирования предполагает описание правил хранения данных и алгоритмов в общем виде независимо от конкретных типов данных. Конкретные типы данных, над которыми выполняются действия, специфицируются позже. Механизмы разделения структур данных и алгоритмов, а также формулирование абстрактных описаний требований к данным, определяются по-разному в различных языках программирования. Вначале возможности обобщенного программирования были предоставлены в семидесятые годы XX столетия языками CLU и Ада (обобщенные функции), позже были реализованы в языке ML (параметрический полиморфизм).

Наиболее полно и гибко идея обобщенного программирования реализована в языке C++ через механизм шаблонов. Шаблон (template) в C++ представляет собой фрагмент кода, обобщенно описывающий работу с некоторым абстрактным типом, заданным как параметр шаблона. Этот фрагмент кода (класс или функция) окончательно компилируется только при инстанцировании шаблона конкретным типом, то есть при подстановке конкретного типа вместо параметра. На использовании шаблонных функций и параметризированных классов построена Стандартная библиотека шаблонов (STL), являющаяся в настоящее время частью Стандартной библиотеки C++. STL включает описание стандартных контейнерных классов и независимых от них алгоритмов.

Для реализации обобщенного программирования в Java используются обобщения - специальная языковая конструкция, появившаяся в синтаксисе языка начиная с версии Java 5.

1.3.2 Проблемы создания универсальных контейнеров в Java 2

Очень часто возникает необходимость в создании так называемых классов-контейнеров - таких, которые содержат объекты произвольных типов. Например, иногда возникает необходимость хранить пару объектов одного типа. Можно предложить класс Pair (пара). Он содержит две ссылки на класс Object:

public class Pair {
    Object first, second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

}

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

Pair p = new Pair("Фамилия", "Имя");    

Такой подход имеет определенные недостатки:

  • Для чтения объектов необходимо применить явное преобразование типов:
  •     String s = (String) p.first; // вместо String s = p.first;
  • Нет уверенности, что в паре хранятся объекты именно того типа, который нас интересует:
  •     Integer i = (Integer) p.second; // Ошибка времени выполнения
  • Нельзя гарантировать, что оба поля будут одного типа:
  •     Pair p1 = new Pair("Фамилия", new Integer(2)); // Ни одного сообщения об ошибке

Аналогичные проблемы в Java 2 возникали со стандартными контейнерными классами. Следствием реализованного таким образом подхода стали потенциальные ошибки времени выполнения, которые не могли быть детектированы при компиляции кода.

1.3.3 Обобщения (Generics)

Указанные ранее проблемы решает появившаяся в Java 5 синтаксическая конструкция, так называемое обобщение - конструкция, включающая в себя параметр класса или функции, который содержит дополнительную информацию о типе элементов и других данных. Этот параметр берут в угловые скобки. Обобщение предоставляют возможность создания и использования структур данных, безопасных с точки зрения типов. Классы, описание которых содержит такой параметр, называются обобщенными. При создании объекта обобщенного типа в угловых скобках указывают имена реальных типов. Можно использовать только типы ссылок. Предыдущий пример можно реализовать с применением обобщений.

public class Pair<T> {
    T first, second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public static void main(String[] args) {
        Pair<String> p = new Pair<String>("Фамилия", "Имя");
        String s = p.first; // Получаем строку без приведения типов
        Pair<Integer> p1 = new Pair<Integer>(1, 2); // Можно использовать целые константы
        int i = p1.second;  // Получаем целое значение без приведения типов
    }
}    

Примечание: Java 7 позволяет не повторять фактический параметр обобщения после имени конструктора. Например:

Pair<Integer> p1 = new Pair<>(1, 2);

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

Pair<String> p = new Pair<String>("1", "2");
Integer i = (Integer) p.second; // ошибка компиляции

Тип данных с параметром в угловых скобках (например, Pair<String>) называется параметризованным типом.

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

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

interface Function<T> {
    T func(T x);
}

class DoubleFunc implements Function<Double> {

    @Override
    public Double func(Double x) {
        return x * 1.5;
    }
}

class IntFunc implements Function<Integer> {

    @Override
    public Integer func(Integer x) {
        return x % 2;
    }
}

Java также позволяет создавать обобщенные функции внутри как обобщенных, так и обычных (необобщенных) классов:

public class ArrayPrinter {

    public static<T> void printArray(T[] a) {
        for (T x : a) {
            System.out.print(x + "\t");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        String[] as = {"First", "Second", "Third"};
        printArray(as);
        Integer[] ai = {1, 2, 4, 8};
        printArray(ai);
    }
}

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

public class TypeConverter {

    public static<T> T convert(Object object) {
        return (T) object;
    }

    public static void main(String[] args) {
        Object o = "Some Text";
        String s = TypeConverter.<String>convert(o);
        System.out.println(s);
    }

}

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

public class PairOfDifferentObjects<T, E> {
    T first;
    E second;

    public PairOfDifferentObjects(T first, E second) {
        this.first = first;
        this.second = second;
    }

    public static void main(String[] args) {
        PairOfDifferentObjects<Integer, String> p = 
            new PairOfDifferentObjects<Integer, String>(1000, "thousand");
        PairOfDifferentObjects<Integer, Integer> p1 = 
            new PairOfDifferentObjects<Integer, Integer>(1, 2);
        //...
    }

}    

Над данными типа параметра обобщения можно осуществлять только действия, разрешенные для объектов класса Object. Иногда для расширения функциональности желательным является конкретизация типа. Например, мы хотим вызвать методы, объявленные в определенном классе или интерфейсе. Тогда можно применить следующий синтаксис описания параметра: <T extends SomeBaseType> или <T extends FirstType & SecondType> и т.д. Слово extends используется как для классов, так и для интерфейсов.

Например, можно создать обобщенную функцию вычисления среднего арифметического в массиве некоторых числовых значений. Стандартные классы Double, Float, Integer, Long и другие классы-оболочки числовых данных имеют общий абстрактный базовый класс - java.lang.Number, декларирующий, в частности, метод doubleValue(), позволяющий, получить хранящееся в объекте число в виде значения типа double. Этот факт можно использовать при вычислении среднего арифметического. Созданная функция может работать с массивами чисел различных типов:

package ua.inf.iwanoff.seventh;

public class AverageTest {

    public static<E extends Number> double average(E[] arr) {
        double result = 0;
        for (E elem : arr) {
            result += elem.doubleValue();
        }
        return result / arr.length;
    }

    public static void main(String[] args) {
        Double[] doubles = { 1.0, 1.1, 1.5 };
        System.out.println(average(doubles)); // 1.2
        Integer[] ints = { 10, 20, 3, 4 };
        System.out.println(average(ints));    // 9.25
    }
}

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

public class MyArray<T> {
    private T[] arr;

    public MyArray(T... arr) {
        this.arr = arr;
    }

    public int size() {
        return arr.length;
    }

    public T get(int i) {
        return arr[i];
    }

    public void set(int i, T t) {
        arr[i] = t;
    }
  
    public void printAll() {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

}

В другом классе осуществляем тестирование:

public class TestClass {

    public static void main(String[] args) {
        MyArray<String> a = new MyArray<>(new String[]{ "1", "2" });
        String s = a.get(a.size() - 1);
        System.out.println(s);     // 2
        a.set(1, "New");    
        a.printAll();              // 1 New
    }

}

Нельзя создавать объекты и массивы обобщенных типов:

T arr = new T[10]; // ошибка!

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

package ua.inf.iwanoff.seventh;

public class MyArray<T> {
    private Object[] arr = {};

    public MyArray(T... arr) {
        this.arr = arr;
    }

    public MyArray(int size) {
        arr = new Object[size];
    }

    public int size() {
        return arr.length;
    }

    public T get(int i) {
        return (T)arr[i];
    }

    public void set(int i, T t) {
        arr[i] = t;
    }

    public void add(T t) {
        Object[] temp = new Object[arr.length + 1];
        System.arraycopy(arr, 0, temp, 0, arr.length);
        arr = temp;
        arr[arr.length - 1] = t;
    }

    public void printAll() {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

}

В другом классе осуществляем тестирование:

package ua.inf.iwanoff.seventh;

public class TestClass {

    public static void main(String[] args) {
        MyArray<String> a = new MyArray<>("1", "2");
        String s = a.get(a.size() - 1);
        System.out.println(s);     // 2
        a.set(1, "New");    
        a.printAll();              // 1 New
        MyArray<Double> b = new MyArray<>(3);
        b.set(0, 1.0);
        b.set(1, 2.0);
        b.set(2, 4.0);  
        b.add(8.0);  
        b.printAll();    
    }

}

Функциональность класса можно расширить методами добавления нового элемента внутри массива, удаления существующего и т.д.

Синтаксис обобщений предполагает использование так называемых масок (wildcard, символ '?'). Маска применяется, например, для описания ссылок на пока неизвестный тип. Использование масок делает обобщенные классы и функции более совместимыми. Маска предоставляет альтернативный способ создания обобщенных функций.

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

package ua.inf.iwanoff.seventh;

public class GenericArrayPrinter {

    public static void printGenericArray(MyArray<?> a) {
        for (int i = 0; i < a.size(); i++) {
            System.out.print(a.get(i) + "\t");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        MyArray<String> arr1 = new MyArray("First", "Second", "Third");
        printGenericArray(arr1);
        MyArray<?> arr2 = new MyArray(1, 2, 3); // MyArray<?> вместо MyArray<Integer>
        printGenericArray(arr2);
    }
    
}

Можно ограничить использование типа параметра функции определенными производными классами, например, MyArray<? super String>. Тогда использование MyArray<Integer> невозможно.

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

1.4 Сортировка массивов

В простейшем случае сортировки всего массива по возрастанию осуществляется с помощью функции sort() с одним параметром - ссылкой на соответствующий массив. Статическая функция sort() класса java.util.Array реализована для массивов всех примитивных типов. Аналогично можно реализовать сортировку объектов классов, для которых определено натуральное сравнение, т.е. реализован интерфейс Comparable. Единственный метод этого интерфейса - compareTo():

public int compareTo(Object o)

Метод должен вернуть отрицательное значение (например, -1), если объект, для которого вызван метод, меньше объекта o, нулевое значение, если объекты равны, и положительное значение в противном случае.

Классы, реализующие интерфейс Comparable, - это классы-оболочки Double, Integer, Long и т.д., а также String. Например, таким образом можно рассортировать массив объектов типа Integer:

public class SortIntegers {

    public static void main(String[] args) {
        Integer[] a = {7, 8, 3, 4, -10, 0};
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }

}

В Java 5 Comparable - это обобщенный интерфейс. Если использовать обобщения, функция compareTo() должна принимать аргумент типа параметра обобщения.

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

class Rectangle implements Comparable<Rectangle> {
    double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }

    public double perimeter() {
        return ( width + height ) * 2;
    }

    public int compareTo(Rectangle rect) {
        return Double.compare(area(), rect.area());
    }

    public String toString() {
        return "\n [" + width + ", " + height + ", area = " + area() + ", 
                                       perimeter = " + perimeter() + "]";
    }

}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = { 
            new Rectangle(2, 7),
            new Rectangle(5, 3),
            new Rectangle(3, 4)
        };
        java.util.Arrays.sort(a);
        System.out.println(java.util.Arrays.toString(a));
    }

}

В приведенном примере используется статическая функция compare() класса Double. Эта функция возвращает значение, необходимое методу sort().

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

public static void sort(T[] a, Comparator<? super T> c)
public static void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)

Интерфейс Comparator содержит описание метода compare() с двумя параметрами. Функция должна вернуть отрицательное число, если первый объект при сортировке необходимо считать меньшим, чем другой, значение 0, если объекты эквивалентны, и положительное число в противном случае.

Начиная с Java 5 Comparator - это также обобщенный интерфейс. Если использовать обобщения, функция compare() должна принимать два аргумента типа параметра обобщения. Например:

import java.util.Comparator;

class Rectangle {
    double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }

    public double perimeter() {
        return (width + height) * 2;
    }

    public String toString() {
        return "\n [" + width + ", " + height + ", area = " + area() + ", 
                                       perimeter = " + perimeter() + "]";
    }
}

class CompareByArea implements Comparator<Rectangle>
{

    @Override
    public int compare(Rectangle r1, Rectangle r2) {
        return Double.compare(r1.area(), r2.area());
    }

}

public class SortRectangles {

    public static void main(String[] args) {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        java.util.Arrays.sort(a, new CompareByArea());
        System.out.println(java.util.Arrays.toString(a));
    }

}

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

Integer[] arr = new Integer[] { 3, 4, 1, 2, 5 };
Arrays.sort(arr, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return -Integer.compare(o1, o2);
    }
}); 
System.out.println(Arrays.toString(arr));//[5, 4, 3, 2, 1]

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

    Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
    // Сортировка по площади:
    java.util.Arrays.sort(a, (r1, r2) -> Double.compare(r1.area(), r2.area()));
    System.out.println(java.util.Arrays.toString(a));
    // Сортировка по периметру:
    java.util.Arrays.sort(a, (r1, r2) -> Double.compare(r1.perimeter(), r2.perimeter()));
    System.out.println(java.util.Arrays.toString(a));

Начиная с Java 8, интерфейс Comparator предлагает стандарнтые статические функции, которые создают и возвращают объекты классов, реализующих этот интерфейс. Это, например, naturalOrder(), reverseOrder() и т. д. Существует набор функций с реализацией по умолчанию, например, reversed(), thenComparing() и т.д.

2 Примеры программ

2.1 Описание и использование перечисления

Предположим, необходимо создать перечисление, описывающее дни недели. Ранее был приведен пример такого перечисления. К нему следует добавить функцию получения следующего дня, причем после субботы вновь должно быть воскресенье т.д. Кроме того, требуется проверить, относится день к уикенду. При выводе также необходимо получать номер дня (0 - воскресенье, 1 - понедельник и т. д.). В функции main(), начиная с понедельника необходимо пройтись по дням недели, вывести данные о дне, а также проверить, уикенд это или нет. Программа будет выглядеть так:

package ua.inf.iwanoff.seventh;

enum DayOfWeek {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY;

    @Override
    public String toString() { 
        return name() + " " + ordinal(); 
    }

    DayOfWeek next() {
        DayOfWeek day = values()[(ordinal() + 1) % values().length];
        return day;
    }

    boolean isWeekend() {
        switch (this) {
            case SATURDAY: 
            case SUNDAY: 
                return true;
            default: 
                return false;
        }
    }

}

public class EnumTest {

    public static void main(String[] args) {
        DayOfWeek d = DayOfWeek.MONDAY;
        for (int i = 0; i < 7; i++) {
            d = d.next();
            System.out.println(d + " " + d.isWeekend());
        }
    }

}

Как и в других случаях, формой вывода данных управляет перекрытый метод toString().

2.2 Решение уравнения

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

  • левая граница интервала больше или равна правой;
  • функция не меняет знака на предложенном интервале.

Для того чтобы сократить количество файлов в проекте, класс-исключение, как и интерфейс Function, можно вложить в класс Solver. Теперь мы можем проверить эти ситуации и сгенерировать исключения:

package ua.inf.iwanoff.seventh;

public class Solver {

    public interface Function {
        double f(double x);
    }

    public static class EquationError extends Exception {
        public void printError() {
            System.out.println("Wrong data!");
            System.exit(1);
        }
    }

    static double solve(double a, double b, double eps, Function func) 
                                                    throws EquationError {
        if (a >= b || func.f(a) * func.f(b) > 0) {
            throw new EquationError();
        }
        double x = (a + b) / 2;
        while (Math.abs(b - a) > eps) {
            if (func.f(a) * func.f(x) > 0) {
                a = x;
            }
            else {
                b = x;
            }
            x = (a + b) / 2;
        }
        return x;
    }

}

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

package ua.inf.iwanoff.seventh;

public class InterfaceTest {

    public static void main(String[] args) {
        try {
            System.out.println(Solver.solve(0, 2, 0.000001, new Solver.Function() {
                public double f(double x) {
                    return x * x - 2;
                }
            }));
        }
        catch (Solver.EquationError err) {
            err.printError();
        }
    }

}

2.3 Реализация интерфейса AutoCloseable

Допустим, мы вычисляем сумму введенных чисел и должны ее вывести, даже если возникло исключение. Можно предложить следующий класс, реализующий интерфейс java.lang.AutoCloseable, и в методе разместить некоторую полезную работу, которая выполнится во всех случаях. Создаем класс:

package ua.inf.iwanoff.seventh;

public class SumFinder implements AutoCloseable {
    private double sum;

    public void add(double value) {
        sum += value;
    }

    @Override
    public void close() throws Exception {
        System.out.println(sum);
    }
}

Тестируем класс:

package ua.inf.iwanoff.seventh;

import java.util.Scanner;

public class ClosableTest {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try (SumFinder finder = new SumFinder()) {
            double x;
            do {
                x = scanner.nextDouble();
                finder.add(x);
            }
            while (x != 0);
        }
        catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
    }
}

Теперь сумма будет выведена во всех случаях. Из примера также видно, что сначала выполняется код метода close(), а потом блока catch.

2.4 Обобщенная функция поиска определенного элемента

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

package ua.inf.iwanoff.seventh;

public class ElementFinder {

    public static <E>int indexOf(E[] arr, E elem) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i].equals(elem)) {
                return i;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Integer[] a = {1, 2, 11, 4, 5};
        System.out.println(indexOf(a, 11));    // 2
        System.out.println(indexOf(a, 12));    // -1
        String[] b = {"one", "two"};
        System.out.println(indexOf(b, "one")); // 0
    }

}

Для сравнения значений объектов следует использовать метод equals() вместо ==.

2.5 Нахождение минимального элемента массива чисел

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

package ua.inf.iwanoff.seventh;

public class MinFinder {

    public static <T extends Number> T min(T[] arr) {
        T result = arr[0];
        for (T t : arr) {
            if (t.doubleValue() < result.doubleValue()) {
                result = t;
            }
        }
        return result;
    }
    
    public static void main(String[] args) {
        Integer [] a = { 3, 1, 2 };
        int minA = min(a);
        System.out.println(minA);
        Double[] b = { 0.3, 0.2, 0.1 };
        double minB = min(b);
        System.out.println(minB);
    }

}

2.6 Добавление целых в обобщенный массив чисел

Предположим, необходимо добавлять в конец обобщенного массива MyArray числа от 1 до 5. Можно реализовать отдельную функцию и осуществить ее тестирование для двух массивов различных типов:

package ua.inf.iwanoff.seventh;

public class NumAdder {

    public static void addNumbers(MyArray<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        MyArray<Integer> digits = new MyArray<>(8, 9);
        addNumbers(digits);
        digits.printAll();
        MyArray<Number> nums = new MyArray(3.14, 6.28);
        addNumbers(nums);
        nums.printAll();
    }

}

2.7 Использование исключений в обобщенных функциях

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

package ua.inf.iwanoff.seventh;

import java.util.Arrays;

public class Replacer {

    public static class IndexException extends Exception {
        private int index;
        private Object value;

        public<T> IndexException(int index, T value) {
            super();
            this.index = index;
            this.value = value;
        }

        public int getIndex() {
            return index;
        }

        public <T>T getValue() {
            return (T)value;
        }

    }
  
    public static <E>void replace(E[] arr, int index, E elem) throws IndexException {
        if (index < 0 || index >= arr.length) {
            throw new IndexException(index, elem);
        }
        arr[index] = elem;
    }
  
    public static void main(String[] args) {
        Integer[] a = {1, 2, 11, 4, 5};
        try {
            replace(a, 5, 100);
            System.out.println(Arrays.toString(a));
        }
        catch (IndexException e) {
            System.err.println(e.getClass().getName() + " Индекс: " + e.getIndex() + 
                               " Значение: " + e.<Double>getValue());
        }
    }

}

Программа демонстрирует возможность создания обобщенного конструктора необобщенного класса. Поскольку осуществляется вывод значения, обобщенная функция getValue() может быть также вызвана без спецификации типа.

3 Задания на самостоятельную работу

3.1 Перечисление для представления дней недели

В перечислении "День недели" добавить функции получения дня "позавчера" и "послезавтра". Протестировать перечисление в функции main() тестового класса.

3.2 Перечисление для представления сезона

Создать перечисление "Сезон". Описать метод получения предыдущего и последующего сезона. Протестировать перечисление в функции main() тестового класса.

3.3 Перечисление для описания месяцев года*

Создать перечисление "Месяц". Необходимо определять в конструкторе и сохранять количество дней (для невисокосного года). Добавить методы получения предыдущего и следующего месяца, а также функцию, которая возвращает сезон для каждого месяца. Предусмотреть вывод месяцев на русском (украинском) языке. Создать статическую функцию вывода данных обо всех месяцах путем перекрытия метода toString(). Протестировать перечисление в функции main() тестового класса.

3.4 Функция вычисления корня четвертой степени*

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

3.5 Реализация интерфейса AutoCloseable*

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

3.6 Обобщенный класс

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

3.7 Библиотека обобщенных функций*

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

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

Осуществить тестирование всех функций на двух массивах различных типов.

3.8 Библиотека функций для работы с массивом чисел*

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

  • нахождение индекса первого нулевого элемента
  • определение количества отрицательных чисел
  • возвращение последнего отрицательного элемента

Осуществить тестирование всех функций на числовых массивах различных типов.

3.9 Использование ограниченных подстановочных типов

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

3.10 Реализация интерфейса Comparable

Создать класс Circle, который реализует интерфейс Comparable. Большей считается окружность с большим радиусом. Осуществить сортировку массива объектов типа Circle.

3.11 Реализация интерфейса Comparator*

Создать класс Triangle. Треугольник задавать длинами сторон. Площадь треугольника в этом случае может быть вычислена по формуле Герона:

где a, b и c - длины сторон треугольника. Осуществить сортировку массива треугольников по уменьшению площади. Для определения признака сортировки использовать объект, реализующий интерфейс Comparator.

3.12 Использование исключений в обобщенных функциях

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

4 Контрольные вопросы

  1. Для чего используются перечисления?
  2. Можно ли создавать методы внутри перечислений?
  3. Можно ли создавать поля внутри перечислений?
  4. Для чего используются конструкторы перечислений?
  5. Для чего предназначен механизм исключений?
  6. Как создать объект-исключение?
  7. Можно ли создавать исключения типа int?
  8. Можно ли использовать основной результат функции, если произошла генерация исключения?
  9. Можно ли поместить вызов функции, генерирующей исключение, вне блока try?
  10. Как в одном catch-блоке обработать исключения нескольких различных типов?
  11. Для чего создаются объекты в блоке try?
  12. В чем назначение функции printStackTrace()?
  13. В чем суть обобщенного программирования?
  14. В каких случаях целесообразно создавать обобщенные классы?
  15. Можно ли создавать обобщенные интерфейсы?
  16. Что такое параметризованный тип?
  17. Чем определяется набор методов, допустимых для обобщенного типа?
  18. Можно ли создавать объекты обобщенных типов?
  19. Для чего при описании параметров используют маски?
  20. Можно ли использовать маски при создании локальных переменных?
  21. Каким требованиям должен удовлетворять объект, чтобы массив таких объектов можно было сортировать без определения признака сортировки?
  22. В чем преимущество подхода, использующего реализацию интерфейса Comparable?
  23. В чем преимущество подхода, использующего реализацию интерфейса Comparator?
  24. Какую функцию необходимо определить, чтобы реализовать интерфейс Comparator?

 

up