Existem basicamente duas perguntas que justifiquem a conversão de um objeto qualquer pra boolean:
- esse é um objeto "válido"?
- esse é um objeto que representa um boolean serializado?
O caso [2] só se aplica a inteiros (0
ou 1
), strings ('true'
ou 'false'
), e booleans propriamente ditos (true
, false
). Um nível aceitável de coersão implícita é o null
, que é por padrão considerado como false
(e como consequência, 'null'
é considerado uma representação do boolean false
).
O caso [1] se aplica a qualquer objeto, e a interpretação é subjetiva e depende do domínio. É considerada boa prática aceitar que, para casos de string, a interpretação de uma string válida seja "uma string que seja não-nula e não-vazia".
Já o caso [2], pra strings, abrange representações como 'y'
, 'yes'
e 'on'
, que podem vir da serialização de um input[checkbox], ou de um formulário qualquer, por exemplo.
Existem várias alternativas pra executar cada uma dessas alternativas em Groovy/Java, algumas mais verbosas que as outras, mas existem extrapolações na subjetividade do que pode representar um boolean serializado que precisam de atenção.
Considere o seguinte script groovy que testa vários objetos diferentes em várias alternativas de coersão de objeto para boolean:
final List<List<Object>> x = []
final List<Object> strings = [
'', '0', 'n', 'no', 'off', 'f', 'false', 'FALSE', 'FaLsE',
'what', '1', 'y', 'yes', 'on', 't', 'true', 'TRUE', 'tRuE',
false, true, null, 0, 1, [], [''], [:], [a: ''],
]
Object handleError(Closure cl) {
try {
return cl()
} catch (Exception ignored) {
return 'ERRO'
}
}
for (final Object s in strings) {
final List<Object> _x = []
_x.add(handleError { !!s })
_x.add(handleError { s as Boolean })
_x.add(handleError { s?.asBoolean() })
_x.add(handleError { s?.toBoolean() })
_x.add(handleError { new Boolean(s as String) })
x.add(_x)
}
final List<String> options = ["!!s", "s as Boolean", "s?.asBoolean()", "s?.toBoolean()", "new Boolean(s as String)"]
final int biggestOptionLength = options.collect { it.length() }.max()
final List<String> t = options.collect { it.center(biggestOptionLength, ' ') }
println(("=" * biggestOptionLength) + strings.collect { it.inspect().center(8, ' ') }.join(""))
x.transpose().eachWithIndex { final List<Object> r, final int idx ->
println(t[idx] + r.collect { it.inspect().center(8, ' ') }.join(""))
}
return
O resultado da execução acima é:
======================== '' '0' 'n' 'no' 'off' 'f' 'false' 'FALSE' 'FaLsE' 'what' '1' 'y' 'yes' 'on' 't' 'true' 'TRUE' 'tRuE' false true null 0 1 [] [''] [:] ['a':'']
!!s false true true true true true true true true true true true true true true true true true false true false false true false true false true
s as Boolean false true true true true true true true true true true true true true true true true true false true null false true false true false true
s?.asBoolean() false true true true true true true true true true true true true true true true true true false true null false true false true false true
s?.toBoolean() false false false false false false false false false false true true false false false true true true false true null 'ERRO' 'ERRO' 'ERRO' 'ERRO' 'ERRO' 'ERRO'
new Boolean(s as String) false false false false false false false false false false false false false false false true true true false true false false false false false false false
Observações relevantes:
-
s?.toBoolean()
é a alternativa mais perigosa, abrangente, e instável. Ela não está disponível para todos os tipos de objetos, e precisa de um safe navigator pra ser parcialmente seguro (NPE). A coersão feita por esse método é dependende de domínio, e a sugestão é que, se a string em questão pode conter uma representação não convencional de um boolean, que a conversão/coersão seja feita manualmente, explicitamente, por quem conhece do domínio do objeto. - as três primeiras alternativas são equivalentes (chegam ao mesmo ponto na cadeia de execução do groovy), e representam fielmente o caso [1] citado no começo do card, sendo que a alternativa
s as Boolean
possui o menor peso sintático, pois não requer um safe navigator e expõe explicitamente pra IDE qual o tipo da coersão (e por isso, apesar de todas serem equivalentes, ela é a recomendada quando legibilidade e null-safety forem prioridades). -
new Boolean(s as String)
é a maneira mais limpa de representar o caso [2], em que busca-se saber se um objeto é a representação de um boolean serializado. Perceba que existe uma coersão interna explícitas as String
que tem quase zero de impacto na performance, aumenta significativamente a confiança do código, dá ao compilador/IDE informação suficiente pra eliminar ambiguidade na chamada do construtor, e não sofre de problemas de coersão como NPE e/ou NoSuchMethod. Portanto, a coersão pra string é necessária pra qualidade de código ótima.
Observações/recomendações adicionais:
- sempre que possível, trate/diferencie devidamente o valor
null
em relação aos otros possíveis valores do objeto, independente do tipo esperado do mesmo. - sempre que possível, descreva de maneira explícita a interpretação esperada do que é um objeto "válido", como por exemplo:
s != null
, ous != null && s.length() != 0
ou atés != null && s.trim().length() != 0
- contextos naturalmente booleanos, como
if
s ou a condição de umfor
, sempre farão a coersãoasType
do groovy, que farábooleanUnbox
, executando uma lógica relativamente extensa (apesar de otimizada e simples) até chegar em uma representação do "groovy truth" do objeto. Essa cadeia de execução tem impacto significativo no tempo de execução e consequentemente na performance do código, caso ele seja chamado com alta frequência. Por conta disso, o ponto anterior é importante: seja explícito sempre que possível, evite coersões implícitas em contextos booleanos. - conheça o domínio do objeto quando ele não for trivial. Anote/documente o domínio se ele não for trivial. Enforce/reforce um domínio restrito se possível, o mais cedo possível.