Время от времени я пропускаю трехзначную 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()
Но этот перевод:
- Дорого
- Не правильно работает с пустыми
ResultSets - Не реализовано во всех драйверах 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 . |