Porque o C# é melhor que o Java, parte II

  

A minha coluna intitulada “Porque a linguagem C# é melhor que a linguagem Java” publicada no site da Microsoft em http://www.microsoft.com/brasil/msdn/colunas/falandoc/col_falandoc_2.asp causou uma certa repercussão. Recebi muitas mensagens, algumas delas até contendo dúvidas técnicas.

Acredito que o C# seja superior ao Java. Também acho que a maioria das pessoas intelectualmente honestas e que fizerem uma comparação objetiva vai pensar o mesmo. Não era a idéia da coluna original ser um tratado sobre as diferenças entre as duas linguagens e sim dar uma idéia geral, como base para que o leitor pudesse pesquisar mais e fazer seus próprios experimentos. Por esta razão, foram apresentadas comparações em forma de tópicos, com poucas explicações. A ausência de explicações detalhadas fez com que algumas pessoas tirassem conclusões apressadas, por falta de conhecimento de C# e pouca disposição em investigar. Neste artigo estou explicando alguns destes pontos com maiores detalhes.

Respeito quem usa e gosta da linguagem Java, a qual não acredito ter menosprezado de forma alguma em meu artigo, muito pelo contrário. Embora estivesse escrevendo para o site da Microsoft, forneci alguns links que considero bem interessantes em locais que não são famosos por gostarem do gigante de Redmond, como Doctor Dobbs Journal, O’Reilly e até da própria Sun!

Para o leitor que “não acha possível existir algo melhor que Java”, gostaria que considerasse, além dos aspectos puramente técnicos, os seguintes:

  • O C# foi desenvolvido DEPOIS do Java e indubitavelmente teve a oportunidade de melhorar várias de suas deficiências - acredite, elas existem. Por exemplo, nem o mais ferrenho defensor do Java vai dizer que os programas Java têm excelente performance.  Pois bem, existem questões de performance intimamente ligadas à linguagem e que foram “observadas” no C#. Veja abaixo os comentários nas discussões sobre “enum”, “structs”, “passagem por referência” e “ponteiros”, para alguns exemplos.
  • A equipe que criou tanto o C#, como a nova plataforma de desenvolvimento como um todo, tem em seu curriculum produtos bastante respeitáveis: o Visual Basic, provavelmente a ferramenta de desenvolvimento mais usada no mundo; o J++, em seu tempo a mais popular ferramenta Java e o Delphi da Borland. Para quem não sabe, Anders Hejsberg é o criador do Turbo Pascal, do Delphi e do C# (*). Você pode até não gostar de alguma destas ferramentas, mas é um curriculum sem dúvida respeitável.
  • A Microsoft investiu bastante na criação da nova plataforma de desenvolvimento. Veja bem: uma equipe que é seguramente o melhor pessoal do ramo, com dinheiro para gastar e sem a pressão de precisar lançar um upgrade por trimestre para “fechar o balanço” e não ir à falência.

Continuo sem a intenção de escrever um tratado, mas vou dar algumas explicações aos pontos que levantaram mais dúvidas. Os tópicos abordados a seguir variam em importância, mas um bom resumo seria:

  • O C# tem intrinsecamente melhor performance que o Java;
  • O C# suporta conceitos de componentes (propriedades e eventos), fundamentais para uma boa ferramenta RAD;
  • O C# restaura certos recursos úteis tirados do C++, como sobrecarga de operadores, e acrescenta outros interessantes, como switch com strings. A importância destes recursos varia de pessoa para pessoa, mas acredito ser bom tê-los à disposição.

(*) Segundo a própria Borland, a Microsoft contratou 34 de seus “funcionários-chave”, o que motivou uma ação na justiça e um posterior “press-release” intitulado “BORLAND SUES MICROSOFT FOR UNFAIR COMPETITION”. Este “press-release” foi recentemente removido de seu site, não sei se com relação ao fato de eu tê-lo citado há alguns meses atrás em um grupo de discussão da Borland. Ainda é possível achar menções em outros sites como http://www.ugeek.com/techupdate/bldsuetc.htm,  http://www.informationweek.com/newsflash/nf629/0507_st10.htm, http://news.zdnet.co.uk/story/0,,s2065714,00.html e http://news.cnet.com/news/0-1005-200-318765.html?tag=. Por freqüentar eventos internacionais como BORCON, Tech-Ed e PDC na qualidade de “membro da imprensa”, eu conheci pessoalmente pelo menos cinco destes ex-funcionários, todos envolvidos diretamente com ferramentas de desenvolvimento. Um deles, Bill Dunlop, me confessou há alguns anos atrás que ele “sentia-se em casa trabalhando com tantos ex-funcionários da Borland”.

Todos os tipos derivados de ancestral comum

Em C#, todos os tipos podem ser tanto atribuídos a object como ter métodos, NÃO APENAS AS CLASSES. Vou dizer de outra forma: em C# você pode atribuir um int diretamente a um object. Tem mais: um int tem métodos! E o melhor de tudo: quando você usar os inteiros “sozinhos”, você não paga o preço adicional de OOP; estes tipos são gerenciados na pilha de forma pouco custosa. Em Java você tem que gerenciar manualmente “classes associadas” aos tipos intrínsecos.

Observe o seguinte código C# que utiliza o recurso de “conversão automática para referência”, também chamado de “boxing”. Note a ausência de “classes wrapper”:

// Estes comandos não carregam o ônus de criarem objetos
int
I = 123, J = 456;
int Y = I + J;
// Agora um objeto será criado automaticamente para ‘embalar’ o inteiro
// Note que o inteiro tem métodos!
string s =Y.ToString();
Console.WriteLine(s);
// Podemos atribuir qualquer valor a um object, até mesmo um inteiro!
object O = s;
O = Y;
Console.WriteLine(O);
// Para pegar o inteiro de volta fazemos um cast
int Z = (int) O;
Console.WriteLine(Z);
// Uma lista pode conter qualquer coisa
ArrayList Lista = new ArrayList();
Lista.Add("Maria tinha um carneirinho");
Lista.Add(false);
Lista.Add(123);
Lista.Add(34.56);
// Até mesmo instâncias de classes
Lista.Add(new System.IO.StreamReader("teste.txt", System.Text.Encoding.Unicode));

Veja um esquema do código e de como as variáveis são alocadas:

É até possível dizer que o C# é “mais orientado a objetos que o Java” por possuir a unificação do sistema de tipos, como o Smalltalk. Embora este tipo de argumentação seja subjetiva e estéril, algumas pessoas acusam o C# de ser “menos orientado a objetos” justamente por suportar structs e enums, apesar das claras vantagens destes dois recursos.

O argumento de que o Java é “mais orientado a objeto por não ter enum e struct” é o mesmo que alguém dizer que “é mais brasileiro que outra pessoa porque o outro sabe falar inglês”.

Propriedades

Uma property funciona sintaticamente como um campo, mas na verdade chama um par de métodos para atribuir ou receber um valor. As propriedades podem ser também "indexadas" com um inteiro, quando funcionam como se fossem arrays ou indexadas com uma string, quando passam a funcionar como dicionários. O ambiente de desenvolvimento sabe criar "editores de propriedades" para alterar seus valores em tempo de desenvolvimento.

O Java sugere usar um padrão com dois métodos, GetXXX e SetXXX. As propriedades não só são conceitualmente mais simples para os programadores, como também são muito importantes em ferramentas RAD. É possível emular propriedades com um par de métodos, mas dá mais trabalho para quem escreve tanto os componentes como os programas e ferramentas que as usam.

Para exemplificar como as propriedades têm uma sintaxe realmente específica e de uso simples, veja o exemplo abaixo:
public
class ClassP {
            // Campo que vai armazenar o valor
     
string fNome;
     
// Propriedade
     
public string Nome {
           
// Pega o valor
           
get
                 
return fNome;
           
}
           
// Atribui o valor

           
set
                 
fNome = value;
           
}
     
}
}

void
UsaProp() {
     
// Cria objeto da classe
     
ClassP P = new ClassP();
     
// Atribui (chama o bloco set)
     
P.Nome = "Maria";
     
// Lê o valor (chama o bloco get)

     
string S = P.Nome;
}

Eventos

Podemos declarar um tipo "ponteiro para método", chamado delegate. Um delegate contém, a princípio, o endereço da função e também do método que a implementa. Todos os eventos, tão importantes para o funcionamento de um ambiente de desenvolvimento "RAD", são delegates. Os delegates permitem que uma classe chame métodos em outras sem exigir que esta outra classe seja derivada de um ancestral conhecido. No artigo original, cito dois links bastante interessantes sobre o assunto que estou repetindo a seguir:

Artigo da Sun "explicando" porque o Java não tem nem terá delegates

Resposta da Microsoft mostrando porque os delegates são úteis

As principais vantagens dos delegates sobre interceptar métodos virtuais de classes derivadas são as seguintes:

  • Os eventos são conceitualmente mais simples, principalmente para o programador médio;
  • Os eventos podem ser arbitrariamente associados em tempo de execução. Interceptar um método virtual em tempo de execução exige a criação de código, algo extremamente complicado, bem além da capacidade de um programador médio e que exige um alto privilégio de segurança, indisponível à maioria dos programas.

A incorporação de eventos no finado J++ e na máquina virtual da Microsoft foi uma das razões pelas quais a Sun abriu um processo - já resolvido - contra a Microsoft. Até por esta razão, a Sun dificilmente colocará eventos na linguagem, pois seria reconhecer que a Microsoft estava “certa” ao alterar a linguagem.

Enums

As enumerações em C# representam tipos à parte, incompatíveis entre si e com inteiros, de forma muito semelhante ao Pascal. Embora implementadas de forma eficiente como inteiros no executável final, você não pode atribuir uma enumeração “de um tipo” à outra, como poderia fazer com constantes inteiras.

As bibliotecas Java como a “Swing”, por exemplo, usam geralmente strings (“NORTH”, “SOUTH”, etc) em situações onde uma enumeração seria mais aconselhável. O uso de strings não apenas permite a verificação dos valores apenas em tempo de execução, como também é bastante “cara” em termos de uso de CPU e memória.

Veja um exemplo em Java/Swing para ajuste de posição de componente de tela:

 

contentPane.add(Button1, BorderLayout.CENTER);

Note que o posicionamento é especificado com string, algo bastante caro em termos de execução. Para piorar, o código a seguir está errado, mas compila com sucesso:

 

contentPane.add(Button1, “lixo”);

O erro acima só será descoberto em tempo de execução. Uma enumeração, além de ser mais “barata”, detectaria o erro em tempo de compilação.

Structs

A struct é semelhante a class no sentido que pode possuir coisas como “campos” e “métodos”. As diferenças em relação a class são as seguintes:

  • Elas são tipos por valor enquanto as classes são tipos por referência e, portanto, alocadas na pilha
  • Não podemos declarar um construtor que não aceite argumentos;
  • Podemos atribuir à variável this;
  • Não suportam herança; elas são implicitamente sealed.

Quando devemos usar a struct ao invés da class? A resposta é a seguinte: nos tipos “leves”, onde uma classe sairia muito cara em termos de uso de CPU, memória e pressão no coletor de lixo. O exemplo canônico é uma coordenada (X, Y) no plano. Será que precisamos pagar o preço de uma classe para implementar algo tão simples? Veja um esquema da memória usada por uma class e uma struct equivalentes:

Passagem por referência

Em Java, assim como no C#, existem “tipos por valor”, como os inteiros e “tipos por referência”, como as instâncias das classes. Quando de uma chamada de função, os “tipos por valor” são passados por valor e os “tipos por referência” passados por referência. Mas e se quisermos passar um “tipo por valor”, como um inteiro, por referência? A resposta é que em Java não podemos. Precisamos “embalar” o tipo por valor em uma classe, alocar a um objeto da classe dinamicamente e aí passar o objeto, que “virou” uma referência. Estas maquinações do Java têm um grande custo tanto em termos de trabalho na programação como de performance.

Já o C# inclui passagem por referência “tradicional”. Este é, aliás, um recurso bastante útil que não havia no C, foi introduzido no C++ e retirado no Java. O C# o trouxe de volta.

A vantagem do mecanismo do C# é que ele é bem mais “barato” que embalar o valor em uma classe. Veja um exemplo bem simples:
static void Soma(int A, int B, out int Resultado) {
     
Resultado = A + B;
}

public
static void Main() {
     
int a;
     
Soma(3, 4, out a);
     
Console.WriteLine(a);
}

Documentação em XML

O Java tem uma ferramenta externa chamada Javadoc (http://java.sun.com/j2se/javadoc/) que pode gerar páginas HTML com documentação a partir de tags especiais colocadas no fonte.

O C# também permite que se coloque tags no fonte com a finalidade de criar documentação, mas o C# vai bem além da criação de páginas HTML. Em primeiro lugar, o Visual Studio cria as tags automaticamente, bastando digitar “///” antes do elemento a ser documentado.

Em segundo lugar, a documentação fica imediatamente disponível dentro do próprio Visual Studio como “dica”, sem que seja necessário rodar qualquer ferramenta externa. Veja um exemplo:

Finalmente, a documentação é disponibilizada em XML, não HTML. Por causa disto, é muito fácil usar esta documentação de forma estruturada para, por exemplo:

·        Converter para HTML com uma style sheet XSL;

·        Efetuar pesquisas;

·        Inserir e manipular em bancos de dados.

Enquanto a documentação do Javadoc pode ser apenas lida, a do C# pode ser manipulada.

Ponteiros

Todas as “instâncias” de classes, tanto em Java como em C# são “referências”, uma espécie de “ponteiro domesticado”, incapaz de apontar para endereços arbitrários de memória, ao contrário de um ponteiro “comum” do C/C++. As duas linguagens também verificam a faixa nos índices de arrays para evitar acesso indevido à memória. Ambos os recursos contribuem para a criação de programas mais seguros e robustos.

Sendo assim, para que precisamos de um ponteiro “cru”? Basicamente, em duas situações:

  • Chamar DLLs: É muito bonito pensar que a biblioteca de runtime vai resolver 100% dos seus problemas. Infelizmente, vão aparecer situações onde você vai precisar acessar um dispositivo de hardware ou rotinas de terceiros através de DLL. Um bom exemplo é controlar impressoras fiscais. Postular a perfeição não resolve o problema. Para estes casos, um programador Java terá que fazer alguma séria ginástica, enquanto um programador C# “liga o modo inseguro” e resolve o problema diretamente, sem sair do ambiente. Na verdade, sem sair sequer do fonte; veja a discussão a seguir sobre “Chamadas a código nativo”;
  • Obter melhor performance. Como os arrays têm sempre a faixa validada em tempo de execução, algumas operações que dependem da manipulação de grandes volumes de dados, como por exemplo redes neurais e grafos em geral, vão ter um impacto de performance bastante negativo. Novamente, o programador C# pode “ligar o Turbo” e usar ponteiros para resolver o problema.

Não estou advogando o uso indiscriminado de ponteiros. A própria .NET Framework não incentiva o uso deste recurso, pois é necessário um privilégio especial de segurança para rodar código contendo ponteiros. Mas quando você precisar, o recurso estará à disposição.

Chamadas a código nativo

O Java permite chamadas a funções nativas através de “JNI”, mas este recurso é bastante complexo, exige programação na linguagem C (argh!) e ainda assim tem limitações. Se você não acredita em mim, leia uma séria limitação no item 14 no final desta página no site da Sun: http://java.sun.com/products/jdk/faq/jni-j2sdk-faq.html. Para um exemplo de como chamar código nativo em Java, dê uma olhada em um tutorial em http://java.sun.com/docs/books/tutorial/native1.1/stepbystep/index.html. Note como é complicado!

Para dar uma idéia da simplicidade do C#, veja um exemplo com chamada de uma API do Windows (a API do Windows não é o melhor exemplo, pois ela é encapsulada na .NET Framework, mas pelo menos é fácil de testar):

  [DllImport("user32.dll")]
public
static extern int MessageBox(int hWnd, string Msg, string Titulo, int Tipo);
protected void button2_Click (object sender, System.EventArgs e) {
 
MessageBox(0, "Mensagem de teste", "Título", 0);
}

Campos const e readonly

Conforme escrito no artigo, o Java tem static final que corresponde ao const do C#. O campo readonly, só presente no C# permite definir um campo que pode ser atribuído apenas no construtor da classe e não em outros métodos. Este recurso simplesmente não existe no Java.

Padronização

A Microsoft submeteu no fim do ano 2000 o C# a um órgão internacional de padronização, o ECMA (http://www.ecma.ch, http://www.microsoft.com/NET/sharedsourcewp.asp). A Sun fez algumas tentativas de padronizar o Java, mas nenhum órgão internacional aceitou seus termos (http://news.cnet.com/news/0,10000,0-1003-200-320974,00.html, http://www.cnn.com/1999/TECH/computing/12/09/java.std.cancel.idg/). O resultado é que o Java é uma linguagem proprietária enquanto o C# não é.

Existem esforços para portar o C#/.NET para outras plataformas, dos quais eu destacaria o Mono (http://lists.ximian.com/mailman/listinfo/mono-list), PNet (http://www.southern-storm.com.au/portable_net.html) e .NET para BSD Unix da Corel (http://www3.corel.com/cgi-bin/gx.cgi/AppLogic+FTContentServer?pagename=Corel/PressRelease/Details&id=CC100K16H9C).

Attributes

Os attributes, um recurso completamente inexistente em Java, permitem que você “marque” código com informações declarativas definidas pelo programador. Estas informações serão interrogadas em tempo de execução através de “Reflections”.

Note que este recurso depende do suporte a Reflection do C#, mas não está presente no suporte a Reflection do Java.

O resultado é algo parecido com uma diretiva de compilação definida pelo programador. Por exemplo, para identificar um método de uma classe como sendo chamável através de SOAP, usamos o atributo WebMethod antes do método:

[WebMethod] string ConsultaPedidos(int Usuário) { ...}

O suporte a chamadas nativas, mostrado acima, também usa attributes.

Compilado X Interpretado

Tanto o C# com o MSIL (Microsoft Intemediate Language) foram feitos para serem compilados e são sempre compilados. Já o Java e o Bytecode Java foram feitos para serem interpretados e podem ser compilados, como o artigo deixa claro. A questão “de qual porcentagem dos programas Java são compilados” pode ser discutida eternamente sem que se chegue a nenhuma conclusão.

Quem quiser pesquisar as origens interpretadas do Java, pode consultar o documento http://java.sun.com/docs/white/langenv/index.html, de James Gosling e Henry McGilton intitulado “The Java Language Environment”. Tal documento contém a palavra “interpreter” ou “interpreted” mencionadas um total de 45 vezes. Veja em especial os capítulo 1.2.5 e 5.1, dos quais tiro alguns trechos: “Em uma plataforma interpretada como aquelas baseadas em tecnologia Java, a fase de linkedição de um programa é simples, incremental e leve”. “Melhores formas de prototipação e desenvolvimento rápidos são necessárias. O ambiente da linguagem Java é uma destas maneiras melhores, por ser interpretada e dinâmica”. “A natureza portável e interpretada da linguagem produz um sistema altamente dinâmico e dinamicamente expansível”. Quem escreveu isso foi James Gosling, o pai da criança.

Quanto a comparações entre Java Bytecode e MSIL, sugiro um artigo de uma universidade australiana que criou compiladores Pascal tanto para Java VM como para .NET em http://www2.fit.qut.edu.au/CompSci/PLAS//ComponentPascal/virtual_machines.pdf. Veja alguns trechos do artigo:

“A plataforma .NET foi explicitamente concebida para suportar múltiplas linguagens...o caso com a JVM é menos claro, pois suporte a múltiplas linguagens nunca foi um objetivo...existem implementações de subconjuntos de outras linguagens, mas elas tem que superar uma série de problemas”.

“A comparação das duas máquinas virtuais mostra claras diferenças entre seus objetivos de projeto. No caso da JVM, a evolução de todo o movimento Java levou a plataforma para bem além de seu contexto original de projeto. Tudo isto ocorreu à frente do escrutínio público e em um contexto onde a compatibilidade com versões anteriores, a nível de VM, quase que certamente tornou-se uma restrição custosa. Em contraste, os projetistas da ‘VM .NET’ tiveram o luxo de postergar decisões de congelamento do conjunto de instruções. Presumivelmente eles também aprenderam com a experiência do Java.”

O artigo deixa bastante claro que a MSIL, ao contrário do Java Bytecode, foi feita para ser compilada e suportar várias linguagens.

Quem quiser ler mais sobre compiladores e máquinas virtuais, sugiro um artigo de Bertrand Meyer em http://www.eiffel.com/doc/manuals/technology/bmarticles/sd/dotnet.html.

Sobrecarga de operadores

A sobrecarga de operadores no C++ sempre foi considerada “açúcar sintático”. Pessoalmente, tendo a concordar com a observação. No entanto, segundo Greg DeMichillie (gerente de produto do C#) em apresentação na qual participei, este recurso foi incluído porque o pessoal que faz cálculos matemáticos com matrizes, vetores e números complexos simplesmente adora a facilidade de usar notação “matemática” ao invés de funções. Veja um possível exemplo:

class Vetor{ . . . }
class
Matriz{ . . . }

Matriz A, B, C, Total;
Vetor V;

// Exemplo usando sobrecarga de operadores

Total = (A + B + C) * V; 

// Exemplo sem sobrecarga de operadores

Total = Matriz.Produto(Matriz.Soma(Matriz.Soma(A, B), C), V);

É óbvio que a notação que usa sobrecarga de operadores é mais simples.

Operadores de conversão

Os operadores de conversão no C++ são, com alguma justiça, acusados de promover bugs ao efetuar conversões automáticas sem que o programador tivesse planejado. Embora existam operadores de conversão em C#, eles não sofrem dos problemas do C++. Em primeiro lugar, o construtor que aceita um único parâmetro nunca é usado como operador de conversão. Em segundo lugar, existem operadores implícitos, chamados automaticamente – perigosos - e explícitos, que devem ser chamados com sintaxe de “cast”, impedindo as conversões inadvertidas.

Uma nota sobre o operador de atribuição: em C#, ao contrário do C++, o operador de atribuição não pode ser sobrecarregado, até porque finalidade do “operator=()” em C++ era copiar VALORES e não REFERÊNCIAS. Em C#, assim como em Java, todos os objetos são referências. A cópia de um “valor objeto” simplesmente não é definida porque “objetos por valor” (alocados na pilha) não são definidos.

Foreach

Este é um recurso que sem dúvida não é essencial, mas com certeza evita alguns bugs. O foreach um loop usado para varrer todos os elementos de uma coleção ou array, substituindo for ou while. Ele evita os “famosos” problemas de loops que indexam um a mais ou um a menos (“será que o loop começa em 0, 1 ou -1?”, “será que o loop termina em N, N-1 ou N+1?”).

Veja um exemplo:
// Concatena o conteúdo do ListBox em um string

string
Tudo = "";
foreach
(string Item in listBox1.Items) {
     
Tudo += Item;
}

Goto

Não estou advogando o uso de gotos como no Applesoft BASIC ou FORTRAN. A questão tem a ver com o switch.

O switch no C# elenca opções mutuamente exclusivas, como no Pascal, mas mantém uma sintaxe semelhante ao C. Por causa disto, a princípio, devemos colocar break em todas as opções. Caso desejemos ter o mesmo comportamento do C/C++, devemos colocar goto de uma opção para outra.

O resultado é que o switch tem um comportamento mutuamente exclusivo “por default”, algo bom, sem perder a capacidade de chamar várias opções (com goto).

Veja um exemplo de switch em C# usando goto:
static string PegaTipoDoDia(string S) {
     
string Msg = "";
     
switch(S.ToUpper()) {
           
case "SEG": 
           
case "TER":
           
case "QUA":

           
case "QUI":
           
case "SEX":
                 
Msg = "Dia de semana";
                 
break;
           
case "SAB":
                 
Msg  = "Acabou o trabalho, ";

                 
goto DOM;
           
case "DOM":
                 
DOM:

                 
Msg += "Fim de semana";
                 
break;
     
}
     
return Msg;
}

Observe que o switch pode usar string, algo bastante útil.

Embora o ponto sobre goto fosse apenas um no meio de outros trinta, este é um tópico que talvez eu não devesse ter incluído no artigo original, já que o goto tem um “carma negativo” e este não é um assunto de muita importância.


Java na plataforma .NET

Apesar de ter desenvolvido a linguagem C# especialmente para a plataforma .NET, a Microsoft permite aos programadores escolher entre várias linguagens de programação para o desenvolvimento sob .NET, inclusive o próprio Java! Isto não é uma contradição, simplesmente quer dizer que a escolha de linguagem é sua, não da Microsoft ou da Sun.

Você pode obter maiores informações sobre suporte a Java na plataforma .NET em http://msdn.microsoft.com/visualj/jump/. Para informações sobre outras linguagens desenvollvidas fora da Microsoft, veja em http://msdn.microsoft.com/vstudio/partners/language/default.asp.


Algumas palavras finais

Gostaria de agradecer imensamente a comunidade de programadores Java no Brasil, que tanto me motivou a escrever este artigo, por meio de comentários enviados depois da publicação do artigo original.

O meu e-mail (mas_mauro@hotmail.com) está à disposição para quem quiser fazer comentários construtivos, mesmo que não concorde comigo.

 

© Copyright 2001 por Mauro Sant’Anna – Todos os direitos reservados

       
 


Microsoft Certified Partners
M.A.S. Informática LTDA.
Al. Campinas, 433 - 13º Andar, Conjunto 132 - Jardim Paulista
CEP: 01404-901 - São Paulo - SP, Tel.: (PABX) (011) 3284-0466/ 3287-9622 - Fax: (011) 3287-5593