Вложенные типы. Интерфейсы. Лямбда-выражения и ссылки на методы

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

1.1 Вложенные классы

1.1.1 Общие понятия

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

class Outer {

    class Inner {
        int i; 
    };

    Inner inner = new Inner();
}

class Another {
    Outer.Inner i;
}

Вложенные классы могут быть объявлены со спецификаторами public, private или protected.

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

void f() {

    class Local {
        int j;
    }

    Local l = new Local();
    l.j = 100;
}

Можно также размещать локальные классы внутри отдельных блоков.

1.1.2 Внутренние классы

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

  • объекты внутренних классов имеют прямой доступ к данным объекта объемлющего класса;
  • для создания объекта внутреннего класса обязательно иметь в наличии объект объемлющего класса.

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

class Outer {
    int k = 100;

    class Inner {
        void show() { 
            System.out.println(k);
        }
    }

} 

public class Test {

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    } 

}

Нестатические вложенные классы не могут содержать статических элементов.

Главное отличие внутренних классов от композиции классов заключается в том, что объект внутреннего класса автоматически не создается. Создание объекта может быть предусмотрено в конструкторе или любом методе объемлющего класса, а также вне его (если этот класс не объявлен как private). Можно также создать массив объектов внутреннего класса. Каждый из таких объектов будет иметь доступ к ссылке на объемлющий объект.

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

class FirstBase {
    int a = 1;
}

class SecondBase {
    int b = 2;
}

class Outer extends FirstBase {
    int c = 3;

    class Inner extends SecondBase {
        void show() {
            System.out.println(a);
            System.out.println(b);
            System.out.println(c);
        }
    }

}
 
public class Test {

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }

}

1.1.3 Безымянные классы

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

new Object() {
    void hello() {
        System.out.println("Hello");
    }
}.hello();

Безымянные классы не могут быть абстрактными. Такие классы всегда по умолчанию final. В следующем примере безымянный класс создается для определения способа сортировки массива строк:

void sortByABC(String[] a) 
{ 
    Arrays.sort(a, new Comparator() { 
        public int compare(Object o1, Object o2) {
            return ((String) o1).compareTo((String) o2);
        }
    }); 
}

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

abstract class Base {
    int k;
 
    Base(int k) {
        this.k = k;
    }

    abstract void show();
} 

public class Test {

    static void showBase(Base b) {
        b.show();
    }

    public static void main(String[] args) {
        showBase(new Base(10) {
            void show() {
                System.out.println(k);
            }
        });
    }
 
}

Допустимо также использование блоков инициализации.

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

1.1.4 Статические вложенные классы

Статические вложенные классы имеют доступ только к статическим элементам объемлющих классов. Такие классы могут быть созданы без создания объектов объемлющих классов:

class Outer {
    int k = 100;
    static int m = 200;

    static class Inner {
        void show() {
            // k недоступно
            System.out.println(m);
        }
    }
}
 
public class Test {

    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner();
        inner.show();
    } 

}

Статические вложенные классы могут содержать свои статические элементы, в том числе свои вложенные статические и нестатические классы.

1.2 Интерфейсы

1.2.1 Описание и реализация интерфейсов

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

interface Int1 {
    void f();
    int g(int x);
}

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

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

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

interface Int1 {
    void f();
    int g(int x);
}

class SomeClass implements Int1 {
    public void f() {
    }
    public int g(int x) {
        return x;
    }
}

Интерфейс может иметь несколько базовых интерфейсов:

interface Int1 {
    void f();
    int g(int x);
}

interface Int2 {
    void h(int z);
}

interface Int3 extends Int1, Int2 {
}

Класс может реализовать несколько интерфейсов:

interface Int1 {
    void f();
    int g(int x);
}

interface Int2 {
    void h(int z);
}

class SomeClass implements Int1, Int2 {
    public void f() {
    }
  
    public int g(int x) {
        return x;
    }

    public  void h(int z) {
    }
}

Классы можно создавать внутри интерфейсов. Такие классы по умолчанию являются статическими. Внутри классов также можно создавать интерфейсы, которые являются статическими по умолчанию.

1.2.2 Реализация методов интерфейсов по умолчанию

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

Например, можно описать такой интерфейс с реализацией функции по умолчанию:

package ua.inf.iwanoff.sixth;

public interface Greetings {
    default void hello() {
        System.out.println("Hello everybody!");
    }
}

Класс, реализующий интерфейс, может быть пустым. Предполагается реализация метода hello() по умолчанию:

package ua.inf.iwanoff.sixth;

public class MyGreetings implements Greetings {

}

При тестировании получим приветствие по умолчанию.

package ua.inf.iwanoff.sixth;

public class GreetingsTest {

    public static void main(String[] args) {
        new MyGreetings().hello(); // Hello everybody!
    }

}

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

package ua.inf.iwanoff.sixth;

public class GreetingsTest {

    public static void main(String[] args) {
        new Greetings() { }.hello(); // Hello everybody!
    }

}

Метод по умолчанию можно переопределить:

package ua.inf.iwanoff.sixth;

public class MyGreetings implements Greetings {

    @Override
    public void hello() {
        System.out.println("Hello to me!");
    }

}

Теперь, создав объект этого класса, мы получим новое приветствие.

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

Greetings.super.hello();

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

package ua.inf.iwanoff.sixth;

public interface FunctionToPrint {
    public double f(double x);
    default void print(double x) {
        System.out.printf("x = %7f f(x) = %7f%n", x, f(x));
    }
}

В классе PrintValues создаем метод печати таблицы printTable(). Этот метод использует созданный ранее интерфейс.

package ua.inf.iwanoff.sixth;

public class PrintValues {

    static void printTable(double from, double to, 
                           double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }

    // В функции main() создаем объект безымянного класса:
    public static void main(String[] args) {
        printTable(-2, 2, 0.5, new FunctionToPrint() {
            @Override
            public double f(double x) {
                return x * x * x;
            }
        });
    }

}

Допустим, при печати таблицы нас не устроила точность вычислений. В этом случае в безымянном классе можно также переопределить метод print():

public static void main(String[] args) {
    printTable(-2, 2, 0.5, new FunctionToPrint() {
        @Override
        public double f(double x) {
            return x * x * x;
        }
        @Override
        public void print(double x) {
            System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
        }        
    });
}

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

package ua.inf.iwanoff.sixth;

public class PrintValues {

    public static void print(double from, double to, double step, Function func) {
        for (double x = from; x <= to; x += step) {
            System.out.printf("x = %7f f(x) = %7f%n", x, func.f(x));
        }
        System.out.println();
    }
 
    public static void main(String[] args) {
        print(-2, 2, 0.5, new Function() {
            @Override
            public double f(double x) {
                return x * x * x;
            }     
        });
    }

}

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

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

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

public interface SomeInterface {
    void f();
}

Этот интерфейс реализовывался некоторым классом:

public class OldImpl implements SomeInterface {
    @Override
    public void f() {
        // реализация
    }
}

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

interface SomeInterface {
    void f();
    default void g() {
        // реализация
    }
}    

Этот метод будет реализован новыми классами:

public class NewImpl implements SomeInterface {
    @Override
    public void f() {
        // реализация
    }
    @Override
    public void g() {
        // реализация
    }
}

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

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

1.2.2 Реализация статических методов в интерфейсах

В Java 8 интерфейсы также могут содержать реализацию статических методов. Логично внутри интерфейса определять методы, имеющие отношение к данному интерфейсу (например, получающие ссылку на интерфейс в качестве параметра). Чаще всего, это вспомогательные методы. Как и все элементы интерфейса, такие статические методы являются публичными. Можно указать public явно, но в этом нет необходимости.

В приведенном ранее примере функцию printTable() можно было бы разместить внутри интерфейса:

package ua.inf.iwanoff.sixth;

public interface FunctionToPrint {
    public double f(double x);

    default void print(double x) {
        System.out.printf("x = %9f f(x) = %9f%n", x, f(x));
    }

    static void printTable(double from, double to, double step, FunctionToPrint func) {
        for (double x = from; x <= to; x += step) {
            func.print(x);
        }
        System.out.println();
    }
}

1.3 Лямбда-выражения

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

В языках программирования присутствует понятие функционального объекта - объекта, который можно использовать как функцию. Лямбда-выражение - это специальный синтаксис описания функционального объекта. Другими словами, лямбда-выражение - это способ описания функции внутри другой функции.

Термин "лямбда-выражение" связан с математической дисциплиной - лямбда-исчислением. Лямбда-исчисление - это формальная система, разработанная американским математиком Алонзо Чёрчем для формализации и анализа понятия вычислимости. Лямбда-исчисление стало формальной основой языков функционального программирования (Lisp, Scheme и т. д.)

Лямбда-выражение в Java имеет следующий синтаксис:

  • список формальных параметров, разделенных запятыми и заключенных в круглые скобки; если параметр один, скобки можно опустить; если параметров нет, нужна пустая пара скобок;
  • стрелка (->);
  • тело, состоящее из одного выражения или блока; если используется блок, внутри него может быть утверждение return;

Например, функция с одним параметром:

k -> k * k

То же самое со скобками и блоком:

(k) -> { return k * k; }

Функция с двумя параметрами:

(a, b) -> a + b

Функция без параметров:

() -> System.out.println("First")

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

Например, имеется функциональный интерфейс:

public interface SomeInt {
    int f(int x);
}

При вызове некоторой функции требуется параметр типа функционального интерфейса. Традиционно можно создать безымянный класс:

someFunc(new SomeInt() {
    @Override
    public int f(int x) {
        return x * x;
    }
});

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

SomeInt func = k -> k * k;
someFunc(func);

Можно также создать переменную при вызове функции с параметром-функциональным интерфейсом:

someFunc(x -> x * x);

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

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

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

Использование функционального интерфейса:

public class PrintWithLambda {

    public static void print(double from, double to, double step, FuncForLambda func) {
        for (double x = from; x <= to; x += step) {
            System.out.printf("x = %8.5f  f(x) = %8.5f%n", x, func.f(x));
        }
        System.out.println();
    }
  
    public static void main(String[] args) {
        print(-2.0, 2.0, 0.5, x -> x * x * x);
    }

}

Вместо того, чтобы создавать новые функциональные интерфейсы, в большинстве случаев достаточно воспользоваться стандартными интерфейсами, описанными в пакете java.util.function.

Интерфейс Описание
BiConsumer<T,U> Представляет операцию, которая принимает два входных аргумента и не возвращает результата
BiFunction<T,U,R> Представляет функцию, которая принимает два аргумента и возвращает результат
BinaryOperator<T> Представляет операцию над двумя операндами одного типа, производя результат того же типа, что и операнды
BiPredicate<T,U> Представляет предикат (функцию с результатом типа boolean) с двумя аргументами
BooleanSupplier Представляет "поставщика" результата типа boolean
Consumer<T> Представляет операцию, которая принимает один аргумент и не возвращает результата
DoubleBinaryOperator Представляет операцию над двумя аргументами типа double и возвращает результат типа double
DoubleConsumer Представляет операцию, которая принимает один аргумент типа double и не возвращает результата
DoubleFunction<R> Представляет операцию, которая принимает один аргумент типа double и возвращает результат
DoublePredicate Представляет предикат (функцию с результатом типа boolean) с одним аргументом типа double
DoubleSupplier Представляет "поставщика" результата типа double
DoubleToIntFunction Представляет операцию, которая принимает один аргумент типа double и возвращает результат типа int
DoubleToLongFunction Представляет операцию, которая принимает один аргумент типа double и возвращает результат типа long
DoubleUnaryOperator Представляет операцию, которая принимает один аргумент типа double и возвращает результат типа double
Function<T,R> Представляет операцию, которая принимает один аргумент и возвращает результат
IntBinaryOperator Представляет операцию над двумя аргументами типа int, возвращающую результат типа int
IntConsumer Представляет операцию, которая принимает один аргумент типа int и не возвращает результата
IntFunction<R> Представляет операцию, которая принимает один аргумент типа int и возвращает результат
IntPredicate Представляет предикат (функцию с результатом типа boolean) с одним аргументом типа int
IntSupplier Представляет "поставщика" результата типа int
IntToDoubleFunction Представляет операцию, которая принимает один аргумент типа int и возвращает результат типа double
IntToLongFunction Представляет операцию, которая принимает один аргумент типа int и возвращает результат типа long
IntUnaryOperator Представляет операцию, которая принимает один аргумент типа int и возвращает результат типа int
LongBinaryOperator Представляет операцию над двумя аргументами типа long, возвращающую результат типа long
LongConsumer Представляет операцию, которая принимает один аргумент типа long и не возвращает результата
LongFunction<R> Представляет операцию, которая принимает один аргумент типа long и возвращает результат
LongPredicate Представляет предикат (функцию с результатом типа boolean) с одним аргументом типа long
LongSupplier Представляет "поставщика" результата типа long
LongToDoubleFunction Представляет операцию, которая принимает один аргумент типа long и возвращает результат типа double
LongToIntFunction Представляет операцию, которая принимает один аргумент типа long и возвращает результат типа int
LongUnaryOperator Представляет операцию, которая принимает один аргумент типа long и возвращает результат типа long
ObjDoubleConsumer<T> Представляет функцию, которая принимает аргументы типов T и double и не возвращает результата
ObjIntConsumer<T> Представляет функцию, которая принимает аргументы типов T и int и не возвращает результата
ObjLongConsumer<T> Представляет функцию, которая принимает аргументы типов T и long и не возвращает результата
Predicate<T> Представляет предикат (функцию с результатом типа boolean) с одним аргументом
Supplier<T> Представляет "поставщика" результата
ToDoubleBiFunction<T,U> Представляет функцию, которая принимает два аргумента и продуцирует результат типа double.
ToDoubleFunction<T> Представляет функцию, которая продуцирует результат типа double
ToIntBiFunction<T,U> Представляет функцию, которая принимает два аргумента и продуцирует результат типа int
ToIntFunction<T> Представляет функцию, которая продуцирует результат типа int
ToLongBiFunction<T,U> Представляет функцию, которая принимает два аргумента и продуцирует результат типа long
ToLongFunction<T> Представляет функцию, которая продуцирует результат типа long
UnaryOperator<T> Представляет операцию над одним операндом, которая возвращает результат того же типа, что и операнд

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

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

Можно осуществлять композицию лямбда-выражений (использовать лямбда-выражения как параметры). С этой целью интерфейсы пакета java.util.function предоставляют методы с реализацией по умолчанию, обеспечивающие выполнение некоторой функции, переданной в качестве параметра до или после данного метода. В частности, в интерфейсе Function определены такие методы:

// Выполняется функция before, а затем вызывающая функция:
Function compose(Function before)
// Функция after выполняется после вызывающей функции:
Function andThen(Function after)

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

package ua.inf.iwanoff.sixth;

import java.util.function.Function;

public class ComposeDemo {
  
    public static Double calc(Function<Double , Double> operator, Double x) {
        return operator.apply(x);
    }
  
    public static void main(String[] args) {
        Function<Double , Double> addTwo = x -> x + 2;
        Function<Double , Double> duplicate = x -> x * 2;
        System.out.println(calc(addTwo.compose(duplicate), 10.0)); // 22.0
        System.out.println(calc(addTwo.andThen(duplicate), 10.0)); // 24.0
    }

}

Композиция может быть более сложной:

System.out.println(calc(addTwo.andThen(duplicate).andThen(addTwo), 10.0));  // 26.0

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

1.4 Использование ссылок на методы

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

Вид ссылки на метод

Синтаксис

Пример

Ссылка на статический метод

имяКласса::имяСтатическогоМетода

String::valueOf

Ссылка на нестатический метод для заданного объекта

имяОбъекта::имяНестатическогоМетода

s::toString

Ссылка на нестатический метод для параметра

имяКласса::имяНестатическогоМетода

Object::toString

Ссылка на конструктор

имяКласса::new

String::new

Например, имеются следующие функциональные интерфейсы

interface IntOperation {
    int f(int a, int b);
}

interface StringOperation {
    String g(String s);
}

Можно создать некоторый класс:

class DifferentMethods
{
    public int add(int a, int b) {
        return a + b;
    }

    public static int mult(int a, int b) {
        return a * b;
    }

}

Тестируем методы:

public class TestMethodReferences {

  static void print(IntOperation op, int a, int b) {
      System.out.println(op.f(a, b));
  }
  
  static void print(StringOperation op, String s) {
      System.out.println(op.g(s));
  }
  
  public static void main(String[] args) {
      DifferentMethods dm = new DifferentMethods();
      print(dm::add, 3, 4);
      print(DifferentMethods::mult, 3, 4);
      print(String::toUpperCase, "text");    
  }

}

Использование ссылок на конструктор особенно полезно в функциях-"поставщиках".

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

2.1 Решение уравнения методом дихотомии

Пример 2.8 предыдущей темы можно реализовать с помощью интерфейсов. Мы можем описать интерфейс для представления левой части уравнения. Для создания интерфейса в среде IntelliJ IDEA используется функция File | New | Java Class и выбираем имя (Function) вариант (Kind): Interafce. Создаем интерфейс:

package ua.inf.iwanoff.sixth;

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

Класс Solver реализует статический метод для решения уравнения:

package ua.inf.iwanoff.sixth;

public class Solver {

    public static double solve(double a, double b, double eps, Function func) {
        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;
    }

}

Для реализации интерфейса можно предолжить несколько вариантов.

2.1.1 Создание класса, реализующего интерфейс

Класс, реализующий интерфейс, содержит конкретную реализацию функции f():

package ua.inf.iwanoff.sixth;

class MyFunction implements Function {
  public double f(double x) {
    return x * x - 2;
  }
}

public class InterfaceTest {
  
    public static void main(String[] args) {
        System.out.println(Solver.solve(0, 2, 0.000001, new MyFunction()));
    }
}

Оба варианта должны обеспечить одинаковый результат при тестировании.

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

2.1.2 Создание безымянного класса

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

package ua.inf.iwanoff.sixth;

public class InterfaceTest {

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

}

2.1.3 Использование лямбда-выражений

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

package ua.inf.iwanoff.sixth;

public class SolveUsingLambda {

    public static void main(String[] args) {
        System.out.println(Solver.solve(0, 2, 0.000001, x -> x * x - 2));
    }

}

2.1.4 Использование ссылок на методы

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

package ua.inf.iwanoff.sixth;

public class SolveWithReference {

    public static double f(double x) {
        return x * x - 2;
    }
  
    public static void main(String[] args) {
        System.out.println(Solver.solve(0, 2, 0.000001, SolveWithReference::f));
		   System.out.println(Solver.solve(0, 2, 0.000001, Math::cos));
    }

}

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

2.2 Использование интерфейсов с реализацией по умолчанию

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

f '(x) = (f(x + dx) - f(x)) / dx

Чем меньше dx, тем точнее будет найдена производная. Вторую производную можно найти как производную первой производной.

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

xn+1 = xn - f(xn) / f '(xn)

Описываем интерфейс. Вычисление первой и второй производной осуществляется методами, реализованными по умолчанию:

package ua.inf.iwanoff.sixth;

public interface FunctionWithDerivates {
    double DX = 0.001;
  
    double f(double x);
  
    default double f1(double x) {
        return (f(x + DX) - f(x)) / DX;
    }
  
    default double f2(double x) {
        return (f1(x + DX) - f1(x)) / DX;
    }
}

Реализуем класс со статической функцией решения уравнения:

package ua.inf.iwanoff.sixth;

public class Newton {
  
    public static double solve(double from, double to, double eps, FunctionWithDerivates func) {
        double x = from;
        if (func.f(x) * func.f2(x) < 0) { // знаки различные
            x = to;
        }
        double d;
        do {
            d = func.f(x) / func.f1(x);
            x -= d;
        }
        while (Math.abs(d) > eps);
        return x;
    }
}

Создаем класс, реализующий интерфейс, и осуществляем тестирование:

package ua.inf.iwanoff.sixth;

public class FirstImplementation implements FunctionWithDerivates {

    @Override
    public double f(double x) {
        return Math.sin(x - 0.5);
    }
  
    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new FirstImplementation()));
    }
}

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

f(x) = x3 - 6x2 + 12x - 9

можно так определить первую и вторую производную;

f '(x) = 3x2 - 12x + 12
f ''(x) = 6x - 12

Тогда класс, реализующий интерфейс, может быть таким:

package ua.inf.iwanoff.sixth;

public class SecondImplementation implements FunctionWithDerivates {

    @Override
    public double f(double x) {
        return x * x * x - 6 * x * x + 12 * x - 9;
    }

    @Override
    public double f1(double x) {
        return 3 * x * x - 12 * x + 12;
    }

    @Override
    public double f2(double x) {
        return 6 * x - 12;
    }

    public static void main(String[] args) {
        System.out.println(Newton.solve(0, 1, 0.000001, new SecondImplementation()));
    }
}

Явное задание производных может повысить эффективность алгоритма.

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

3.1 Минимум функции*

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

3.2 Минимум функции с использованием ссылок на методы*

Реализовать программу нахождения минимума функции с использованием ссылок на методы. Использовать стандартные функциональные интерфейсы пакета java.util.function.

3.3 Использование реализации методов интерфейса по умолчанию

Используя интерфейс из примера 2.2 решить задачу нахождения точек экстремумов (корней первой производной).

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

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

 

up