Замыкания в Java: что может быть

Как определить — нужна та или иная «фишка» в языке программирования или нет? Можно ли то же самое сделать стандартными средствами, или она настолько необходима, что нужно расширить сам язык новыми конструкциями, тем самим и усложнив его, и упростив?

Появление «настоящих» замыканий в Java может спровоцировать волну новых споров о «чистоте» вроде споров об универсальных типах. Однако здесь снова всё неоднозначно, поскольку замыкания — идея очень простая, однако вместе с тем достаточно мощная.

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

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

Так что же такое замыкание? Говоря неформально, это «вложенная» функция, которая содержит ссылки на свободные переменные (локальные переменные внешней функции). Замыкание создаётся каждый раз при выполнении её «внешней» функции. Мы можем выполнять нашу функцию, передавать в другие функции, а так же возвращать из других функций.

Анонимные классы как замыкания

В некотором своём виде замыкания, или лямбда-функции, в Java уже косвенно поддерживаются через внутренние анонимные классы. Это вполне объектно-ориентированное решение.

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

interface I {
    void Block();
}                 

// ...                 

final String msg = "ADA";
new I() {
    public void Block() {
        System.out.println(msg);
    }
}.Block();

Замыкаем map и each с помощью анонимных классов

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

interface OneArgFunc<T> {
    T DoWork(T a);
}                 

// ...                 

public static <T> Iterable<T> map(Iterable<T> A, OneArgFunc<T> m) {
    ArrayList<T> newA = new ArrayList<T>();
    for (T pe : A)
        newA.add( m.DoWork(pe) );
    return newA;
}                 

// ...                 

Iterable<Integer> result = map(mylist,
    new OneArgFunc<Integer>() {
        public Integer DoWork(Integer a) {
            return a*a;
        }
    });

Достаточно много кода ради такой простой задачи. Помимо этого с анонимными типами мы имеем ещё несколько проблем. Например, мы не можем внутри нашего внутреннего класса изменять внешние переменные. Если мы хотим с ними работать, то мы должны объявить их как final. Так, если у нас есть метод each:

public static <T> void each(Iterable<T> A, OneArgFunc<T> m) {
    for (T pe : A)
        m.Proceed(pe);
}

Мы не можем сделать так:

boolean found = false;
each(mylist, new OneArgFunc<Integer>() {
        public void DoWork(Integer a) {
            if (a == 10)
               found = true; // ошибка! flag должен быть final
        }
});

А если мы, например, захотим кинуть исключение из тела нашего «замыкания», то и тут нас подстерегают сложности и много излишнего кода. Компилятор может сгенерировать его автоматически, предоставив нам возможность записывать то, чего мы хотим добиться, другим, более элегантным способом. Плюс ко всему здесь значению ключевых слов this, break, continue и return придаётся не совсем не тот смысл, который мы можем ожидать от замыкания.

Замыкания как замыкания

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

{int, int => int} sum = {int x, int y => x + y};
sum.invoke(2,3); // -> 5

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

{String => void throws IOException} print = ...

В конечном счёте, для этого генерируются интерфейсы, подобно тому, что мы рассмотрели в предыдущей главе.

Так как замыкания теперь у нас настоящие, мы можем изменять «внешние» переменные.

boolean myflag = false;
{ => void } block = { =>
	myflag = true;
	System.out.println("Hello from closure!");
};
System.out.println(myflag);
block.invoke();
System.out.println(myflag);

Даст следующий вывод:

false
Hello from closure!
true

Замыкаем map и each по-настоящему

Пользуясь новыми возможностями, перепишем нашу функцию map:

public static <T> Iterable<T> map(Iterable<T> a, {T => T} func) {
    ArrayList<T> b = new ArrayList<T>();
    for(T e : a)
        b.add( func.invoke(e) );
    return b;
}

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

Iterable<Integer> result = map(myintlist, {Integer e => e*e} );

Теперь на счёт функции each. Используя следующую версию:

public static <T> void each(Iterable<T> a, {T => void} func) {
    for (T e : a)
        func.invoke(e);
}

Мы можем проделать то, что в прошлый раз не получилось:

boolean found = false;
each(mylist, { int a =>
    if (a == 10) found = true; // всё ОК
});

Ещё пример

Допустим, нам нужно выполнить что-то в отдельном потоке. Мы можем поступить так:

final String message = "hello!";
new Thread(new Runnable() {
    public void run() {
	System.out.print(message);
	System.out.printf("We in thread %d\n",
             Thread.currentThread().getId());
    }
}).start();

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

public static void async(final { => void } block) {
	new Thread( new Runnable() {
		public void run() {
			block.invoke();
		}
	}).start();
}

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

String message = "hello!";
async({ =>
    System.out.println(message);
    System.out.printf("We in thread %d\n",
            Thread.currentThread().getId());
});

Либо вообще так (но у меня следующий вариант не скомпилировался):

String message = "hello!";
async {
    System.out.println(message);
    System.out.printf("We in thread %d\n",
            Thread.currentThread().getId());
}

Заключение

Думаю, введение замыканий в Java может быть хорошей идеей. Они могут помочь не только в работе с контейнерами и алгоритмами, но и других случаях. Однако мне, разбалованному фишками C# 3.0, кажется, что здесь не хватает ещё вывода типов, методов расширения и прочего «сахара». В документации по замыканиям рассказано о «Control invocation syntax», позволяющему ещё больше упростить синтаксис замыканий (например, не писать круглые скобки или => там где без этого можно обойтись), однако запустить эти примеры у меня не получилось. Видимо, всё это ещё в разработке. Тем не менее, радует, что разработка ведётся, и интересно будет проследить за тем, куда она заведёт :).

Дополнительная информация

Страница проекта — http://www.javac.info
Описание замыканий для Java — http://www.javac.info/closures-v05.html
Блог Zdeněk Troníček, посвящённый замыканиям в Java — http://tronicek.blogspot.com/
Пара видео о замыканиях в Java — тут и тут.

Реклама

6 ответов на “Замыкания в Java: что может быть

  1. > Тем не менее, радует, что разработка ведётся,
    > и интересно будет проследить за тем, куда она заведёт

    Пофантазирую… Раз джентльменский набор фенечек уже очерчен и вопрос только «вносить или не вносить», то лет через десять будет нормальным диалог:
    — Вы на каком языке пишете?
    — На ООП.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s