Linguagens interpretadas e linguagens compiladas

Existe um número enorme de linguagens de programação. Mesmo levando em consideração somente as principais elas podem ser contadas em centenas: https://en.wikipedia.org/wiki/List_of_programming_languages. Portanto é natural que existam diversas maneiras de classificar as linguagens de programação.

Uma das classificações possíveis e que pode gerar dúvidas para iniciantes é quanto ao modo em que as linguagens são implementadas. Existem duas principais maneiras de implementar uma linguagem de programação: através de um compilador ou através de um interpretador.

Esse texto busca esclarecer esses conceitos e as diferenças entre eles. Dependendo qual foi a primeira linguagem que você aprendeu ou pretende aprender um dos dois pode parecer ser mais “natural” para você.

Programa compilado

Uma das linguagens de programação mais antigas, o Fortran, usava o caminho da compilação e pode-se dizer que durante muitos anos esse caminho foi o dominante no projeto de linguagens. Linguagens como C e C++ que por décadas dominaram o mercado usaram esse método.

Qual é a proposta da compilação?

A idéia é ter um programa, chamado de compilador, que irá ler um arquivo escrito em uma linguagem de alto nível (C, C++, Fortran, etc) e irá transformar esse resultado em código binário executável.

Esse código geralmente é um programa independente que pode ser executado mesmo que o compilador seja, por exemplo, desinstalado.

Veja, por exemplo, a imagem abaixo, na qual um programa (escrito na linguagem C) é compilado com o gcc (gcc é a sigla para Gnu C Compiler) e depois que o programa final é gerado (essa etapa da geração se chama compilação) podemos simplesmente executar o arquivo “programa” que foi gerado.

Nesse caso mesmo que o programa “gcc” fosse desinstalado, o programa final poderia ser executado sem problemas. Da mesma maneira poderíamos copiar o programa para outro computador e o mesmo também funcionaria (desde que o ambiente disponível para EXECUÇÃO estivesse disponível). Poderíamos até mesmo deletar o arquivo fonte original exemplo.c e o programa continuaria executando.

Ou seja: na implementação via compilador temos duas etapas bem separadas e relativamente independentes: a COMPILAÇÃO, na qual o programa é gerado, e a EXECUÇÃO do programa.

Essa separação tem algumas vantagens e algumas desvantagens.

Vantagens:

Como o programa gerado é um código binário pronto a etapa de execução tem uma performance maior. Não é preciso usar o processador do computador para decidir como o programa irá se comportar. Certo ou não (dependendo se o programa tem bugs), o seu comportamento já está definido. Esse aumento de performance é significativo e em muitos sistemas nos quais o desempenho é algo MUITO importante pode ser mais interessante usar linguagens compiladas. Por isso que quase todos os sistemas operacionais são gerados a partir do processo de compilação (na verdade existem alguns detalhes adicionais mas isso pode ser tema para outro texto).

O programa, uma vez gerado, não precisa do compilador instalado. Isso permite economia de disco pois em ambientes de produção não precisamos ter o compilador instalado. Em certas circunstâncias isso pode até melhorar a segurança geral de um sistema afinal nunca é recomendável deixar compiladores em sistemas que podem ser alvos de atacantes.

Desvantagens:

O processo de desenvolvimento acaba sendo um pouco mais tedioso pois o desenvolvedor precisa, enquanto está desenvolvendo e testando o sistema, realizar duas etapas: compilar e executar ao invés de uma. Claro que certas automatizações podem ser feitas mas isso leva a outro problema do método de compilação.

A demora na compilação: dependendo da complexidade e tamanho do programa a compilação pode levar vários minutos. Então o desenvolvedor manda o programa compilar e precisa aguardar, às vezes um tempo considerável, até o programa concluir a compilação. Para não mencionar que alguns programas podem consumir algum tempo e nem terminar de compilar, por algum erro de programação por exemplo.

Programa interpretado

Algumas das linguagens mais usadas atualmente são interpretadas: Python, Javascript, Ruby, PHP, etc.

Aqui o processo de desenvolvimento é diferente. Vejamos esse exemplo de um programa em Python.

Observe que aqui, ao invés de compilar o programa o executamos chamando “python3 programa.py”. O que está acontecendo aqui? python3 é um programa que chamamos de interpretador. Ao invés de transformar o arquivo fonte programa.py em um arquivo binário o interpretador lê o arquivo citado, uma linha por vez, e na medida do possível – se não houver erros – vai executando o arquivo.

Esse processo de leitura do arquivo-fonte e interpretação do mesmo é feito a cada execução do programa. Ou seja, o nosso programa python precisa do interpretador e do arquivo fonte em cada uma das suas execuções. Se deletássemos o arquivo programa.py, ao tentar executar novamente a linha “python3 programa.py” resultaria em um erro.

As vantagens e desvantagens são praticamente o inverso de um programa compilado.

Vantagens:

O desenvolvimento é um pouco mais rápido: não é preciso realizar duas etapas para ver o resultado do programa. Além disso o interpretador começa a executar mais rapidamente e assim que um erro é detectado a interpretação para. Em geral, erros simples acabam sendo detectados em menos tempo pois não é preciso esperar o término da compilação.

Desvantagens:

Como o programa é sempre lido e interpretado existe uma perda de performance devido ao retrabalho considerando múltiplas execuções. Além disso o próprio interpretador precisa de um tempo para executar e tomar decisões. Em programas compilados pode-se dizer que a maioria dessas decisões são tomadas na compilação tornando a execução mais rápida.

O interpretador precisa estar disponível senão o programa não funciona. Isso pode levar a um aumento do uso de disco rígido.

Caminhos intermediários

Claro que a divisão acima é essencialmente teórica. Existem inclusive propostas híbridas sendo a de maior destaque a linguagem Java. Em java existe a etapa da compilação porém o resultado da mesma não é um código binário diretamente executável mas sim um código simplificado chamado de bytecode. Esse código, por ser simplificado executa muito rapidamente mas ainda precisa de um tipo de interpretador, que no caso da linguagem Java é chamado de Java Virtual Machine (JVM) ou Máquina Virtual Java.

O Java foi implementado dessa maneira para ter um interpretador mais eficiente porém com a possibilidade de compartilhar código para diversos hardwares (a proposta inicial do Java era escrever um único código e executar o mesmo – através da jvm – em diversos hardwares diferentes, como por exemplo aparelhos celulares).

Hoje muitas linguagens adotam uma solução próxima da linguagem Java. Inclusive muitas linguagens foram adaptadas para gerar bytecode e portanto utilizar a eficiente jvm mesmo que o código-fonte seja criado em outras linguagens.

Uma solução intermediária ligeiramente diferente é rodar código interpretado durante o desenvolvimento e gerar uma espécie de bytecode (usar um compilador) somente no ambiente de produção com o código já plenamente desenvolvido e teoricamente testado.

Conclusão

De maneira geral podemos resumir que devido à melhoria do hardware a implementação via interpretador ganhou força ao longo do tempo. A estratégia da compilação ainda persiste mais utilizada em ambientes nas quais a performance precisa ser extraída ao máximo como, por exemplo sistemas operacionais e jogos. Em outros ambientes nos quais a velocidade de desenvolvimento e alteração dos sistemas tem uma importância maior acaba favorecendo linguagens interpretadas.

Dito isso é importante notar que na verdade nada impede que, na teoria, uma linguagem projetada para ser compilada seja executada através de um interpretador ou que uma linguagem originalmente interpretada não possa ser compilada. Mas, na prática, isso não é muito comum já que linguagens acabam sendo mais adotadas ou em um contexto de compilação, interpretação ou em uma mistura de ambos, caso da linguagem Java.

Resolvendo problema de compatibilidade entre java e javac

Me deparei (sic) com um problema ao tentar executar um código Java.

Primeiro eu compilei o programa normalmente com javac. Porém ao tentar executar o programa apareceu a mensagem de erro abaixo:

Transcrição de parte do erro:

“Error: A JNI error has occurred, please check your installation and try again
Exception in thread “main” java.lang.UnsupportedClassVersionError: Aleatorios has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0″

Usando os comandos which e ls descobri que javac apontava para /usr/lib/jvm/java-11-openjdk-amd64/bin/javac e java apontava para /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java, claramente versões diferentes do Java, o que era condizente com o erro acima. Lembro vagamente de ter alterado a versão do runtime Java (não me lembro o motivo) mas eu acreditava que o javac mudava junto. Parece que não.

Bom, com o comando

sudo update-alternatives --config java

Selecionei a versão mais recente do Java (mesma versão do javac).

E aí agora tudo voltou a funcionar. Espero ter ajudado alguém.

Referência utilizada: https://aboullaite.me/switching-between-java-versions-on-ubuntu-linux/