Programação em geral
Este artigo é dedicado aos detalhes
práticos da linguagem Java. Ele discute o tratamento de
variáveis locais, o uso de bibliotecas, o uso dos tipos de
dados, e dois recursos extralingüísticos: reflection e
métodos nativos. Por fim, ele discute otimizações
e convenções de nomes.
Item 29: Minimize o escopo de
variáveis locais
Minimizando o escopo de variáveis locais você aumenta a
legibilidade do código e reduz a probabilidade de erros.
A programação Java, diferentemente do C, permite que
você declare variáveis em qualquer lugar que é
permitida uma sentença de código. Assim, a melhor forma
de minimizar o escopo de uma variável é declará-la
onde ela for utilizada pela primeira vez. Desta forma fica mais
fácil para o leitor do código lembrar qual é seu
tipo e valor inicial, e caso após algum tempo a variável
não seja mais utilizada é menos provável que sua
declaração seja esquecida no código. Portanto,
quase todas as declarações de variáveis locais
devem ser seguidas de sua inicialização. Uma
exceção é o caso de inicialização
dentro de um bloco try/catch, em que
você pode declarar a variável fora do bloco para que ela
possa ser acessada pelo resto do método.
No caso de loops, prefira o for ao while, já
que o primeiro permite que seja declarada uma variável que
é usada no corpo do loop e também na
inicialização, teste e incrementação do
valor, enquanto no segundo seria necessário declarar a
variável fora do bloco e ela poderia ser reutilizada por engano
posteriormente.
Exemplo:
for (int i
= 0, n = list.size(); i < n; i++) {
doSomething(list.get(i));
}
O código acima é uma boa implementação para
listas de acesso aleatório, como ArrayList e Vector, pois
é executado mais rápido do que com a
utilização de um Iterator. Além
disso, são declaradas duas variáveis com o escopo correto.
A última técnica para minimizar o escopo de
variáveis é deixar os métodos pequenos e focados.
Se você combina duas atividades em um único método,
uma variável relevante para uma atividade pode ficar no escopo
da parte do código que executa outra atividade, portanto o ideal
é separar o método em dois.
Item 30: Conheça e utilize as
bibliotecas
Utilizando as bibliotecas padrão, você é
beneficiado pelo conhecimento dos experts que as escreveram e a
experiência daqueles que as utilizaram antes de você. Por
exemplo, muitos programadores geram números aleatórios
entre zero e um determinado limite através do seguinte
código:
static
Random rnd = new Random();
static int random(int n) {
return
Math.abs(rnd.nextInt()) % n;
}
Este
exemplo parece funcionar, porém possui três falhas graves:
se n for uma potência de dois pequena, a seqüência de
números gerados será repetida em pouco tempo; se n
não for uma potência de dois, alguns números
serão retornados com maior freqüência que outros; e
se o número gerado for Integer.MIN_VALUE e n
não for uma potência de 2 o retorno do método
será um número negativo. Para corrigir esses problemas
seria necessário conhecimentos avançados de
matemática, mas este trabalho já foi realizado para
você pela pessoa que criou o método Random.nextInt(int) e testado por
milhares de pessoas durante vários anos.
A segunda vantagem de utilizar as bibliotecas é não
precisar perder tempo escrevendo soluções que não
estão diretamente relacionadas ao seu trabalho.
Outra vantagem é que a performance das bibliotecas padrão
tendem a aumentar no decorrer do tempo, sem nenhum esforço da
sua parte. Como elas são utilizadas por muitas pessoas e em
benchmarks da indústria, as organizações que as
criam são incentivadas a torná-las mais rápidas.
E a maior vantagem do uso das bibliotecas é destacar o seu
código, tornando-o mais legível, fácil de manter e
reutilizável por outros desenvolvedores.
Várias funcionalidades são adicionadas à
biblioteca em cada release do Java, e vale a pena estar familiarizado
com elas para evitar criar código desnecessariamente. Todos os
programadores devem conhecer pelo menos o conteúdo do java.lang, java.util e java.io. As outras
podem ser estudadas conforme surgir a necessidade. Uma das
funcionalidades que merecem atenção especial é a
framework de Collections.
Item 31: Evite float
e double
se forem necessárias respostas exatas
Os tipos float
e
double utilizam aritimética
binária de ponto flutuante e foram criados
principalmente para cálculos científicos e de engenharia,
que requerem aproximações precisas rápidas sobre
determinadas magnitudes. Eles não fornecem resultados exatos e
devem ser evitados em casos como cálculos monetários.
Um exemplo é o código System.out.println(1.03 - 0.42); que gera como
resultado 0.6100000000000001.
Para cálculos monetários o correto é utilizar int, long ou BigDecimal. Utilizar os
primitivos normalmente é mais rápido e mais
prático, mas aí você deve controlar as casas
decimais diretamente, por exemplo, utilizando centavos ao invés
de reais. Se você preferir que o computador faça este
trabalho, utilize o BigDecimal. Se as
quantidades não ultrapassarem nove dígitos, você
pode usar int; até
dezoito dígitos, utilize long; e acima
disto será necessário o BigDecimal.
Item 32: Evite Strings quando outros tipos forem mais apropriados
Strings foram criadas para representar texto. Mas quando um dado
vem através de um arquivo, da rede ou input do teclado
normalmente também está na forma de string. Se esse dado
for numérico, deve ser traduzido para o formato numérico
apropriado; se for a resposta de uma pergunta do tipo sim ou
não, para boolean; e se representa um objeto, deve ser
convertido para ele, mesmo que seja necessário criar uma nova
classe.
Strings também não são apropriados para tipos
"enumerados", agregados (quando são concatenados valores para
representar um objeto) ou como chaves para permitir acesso a certa
funcionalidade.
Item 33: Cuidado com a performance de
concatenação de strings
O operador de concatenação de strings (+) é
conveniente para combinar algumas strings em uma única string.
Mas usando este operador repetidamente para concatenar n strings requer
um tempo quadrático em n, pois strings são
imutáveis. Para alcançar uma performance
aceitável, utilize a classe StringBuffer para guardar
o conteúdo em construção. Através desta
classe a performance é linear, apresentando uma diferença
dramática quando o número de concatenações
é elevado. Mesmo que o tamanho do StringBuffer não
esteja otimizado para receber todo o conteúdo, a performance
é bastante superior.
Portanto, não utilize o operador + para combinar mais do que
algumas strings se a performance for importante. Utilize o
método
append da classe StringBuffer ou trabalhe
com uma array de caracteres.
Item 34: Refira-se aos objetos
através de suas interfaces
O item 25 dá a dica que você deve utilizar interfaces ao
invés de classes como tipos de parâmetros. De forma mais
geral,
você deve preferir o uso de interfaces ao invés de classes
para referir-se a objetos. Portanto, se existir uma interface
apropriada, parâmetros, valores de retorno, variáveis e
campos devem ser declarados do tipo da interface. O único
momento em que você precisa referir-se à classe do objeto
é durante sua criação.
Assim, seu programa fica mais flexível, bastando alterar a
classe na linha em que é chamado o construtor do objeto.
Porém, se
a classe original prover alguma funcionalidade especial em seus
métodos que não é requerida pelo contrato da
interface, é necessário tomar cuidado com o impacto disso
no seu código. Por exemplo, você pode declarar List subscribers = new Vector() e depois
mudar der Vector
para
ArrayList. Ainda que
ambos possuam os métodos de List, o primeiro
é sincronizado, sendo incorreto fazer a
substituição se o seu código depender disso.
Mesmo
que hoje não exista previsão da necessidade de alterar a
classe, futuramente pode surgir uma nova classe que desempenhe a mesma
tarefa melhor ou de forma mais rápida, ficando
fácil aproveitá-la caso seu objeto esteja referenciado
pela
interface.
Por outro lado, caso não exista uma interface apropriada,
é correto referir-se ao objeto pela classe. Um exemplo disso
são as classes de valores, como String e BigInteger. Já no
caso de frameworks, muitas vezes algumas classes têm como tipo
fundamental classes abstratas ao invés de interfaces, portanto o
ideal é referenciar o objeto pela classe base. E o último
caso em que uma classe é usada diretamente ao invés de
sua interface é quando seu código depende de
métodos que não estão declarados na interface.
Item 35: Prefira interfaces ao invés
de reflection (reflexão)
O mecanismo de reflection, do pacote java.lang.reflect, oferece
acesso programático a construtores, métodos e campos de
uma classe. Desta forma é possível que uma classe utilize
outra, mesmo que a última ainda não existisse quando a
primeira foi compilada. É um mecanismo poderoso, mas tem
desvantagens:
- você perde o benefício das verificações de
tipo que acontecem em tempo de compilação, incluindo
checagem de exceções;
- o código necessário para realizar acesso reflexivo
é mais confuso que o tradicional;
- há um impacto de performance (no Java 1.3 era 40 vezes mais
lento chamar um método por reflexão do que diretamente, e
no Java 1.4 ainda gasta o dobro do tempo do acesso normal).
A reflection foi inserida no Java para ferramentas de
construção baseadas em componentes. Portanto, deve ser
utilizada em tempo de design. Objetos não devem ser acessados
por reflexão em uma aplicação normal em tempo de
execução. Apenas algumas aplicações mais
sofisticadas são exceções a esta regra, como
navegadores de classes, inspetores de objetos, ferramentas de
análise de código, sistemas interpretados e RPC.
Você consegue obter a maior parte do benefício de
reflection incorrendo apenas um pequeno custo utilizando-a de forma
limitada. Por exemplo, se a classe não está
disponível em tempo de compilação mas existe uma
interface ou superclasse dela que está disponível, a
instância é criada por reflexão mas os
métodos são acessados normalmente através da
superclasse ou interface. Se o construtor não tiver
parâmetros não é necessário nem usar o java.lang.reflect, pois o Class.newInstance provê a
funcionalidade necessária.
Item 36: Use métodos nativos
moderadamente
O JNI (Java
Native Interface) permite que aplicações Java
chamem métodos nativos. Eles são utilizados para obter-se
acesso a características específicas da plataforma, como
registros e trava de arquivos; para ter acesso a bibliotecas de
código legado, muitas vezes responsáveis pelo acesso a
dados legados; e para melhorar a performance em partes críticas
da aplicação. Muitas vezes o Java provê um
mecanismo de acesso mesmo para esses
casos, como o pacote java.util.prefs para a
funcionalidade de registro e a API JDBC para acesso a bancos de dados
legados. Desde a release 1.3 do Java, também é raramente
aconselhado usar métodos nativos para melhorar a performance,
pois para a maioria das tarefas já é possível
atingir uma performance comparável ao de métodos nativos
utilizando apenas os recursos do Java. Por exemplo, a BigInteger inicialmente
era implementada utilizando uma biblioteca aritimética de
multiprecisão rápida escrita em C. A partir da
versão 1.3 ela foi reescrita totalmente em Java e está
mais rápida que a antiga.
O uso de métodos nativos tem sérias desvantagens, pois
não são seguros (estão sujeitos a
corrupção de memória), são dependentes de
plataforma (perdendo portabilidade) e o processo de entrar e sair de
métodos nativos causa impacto na performance. Além disso,
são mais difíceis de escrever e entender.
Item 37: Otimize moderadamente
Existem três aforismos sobre otimização que todos
deveriam saber:
"Mais pecados computacionais são
cometidos em nome da eficiência (sem necessariamente
alcançá-la) do que por qualquer outra razão -
incluindo estupidez cega."
- Willian A.
Wulf
"Nós deveríamos esquecer as pequenas eficiências 97%
das vezes: otimização prematura é a raiz de todo
mal"
- Donald E.
Knuth
"Nós seguimos duas regras a respeito
de otimização:
Regra 1. Não faça
Regra 2 (apenas para experts). Não
faça ainda - ou seja, não até que você tenha
uma solução perfeitamente clara e não otimizada"
- M. A. Jackson
Todos
esses aforismos são 20 anos mais antigos que o Java e dizem uma
grande verdade sobre otimização: é mais
provável piorar do que melhorar o código, especialmente
se você
otimizar de forma prematura. Durante o processo você pode
produzir software que não é rápido nem funciona
corretamente e é difícil de corrigir. Esforce-se em
escrever bons
programas, não programas rápidos. Se a arquitetura for
boa, será fácil otimizá-lo, pois as
informações estarão encapsuladas e a
alteração de um módulo individual não
afetará o resto do sistema. Porém, é
necessário pensar sobre a performance durante o processo de
planejamento do sistema, pois uma arquitetura que limita a performance
fica difícil de consertar depois que o sistema está
pronto. Desta forma, os componentes que especificam a
interação entre módulos e com o mundo exterior
devem ser planejados cuidadosamente.
Considere
as conseqüências das suas decisões da API. Deixar um
tipo público mutável, por exemplo, pode requerer grande
quantidade de cópia defensiva desnecessariamente. Usar
herança em uma
classe pública em que composição seria apropriada
prende para sempre a classe a sua superclasse, colocando um limite
artificial na performance da subclasse. E usar a classe de
implementação ao invés da interface em uma API
prende à implementação específica, sendo
que
implementações mais rápidas podem surgir
futuramente. De forma geral, um bom planejamento da API é
consistente com boa performance. Porém, deformar a API para
melhorar a performance é uma má idéia.
Quando o sistema já está funcionando e você decide
otimizá-lo, outra dica é medir a performance antes e
depois de uma otimização. Em média um
programa gasta 80% do tempo em 20% do código, então
não adianta perder tempo otimizando uma parte do código
que não é o gargalo. Ferramentas de profiling ajudam a
identificar qual parte precisa de mais otimização. No
caso do Java é particularmente importante medir os efeitos de
uma otimização pois ele não tem um modelo de
performance bem definido, não ficando bem claro os custos de
cada operação primitiva e ocorrendo diferenças
significativas de performance em diferentes JVMs.
Outro fator importante durante a otimização é a
escolha do algoritmo, pois a otimização em baixo
nível não consegue compensar um algoritmo inadequado.
Item 38: Siga as convenções
de nomes
Violações de regras de nomes aumentam a dificuldade de
entender a API e manter o código, confundindo e irritando outros
programadores.
Nomes de pacotes devem ser hierárquicos, com suas partes
separadas por pontos. As partes devem ser letras minúsculas e
(raramente) números. O nome de qualquer pacote que vai ser
utilizado fora de sua organização deve começar com
o domínio de internet dela ao contrário (ex.: com.sun).
As bibliotecas padrão e pacotes opcionais, cujos nomes
começam com java e javax, são exceções a
esta regra e não são permitidos esses nomes por
usuários. O resto do nome do pacote deve consistir em uma ou
mais partes que descrevem o pacote.. As partes devem ser curtas,
normalmente com oito ou menos caracteres. Abreviações
significativas são encorajadas, por exemplo util no lugar de
utilities. Acrônimos também são aceitos, como awt.
As partes geralmente devem ser uma palavra única ou
abreviação. Muitos pacotes têm nomes com apenas uma
parte adicionalmente ao nome do domínio. Partes adicionais
são apropriadas para pacotes maiores, cujo tamanho exige que
sejam divididos em uma hierarquia informal (não há
suporte lingüístico para uma hierarquia verdadeira).
Nomes de classes e interfaces devem consistir em uma ou mais palavras
(substantivos ou frases substantivas), com a primeira letra de cada
palavra em letra maiúscula. Abreviações devem ser
evitadas, exceto por acrônimos e algumas
abreviações
comuns como max e min. Não há um consenso sobre
acrônimos ficarem inteiros em letra maiúscula ou apenas a
primeira letra. Apesar da primeira abordagem ser mais comum, fica mais
legível deixar apenas a primeira letra maiúscula,
especialmente quando vários acrônimos ocorrem juntos.
Nomes de métodos e campos devem seguir as mesmas
convenções tipográficas de classes e interfaces,
exceto que a primeira letra deve ser minúscula, mesmo que seja
um acrônimo. A única exceção é o caso
de constantes, que devem ter seus nomes todos em maiúsculas com
as palavras separadas por underscore. Os
métodos normalmente utilizam verbos ou frases verbais como nome.
Métodos que retornem um boleano usam o prefixo "is" seguido de
um adjetivo ou substantivo, ex: isDigit. Já os que retornam o
resultado de uma função ou um atributo do objeto
são nomeados com substantivos com ou sem o prefixo "get". O uso
do get só é obrigatório se a classe for um
JavaBean. Havendo um método para atribuir o valor a um atributo
do objeto utiliza-se o prefixo set. Métodos que convertem o tipo
de um objeto são chamados toTipo.
Métodos que retornam uma "visualização" de um tipo
diferente do objeto recebido normalmente são chamados de asTipo. Métodos que retornam
um primitivo com o mesmo valor do objeto em que foram chamados recebem
o nome de tipoValue. Nomes
comuns para static factories são valueOf e getInstance.
Variáveis
locais seguem as mesmas convenções de métodos e
campos, mas abreviações são permitidas.
Exemplos:
Pacote: com.sun.medialib, com.sun.jdi.event
Classe ou interface: Timer, TimerTask, KeyFactorySpi, HttpServlet
Método ou campo: remove, ensureCapacity, getCrc
Valor constante: VALUES, NEGATIVE_INIFINITY
Variáveis locais: i, xref, houseNumber
Bibliografia:
Bloch, Joshua. Effective Java.
Resumo por: Vanessa Sabino