Um Modelo Semântico para a Linguagem Java Jorge Henrique Cabral Fernandes CIC-UnB Junho de 2004 Introdução Este texto apresenta um modelo semântico inicial para um subconjunto da linguagem Java. A Figura 1 apresenta o modelo geral, onde se destacam três principais módulos: Memória, ClassLoader e Sistema de Arquivos. Memória: é onde são alocados o código e os dados usados para executar um programa Java. A memória é composta por três módulos, chamados Código, Pilha e Heap, detalhados mais adiante. ClassLoader: é o módulo que controla a carga do código das classes na Memória. O ClassLoader inicia carregando a classe especificada pelo usuário na linha de comando, por exemplo, a linha de código abaixo: java Eco Indica ao ClassLoader que a classe Eco deve ser carregada na Memória, e que o método main() da classe Eco deve ser executado. Sistema de arquivos: é composto por vários caminhos (pastas), onde o ClassLoader procura as classes para carregar na memória.
Código. É o módulo da memória que armazena todas as declarações de tipos de dados, principalmente classes. O módulo de código é composto por um conjunto de declarações de classes, cada uma composta por um nome e três categorias de declarações: Métodos, Variáveis de Classe (static) e Variáveis de Instância. Cada declaração de método é composta por um nome, e três declarações: Parâmetros Formais, Variáveis Locais e Seqüência de Comandos. As outras declarações que existem na classe são declarações de Variáveis de Classe e de Variáveis de Instância. Na figura ao lado, cada um dos quatro tipos de declarações de variáveis (parâmetro formal, variável local, variável de classe e variável de instância) é representado por uma cor diferente, para facilitar o relacionamento com a alocação destas variáveis nos módulos de Heap e Pilha, apresentados abaixo. Na figura existem três declarações de classe, uma delas que declara três métodos, duas variáveis de classe e duas variáveis de instância. Um dos métodos de classe apresenta dois parâmetros formais e duas variáveis locais.
O módulo de Código ao lado mostra o estado da memória quando a classe Teste é carregada para execução. Além da carga de Teste na memória, é necessário carregar as classes IO e String, das quais Teste depende, conforme se pode-se ver no código abaixo, onde a dependência está marcada com vermelho.. class Teste { static int var_class1 = 999; static String var_class2 = Digite Número ; String var_instancia1 = null; int var_instancia2 = 333; public static void main( String[] args) { IO.println(); int x; x = IO.readInt(); IO.println(num); No diagrama há a indicação de que o método de nome main declara um parâmetro formal chamado args, do tipo String[], e uma variável local chamada x, do tipo int. Adicionalmente, foram inseridas declarações de variáveis de classe e instância, com nomes sugestivos da categoria à qual pertencem.
Heap. É o módulo da memória onde ficam alocados os objetos. Apenas objetos estão alocados na Heap, e tudo que está alocado na heap é objeto. Os Objetos contém basicamente variáveis alocadas em seu interior, e cada variável possui um nome, um tipo e um valor. Existem duas categorias de objetos alocados na Heap: Objeto_classe e Objeto_Instância. Os Objeto_classe existem na proporção 1:1 para cada declaração de classe na área de código. Se três classes estão alocadas na área de código então três Objeto_classe estarão alocados na área de Heap, cada um deles contendo as variáveis estáticas que correspondem às variáveis estáticas declaradas na (declaração de) classe, apresentadas em laranja. A outra categoria de objetos que existe na heap é a dos chamados Objeto_instância ou simplesmente instâncias. Cada instância está relacionada diretamente a uma classe é da classe x (indicada por uma seta na figura), e várias instâncias podem estar associadas a uma única classe. O diagrama ao lado mostra que existem três objeto_instância diretamente ligados a uma classe, enquanto que o quarto objeto instância está ligado a outra classe. Existem ainda classes para a qual não há nenhuma instância associada. Cada objeto_instância possui um conjunto de variáveis que corresponde exatamente às variáveis de instância declaradas na classe à qual estão associados (apresentadas em azul).
O diagrama ao lado apresenta uma situação na qual existem três objeto_classe (representandos em cor branca) com nomes Teste, String e IO, e quatro objetos_instância (representados em cor cinza), sendo três da classe Teste e um da classe String. Para cada objeto instância da classe Teste é alocada uma cópia individual das variáveis de instância. Por outro lado, cada variável de classe (static) só existe em um único lugar, que é no objeto_classe correspondente. A variável de classe var_class2 contém uma referência para o objeto_instância da classe String. O nome do objeto String contém os caracteres que formam a string Digite Número. O código abaixo contém ainda três expressões que, quando avaliadas, criam as três instância da classe Teste. class Teste { static int var_class1 = 999; static String var_class2 = Digite Número ; String var_instancia1 = null; int var_instancia2 = 333; public static void main(string[] args) { new Teste(); new Teste(); new Teste(); IO.println(); int x; x = IO.readInt(); IO.println(num);
Pilha. A pilha é a área que contém as informações referentes à invocação de métodos. Uma pilha é uma estrutura de dados na qual são empilhados e desempilhados nós, e à qual só se realizam operações de empilhamento e desempilhamento de nós em uma extremidade da pilha, chamada de topo. Para cada invocação (call) de método é empilhado um registro de ativação de método, e cada registro de ativação de método corresponde a uma invocação de método. Quando um método chama outro método, o registro de ativação do método chamado é empilhado sobre o registro de ativação do método chamador. Quando o método chamado encerra sua execução (return) o seu registro de ativação é desempilhado, e este deixa no topo da pilha um valor residual, que corresponde a um valor que pode ser usado pelo método chamado. Como um método pode ser chamado muitas vezes, muitos registros de invocação são empilhados e desempilhados durante a execução de um programa. A figura ao lado mostra um esquema genérico de pilha, mostrando que um registro de invocação contém em seu interior um conjunto de variáveis. Como cada registro de invocação corresponde à execução de um método específico, as variáveis que existem em um registro de ativação correspondem exatamente a uma cópia das variáveis declaradas no método, que são os parâmetros formais e variáveis locais. O nome do registro de ativação indica à ativação de qual método e de qual classe o registro de ativação se refere.
A figura ao lado corresponde à situação da pilha de execução do programa Teste, quando o método println(string) da classe IO estiver sendo executado, após ter sido chamado pelo método main(string[]) da classe Teste. Veja que as variáveis contidas no registro de ativação do método Teste.main() corresponde exatamente a uma cópia dos parâmetros formais e das variáveis locais declaradas no método Teste.main(). Adicionalmente, sabe-se que pelo menos uma variável do tipo String deve fazer parte do registro de ativação do método IO.println(), porque este método recebe um parâmetro de tipo String. Abaixo o código que pode criar a configuração abaixo, onde se destaca em vermelho a linha de código executada no momento em que o método IO.println() é chamado. class Teste { public static void main(string[] args) { IO.println( Digite Número ); int x = 0; x = IO.readInt(); IO.println(num); A figura abaixo mostra um modelo apenas da heap e da pilha, no momento da execução do método IO.println(String);
Exercícios. Ex1. Construa um diagrama descrevendo o modelo semântico similar que corresponde à execução do programa abaixo, no momento em que o método IO.println() está sendo executado. Represente o estado do código, da heap e da pilha. class Impressor { public static void imprime(string s) { IO.println(s); static String s = Olá ; public static void main(string[] a) { Impressor.imprime(s); int i = 90;
Ex2. Construa dois diagramas de semântica, onde o primeiro descreve o modelo de memória completo (código + heap + pilha) que corresponde à execução do programa AloMundo, no momento em que a última linha do método Calculadora.multiplique() está sendo executado. O segundo diagrama deve descrever apenas modelo da heap + pilha, quando a última linha do método imprime está sendo executado. public class AloMundo { public static void main(string [] args) { String saudacao = Mensagens.ola; int qtdvezes = Calculadora.multiplique(5, 3); Impressora.imprime(qtdVezes, saudacao); class Impressora { public static void imprime(int qtd, String msg) { int contador = 0; while (contador < qtd) { IO.println(msg); contador = contador + 1; return; class Calculadora { public static int multiplique(int a, int b) { int resultado = a * b; return resultado; class Mensagens { static String ola= "Ola Pessoal";