Статьи

Булевы три состояния в Java

Время от времени я пропускаю трехзначную BOOLEAN семантику SQL в Java. В SQL мы имеем:

  • TRUE
  • FALSE
  • UNKNOWN (также известный как NULL )

Время от времени я нахожусь в ситуации, когда мне хотелось бы также выразить эту UNKNOWN или UNINITIALISED семантику в Java, когда просто true и false недостаточно.

Реализация ResultSetIterator

Например, при реализации ResultSetIterator для jOOλ , простая библиотека, моделирующая потоки SQL для Java 8 :

1
2
3
4
5
6
7
SQL.stream(stmt, Unchecked.function(r ->
    new SQLGoodies.Schema(
        r.getString("FIELD_1"),
        r.getBoolean("FIELD_2")
    )
))
.forEach(System.out::println);

Чтобы реализовать поток Java 8 , нам нужно создать Iterator , который мы затем можем передать новому методу Spliterators.spliteratorUnknownSize () :

1
2
3
4
StreamSupport.stream(
  Spliterators.spliteratorUnknownSize(iterator, 0),
  false
);

Другой пример этого можно увидеть здесь, на переполнении стека .

При реализации интерфейса Iterator мы должны реализовать hasNext() и next() . Обратите внимание, что в Java 8 метод remove () теперь имеет реализацию по умолчанию, поэтому нам больше не нужно его реализовывать.

Хотя большую часть времени вызову next() предшествует вызов hasNext() ровно один раз, ничто в контракте Iterator требует этого. Это прекрасно писать:

01
02
03
04
05
06
07
08
09
10
11
if (it.hasNext()) {
    // Some stuff
 
    // Double-check again to be sure
    if (it.hasNext() && it.hasNext()) {
 
        // Yes, we're paranoid
        if (it.hasNext())
            it.next();
    }
}

Как перевести вызовы Iterator в резервные вызовы в JDBC ResultSet ? Нам нужно вызвать ResultSet.next() .

Мы могли бы сделать следующий перевод:

  • Iterator.hasNext() == !ResultSet.isLast()
  • Iterator.next() == ResultSet.next()

Но этот перевод:

  • Дорого
  • Не правильно работает с пустыми ResultSet s
  • Не реализовано во всех драйверах JDBC (поддержка метода isLast является необязательной для ResultSets с типом набора результатов TYPE_FORWARD_ONLY)

Итак, нам нужно будет поддерживать внутренний флаг, который говорит нам:

  • Если мы уже вызвали ResultSet.next()
  • Каков был результат этого звонка

Вместо того чтобы создавать вторую переменную, почему бы не использовать трехзначный java.lang.Boolean . Вот возможная реализация из jOOλ :

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
class ResultSetIterator<T> implements Iterator<T> {
 
    final Supplier<? extends ResultSet>  supplier;
    final Function<ResultSet, T>         rowFunction;
    final Consumer<? super SQLException> translator;
 
    /**
     * Whether the underlying {@link ResultSet} has
     * a next row. This boolean has three states:
     * <ul>
     * <li>null:  it's not known whether there
     *            is a next row</li>
     * <li>true:  there is a next row, and it
     *            has been pre-fetched</li>
     * <li>false: there aren't any next rows</li>
     * </ul>
     */
    Boolean hasNext;
    ResultSet rs;
 
    ResultSetIterator(
        Supplier<? extends ResultSet> supplier,
        Function<ResultSet, T> rowFunction,
        Consumer<? super SQLException> translator
    ) {
        this.supplier = supplier;
        this.rowFunction = rowFunction;
        this.translator = translator;
    }
 
    private ResultSet rs() {
        return (rs == null)
             ? (rs = supplier.get())
             :  rs;
    }
 
    @Override
    public boolean hasNext() {
        try {
            if (hasNext == null) {
                hasNext = rs().next();
            }
 
            return hasNext;
        }
        catch (SQLException e) {
            translator.accept(e);
            throw new IllegalStateException(e);
        }
    }
 
    @Override
    public T next() {
        try {
            if (hasNext == null) {
                rs().next();
            }
 
            return rowFunction.apply(rs());
        }
        catch (SQLException e) {
            translator.accept(e);
            throw new IllegalStateException(e);
        }
        finally {
            hasNext = null;
        }
    }
}

Как видите, метод hasNext() локально кэширует трехзначное логическое состояние hasNext только если оно ранее было null . Это означает, что вызов hasNext() несколько раз не будет иметь никакого эффекта, пока вы не hasNext next() , который сбрасывает hasNext кэширования hasNext .

И hasNext() и next() при необходимости перемещают курсор ResultSet .

Читаемость?

Некоторые из вас могут утверждать, что это не помогает удобочитаемости. Они вводят новую переменную, такую ​​как:

1
2
boolean hasNext;
boolean hasHasNextBeenCalled;

Проблема в том, что вы все еще реализуете трехзначное логическое состояние, но распределяете его по двум переменным, которые очень трудно назвать так, чтобы они были действительно более читабельными, чем реальное решение java.lang.Boolean . Кроме того, на самом деле существует четыре значения состояния для двух boolean переменных, поэтому риск ошибок увеличивается незначительно.

Каждое правило имеет свое исключение. Использование null для вышеуказанной семантики — очень хорошее исключение из null -is-bad histeria, которая продолжается с момента появления Option / Optional

Другими словами: какой подход лучше? Там нет TRUE или FALSE ответ, только UNKNOWN !

Будь осторожен с этим

Однако, как мы уже говорили в предыдущем посте в блоге , вам следует избегать возврата null из методов API, если это возможно. В этом случае использование null в качестве средства для моделирования состояния вполне допустимо, поскольку эта модель инкапсулирована в нашем ResultSetIterator . Но постарайтесь избежать утечки такого состояния за пределы вашего API.

Ссылка: Booleans с тремя состояниями на Java от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .