Saturday, December 1, 2007

To final or not to final? That is the question...

Essa frase Shakespeareana provavelmente já rendeu mais trocadilhos do que qualquer outra frase na história da humanidade. Mas é incrível a quantidade de situações que ela se aplica, não? De qualquer forma, o motivo do post não é discutir Shakespeare, mas sim a palavra chave final do Java.

A maioria dos desenvolvedores em Java estão bastante acostumados em ver esse carinha associado com sua quase comanheira de longa data static para formar constantes. Constantes em Java além de permitir algum grau a mais de manutenção e prevenção de bugs ajudam o compilador a fazer algumas pequenas otimizações. Como assim "constantes ajudam na manutenção e prevenção de bugs"? Simples, vamos supor que precisamos desenvolver uma classe com diversas funções de manipulação de circunferências. Nessa classe provavelmente vamos usar uma porção de vezes a constante matemática PI. Se a cada vez que fôssemos usá-la fosse necessário escrever o literal 3.14159 aumentamos muito o risco de usar valores diferentes de PI em métodos diferentes. Imagine que um desenvolvedor novato precisou codificar o método para calcular a área do circunfência, e usou 3.1415 como valor de PI, e um outro desenvolvedor fez um método para calcular o perímetro da circunfêrencia e usou o valor de PI como 3.14, afinal ele é um desenvolvedor mais experiente e sabe que 2 casas de precisão são suficientes para o projeto (!?). Alguém que fosse usar as 2 funções em momentos diferentes poderia ter resultados diferentes. Já temos a "precisão" característica do ponto flutuante, e ainda introduzimos uma variação nos próprios valores usados nos cálculos. Nada bom pode sair disso. A solução aqui é bastante trivial: definir uma constante final static PI, e usá-la ao invés do valor hardcoded de pi. Na verdade, uma solução melhor é usar a constante PI da classe Math do Java ;). Mas esse tipo de coisa não prejudica a performance? Não, pois o compilador do Java na primeira passada da compilação (são 3, acho) ele troca as referências das constantes de tipos primitivos e Strings pelos valores declarados, então o efeito no código é o mesmo de ter os valores hardcoded. Mas por que só primitivos e String? As demais variáveis são referências, o código ficaria incorreto:

private static final Pessoa GERENTE = new Pessoa("Gerente");

...
public void desenvolvedorFezMerda(Pessoa desenvolvedor) {
GERENTE.mata(desenvolvedor);
}

public void desenvolvedorEntregouAntesDoPrazo(Pessoa desenvolvedor) {
GERENTE.parabeniza(desenvolvedor);
}
}


Se o compilador substituísse a refeência da constante GERENTE, os 2 métodos usariam uma instância diferente da classe Pessoa, o que não estaria de acordo com a lógica do código fonte.

Mas essa característica de inline de constantes pode trazer alguns problemas para desenvolvedores descuidados. Suponha que você colocou uma constante public static final int MAX_VALUE = 100 numa classe de sua biblioteca. Alguma aplicação externa faz uso de sua biblioteca e em certo momento faz uma comparação (value > MAX_VALUE). Como Murphy ainda impera na informática, algum tempo depois que o código entrar em produção vai surgir a necessidade de um value maior que 100 ser usado em uma condição válida. Solução óbvia: redefine o valor da constante MAX_VALUE (supondo que não há nenhuma outra lógica que impeça isso), recompila a biblioteca, troca o JAR na aplicação do cliente e voilà, tudo pronto, certo? Errado. No momento que a aplicação foi compilada o Java trocou MAX_VALUE por 100, então a alteração não faz efeito sem uma recompilação da aplicação.

Mas o uso do final não é restrito a membros static nas classes para evitar coisas hardcoded simplesmente. Na verdade, o final pode aparecer praticamente em qualquer lugar, e em alguns lugares ele deve aparecer. Um pouco sobre a biografia do final:

final, s.m.: Texto da família palavras-chave, do gênero Java. Pode ser visto habitando classes não abstratas, métodos, parâmetros e variáveis locais em códigos Java.


Na verdade, o final tem várias aplicações em um código Java. Declarar uma classe como final é equivalente a dizer ao compilador que a classe está completa e não pode dar origem a subclasses. Exemplos de uso seriam para classes utilitárias, onde gerar subclasses pode levar a sobreescrição de métodos importantes, ou classes singleton, etc. De uma forma genérica, todas as classes que definem apenas construtores private são automaticamente final. Eu particularmente não recordo de algum caso em que o uso do final fosse importante em alguma classe. Talvez classes que validam restrições de segurança devem fazer bom uso desse cara, para evitar que algum código malicioso troque em tempo de execução uma instância de um autenticador de login, por exemplo. Algo bastante óbvio que surge da característica do final é que não é possível declarar uma classe simultaneamente abstract e final, pois o último modificador não permite a geração de subclasses e o primeiro exige que uma subclasse complete alguns métodos não definidos.

Um outro lugar onde é possível usar o final é na declaração de métodos. Tornar um método final é equivalente a dizer para o Java que nenhuma subclasse pode redefinir aquele método. Essa restrição é garantida tanto em tempo de compilação através de erros, quanto de execução através de um VerifyError.

Existe uma pequena discussão sobre o uso do final nesses casos, pois em geral é bastante difíci prever quando alguma classe precisará ser extendida. Eu mesmo ao longo do desenvolvimento de software vi alguns casos em que uma classe não foi pensada para gerar subclasses e em algum momento na manutenção do sistema uma subclasse foi necessária. Então a recomendação que eu deixo é somente usar o final em classes ou métodos se houver uma razão no design do sistema para isso.

Agora o uso do final nos pontos que me deixaram mais curioso sobre ele e me levaram a esse post. Uso de final em variáveis, parâmetros e atributos de classe é útil? Bom, pra tentar responder essa pergunta eu fui atrás de algumas fontes, e acabei cruzando com um livro do O'Relly, "Hardcore Java". A julgar pelo conteúdo, parece um livro muito bom. Consegui acesso ao 2o capítulo do livro em PDF, não tenho certeza se a lincesa me permite linkar aqui, então vou deixar o link de fora, mas uma busca no google deve trazer a referência ;). Ele dedicou 31 páginas falando os diversos usos do final. Em geral, ele recomenda usar o final em todas as variáveis, atributos e parâmetros, principalmente pelos benefícios que o uso traz em relação a manutenção de código. Achei alguns exemplos forçados, mas tem algum fundo de verdade. Já vi alguns casos de código em que variáveis de nomes semelhantes são usadas ao longo de um método e fica bastante simples atribuir o valor na variável errada. Um ponto a favor do final para esses casos: ele forçaria um erro de lógica a aparecer durante a compilação - ou da edição, se estiver usando uma boa IDE. Sem dúvida, detectar e corrigir um erro nessa fase é bem mais barato do que durante os testes, ou, de acordo com Murphy, quando os clientes começarem a gritar pedindo uma correção para o problema do relatório X estar dizendo que o salário do faxineiro é negativo. Praticamente só esse argumento (de detectar o problema antes, e não o salário) já me deixou com uma coceira para ligar todas as opções de inserção de final do Eclipse (nesse post). Até então eu liguei apenas para ele inserir nos atributos private. Mas vamos lá, um pouco mais de desconfiança e vamos ver se há mais benefícios. Indo na especificação da linguagem Java, há razoavelmente bastante referências ao final, mas nenhuma dizendo claramente as vantagens de usá-lo. Então uma pequena busca no google, e alguns foruns mais tarde, a conclusão que parece óbvia. Tanto os compiladores quanto a própria JVM podem se valer da informação de algo ser final para aplicar otimizações. Uma variável ser declarada como final pode fazer ela ir parar em uma área especial da heap, fazendo o acesso ser ligeiramente mais rápido. Observe o "pode" nessa frase. Eu fiz alguns testes com os compiladores do Java 6 e do Eclipse e o bytecode gerado com e sem final foi o mesmo. Nem sequer uma indicação de se a variável é final ou não. Vi algumas referências de fontes "confiáveis" (leia: posts em foruns) de que a máquina virtual consegue otimizar essas coisas. Mas procurando um pouco mais, fiu atras da especificação da máquina virtual, e no bytecode só é anotado se um atributo é final, e não se uma variável ou parâmetro é. Dessa forma, acho que do ponto de vista de otimização de código, não tem diferença nenhuma para a máquina virtual. O HotSpot(tm) vai identificar os pontos de otimização de variáveis e parâmetros por conta própria mesmo, sem ajuda do compilador.

Bom, eu vou procurar usar o final em todos os cantos mais por precaução contra atribuições em lugares errados, de qualquer forma.

1 comment:

Anonymous said...

Legal seu post, cara... Bom saber que em determinadas circunstancias, o uso de final pode até trazer melhor desempenho.. apesar de nem sempre ser assim...
valeu!