Статьи

Бесконечные множества в Java 9

Множество

Set — это набор элементов, в котором любой данный элемент в Set появляется только один раз.

Более формально, множество не содержит пары элементов e1 и e2 , для которых e1.equals(e2) .

Мы можем легко создать Set в Java 9 следующим образом:

1
2
final Set<Integer> s = Set.of(1, 2, 3);
System.out.println(s);

Это может привести к следующему выводу:

1
[2, 3, 1]

Произведенное выше Set является неизменным, то есть оно не может изменяться, и оно также конечно, потому что в Set есть различное количество элементов, а именно три. Порядок, в котором элементы возвращаются через его методы чтения (такие как stream() , iterator() и forEach() ), не указан.

Бесконечный набор

Бесконечный набор содержит неограниченное количество элементов. Одним примером бесконечного набора является набор всех целых чисел […, -1, 0, 1, 2,…], где целое число не является классом Java Integer а является целым числом в соответствии с математическим определением целого числа, посредством которого существует всегда большее целое число n + 1 для любого заданного целого числа n.

Существует множество бесконечных множеств, таких как множество всех простых чисел, множество четных целых чисел, множество чисел Фибоначчи и т. Д.

По понятным причинам мы не можем предварительно вычислить и сохранить все элементы бесконечного Set Java. Если мы попробуем, у нас в конечном итоге не хватит памяти

Фундаментальный вопрос, который мы должны задать себе: есть ли на самом деле бесконечные множества для типов Java, которые у нас есть? Если у нас есть Set<Byte> то в Set<Byte> не более 256 элементов, и это далеко не бесконечно, то же самое относится и к Short и даже к Integer . В конце концов, существует всего около четырех миллиардов различных объектов Integer и если бы мы использовали битовую карту для представления членства, мы могли бы поместить Set<Integer> всего в 0,5 ГБ. Хоть и большой, но не бесконечный.

Но если мы говорим об элементах Long или String , мы приближаемся, по крайней мере, к практически бесконечным множествам. Для хранения растрового изображения всех длинных потребуется количество ПБ внутренней памяти. Истинный бесконечный Set — это Set String со всеми возможными комбинациями символов [az] любой длины.

Прежде чем мы продолжим, я хотел бы отметить, что код в этом посте также доступен на GitHub, как описано в самом конце поста.

ImmutableStreamSet

Чтобы отойти от парадигмы, в которой мы храним элементы Set , мы могли бы создать ImmutableStreamSet который определяет элементы Set только через его метод stream() . ImmutableStreamSet может быть определен как FunctionalInterface следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@FunctionalInterface
public interface ImmutableStreamSet<E> extends Set<E> {
 
    // This is the only method we need to implements
    @Override
    public Stream<E> stream();
 
    @Override
    default int size() {
        return (int) stream().limit(Integer.MAX_VALUE).count();
    }
 
    @Override
    default boolean contains(Object o) {
        return stream().anyMatch(e -> Objects.equals(e, o));
    }
 
    @Override
    default boolean containsAll(Collection<?> c) {
        return (this == c) ? true : c.stream().allMatch(this::contains);
    }
 
    @Override
    default boolean isEmpty() {
        return !stream().findAny().isPresent();
    }
 
    @Override
    default <T> T[] toArray(T[] a) {
        return stream().collect(toList()).toArray(a);
    }
 
    @Override
    default Object[] toArray() {
        return stream().toArray();
    }
 
    @Override
    default Spliterator<E> spliterator() {
        return stream().spliterator();
    }
 
    @Override
    default Iterator<E> iterator() {
        return stream().iterator();
    }
 
    @Override
    default Stream<E> parallelStream() {
        return stream().parallel();
    }
 
    @Override
    default void forEach(Consumer<? super E> action) {
        stream().forEach(action);
    }
 
    // We are immutable
    @Override
    default boolean removeIf(Predicate<? super E> filter) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default void clear() {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default boolean removeAll(Collection<?> c) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default boolean retainAll(Collection<?> c) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default boolean addAll(Collection<? extends E> c) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    default boolean add(E e) {
        throw new UnsupportedOperationException();
    }
 
    static <E> ImmutableStreamSet<E> of(Supplier<Stream<E>> supplier) {
    // Check out GitHub to see how this Impl class is implemented
        return new ImmutableStreamSetImpl<>(supplier);
    }
 
}

Круто, теперь мы можем создавать бесконечные множества, просто предоставив поставщика потоков, например так:

1
2
ImmutableStreamSet<Long> setOfAllLong
            = LongStream.rangeClosed(Long.MIN_VALUE, Long.MAX_VALUE)::boxed;

Это создаст Set всех Long значений (например, с 2 ^ 64 элементами). При предоставлении поставщика потока обязательно убедитесь, что он придерживается свойства Set уникальности элемента. Рассмотрим следующий незаконный набор:

1
2
final ImmutableStreamSet<Long> illegalSet =
            () -> Stream.of(1l, 2l, 1l);

Ясно, что 11 встречается два раза в наборе, что заставляет этот объект нарушать требования набора.

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

PositiveLongSet

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class PositiveLongSet implements ImmutableStreamSet<Long> {
 
    public static final PositiveLongSet INSTANCE = new PositiveLongSet();
 
    private PositiveLongSet() {
    }
 
    @Override
    public Stream<Long> stream() {
        return LongStream.rangeClosed(1, Long.MAX_VALUE).boxed();
    }
 
    @Override
    public int size() {
        return Integer.MAX_VALUE;
    }
 
    @Override
    public boolean contains(Object o) {
        return SetUtil.contains(this, Long.class, other -> other > 0, o);
    }
 
    @Override
    public boolean isEmpty() {
        return false;
    }
 
    @Override
    public String toString() {
        return SetUtil.toString(this);
    }
 
}

Обратите внимание, как мы соблюдаем формальное требование в методе size() где мы возвращаем Integer.MAX_VALUE хотя Set намного больше. Если бы Set был определен сегодня, вполне вероятно, что size() вернул бы long вместо int . Но в начале 90-х годов объем внутренней оперативной памяти обычно составлял менее 1 ГБ. Мы используем два служебных метода в классе:

SetUtil.toString() принимает Set , перебирает первые восемь элементов и возвращает String представление этих элементов.

Метод SetUtil.contains() принимает Set , класс типа Element (здесь Long.class ) и Predicate который вызывается, если объект, который мы сравниваем, имеет заданный класс типа Element (если объект, с которым мы сравниваем, является null или другого типа, тогда тривиально Set не содержит объект).

Вот как выглядит SetUtil :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
final class SetUtil {
 
    private static final int TO_STRING_MAX_ELEMENTS = 8;
 
    static <E> String toString(Set<E> set) {
        final List<String> first = set.stream()
            .limit(TO_STRING_MAX_ELEMENTS + 1)
            .map(Object::toString)
            .collect(toList());
 
        final String endMarker = first.size() > TO_STRING_MAX_ELEMENTS ? ", ...]" : "]";
 
        return first.stream()
            .limit(TO_STRING_MAX_ELEMENTS)
            .collect(
                joining(", ", "[", endMarker)
            );
    }
 
    static <E> boolean contains(
        final Set<E> set,
        final Class<E> clazz,
        final Predicate<E> predicate,
        final Object o
    ) {
        if (o == null) {
            return false;
        }
        if (!(clazz.isAssignableFrom(o.getClass()))) {
            return false;
        }
        final E other = clazz.cast(o);
        return predicate.test(other);
    }
 
}

Вооружившись классами SetUtil и SetUtil мы теперь можем легко создавать другие бесконечные наборы, такие как PostitiveEvenLongSet (не показан ниже, попробуйте написать его самостоятельно), PrimeLongSet (содержащий все простые числа, которые могут быть представлены Long ) и даже FibonacciLongSet (содержащий все числа FibonacciLongSet что может быть представлено Long ). Вот как могут выглядеть эти классы:

PrimeLongSet

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public final class PrimeLongSet implements ImmutableStreamSet<Long> {
 
    public static final PrimeLongSet INSTANCE = new PrimeLongSet();
 
    private PrimeLongSet() {
    }
 
    private static final LongPredicate IS_PRIME =
        x -> LongStream.rangeClosed(2, (long) Math.sqrt(x)).allMatch(n -> x % n != 0);
 
    @Override
    public Stream<Long> stream() {
        return LongStream.rangeClosed(2, Long.MAX_VALUE)
            .filter(IS_PRIME)
            .boxed();
    }
 
    @Override
    public int size() {
        return Integer.MAX_VALUE;
    }
 
    @Override
    public boolean contains(Object o) {
        return SetUtil.contains(this, Long.class, IS_PRIME::test, o);
    }
 
    @Override
    public boolean isEmpty() {
        return false;
    }
 
    @Override
    public String toString() {
        return SetUtil.toString(this);
    }
 
}

FibonacciLongSet

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public final class FibonacciLongSet implements ImmutableStreamSet<Long> {
 
    public static final FibonacciLongSet INSTANCE = new FibonacciLongSet();
 
    private FibonacciLongSet() {
    }
 
    @Override
    public Stream<Long> stream() {
        return Stream.concat(
            Stream.of(0l),
            Stream.iterate(new Fibonacci(0, 1), Fibonacci::next)
                .mapToLong(Fibonacci::getAsLong)
                .takeWhile(fib -> fib > 0)
                .boxed()
        );
    }
 
    @Override
    public int size() {
        return 92;
    }
 
    @Override
    public boolean contains(Object o) {
        return SetUtil.contains(
            this,
            Long.class,
            other -> stream().anyMatch(fib -> Objects.equals(fib, other)),
            o
        );
    }
 
    @Override
    public boolean isEmpty() {
        return false;
    }
 
    @Override
    public String toString() {
        return SetUtil.toString(this);
    }
 
    private static class Fibonacci {
 
        final long beforeLast;
        final long last;
 
        public Fibonacci(long beforeLast, long last) {
            this.beforeLast = beforeLast;
            this.last = last;
        }
 
        public Fibonacci next() {
            return new Fibonacci(last, last + beforeLast);
        }
 
        public long getAsLong() {
            return beforeLast + last;
        }
 
    }
 
}

Обратите внимание, как мы используем Stream::takeWhile чтобы разорвать поток, когда long оборачивается в отрицательное значение. Возможно, мы «обманываем», когда выполняем предварительные вычисления и предоставляем размер 92, но в противном случае size() был бы немного медленнее.

Сшить все это

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public interface Sets {
 
    static Set<Long> positiveLongSet() {
        return PositiveLongSet.INSTANCE;
    }
 
    static Set<Long> positiveEvenLongSet() {
        return PositiveEvenLongSet.INSTANCE;
    }
 
    static Set<Long> primeLongSet() {
        return PrimeLongSet.INSTANCE;
    }
 
    static Set<Long> fibonacciLongSet() {
        return FibonacciLongSet.INSTANCE;
    }
 
}

Мы также можем инкапсулировать наш код в модуль Java 9, чтобы убедиться, что видны только классы Sets и ImmutableStreamSet , выставив их в самом верхнем пакете проектов и поместив все остальные классы в пакет с именем «internal» (который не выставляется ).

Вот module-info.java может выглядеть наш module-info.java при условии, что два открытых класса находятся в com.speedment.infinite_sets а классы реализации — в пакете, подобном com.speedment.infinite_sets.internal :

module-info.java

1
2
3
module com.speedment.infinite_sets {
    exports com.speedment.infinite_sets;
}

Пробовать

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

module-info.java

1
2
3
module Infinite_sets_app {
    requires com.speedment.infinite_sets;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import static com.speedment.infinite_sets.Sets.*;
public class Main {
 
    public static void main(String[] args) {
 
        Stream.of(
            Set.of(1, 2, 3),
            positiveLongSet(),
            positiveEvenLongSet(),
            primeLongSet(),
            fibonacciLongSet()
        ).forEachOrdered(System.out::println);
 
        // This actually completes fast due to identity equality
        positiveLongSet().containsAll(positiveLongSet());
 
    }
 
}

Это может привести к следующему выводу:

1
2
3
4
5
[3, 2, 1]
[1, 2, 3, 4, 5, 6, 7, 8, ...]
[2, 4, 6, 8, 10, 12, 14, 16, ...]
[2, 3, 5, 7, 11, 13, 17, 19, ...]
[0, 1, 2, 3, 5, 8, 13, 21, ...]

Заниматься на GitHub

Исходный код в этом посте доступен на GitHub здесь .
Игра, сет и матч …

Смотрите оригинальную статью здесь: Бесконечные множества в Java 9

Мнения, высказанные участниками Java Code Geeks, являются их собственными.