Este é um post disponível para assinantes MVPArtigo Java Magazine 14 - A dinâmica de Java
Artigo publicado pela Java Magazine 06.
A Dinâmica de Java
Reflection e Metaprogramação
Conheça técnicas sofisticadas do uso da metaprogramação em Java – dos fundamentos a proxies dinâmicas e geração de bytecodes
Sabemos
que a linguagem Java introduziu, a milhões de programadores, a
arquitetura de máquina virtual, um modelo de objetos forte, o garbage
collection e a programação concorrente. Mas há outro item importante e
menos reconhecido também popularizado pelo Java: a metaprogramação.
Neste artigo vamos examinar recursos que, apesar de disponíveis há um
bom tempo na plataforma, ainda são pouco explorados por muitos
desenvolvedores.
Metaprogramação
é uma daquelas coisas que parecem mais complicadas do que são. Muita
gente tem receio de mexer com recursos muito dinâmicos, que lembram
mais a construção de compiladores do que de aplicações comuns. Mas tudo
é questão de prática, até de hábito. Por exemplo, ao construir comandos
SQL no código você está criando parte da sua aplicação dinamicamente.
Só estamos mais acostumados a fazer isso com SQL do que com Java.
Vamos também mostrar um panorama geral do tópico de metaprogramação. Isso inclui as APIs tradicionais em java.lang.reflection,
mas vai além, explorando as possibilidades mais recentes e avançadas do
Java, como proxies dinâmicas, além de mostrar coisas que vêm por aí.
Histórico e um pouco de teoria
Programas são dados.
Esse axioma da metaprogramação de fato é verdadeiro desde a introdução
do conceito de “programa armazenado”. A idéia é atribuída a John Von
Neumann (1946), por isso computadores modernos são chamados “máquinas
de Von Neumann”, com sua arquitetura abstrata composta por CPU e
memória, e o programa armazenado na memória principal da mesma forma
que os dados. Essa arquitetura demorou para se tornar predominante,
pois o preço da memória era muito alto nos anos 40-50; por isso os
programas eram mais freqüentemente lidos instrução por instrução (a
partir de cartões perfurados ou fitas magnéticas). Isso acontecia à
medida que iam sendo executados, e o computador só mantinha na memória
principal uma instrução por vez, ou, no máximo, um cache minúsculo para
agilizar loops curtos.
Se
os programas são dados e são representados na memória como uma
seqüência de bytes, um passo lógico é querer manipulá-los da mesma
forma como fazemos com outros dados. Mas um código executável normal
(compilado) é muito complexo para ser manipulado corriqueiramente; o
hardware antigo não suportaria arquiteturas complexas como nossas JVMs.
Interpretar código fonte ou compilá-lo era muito demorado –
programadores costumavam iniciar compilações à noite e pegar o
resultado somente no dia seguinte, isso para programas de poucos Kb.
A
solução exigida era uma linguagem e um runtime planejados para
funcionamento dinâmico desde o começo. Essa solução surgiu em 1960 com
o Lisp. Além de inovar com as VMs, garbage collection
e a programação funcional, o sistema Lisp passou a representar o
programa compilado num formato que era um meio-termo entre o código
binário da CPU (executável diretamente) e o ASCII do código fonte (mais
fácil de manipular). Esse formato intermediário é uma estrutura de
dados muito eficiente, que permite um desempenho razoável mesmo por um
interpretador. Veja um exemplo na Listagem 1.
O
aspecto esquisito do código (para nós) deve-se a essa representação do
programa “executável”. A compilação transforma o programa numa “lista
de listas” (daí o nome da linguagem, que vem de List Processing – processamento de listas).
A lista-raiz do corpo da função fatorial tem como primeiro elemento o cond (condicional): uma referência para a função que faz o papel do if ou switch. O segundo elemento dessa lista-raiz é uma lista de cláusulas para o cond. Cada cláusula é outra lista, com a estrutura "(condição resultado)". Na primeira cláusula a condição é outra lista, que executa a função "=" com os argumentos (n 0), e o resultado é 1. Na segunda cláusula, que só é executada se as condições anteriores falharem, a condição é o valor t (true),
portanto a cláusula sempre executa se o processamento chegar até ela. O
resultado é outra lista, com uma expressão mais complexa, envolvendo
listas aninhadas.
Você
pode imaginar que, com uma representação tão simples do programa
executável (listas aninhadas onde cada elemento é uma função, um valor
ou outra lista que também pode ser executável) pode ser construída
dinamicamente pelo programa de forma muito fácil e eficiente. Não temos
espaço para técnicas avançadas em Lisp – nem estamos na Lisp Magazine!
–, mas um exemplo modesto já ilustra as possibilidades dessa idéia
(veja a Listagem 2).
A “meta-função” ind-f implementa qualquer função recursiva que gera um resultado a partir de computações para uma seqüência 0,1,2,...,N, daí o nome ind-f (do “método indutivo” de prova de teoremas). Observe que um dos parâmetros adicionais dessa meta-função (n0) é um valor comum, mas o outro argumento (op) é outra função. Se você editar o código de ind-f, substituindo todas as ocorrências de n0 por 1 e todas ocorrências de op por "*", o resultado será igual ao fatorial do primeiro exemplo. Por isso nossa nova definição de fatorial funciona. E podemos fazer outras definições, como a função somatoria que implementa um algoritmo diferente.
"
ATENÇÃO! A exibição deste artigo foi interrompida.
Este é um post disponível para assinantes MVP
Space do autor



0
0
