Можно ли разумно эмулировать синтаксис yield, возможно, с помощью Java 8?
Я экспериментировал с этим вопросом сегодня, от Эйлера.
Палиндромное число читается одинаково в обоих направлениях. Самый большой палиндром из двух двузначных чисел равен 9009 = 91 × 99.
Найдите самый большой палиндром из двух трехзначных чисел.
Я думал об этом, и это, конечно, можно сделать с помощью циклов for, однако я хочу использовать Java 8, так как он открывает новые возможности.
Однако, прежде всего, я не знаю, как генерировать IntStream
который производит такие элементы, так что я все равно использовал обычные циклы for:
public class Problem4 extends Problem<Integer> {
private final int digitsCount;
private int min;
private int max;
public Problem4(final int digitsCount) {
this.digitsCount = digitsCount;
}
@Override
public void run() {
List<Integer> list = new ArrayList<>();
min = (int)Math.pow(10, digitsCount - 1);
max = min * 10;
for (int i = min; i < max; i++) {
for (int j = min; j < max; j++) {
int sum = i * j;
if (isPalindrome(sum)) {
list.add(sum);
}
}
}
result = list.stream().mapToInt(i -> i).max().getAsInt();
}
private boolean isPalindrome(final int number) {
String numberString = String.valueOf(number);
String reversed = new StringBuilder(numberString).reverse().toString();
return (numberString.equals(reversed));
}
@Override
public String getName() {
return "Problem 4";
}
}
Как вы можете видеть, я могу быть немного ленивым, IntStream::max
Это очень хороший метод, и я думаю, что лучше использовать его, чтобы написать его самостоятельно.
Здесь возникает проблема, хотя, мне нужно иметь list
Теперь я могу получить максимум таким способом, а это значит, что мне нужно хранить данные там, где я действительно не должен этого делать.
Итак, вопрос сейчас, возможно ли реализовать это в Java 8?
for (int i = min; i < max; i++) {
for (int j = min; j < max; j++) {
yield i * j;
}
}
А затем из этого метода создать PrimitiveIterator.OfInt
(распаковывает версию Iterator<Integer>
или создайте IntStream
напрямую?
Затем получить ответ с streamFromYield.filter(this::isPalindrome).max().getAsInt()
было бы действительно легко реализовать.
Наконец, я знаю, что этот вопрос задавался ранее, однако в последний раз это было совсем немного назад, и теперь Java 8 появится очень скоро, где они были добавлены как большая концепция. Stream<T>
и новая языковая конструкция, называемая лямбдами.
Поэтому создание такого кода сейчас может сильно отличаться от того, когда люди создавали его для Java 6 или 7.
4 ответа
Ну, я думаю, что мы увлеклись использованием Streams API извне, используя flatMap
оптимизация алгоритма поиска палиндрома и т. д. См. ответы Бориса Паука и ассилий. Тем не менее, мы обошли первоначальный вопрос о том, как написать функцию генератора, используя что-то вроде Python yield
заявление. (Я думаю, что OP является вложенным, например, с yield
использовал Python.)
Одна из проблем с использованием flatMap
является то, что параллельное расщепление может происходить только на самом внешнем потоке. Внутренние потоки (возвращенные из flatMap
) обрабатываются последовательно. Мы могли бы попытаться сделать внутренние потоки также параллельными, но они могли бы конкурировать с внешними. Я полагаю, что вложенное разбиение может работать, но я не слишком уверен в этом.
Одним из подходов является использование Stream.generate
или (как ответ Ассилия) Stream.iterate
функции. Они создают бесконечные потоки, поэтому внешний limit
должен быть предоставлен для завершения потока.
Было бы хорошо, если бы мы могли создать конечный, но "сплющенный" поток, чтобы весь поток значений подвергался расщеплению. К сожалению, создание потока не так удобно, как функции генератора Python. Это может быть сделано без особых проблем. Вот пример, который использует StreamSupport
а также AbstractSpliterator
классы:
class Generator extends Spliterators.AbstractIntSpliterator {
final int min;
final int max;
int i;
int j;
public Generator(int min, int max) {
super((max - min) * (max - min), 0);
this.min = min;
this.max = max;
i = min;
j = min;
}
public boolean tryAdvance(IntConsumer ic) {
if (i == max) {
return false;
}
ic.accept(i * j);
j++;
if (j == max) {
i++;
j = min;
}
return true;
}
}
public static void main(String[] args) {
Generator gen = new Generator(100, 1000);
System.out.println(
StreamSupport.intStream(gen, false)
.filter(i -> isPalindrome(i))
.max()
.getAsInt());
}
Вместо того, чтобы переменные итерации находились в стеке (как в методе вложенных форм с выходом), мы должны сделать их полями объекта и иметь tryAdvance
увеличивайте их до тех пор, пока итерация не будет завершена. Теперь, это самая простая форма сплитератора, и она не обязательно хорошо распараллеливается. С дополнительной работой можно реализовать trySplit
метод для лучшего разделения, что, в свою очередь, позволило бы улучшить параллелизм.
forEachRemaining
метод может быть переопределен, и он будет выглядеть почти как пример вложенного цикла с выходом, вызывая IntConsumer
вместо yield
, к несчастью tryAdvance
является абстрактным и, следовательно, должен быть реализован, поэтому все еще необходимо, чтобы переменные итерации были полями объекта.
Как насчет того, чтобы смотреть на это с другой стороны:
Вы хотите Stream
из [100,1000)
и для каждого элемента этого Stream
ты хочешь другого Stream
этого элемента, умноженного на каждый из [100, 1000)
, Это то, что flatMap
для:
public static void main(final String[] args) throws Exception {
OptionalInt max = IntStream.range(100, 1000).
flatMap((i) -> IntStream.range(i, 1000).map((j) -> i * j)).
unordered().
parallel().
filter((i) -> {
String forward = Integer.toString(i);
String backward = new StringBuilder(forward).reverse().toString();
return forward.equals(backward);
}).
max();
System.out.println(max);
}
Не уверен, если получаю String
и наоборот, самый эффективный способ обнаружить палиндромы, с моей головы это может показаться быстрее:
final String asString = Integer.toString(i);
for (int j = 0, k = asString.length() - 1; j < k; j++, k--) {
if (asString.charAt(j) != asString.charAt(k)) {
return false;
}
}
return true;
Он дает тот же ответ, но я не подверг его тщательному тестированию... Кажется, на моей машине он работает примерно на 100 мс быстрее.
Также не уверен, что эта проблема достаточно велика для unordered().parallel()
- удаление, которое также дает небольшой прирост скорости.
Просто пытался продемонстрировать возможности Stream
API.
РЕДАКТИРОВАТЬ
Как указывает @Stuart в комментариях, так как умножение является коммутативным, нам нужно только IntStream.range(i, 1000)
в подпотоке. Это потому, что как только мы проверим a x b
нам не нужно проверять b x a
, Я обновил ответ.
Всегда были способы подражать этому переоцененному yield
особенность, даже без Java 8. По сути, речь идет о сохранении состояния выполнения, т. е. стековых фреймов, которые могут быть выполнены потоком. Очень простая реализация может выглядеть так:
import java.util.Iterator;
import java.util.NoSuchElementException;
public abstract class Yield<E> implements Iterable<E> {
protected interface Flow<T> { void yield(T item); }
private final class State implements Runnable, Iterator<E>, Flow<E> {
private E nextValue;
private boolean finished, value;
public synchronized boolean hasNext() {
while(!(value|finished)) try { wait(); } catch(InterruptedException ex){}
return value;
}
public synchronized E next() {
while(!(value|finished)) try { wait(); } catch(InterruptedException ex){}
if(!value) throw new NoSuchElementException();
final E next = nextValue;
value=false;
notify();
return next;
}
public void remove() { throw new UnsupportedOperationException(); }
public void run() {
try { produce(this); }
finally {
synchronized(this) {
finished=true;
notify();
}
}
}
public synchronized void yield(E next) {
while(value) try { wait(); } catch(InterruptedException ex){}
nextValue=next;
value=true;
notify();
}
}
protected abstract void produce(Flow<E> f);
public Iterator<E> iterator() {
final State state = new State();
new Thread(state).start();
return state;
}
}
Если у вас есть такой вспомогательный класс, сценарий использования будет выглядеть просто:
// implement a logic the yield-style
Iterable<Integer> y=new Yield<Integer>() {
protected void produce(Flow<Integer> f) {
for (int i = min; i < max; i++) {
for (int j = min; j < max; j++) {
f.yield(i * j);
}
}
}
};
// use the Iterable, e.g. in a for-each loop
int maxPalindrome=0;
for(int i:y) if(isPalindrome(i) && i>maxPalindrome) maxPalindrome=i;
System.out.println(maxPalindrome);
Предыдущий код не использовал никаких функций Java 8. Но это позволит использовать их без каких-либо изменений:
// the Java 8 way
StreamSupport.stream(y.spliterator(), false).filter(i->isPalindrome(i))
.max(Integer::compare).ifPresent(System.out::println);
Обратите внимание, что Yield
Класс поддержки выше - не самая эффективная реализация, и он не обрабатывает случай, если итерация не завершена, но Iterator
отказались. Но это показывает, что такую логику действительно возможно реализовать в Java (в то время как другие ответы убедительно показывают, что такая yield
логика не нужна для решения такой проблемы).
Я попробую. Версия с петлей, затем с потоком. Хотя я начинаю с другого конца, так что легче, потому что я могу limit(1)
,
public class Problem0004 {
public static void main(String[] args) {
int maxNumber = 999 * 999;
//with a loop
for (int i = maxNumber; i > 0; i--) {
if (isPalindrome(i) && has3DigitsFactors(i)) {
System.out.println(i);
break;
}
}
//with a stream
IntStream.iterate(maxNumber, i -> i - 1)
.parallel()
.filter(i -> isPalindrome(i) && has3DigitsFactors(i))
.limit(1)
.forEach(System.out::println);
}
private static boolean isPalindrome(int n) {
StringBuilder numbers = new StringBuilder(String.valueOf(n));
return numbers.toString().equals(numbers.reverse().toString());
}
private static boolean has3DigitsFactors(int n) {
for (int i = 999; i > 0; i--) {
if (n % i == 0 && n / i < 1000) {
return true;
}
}
return false;
}
}