UNIVERSIDADE DO ESTADO DO AMAZONAS - UEA ESCOLA SUPERIOR DE TECNOLOGIA ENGENHARIA DE COMPUTAÇÃO RODRIGO BARROS BERNARDINO IMPLEMENTAÇÃO DO SUPORTE A ORIENTAÇÃO A OBJETOS NO COMPILADOR JARAKI Manaus 2012 RODRIGO BARROS BERNARDINO IMPLEMENTAÇÃO DO SUPORTE A ORIENTAÇÃO A OBJETOS NO COMPILADOR JARAKI Trabalho de Conclusão de Curso apresentado à banca avaliadora do Curso de Engenharia de Computação, da Escola Superior de Tecnologia, da Universidade do Estado do Amazonas, como pré-requisito para obtenção do tı́tulo de Engenheiro de Computação. Orientador: Prof. M. Sc. Jucimar Maia da Silva Júnior Manaus 2012 ii Universidade do Estado do Amazonas - UEA Escola Superior de Tecnologia - EST Reitor: José Aldemir de Oliveira Vice-Reitor: Marly Guimarães Fernandes Costa Diretor da Escola Superior de Tecnologia: Mário Augusto Bessa de Figueiredo Coordenador do Curso de Engenharia de Computação: Raimundo Corrêa de Oliveira Coordenador da Disciplina Projeto Final: Mário Augusto Bessa de Figueiredo Banca Avaliadora composta por: Data da Defesa: 23/11/2012. Prof. M.Sc. Jucimar Maia da Silva Júnior (Orientador) Prof. M.Sc. Raimundo Corrêa de Oliveira Prof. M.Sc. Rodrigo Choji de Freitas CIP - Catalogação na Publicação B523i BERNARDINO, Rodrigo Implementação do Suporte a Orientação a Objetos no Compilador Jaraki / Rodrigo Bernardino; [orientado por] Prof. MSc. Jucimar Maia da Silva Júnior - Manaus: UEA, 2012. 84 p.: il.; 30cm Inclui Bibliografia Trabalho de Conclusão de Curso (Graduação em Engenharia de Computação). Universidade do Estado do Amazonas, 2012. CDU: 004.4’4 iii RODRIGO BARROS BERNARDINO IMPLEMENTAÇÃO DO SUPORTE A ORIENTAÇÃO A OBJETOS NO COMPILADOR JARAKI Trabalho de Conclusão de Curso apresentado à banca avaliadora do Curso de Engenharia de Computação, da Escola Superior de Tecnologia, da Universidade do Estado do Amazonas, como pré-requisito para obtenção do tı́tulo de Engenheiro de Computação. Aprovado em: 23/11/2012 BANCA EXAMINADORA Prof. Jucimar Maia da Silva Júnior, Mestre UNIVERSIDADE DO ESTADO DO AMAZONAS Prof. Raimundo Corrêa de Oliveira, M.Sc. UNIVERSIDADE DO ESTADO DO AMAZONAS Prof. Rodrigo Choji de Freitas, M.Sc. UNIVERSIDADE DO ESTADO DO AMAZONAS iv Agradecimentos Serei eternamente grato aos meus pais e meu grande irmão, que me acompanharam desde o inı́cio da minha vida. Porém, nada adiantaria o apoio e ensinamentos deles sem a sincera dedicação que meus caros professores tiveram. Acredito ainda, que muito desse saber teria sido desperdiçado e bem menos desenvolvido se não fossem os vários conselhos, ensinamentos e a grande confiança que o professor MSc. Jucimar Júnior tanto dedicou a mim, fazendo jus ao posto de orientador e, ainda mais, o de um verdadeiro educador! Obrigado! Seja como for, sem as várias oportunidades e a proteção divina que tive, absolutamente nada disso teria sido possı́vel em minha vida. Portanto, agradeço a Deus por tudo que veio e está por vir. v Resumo Este trabalho apresenta a implementação do suporte a diversos recursos e funcionalidades relacionados à orientação a objetos (OO) da linguagem Java no compilador Jaraki. Este compilador recebe como entrada um código na linguagem de programação Java e gera um outro com mesmo comportamento na linguagem Erlang. A grande diferença é que o programa escrito pelo usuário em Java poderá utilizar as mesmas vantagens da Máquina Virtual do Erlang. Palavras Chave: compiladores, erlang, java, tradutor, linguagens de programação vi Abstract This work provides the implementation of the support to several resources and functionalities related to object orientation (OO) of the Java language on the Jaraki compiler. This compiler receives as input a code written in the Java programming language and generates another one with the same behaviour in the Erlang language. The great difference is that the program written by the user in Java would be able to explore the same advantages of the Erlang Virtual Machine. Key-words: compilers, erlang, java, translator, programming languages vii Sumário Lista de Tabelas x Lista de Figuras xi Lista de Códigos xii 1 Introdução 1.1 Objetivos . . . . . . . . 1.2 Trabalhos Relacionados . 1.3 Justificativa . . . . . . . 1.4 Metodologia . . . . . . . 1.5 Estrutura da Monografia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Referencial Teórico 2.1 Compilador . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Estrutura de um Compilador . . . . . . . . . 2.1.2 Análise Léxica . . . . . . . . . . . . . . . . . . 2.1.3 Análise Sintática . . . . . . . . . . . . . . . . 2.1.4 Análise Semântica . . . . . . . . . . . . . . . 2.1.5 Tabela de Sı́mbolos . . . . . . . . . . . . . . . 2.1.6 Ferramentas para construção de compiladores 2.2 Orientação a Objetos . . . . . . . . . . . . . . . . . . 2.2.1 Objetos . . . . . . . . . . . . . . . . . . . . . 2.2.2 Classes . . . . . . . . . . . . . . . . . . . . . . 2.2.3 Representação de um objeto . . . . . . . . . . 2.2.4 Diagrama de Classes . . . . . . . . . . . . . . 2.2.5 Herança . . . . . . . . . . . . . . . . . . . . . 2.2.6 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 3 3 4 5 . . . . . . . . . . . . . . 6 6 8 8 9 12 12 13 15 16 16 18 20 21 23 viii 2.3 2.4 2.5 2.2.7 Sobrescrita de método . . . . . . . . . . . . . . . 2.2.8 Vinculação Dinâmica de Métodos . . . . . . . . . Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Funções . . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Compilação e Execução . . . . . . . . . . . . . . . 2.3.3 Variáveis . . . . . . . . . . . . . . . . . . . . . . . 2.3.4 Tipos de Dados . . . . . . . . . . . . . . . . . . . Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Compilação e Execução . . . . . . . . . . . . . . . 2.4.2 Modificadores de Visibilidade . . . . . . . . . . . 2.4.3 Construtores . . . . . . . . . . . . . . . . . . . . 2.4.4 Campos static . . . . . . . . . . . . . . . . . . . 2.4.5 Herança de Métodos . . . . . . . . . . . . . . . . 2.4.6 Assinaturas e Subassinaturas de Métodos . . . . . 2.4.7 Sobrescrita de Métodos . . . . . . . . . . . . . . . 2.4.8 As variáveis this e super . . . . . . . . . . . . . 2.4.9 Ocultamento de Métodos . . . . . . . . . . . . . . 2.4.10 Sobrecarga de Métodos . . . . . . . . . . . . . . . 2.4.11 Exemplo: Sobrecarga, Sobrescrita e Ocultamento Jaraki . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 Análise Léxica e Sintática . . . . . . . . . . . . . 2.5.2 Arquitetura Geral . . . . . . . . . . . . . . . . . . 3 Desenvolvimento 3.1 O Compilador Jaraki . . . . . . 3.1.1 Estrutura do Compilador 3.1.2 Tabela de Sı́mbolos . . . 3.2 Tabela de Sı́mbolos para Classes 3.3 Compilando Classes . . . . . . . 3.3.1 Corpo da Classe . . . . . 3.3.2 Objetos . . . . . . . . . 3.3.3 Métodos . . . . . . . . . 3.3.4 Campos . . . . . . . . . 3.3.5 Construtores . . . . . . 3.4 Herança . . . . . . . . . . . . . 3.4.1 Herança de Campos . . 3.4.2 Herança de Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 25 26 27 28 28 29 30 31 32 32 33 34 35 35 36 36 37 37 38 38 39 . . . . . . . . . . . . . 41 41 41 42 43 43 44 44 47 50 53 55 56 57 ix 4 Testes 4.1 Metodologia de Teste . . . . . . . . . . . . . . . . . 4.1.1 Ambiente de Testes . . . . . . . . . . . . . . 4.1.2 Medição de Tempo . . . . . . . . . . . . . . 4.2 Processo de Compilação . . . . . . . . . . . . . . . 4.3 Instanciação de Objetos . . . . . . . . . . . . . . . 4.3.1 Construtor Padrão . . . . . . . . . . . . . . 4.3.2 Construtor Definido pelo Usuário . . . . . . 4.4 Manipulação de Campos de Objeto . . . . . . . . . 4.4.1 Manipulação Externa . . . . . . . . . . . . . 4.4.2 Manipulação Interna . . . . . . . . . . . . . 4.5 Herança . . . . . . . . . . . . . . . . . . . . . . . . 4.5.1 Herança de Campos, Métodos e Sobrescrita 4.5.2 Polimorfismo . . . . . . . . . . . . . . . . . 4.6 Resultado dos Testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 60 60 61 61 63 63 64 64 64 66 68 68 71 73 5 Conclusão 5.1 Trabalhos Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 75 Referências Bibliográficas 76 A Conteúdo do CD: Códigos Fonte, PDFs e Vı́deos A.1 Estrutura Principal e Simbologia . . . . . . . . . . A.2 Pasta jaraki . . . . . . . . . . . . . . . . . . . . . A.2.1 Pasta src . . . . . . . . . . . . . . . . . . . . A.3 Pasta testes . . . . . . . . . . . . . . . . . . . . . A.3.1 Testes de Instanciação . . . . . . . . . . . . A.3.2 Testes de Campos . . . . . . . . . . . . . . . A.3.3 Testes de Herança . . . . . . . . . . . . . . . A.4 Pasta PDF . . . . . . . . . . . . . . . . . . . . . . . A.5 Vı́deos . . . . . . . . . . . . . . . . . . . . . . . . . 78 78 79 80 81 82 83 84 85 85 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . x Lista de Tabelas 3.1 3.2 3.3 Estrutura da Tabela de Sı́mbolos para dados das classes . . . . . . . . . . . Dados dos Campos das Classes . . . . . . . . . . . . . . . . . . . . . . . . Dados dos Campos das Classes . . . . . . . . . . . . . . . . . . . . . . . . 43 56 56 4.1 4.2 4.3 4.4 4.5 4.6 4.7 Teste 1: Instanciação . . . . Teste 2: Instanciação . . . . Teste 3: Campos Externo . Teste 4: Campos Interno . . Teste 5: Herança 1 . . . . . Teste 5: Herança 2 . . . . . Tempos de Todos os Testes . 63 64 65 67 70 71 73 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi Lista de Figuras 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12 2.13 2.14 2.15 2.16 2.17 Um compilador de Linguagem de Programação . . . . . . . . . Um interpretador de Linguagem de Programação . . . . . . . Combinação de compilação e interpretação . . . . . . . . . . . Processamento de Linguagem com Carregador . . . . . . . . . Exemplo de sistema de processamento de linguagem . . . . . . Exemplo de Gramática . . . . . . . . . . . . . . . . . . . . . . Análise Ascendente . . . . . . . . . . . . . . . . . . . . . . . . Exemplo de sistema de processamento de linguagem detalhado Como armazenar um objeto . . . . . . . . . . . . . . . . . . . Diagrama de Classes . . . . . . . . . . . . . . . . . . . . . . . Diagrama dos Animais . . . . . . . . . . . . . . . . . . . . . . Objeto Passaro . . . . . . . . . . . . . . . . . . . . . . . . . . Diagrama dos Animais com Peregrino . . . . . . . . . . . . . . Compilador Jaraki . . . . . . . . . . . . . . . . . . . . . . . . Geração dos analisadores léxico e sintático no Jaraki . . . . . Compilador Jaraki: Arquitetura Geral - Parte 1 . . . . . . . . Compilador Jaraki: Arquitetura Geral - Parte 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 7 7 8 9 11 11 13 19 21 21 22 23 38 39 39 40 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 Guardando um Objeto do Tipo Animal . . . . . . . . . . Execução da Instanciação de um Objeto . . . . . . . . . Execução de Chamada de Método de Objeto . . . . . . . Acesso a Campos em Método de Objeto . . . . . . . . . Acesso a Campos de Método de Objeto por outra Classe Execução de um Construtor Definido pelo Usuário . . . . Diagrama dos Animais . . . . . . . . . . . . . . . . . . . Instanciando com Campos da Superclasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 46 49 52 53 54 55 57 4.1 Compilação e Execução do Cadastro de Bola . . . . . . . . . . . . . . . . . 62 . . . . . . . . . . . . . . . . . . . . . . . . xii 4.2 Diagrama dos Funcionários . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 A.1 A.2 A.3 A.4 A.5 A.6 A.7 A.8 Simbologia para Representar Pastas e Arquivos Pasta do Jaraki . . . . . . . . . . . . . . . . . . Pasta dos Códigos fonte do Jaraki . . . . . . . . Pasta de Testes . . . . . . . . . . . . . . . . . . Arquivos dos Testes de Instanciação . . . . . . . Arquivos dos Testes de Campos . . . . . . . . . Arquivos dos Testes de Herança . . . . . . . . . Documentos da Monografia e Apresentação . . . 78 79 80 81 82 83 84 85 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii Lista de Códigos 2.1.1 Exemplo de descrição de analisador léxico . . . . . . . . . . . . . . . . . . 15 2.1.2 Exemplo de descrição de analisador sintático . . . . . . . . . . . . . . . . . 16 2.2.1 Código de exemplo: Classe Bicicleta . . . . . . . . . . . . . . . . . . . . . . 18 2.2.2 Código de exemplo: Classe Principal1 . . . . . . . . . . . . . . . . . . . . . 19 2.2.3 Código de exemplo: Classes dos Animais . . . . . . . . . . . . . . . . . . . 22 2.2.4 Código de exemplo: Classe Principal2 . . . . . . . . . . . . . . . . . . . . . 22 2.2.5 Código de exemplo: Classe Principal3 . . . . . . . . . . . . . . . . . . . . . 23 2.2.6 Código de exemplo: Sobrescrita de métodos . . . . . . . . . . . . . . . . . . 24 2.2.7 Código de exemplo: Principal4 . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.2.8 Código de exemplo: Vários tipos de Passaro . . . . . . . . . . . . . . . . . 25 2.3.1 Código Exemplo de Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 2.3.2 Exemplo de Compilação de Código em Erlang . . . . . . . . . . . . . . . . 28 2.3.3 Atribuindo novos valores a mesma variável . . . . . . . . . . . . . . . . . . 29 2.4.1 Código de exemplo: Pessoa e PrincipalPessoa . . . . . . . . . . . . . . . . 31 2.4.2 Exemplo de Construtor na Classe Ponto . . . . . . . . . . . . . . . . . . . 33 2.4.3 Exemplo de Principal para Construtor na Classe Ponto . . . . . . . . . . . 33 2.4.4 Exemplo de Principal para Campos Static . . . . . . . . . . . . . . . . . . . 34 2.4.5 Exemplo de Classe para Assinatura de Métodos . . . . . . . . . . . . . . . 35 2.4.6 Exemplo de Sobrescrita . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.4.7 Exemplo de Sobrecarga, Sobrescrita e ocultamento . . . . . . . . . . . . . . 38 3.3.1 Exemplo de Instanciação de um Objeto . . . . . . . . . . . . . . . . . . . . 45 3.3.2 Tradução de Chamada de Método Estático . . . . . . . . . . . . . . . . . . 48 3.3.3 Tradução de Métodos de Objeto . . . . . . . . . . . . . . . . . . . . . . . . 48 xiv 3.3.4 Acesso a Campos de Objeto . . . . . . . . . . . . . . . . . . . . . . . . . . 51 3.3.5 Construtor Definido pelo Usuário . . . . . . . . . . . . . . . . . . . . . . . 54 3.3.6 Construtor Padrão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.4.1 Instanciando com Campos da Superclasse . . . . . . . . . . . . . . . . . . . 57 3.4.2 Herdando Métodos das Superclasses . . . . . . . . . . . . . . . . . . . . . . 58 3.4.3 Exemplo do Uso do Super . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.2.1 Códigos de Exemplo para Processo de Compilação: Cadastro de Bola . . . 62 4.3.1 Teste 1: Instanciação - Java . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.3.2 Teste 2: Instanciação - Java . . . . . . . . . . . . . . . . . . . . . . . . . . 64 4.4.1 Teste 3: Manipulação de Campos - Java . . . . . . . . . . . . . . . . . . . 65 4.4.2 Execução do Teste 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.4.3 Teste 4: Manipulação de Campos - Java . . . . . . . . . . . . . . . . . . . 66 4.4.4 Execução do Teste 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 4.5.1 Teste de Herança 1: Classe Principal . . . . . . . . . . . . . . . . . . . . . 69 4.5.2 Teste 5: Herança 1 - Classes Funcionario e Tecnico . . . . . . . . . . . . . 69 4.5.3 Execução do Teste 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.5.4 Teste de Herança 2: Classe Principal . . . . . . . . . . . . . . . . . . . . . 71 4.5.5 Execução do Teste 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Capı́tulo 1 Introdução Como exposto por [Stroustrup1991], programação orientada a objeto surgiu da necessidade de dar um melhor controle e hierarquia ao código. A princı́pio, a programação imperativa focava nos procedimentos: “decida quais procedimentos você quer; use os melhores algoritmos que você encontrar”. Com a evolução das linguagens, uma importância maior foi dada aos dados, dando inı́cio aos tipos abstratos de dados e ao paradigma “decida os tipos desejados; forneça um conjunto completo de operações para cada tipo” [Stroustrup1991]. Porém, não havia nenhuma estrutura hierárquica com dados e foi então que a programação orientada a objetos surgiu. As partes comuns de uma coleção de tipos abstratos de dados similares são fatoradas e colocadas em um novo tipo. Os membros da coleção herdam essas partes comuns do novo tipo. Esse recurso é a herança, que, segundo Sebesta, está no centro da programação orientada a objetos e das linguagens que a suportam. [Sebesta2011]. Java é a linguagem orientada a objetos (OO) mais utilizada segundo levantamento do [TIOBE2012]. Gosling, um dos criadores da linguagem, declarou que ela foi projetada para não exigir muito treinamento para usá-la, que códigos escritos em Java pudessem ser facilmente reutilizados, os dados, em tempo de execução, pudessem ser facilmente distribuı́dos em uma rede de computadores, fornecendo um ambiente seguro para essas ocasiões e independente de arquitetura, podendo estar presentes em pequenos computadores e grandes sistemas de vários tipos. [Gosling1995] Erlang, como descrita por Armstrong, criador da linguagem, é uma linguagem funcional desenvolvida para ser usada em sistemas distribuı́dos que exigem uma alta performance, Objetivos 2 processando grandes quantidades de dados paralela e concorrentemente. Também de acordo com Armstrong, programas escritos em Erlang podem ser facilmente escaláveis, tolerantes a falhas e exigem uma quantidade menor de linhas de código para serem implementados, desde que o problema seja adequado ao paradigma. [Armstrong2007] Cada linguagem e paradigma de programação foram projetados com propósitos diferentes e visando atender situações diferentes. Sendo assim, desenvolver uma aplicação em uma linguagem pode ser mais difı́cil e menos eficiente que a mesma aplicação em outra linguagem. Porém, os programadores podem estar mais habituados a desenvolver em determinada linguagem e, logo, mantêm seus projetos na mesma linguagem. Para que não seja necessário trocar a linguagem de programação, diversos projetos como [Jython2012] ou [Efene2012] procuram desenvolver compiladores alternativos para que uma linguagem se beneficie das vantagens de outra linguagem. 1.1 Objetivos Este trabalho apresenta a implementação do suporte a Orientação a Objetos (OO) no compilador Jaraki. O compilador Jaraki tem o propósito de permitir que softwares escritos em Java sejam interpretados na Máquina Virtual do Erlang (EVM). Para tanto, o compilador deve interpretar os aspectos imperativo e orientado a objetos do Java e gerar códigos em Erlang. Para suportar OO é necessário implementar diversos recursos relacionados, são eles: • Programação modular através de classes com funções estáticas; • Definição de tipos abstratos de dados (classes e instanciação de objetos); • Toda classe deve herdar os métodos e atributos de sua classe pai; • Vinculação dinâmica em chamadas de métodos (polimorfismo); • Diferentes nı́veis de visibilidade de atributos e métodos. Trabalhos Relacionados 1.2 3 Trabalhos Relacionados Entre as que utiliza a Máquina Virtual do Java (JVM) está o Jython [Jython2012], uma implementação da linguagem de programação Python escrita em Java 100% puro. Outro projeto semelhante é o JRuby [JRuby2012], que é uma implementação da linguagem Ruby na JVM. Seguindo a mesma linha, foi desenvolvido Clojure [Clojure2012], uma implementação de um dialeto da linguagem Lisp que roda sobre a JVM. Todas elas permitem uma integração totalmente transparente entre programas escritos na linguagem sendo compilada e programas em Java, além de usufruı́rem de todas as caracterı́sticas do Java, como, por exemplo, a portabilidade. Existem linguagens que rodam também sobre a EVM. Reia [Reia2012] é uma linguagem script semelhante ao Ruby escrito para a EVM. Já a linguagem Efene [Efene2012] possui sintaxe semelhante a python e javascript. Joxa [Joxa2012] possui sintaxe semelhante ao Lisp, mas é uma linguagem com diversas caracterı́sticas únicas. Essas linguagens procuram fornecer uma sintaxe mais intuitiva para gerar código executável na EVM. Especificamente o Reia procura também facilitar o uso de OO em um ambiente próprio para concorrência: a EVM. 1.3 Justificativa Programação orientada a objetos está no centro da linguagem Java, logo, para tornar o compilador Jaraki totalmente funcional, é necessário que este suporte OO. Além disso, interpretar OO com uma linguagem funcional implica em diversos desafios por se tratarem de paradigmas diferentes. Este trabalho apresenta soluções para se representar e gerenciar certas estruturas exclusivas de linguagens OO em uma linguagem funcional. Algumas das linguagens citadas na seção anterior possuem caracterı́sticas OO, porém de forma diferente de como é visto no Java. No caso do Jython, foi implementado OO de acordo com a sintaxe e lógica do Python. Já as linguagens Reia e Efene utilizam uma abordagem compatı́vel com a EVM, pois, devido à filosofia do Erlang, não existem variáveis globais e variáveis na função só podem ser atribuı́das uma única vez, dentre outras limitações. Este projeto procura, de fato, implementar OO sem oferecer nenhuma limitação Metodologia 4 devido à natureza do Erlang. 1.4 Metodologia O desenvolvimento desse trabalho foi estruturado sobre as seguintes atividades: 1. Estudo da linguagem de programação Java. Há detalhes que passam desapercebidos pelos programadores, erros que raramente são cometidos, mas que o compilador deve verificar. Nesta etapa foram estudadas algumas possibilidades sintáticas e semânticas da linguagem. 2. Delimitação das funcionalidades suportadas pelo Jaraki. Java possui diferentes pacotes, módulos e recursos sintáticos e semânticos. Foi preciso selecionar quais seriam implementados por este trabalho. 3. Desenvolvimento da arquitetura e protótipo inicial. O compilador foi dividido em diversos módulos e nesta etapa implementou-se um conjunto bem reduzido de recursos, focando mais na arquitetura e interface com o usuário do que na funcionalidade. 4. Desenvolvimento iterativo e incremental. Foram desenvolvidos vários pequenos programas que utilizam diferentes recursos da linguagem, sendo que, para cada novo programa, todos os módulos do compilador eram incrementados (analisador léxico, sintático e semântico). 5. Testes de validação e desempenho. Comparou-se as entradas e saı́das de programas compilados com o compilador da OracleTM e o Jaraki, além do desempenho dos mesmos. Estrutura da Monograa 1.5 5 Estrutura da Monografia Esta seção descreve brevemente o conteúdo dos próximos capı́tulos. no capı́tulo “Referencial Teórico” são apresentados os principais conceitos utilizados de compiladores, OO e as linguagem Java e Erlang. No capı́tulo “Desenvolvimento” são descritos os módulos e bibliotecas desenvolvidos para suportar OO tanto na compilação quanto na execução. O capı́tulo “Testes” apresenta a eficácia e eficiência das bibliotecas e módulos desenvolvidos comparando com o compilador oficial da OracleTM . Por fim, em “Conclusões” são descritas as lições aprendidas, habilidades desenvolvidas e propostas de trabalhos futuros. Capı́tulo 2 Referencial Teórico Neste capı́tulo são apresentados os principais conceitos utilizados no desenvolvimento do trabalho. Será descrito a estrutura de um compilador, detalhando seus principais componentes; os recursos de orientação a objetos (OO) implementados neste trabalho; as principais caracterı́sticas da linguagem Erlang; os recursos especı́ficos da linguagem Java relacionados a OO implementados neste trabalho; o que é o compilador Jaraki e como este trabalho afetou a sua estrutura. 2.1 Compilador Um compilador (Figura 2.1) é um programa que recebe como entrada um programa em uma linguagem de programação (linguagem fonte) e traduz para um programa equivalente em outra linguagem de programação (linguagem objeto) [Aho2007]. Geralmente a tradução é feita de uma linguagem de alto nı́vel para outra de baixo nı́vel, sendo que a linguagem objeto pode estar em nı́vel de máquina ou não. programa fonte Compilador programa objeto Figura 2.1: Um compilador de Linguagem de Programação Compilador 7 Um outro tipo de processador de linguagens é o interpretador (Figura 2.2). Ao invés de gerar novos programas para que posteriormente sejam executados, ele lê o programa e suas entradas e executa diretamente. Geralmente programas compilados (que foram processados por um compilador) são mais eficientes que programas interpretados, porém os últimos geralmente oferecem uma melhor forma de diagnosticar erros [Aho2007]. programa fonte entrada Interpretador saída Figura 2.2: Um interpretador de Linguagem de Programação Os processadores de linguagens como Java e Erlang combinam compilação e interpretação. Para tanto, eles fazem uso do recurso conhecido como “máquina virtual”. Primeiramente o programa fonte é compilado para uma forma intermediária, chamada bytecodes, e então são interpretados em uma máquina virtual (Figura 2.3) [Aho2007]. programa fonte Tradutor código intermediário entrada Interpretador saída Figura 2.3: Combinação de compilação e interpretação Em alguns casos, um programa pode ser divido em vários arquivos (módulos) e, para compilá-lo, é preciso unir a informações de todos eles. Esse trabalho tanto pode ser feito por um outro programa, chamado pré-processador, quanto pelo próprio compilador [Aho2007]. Além disso, certas linguagens de programação, como o Java, possuem programas précompilados (bibliotecas) que estão disponı́veis em forma de rotinas ou funções para o programador utilizar na composição de seu programa. Quando determinada biblioteca for utilizada, é preciso acrescentar seu código ao programa objeto do código do programador. Esse trabalho também pode ser feito pelo compilador ou por outro programa, chamado “editor de ligação” ou “carregador” (Figura 2.4) [Aho2007]. Compilador 8 programa fonte Compilador código de máquina relocável Editor de ligação / Carregador código de máquina alvo Figura 2.4: Processamento de Linguagem com Carregador 2.1.1 Estrutura de um Compilador Até aqui o compilador foi tratado como uma “caixa preta”, que mapeia um programa fonte para um semanticamente equivalente na linguagem objeto. No conteúdo dessa “caixa” existem duas partes: análise e sı́ntese, cada uma com suas respectivas subdivisões [Aho2007]. A parte da análise separa um código fonte em diversas pequenas partes e verifica se estão organizadas de acordo com uma estrutura gramatical. Essa estrutura é usada para criar uma representação intermediária do programa fonte. Se forem detectados erros, devem ser apresentadas mensagens esclarecedoras ao usuário para que este possa tomar uma ação corretiva. Além disso, a análise também é responsável por reunir informações sobre o programa de entrada, que são guardadas na chamada “tabela de sı́mbolos”, passada junto com a representação intermediária para a parte de sı́ntese. A parte de sı́ntese constrói o programa objeto a partir dessas informações [Aho2007]. A figura 2.5 representa um compilador como uma sequência de fases, onde o programa é transformado de uma representação para outra, até a geração do código objeto. A tabela de sı́mbolos, que armazena informações sobre todo o programa fonte, é usada em todas as fases [Aho2007]. 2.1.2 Análise Léxica A primeira fase de um compilador é a análise léxica. O analisador léxico lê o fluxo de caracteres que compõem o programa fonte e os agrupa em sequências significativas, Compilador 9 fluxo de caracteres Analisador Léxico fluxo de tokens Analisador Sintático árvore sintática Tabela de Símbolos Analisador Semântico árvore sintática Gerador de código intermediário representação de código intermediário Gerador de código código de máquina alvo Figura 2.5: Exemplo de sistema de processamento de linguagem chamadas lexemas [Aho2007]. Ou seja, é nessa fase que as palavras ganham significado. A sequência de caracteres “1”, “.” e “0”, por exemplo, formará um lexema e, mais adiante, ele será identificado como o número de ponto flutuante “1.0”. Para representar essa informação, o analisador léxico produz como saı́da um token no formato [nome-token, valor-atributo] [Aho2007]. O componente nome-token é um sı́mbolo abstrato usado durante a análise sintática e o segundo componente valor-atributo contém uma especificação desse token ou aponta para uma entrada na tabela de sı́mbolos referente a esse token. No exemplo dado anteriormente o token dele pode ser construı́do assim: [float, 1.0]. O primeiro bloco da Figura 2.8 demonstra essa fase da compilação. 2.1.3 Análise Sintática A segunda fase do compilador é a análise sintática. O analisador sintático, também chamado de parser, utiliza o primeiro componente dos tokens (nome-token), produzido pelo analisador léxico, para criar uma representação intermediária tipo árvore, que mostra a estrutura gramatical da sequência de tokens, chamada “árvore sintática” [Aho2007]. As Compilador 10 instruções na linguagem fonte são quebradas em partes de nı́vel mais baixo e colocadas na ordem de execução adequada (segundo bloco da Figura 2.8). Nesse momento também são verificados se existem erros sintáticos que, caso presentes, são notificados ao usuário. Gramáticas Livres de Contexto A estrutura sintática das linguagens de programação são descritas por “gramáticas livre de contexto”, ou “gramáticas” para abreviar. Isso significa que é definidos um ou mais “agrupamentos sintáticos” de forma hierárquica. Por exemplo, uma express~ ao pode ser uma soma de outra express~ ao com um termo; um termo, por sua vez, pode ser um número. Assim, juntando as várias partes se constrói a sintaxe da linguagem. Formalmente, como exposto por [Aho2007], essas gramáticas são compostas por: • Terminais: são os sı́mbolos básicos a partir dos quais as cadeias são formadas. São os nomes dos tokens provenientes da análise léxica; • Não-terminais: são variáveis sintáticas que representam conjuntos de cadeias. Eles impõem uma estrutura hierárquica sobre a linguagem que é a chave para a análise sintática e tradução; • Sı́mbolo inicial: um não-terminal que representa a linguagem gerada pela cadeia; • Produções: especificam a forma como os terminais e não-terminais podem ser combinados para formar cadeias. Cada produção consiste em: – Um não-terminal chamado de cabeça ou lado esquerdo da produção, que define algumas das cadeias representadas pela cabeça; – O sı́mbolo de “seta”; – Um corpo ou lado direito da produção, que consiste de zero ou mais terminais e não-terminais. Eles descrevem uma forma como as cadeias do lado esquerdo da produção podem ser construı́das. A gramática da Figura 2.6 define expressões de soma e subtração, que podem, ou não, vir entre parênteses. Os sı́mbolos terminais estão em negrito e os não-terminais não. O sı́mbolo inicial é expressao. Compilador 11 expressao expressao expressao termo termo → → → → → expressao + termo expressao - termo termo ( expressao ) id Figura 2.6: Exemplo de Gramática Análise Ascendente com LALR(1) O método para construção da árvore sintática mais frequentemente usado é a análise ascendente LALR(1) [Aho2007]. A análise sintática ascendente corresponde à construção da árvore sintática a partir das folhas. A Figura 2.7 representa a construção de uma árvore de derivação com esse tipo de análise para uma soma. Esta árvore é uma representação gráfica de como as produções são aplicadas para substituir não-terminais. As letras E e T representam as regras da gramática da Figura 2.6 expressao e termo, respectivamente. id + id T + id id E + id E + T T T id id id E E + T T id id Figura 2.7: Análise Ascendente Analisadores LALR(1), ou LR(1) com Look Ahead (LA) são uma otimização dos LR(k). O L representa que a entrada é lida da esquerda para a direita; o R, que a construção das derivações será feita à direita ao reverso; o k, quantos sı́mbolos (tokens) à frente no fluxo de entrada auxiliam na decisão de qual regra adotar. Seu princı́pio de funcionamento é a construção de tabelas que indicam o que fazer ao encontrar determinado token. As ações são ler o próximo token ou substituir por uma regra o que já foi lido. Mais detalhes podem ser encontrados em [Aho2007]. Compilador 2.1.4 12 Análise Semântica O analisador semântico utiliza a árvore sintática e as informações na tabela de sı́mbolos para verificar a consistência semântica do programa fonte com a definição da linguagem. Nesta fase o significado dos ramos da árvore sintática e a relação entre eles são analisados. A “verificação de tipos” é uma parte importante nessa análise [Aho2007]. Para tanto, compilador verifica se cada operador possui operandos compatı́veis. Por exemplo, se determinada linguagem não permite que uma variável do tipo string receba valores inteiros, isso deve ser checado nessa fase. Outra importante atividade relacionada é a realização de conversões de tipos, chamadas “coerções” [Aho2007]. Considere, por exemplo, que em uma operação de atribuição uma variável de um tipo ponto flutuante receba um número inteiro. Em certas linguagens, como o Java, o compilador deve realizar a conversão do valor do número para ponto flutuante. O terceiro bloco da Figura 2.8 ilustra essa fase da compilação. 2.1.5 Tabela de Sı́mbolos A tabela de sı́mbolos é uma estrutura de dados usada pelos compiladores para conter informações sobre as construções do programa fonte. Ela é fundamental durante o processo de compilação, pois registra os nomes de variáveis usados no programa fonte e guarda informações sobre os diversos atributos de cada nome. Esses atributos podem determinar qual o escopo de um nome (onde ele pode ser usado), seu tipo, a quantidade de tipos dos parâmetros para funções, o valor e espaço de memória alocados para variáveis, onde se encontra a implementação de determinada função, entre outros. [Aho2007] Ela deve ser projetada de tal forma que rapidamente o compilador possa consultar informações especı́ficas de cada nome. Além disso, em certos casos, ela pode ser composta de outros tipos de tabelas, como no caso de classes, em que é preciso armazenar uma entrada para cada campo e método [Aho2007]. Ela deve estar acessı́vel durante todas as fases da compilação, como mostrado na Figura 2.8. Compilador 13 position = initial + rate * 60 Analisador Léxico <id,1><=><id,2><+><id,3><*><60> Analisador Sintático = + <id,1> * <id,2> <id,3> Tabela de Símbolos 60 Analisador Semântico = + <id,1> <id,2> * <id,3> inttofloat 60 Gerador de código intermediário t1 = inttofloat(60) t2 = id3 * t1 t3 = id2 + t2 id1 = t3 Figura 2.8: Exemplo de sistema de processamento de linguagem detalhado 2.1.6 Ferramentas para construção de compiladores Muitos desenvolvedores de software tiram proveito de diversos “ambientes de desenvolvimento”, como editores de texto integrados a compiladores, depuradores, gerenciadores de versão, entre outros. O projetista de compiladores também possui suas ferramentas que auxiliam o desenvolvimento desse tipo de software. Essas ferramentas utilizam linguagens próprias para especificar e implementar diversas partes de um compilador e frequentemente usam algoritmos bastante sofisticados. Algumas das mais utilizadas são os geradores de analisadores sintáticos e geradores de analisadores léxicos [Aho2007]. Compilador 14 Flex Flex, ou Fast Lexical Analyser, [Flex2012] é um gerador de analisador léxico rápido. Ele é uma implementação gratuita do programa lex. Este programa recebe como entrada um arquivo de texto contendo a descrição do analisador a ser gerado. Esta descrição é composta por uma série de pares de expressões regulares (regras que definem como formar os lexemas) e código em C (definem os tokens). Após processar as regras, o Flex gera um código-fonte em C, chamado lex.yy.c, que define a função yylex(). Este programa gerado analisa sua entrada (código-fonte da linguagem desenvolvida) em busca de ocorrências de texto que correspondam a algum dos padrões definidos nas regras. Ao encontrar um padrão ele executa o código em C correspondente. Leex Leex é o gerador de analisador léxico do Erlang e é similar ao flex [Leex2012]. Recebe como entrada um arquivo de extensão .xrl contendo a descrição do analisador. A descrição é dividida em definições, com expressões regulares associadas a nomes; regras, que utilizam as definições através dos nomes para formar os lexemas e tokens correspondentes; e código Erlang, que permite definir funções auxiliares para formar os tokens. Ao processar o .xrl, o Leex gera um código em Erlang com o mesmo nome do arquivo de entrada. Este arquivo define a função string(), que recebe uma cadeia de caracteres contendo o código-fonte e gera uma lista de tokens para ser usado posteriormente. O Código 2.1.1 exemplifica essa estrutura. 1 2 3 Definitions. ComparatorOp = (<|<=|==|>=|>|!=) 4 5 6 Rules. {ComparatorOp} : {token, {comparation_op, TokenLine, op(TokenChars)}}. 7 8 Erlang code. op(OpChar) -> case OpChar of "<=" -> "!=" -> "&&" -> "||" -> "!" -> _ end. 9 10 11 12 13 14 15 16 ’=<’; ’=/=’; ’and’; ’or’; ’not’; -> list_to_atom(OpChar) Código 2.1.1: Exemplo de descrição de analisador léxico Compilador 15 Bison Bison é um gerador de analisador sintático de propósito geral que converte uma gramática livre de contexto em um analisador LALR(1) [Bison2012]. Para tanto, o arquivo de entrada deve definir um ou mais sı́mbolos terminais e não-terminais, a precedência de operadores e a gramática. Além disso, pode conter código em C para auxiliar na tomada de decisão durante a análise. Após analisar a definição da linguagem, ele gera um código em C que define a função yyparse(). Para criação da árvore sintática e uso posterior pelo analisador semântico, o usuário deve importar tipos auxiliares definidos no bison, como o tree, que está na biblioteca ptypes.h. Yecc Yecc é o gerador de analisador sintático LALR(1) para o Erlang [Yecc2012] e é semelhante ao bison. Ele recebe uma gramática como entrada e produz código Erlang de um analisador sintático. O arquivo de entrada deve ter extensão .yrl, contendo uma lista de não-terminais; terminais; a definição do sı́mbolo inicial ; opcionalmente uma ordem de prioridade ao analisar terminais; um conjunto de regras (a gramática); e código Erlang para auxiliar na construção da árvore. O código em Erlang gerado pelo analisador define uma função parse() que recebe como entrada uma lista de tokens e aplica as definições para criar a árvore sintática, que em Erlang são tuplas dentro de tuplas, sem a necessidade de definir novos tipos. O Código 2.1.2 exemplifica essa estrutura. 1 2 3 4 5 6 7 8 9 Nonterminals start_parser Rootsymbol start_parser. start_parser -> add_expr : ’$1’. add_expr -> mult_expr add_op add_expr : {op, line(’$2’), unwrap(’$2’), ’$1’, ’$3’}. 10 11 12 mult_expr -> literal mult_op mult_expr : {op, line(’$2’), unwrap(’$2’), ’$1’, ’$3’}. mult_expr -> literal : ’$1’. 13 14 15 literal -> integer 16 17 18 Erlang code. unwrap({_, _, Value}) line({_, Line, _}) : ’$1’. -> Value. -> Line; Código 2.1.2: Exemplo de descrição de analisador sintático Orientação a Objetos 2.2 16 Orientação a Objetos Programação Orientada a Objetos (POO) surgiu da necessidade de fornecer uma estrutura hierárquica ao código e dispor de funções e estruturas de dados como uma única unidade [Sebesta2011]. Um objeto é um tipo abstrato que contém caracterı́sticas (atributos) e comportamentos (métodos). Uma linguagem que suporta POO deve oferecer meios de descrever um objeto qualquer, criar objetos que são especializações deste primeiro e permitir exprimir a distinção entre eles. Por exemplo, uma forma possui area e cor e a função desenhar. Já um circulo, que também é uma forma, possui area, cor e raio e a função desenha-circulo. Se a linguagem permite definir tipos abstratos de dados, expressar a distinção entre os tipos que são generalizações de outros (como entre forma e circulo) e permite o polimorfismo, então ela suporta orientação a objetos [Sebesta2011]. 2.2.1 Objetos Objetos no mundo real possuem todos um estado e um comportamento. Cachorros tem um estado (nome, cor, fome, humor) e comportamento (latir, andar, comer). Bicicletas também tem um estado (marcha, velocidade atual) e comportamentos (mudar a marcha, frear, pedalar). Objetos de software são conceitualmente semelhantes aos objetos do mundo real. Objetos guardam o estado em campos (variáveis das linguagens de programação) e apresentam comportamentos através de métodos (funções em linguagens de programação). É possı́vel ocultar o estado interno e exigir que toda interação com o objeto seja feita através de métodos, isso é chamado encapsulamento de dados, que é um conceito fundamental em POO. 2.2.2 Classes No mundo real, frequentemente encontra-se muitos objetos do mesmo tipo. Devem existir várias bicicletas, todas da mesma marca e modelo. Cada uma foi construı́da do mesmo conjunto de diagramas e, então, contêm os mesmos componentes. Em termos de orientação a objetos, a bicicleta de um indivı́duo é dita como uma instância da classe de objetos conhecida como bicicletas. Classes em software, de forma semelhante, agrupam objetos semelhantes em um novo tipo. Orientação a Objetos 17 Considere o Código 2.2.1. Ele contém uma possı́vel implementação de Bicicleta na linguagem de programação Java. 1 2 3 class Bicicleta { int velocidade = 0; int marcha = 1; 4 5 void mudarMarcha(int novoValor) { marcha = novoValor; } 6 7 8 void acelerar(int incremento) { velocidade = velocidade + incremento; } 9 10 11 12 13 void frear(int decremento) { velocidade = velocidade - decremento; } 14 15 16 void imprimirEstado() { System.out.println( "velocidade: " + velocidade + "\n" + "marcha: " + marcha); } 17 18 19 20 21 22 } Código 2.2.1: Código de exemplo: Classe Bicicleta O código apresenta uma descrição de como todo objeto dessa classe deve ser. Os campos velocidade e marcha representam o estado do objeto e os métodos mudarMarcha, acelerar, frear e imprimirEstado definem a interação dele com outros objetos. Porém, este código não pode ser executado, ele é de fato apenas um “protótipo”. É necessário um método principal (em Java chamado main) que instancie um objeto dessa classe para que ele possa ser usado. 2.2.3 Representação de um objeto Sempre que um objeto é criado, é reservado um espaço na memória para guardar o valor das variáveis dele e a qual classe ele pertence (Figura 2.9). Uma referência a esse espaço de memória é criada para que seja possı́vel alterar o estado do objeto no futuro. A referência à classe do objeto deve ser mantida para dar suporte à vinculação dinâmica de métodos, como mostrado na Seção 2.2.8, Página 25. Orientação a Objetos 18 memória heap área dos métodos referência para dados da classe referência para objeto dado de instância dado de instância ...... dados da classe Figura 2.9: Como armazenar um objeto Em Java, para criar um objeto, deve ser usado o operador new juntamente com um construtor da classe que se deseja instanciar. As variáveis que armazenam referências a objetos são chamadas “variáveis de referência”. O Código 2.2.2 contém um exemplo de instanciação. 1 2 3 4 5 6 7 class Principal1 { public static void main(String[] args) { Bicicleta b = new Bicicleta(); b.mudarMarcha(2); b.acelerar(2); } } Código 2.2.2: Código de exemplo: Classe Principal1 Orientação a Objetos 2.2.4 19 Diagrama de Classes Diagrama de classe é um tipo de representação da UML [Booch2006] para expressar a estrutura e as relações entre as classes. Eles podem expressar conceitos de POO como associação, agregação, multiplicidades, etc., mas para entendimento deste trabalho é exposto apenas a representação da generalização (herança), campos e métodos. UML A UML (Unified Modeling Language) é uma linguagem de modelagem visual de propósito geral usada para especificar, visualizar, construir e documentar artefatos de um sistema de software. Como exposto em [Booch2006], ela expressa, dentre outros detalhes, as decisões e o entendimento de um sistema a ser construı́do. Ela inclui conceitos semânticos, notações e diretrizes. Além disso, pode representar a estrutura estática e o comportamento dinâmico de um sistema. Um sistema é modelado como uma coleção de objetos discretos que interagem entre si para beneficiar um usuário externo. A estrutura estática define os tipos de objetos importantes para um sistema, suas implementações e o relacionamento entre eles. O comportamento dinâmico define a história dos objetos ao longo do tempo e a comunicação entre eles. Modelar um sistema através de diversos, porém correlatos, pontos de vista [Booch2006]. Representação de Classes A notação para uma classe em UML é um retângulo com “compartimentos” para o nome da classe, atributos (campos) e operações (métodos), como mostrado na Figura 2.10. A visibilidade dos métodos é expressa pelo sinal de “+” ou “-” antes do nome do campo ou método, sendo public ou private, respectivamente. Após o nome do campo há um sinal “:” seguido do tipo dele. Já para o método, o que vem após esse sinal é o tipo de retorno. Para expressar a generalização (ou herança, explicado na Seção 2.2.5), são usadas setas com a extremidade branca. A seta aponta para a superclasse. Sendo assim, o diagrama da Figura 2.10 é interpretado da seguinte maneira: define a classe Animal, que tem os campos public peso, do tipo int e cor do tipo int; os métodos public comer(), com tipo de retorno void e andar(), com tipo de retorno void. Orientação a Objetos 20 tipo Animal visibilidade + peso : int + cor : int campos + comer() : void + andar() : void métodos retorno classe generalização Figura 2.10: Diagrama de Classes 2.2.5 Herança POO permite que classes herdem variáveis e comportamentos comuns. Na prática, é a funcionalidade de definir uma nova classe que é uma versão modificada de uma classe existente sem a necessidade de redefinir as partes comuns à classe herdada. A classe original é chamada superclasse e a nova classe é chamada subclasse. Uma vantagem disso é que a subclasse pode criar novos campos e métodos, além de redefinir métodos existentes. Para exemplificar, considere o diagrama de classes da Figura 2.11 e as implementações de suas classes no Código 2.2.3. Um Animal pode ser um Cachorro ou um Passaro. Ambos podem comer e andar, possuem peso e cor. Mas o Cachorro late e tem pelos, o Passaro voa e tem penas. Figura 2.11: Diagrama dos Animais Orientação a Objetos 1 2 3 4 5 6 7 8 21 class Animal { float peso; String cor; void comer(){} void andar(){} } 9 10 class Cachorro extends Animal { int pelos; 11 12 13 } 14 15 16 class Passaro extends Animal { int penas; void latir(){} 17 18 19 void voar(){} } Código 2.2.3: Código de exemplo: Classes dos Animais Para esse conjunto de classes, um objeto da classe Passaro deve ser armazenado como mostrado na Figura 2.12. memória heap área dos métodos referência para métodos de Passaro peso cor penas Passaro comer() {...} andar(){...} voar(){...} Figura 2.12: Objeto Passaro Isso porque o estado de um Passaro é definido pelos campos definidos na superclasse e na própria classe. Sendo assim, o Código 2.2.4 é válido: 1 2 3 4 5 6 class Principal2 { public static void main(String[] args) { Passaro p = new Passaro(); p.peso = 15; p.penas = 100; p.comer(); p.voar(); 7 8 9 10 } } Código 2.2.4: Código de exemplo: Classe Principal2 Orientação a Objetos 2.2.6 22 Polimorfismo O polimorfismo é uma propriedade de linguagens OO que permite um objeto em particular ser usado como se ele fosse de vários tipos. Esses tipos correspondem às suas superclasses. Para exemplificar, considere que no exemplo dos animais, da Figura 2.2.3, fosse criado uma nova classe de pássaros, que podem mergulhar, chamada Peregrino (Figura 2.13). Figura 2.13: Diagrama dos Animais com Peregrino Nesse caso, um objeto da classe Peregrino possui todos os campos e métodos de Passaro e Animal, logo pode interagir com outros objetos como se ele fosse dessas classes. A linguagem Java permite, com o polimorfismo, o que é mostrado no Código 2.2.5. 1 2 3 4 5 class Principal2 { public static void main(String[] args) { Passaro p = new Passaro(); p.peso = 15; p.penas = 100; 6 7 8 9 10 p.comer(); p.voar(); } } Código 2.2.5: Código de exemplo: Classe Principal3 Orientação a Objetos 2.2.7 23 Sobrescrita de método Algumas linguagens OO, como o Java, permitem que um método definido em uma superclasse seja reimplementado pela subclasse. No exemplo dos animais, uma espécie de pássaro pode andar dando saltos, enquanto outra pode andar dando passos de fato. São formas diferentes de ter o mesmo comportamento de andar. Para tanto, o método original e o sobrescrito devem ser definidos com o mesmo nome, tipo de retorno, quantidade e tipos de parâmetros na mesma ordem. Por exemplo, considere as classes do Código 2.2.6. 1 2 3 4 5 6 7 8 9 10 11 class Passaro { void andar(int distancia) { System.out.println("Andei como um Passaro"); } } class Pombo extends Passaro { void andar(int distancia) { System.out.println("Andei como um Pombo " + distancia + " metros"); } void andar(float distancia) { System.out.println("Andei como um Pombo " + distancia + " metros com ponto flutuante!"); } 12 13 14 15 } Código 2.2.6: Código de exemplo: Sobrescrita de métodos Na linha 2, o método andar, com parâmetro int foi definido. Na linha 8, a classe Pombo também define um método andar com parâmetro int. Como Pombo estende Passaro, caso um objeto da classe Pombo seja criado e o método andar seja chamado com parâmetro int, a implementação da subclasse é que será usada. Na linha 12, o método andar foi definido com um parâmetro float, logo ele é identificado como outro método. Por exemplo, a execução do Código 2.2.7, imprimiria as seguintes mensagens na tela: Andei como um Pombo 14 metros Andei como um Pombo 5.0 metros com ponto flutuante! 1 2 3 4 5 6 7 class Principal4 { public static void main(String[] args) { Pombo p = new Pombo(); p.andar(14); p.andar(5.0f); } } Código 2.2.7: Código de exemplo: Principal4 Orientação a Objetos 2.2.8 24 Vinculação Dinâmica de Métodos Outra funcionalidade fundamental que toda linguagem OO deve implementar é a vinculação dinâmica de métodos. É um tipo de polimorfismo onde, ao realizar uma chamada de método, a determinação de qual implementação do método será usada é feita em tempo de execução [Sebesta2011]. Considere, por exemplo, que foram definidos vários tipos diferentes de pássaros, todos estendendo a classe Passaro. Na superclasse existe o método cantar e cada pássaro sobrescreve esse método. Pelo polimorfismo, é possı́vel instanciar um pássaro de cada tipo e guardá-los todos em um mesmo vetor do tipo Passaro. Se esse vetor fosse percorrido, executando o método cantar de cada objeto, não seria a implementação genérica desse método em Passaro que seria chamada, mas sim cada uma das implementações especı́ficas definidas nas várias subclasses. Ou seja, a vinculação de qual implementação de método chamar não é feita em tempo de compilação, ao determinar o tipo da variável que conterá o objeto, mas sim em tempo de execução. O Código 2.2.8 exemplifica essa situação. 1 2 class Passaro { void cantar() { System.out.println("Cantando como um Passaro! piu piu piu..."); } 3 4 5 public static void main(String[] args) { Passaro[] listaPassaros = new Passaro[2]; 6 7 Pombo pb = new Pombo(); Aguia ag = new Aguia(); 8 9 10 listaPassaros[0] = pb; listaPassaros[1] = ag; 11 12 13 for(int i = 0; i<2; i++) listaPassaros[i].cantar(); 14 15 16 17 18 19 20 21 22 23 24 } } class Pombo extends Passaro { void cantar() { System.out.println("Cantando como um Pombo! grururuuuu gururuuu..."); } } class Aguia extends Passaro { void cantar() { System.out.println("Cantando como uma Aguia! piaaaaaa piaaaaaaa..."); } } Código 2.2.8: Código de exemplo: Vários tipos de Passaro Erlang 25 A execução do método main presente na classe Passaro resultaria na seguinte saı́da: Cantando como um Pombo! grururuuuu gururuuu... Cantando como uma Aguia! piaaaaaa piaaaaaaa... Os dois objetos, o do tipo Pombo e o outro do tipo Aguia, foram armazenados no mesmo vetor do tipo Passaro. Em seguida, o método cantar() foi chamado para cada um dos objetos a partir do vetor preenchido. A implementação particular de cada objeto para esse método foi então executada. Isso porque Passaro é superclasse de Pombo e Aguia, herdam o método cantar(), mas reimplementam eles, cada uma com suas próprias necessidades. 2.3 Erlang Em telecomunicações existem sistemas distribuı́dos, de alta performance, larga escala que precisam executar de forma confiável e serem bastante escaláveis. Como descrito em [Armstrong2007] e [Cesarini2009], Erlang procura atender a esse tipo de problema e foi desenvolvido para que: • Os programas rodem mais rápido em computadores de vários núcleos (multicore); • Softwares distribuı́dos pudessem ser desenvolvidos com facilidade e eficiência ; • Sistemas pudessem ser tolerantes a erros de software e falhas de hardware; • O software pudesse ser atualizado sem a necessidade de parar a execução. Erlang é uma linguagem funcional com tipagem dinâmica, os programas são divididos em módulos e frequentemente executam em vários processos. Ela é essencialmente concorrente e, como dito por [Armstrong2007], criou o paradigma de programação orientado a concorrência (POC). Dentre outras coisas, isso significa que ela proı́be o uso de variáveis globais e processos não compartilham estado nem variáveis. Além disso, processos comunicam-se por troca de mensagens e a detecção e correção de falhas é bastante facilitada, como discutido em [Armstrong2003]. Estes processos (chamados “processos Erlang”) são leves, ou seja, a criação e envio de mensagens é relativamente barata em comparação com os processos ou threads usuais do sistema operacional. Essas caracterı́sticas fazem essa linguagem ideal para o desenvolvimento de aplicações distribuı́das que exijam alta escalabilidade e comunicação rápida. Nesta seção será descrito Erlang 26 uma visão geral dos conceitos da linguagem Erlang necessários para a implementação desse trabalho. 2.3.1 Funções Um programa em Erlang é divido em módulos. Cada módulo tem seu próprio arquivo com extensão .erl. Por exemplo, o código calcula.erl: 1 2 3 4 5 6 7 8 9 10 11 12 %% programa de demonstracao 1 -module(calcula). -export([soma/2, dist_pontos/4]). soma(X, Y) -> X + Y. dist_pontos(X1, Y1, X2, Y2) -> A = math:pow(X1 - X2, 2), B = math:pow(Y1 - Y2, 2), C = soma(A, B), math:sqrt(C). Código 2.3.1: Código Exemplo de Erlang 1. A linha 1 é um comentário. 2. A linha 2 indica o nome do módulo, deve ter o mesmo nome do arquivo .erl. 3. A linha 3 lista as funções exportadas (visı́veis a outros módulos). É indicado também o número de parâmetros. 4. A linha 5 é o cabeçalho da função soma/2. 5. A linha 6 é uma única expressão que representa o corpo de soma/2. 6. As linhas 8-12 contêm a definição de uma função para calcular a distância entre dois pontos. 7. A linha 9 contém a chamada de uma função de outro módulo. 8. A linha 11 contém a chamada de uma função no mesmo módulo. Erlang 27 Funções pré-definidas Funções pré-definidas, ou built-in functions (BIF) são funções construı́das internamente no Erlang. Geralmente realizam tarefas impossı́veis de se implementar em Erlang, como conversões de tipos. 2.3.2 Compilação e Execução Para compilar e executar programas Erlang utiliza-se o shell : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 usuario@linux:~/erlang$ erl Erlang R15B01 (erts-5.9.1) [source] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.9.1 (abort with ^G) 1> c(calcula). c(calcula). {ok,calcula} 2> calcula:soma(1, 2). 3 3> A = 1. 1 4> B = 2. 2 5> C = calcula:soma(A, B). 3 6> C. 3 7> Código 2.3.2: Exemplo de Compilação de Código em Erlang 1. A linha 1 inicia o shell do Erlang. 2. A linha 5 compila o módulo de exemplo. 3. Linhas 10 a 13 mostram como atribuir variáveis no shell. 4. A linha 14 chama a função soma e na próxima linha é exibido o seu retorno. 2.3.3 Variáveis Variáveis em Erlang começam, por definição, com letras maiúsculas. O escopo delas é a unidade léxica em que ela aparece. Além disso, elas são imutáveis, ou seja, após receberem um valor ele não pode ser mudado. Para simular re-atribuições das linguagens imperativas é preciso criar novas variáveis, como mostrado no Código 2.3.3. Erlang 1 2 3 4 5 6 28 Em Java int a = int b = a = a + b = b + b = b * 1; 1; 1; 1; a; Em Erlang A = 1, B = 1, A2 = A + 1, B2 = B + 1, B3 = A2 * B2, Código 2.3.3: Atribuindo novos valores a mesma variável 2.3.4 Tipos de Dados Existem três tipos de dados em Erlang: tipos de dados primitivos, tipos de dados compostos e tipos de dados de sintaxe amigável. Mais informações sobre estes e outros aspectos do Erlang podem ser encontrados em [Armstrong2003]. Tipos de dados primitivos Existem 8 tipos de dados primitivos em Erlang, mas aqui são descritos apenas os mais relevantes para este trabalho: • References ou refer^ encias são sı́mbolos globais únicos. Sempre que a BIF (função pré-definida do Erlang) make_ref/0 é chamada, é gerada uma nova refer^ encia e esse sı́mbolo é único para todo programa executado após a inicialização do ambiente de execução do Erlang. • Atoms ou átomos são uma sequência de caracteres (string) única, não numérica e constante. O primeiro caractere deve ser uma letra minúscula e pode ser seguida de caracteres alfanuméricos, sublinhados (_) ou at (@). Opcionalmente, se a string for colocada entre aspas simples, ela poderá conter qualquer caractere. Exemplos de átomos: segunda, terca, ’Janeiro’, ’ tipos’, etc. Tipos de dados compostos Esses tipos de dados são estruturas que contêm elementos. Os elementos podem conter qualquer tipo válido m Erlang. • Tuplas são recipientes para um número fixo de elementos. Os elementos são separados por vı́rgulas e colocados entre chaves. Para criar uma tupla com os elementos E1, E2, ..., En, por exemplo, deve-se escrever: {E1, E2, ..., En}. Essa é uma Java 29 expressão que pode ser atribuı́da a uma variável: Var = {E1, E2}. A ordem dos elementos é fixa, ou seja, um elemento pode ser acessado pela sua posição. Esse acesso é realizado a tempo constante, ou seja, uma tupla se comporta de forma semelhante aos vetores (arrays) nas linguagens imperativas. Porém, tuplas são imutáveis, como todos os outros valores em Erlang. Exemplos concretos de tuplas: {1, 2, 3}, {atomo1, 2.0, 3}. • Listas podem conter um número variável de elementos. Listas também são imutáveis, o que pode parecer um paradoxo. Na realidade, quando se diz isso, significa que existe uma operação eficiente para adicionar novos elementos à lista: ela recebe o elemento e a lista e resulta em uma nova lista com o novo elemento no topo da lista seguido dos elementos da lista original. Diferente das tuplas, o tempo de acesso a um determinado elemento de uma lista é proporcional à posição dele. Cria-se uma lista colocando os elementos separados por vı́rgulas em volta de colchetes. Exemplo: Lista = [elemento, 1, 2]. Tipos de dados de sintaxe amigável Em Erlang é possı́vel trabalhar com tuplas com uma sintaxe que se assemelha a algumas estruturas das linguagens imperativas, que é feito com os registros, ou records. Além disso, internamente strings em Erlang são apenas listas com números correspondentes ao código ASCII dos caracteres exibidas de forma mais amigável. 2.4 Java Java é uma linguagem de programação Orientada a Objetos (OO) que se baseou na linguagem C++ procurando melhorá-la. A Programação OO (POO) foi simplificada e vários recursos confusos do C++ foram removidos. Sendo assim, ela foi projetada para ser fácil de aprender, distribuı́da, robusta, segura, independente de arquitetura, portável, de alta performance, dinâmica e que seus códigos sejam reutilizáveis, como exposto por [Gosling1995], o criador da linguagem. Em Java, um programa pode ser dividido em pacotes, classes e interfaces. Os pacotes são conjuntos de classes relacionadas. As classes encapsulam dados e funcionalidades como uma única unidade. Interfaces são protótipos de classes e definem regras para implementar Java 30 classes. Com esses recursos pode-se dar uma complexa hierarquia ao código, onde cada parte tem uma tarefa bem definida. Assim, cada parte pode ser definida em paralelo por diferentes pessoas e equipes e o código pode ser facilmente reusado [Deitel2010]. Nesta seção serão apresentados brevemente os principais conceitos da linguagem Java dando especial atenção aos conceitos de POO. 2.4.1 Compilação e Execução Um programa em Java é dividido em classes. Cada classes tem seu próprio arquivo com extensão .java. A compilação de cada classe resulta em um arquivo com extensão .class escrito em código intermediário (bytecodes). Esse arquivo pode, então, ser interpretado em uma implementação da Máquina Virtual do Java (JVM). Considere o Código 2.4.1. Em Java, para executar um programa é necessário que a classe tenha um método main e algumas vezes uma classe é criada só para conter esse método, como mostrado nas linhas 20 a 27. Para simplificação ele foi mostrado como um código só, porém deve ser criado um arquivo para cada classe. 1 2 3 4 package cidade; public class Pessoa { private int idade; public int getIdade() { return idade; } public int setIdade(int idade) { this.idade = idade; } public void envelhecer(int anos) { idade = somar(idade, anos); } private void somar(int a, int b) { return a + b; } 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 } package cidade; public class PrincipalPessoa { public static void main(String[] args) { Pessoa p = new Pessoa(); p.setIdade(20); System.out.println("Idade 1: " + p.getIdade()); p.envelhecer(1); System.out.println("Idade 2: " + p.getIdade()); } } Código 2.4.1: Código de exemplo: Pessoa e PrincipalPessoa Java 31 • A linha 1 indica o nome do pacote onde foi definido a classe Pessoa; • A linha 2 indica o nome da classe; • A linha 3 define o campo idade; • As linhas 5 a 16 definem os métodos da classe; • Na linha 21 é definido o método que torna possı́vel executar o programa; • Na linha 22 instancia-se um objeto da classe Pessoa, em seguida é realizada uma chamada de método e então é mostrado o novo valor do campo idade. 2.4.2 Modificadores de Visibilidade Classes, campos e métodos podem conter modificadores de visibilidade em sua declaração. Quando aplicados em classes, podem limitar a visibilidade de dentro e fora do pacote onde foram definidas. Da mesma forma, métodos e campos podem ser isolados de outras classes e até mesmo de classes estendidas. No Código 2.4.1, o campo idade e o método somar, por serem private, não podem ser acessados por outras classes. Isso significa que se na linha 21, método main, tivesse sido feito p.idade seria gerado um erro de compilação. Da mesma forma, se fosse chamado p.somar(20, 1) no método main seria gerado um erro. Já com relação às classes, na definição de Pessoa, se fosse trocado o modificador public por private, uma classe definida em outro pacote (diferente de cidade) não poderia manipular objetos dessa classe. 2.4.3 Construtores Construtores permitem instanciar objetos com parâmetros, permitindo definir valores iniciais para seus campos. São parecidos com métodos, com exceção que seus nomes correspondem ao nome da classe e não possuem tipo de retorno. Considere, por exemplo, o construtor da classe Ponto, no Código 2.4.2. Java 1 2 32 class Ponto { public int x, y, contUso; 3 4 5 public static int origemX, origemY; final public static Ponto origem = new Ponto(0, 0); 6 7 8 public Ponto(int x, int y) { this.x = x; this.y = y; } 9 10 11 12 } Código 2.4.2: Exemplo de Construtor na Classe Ponto Caso seja feito new Ponto(1, 2) para instanciar um objeto da classe Ponto, primeiramente a estrutura para guardar os campos é criada (instanciação comum). Em seguida, o corpo do construtor é executado (linhas 7 e 8). A execução desse corpo é realizada da mesma forma que um método de objeto, ou seja, os campos da classe correspondente podem ser acessados. Sendo assim, no Código 2.4.3, caso não houvesse erro, a linha 4 exibiria os valores “1” para x e “2” para y. Quando não se define um construtor, é implicitamente declarado o chamado construtor padrão (new Ponto()), que não recebe parâmetros e apenas instancia o objeto. Porém, após o usuário definir seu construtor, o padrão deixa de existir, logo, no Código 2.4.3, a linha 3 está correta, mas a linha 6 gerará um erro de compilação. 1 2 3 4 5 6 7 8 class PrincipalPonto { public static void main(String[] args) { Ponto p1 = new Ponto(1, 2); System.out.println("x: " + p1.x + " y: " + p1.y); Ponto p2 = new Ponto(); // erro } } Código 2.4.3: Exemplo de Principal para Construtor na Classe Ponto 2.4.4 Campos static Um campo pode ser definido também como static (estático). Nesse caso, existe exatamente uma instância desse campo, não importando quantas instâncias (mesmo que nenhuma) da classe que o contém existam. Nesse caso, são chamados de variáveis de classe, já que são únicos para a classe. Quando ele não é estático, é chamado variável Java 33 de inst^ ancia, pois sempre que uma nova instância da classe é criada (objeto), uma nova variável é declarada para cada campo declarado na classe ou em qualquer das superclasses. Ainda considerando a classe Ponto, o Código 2.4.4 ilustra o uso dos campos static: 1 2 3 4 5 6 class PrincipalPonto { public static void main(String[] args) { Ponto p1 = new Ponto(1, 2); Ponto p2 = new Ponto(3, 4); Ponto.origemX = 0; System.out.printl("origem x: " + p1.origemX + "; "); 7 8 System.out.print(Ponto.origem.contUso + System.out.print(p1.origem.contUso + ", p1.origem.contUso = 1; System.out.print(p2.origem.contUso + ", System.out.print(Ponto.origem.contUso + 9 10 11 12 13 14 ", "); "); "); " "); } } Código 2.4.4: Exemplo de Principal para Campos Static A saı́da seria: origem x: 0; 0, 0, 1, 1 Nas linhas 4 e 5, a mesma variável é acessada de duas formas diferentes, afinal só existe uma única instância do campo origemX. Nas linhas 8 a 12 é exemplificado como, quando o campo static for um tipo de referência, ele pode ser acessado de diversas formas. Os objetos referenciados por p1.origem, Ponto.origem e p2.origem são os mesmos, como evidenciados pelas saı́das. 2.4.5 Herança de Métodos Em Java, uma classe CSub herda de sua superclasse CSuper todos os métodos que satisfazem às seguintes regras: [Gosling2005] • Os métodos não podem ser privados; • CSuper, onde o método é declarado, deve ter os modificadores public ou protected; • Se a regra acima não for satisfeita, CSuper deve estar no mesmo pacote que CSub e ser declarada com o acesso padrão (sem nenhum modificador de visibilidade). Java 34 2.4.6 Assinaturas e Subassinaturas de Métodos A assinatura de um método é utilizada para identificar um método como único em determinada classe. A assinatura é definida pelo nome e sequência de tipos de parâmetros. No Código 2.4.5, o método mover() das linhas 4 e 5 têm a mesma assinatura, o que é um erro. Já os das linhas 5, 6 e 7 são todos de assinaturas diferentes, pois os tipos de parâmetro e ordem são diferentes, então não há erro, pois o Java permite a sobrecarga de métodos e, nesse caso, é dito que a assinatura de um é subassinatura do outro. 1 2 3 4 class PontoAssinatura { public int x, y; void void void void 5 6 7 8 mover(int dx, int dy) { mover(int dx, int dy) { mover(int dx, float dy) mover(float dx, int dy) x = x + dx *2; y = y + dy/2;} x += dx; y += dy; } // erro, redeclarando mover() {x += dx; y += dy;} {x += dx; y += dy;} } Código 2.4.5: Exemplo de Classe para Assinatura de Métodos 2.4.7 Sobrescrita de Métodos Em Java é possı́vel que uma subclasse “re-implemente” um método de uma de suas superclasses. Pode-se então alterar a visibilidade, os parâmetros que recebe, o tipo de retorno, seu corpo, enfim, seu comportamento como um todo. Mas isso só pode ocorrer se determinadas regras forem satisfeitas. Considere um método de instância mSub() declarado na classe CSub e outro mSuper() declarado na classe CSuper. Diz-se que mSub() sobrescreve mSuper() se: [Gosling2005] • CSub é uma subclasse de CSuper; • a assinatura de mSub() é uma subassinatura de mSuper(); • os métodos se encontram em uma das situações: – mSuper() é public ou protected, ou – mSuper() é declarado com acesso padrão e CSuper() está no mesmo pacote de CSub • mSuper() não é static, pois métodos com esse modificador são imutáveis. Java 35 Para ilustrar o funcionamento da sobrescrita, considere o Código 2.4.6. A classe PontoLento sobrescreve a declaração do método mover da classe Ponto com seu próprio método, o que limita a distância que aquele ponto pode mover a cada chamada do método. Quando o método mover é chamado a partir de um objeto da classe PontoLento, a definição sobrescrita (linha 8) será sempre chamada, mesmo que a referência ao PontoLento seja obtida a partir de uma variável do tipo Ponto. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Ponto { int x, y; void mover(int dx, int dy) { x += dx; y += dy; } } classe PontoLento extends Ponto { int xLimite, yLimite; void mover(int dx, int dy) { super.mover(limit(dx, xLimit), limit(dy, yLimit)); } static int limitar(int d, int limit) { return d > limit ? limit : d < -limit ? -limit : d; } } Código 2.4.6: Exemplo de Sobrescrita 2.4.8 As variáveis this e super Em todo método de objeto a variável this pode ser usada. Ela aponta para o próprio objeto e é geralmente usada em construtores para referenciar explicitamente o campo do objeto ao invés do nome de uma variável declarada no método com o mesmo nome do campo. No código 2.4.2, Seção 2.4.3, ele é usado na classe Ponto. Outra variável semelhante é a super, que também aponta para o próprio objeto, porém com a visão da superclasse. Isso significa que apenas as variáveis e métodos presentes na superclasse estarão visı́veis. Além disso, caso um método tenha sido sobrescrito e seja feita a chamada super.metodoSobrescrito(), a implementação do método original, da superclasse, é que será usada. 2.4.9 Ocultamento de Métodos Considere o exemplo anterior. Caso o método mover da classe PontoLento fosse declarado como static, então o método original, da classe Ponto, seria ocultado. Isso geraria um erro de compilação. Java 36 Alguns Requisitos para Sobrescrita e Ocultamento O modificador de acesso de um método sobrescrito ou oculto deve fornecer ao menos o acesso que o método sobrescrito ou ocultado tem, ou seja, considerando um método mSub (da subclasse) que sobrescreve mSuper (da superclasse), então: • Se mSuper é public, mSub deve também ser public; • Se mSuper é protected, mSub deve ser public ou protected; • Se mSuper possui o acesso padrão (sem modificadores), então mSub não pode ser private. Note que não foi considerado o caso em que mSuper é private. Isso porque nessa situação não haveria sobrescrita. Um método em uma subclasse pode ter a mesma assinatura que um método private em sua superclasse e nesse caso não há nenhuma restrição, pois o método da superclasse não teria nenhuma relação com o de mesma assinatura na subclasse. 2.4.10 Sobrecarga de Métodos Se dois métodos da mesma classes têm o mesmo nome, porém suas assinaturas não são equivalentes de forma a se sobrescreverem, então o nome do método é dito como sendo “sobrecarregado”. Isso pode ocorrer caso os métodos sejam declarados na mesma classe, ou ambos sejam herdados ou até se um for declarado e o outro herdado. [Gosling2005] Métodos são sobrescritos baseados em suas assinaturas. Isso significa que se uma classe declara dois métodos public com o mesmo nome e uma subclasse desta sobrescreve um desses métodos, a subclasse continua a herdar o outro método. 2.4.11 Exemplo: Sobrecarga, Sobrescrita e Ocultamento No exemplo do Código 2.4.7, a classe PontoReal oculta as declarações das variáveis de instância do tipo int x e y (linha 2) e sobrescreve o método mover() (linha 6) da classe Ponto com seu próprio método mover() (linha 12). Ele também sobrecarrega o método mover() com outro método com uma assinatura diferente (parâmetros float, linha 13). Além disso, o campo cor é herdado para a classe PontoReal, porém o campo contUso não, pois é privado. Jaraki 1 2 3 4 5 6 7 8 9 10 class Ponto { int x = 0, y = 0; int cor; private int contUso; void mover(int dx, int dy) { x += dx; y += dy; } } class PontoReal extends Ponto { float x = 0.0f, y = 0.0f; 11 12 13 14 37 void mover(int dx, int dy) { mover((float) dx, (float) dy); } void mover(float dx, float dy) { x += dx; y += dy; } } Código 2.4.7: Exemplo de Sobrecarga, Sobrescrita e ocultamento 2.5 Jaraki O Jaraki é o projeto de um compilador que recebe como entrada um código em Java e gera outro com o mesmo comportamento em Erlang. O código gerado é então processado pelo compilador padrão do Erlang e ligado às bibliotecas do Jaraki para que possam ser executados na máquina virtual do Erlang (Figura 2.14). Java Jaraki Erlang bibliotecas erros random vector ... Figura 2.14: Compilador Jaraki 2.5.1 Análise Léxica e Sintática A análise léxica é realizada pelo analisador gerado pelo LEEX. A análise sintática é realizada pelo analisador gerado pelo YECC. Os arquivos .xrl e .yrl especificam como gerar os analisadores que, uma vez processados, criam os módulos Erlang lexer (análise léxica) e parser (análise sintática) (Figura 2.15). Jaraki 38 lexer .xrl lexer .erl leex parser .yrl parser .erl yecc Figura 2.15: Geração dos analisadores léxico e sintático no Jaraki 2.5.2 Arquitetura Geral fonte .java st 4 erros oo e pckg 1 AST + info ast jaraki (IU) 5 string tokens tokens AST lexer parser 10110 2 core 3 Figura 2.16: Compilador Jaraki: Arquitetura Geral - Parte 1 A análise semântica e geração de código foram inteiramente implementados sem geradores de analisador, com os módulos apresentados na Figura 2.16 e, mais adiante, na Figura 2.17. A seguir, são descritos os passos que o compilador executa: • Primeiramente, em 1, o código fonte em Java é lido do arquivo pelo módulo de “interface com o usuário” jaraki e convertido em uma cadeia de caracteres (string); • Em seguida, em 2, a string é passada para o lexer, que realiza a análise léxica e retorna as palavras (tokens) correspondentes ao fonte; • Em 3, o módulo ast repassa os tokens para o parser, que analisa a estrutura do código e retorna a árvore sintática (AST); • Após isso, em 4, o mesmo módulo ast percorre a AST em busca de erros relativos ao nome do arquivo, classe, atributos e métodos, como no tratamento de pacotes ou orientação a objetos; Jaraki 39 • Além disso, o módulo ast extrai as informações da classe e de seus respectivos atributos e métodos. Em 5, essas informações, bem como a AST são repassadas para o módulo core. 8 core 6 jaraki (IU) java expr info objeto .erl .beam erlang expr gen_erl_code st variáveis erros 7 Figura 2.17: Compilador Jaraki: Arquitetura Geral - Parte 2 • Em 6, o módulo core gerencia a tradução de expressões Erlang contidas nos métodos, bem como a montagem das funções Erlang a partir dos métodos. Para tanto, antes de iniciar as traduções, as informações das classes, extraı́das da AST, são inseridas na Tabela de Sı́mbolos (módulo st). • Em 7, o módulo gen_erl_code é responsável por identificar, dada expressão em Java, como ela se comporta em Erlang. Nesse momento a AST do código Java lido é convertida em uma AST para o código Erlang a ser gerado. • Finalmente, em 8, o core monta o módulo Erlang gerado, e o jaraki gera o código Erlang, que posteriormente é compilado para o arquivo .beam, executável na máquina virtual do Erlang. Capı́tulo 3 Desenvolvimento Este capı́tulo trata dos módulos e bibliotecas desenvolvidos para suportar OO no compilador Jaraki e uma breve introdução do funcionamento deste. 3.1 O Compilador Jaraki O compilador Jaraki recebe como entrada um ou mais arquivos .java e gera um programa de mesmo comportamento em Erlang. Para tanto, ele gera um analisador léxico e sintático e aplica-os no código de entrada para gerar a árvore sintática no formato do Java (jAST ). Em seguida é preciso percorrer todas as jAST geradas para reunir informações de todas as classes. Então, ele compila cada expressão de cada método, percorrendo as subárvores que formam o seus corpos. O resultado dessa etapa é a criação de uma árvore sintática no formato do Erlang, a eAST. Ela é usada para criar um módulo Erlang para cada arquivo Java da entrada. Diversos módulos estão envolvidos nesse processo e nesta seção é descrito como está estruturado o compilador Jaraki, bem como o funcionamento dos seus principais módulos. 3.1.1 Estrutura do Compilador Os principais módulos do compilador são: • lexer.xrl - descrição do analisador léxico a ser gerado pelo leex ; • parser.yrl - descrição do analisador sintático a ser gerado pelo yecc; • jaraki.erl - fornece a interface com o usuário final, chama o analisador léxico e sintático e repassa a árvore sintática para o módulo core.erl; O Compilador Jaraki 41 • ast.erl - monta a árvore sintática com o analisador sintático gerado, monta a estrutura das informações das classes; • st.erl - módulo para criar, destruir e gerenciar a tabela de sı́mbolos, usada na compilação, e o dicionário de variáveis, usado na execução; • gen_erl_code.erl - módulo que contém funções para percorrer, recursivamente, uma subárvore com expressões provenientes do analisador sintático e gerar a subárvore no formato do Erlang (eAST); • core.erl - insere na tabela de sı́mbolos as informações das classes, provenientes do módulo ast.erl e usando funções do módulo st.erl, compila cada método chamando o gen_erl_code.erl para traduzir as expressões e une os resultados para formar o módulo Erlang. 3.1.2 Tabela de Sı́mbolos A Tabela de Sı́mbolos (do inglês, Symbol Table - ST) do Jaraki guarda informações do escopo, classes e variáveis. O escopo define qual classe e método está sendo analisado em dado instante da compilação. Para isso, ao compilar um método, antes de chamar o gen_erl_code, o core atualiza a entrada do escopo com o método que será analisado. As informações das classes são descritas na Seção 3.2. Para as variáveis são guardados seus tipos e a chave é formada pelo escopo e o nome da variável. As principais funções do módulo st.erl são: • put_scope(Escopo) e get_scope() - insere e busca valor do escopo. O escopo tem o formato {NomeClasse, {NomeMetodo, ListaTiposParametros}}; • get_value(Linha, Escopo, NomeVariavel) - busca o valor da variável especificada; • put_value({Escopo, NomeVariavel}, Valor - insere ou atualiza valor da variável especificada. Tabela de Símbolos para Classes 3.2 42 Tabela de Sı́mbolos para Classes Em determinadas fases da análise semântica de uma classe é necessário utilizar as informações de outras classes. Por exemplo, quando um método de um objeto de uma classe C1 utiliza um método de um objeto de outra classe C2, é preciso checar se esse método de fato existe e, caso não exista, gerar um erro de compilação. Sendo assim, antes da análise semântica de uma classe é realizada a análise sintática de todas as outras envolvidas. As informações das classes são extraı́das das árvores sintáticas e então postas na ST. Para cada classe é criada uma entrada na ST. A chave dessa entrada é o próprio nome da classe e o valor é uma tupla com o nome da superclasse, se houver, lista com dados dos campos, lista com dados dos métodos e lista com dados dos construtores. A estrutura de cada elemento dessas listas é mostrada na Tabela 3.1. Lista Estrutura dos elementos ChaveClasse { oo_classes, NomeDaClasse } ValorClasse { NomeSuperClasse, Campos, Metodos, Construtores } Campos {Nome, ValorCampo} ValorCampo {Tipo, Modificadores} Metodos {ChaveMetodo, ValorMetodo} ChaveMetodo {Nome, Parametros} ValorMetodo {TipoDeRetorno, Modificadores} Construtores {Parametros, Visibilidade} Tabela 3.1: Estrutura da Tabela de Sı́mbolos para dados das classes 3.3 Compilando Classes A declaração de classes em Java é feita de acordo com a seguinte sintaxe: <class declaration> ::= “public” “class” “identifier” “{” <class body> “}” <class declaration> ::= “public” “class” “identifier” “extends” “identifier” “{” <class body> “}” O nome da classe é obtido com o primeiro token identifier e é usado para definir o nome do módulo em Erlang. As informações de que a classe é public e o nome da Compilando Classes 43 sua superclasse (segundo identifier), quando existir, são usadas para consulta futura na análise semântica. 3.3.1 Corpo da Classe O corpo da classe é definido pela regra class_body e é composto de declaração de campos, métodos ou construtores. A sintaxe é como segue: <class body> ::= <method declaration> [<class body>] <class body> ::= <constructor declaration> [<class body>] <class body> ::= <f ield declaration> [<class body>] 3.3.2 Objetos As variáveis de referência, no compilador Jaraki, contêm instâncias do tipo reference do Erlang. A reference é usada para formar chaves únicas correspondentes a entradas no dicionário de variáveis. Para cada objeto existe uma chave para buscar pela classe do objeto e uma chave para cada campo. Cada chave pode ser entendida, analogamente, como um endereço de memória e o conteúdo desse endereço como estruturas de dados (tuplas do Erlang) com diversas informações. A Figura 3.1 ilustra essa estrutura para um objeto do tipo Animal, que possui o atributo peso e método printPeso(). referência a um objeto ref#123 Módulos Erlang Dicionário chave animal.beam valor {objt, ref#123} ``Animal´´ {objt_var, ref#123, "peso"} {integer, 0} mov, goto, ... Figura 3.1: Guardando um Objeto do Tipo Animal Os átomos objt e objt_var são utilizados para indicar, dentre as outras chaves, que essas são, respectivamente, chaves para buscar a classe de um objeto e chaves para buscar os campos de um objeto. Os arquivos .beam são os módulos Erlang compilados e contêm a implementação das funções, logo, métodos das classes. Para acessá-lo basta ter o nome do módulo. Então, para realizar a vinculação dinâmica de métodos basta descobrir o nome da classe. Compilando Classes 44 Manipular os Campos Para manipular os campo é preciso da referência do objeto ao qual ela pertence e do nome do campo. Mais detalhes se encontram na Seção 3.3.4. Para cada campo, é guardado o seu tipo e valor. As funções da biblioteca de OO desenvolvida que realizam essas operações são: • get_attribute (ObjectID, VarName): Recebe a referência ao objeto (ObjectID) e o nome da variável (VarName), busca o campo no dicionário e retorna seu valor; • update_attribute (ObjectID, {VarName, VarType, NewVarValue}): Recebe a referência ao objeto, o nome da variável, seu tipo e seu novo valor. Com essas informações, o campo é atualizado no dicionário. Instanciar um Objeto Para instanciar um objeto utiliza-se o operador new e uma chamada a um construtor da classe, como descrito na sintaxe a seguir: <new stmt> ::= “new” “identifier” “(” [<argument list>] “)” Ao encontrar uma expressão desse tipo, o compilador busca todas as variáveis que esse objeto deve ter e gera uma chamada à função new(ClassName, VarsList), da biblioteca OO desenvolvida. Ela recebe o nome da classe do objeto e a lista de variáveis com seus respectivos nomes, tipos e valores iniciais. Para isso, o compilador busca informações da classe do objeto e de todas as suas superclasses (herança de campos, Seção 3.4.1). O Código 3.3.1 demonstra a instanciação de um objeto e, logo em seguida, a Figura 3.2 demonstra os passos que o código gerado em Erlang executa. 2 3 public class Animal { int peso; } 1 Animal a = new Animal(); 1 Código 3.3.1: Exemplo de Instanciação de um Objeto Compilando Classes 45 3 2 1 declara variável "a" chama construtor tipo: "Animal" gera reference 1.1 {a, Escopo} 2.1 {animal, null} insere peso no dicionário atualiza "a" com a reference 3.1 2.2 ref#123 {ref#123, peso} animal {integer, 0} {a, Escopo} {animal, ref#123} dicionário de variáveis Figura 3.2: Execução da Instanciação de um Objeto • Em 1, a variável a é declarada, sendo seu valor inicial nulo (null, em 1.1); • Em 2, o construtor é chamado (que por sua vez chama a função new) para criar a referência única ao novo objeto (em 2.1) e criar a entrada do dicionário para seu campo (peso, em 2.2); • Por fim, em 3 a atribuição proveniente da declaração de a é concluı́da. O construtor retorna a referência do objeto e ela é atribuı́da a a. É importante notar que, devido ao polimorfismo, o tipo da variável pode ser diferente do tipo do objeto, que é definido na instanciação pelo primeiro parâmetro da função oo_lib:new(). Sendo assim, em 2.1 a classe poderia ser Animal, como está, ou uma das subclasses de Animal. Polimorfismo Em uma atribuição onde a variável que está recebendo um novo valor for um “tipo de referência”, o compilador se comporta um pouco diferente dos outros tipos. Ele deve checar se o tipo da variável é igual ao tipo do objeto ou de uma de suas superclasses. Para isso, durante a análise semântica da expressão ele consulta a ST buscando pelas informações das classes envolvidas (superclasses do objeto). Compilando Classes 3.3.3 46 Métodos No que se refere a métodos, neste trabalho é descrito a compilação da chamada de métodos estáticos (static) entre duas classes diferentes e chamada de métodos de objeto. A chamada de métodos de objeto pode ser realizada usando uma variável de referência ou a super (discutida na Seção 3.4.2, página 57). A sintaxe nesses casos é: <method invocation> ::= “identifier” “.” “identifier” “(” [<argument list>] “)” <method invocation> ::= “super” “.” “identifier” “(” [<argument list>] “)” O primeiro identifier pode tanto ser o nome de uma classe quanto uma variável que faz referência a um objeto. Essa variável pode ter o escopo do método que realizou a chamada ou ser um atributo da classe. A associação do identifier com algum desses casos é feita durante a análise semântica. O valor desse identifier (um nome) é comparado com as entradas da ST e o compilador executa os seguintes passos: 1. Se existir uma variável no escopo do método com o mesmo nome do valor desse identifier, então ele é associado a essa variável; 2. Se a checagem anterior falhar, procura-se um atributo da classe com esse nome; 3. Se a checagem anterior falhar, procura-se uma classe com esse nome; 4. Se não for encontrada uma classe com esse nome, gera-se um erro. Métodos de Classe Caso o primeiro identificador seja um nome de classe válido, este método foi chamado como sendo um “método de classe”, ou seja, static. Busca-se então na ST, dentre as informações das classes, se existe um método com o nome correspondente ao segundo identificador. Se for encontrado, a chamada é finalmente realizada como mostrado no Código 3.3.2. Como o Erlang já suporta programação modular, a tradução é direta: o nome da classe se torna o nome do módulo e o do método o da função Erlang. Compilando Classes 1 2 3 Java: 4 5 Erlang: 47 Animal.propriedades(); animal:propriedades(), Código 3.3.2: Tradução de Chamada de Método Estático Métodos de Objeto Caso o primeiro identificador seja uma variável, busca-se na ST seu tipo e valor para checar se de fato é um objeto ou não, gerando erro caso não seja. Sendo um objeto, o tipo corresponde a uma classe. Verifica-se então na ST se existe o método sendo chamado naquela classe. Caso não exista, é gerado um erro. Caso exista, checa-se se a visibilidade é suficiente para ser chamado. Se for, finalmente o método é chamado. A chamada de método, porém, é diferente de um método de classe, pois acrescentase um parâmetro extra na chamada: a referência ao objeto. Essa referência é sempre recebida no primeiro parâmetro da função gerada. Com a referência, durante a execução do método de objeto, é possı́vel manipular os campos do objeto sem precisar consultá-la no dicionário. O Código 3.3.3 contém um exemplo de chamada de método de objeto e a Figura 3.3 demonstra os passos que o código gerado executa. 1 2 3 4 5 1 2 3 4 5 6 public class Passaro{ public void voar() { System.out.println("voando..."); } } public class Principal { public static void main(String[] args) { Passaro p = new Passaro(); p.voar(); } } 2 Código 3.3.3: Tradução de Métodos de Objeto Compilando Classes 48 3 2 1 cria objeto "p" busca classe de "p" busca reference de "p" 1.2 1.1 {p, Escopo} 2.1 {ref#312, peso} {p, Escopo} {passaro, ref#123} 2.2 passaro 3.1 3.2 {p, Escopo} ref#123 {integer, 0} dicionário de variáveis 4 realiza chamada de função dinâmica apply( passaro, voar, [ref#123]) Figura 3.3: Execução de Chamada de Método de Objeto • Primeiramente, cria-se o objeto p, em 1; • Então, em 2 busca-se a classe deste objeto. A chave (2.1) e o nome da classe obtida (2.2) foram definidos em 1.1, na instanciação; • Em seguida, em 3, semelhante ao passo 2, a referência ao objeto é buscada; • Finalmente, em 4, com as informações do nome da função (voar), nome do módulo Erlang (passaro) e referência ao objeto (ref#123), é realizada uma chamada de função dinâmica. É importante ressaltar que a função apply(Modulo, Funcao, Parametros) é usada (chamada de função dinâmica), porque o módulo que contém a função a ser chamada só é descoberto em tempo de execução. Isso é feito para fornecer o suporte à vinculação dinâmica de métodos (Seção 2.2.8). Além disso, o nome do módulo Erlang é conhecido porque ele corresponde ao nome da classe, obtida no passo 2. Compilando Classes 3.3.4 49 Campos Os campos são declarados da mesma forma que as variáveis comuns no corpo de um método, porém é possı́vel acrescentar modificadores aos campos: <f ield declaration> ::= { <modif iers> } <local variable declaration statement> <modif iers> ::= ( static | <visibility modif iers> ) <visibility modif iers> ::= (“public” | “protected” | “private”) A declaração de campos não gera nenhum código diretamente. Durante a análise semântica, ao ler essas informações da árvore, guarda-se elas na ST juntamente com as outras informações das classe. Elas indicarão quais variáveis devem ser criadas na ST para cada objeto da classe no momento da instanciação. Os modificadores são usados apenas para consulta na análise semântica e geração de erros quando necessário. Acesso aos Campos de Objeto O acesso aos campos de objeto pode ser de três formas: a partir de um método do próprio objeto com o operador this, sem o operador e a partir de um método externo ao objeto. A diferença entre eles está na forma como, durante a execução, a referência ao objeto é obtida para manipular seus campos. A sintaxe para uma atribuição que pode alterar um campo é como segue: <element value pair> ::= “identifier” “.” “identifier” “=” <element value> “;” <element value pair> ::= “this” “.” “identifier” “=” <element value> “;” <element value pair> ::= “identifier” “=” <element value> “;” Compilando Classes 1 2 public class Animal { int peso; 3 4 5 6 7 8 1 2 3 4 5 6 50 public void printPeso() { System.out.println("Peso1: " + peso); System.out.println("Peso2: " + this.peso); } } public class Principal { public static void main(String[] args) { Animal a = new Animal(); System.out.println("Peso de fora: " + a.peso); } } Código 3.3.4: Acesso a Campos de Objeto As classes no Código 3.3.4 ilustram os três casos. Na linha 5 da classe Animal, para determinar a qual variável o nome peso deve ser associado o compilador executa os seguintes passos: 1. Procura uma variável declarada no método printPeso() com nome peso 2. Se houver, a variável do método é associada a ela 3. Se não houver, verifica se, na classe Animal, existe um campo com esse nome 4. Se não houver gera um erro e, caso contrário, verifica se a visibilidade é suficiente 5. Se for visı́vel, verifica então se o campo é estático 6. Se for estático, gera erro, se não for, o nome é associado ao campo peso Caso o nome seja associado a um campo do objeto e o método de objeto seja chamado, o código gerado executará os passos como descritos na Figura 3.4. Quando é utilizado o operador this, o procedimento de execução é o mesmo, porém, ao gerar o código, o compilador realiza as checagens a partir do item 3 (verificar se existe o campo na classe). Compilando Classes 51 2 1 cria objeto "a" ref#234 3 realiza chamada de método 4 busca valor do campo peso imprime valor printPeso(ref#234) 3.1 {ref#234, peso} 3.2 Valor dicionário de variáveis Figura 3.4: Acesso a Campos em Método de Objeto • Em 1 é realizada a instanciação; • Em 2 é chamado o método de objeto; • Em 3, já dentro do método, o nome “peso” foi associado ao campo e a referência recebida por parâmetro (em 2, ref#234) é usada para buscar o valor do campo (3.2); • Em 4 é impresso o valor do campo. Outra forma de realizar o acesso a campos é a partir de um método externo, como na linha 4 da classe Principal, Código 3.3.4. Nesse caso, na análise semântica, o compilador deve checar a variável que referencia o objeto e gerar código para obter sua referência: 1. Verificar se a variável é um objeto. Se não for, gerar erro; 2. Se for, checa se, na classe Animal, existe um campo com esse nome 3. Se não houver gera um erro e, caso contrário, verifica se a visibilidade é suficiente 4. Se for visı́vel, verifica então se o campo é estático 5. Se for estático, gera erro, se não for, cria o acesso ao campo. Nesse caso, o código traduzido deverá buscar a referência ao objeto no dicionário de variáveis antes de recuperar o valor do campo, como mostrado na Figura 3.5. Compilando Classes 52 3 2 1 cria objeto "a" 4 busca valor do campo peso busca valor de "a" imprime valor ref#152 2.1 {a, Escopo} 2.2 ref#152 3.1 {ref#152, peso} 3.2 Valor dicionário de variáveis Figura 3.5: Acesso a Campos de Método de Objeto por outra Classe • Em 1 cria-se o objeto, que é armazenado na variável a; • Em 2, a partir de a, busca-se a referência a esse objeto (ref#152, em 2.2); • Em 3, com a referência, busca-se o valor do campo; • Em 4, o valor é impresso. 3.3.5 Construtores Para cada construtor, o compilador gera uma função no módulo resultante com o nome especial de ’__constructor__’. A função, primeiramente, cria uma instância da classe a que pertence e guarda a referência ao objeto criado na variável de nome ObjectID. A partir daı́, as expressões definidas no corpo do construtor são compiladas como um método de objeto comum. Ao final do corpo da função também é acrescido o nome da variável ObjectID, pois assim a função sempre retornará a referência ao objeto criado. A declaração de um construtor é feita seguindo a seguinte sintaxe: <constructor declaration> ::= “public” “identifier” “(” [<parameters list>] “)” <block> O Código 3.3.5 ilustra um exemplo de construtor definido pelo usuário em Java. É importante notar o uso da variável this. Ela indica que a variável peso que receberá o valor é um campo do objeto. Isso é necessário porque quando se tem uma variável em um método com o mesmo nome de um campo o nome é associado à variável. Na Figura 3.6 é detalhada a execução do código gerado. Compilando Classes 53 public class Animal { int peso; 1 2 3 4 5 public Animal(int peso) { this.peso = peso; } 6 7 } Código 3.3.5: Construtor Definido pelo Usuário 3 2 1 recebe parâmetros peso:integer declara objeto e campos 2.1 1.1 ref#312 {peso, Escopo} animal {integer, Valor} busca valor da variável peso 2.2 3.2 {ref#312, peso} 3.1 {peso, Escopo} Valor {integer, 0} dicionário de variáveis 4.1 4 {integer, Valor} {ref#312, peso} atualiza campo peso 5 retorna reference Figura 3.6: Execução de um Construtor Definido pelo Usuário • Em 1.1 o parâmetro recebido (peso) é declarado como variável; • Em 2 cria-se a referência ao novo objeto (ref#312, em 2.1) e declara-se o campo do objeto, no caso apenas peso (2.2); • Em seguida a expressão da atribuição deve ser resolvida e, para isso, em 3 o valor da variável peso é buscado; • Em 4, o valor obtido em 3.1 é atribuı́do ao campo do objeto recém-criado; • Por fim, em 5 o construtor retorna a referência ao objeto criado. Herança 54 Construtor Padrão Se, e somente se, nenhum construtor for definido pelo usuário, é gerado o “construtor padrão”, que não recebe nenhum parâmetro e apenas cria o objeto, inicializando-o como descrito na classe a que ele pertence. O Código 3.3.6 ilustra esse comportamento. 1 2 3 4 -module(animal). ’__constructor__’() -> oo_lib:new(animal, [], [{peso, int, 0}]). Código 3.3.6: Construtor Padrão 3.4 Herança Para suportar a herança, as informações de métodos e campos (membros) da classe, presentes na ST, são acrescidas dos membros visı́veis nas superclasses. Sendo assim, primeiramente ele insere na ST as informações definidas na própria classe. Em seguida, ele percorre as informações recém inseridas para mesclar com a superclasse de cada classe. Desse modo, ficará transparente para o resto do processo de compilação quando for feita uma chamada de método ou acesso a campo, seja ele da própria classe ou de uma superclasse. Para as explicações dadas nessa seção, considere o diagrama da Figura 3.7. Figura 3.7: Diagrama dos Animais Herança 55 Inicialmente, apenas as informações das classes Animal e Passaro são inseridas na ST (Tabela 3.2). Chave Valor {oo_classes, animal} {null, CamposAnimal, MetodosAnimal} CamposAnimal [ {peso, {integer, public}}, {cor, {integer, public}}} ] MetodosAnimal [ {{comer, []}, {void, [public]}}, {{andar, []}, {void, [private]}} ] {oo_classes, passaro} {animal, CamposPassaro, MetodosPassaro} CamposPassaro [ {penas, {integer, public}} ] MetodosPassaro [ {{voa, []}, {void, [public]}} ] Tabela 3.2: Dados dos Campos das Classes Em seguida, o compilador percorre essas informações inseridas e verifica que a classe Passaro tem a superclasse Animal. Sabendo disso, ele busca os dados dos campos e métodos de Animal e adiciona aos dados da classe Passaro todos aqueles que não são private, ficando como mostrado na Tabela 3.3. Chave Valor {oo_classes, passaro} {animal, CamposPassaro, MetodosPassaro} CamposPassaro [ {penas, {integer, public}}, {peso, {integer, public}}, {cor, {integer, public}} ] MetodosPassaro [ {{voa, []}, {void, [public]}}, {{comer, []}, {void, [public]}} ] Tabela 3.3: Dados dos Campos das Classes 3.4.1 Herança de Campos A principal consequência para a geração de código devido à herança de campos está na geração dos construtores. Os construtores de Passaro devem inserir os campos peso, cor e penas no dicionário de variáveis. Como as informações das classes são mescladas com as da sua superclasse antes da geração dos construtores, a instanciação segue os mesmos passos como mostrado na Seção 3.3.2. O Código 3.4.1 apresenta uma implementação da classe Passaro e uma instanciação de um objeto dessa classe. Herança 56 3 4 5 public class Passaro extends Animal { int penas; public void voar(){} public void comer(){} } 1 Passaro p = new Passaro(); 1 2 Código 3.4.1: Instanciando com Campos da Superclasse Mais adiante, a Figura 3.8 demonstra como é feita a execução da instanciação em Erlang. É semelhante à instanciação da Figura 3.2 (página 46), porém, em 2.2, além do campo definido na própria classe, é inserido também os que foram definidos na sua superclasse. 3 2 1 declara variável "p" cria objeto com construtor tipo: "Passaro" insere peso, pena e cor 2.2 2.1 1.1 atualiza "p" com a reference {p, Escopo} ref#312 {passaro, null} passaro {ref#312, peso} => {integer, 0} {ref#312, pena} => {integer, 0} {ref#312, cor} => {integer, 0} 3.1 {p, Escopo} {passaro, ref#312} dicionário de variáveis Figura 3.8: Instanciando com Campos da Superclasse 3.4.2 Herança de Métodos Considere a classe Peregrino. Devido à herança, o compilador acrescenta na entrada da ST dessa classe as informações do método comer(), de Animal e voar(), de Passaro. Porém, a implementação destes métodos estão nas suas classes de origem e não na classe Peregrino. Logo, uma chamada ao método peregrino:comer() no código gerado em Erlang geraria um erro, pois ele não existe no módulo peregrino, mas sim em animal. Para resolver isso, o compilador insere no corpo do módulo peregrino uma nova função, que pode ser entendida como “proxy”, pois ela apenas recebe os parâmetros e repassa-os para a função na superclasse correspondente. Para exemplificar, o Código 3.4.2 demonstra essa situação. É importante notar que a função proxy deve chamar a função correspondente na superclasse imediatamente acima. Ou seja, a função peregrino:comer() chama a Herança 57 função passaro:comer(), que, por sua vez, pode ou não (no caso de sobrescrita) chamar animal:comer(). Sobrescrita de Métodos Para suportar a sobrescrita, o compilador precisa detectar, durante a mesclagem de métodos, que determinado método foi sobrescrito. Para tanto, antes de mesclar cada método, é checado se ele já existe na ST original (antes da mesclagem), nas informações da subclasse. Caso exista, a função proxy não é gerada e compila-se o método normalmente. O método comer(), de Animal é sobrescrito em Passaro e o resultado da compilação é apresentado no Código 3.4.2. 1 2 3 4 1 2 3 4 5 6 7 1 2 3 1 2 3 4 5 1 2 3 4 5 6 Java, Animal - método comer() public void comer() { System.out.println("comendo..."); } Java, Passaro - métodos comer() e voar() public void comer() { System.out.println("comendo..."); } public void voar() { System.out.println("comendo..."); } Erlang, animal - funç~ ao comer() comer(ObjectID) -> io:format("~s~n", ["comendo..."]). Erlang, passaro - funç~ oes comer() e voar() comer(ObjectID) -> ’%% sobrescrito io:format("~s~n", ["comendo como um passaro..."]). voar(ObjectID) -> io:format("~s~n", ["voando..."]). Erlang, peregrino - funç~ oes comer(), voar() e mergulhar() comer(ObjectID) -> passaro:comer(ObjectID). %% herdado voar(ObjectID) -> passaro:voar(ObjectID). %% herdado mergulhar(ObjectID) -> io:format("~s~n", ["mergulhando..."]). Código 3.4.2: Herdando Métodos das Superclasses A Variável super A variável super pode ser utilizada para realizar uma chamada ao método da superclasse. Nesse caso, ao encontrar uma expressão desse tipo, o compilador deve seguir os seguinte passos: Herança 58 1. Buscar na ST o método onde foi encontrada a expressão e verificar se ele possui o modificador (static), gerando erro caso seja. 2. Buscar qual a classe onde foi encontrada a expressão; 3. Verificar se essa classe tem uma superclasse, gerando erro caso contrário; 4. Verificar se a superclasse possui o método e ele é visı́vel, gerando erro caso contrário; 5. Verificar se o método chamado é de classe (static), gerando erro caso seja; 6. Gerar uma chamada de função para a implementação do método na superclasse passando os parâmetros da função e acrescentando a referência do objeto, obtida pela variável ObjectID, como mostrado na Seção 3.3.3. Para exemplificar o uso do super, considere que o método comer(), sobrescrito em Passaro, realizasse uma chamada super.comer(). O código gerado seria como mostrado no Código 3.4.3. 1 2 3 public class Passaro extends Animal { int penas; public void comer() { super.comer(); } 4 5 6 7 8 9 } 1 -module(passaro). 2 3 4 comer(ObjectID) -> animal:comer(). 5 6 7 public void voar(){...} voar(ObjectID) -> ... Código 3.4.3: Exemplo do Uso do Super Capı́tulo 4 Testes Este capı́tulo tem por finalidade apresentar a eficácia e eficiência do compilador Jaraki no que se refere ao aspecto orientado a objetos da linguagem. Para cada teste é apresentado o código original em Java (.java), e os tempos de execução e compilação utilizando o programa da OracleTM e o Jaraki. Os códigos Erlang gerados e as árvores sintáticas para cada teste encontram-se no apêndice. Cada teste foi realizado 10 vezes e o tempo médio é apresentado. Além dos testes apresentados, outros 9 foram realizados para cada caso e seus respectivos códigos estão no apêndice. Ao final do capı́tulo é apresentado um resumo dos tempos de todos os testes executados. 4.1 Metodologia de Teste Aqui são apresentadas as condições nas quais os testes foram realizados, bem como as ferramentas utilizadas para medir os tempos de compilação e execução. 4.1.1 Ambiente de Testes Os testes foram realizados nas seguintes condições: • Sistema Operacional: Linux Mint 12 (lisa), Unix-like, baseado no Ubuntu e Debian; • Kernel Linux 3.0.0-12-generic; • Interface: GNOME 3.2.1 • Memória RAM: 1,6 GB; • Processador dual-core 32bits AMD E-350; Processo de Compilação 60 • Versão do Java: OracleTM 1.7.0_03; • Versão do Erlang: Erlang R15B01. 4.1.2 Medição de Tempo Para a medição dos tempos de compilação e execução foi utilizado a função time, presente na biblioteca padrão fornecida pelo kernel do Linux. A saı́da desta função que indica o tempo total de execução de determinado programa é o “tempo real” (real time), sendo assim apenas este foi considerado nos testes. Para exemplificar, caso desejássemos medir o tempo de compilação do código Principal.java, a linha a seguir seria executada, onde o tempo de execução (ou compilação) foi de 1,095s: rodrigo@rodrigo-PC: $ time javac Principal.java real 0m1.095s user 0m1.196s sys 0m0.124s 4.2 Processo de Compilação Nesta seção é demonstrado o processo de compilação de um .java (Código 4.2.1) utilizando o Jaraki. O compilador primeiramente gera a árvore sintática do código Java, chamada jAST e, após processá-la, cria a árvore sintática do código em Erlang a ser gerado, chamada eAST. Por fim, a partir da eAST, o .erl é gerado. Processo de Compilação 1 2 3 4 5 public class Principal { public static void main(String args[]){ Bola b = new Bola(); b.cor = "azul"; System.out.println(b.cor); b.mostraCor(); 6 7 8 9 61 } } (a) Classe Principal 1 2 3 4 5 6 7 8 9 public class Bola { String cor; float circunferencia; String material; public void mostraCor() { System.out.println("Cor: " + cor); } } (b) Classe Bola Código 4.2.1: Códigos de Exemplo para Processo de Compilação: Cadastro de Bola Para executar o compilador, foi desenvolvido um script (jkc), que inicia o ambiente de execução do Erlang, recebe o nome e localização do código Java e executa o compilador. Sendo assim, os comandos para compilação e execução, bem como suas respectivas saı́das são apresentados na Figura 4.1 rodrigo@myPC:~/jaraki/ex0$ javac Principal.java Bola.java rodrigo@myPC:~/jaraki/ex0$ java Principal azul Cor: azul (a) Java rodrigo@myPC:~/jaraki$ ./jkc ex0/Principal.java ex0/Bola.java rodrigo@myPC:~/git/jaraki$ ./jk mono/ex0/principal.beam azul Cor: azul (b) Erlang Figura 4.1: Compilação e Execução do Cadastro de Bola Instanciação de Objetos 4.3 62 Instanciação de Objetos Nesta seção são apresentados os resultados para a instanciação de objetos. Para cada teste, variou-se a quantidade e tipos dos campos da classe, os nı́veis de hierarquia e os campos das superclasses. Além disso, a instanciação divide-se em duas categorias principais: com construtor padrão e com construtor definido pelo usuário. 4.3.1 Construtor Padrão Instanciação com construtor padrão Código 4.3.1. 1 2 3 4 5 public class Principal { public static void main(String[] args) { Produto p = new Produto(); } } (a) Classe Principal 1 2 3 4 5 public class Produto { String nome; float preco; int codigo; } (b) Classe Produto Código 4.3.1: Teste 1: Instanciação - Java Os tempos para esse teste foram: Compilação Execução Java 1.165ms 2ms Erlang 367ms 10ms Tabela 4.1: Teste 1: Instanciação Manipulação de Campos de Objeto 4.3.2 63 Construtor Definido pelo Usuário Instanciação com construtor definido pelo usuário. Código 4.3.2. 1 2 3 4 5 public class Principal { public static void main(String[] args) { Produto p = new Produto("Sabonete Avon", 1.50f, 1); } } (a) Classe Principal 1 2 3 4 5 public class Produto { String nome; float preco; int codigo; public Produto(String nome, float preco, int codigo) { this.nome = nome; this.preco = preco; this.codigo = codigo; } 6 7 8 9 10 11 } (b) Classe Produto Código 4.3.2: Teste 2: Instanciação - Java Os tempos medidos para esse teste foram: Compilação Execução Java 1.093ms 1ms Erlang 385ms 17ms Tabela 4.2: Teste 2: Instanciação 4.4 Manipulação de Campos de Objeto Para o testes de acesso a campos de objeto, foram consideradas duas situações: manipulação do objeto externamente a ele e internamente (métodos de objeto). 4.4.1 Manipulação Externa O Código 4.4.1 ilustra esse caso. Manipulação de Campos de Objeto 1 2 3 4 5 public class Principal { public static void main(String[] args) { Produto produto1 = new Produto(); Produto produto2 = new Produto(); Produto produto3 = new Produto(); 6 7 8 produto1.nome = "Sabonete Avon"; produto1.preco = 2.5f; produto1.codigo = 1; System.out.println(produto1.codigo + ". " + produto1.nome + " - R$" + produto1.preco); 9 10 11 12 13 produto2.nome = "Detergente LimpaTudo"; produto2.preco = 9.90f; produto2.codigo = 2; System.out.println(produto2.codigo + ". " + produto2.nome + " - R$" + produto2.preco); 14 15 16 17 18 produto3.nome = "Escova de Dentes OralB"; produto3.preco = 6.10f; produto3.codigo = 3; System.out.println(produto3.codigo + ". " + produto3.nome + " - R$" + produto3.preco); 19 20 21 22 23 24 25 64 } } (a) Classe Principal 1 2 3 4 5 public class Produto { String nome; float preco; int codigo; } (b) Classe Produto Código 4.4.1: Teste 3: Manipulação de Campos - Java Os tempos para esse teste foram: Compilação Execução Java 1.186ms 4ms Erlang 417ms 21ms Tabela 4.3: Teste 3: Campos Externo A execução desse teste para o Java e Erlang foram como mostrado no Código 4.4.2. Manipulação de Campos de Objeto 1 2 3 4 65 rodrigo@rodrigo-PC:~\$ java Principal 1. Sabonete Avon - R$2.5 2. Detergente LimpaTudo - R$9.9 3. Escova de Dentes OralB - R$6.1 (a) Java 1 2 3 4 rodrigo@rodrigo-PC:~\$ ./jk principal.beam 1. Sabonete Avon - R$2.500000 2. Detergente LimpaTudo - R$9.900000 3. Escova de Dentes OralB - R$6.100000 (a) Erlang Código 4.4.2: Execução do Teste 3 4.4.2 Manipulação Interna O Código 4.4.3 ilustra esse caso. 1 2 3 4 5 public class Principal { public static void main(String[] args) { Produto produto1 = new Produto("Sabonete Avon", 2.5f, 1); Produto produto2 = new Produto("Detergente LimpaTudo", 9.9f, 2); Produto produto3 = new Produto("Escova de Dentes OralB", 6.1f,3); 6 7 8 System.out.println("-------------Produtos-------------\n"); 9 10 11 produto1.printInfo(); System.out.println("------------\n"); 12 13 produto2.printInfo(); System.out.println("------------\n"); 14 15 16 produto3.printInfo(); System.out.println("------------\n"); 17 18 } } (a) Classe Principal 1 2 3 4 5 6 public class Produto { String nome; float preco; int codigo; public Produto(String nome, float preco, int codigo) { this.nome = nome; this.preco = preco; this.codigo = codigo; } 7 8 9 10 11 public void printInfo() { System.out.println("Codigo: " + codigo); System.out.println("Nome: " + nome); System.out.println("Preco: " + preco); } 12 13 14 15 16 17 } (a) Classe Produto Código 4.4.3: Teste 4: Manipulação de Campos - Java Manipulação de Campos de Objeto 66 Os tempos para esse teste foram: Compilação Execução Java 1.185ms 3ms Erlang 411ms 22ms Tabela 4.4: Teste 4: Campos Interno A execução desse teste para o Java e Erlang foram como mostrado no Código 4.4.4. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 rodrigo@rodrigo-PC:~\$ java Principal -------------Produtos------------Codigo: 1 Nome: Sabonete Avon Preco: 2.5 -----------Codigo: 2 Nome: Detergente LimpaTudo Preco: 9.9 -----------Codigo: 3 Nome: Escova de Dentes OralB Preco: 6.1 ------------ (a) Java 1 2 3 rodrigo@rodrigo-PC:~\$ ./jk principal.beam -------------Produtos------------- 4 5 Codigo: 1 Nome: Sabonete Avon Preco: 2.500000 ------------ 6 7 8 9 10 11 12 13 14 15 16 17 18 Codigo: 2 Nome: Detergente LimpaTudo Preco: 9.900000 -----------Codigo: 3 Nome: Escova de Dentes OralB Preco: 6.100000 ------------ (a) Erlang Código 4.4.4: Execução do Teste 4 Herança 4.5 67 Herança Para os testes de herança, considere o diagrama da Figura 4.2 Figura 4.2: Diagrama dos Funcionários 4.5.1 Herança de Campos, Métodos e Sobrescrita O Código 4.5.1 exemplifica o uso da herança de campos e métodos. O Código 4.5.2 apresenta a implementação das classes Funcionário e Técnico. Herança 1 2 3 4 5 6 7 8 68 public class Principal{ public static void main(String[] args) { Gerente ger = new Gerente("Jucimar Jr", 500000.0f, 100.0f); Administrativo asstAdm = new Administrativo("Pedro Nascimento", 1000.0f, 1, 1, 100.0f); Tecnico asstTecnico = new Tecnico("Jorge Barroso", 100.0f, 2, "eletricista"); ger.fazerHoraExtra(3.4f); ger.exibeDados(); asstAdm.exibeDados(); asstTecnico.exibeDados(); System.out.println("Gerente: " + ger.nome); System.out.println("Assistente Adiministrativo: " + asstAdm.nome + " - mat " + asstAdm.matricula); System.out.println("Assistente Tecnico: " + asstTecnico.nome + " - mat " + asstTecnico.matricula); 9 10 11 12 13 14 15 16 17 18 } } Código 4.5.1: Teste de Herança 1: Classe Principal 1 2 3 4 public class Funcionario { String nome; float salario; public void exibeDados() { System.out.println("======== DADOS ========"); System.out.println("Nome: " + nome); System.out.println("Salario: " + salario); } 5 6 7 8 9 10 } (a) Classe Funcionario 1 2 3 4 // bonus salarial public class Tecnico extends Assistente { String especialidade; public Tecnico(String nome, float salario, int matricula, String especialidade) { this.nome = nome; this.salario = salario; this.matricula = matricula; this.especialidade = especialidade; } 5 6 7 8 9 10 11 12 public void exibeDados() { super.exibeDados(); 13 14 15 16 17 18 System.out.println("Especialidade: " + especialidade); System.out.println("--------------------------------"); } } (b) Classe Assistente Código 4.5.2: Teste 5: Herança 1 - Classes Funcionario e Tecnico Herança 69 Os tempos medidos para esse teste foram: Compilação Execução Java 1.599ms 225ms Erlang 966ms 384ms Tabela 4.5: Teste 5: Herança 1 A execução desse teste para o Java e Erlang foram como mostrado no Código 4.5.3. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 rodrigo@rodrigo-PC:~\$ java Principal ======== DADOS ======== Nome: Jucimar Jr Salario: 500000.0 Salario + bonus: 500340.0 -------------------------------======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.0 Matricula: 1 Turno: diurno -------------------------------======== DADOS ======== Nome: Jorge Barroso Salario: 100.0 Matricula: 2 Especialidade: eletricista -------------------------------Gerente: Jucimar Jr Assistente Adiministrativo: Pedro Nascimento - mat 1 Assistente Tecnico: Jorge Barroso - mat 2 (a) Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 rodrigo@rodrigo-PC:~\$ ./jk principal.beam ======== DADOS ======== Nome: Jucimar Jr Salario: 500000.000000 Salario + bonus: 500340.000000 -------------------------------======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.000000 Matricula: 1 Turno: diurno -------------------------------======== DADOS ======== Nome: Jorge Barroso Salario: 100.000000 Matricula: 2 Especialidade: eletricista -------------------------------Gerente: Jucimar Jr Assistente Adiministrativo: Pedro Nascimento - mat 1 Assistente Tecnico: Jorge Barroso - mat 2 (a) Erlang Código 4.5.3: Execução do Teste 5 Herança 4.5.2 70 Polimorfismo O Código 4.5.4 exemplifica o uso do polimorfismo utilizando as classes já apresentadas nesta seção. 1 2 3 4 5 public class Principal{ public static void main(String[] args) { Assistente assistente1 = new Tecnico("Jorge Barroso", 100.0f, 2, "eletricista"); Administrativo asstAdm = new Administrativo("Pedro Nascimento", 1000.0f, 1, 1, 100.0f); Assistente assistente2 = asstAdm; 6 7 8 Funcionario funcionario1 = new Gerente("Jucimar Jr", 500000.0f, 100.0f); Funcionario funcionario2 = assistente1; Funcionario funcionario3 = assistente2; 9 10 11 12 13 System.out.println("-------------------------------------------------------"); System.out.println("Tipos dos funcionarios"); System.out.print("Funcionario 1: "); funcionario1.printTipo(); System.out.print("Funcionario 2: "); funcionario2.printTipo(); System.out.print("Funcionario 3: "); funcionario3.printTipo(); 14 15 16 17 18 System.out.println("-------------------------------------------------------"); System.out.println("Dados de funcionario3 X dados de assistente2 X dados de asstAdm"); System.out.println("Funcionario 3:"); funcionario3.exibeDados(); System.out.println("Assistente 2:"); assistente2.exibeDados(); System.out.println("Assistente Administrativo:"); asstAdm.exibeDados(); 19 20 21 22 23 24 25 26 27 28 } } Código 4.5.4: Teste de Herança 2: Classe Principal Os tempos medidos para esse teste foram: Compilação Execução Java 1.370ms 180ms Erlang 979ms 389ms Tabela 4.6: Teste 5: Herança 2 Herança 71 A execução desse teste para o Java e Erlang foram como mostrado no Código 4.5.5. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rodrigo@rodrigo-PC:~\$ java Principal ------------------------------------------------------Tipos dos funcionarios Funcionario 1: Sou um Funcionario Gerente Funcionario 2: Sou um Funcionario Assistente Tecnico Funcionario 3: Sou um Funcionario Assistente Administrativo ------------------------------------------------------Dados de funcionario3 X dados de assistente2 X dados de asstAdm Funcionario 3: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.0 Matricula: 1 Turno: diurno Assistente 2: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.0 Matricula: 1 Turno: diurno Assistente Administrativo: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.0 Matricula: 1 Turno: diurno (a) Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rodrigo@rodrigo-PC:~\$ ./jk principal.beam ------------------------------------------------------Tipos dos funcionarios Funcionario 1: Sou um Funcionario Gerente Funcionario 2: Sou um Funcionario Assistente Tecnico Funcionario 3: Sou um Funcionario Assistente Administrativo ------------------------------------------------------Dados de funcionario3 X dados de assistente2 X dados de asstAdm Funcionario 3: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.000000 Matricula: 1 Turno: diurno Assistente 2: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.000000 Matricula: 1 Turno: diurno Assistente Administrativo: ======== DADOS ======== Nome: Pedro Nascimento Salario: 1000.000000 Matricula: 1 Turno: diurno (a) Erlang Código 4.5.5: Execução do Teste 6 Resultado dos Testes 4.6 72 Resultado dos Testes Nesta seção é apresentado o resultado geral dos testes. Java Erlang Diferença Dif. em % Teste 1 Comp. 1.165ms Exec. 2ms 367ms 10ms 798ms 8ms 68% 80% Teste 2 Comp. 1.093ms Exec. 1ms 385ms 17ms 708ms 16ms 65% 94% Teste 3 Comp. 1.186ms Exec. 4ms 417ms 21ms 769ms 17ms 65% 81% Teste 4 Comp. 1.185ms Exec. 3ms 411ms 22ms 774ms 19ms 65% 86% Teste 5 Comp. 1.599ms Exec. 225ms 966ms 384ms 633ms 159ms 40% 41% Teste 6 Comp. 1.370ms Exec. 180ms 979ms 389ms 391ms 209ms 29% 54% Tabela 4.7: Tempos de Todos os Testes Houve uma considerável diferença entre os tempos de compilação e execução entre o compilador da OracleTM e o Jaraki. A diferença entre os tempos em porcentagem indica quanto o menor tempo foi mais rápido que o maior. Sendo assim, o tempo de compilação com o Erlang para os exemplos de herança foi, em média, 35% mais rápido que com o Java e para os outros testes, 65% mais rápido. Já para o tempo de execução, usando a herança o Java foi em média 48% mais rápido e, para os outros testes, 85%. Porém, desconsiderando os zeros a direita dos pontos flutuantes, o Jaraki teve uma eficácia de 100% da saı́da gerada, fornecendo saı́das exatamente iguais quando compilados com o compilador da OracleTM . A diferença no tempo de execução deve-se às diferenças no tratamento de variáveis. Considere, por exemplo, que uma variável seja declarada, um valor é atribuı́do a ela e depois recuperado. Em Java, é alocado memória, ela é preenchida com o valor do usuário e depois acessada. Já em Erlang, para ter o mesmo comportamento, primeiramente é alocado memória, ela é preenchida com um valor padrão e então é copiado o valor do usuário. Em seguida, a variável é acessada, seu valor é copiado para onde ela foi requerida e só então é usada. Sendo assim, a memória é sempre acessada indiretamente, com cópias. Capı́tulo 5 Conclusão O resultado deste trabalho permite que o Jaraki, um compilador da linguagem Java implementado em Erlang, suporte a programação orientada a objetos do Java, logo o objetivo proposto foi atingido. Para tanto, alguns módulos do Jaraki tiveram suas bases modificadas e foi criada uma nova biblioteca que dá suporte à POO no ambiente de execução. Códigos de teste foram criados para verificar a eficácia e eficiência dos recursos implementados. Além disso, o Jaraki foi desenvolvido em conjunto, o que exigiu um controle extra para garantir o sucesso. Para cada programa de teste, o desempenho do Jaraki foi comparado com o do compilador Java da OracleTM . Para tanto, as saı́das dos programas e tempos de compilação e execução foram registrados. Observou-se que o programa original em Java teve o mesmo comportamento que o seu correspondente em Erlang, porém os tempos medidos apresentaram uma considerável divergência. Quando se fez uso de herança, o tempo de compilação do Erlang foi, em média, 35% mais rápido que do Java e para os outros testes, 65% mais rápido. Já para o tempo de execução, usando a herança, o Java foi em média 48% mais rápido e para os outros testes, 85%. A diferença no tempo de compilação deve-se ao fato de o Jaraki não ser um compilador completo da linguagem Java, apenas os principais aspectos foram implementados. Já com relação à execução, o maior gargalo está no tratamento das variáveis, pois o princı́pio fundamental do Erlang são as variáveis imutáveis e a impossibilidade de acesso direto à memória. Trabalhos Futuros 74 No inı́cio do desenvolvimento, todos participavam da programação e, posteriormente, cada um ficou responsável por uma parte. A fase em grupo foi essencial para definir o estilo de programação e permitir que todos conhecessem as bases do compilador. Após a divisão, outro ponto importante foi a comunicação: todos eram mantidos informados do que e como cada um estava implementando a sua parte. Sempre que algo que fosse afetar todas as partes do compilador precisasse ser feito, uma reunião era realizada para compatibilizar as mudanças. Desse modo, os poucos conflitos que ocorreram puderam ser resolvidos sem grandes problemas. 5.1 Trabalhos Futuros Ao definir o escopo deste trabalho, considerou-se as funcionalidades mais usadas e que definem a POO na linguagem Java, porém há outros aspectos relacionados que não foram abordados e em trabalhos futuros podem ser implementados. Dentre esses, há recursos nativos da linguagem (como interface) e bibliotecas padrão (como as de interface gráfica ou criação e gerenciamento de Threads). A interface é um tipo especial de classe abstrata usada para criar um “contrato” de como classes devem ser implementadas. Para utilizar recursos gráficos, uma biblioteca padrão é o SWING. Para manipular Threads (processos leves), existem as classes Thread e Runnable, o pacote concurrent com classes como BlockingQueue e Callable, dentre outros. Além disso, é possı́vel também realizar diversas melhorias na eficiência do código da compilação e no código gerado, porém afetariam diversas partes do compilador e por isso não foram realizadas durante esse trabalho. Referências Bibliográficas [Aho2007] Aho, A. V. (2007). Compiladores: Princı́pios, técnicas e ferramentas. AddisonWesley, segunda edição. [Armstrong2003] Armstrong, J. (2003). Making reliable distributed systems in the presence of software errors. PhD in Computer Science, Royal Institute of Technology, Swedish Institute of Computer Science (SICS), Stockholm. [Armstrong2007] Armstrong, J. (2007). Pragmatic Programming in Erlang. Pragmatic Bookshelf. [Bison2012] Bison (2012). Disponı́vel em: http://www.gnu.org/software/bison/. Acessado em Março de 2012. [Booch2006] Booch (2006). UML Guia do Usuário. Elsevier Editora, segunda edição. [Cesarini2009] Cesarini, F. (2009). Erlang Programming. O’Reilly. [Clojure2012] Clojure (2012). Disponı́vel em: http://clojure.org/. Acessado em Março de 2012. [Deitel2010] Deitel, P. (2010). Java - Como Programar. Prentice Hall, oitava edição. [Efene2012] Efene (2012). Disponı́vel em: http://www.marianoguerra.com.ar/efene/. Acessado em Março de 2012. [Flex2012] Flex (2012). Disponı́vel em: http://flex.sourceforge.net/. Acessado em Março de 2012. [Gosling1995] Gosling, J. (1995). Java: an overview. [Gosling2005] Gosling, J. (2005). The Java Language Specification. Addison-Wesley. REFERÊNCIAS BIBLIOGRÁFICAS 76 [Joxa2012] Joxa (2012). Disponı́vel em: http://joxa.org/. Acessado em Março de 2012. [JRuby2012] JRuby (2012). Disponı́vel em: http://jruby.org/. Acessado em Março de 2012. [Jython2012] Jython (2012). Disponı́vel em: http://www.jython.org/. Acessado em Março de 2012. [Leex2012] Leex (2012). Disponı́vel em: http://www.erlang.org/doc/man/leex.html. Acessado em Março de 2012. [Reia2012] Reia (2012). Disponı́vel em: http://reia-lang.org/. Acessado em Março de 2012. [Sebesta2011] Sebesta, R. W. (2011). Conceitos de Linguagens de Programao. Bookman, nona edição. [Stroustrup1991] Stroustrup, B. (1991). What is “Object-Oriented Programming”? [TIOBE2012] TIOBE (2012). Tiobe programming community index for august 2012”. Disponı́vel em: http://www.tiobe.com/index.php/content/paperinfo/tpci/. Acessado em Agosto de 2012. [Yecc2012] Yecc (2012). Disponı́vel em: http://www.erlang.org/doc/man/leex.html. Acessado em Março de 2012. Apêndice A Conteúdo do CD: Códigos Fonte, PDFs e Vı́deos Aqui é apresentado a estrutura do CD que contém os códigos-fonte do compilador, dos testes realizados, os documentos em PDF da monografia, da apresentação e os vı́deos de demonstração. Para cada teste foi guardado o código original em Java (arquivo .java), a árvore sintática gerada pelo parser (jAST, arquivo .jast), a árvore sintática do código em Erlang gerado (eAST, arquivo .east) e o código gerado (arquivo .erl). A numeração de cada teste é única e foram divididos em classes de teste, a saber: instanciação, campos e herança. A.1 Estrutura Principal e Simbologia O CD contém três pastas na raiz: jaraki, testes e pdf. A primeira contém o código fonte do Jaraki, bem como scripts que funcionam como uma interface com o usuário. Há scripts para compilar o Jaraki, utilizar o Jaraki para compilar códigos em Java e executar os códigos gerados em Erlang. Nas seções deste capı́tulo, a simbologia usada para representar as pastas e arquivos é como exemplificado na Figura A.1. jaraki pastas jk arquivos Figura A.1: Simbologia para Representar Pastas e Arquivos Pasta jaraki A.2 78 Pasta jaraki A pasta jaraki possui a estrutura apresentada na Figura A.2. jaraki ebin include bytecodes dos .erl definição de constantes jaraki_define.hrl src java_src jk jkc Makefile código-fonte do compilador testes utilizados durante o desenvolvimento script para executar .erl gerado script para compilar .java Makefile para compilar compilador Figura A.2: Pasta do Jaraki Pasta jaraki A.2.1 79 Pasta src A pasta src contém os códigos-fonte do compilador Jaraki, ou seja, são módulos Erlang que contêm as funções para a compilação. A Figura A.3 apresenta os arquivos nela contidos. src ast.erl interface com o analisador sintático core.erl interface para converter jAST em eAST file_lib.erl biblioteca para suportar manipulação de arquivos gen_ast.erl funções auxiliares para montar nós da eAST gen_erl_code.erl principal módulo do analisador semântico helpers.erl funções auxiliares diversas jaraki.erl interface com usuário para compilar .java jaraki_exception.erl módulo que trata erros de compilação jaraki_lexer.erl analisador léxico gerado jaraki_lexer.xrl código fonte para o leex gerar o analisador léxico jaraki_parser.erl analisador sintático gerado jaraki_parser.yrl código fonte para o yecc gerar o analisador sintático jaraki_utils.erl funções para auxiliar o desenvolvimento do Jaraki loop.erl biblioteca para suportar laços matrix.erl oo_lib.erl biblioteca para suportar matrizes random_lib.erl biblioteca para suportar o uso da classe Random st.erl funções para gerenciar a tabela de símbolos vector.erl biblioteca para suportar vetores biblioteca para suportar orientação a objetos Figura A.3: Pasta dos Códigos fonte do Jaraki Pasta testes A.3 80 Pasta testes A pasta testes contém os testes realizados, separados por pastas no formato "número - tipo_de_teste", onde número é a numeração única do teste e o tipo_de_teste é instanciação, campos ou herança. A Figura A.4 apresenta esta estrutura. testes 1 - instanciacao 2 - instanciacao 3 - campos 4 - campos 5 - heranca 6 - heranca Figura A.4: Pasta de Testes Pasta testes A.3.1 81 Testes de Instanciação Os arquivos dos testes de instanciação são apresentados na Figura A.5. 1 - instanciacao principal.east principal.erl Principal.jast Principal.java árvore sintática do código gerado código gerado árvore sintática extraída pelo parser código em Java original nome da classe produto.east produto.erl Produto.jast Produto.java 2 - instanciacao principal.east principal.erl Principal.jast Principal.java produto.east produto.erl Produto.jast Produto.java Figura A.5: Arquivos dos Testes de Instanciação Pasta testes A.3.2 82 Testes de Campos Os arquivos dos testes de acesso a campos são apresentados na Figura A.6. 3 - campos principal.east principal.erl Principal.jast Principal.java produto.east produto.erl Produto.jast Produto.java 4 - campos principal.east principal.erl Principal.jast Principal.java produto.east produto.erl Produto.jast Produto.java Figura A.6: Arquivos dos Testes de Campos Pasta testes A.3.3 83 Testes de Herança Os arquivos dos testes de herança são apresentados na Figura A.7. 5 - heranca administrativo.east administrativo.erl Administrativo.jast Administrativo.java assistente.east assistente.erl Assistente.jast Assistente.java funcionario.east funcionario.erl Funcionario.jast Funcionario.java gerente.east gerente.erl Gerente.jast Gerente.java principal.east principal.erl Principal.jast Principal.java tecnico.east tecnico.erl Tecnico.jast Tecnico.java 6 - heranca administrativo.east administrativo.erl Administrativo.jast Administrativo.java assistente.east assistente.erl Assistente.jast Assistente.java funcionario.east funcionario.erl Funcionario.jast Funcionario.java gerente.east gerente.erl Gerente.jast Gerente.java principal.east principal.erl Principal.jast Principal.java tecnico.east tecnico.erl Tecnico.jast Tecnico.java Figura A.7: Arquivos dos Testes de Herança Pasta PDF A.4 84 Pasta PDF Na pasta PDF estão os documentos da monografia e dos slides da defesa. A Figura A.8 ilustra essa estrutura. pdf mono-rodrigo_bernardino.pdf slides-rodrigo_bernardino.pdf monografia apresentação Figura A.8: Documentos da Monografia e Apresentação A.5 Vı́deos Na pasta videos encontram-se os vı́deos usados para a demonstração do uso do compilador.