Время от времени я пропускаю трехзначную 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 . |