Este material foi elaborado por Edmar Welington Oliveira,...
Transcript of Este material foi elaborado por Edmar Welington Oliveira,...
1
2
Direitos: Esta obra foi disponibilizada sob uma Licença Creative Commons Atribuição Uso não-comercial 3.0 Brasil.
Direitos de distribuição e publicação: CAPES/MEC, conforme Parágrafo Único, do Artigo 5º, da Resolução CD/FNDE nº 24 de 04 de Junho de 2008.
Universidade Federal de Juiz de Fora Reitor: Henrique Duque de Miranda Chaves Filho Instituto de Ciências Exatas Diretor: Rubens de Oliveira Departamento de Ciência da Computação Chefe: Custodio Motta Curso de Licenciatura em Computação Coordenação: Fernanda Claudia Alves Campos
Organização Edmar Welington Oliveira
Comissão Editorial
Eduardo Barrére Fernanda Claudia Alves Campos
Revisão Gramatical Hortência Cezar Pinto
Editoração Eletrônica
Eduardo Barrére
Oliveira, Edmar W. Linguagem de Programação II / Edmar Welington Oliveira – 2013. 119 f. : il.
Material Didático — Curso de Licenciatura em Computação da
Universidade Federal de Juiz de Fora, Juiz de Fora, 2012. 1. Educação à Distância. 2. Linguagem de Programação. 3.
Orientação a Objetos. 4. Java. I. Título.
3
Apresentação
Este material foi elaborado por Edmar Welington Oliveira,
Professor Efetivo do Departamento de Ciência da Computação
da Universidade Federal de Juiz de Fora (UFJF). Conforme
proposta para a disciplina, o material didático aborda a
Orientação a Objetos, seus conceitos e aplicações. A
linguagem utilizada para exemplificar o uso da Orientação a
Objetos é Java.
Sucesso e bom aprendizado!!!
Prof. Edmar Welington Oliveira.
4
Iconografia Conheça os diversos ícones utilizados nos materiais didáticos desenvolvidos pelos professores e tutores do curso de Licenciatura em Computação – DCC/UFJF:
Pesquise.
Exercícios.
Material complementar (texto, vídeo, etc.) disponível na Internet.
Leitura Complementar.
Comentário do Autor.
Tome nota.
Conclusão ou síntese de conteúdo.
Fique atento.
5
Sumário
1. Orientação a Objetos .................................................................................... 6 1.1. Orientação a Objetos ............................................................................ 6 1.2. Vantagens da Orientação a Objetos ..................................................... 7 1.3. Surgimento da Orientação a Objetos .................................................... 8 1.3. Abordagem Estruturada ...................................................................... 10 1.4. Limitação da Abordagem Estruturada ................................................. 12 1.5. Paralelo entre Estruturada e a OO ...................................................... 25
2. Objetos......................................................................................................... 26 2.1. Objeto ................................................................................................. 26 2.2. Estado de um Objeto .......................................................................... 27 2.3. Comportamento de um Objeto ............................................................ 28 2.4. Identidade de um Objeto ..................................................................... 28
3. Classes ........................................................................................................ 29 3.1. Paralelo entre Estruturada e a OO ...................................................... 29 3.2. Identificação de Classes ..................................................................... 30 3.3. Representação de Classes em UML .................................................. 31 3.4. Objetos – Instâncias de Classes ......................................................... 32 3.5. Codificação de Classes em Linguagem Java ..................................... 33 3.6. Composição das Classes ................................................................... 34 3.6.1 – Atributos........................................................... ...............................35 3.6.2 – Métodos.................. ........................................................................36 3.7. Instanciação de Objetos de Java ........................................................ 39 3.8. Objetos que Instanciam Objetos ......................................................... 41 3.9. Mensagens ......................................................................................... 42 3.10. Chamadas de Método – Interna e Externa ....................................... 45 3.11. Variável de Classe ............................................................................ 47
4. Construtores ............................................................................................... 55 4.1. Construtores ....................................................................................... 55 4.2. Construtores Sobrecarregados ........................................................... 65 4.2. Referência THIS em Construtores ...................................................... 67
5. Pacotes ........................................................................................................ 72 5.1. Pacotes ............................................................................................... 72 5.2. Importação Implícita e Explícita .......................................................... 82
6. Visibilidade .................................................................................................. 86 6.1. Visibilidade .......................................................................................... 86 6.2. Métodos Públicos e Privados .............................................................. 96
7. Encapsulamento ......................................................................................... 99 7.1. Encapsulamento ................................................................................. 99 7.2. Utilizando Encapsulamento .............................................................. 100 7.3. Métodos de Acesso e Modificação ................................................... 102
Apêndice A - Introdução à Tecnologia Java................................................... 109 A.1. Máquina Virtual................................................................................. 110 A.2. Processo de Compilação e Interpretação ......................................... 112 A.3. Execução de Programas - Exemplo ................................................. 114
Apêndice B - Método Main................................................................................ 116 B.1. Método main ..................................................................................... 117
EADDCC031 – Linguagem de Programação II
6
1. Orientação a Objetos A construção de uma solução computacional consiste no mapeamento do
problema a ser resolvido no mundo real - chamado de Espaço do Problema - em
um modelo de solução no meio computacional - chamado de Espaço de
Soluções. À distância existente entre o mundo real e o modelo abstrato
construído, convencionou-se chamar de gap semântico. Obviamente, quanto
menor o mesmo, mais rapidamente serão construídas soluções para o problema.
Sob esse contexto, percebe-se que o gap semântico representa a área de
atuação da Engenharia de Software. Diversas técnicas e modelos têm sido
propostos ao longo dos anos, para as várias fazes do processo de
desenvolvimento de sistemas, buscando minimizá-lo. A Orientação a Objetos
(OO) é um dos paradigmas existentes para apoiar o desenvolvimento de sistemas
de software que busca fornecer meios para se diminuir o gap semântico. A
computação é parte da sociedade moderna. Em termos de hardware e software, a
evolução é constante, atingindo níveis altos. Pode-se dizer que grande parte dos
problemas do mundo real que precisam ser mapeados em sistemas
computacionais – particularmente, sistemas de softwares – possuem as
tecnologias necessárias. Assim, a preocupação passa a ser como resolver os
problemas da forma mais eficiente possível. Orientação a Objetos está entre as
formas mais eficientes para se desenvolver sistemas computacionais grandes e
complexos. Aprender a raciocinar em OO, bem como utilizar corretamente suas
técnicas e conceitos, não é trivial. Contudo, pode-se afirmar que é essencial para
qualquer profissional relacionado à área de desenvolvimento de sistemas.
1.1. Orientação a Objetos
A Orientação a Objetos (OO) pode ser definida como um paradigma de
desenvolvimento de software que fornece meios para representar os elementos
existentes no espaço do problema. Esses elementos, bem como sua
contrapartida no espaço de programação, são referidos como “objetos”. Dessa
forma, torna-se possível descrever o problema em termos análogos àqueles que
seriam utilizados para descrever o problema em linguagem natural. É claro que,
EADDCC031 – Linguagem de Programação II
7
ainda, existe uma conexão com o computador, visto que este será o responsável
por resolver o problema em última instância. Portanto, em termos práticos, a
proposta da OO é possibilitar representar situações do mundo real nos sistemas
computacionais o mais fielmente possível. Isto é possível, visto que o mundo
pode ser visto como um conjunto de entidades que se relacionam umas com as
outras. Portanto, a OO considera os sistemas computacionais como uma coleção
de entidades que se relacionam.
Orientação a Objetos Paradigma de análise, projeto e programação de sistemas computacionais baseados na composição e interação entre diversas unidades de software chamadas objetos.
Os fundadores da Orientação a Objetos formularam a chamada analogia
biológica, na qual se imaginou como seria um sistema de software que
funcionasse como um “ser vivo” - onde cada “célula” interage com outras, através
do envio de mensagens, para atingir um objetivo comum. De forma mais ampla,
pensou-se em se construir um sistema de software a partir de agentes
autônomos, que interagem entre si. Os princípios da OO estabeleciam, dentre
outros, que qualquer coisa podia ser vista como um agente, que agentes
realizavam tarefas através de requisição a outros agentes e que cada agente
pertencia a um conjunto (uma categorização ou agrupamento de agentes
similares). Essas premissas iniciais nortearam a definição dos vários conceitos
que forma o paradigma OO.
1.2. Vantagens da Orientação a Objetos
Em termos de vantagens, podemos citar que OO reduz o gap semântico.
De fato, se o mundo real é composto por agentes (objetos) que interagem entre si
para a realização de tarefas, o mesmo ocorre com o mundo computacional,
facilitando a compreensão dos problemas e a solução dos mesmos. Além disso,
verifica-se que OO centraliza funções e dados em uma única unidade de
software, o objeto; permite a modularização (divisão do sistema em várias partes
EADDCC031 – Linguagem de Programação II
8
distintas e tão independentes quanto possível); facilita a manutenção dos
sistemas, já que trata da separação de dados e funções, além de promover o
ocultamento de informações, provendo segurança e, igualmente, facilitando a
manutenção. De certa forma, OO permite aos sistemas assumirem um maior grau
de organização e simplicidade – favorecendo, direta e indiretamente, o reuso de
código, as alterações no código e a não propagação de erros em função destas, a
extensão rápida do sistema, o controle da complexidade, etc. OO, também,
permite melhor entendimento do domínio do problema, uma vez que buscamos
enxergar as entidades desse domínio - bem como suas características,
relacionamentos e ações - para criar agentes de software que representam tais
entidades. Sob o aspecto do desenvolvimento em si, podemos obter
independência de implementação até estágios mais avançados. Isso quer dizer
que não começamos um sistema pela codificação, mas pela análise do domínio e
da solução. A codificação, portanto, passa a ser uma das etapas finais e
complementares do desenvolvimento de sistemas. Além disso, podemos dizer
que sistemas OO, quando corretamente desenvolvidos sobre os preceitos do
paradigma, são mais estáveis, de melhor qualidade e mais propensos a
mudanças.
1.3. Surgimento da Orientação a Objetos
No início da década de 70, os computadores eram utilizados somente por
grandes empresas, devido ao seu alto valor comercial. Entretanto, a queda
constante desse valor e a consequente difusão desses equipamentos fizeram
crescer a demanda por software. O interessante é notar que as técnicas de
desenvolvimento de sistemas, até então, eram insuficientes para contornar os
problemas existentes e inerentes ao processo de construção de softwares,
principalmente quando produzidos em larga escala, como era exigido. De fato,
pouco se possuía em termos de técnicas que fossem realmente viáveis e que
favorecessem este processo. Foi nesse contexto inicial que surgiu a programação
estruturada, seguida pelo conceito de desenvolvimento estruturado de sistemas.
Neste, pregava-se que para resolver um problema, o analista deveria analisá-lo
separadamente dos demais (isto é, abstrair dos detalhes) – princípio da
EADDCC031 – Linguagem de Programação II
9
abstração. Além disso, o analista deveria seguir um caminho rigoroso e metódico
para solucionar um problema – princípio da formalidade. Considerava-se, ainda,
que o analista deveria dividir o problema em partes menores, independentes e
com possibilidade de serem simples de se entender e solucionar – princípio da
divisão e conquista. Por fim, o analista deveria organizar os componentes da
solução na forma de uma árvore, com estrutura hierárquica. O sistema seria,
então, entendido e construído nível a nível, onde cada nível acrescentaria mais
detalhes. Essas técnicas tiveram rápida disseminação, sendo amplamente
adotadas. Entretanto, as aplicações tornavam-se cada vez mais complexas,
exigindo grande interação com o usuário, uso de interfaces gráficas, necessidade
crescente de alteração e expansão, interação com outros sistemas (possibilitando
troca de dados), portabilidade para diversas plataformas e sistemas operacionais,
processamento paralelo, etc. As técnicas oferecidas pela programação
estruturada superavam, com certa dificuldade, as complexidades no
desenvolvimento de sistemas com tais características. O problema é que
evolução é permanente, contribuindo para o aumento da complexidade.
Demandavam-se, portanto, novas técnicas para construção de sistemas. Foi
nesse contexto que as atenções se voltaram para a Orientação a Objetos. Devido
às suas características, OO permitia contornar a complexidade crescente das
novas aplicações, reduzindo as dificuldades de desenvolvimento. De fato, as
linguagens do analista e do usuário passaram a ser semelhantes, por se referirem
a objetos do mundo real. Além disso, tornava-se possível utilizar um mesmo
objeto em diferentes sistemas, aumentando a produtividade do processo. De fato,
a metodologia de objetos oferecia uma solução alternativa para o
desenvolvimento de sistemas, e significava uma grande evolução se comparada à
programação estruturada. Era uma evolução, pois apesar de OO utilizar os
mesmos princípios da abordagem estrutura (abstração, hierarquização e
decomposição), ela acrescentava novos e poderosos conceitos: objetos, classes,
herança, etc. Podemos dizer que o surgimento de OO foi, então, motivado pelo
aumento na complexidade dos sistemas, bem como a necessidade de se
organizar o desenvolvimento dos mesmos. De fato, quanto mais complexos os
sistemas se tornam, mais complexo se torna seu desenvolvimento, manutenção e
extensão.
EADDCC031 – Linguagem de Programação II
10
1.3. Abordagem Estruturada
O paradigma estruturado adota uma visão de desenvolvimento baseada
em um modelo de entrada e saída. Os dados são considerados separadamente
das funções que os transformam e a decomposição funcional é usada
intensamente. É clara a distinção entre funções e dados. Funções, a princípio,
são ativas e possuem comportamento, enquanto dados são repositórios passivos
de informação, afetados pelas funções. A figura a seguir representa a relação
entre dados e funções em um sistema estruturado.
Figura 1.1 – Representação do paradigma estruturado
Um sistema desenvolvido usando abordagem estruturada, em geral, é
difícil de ser mantido. A ênfase na construção desses sistemas está na
construção de algoritmos, com aplicação de refinamentos sucessivos. Os
subproblemas são codificados como unidades denominadas procedimentos ou
funções. Além disso, percebe-se um forte uso de decomposição funcional. Os
problemas com esse tipo de desenvolvimento são vários. Programas grandes
tornam-se complexos e difíceis de entender e manter. Existe uma dificuldade
natural em se modelar problemas do mundo real com enfoque em algoritmos. De
fato, é mais fácil e compreensível simular o funcionamento de sistemas
complexos quando o foco está em suas partes constituintes (bem como suas
relações) do que em seus algoritmos. Por exemplo, um veículo é entendido de
forma mais clara em termos de suas peças e das relações entre elas do que em
função dos algoritmos que o fazem funcionar. Além disso, linguagens ditas
procedimentais não oferecem facilidades para a criação de novos tipos de dados
EADDCC031 – Linguagem de Programação II
11
que possam funcionar como os tipos primitivos. Apesar da inviabilidade de sua
utilização na construção de sistemas complexos, o paradigma estruturado, como
dito, propôs características – tais como abstração e modularização – que
nortearam a estruturação de outras técnicas de desenvolvimento. A figura a
seguir ilustra a modularização em um código estruturado.
Figura 1.2 – Algoritmos modularizado
No paradigma estruturado, modularização é uma técnica de desenvolver
algoritmos através de refinamentos sucessivos, podendo-se fazer uso de
módulos. Um módulo é um conjunto de comandos que constitui uma parte de um
algoritmo principal e que possui uma tarefa bem definida e independente em
relação ao resto do algoritmo. Para inserir módulos em um algoritmo, são
utilizados procedimentos e/ou funções. As ações dos procedimentos e funções
estão hierarquicamente subordinadas a um algoritmo principal, geralmente
chamado módulo principal. Além disso, pode haver vários outros procedimentos
ou funções dentro de um procedimento ou uma função. A figura a seguir ilustra a
estrutura interna de um algoritmo estruturado
Figura 1.3 – Exemplo interno de algoritmo modularizado
EADDCC031 – Linguagem de Programação II
12
Apesar de utilizar conceitos e técnicas que ajudam no desenvolvimento de
sistemas grandes e complexos, a abordagem estruturada, em si, não consegue
atender a construção desse tipo de sistema. O que se observa é uma limitação na
possibilidade de utilização desses conceitos. É possível, por exemplo, pensar na
modularização até certo nível, a partir do qual não se pode mais avançar. Assim,
não se consegue atender às demandas dos sistemas. A seção seguinte
apresenta um exemplo que ilustra a limitação da abordagem estruturada.
1.4. Limitação da Abordagem Estruturada
Considere o código da figura 1.4, em linguagem de programação C. Esse
pequeno programa em C apresenta a função main (linha 3), dentro da qual são
declaradas duas variáveis inteiras (linhas 5 e 6), uma estrutura de repetição (linha
8) e uma instrução de retorno (linha 14).
Figura 1.4 – Programa exemplo em C não modularizado
O objetivo desse programa é imprimir o número total de combinações para
um número X de bits. Realizando um teste de mesa, temos os resultados
ilustrados na tabela 1.1. A primeira coluna representa o número de bits; a
segunda, o número total de combinações. Ambas representam o que será
impresso pelo programa. A terceira coluna ilustra as combinações possíveis,
apenas para conferência. Essas combinações não são impressas pelo programa.
EADDCC031 – Linguagem de Programação II
13
Tabela 1.1 – Teste de mesa
Pela tabela 1.1, podemos observar que para 1 bit, temos 2 combinações
possíveis. Para 2 bits, esse número sobre para 4. Para 3 bits, obtemos 8
combinações. Embora o programa seja simples e com poucas linhas de código, é
possível modularizá-lo. A figura a seguir ilustra a tela com o resultado da
execução do programa.
Figura 1.5 – Resultado da execução do programa
A figura 1.6 apresenta a primeira tentativa de modularização do programa
da figura 1.4. Observe que mantivemos a função main (linha 16), embora
ligeiramente alterada. Em relação ao código da figura 1.4, criamos a função
combinar (linha 10) e o procedimento inicializar (linha 5). Além disso, criamos uma
variável global: combinações (linha 3). A função combinar multiplica, por 2, o valor
atual da variável combinacoes e atualiza a própria variável com o resultado, além
de retornar o valor atualizado. O procedimento inicializar inicializa a variável
global combinacoes, com valor 1.
EADDCC031 – Linguagem de Programação II
14
Figura 1.6 – Primeira modularização
Agora, observe o código da função main (linha 16) e note as diferenças
para o código da figura 1.4. Continuamos a declarar a variável inteira i (sem
inicialização), mas deixamos de declarar e inicializar a variável combinacoes. Na
linha 19, especificamos uma chamada ao procedimento inicializar. Dentro da
estrutura de repetição, retiramos a instrução para cálculo das combinações e
mantivemos a instrução para impressão do resultado (linha 23). Observe que
criamos dois módulos novos: a função combinar e o procedimento inicializar.
Cada um possui uma responsabilidade bem definida e realiza uma função bem
específica – exigências do paradigma OO que começamos a observar nesse
código estruturado. Antes, na figura 1.4, tudo (inicialização e cálculo) estava
inserido na função main. Nosso código, então, passa a contar com três módulos:
o módulo principal (main) e os módulos auxiliares (inicializar e combinar). Apesar
deste benefício, há problemas com esse primeiro nível de modularização.
EADDCC031 – Linguagem de Programação II
15
Depender da variável global combinacoes é arquiteturalmente ruim, uma
vez que o programa principal se torna responsável por detalhes de
implementação dos módulos, causando dependência e, consequentemente,
prejudicando a reutilização. Além disso, toda vez que um programa quiser reusar
os módulos (inicializar e/ou combinar), precisará declarar a variável combinacoes
em seu código. O problema é que isso pode causar conflito de variáveis, já que
essa nova variável pode entrar em conflito com uma já existente, forçando
mudanças (em geral, complexas) em nível de código. Precisamos resolver esse
problema, mas sem perder os benefícios da modularização conquistados.
Figura 1.7 – Segunda modularização
Problemas - 1ª Modularização Os módulos são dependentes da variável global combinação.
EADDCC031 – Linguagem de Programação II
16
A figura 1.7 ilustra uma possibilidade de solução. Neste código, inserimos a
variável combinacoes no módulo inicializar (linha 6) e no módulo combinar (linha
12). O objetivo é a remoção da dependência desses módulos com o programa
principal, causa dos problemas de reuso. Ao analisarmos o código da figura 1.7,
percebemos que, de fato, eliminamos a dependência. Entretanto, introduzimos um
problema.
Observe a proposta de solução, apresentada na figura 1.8. Neste código,
voltamos a declarar uma variável global combinacoes (linha 3). A diferença é que
estamos passando a mesma, por parâmetro, para os módulos inicializar e
combinar – utilizando ponteiros.
Figura 1.8 – Terceira modularização
Problemas - 2ª Modularização A variável combinacoes é criada e destruída a cada entrada/saída de cada um dos módulos. Isso impossibilita a continuidade correta do programa, causando um resultado incorreto ao final de sua execução.
EADDCC031 – Linguagem de Programação II
17
A vantagem dessa solução é que a variável combinacoes do programa
principal se torna independente da variável combinacoes dos módulos – isto é, os
nomes podem ser diferentes. Logo, eliminamos o problema de um possível
conflito de variáveis. Observe a figura 1.9 e note que os nomes das variáveis dos
módulos são diferentes entre si e diferentes em relação ao programa principal.
Apesar desse benefício, ainda temos problemas.
De fato, observando o programa 1.9, mesmo alterando os nomes das
variáveis nos módulos, a variável combinacoes ainda precisa ser declarada no
programa principal e, indiretamente, os módulos continuam dependentes deste.
Figura 1.9 – Solução do problema de conflito de nomes
Em termos de modularização estruturada (decomposição funcional), não há
mais o que fazer. Sob a ótica da abordagem estruturada, esgotamos as
possibilidades. Para conseguir continuar com a proposta de modularização, sem
Problemas - 3ª Modularização O programa principal continua precisando declarar e manter a variável combinacoes, o que ainda causa dependência dos módulos.
EADDCC031 – Linguagem de Programação II
18
os problemas observados, precisamos adotar novas estratégias. Uma proposta é
dividir o programa em arquivos diferentes. Em primeiro lugar, criamos um arquivo
de cabeçalho, com a definição dos nomes da função e o do procedimento. A
figura a seguir ilustra o código do mesmo.
Figura 1.10 – Arquivo de cabeçalho
Para esse arquivo, atribuímos o nome “bits.h”. Observe que apenas
definimos as assinaturas da função e do procedimento. Em segundo lugar,
criamos um arquivo para, de fato, inserir os códigos da função e do procedimento.
Nomeamos esse arquivo como “bits.c” (figura 1.11). Note, na linha 1, que estamos
incluindo o arquivo de cabeçalho. A diretiva #include provoca a inclusão de outro
arquivo no programa fonte. Na verdade, o compilador substitui a linha contendo
essa diretiva pelo conteúdo do arquivo indicado. Essa substituição é realizada
antes de o programa ser compilado. Após a inclusão, temos a ligação entre o
arquivo de cabeçalho e o arquivo da figura 1.11. Neste caso em específico, a
linha 1 (figura 1.11) poderia ser retirada e o programa funcionaria sem problemas.
Contudo, é importante mantê-la para sabermos que as funções implementadas no
código da figura 1.11 foram definidas no arquivo de cabeçalho cujo nome é
“bits.h” – e não em outro arquivo que possa existir no nosso programa. Para um
exemplo como o nosso, com apenas 3 arquivos, não ter esse conhecimento
acabando sendo irrelevante. Entretanto, imagine se tivéssemos vários outros
módulos como o arquivo bits.c, além de vários outros arquivos de cabeçalhos.
Muito provavelmente, teríamos dificuldade em, caso necessário, saber qual a
relação entre os vários arquivos. Lembre-se que problemas ocorrem em grandes
e complexos sistemas, e não em pequenos programas. Na linha 3, criamos a
variável global combinacoes. Contudo, esta variável é exclusiva deste módulo
(“bits.c”) e só possui efeito dentro do mesmo. Logo, não temos problemas em
termos de dependência com outros módulos.
EADDCC031 – Linguagem de Programação II
19
Figura 1.11 – Arquivo com função e procedimento
A figura 1.12 ilustra como fica nosso programa principal após as
intervenções anteriores. Na linha 1, incluímos a biblioteca STDIO – como
vínhamos fazendo. Na linha 2, incluímos nosso arquivo de cabeçalho. Assim,
temos a ligação indireta entre o programa principal e o módulo que contém a
função e o procedimento. Isso é essencial, causando erro de compilação se assim
não definirmos. De fato, o programa principal não sabe de onde são a função
(chamada na linha 11) e o procedimento (chamado na linha 7). Neste caso, é
obrigatório realizarmos esta inclusão.
Figura 1.12 – Arquivo principal
Nosso programa está completo e modulado. Temos nosso programa
principal e um módulo. A vantagem é que o módulo é quem controla e mantém o
EADDCC031 – Linguagem de Programação II
20
estado da variável combinacoes. Como dito anteriormente, esta variável é visível
apenas dentro do módulo. O programa principal não possui qualquer
conhecimento da mesma. Isso é bom, pois o módulo pode se reutilizado sem
causar as dependências e eventuais conflitos de nomes. Se outro programador
precisar utilizar esse módulo, basta saber o que o módulo faz: retornar a
quantidade de combinações possíveis para certo número de bits. O programador
não precisa saber como isto é feito, isto é, como é implementado. Logo,
escondemos detalhes de implementação. Em resumo, obtemos modularização e
ocultamento de informações, dois dos principais conceitos utilizados por
paradigmas mais avançados, como OO. Então, ao que parece, conseguimos um
nível alto em termos de modularização, mesmo utilizando a abordagem
estruturada. Como conseguimos para este exemplo, provavelmente
conseguiríamos para programas maiores. Contudo, apesar de obtermos bom
nível de organização do código, além de incorporar caraterísticas essenciais para
um bom desenvolvimento de sistemas, ainda temos problemas. Na verdade,
limitações. Lembre-se que o paradigma estruturado foi “substituído” não pelos
conceitos e técnicas que possuía, mas por suas limitações em atender às
demandas dos novos sistemas. Entre as várias demandas, havia a questão do
processamento paralelo. Embora o programa da figura 1.12, com toda a
modularização, funcione perfeitamente, temos problemas se quisermos realizar
mais de um cálculo de combinações ao mesmo tempo. Embora não seja
impossível, a solução torna o código complexo. Se há complexidade para este
simples programa, imagine como pode ser em programas que, por natureza, já
são complexos.
Foi devido às limitações do paradigma estruturado que outras abordagens
para construção de sistemas, como a Orientação a Objetos (OO), foram
adotadas. Usando os conceitos de OO, vamos remodelar o exemplo anterior e
Problemas - 4ª Modularização O módulo funciona apenas para uma instância de cálculo de combinações, não sendo viável, devido à complexidade, a realização de dois ou mais cálculos em paralelo – isto é, ao mesmo tempo.
EADDCC031 – Linguagem de Programação II
21
mostrar que o problema de calculo em paralelo pode ser resolvido. Observe o
código da figura a seguir.
Figura 1.13 – Arquivo principal
Estamos utilizando linguagem de programação Java, uma das várias
Orientadas a Objetos. Em Java, o conceito de módulo denomina-se classe (do
inglês, class). Basicamente, uma classe corresponde ao bloco de programação (e
desenvolvimento) fundamental do paradigma OO. Não iremos entrar em detalhes
sobre esse conceito ou mesmo a sintaxe do código acima. Isso será visto em
unidades posteriores. O objetivo, agora, é apenas traçar uma comparação com o
programa em C, modularizado. Na linha 1, temos a indicação da nossa classe,
que possui o mesmo nome (bits) do nosso módulo em C. Por padrão OO, apenas
definimos a primeira letra como maiúscula. Note que os códigos da nossa classe
Java e do nosso módulo C são idênticos, salvo alguns termos específicos da
linguagem Java (por exemplo, private e public). Observe que temos a mesma
função combinar (linha 9) e o mesmo procedimento inicializar (linha 5). Note,
ainda, que as implementações de ambos, em nossa classe, são idênticas às do
módulo C. Além dessa classe, precisamos especificar nosso módulo principal.
Observe a figura a seguir.
EADDCC031 – Linguagem de Programação II
22
Figura 1.14 – Classe principal
Essa é a nossa classe Principal, equivalente ao módulo principal do
programa em C. Ao contrário deste, nomeamos a classe como Principal e não
“main”, uma vez que main é uma palavra-chave na linguagem Java. Note, na linha
3, que especificamos a função main. As linhas 8 e 10 são idênticas,
respectivamente, às linhas 6 e 9 do código da figura 1.12. A linha 11 é quase
idêntica à linha 11 da figura 1.12, salvo pela forma de chamada à função
combinar. As demais diferenças nessa linha são pontuais, em virtude das
diferenças de sintaxe entre as linguagens Java e C. Já a linha 7 é, também,
quase idêntica à linha 7 da figura 1.12. A diferença está na forma de chamada ao
procedimento inicializar. A real diferença entre os dois códigos se resume à linha
5 da figura 1.14. Sem discutir os detalhes, estamos criando uma instância
(denominada “i1”) da classe Bits. A partir dessa instância, podemos chamar o
procedimento inicializar (linha 7) e a função combinar (linha 11). A chamada é
realizada especificando-se o nome da instância, seguida de um ponto (.) e do
nome do procedimento (ou função). Então, note que a chamada de ambos está
ocorrendo apenas para a instância i1. Executando o programa, obtemos o
resultado ilustrado na figura a seguir.
Figura 1.15 – Resultado da execução do programa Java
EADDCC031 – Linguagem de Programação II
23
Sabemos que o programa funciona. A questão, então, é possibilitar que
múltiplas instâncias de cálculo executem ao mesmo tempo. Da mesma forma que
criamos a instância i1, podemos criar quantas outras forem necessárias, no
mesmo código. Uma vez criadas, podemos fazê-las chamar tanto a função quanto
o procedimento. Note que essas instâncias são instâncias do módulo “Bits”.
Observe a figura a seguir.
Figura 1.16 – Execução de múltiplas instâncias
Nas linhas 6 e 7, estamos criando mais duas instâncias da classe (módulo)
Bits, i2 e i3. Nas linhas 10 e 11, estamos realizando a chamada ao procedimento
inicializar para ambas. Já nas linhas 17 e 18, estamos imprimindo o resultado da
chamada à função combinar, para ambas as instâncias i2 e i3. A instância i1 foi
mantida. Observe, na figura a seguir, o resultado da execução do código 1.16.
EADDCC031 – Linguagem de Programação II
24
Figura 1.17 – Resultado da execução para múltiplas instâncias
Observe que foi impresso o resultado para todas as instâncias ( i1, i2 e i3).
Para 1 bit, temos duas combinações e o resultado foi impresso para as três
instâncias. Para 2 bits, temos 4 combinações e, igualmente, o resultado foi
impresso para as três instâncias. O mesmo ocorre com o resultado ao se
considerar 3 bits. A figura 1.18 apresenta o mesmo resultado, apenas tentando
mostrá-lo separado para as instâncias, Se necessário, podemos criar outras
instâncias e solicitar a execução das mesmas. Portanto, o uso da Orientação a
Objetos resolve o problema de se possibilitar a execução de múltiplas instâncias.
Não existe, aqui, a limitação imposta pela abordagem estruturada. Percebemos,
então, que OO agrega novas técnicas e conceitos aos que já existem no
paradigma estruturado, permitindo a construção de sistemas maiores e mais
complexos.
Figura 1.18 – Separação do resultado
EADDCC031 – Linguagem de Programação II
25
Para finalizar esta primeira unidade, vamos traçar um rápido paralelo de
como a abordagem estruturada e a orientada a objetos norteiam a forma de
pensar dos programadores. Para a primeira, o importante é, basicamente, a
definição das funções que o sistema deve possuir. Já a segunda se baseia na
definição das entidades do mundo real que terão uma representação em nível de
software. Observe na seção abaixo.
1.5. Paralelo entre Estruturada e a OO
Considere um exemplo simples: de construção de um sistema de
biblioteca. Se cogitarmos a construção desse sistema através da abordagem
estruturada, seremos forçados a pensar nas funções que o mesmo deve ter.
Inevitavelmente, consideraríamos funções do tipo: realizar empréstimo, cobrar
multa, realizar devolução, realizar reserva, pesquisas livro (por título, autor, etc.),
etc. Ao pensarmos em OO, nosso foco muda das funções para as entidades que
devem fazer parte do sistema, incluindo os relacionamentos entre elas. Então,
poderíamos considerar as entidades biblioteca, livro, catálogo, bibliotecário, etc.
Iremos perceber, ao longo desta apostila, que as funções necessárias para a
execução do programa não estão “soltas” no código, mas associadas a entidades
especificas. Isso quer dizer que cada entidade que faz parte do sistema possui
suas responsabilidades (funções) muito bem definidas. As funções existem, mas,
em OO, elas estão ligadas a alguma entidade (classe). Veremos que isso facilita,
em muito, o desenvolvimento de sistemas computacionais, principalmente
aqueles considerados grandes e complexos. A figura a seguir ilustra, para nosso
exemplo, a diferença, entre OO e estruturado.
Figura 1.19 – Separação do resultado
EADDCC031 – Linguagem de Programação II
26
2. Objetos A Orientação a Objetos corresponde a um paradigma de desenvolvimento
de sistemas em que se procura representar, computacionalmente, cenários do
mundo real, visando à solução de um problema. Em outras palavras, propõe-se
que entidades do mundo real, bem como suas relações, sejam
computacionalmente representadas. Podemos dizer, ainda, que a OO permite aos
desenvolvedores raciocinar e solucionar problemas em termos de objetos, os
quais estão diretamente associados às coisas reais. Como resultado deste
mapeamento natural, podemos os concentrar nos objetos que compõem o
sistema, em vez de tentarmos vislumbra o sistema como um conjunto de
procedimentos (funções) e dados.
2.1. Objeto
Um objeto, em Orientação a Objetos, é uma unidade de software utilizada
para representar, dentro do contexto computacional, um elemento do mundo real.
Para explicarmos com mais detalhes, considere que nos seja proposto o
desenvolvimento de um sistema de matrículas escolares, com objetivo de
oferecer suporte ao processo de matrícula de alunos em um dado curso. Ao
analisar o mundo real, verificamos que alunos são matriculados em disciplinas, as
quais são ministradas por certos professores. Inicialmente, percebemos a
existência de três entidades: aluno, disciplina e professor. Verificamos que essas
entidades se relacionam da seguinte forma: aluno e professor se relacionam com
disciplina. Além disso, aluno se relaciona com professor, indiretamente (através
da entidade disciplina). Como essas são as nossas entidades principais no
cenário real, elas são fortes candidatas a se tornarem objetos do sistema.
Convêm ressaltarmos, entretanto, que desenvolver sistemas não é tarefa simples,
principalmente quando a complexidade envolvida é alta. Portanto, a definição dos
objetos do sistema demanda experiência e análise cuidadosa. De fato, essa é
uma tarefa em que muitos desenvolvedores, principalmente os iniciantes, falham
– comprometendo a realização de todo um projeto. É comum, ao se pensar em
objetos, considerarmos o seguinte:
EADDCC031 – Linguagem de Programação II
27
• Um objeto é feito de material tangível
• Um objeto é algo que pode ser identificável
• Um objeto possui características
• Um objeto pode realizar ações
A primeira consideração parece ser bastante restritiva. Uma conta
bancária, por exemplo, é uma entidade que existe dentro do contexto de um
banco, assim como as entidades “cliente” e “gerente”. Portanto, ela poderia ser
representada como um objeto do sistema. Acontece que conta bancária não é
algo feito de material tangível. Logo, objetos não são representações apenas de
coisas tangíveis, como uma pessoa ou um carro. Podemos dizer que objetos
podem representar entidades do mundo real que sejam tanto concretas quanto
abstratas. Exemplos de objetos concretos: pessoa, veículo, mamífero, etc.
Exemplos de objetos abstratos: pedido de compra, conta bancária, solicitação de
venda, pedido de reserva de filme, locação de CD, etc. Já para as demais
considerações, podemos dizer que parecem intuitivas. De fato, objetos podem (e
devem) ser (univocamente) identificáveis dentro de um sistema – assim como
deve ser possível, por exemplo, identificar um aluno dentre um conjunto de alunos
em um cenário real. Sobre o fato de objetos executarem ações, basta analisarmos
o mundo real: alunos, por exemplo, podem se matricular em uma disciplina,
realizar um trabalho escolar, assistir a uma aula, etc. Logo, entidades reais
realizam ações. Da mesma forma, ações podem ser executadas por objetos.
Pode-se sacar dinheiro de uma conta bancária – modelada como um objeto em
um sistema computacional bancário. Podemos, com base nessas considerações,
afirmar que objetos, em OO, possuem as seguintes características:
• Objetos possuem estado
• Objetos possuem comportamento
• Objetos possuem identidade
2.2. Estado de um Objeto
Por estado, se entende os valores assumidos pelas propriedades de um
objeto em um dado momento, as quais podem mudar ao longo da vida do objeto.
Em OO, tais propriedades são denominadas atributos. Um objeto conta bancária
EADDCC031 – Linguagem de Programação II
28
pode possuir saldo como característica. Logo, o valor do saldo em um dado
momento determina o estado desse objeto. Assim como no mundo real, o valor
pode ser alterado diversas vezes (para mais ou para menos), enquanto esta conta
existir.
2.3. Comportamento de um Objeto
Por comportamento, se entende as ações que um objeto pode executar.
Em OO, chamamos essas ações de métodos. Um objeto conta bancária pode, por
exemplo, executar ações do tipo: sacar um valor X, depositar um valor Y, imprimir
o nome do dono da conta, imprimir o saldo atual, imprimir o tipo de conta
(corrente, poupança, salário), etc.
2.4. Identidade de um Objeto
Por identidade, se entende a propriedade segundo a qual um objeto pode
ser identificado. Todos os objetos possuem existência própria, ou seja, são
distintos (ainda que seus estados e comportamentos sejam iguais).
EADDCC031 – Linguagem de Programação II
29
3. Classes
A palavra classe vem da taxonomia da biologia. Todos os seres vivos de
uma mesma classe biológica possuem uma série de características e
comportamentos comuns. Da mesma forma, uma classe em Orientação a Objetos
representa um conjunto de objetos com características e comportamentos
comuns. Classes são elementos fundamentais na composição de softwares OO.
Podemos entendê-las como descrições genéricas ou coletivas de entidades do
mundo real. A definição das classes de um sistema deve procurar inspiração nas
entidades do mundo real, visando representá-las computacionalmente. Ao
contrário dos objetos, que representam entidades individualizadas, podemos dizer
que classes são representações de um coletivo de entidades semelhantes.
3.1. Paralelo entre Estruturada e a OO
É comum encontrarmos no mundo real diferentes objetos desempenhando
um mesmo papel. Considere duas cadeiras. Apesar de serem objetos diferentes,
ambas compartilham mesma estrutura e o mesmo comportamento. Se pensarmos
em dois cachorros, observaremos que, da mesma forma, ambos compartilham
mesma estrutura e comportamento. Para quaisquer outras entidades reais, o
mesmo será verificado. Assim, podemos sempre definir um modelo genérico que
descreva a estrutura de objetos de um mesmo tipo. Em Orientação a Objetos,
este modelo é denominado classe. Uma classe é, portanto, uma estrutura que
descreve um conjunto de objetos com as mesmas características (atributos),
comportamentos (métodos) e relacionamentos com outros objetos. É importante
ressaltar que a ênfase do desenvolvimento OO está na especificação de classes,
e não de objetos, como se poderia imaginar pelo nome. Modelamos o mundo real,
em OO, através de um conjunto de classes que se relacionam entre si. Perceba
que uma classe não é um conjunto de objetos, mas a representação da estrutura
de um conjunto de objetos. A questão é que essa estrutura é idêntica para todo e
qualquer objeto de uma classe. Em outras palavras, objetos podem ser
categorizados e uma classe descreve – de maneira abstrata – todos os objetos de
EADDCC031 – Linguagem de Programação II
30
um tipo particular. Imaginando, por exemplo, os alunos envolvidos em um sistema
acadêmico, cada aluno é uma entidade individualizada que, dependendo do
contexto do sistema, seria representada por um objeto. Observando-se esses
objetos e comparando-os, pode-se constatar que o conjunto de suas
características (nome, sexo, idade) e de seus comportamentos é análogo -
embora, obviamente, os valores dos atributos sejam diferentes. A partir da
observação de características e comportamentos comuns a um conjunto de
entidades similares, é possível estabelecermos um modelo genérico para este
coletivo, contento os atributos e comportamentos comuns ao mesmo.
3.2. Identificação de Classes
Não existe um conjunto de conceitos do mundo real que sempre serão
definidos como classes ao construirmos um sistema computacional OO. Cabe ao
desenvolvedor analisar o domínio do problema, as necessidades do sistema a ser
desenvolvido e, então, especificar as classes que o mesmo deve possuir. Logo
abaixo, apresentamos uma lista – que embora não exaustiva – descreve alguns
exemplos de classes. Classes podem ser:
• Entidades Externas: outros sistemas, dispositivos, pessoas, etc.
• Coisas: relatórios, sinais, displays, etc.
• Ocorrências ou eventos: vendas, voos de avião, locação, empréstimo, etc.
• Unidades organizacionais: grupos, times, departamentos, divisões, etc.
• Estruturas: sensores, veículos, etc.
• Locais: locais de carga, locais de entrega, etc.
Como podemos observar pela lista acima, praticamente tudo pode ser
definido como classe. Essa lista é apenas um exemplo de categorias a partir das
quais classes podem ser definidas. A definição das classes necessárias para a
realização de todas as funcionalidades de um sistema envolve um processo
detalhado de estudo e análise do domínio do problema, dos requisitos
(propriedade ou comportamento que o sistema deve atender) do sistema e das
possibilidades de separação dos dados e processos.
EADDCC031 – Linguagem de Programação II
31
3.3. Representação de Classes em UML
A UML (Linguagem de Modelagem Unificada) corresponde a um padrão
para a representação de modelagens utilizadas no desenvolvimento orientado a
objetos. A notação para classes em UML é um retângulo com três
compartimentos, nos quais são representados: identificação da classe, atributos
(características) e métodos (comportamento). A identificação da classe é um
nome para a mesma. É recomendável, sempre que possível, nomear classes de
modo que seja fácil compreender seu significado. Habitualmente, escrevemos o
nome como um substantivo no singular e com a 1ª letra maiúscula. O segundo
compartimento é destinado à especificação das características (atributos). O
terceiro compartimento declara os métodos que a classe possui. A figura a seguir
ilustra a representação genérica UML para uma classe e um exemplo de classe.
Figura 3.1 – Representação de classes em UML
Observe que para a identificação da classe, atribuímos o nome Carro. A
palavra Carro nos oferece uma ideia clara do que a classe representa. Como
atributos, definimos: modelo, placa e potencia. Isso quer dizer que todos os carros
desta classe possuem estes atributos. Como método, apenas o imprimeDados foi
definido. Da mesma forma, todos os carros desta classe possuem o mesmo. Os
símbolos de “+” e “-“ que aparecem antes dos atributos e dos métodos possuem
significado: são os chamados modificadores de acesso - assunto a ser discutido
em unidades posteriores. Os termos String e int – definidos para os atributos –
definem o tipo de dado de cada atributo. Já o termo void determina o tipo de
retorno para o método da classe. A representação de classes para sistemas OO
segue o modelo UML representado na figura 3.1. A especificação e representação
das classes que compõem um sistema OO forma o modelo de classes (diagrama
EADDCC031 – Linguagem de Programação II
32
de classes). A título de exemplificação, a figura a seguir apresenta um diagrama
de classes.
Figura 3.2 – Exemplo de diagrama de classes
Como podemos verificar pela figura acima, além de classes, um diagrama
de classes também especifica os relacionamentos entre as mesmas. Esta
estrutura, como um todo, é parte essencial para o desenvolvimento de qualquer
sistema OO.
3.4. Objetos – Instâncias de Classes
Considerando o mundo real, podemos citar vários exemplos de classes:
funcionários, pessoas, alunos, etc.– cada uma definindo suas próprias
características e comportamentos. Uma classe, portanto, captura a estrutura e
EADDCC031 – Linguagem de Programação II
33
comportamento comum a todos os objetos que ela descreve. Representa um
gabarito para objetos, especificando a forma segundo a qual os mesmos estão
estruturados internamente. Em resumo, uma classe representa um molde para a
criação de objetos, determinando que atributos e métodos os objetos criados a
partir dela possuirão. Ao processo de criação de objetos, dá-se o nome de
instanciação. Portanto, objetos são ditos serem instâncias de classes. A figura a
seguir apresenta um exemplo de classe e a instanciação de objetos a partir da
mesma.
Figura 3.3 – Classe e instâncias
Na figura acima, podemos observar a especificação de uma classe -
Empregado - e de duas instâncias (objetos) da mesma: Empregado A e
Empregado B. A classe Empregado apenas especifica os atributos e métodos. As
instâncias criadas, como ilustrado, possuem os mesmos atributos e métodos, mas
especificando valores distintos para os mesmos. Se outro objeto fosse instanciado
(criado), o mesmo teria os mesmos atributos e métodos especificados em
Empregado. Portanto, como havíamos dito, uma classe responde a um molde
para a criação de objetos.
3.5. Codificação de Classes em Linguagem Java
Para representação de classes, utilizamos a linguagem de modelagem
UML. Já a codificação da mesma pode ser feita por qualquer linguagem de
EADDCC031 – Linguagem de Programação II
34
programação OO. A figura a seguir apresenta a representação UML da classe
Pessoa e seu respectivo código em linguagem de programação Java.
Figura 3.4 – Classe em UML e respectivo código Java
A declaração de classe é feita na linha 1. A palavra-chave public é um
modificador de acesso. Cada declaração de classe contém a palavra-chave class,
seguida, imediatamente, do nome da classe. O corpo da classe é delimitado pelas
chaves de abertura e fechamento (linhas 1 e 12). As linhas 3, 4 e 5 descrevem os
atributos, formados por um modificador de acesso, seu tipo e do nome do atributo.
No nosso exemplo, todos os atributos foram definidos como sendo do tipo String e
com modificador de acesso private. Assim como classes, atributos devem ser
nomeados com substantivos que deixem claro o sentido (significado) do mesmo.
Em seguida, na linha 7, declaramos o método definido na classe Pessoa. Da
mesma forma que os atributos, métodos são definidos por um modificador de
acesso, seu tipo (de retorno) e nome. Para nosso exemplo, o método foi definido
como sendo do tipo String e com modificador public. Por representarem ações,
métodos, habitualmente, são nomeados com verbos. Após o nome do método, há
um abre e fecha parênteses - dentro dos quais podemos especificar valores a
serem recebidos pelo mesmo.
3.6. Composição das Classes
Vimos que uma classe é composta por identificação, atributos e métodos.
Contudo, nem toda classe possui atributos ou métodos. Dependendo da
necessidade, podemos especificar classes que possuam apenas atributos, ou
EADDCC031 – Linguagem de Programação II
35
apenas métodos. Embora pareça não fazer sentido, em casos muito específicos
classes podem ser especificadas sem ambos, visando futura extensão de código.
Atributos armazenam dados que correspondem às características de objetos.
Métodos implementam o comportamento desses objetos. Há, ainda, um terceiro
elemento, chamado de construtor, o qual é utilizado para que cada objeto seja
adequadamente configurado ao ser criado (instanciado).
3.6.1 - Atributos
Para que possamos entender conceitos posteriores, precisamos detalhar
como os valores dos atributos de objetos são gerenciados durante a execução de
um sistema OO. Basicamente, a execução de um sistema OO implica na criação
de vários objetos, cada um possuindo valores específicos para seus atributos. Ao
instanciarmos um objeto, um espaço de memória é usado para o armazenamento
tanto do objeto quanto os valores de seus atributos. A figura a seguir ilustra essa
situação.
Figura 3.5 – Ilustração do armazenamento de atributos
Para a figura acima, estamos considerando um objeto da classe Pessoa,
ilustrada na figura 3.4. A figura acima é ilustrativa, visando prover uma visão
minimalista (em detalhes) de como objetos e atributos são armazenados. Observe
que cada atributo possui um espaço de memória específico, onde seus valores
são armazenados. Dizemos que os atributos são, em código, representados como
variáveis em uma declaração de classe. Estas são comumente chamadas de
atributos. Quando cada objeto de uma classe mantém sua própria cópia de um
atributo, como vimos anteriormente, o atributo que representa o atributo também é
EADDCC031 – Linguagem de Programação II
36
conhecido como uma variável de instância – ou seja, os valores são específicos a
uma única instância (objeto). Esse é um conceito importante, pois veremos, mais
adiante em nosso estudo, que também temos as chamadas variáveis de classes.
Por agora, entenda que cada objeto, uma vez criado, terá um espeço para cada
atributo declarado em sua classe. E a razão para que cada objeto tenha esses
atributos específicos é simples: os valores podem ser alterados, dependendo do
sistema, ao longo da vida do objeto. Considere uma classe Conta, com um
atributo saldo. Agora, considere dois objetos dessa classe, denominados conta1 e
conta2. Suponhamos que conta1 e conta2 tenham, como saldo, respectivamente,
R$1.000,00 e R$2.000,00. Se fosse necessário alterar o valor do saldo de conta1,
essa alteração deve afetar apenas o atributo saldo deste objeto – ou seja, não
deve haver influência com nenhum outro objeto da classe Conta. Alterando o
saldo de conta1 para R$3.000,00, o saldo da conta2 deve permanecer
R$2.000,00. Contudo, isso só é possível se cada atributo - de cada objeto -
possuir um espaço de memória particular, de modo que a alteração em um
desses espaços não afete os demais. Os atributos, em resumo, definem o que um
objeto tem (e não o que ele faz). O que um objeto faz em termos de ações (ou
serviços) é definido pelos métodos.
3.6.2 - Métodos
Dentro de cada classe, também declaramos o que cada objeto da mesma
faz (e como é feito). Pense, novamente, na classe Conta - utilizada para a
explicação de atributos. Sabemos que ela possui saldo como atributo. Logo, todo
e qualquer objeto desta classe também possuirá esse atributo. Agora, considere
que ela tenha um método “sacar” – utilizado para sacar (retirar) dinheiro de uma
conta (ou seja, de um objeto conta). Retirar dinheiro de uma conta significa alterar
o valor corrente do atributo saldo, para menos. Para nosso exemplo, queremos
um método que permita sacar uma determinada quantia e que não devolva
nenhuma informação – isto é, seja definido com tipo de retorno void. Uma
possível implementação para tal método é ilustrada na figura a seguir.
EADDCC031 – Linguagem de Programação II
37
Figura 3.6 – Exemplo de método
Como sabemos, um método é definido por um modificador de acesso, tipo
de retorno e nome – além de valores passados dentro do abre e fecha
parênteses, se necessário. Tudo isso forma a assinatura do método (linha 5). A
palavra chave void significa que quando esse método for executado, nenhuma
informação será devolvida pelo mesmo – ou seja, não existe um retorno. Para que
nosso método possa ser executado, precisamos informar um valor: variável
quantidade, do tipo double (linha5). Chamamos o que vem dentro desses
parênteses como parâmetro ou argumento. Essa variável é uma variável local,
utilizada somente dentro do escopo do método (linhas 5 e 8). Isso significa que
quando o método terminar sua execução, os valores de suas variáveis locais são
perdidos. Dentro do método, estamos declarando uma variável do tipo double
(linha 6) – que, como o argumento, vai morrer ao fim da execução do método. No
momento em que vamos acessar o atributo da classe, utilizamos a palavra-chave
this (linha 6). Essa palavra-chave serve para indicar que estamos acessando um
elemento específico (neste caso, o atributo saldo) da classe - ou seja, indica que
saldo é um atributo da classe e não uma variável local. O que o método realiza,
afinal, é diminuir do valor corrente do atributo saldo o valor definido no parâmetro
quantidade. Após a execução, o valor do atributo é atualizado com o novo saldo
(linha 7). Note que o valor do saldo pode ficar negativo caso o valor definido em
quantidade seja maior que o valor corrente de saldo. Se for necessário um
método para depositar um determinado valor, precisaremos de uma
implementação como a apresentada na figura a seguir.
EADDCC031 – Linguagem de Programação II
38
Figura 3.7 – Exemplo de método sem retorno
Observe, agora, que temos um método denominado depositar em nossa
implementação (linha 10). Este método também recebe um valor como
argumento. Dentro do método, o valor do atributo saldo é atualizado com a soma
entre o valor corrente do mesmo e o valor recebido por parâmetro. Ambos os
métodos não retornam informação após serem executados. A título de
exemplificação, vamos alterar o método depositar, fazendo com que o mesmo
retorne uma informação após a execução.
Figura 3.8 – Exemplo de método com retorno
Para simplificação, omitimos o método sacar. Compare o método depositar
nas duas figuras anteriores (3.7 e 3.8). Podemos verificar que, nessa última, a
assinatura do método mudou. Se antes ela era definida como tendo o tipo de
retorno void, agora ela possui o tipo de retorno String. Isso significa que o método
EADDCC031 – Linguagem de Programação II
39
depositar deve, ao final da sua execução, retornar uma String. Veja o uso da
palavra-chave return (linha 9). Portanto, após sua execução, o método devolve o
texto definido nesta linha. Agora que estamos cientes dos conceitos de métodos e
atributos, vamos nos aprofundar em nosso estudo, destacando como é o
processo de criação de objetos.
3.7. Instanciação de Objetos de Java
Precisamos entender como, em linguagem de programação, ocorre a
instanciação de objetos. A figura a seguir ilustra a instanciação em linguagem
Java.
Figura 3.9 – Instanciação de objetos
Para ilustrar a criação de objetos, criamos a classe Sistema e
implementamos na mesma o método main (no apêndice A, discutimos sobre este
método). Observe a linha 5. Essa linha não cria um objeto. Inserimos essa linha
para que possamos fazer uma comparação com as linhas 7 e 8 – que, de fato,
criam objetos. A linha 5 especifica um trecho de código separado pelo sinal “=”.
Na primeira parte dessa linha, antes desse sinal, temos a especificação de uma
variável chamada valor, do tipo int. Na segunda parte, temos o valor inteiro 10.
Essa linha de código atribui o valor 10 para a variável inteira valor. Agora, observe
atentamente a linha 7. Veja que a estrutura é praticamente a mesma a apresenta
na linha 5. Na primeira parte dessa linha de código, antes do sinal “=”, temos a
especificação da variável conta1, do tipo Conta. Conta é uma classe, previamente
especificada. Ela é o que denominamos tipo de referência. A segunda parte dessa
linha, posterior ao sinal “=”, é a mais importante, pois é ela quem cria o objeto.
Utilizamos a palavra-chave new, seguido do nome da classe e por parênteses. A
EADDCC031 – Linguagem de Programação II
40
linha 7 pode, então, ser definida da seguinte forma: cria-se, com a palavra-chave
new, um objeto de uma classe e atribui-se o mesmo a uma variável do tipo da
classe. A linha 8 realiza o mesmo procedimento, mas criando um objeto diferente,
chamado de conta2. Ao final da execução do método main, temos dois objetos
instanciados, nomeados como conta1 e conta2. Note que se a classe Conta não
tivesse sido criada antes de implementarmos as linhas 7 e 8, teríamos um erro de
compilação. O compilador informaria que não reconhece o tipo de referência
Conta, especificado em ambas as linhas 7 e 8.
Tipo por Referência Os tipos de dados, em Java, estão divididos em duas categorias: tipos primitivos e tipos por referência (às vezes, chamados de tipos não primitivos). Os tipos primitivos são: boolean, char, short, int, long, float e double. Todos os tipos não primitivos são tipos por referência. Portanto, as classes que especificam os tipos de objetos são tipos por referência. String é um tipo por referência.
Os programas utilizam as variáveis por tipo de referência para armazenar
as localizações de objetos na memória do computador. Essas variáveis
referenciam objetos no programa. Logo, considerando o nosso exemplo de código
de instanciação, as variáveis de referência conta1 e conta2 mantém a localização
dos objetos criados com a palavra-chave new. A figura a seguir ilustra melhor
essa questão. Os objetos referenciados podem conter variáveis de instância
(atributos) e métodos.
Figura 3.10 – Referenciação de objetos
EADDCC031 – Linguagem de Programação II
41
Como ilustrado, as variáveis conta1 e conta2 referenciam objetos de Conta,
armazenados na memória. Cada variável referencia o objeto que lhe foi atribuído,
conforme especificado no código da classe Sistema (figura 3.9). As setas
pontilhadas mostram a relação existente entre as variáveis e as posições de
memória.
3.8. Objetos que Instanciam Objetos
Analisando o código da figura 3.9, percebemos que os objetos conta1 e
conta2 foram criados dentro do método main. Contudo, em vários casos, a
instanciação de objetos não ocorre neste método. De fato, objetos podem ser
criados por quaisquer outros objetos. Vamos considerar as implementações das
classes Conta e Sistema, ilustradas, respectivamente, nas figuras 3.6 e 3.9.
Figura 3.11 – Objetos que criam objetos
Observe a figura acima. Note que há um método criarConta (linha 3),
dentro do qual criamos um objeto da classe Conta e atribuímos o mesmo para a
variável conta. Isso significa que quando este método for executado, um objeto de
Conta é criado. Observe que não detalhamos o que será feito com este objeto –
apenas ilustramos que é perfeitamente possível criamos um objeto a partir de
outro objeto. É obvio que deve existir um objeto instanciado da classe Sistema
para que seja possível invocar o método da linha 3. No cenário geral, teremos um
objeto da classe Sistema invocando seu método criarConta para criar um objeto
da classe Conta. A figura a seguir ilustra este cenário.
EADDCC031 – Linguagem de Programação II
42
Figura 3.12 – Objeto criando outro objeto
A figura acima especifica a criação da classe Teste, dentro da qual
definimos o método main (linha3). Na linha 4, instanciamos um objeto da classe
Sistema (definida na figura 3.11). Uma vez instanciado o objeto, podemos realizar
uma chamada para seu método criarConta (linha 5). Quando esse método é
chamado, conforme definido no código da classe Sistema, cria-se um objeto de
Conta. Portanto, o objeto sistema (do tipo Sistema) cria um objeto conta (do tipo
Conta).
3.9. Mensagens
Todo sistema OO funciona com base nas interações entre seus objetos.
Considere o seguinte cenário do mundo real: um aluno precisa realizar o aluguel
de um livro em uma biblioteca. Nesse caso, ele precisa ir à biblioteca para solicitar
o aluguel do livro. Observe que houve uma interação entre entidades – entre
aluno, livro e biblioteca. Entidades reais estão, portanto, sempre interagindo com
o ambiente à sua volta. Se sistemas OO são representações do mundo real – no
qual as entidades desse mundo são representadas, individualmente, através de
objetos –, então faz todo sentido pensar que o sistema funciona com base nas
interações entre os diversos objetos existentes no mesmo. Precisamos pensar,
agora, em como essas interações ocorrem em nível de sistema – ou seja, como
são implementadas. A noção de comportamento incorporado por um objeto é
caraterizado por um conjunto de métodos que podem ser requisitados por outros
objetos. Para que um objeto realize uma tarefa, é necessário enviar a ele uma
mensagem, solicitando a execução de um método específico. Uma mensagem
estimula a ocorrência de alguma ação no objeto que a recebe. Por padrão, as
informações utilizadas na emissão de uma mensagem são: destino (o objeto
EADDCC031 – Linguagem de Programação II
43
receptor), método (a operação que deve ser realizada) e parâmetros (informações
necessárias à execução do método). Vamos analisar, em código Java, como esta
emissão de mensagens ocorre.
Figura 3.13 – Especificação da classe Pessoa
Observe a classe Pessoa, ilustrada na figura acima. Note que ela possui
um atributo nome e um método. Analisando o método, podemos verificar que ele
recebe um parâmetro e altera o valor do atributo da classe para o valor deste
parâmetro. Em outras palavras, ele recebe um nome como argumento e
armazena este nome no atributo nome da classe (especificado no código pela
palavra-chave this). Sabemos, portanto, que se invocarmos este método, ele irá
alterar o valor do atributo nome. Precisamos, agora, saber como realizar a
invocação. Vamos analisar o código abaixo.
Figura 3.14 – Primeiro exemplo de invocação de métodos
Temos a classe Sistema, dentro da qual definimos o método main. Dentro
desse método, há a criação de um objeto de Pessoa (linha 5). A linha 6 é a nossa
fonte de discussão. Nela, temos a variável pessoa, seguida de um operador ponto
(.) e do nome do método de Pessoa que queremos invocar. Note que, na classe
Pessoa, o método precisa receber uma String como argumento. É por esta razão
que especificamos, na linha 6 da figura 3.14, o texto “Novo Nome”. O operador
EADDCC031 – Linguagem de Programação II
44
ponto (.) na linha 6 é utilizado para realizar a invocação do método alterarNome.
Em outras palavras, estamos enviando uma mensagem para o objeto
armazenado na variável pessoa, solicitando que o mesmo execute o método
alterarNome. O objeto, então, executa o método e altera o valor de seu atributo
nome para “Novo Nome”.
Figura 3.15 – Segundo exemplo de invocação de métodos
Observando a figura acima, percebemos que a classe Sistema, agora,
possui um método para cadastrar uma pessoa (linha 3). Esse método cria um
objeto de Pessoa (linha 4) e invoca o já conhecido método alterarNome (linha 5).
Agora, temos um exemplo claro da relação de um objeto chamando outro objeto.
Para que possamos invocar o método cadastrarPessoa, é necessário que
tenhamos instanciado um objeto da classe Sistema. O que temos, então, é um
objeto da classe Sistema invocando um método de outra classe. No nosso
exemplo, o método alterarNome de Pessoa. Como precisamos de um objeto de
Sistema para poder invocar seu método, é óbvio que, em algum lugar do sistema,
deverá haver um código como o ilustrado abaixo.
Figura 3.16 – Terceiro exemplo de invocação de métodos
EADDCC031 – Linguagem de Programação II
45
O código acima cria um objeto da classe Sistema (linha 3) e utiliza o
mesmo para invocar seu método cadastrarPessoa (linha 4). Essa invocação
realiza as ações ilustradas na figura 3.15 – isto é, cria um objeto de Pessoa e
invoca seu método alterarNome. Logo, temos um objeto, sistema, enviando uma
mensagem para que outro objeto, pessoa, execute um método específico.
Portanto, a interação entre objetos, essencial para o funcionamento de qualquer
sistema OO, é realizado por meio de mensagens (ou invocação de métodos).
3.10. Chamadas de Método – Interna e Externa
Observe a figura 3.17. A instrução da linha 6 chama o método alterarNome
do objeto pessoa. Note que estamos realizando isto de dentro do método main
que, por sua vez, está dentro da classe Sistema. Logo, estamos invocando
alterarNome a partir de outra classe. Uma chamada de método para um método
de outro objeto é referida como uma chamada de método externa. A sintaxe, em
linguagem Java, para uma chamada deste tipo é definida conforme a seguir:
objeto.nomeDoMétodo(lista de parâmetros). Esta sintaxe é conhecida como
notação de ponto. Ela consiste no nome de um objeto, um ponto, o nome de um
método e os parâmetros para a chamada. É particularmente importante
utilizarmos o nome do objeto (ou melhor, da variável que referencia o objeto) e
não o nome da classe.
Figura 3.17 – Chamada a método externo
Além da chamada de método externa, temos a chamada de método
interna. Vimos que a chamada de método externo se caracteriza por um método
de um objeto estar sendo invocado externamente por outro objeto. A chamada
EADDCC031 – Linguagem de Programação II
46
interna, por outro lado, se caracteriza pelo fato do método que está sendo
chamado estar na mesma classe que o método chamador – ou seja, o objeto
chama, através de um método X, outro método próprio, digamos Y. Por esta
razão, denomina-se chamada interna. Vejamos um exemplo em código.
Figura 3.18 – Chamada a método interno
Observe o código acima. Temos dois métodos implementados (linhas 5 e
10). O método depositar, além de atualizar o valor do atributo saldo, invoca (linha
7), o método imprimir (linha 10), que apenas imprime um texto informando que a
operação de depósito foi realizada. Uma chamada de método interna é
exatamente isso: um método dentro de uma classe realizando a chamada de
outro método, definido na própria classe. Observe, agora, o código abaixo.
Figura 3.19 – Exemplo de chamada externa e interna
Note que estamos criando um objeto conta (linha 5), do tipo Conta, no
código da figura acima. Em seguida, invocamos o método depositar, passando o
inteiro 1000 como parâmetro (linha 6). Ao invocarmos esse método, o próprio
EADDCC031 – Linguagem de Programação II
47
objeto invocará o método imprimir. Neste exemplo, temos, ao mesmo tempo, uma
chamada externa e interna. A externa ocorre entre as classes Sistema de Conta.
A chamada interna ocorre entre a classe Conta e ela mesma.
3.11. Variável de Classe
Vimos, pela figura 3.5, que cada objeto possui seu próprio espaço de
memória, para si e para seus atributos. Também vimos que cada variável que
representa um atributo de um objeto é conhecida como variável de instância, pois
está relacionada a uma instância em particular. Neste caso, cada objeto possui
uma cópia (analogamente, um espaço de memória) de todos os atributos
definidos para a classe. Isso evita que a alteração no valor de um atributo do
objeto X afete o valor do atributo de mesmo nome do objeto Y. E isso faz sentido,
já que estes atributos estão em espaços de memória diferentes. Contudo, há
casos em que precisamos que um atributo de classe esteja relacionado a todos
os objetos da classe, e não a cada objeto isoladamente. Em outras palavras,
precisamos que certo atributo seja compartilhado por todos os objetos da classe.
Neste caso, temos apenas uma posição de memória referente ao atributo, e esta
posição é compartilhada por todos os objetos. De início, podemos nos questionar
que, com essa estratégia, se um objeto X alterar o valor desse atributo, outro
objeto Y será afetado, já que eles compartilham um mesmo atributo (com uma
única posição de memória). Embora pareça problemática, a ideia é essa mesma.
A proposta é que atributos desse tipo estejam associados à classe
(consequentemente, a todos os seus objetos) e não a cada objeto em particular.
Em linguagem Java, esse atributo especial é denominado atributo estático (do
inglês static) e é chamado de variável de classe. Logo, uma variável static
representa informações de escopo de classe – todos os objetos da classe
compartilham os mesmos dados. Além de poder ser aplicado a atributos, a
palavra-chave static pode, também ser aplicada em métodos. Nesse caso,
teremos métodos estáticos. A vantagem dos métodos estático, em relação aos
demais, é que ele pode ser invocado mesmo que não tenha sido instanciado um
objeto da classe. Em outras palavras, se uma classe X possui um método estático
XPTO, este pode ser invocado sem que seja necessário criar um objeto da classe
X. Lembre-se que métodos não estáticos só podem ser invocados se houver um
EADDCC031 – Linguagem de Programação II
48
objeto instanciado. O método main (apêndice A) é um bom exemplo de método
estático. Observe a figura a seguir, sobre atributos estáticos.
Figura 3.20 – Representação de atributo estático em memória
Para o exemplo da figura acima, estamos considerando uma classe Conta,
com atributos: numero, saldo e limite. Especificamente, definimos limite como
sendo do tipo static. Criamos, então, dois objetos dessa classe Conta. Como já
discutido, cada objeto possui sua própria posição de memória. Os nomes conta1
e conta2 (atribuídos para identificar os dois objetos criados) são referências para
os mesmos. Observe, pelas lupas, que cada objeto possui uma posição para cada
um dos seus atributos nome e saldo. Note, no entanto, que tanto conta1 quanto
conta2 não possuem, em suas respectivas posições de memória, o espaço para o
atributo limite. Se olharmos atentamente, veremos que limite está em um local
separado da memória. Apesar disso, existe uma ligação entre os objetos conta1 e
conta2 para esta posição – eles compartilham a mesma posição de memória
relacionada ao atributo limite. O fato de ambos os objetos não possuírem uma
posição particular para o atributo limite não significa que eles não o possuem. O
objeto conta1 possui numero, saldo e limite como atributos. O objeto conta2
possui numero, saldo e limite como atributos. Como limite é estático, só se define
uma única posição de memória, compartilhada por todos os objetos de conta,
para armazenar seu valor. Convém apenas ressaltarmos que a figura 3.20 é
apenas uma ilustração do conceito – não quer dizer que seja exatamente assim
que estes atributos são gerenciados pelo computador. Agora, observe a figura
abaixo.
EADDCC031 – Linguagem de Programação II
49
Figura 3.21 – Classe Conta
A figura acima representa a classe conta. Note o uso da palavra-chave
static para definir que o atributo limite é estático. Para ilustrar o gerenciamento
dos valores de limite, definimos um método para alterar seu valor e outro método
para capturar o mesmo. Observe o código da figura abaixo.
Figura 3.22 – Exemplo de programa com atributo estático
EADDCC031 – Linguagem de Programação II
50
No código da figura acima, criamos os dois objetos, conta1 e conta2 (linhas
5 e 6). Nas linhas 8 e 9, definimos os valores do atributo limite para ambos, como
sendo 0. Em seguida, nas linhas 11 e 12, solicitamos que estes valores fossem
impressos, apenas para conferência. A figura abaixo ilustra o resultado da
execução deste programa, somente até essa impressão. Observe a mesma.
Figura 3.23 – Resultado parcial da execução
Note que, como solicitado, ambos os valores estão com valor 0. Na linha
14, definimos que o valor do atributo limite de conta1 é 1000. Em seguida, nas
linhas 16 e 17, solicitamos a impressão dos valores do atributo limite sejam
impressos, para ambos os objetos. A figura abaixo ilustra o resultado da execução
deste programa, somente até essa impressão. Observe a mesma.
Figura 3.24 – Resultado parcial da execução
Como solicitado, o valor do atributo limite para o objeto conta1 foi alterado
de 0 para 1000. Contudo, observe o resultado obtido ao imprimir o valor desse
atributo para o objeto conta2. Em vez de imprimir 0, o valor impresso foi 1000. O
curioso é que nosso código não possui instrução – antes da linha 14 – para alterar
o valor de limite do objeto conta2 para 1000. A razão para isto está no fato de
EADDCC031 – Linguagem de Programação II
51
limite estar definido como estático. Como dizemos, um atributo estático é
compartilhado por todos os objetos da classe que o define. Se um objeto alterar o
valor deste atributo, essa alteração será compartilhada por todos os demais
objetos da classe. Continuando a análise do programa, temos que na linha 19,
definimos a instrução para alterar o valor atual do limite da conta2, para 2000. Em
seguida, nas linhas 21 e 12, solicitamos a impressão dos valores do atributo
limite, para ambos os objetos. A figura abaixo ilustra o resultado da execução
deste programa, somente até essa impressão. Observe a mesma.
Figura 3.25 – Resultado final da execução
O valor do limite de conta2 foi alterado para 2000. Contudo, note que o
valor do limite para conta1 também foi alterado para 2000. A alteração realizada
pelo objeto conta2 foi compartilhada com os demais objetos da classe. Este é o
comportamento que se deseja ao se definir um atributo como sendo estático. Para
comparação, suponha que alteramos o atributo limite da classe Conta, de estático
para não estático, e executamos o programa novamente. Observe o resultado
abaixo.
Figura 3.26 – Resultado final da execução com atributo não estático
EADDCC031 – Linguagem de Programação II
52
Observe que o resultado foi diferente, como esperado. Começamos
atribuindo valores 0 para o limite de ambos os objetos. O resultado impresso foi 0.
Em seguida, alteramos o valor de limite de conta1, de 0 para 1000. Solicitamos a
impressão desse atributo para ambos, conta1 e conta2, e o resultado foi 100 para
conta1 e 0 para conta2. Como o atributo deixou de ser estático, e como alteramos
apenas o valor do limite para conta1, o valor para conta2 continua 0. Em seguida,
fizemos a alteração do valor do limite de conta2, do valor atual para 2000. Após a
impressão, obtivemos 100 para conta1 (valor antigo) e 2000 para conta2 (valor
para o qual solicitamos a alteração). Então, perceba que o uso de static afeta, em
muito, o resultado de um programa. Agora que entendemos o uso dos atributos
estáticos, convém verificamos a real utilidade dele. Observe a figura abaixo.
Figura 3.27 – Classe Livro
Suponha uma classe livro, como definida no código da figura acima.
Suponha que ela esteja definida no sistema de uma livraria. Esta livraria oferece
descontos para os livros que vende – razão pela qual temos o atributo desconto
(linha 5). Além deste, definimos os atributos titulo e preco. Nas linhas 7 e 11,
definimos métodos para retornar, respectivamente, os valores de preco e
desconto. Na linha 15, definimos um método para alterar o valor de desconto.
Agora, observe o código abaixo.
EADDCC031 – Linguagem de Programação II
53
Figura 3.28 – Classe Livraria
No código da classe Livraria, definimos um método (linha 5) para calcular o
valor final de um livro. Este método recebe um objeto livro como parâmetro e
invoca os métodos para obter valor do livro e valor do desconto. Da forma como
implementada a classe Livro, cada livro possui um desconto particular,
relacionado apenas a si. Isso quer dizer que um objeto livro1 pode ter desconto
de R$10,00 e um livro2 pode ter desconto de R$50,00. Suponha que o dono da
livraria não queira oferecer descontos diferentes para cada livro – ou seja, deseja-
se um valor de desconto único para todos os livros. A questão, então, é como
garantir que todos os livros tenham o mesmo desconto. A resposta é simples: uso
de static. Basta alterarmos o atributo desconto para estático. Assim, toda vez que
ele precisar aumentar ou diminuir valor do desconto, basta fazê-lo para apenas
um objeto – os demais (por compartilharem uma única posição de memória) terão
os valores de seus descontos alterados automaticamente. Sem o uso do atributo
estático e, ainda, adotando a mesma política de descontos, teríamos que utilizar o
método da linha 15 (figura 3.27) para cada objeto livro do sistema – ou seja, se
tivermos um livro1 e um livro2, e se quisermos alterar o valor do desconto,
teríamos que chamar o método da linha 15 para livro1 e, depois, para o livro 2.
Imagine fazer isto para uma biblioteca com 1000 livros. É para cenários assim que
a palavra-chave static é útil.
EADDCC031 – Linguagem de Programação II
54
Fique Atento Assim como tipos primitivos, objetos também podem ser passados como parâmetros. A figura 3.28 apresenta um exemplo. Ao passarmos um tipo primitivo, declaramos o tipo e, em seguida, a variável local que será usada dentro do método.
Exemplo: public void calcular(int a); A passagem de objetos por parâmetro acontece da mesma forma. Primeiro, declara-se o tipo de objeto que está sendo passado – ou seja, o nome da classe a partir do qual o mesmo foi instanciado. Em seguida, define-se um nome de variável local, que será utilizada dentro do escopo do método. Para o nome da variável, pode-se adotar qualquer termo (ou conjunto de termos) permitido pela linguagem. Exemplo: public void calcular(Livro livro) Onde: • Livro (maiúsculo) é o nome da classe (ou seja, o tipo do objeto) • livro (minúsculo) é o nome da variável local do método
EADDCC031 – Linguagem de Programação II
55
4. Construtores
Toda classe, além de atributos e métodos (ambos opcionais) possuem o
que chamamos de construtores. Estes não são opcionais. Logo, toda e qualquer
classe possui construtor (pelo menos um, mas podendo ter mais). Os construtores
de uma classe possuem um papel especial a cumprir: é sua responsabilidade
colocar cada objeto da classe, quando ele está sendo criado, em um estado
previamente definido para ser utilizado. É o que chamamos em OO de
inicialização. O construtor inicializa o objeto para um estado adequado de uso.
4.1. Construtores
É importante citarmos que um construtor pode (não é obrigatório) receber
parâmetros, os quais serão utilizados para inicializar seus atributos. Vejamos em
código.
Figura 4.1 – Exemplo de método construtor
Observe a classe Pessoa, definida na figura acima. Podemos verificar que
esta classe possui dois atributos, nome e CPF (linhas 3 e 4). Agora, analise com
cuidado o código na linha 6. Este é o construtor da classe Pessoa. Um dos
recursos característicos dos construtores é que eles possuem o mesmo nome da
classe onde estão definidos. Portanto, a linha 6 possui um modificador de acesso
public, seguido do nome da classe, Pessoa, e por dois parâmetros, nome e CPF.
Podemos notar que um construtor é bem parecido com um método. A diferença é
que não existe um tipo de retorno (por exemplo, void). Essa é a assinatura de
EADDCC031 – Linguagem de Programação II
56
construtores e deve ser sempre seguida. Obrigatoriamente, o modificador de
acesso deve ser public – para que possa ser invocado por classes externas. As
linhas 7 e 8 apenas atualizam os valores dos atributos nome e saldo com os
valores dos argumentos nome e saldo. O uso da palavra-chave this, novamente,
foi realizado para indicar que nome e CPF, lado esquerdo do símbolo “=”, indicam
atributos e não variáveis locais. Por este exemplo, já começamos a entender
outro uso da palavra this (além de referencia membros da própria classe): evitar
ambiguidade de nomes entre atributos e variáveis locais. Agora, observe o código
abaixo.
Figura 4.2 – Instanciação de objetos com construtor
Podemos notar que, em virtude do construtor da classe Pessoa (figura 4.1),
toda vez que quisermos criar um objeto dessa classe, obrigatoriamente, teremos
que passar duas strings como parâmetro. Se tentarmos criar um objeto da classe
Pessoa sem especificar a passagem dessas duas strings como parâmetro,
teremos um erro de compilação. Algo curioso sobre os construtores é o fato de
podermos escolher os atributos que serão inicializados – em outras palavras, não
há qualquer obrigatoriedade de inicializarmos, com valores específicos, todos os
atributos. Observe o código abaixo – o qual altera o construtor da classe Pessoa
(figura 4.1).
Figura 4.3 – Exemplo de método construtor
EADDCC031 – Linguagem de Programação II
57
Observe o construtor da classe acima. Podemos perceber que, agora,
apenas um dos atributos da classe Pessoal, nome, está sendo inicializado pelo
construtor. Ao tentarmos instanciar um objeto desta classe Pessoa, apenas um
atributo do tipo string será cobrado. A figura a seguir ilustra este cenário.
Figura 4.4 – Instanciação de objetos com construtor
Precisamos nos perguntar o que acontece com o atributo CPF, já que o
mesmo não está sendo inicializado pelo construtor. Vamos discutir mais sobre
construtores antes de respondermos esta questão. A instanciação de qualquer
objeto requer a existência de um construtor. No caso de Java, a instanciação é
realizada através do uso da palavra-chave new – conforme vimos nos exemplos
sobre criação de objetos. Analise o código da figura 4.5. Observe que estamos
realizando a instanciação de um objeto da classe Aluno (linha 4). A instrução da
linha 4, primeiro, utiliza a palavra-chave new para criar um objeto da classe Aluno.
Os parênteses vazios depois de new Aluno indicam uma chamada ao construtor
da classe. Observe que, nesse caso, não há parâmetro sendo passado. O que
está sendo realizado aqui é uma chamada ao construtor padrão da classe Aluno.
Figura 4.5 – Chamada ao construtor de classe
Vamos analisar a classe Aluno, ilustrada na figura 4.6. A princípio,
poderíamos dizer que a classe Aluno não possui um construtor. Contudo, ela o
possui. Por padrão, o compilador fornece um construtor-padrão sem parâmetros
EADDCC031 – Linguagem de Programação II
58
para toda e qualquer classe que não inclua, explicitamente, um construtor em seu
código. O construtor-padrão não é codificado na classe.
Figura 4.6 – Classe Aluno
Analise, novamente, o código da classe Pessoa (figura 4.1). Observe que
definimos um construtor para ela, o qual recebe dois parâmetros (nome e CPF).
Ao contrário da classe Pessoa, nenhum construtor foi definido para a classe Aluno
(figura 4.6) – ela possui apenas os atributos nome e matrícula. Entretanto, assim
como os objetos da classe Pessoa, os objetos da classe Aluno precisam ser
inicializados. Precisamos verificar, então, com que valores os atributos da classe
Aluno são inicializados. Ademais, para o exemplo da figura 4.4, precisamos definir
com que valor o atributo CPF de Pessoa é inicializado – já que o atributo nome
está sendo, por meio do construtor (linha 4 – figura 4.4). Para respondermos essa
questão, precisamos entender que todo atributo (variável, em código, que
representa um atributo) em Java tem um valor inicial padrão: um valor fornecido
pelo Java quando o programador não especifica o valor inicial do atributo. Para
simplificação, trataremos os termos atributo e atributo como sinônimos (que, de
certa forma, são). A partir deste ponto, usaremos somente o termo atributo.
Tipo por Referência Cada tipo primitivo em Java possui um valor como valor-padrão. Esses valores, como vimos, são utilizados pelo compilador para inicialização dos atributos (atributos) das classes. Os tipos byte, short, int e long, float e double possuem valor-padrão 0. O tipo boolean possui valor-padrão false. Os tipos por referência também possuem valores-padrão. Para estes, o valor-padrão é null.
Observe o código da figura 4.6 e perceba que não definimos valores iniciais
para os atributos nome e matrícula – apenas declaramos os mesmos, como
sendo dos tipos String e int. Logo, se não inicializarmos os mesmos, o compilador
EADDCC031 – Linguagem de Programação II
59
o fará por nós. Portanto, não se exige que os atributos sejam, explicitamente,
inicializados antes de serem utilizados em um programa – a menos que devam
ser inicializados com valores diferentes de seus valores-padrão. Note que todos
os atributos serão inicializados: ou por seus valores-padrão ou por um valor
específico, que o programador deve informar através de um construtor. No caso
da classe Pessoa (figura 4.3), temos os atributos nome e CPF. Observe o
construtor na linha 6 e perceba que estamos inicializando (com um valor passado
por parâmetro) apenas o atributo nome. Portanto, esse atributo nome é
inicializado por um valor a ser passado no momento em que um objeto de Pessoa
é criado (figura 4.4). O valor de CPF também é inicializado, mas com o valor
padrão para o tipo String (no caso, null). Já no código da classe Aluno (figura 4.6)
- que não possui um construtor definido pelo programador (embora possua um
construtor-padrão), - ambos os atributos serão inicializados com seus valores-
padrão.
Fique Atento Toda classe possui um construtor: ou o construtor-padrão ou um construtor definido pelo programador. Se nenhum construtor for definido pelo programador, o compilador criará e usará o construtor-padrão. Contudo, se um construtor for definido pelo programador, o compilador usará sempre este e não mais o construtor-padrão.
Se o construtor-padrão for utilizado, todos os atributos da classe serão
inicializados com seus respectivos valores-padrão. Se um construtor for definido
pelo programador, este poderá decidir com que valores específicos os atributos
serão inicializados. É curioso observarmos que no caso de definirmos um
construtor, não somos obrigados a inicializar todos os atributos da classe com
valores específicos (diferentes dos valores-padrão). Isso quer dizer que podemos
decidir quantos atributos iremos, de fato, inicializar com o construtor. No caso da
classe Pessoa (figura 4.3), o construtor inicializa apenas o atributo nome. O
atributo CPF, como já explicado, será inicializado com seu valor-padrão. Neste
caso, criamos o construtor (linha 6) apenas para inicializarmos o atributo nome
com um valor diferente do seu valor padrão.
EADDCC031 – Linguagem de Programação II
60
Fique Atento Podemos dizer que o construtor-padrão serve para inicializar (com valores-padrão) os atributos da classe - possibilitando que, ao serem criados a partir da mesma, os objetos assumam um estado adequado de uso. Os construtores definidos pelo programador servem para inicializar, com valores diferentes dos valores-padrão, certos (alguns ou todos) atributos da classe.
Suponha que seja necessário inicializar, para a classe Aluno (figura 4.6),
ambos os atributos nome e matrícula, com valores diferentes dos valores padrão.
Neste caso, precisamos definir um construtor para essa classe, visto que o atual
construtor (construtor-padrão) não atende à nova necessidade. O código da
classe fica como ilustrado a seguir.
Figura 4.7 – Construtor da classe Aluno
Observe que, agora, ambos os atributos de Aluno são inicializados pelo
construtor (linha 5). Isso obriga que, no ato de instanciação de qualquer objeto de
Aluno, sejam passados valores para ambos os atributos. Se nenhum valor for
passado, ou se apenas um valor for passado (em vez de dois), teremos um erro
de compilação. A figura a seguir ilustra a instanciação de objetos da classe Aluno.
EADDCC031 – Linguagem de Programação II
61
Figura 4.8 – Exemplos de instanciação de objetos Aluno
Observe o código da figura acima. Nas linhas 9 e 10, definimos a
instanciação de dois objetos de Aluno: aluno1 e aluno2. Note que,
obrigatoriamente, tivemos que especificar a passagem de dois parâmetros. Isto
porque o construtor da classe Aluno precisa receber dois valores. Portanto, toda
vez que um objeto de Aluno for criado, teremos que passar valores para os
atributos nome e matricula. Caso esses dados não sejam passados, teremos erro
de compilação. As linhas 16, 17 e 18 ilustram exemplos de instruções que
causariam erro. Nas linhas 16 e 17, apenas um parâmetro está sendo passado.
Na linha 18, nenhum. A instrução da linha 18 não causaria erro, caso tivéssemos
optado por não especificar o construtor de Aluno (linha 5, figura 4.7). Neste caso,
seria usado o construtor padrão da linguagem e ambos, nome e matrícula, seriam
inicializados com seus valores-padrão. Mas como definimos um construtor
específico, o construtor padrão deixa de ser utilizado. O compilador entende que
se definimos um construtor específico é porque não precisamos do construtor
padrão. Falta entendermos porque deixaríamos de usar o construtor padrão da
linguagem para especificar um próprio. Talvez fosse mais fácil deixar que todos
os atributos fossem inicializados pelo compilador. Contudo, observe a figura a
seguir.
EADDCC031 – Linguagem de Programação II
62
Figura 4.9 – Exemplo de uso dos construtores
Considere, ainda, que exista um sistema responsável pelo cadastro de
alunos. Pensando em termos de interface gráfica, poderia haver algo parecido
com o formulário Cadastro de Alunos da figura 4.9. O formulário é extremamente
simples, contendo apenas dois atributos a serem preenchidos: nome e matrícula.
Esses dados, dentro do nosso exemplo, devem ser fornecidos para se cadastrar
qualquer aluno. No nosso contexto, cadastrar um aluno significa instanciar um
objeto da classe Aluno no sistema de cadastro. Observe que a classe Aluno
(figura 4.7) possui um construtor, o qual deve receber os dados de nome de
matrícula, por parâmetro. Assim, os dados informados no formulário de cadastro
são usados na chamada ao construtor (instrução da figura 4.9). O nome
“Fernando” é passado como primeiro parâmetro, visto que este se refere ao nome
do aluno. A matrícula é passada como segundo parâmetro, uma vez que o
segundo parâmetro do construtor espera um valor inteiro de matrícula. Observe o
código a seguir.
Figura 4.10 – Método para cadastro de alunos
No código acima, temos a classe Sistema, a qual implementa o método
cadastrarAluno. Note o que este método faz: recebe dois dados como parâmetros
(nome e matrícula) e instancia um objeto da classe Aluno, passando, também
EADDCC031 – Linguagem de Programação II
63
como parâmetro, os dois dados recebidos. A linha 7 corresponde à chamada de
construtor da classe Aluno (figura 4.7). Vamos considerar que o botão Cadastrar
(figura 4.9), ao ser acionado, invoque o método cadastrarAluno, passando os
dados Fernando e 201322001 para o mesmo. Em tempo de execução, teremos
algo como na figura a seguir.
Figura 4.11 – Representação da chamada de método em tempo de execução
O código acima apenas simula o que ocorre em tempo de execução.
Observe que é o mesmo método da figura 4.10 – substituímos, respectivamente,
os parâmetro nome e matrícula por “Fernando” e “201322001”. Assim, quando
preenchermos o formulário e acionarmos o botão Cadastrar, os dados Fernando e
201322001 serão passados para o método cadastrarAluno, o qual repassará os
mesmos para o construtor de Aluno – criando (cadastrando) o objeto aluno1. Este
exemplo simula, com menor nível de detalhe, o que acontece em um sistema real
de cadastro. Não entraremos em detalhes sobre criação e manipulação de
interface gráfica nesta apostila. Continuando nosso estudo sobre construtores,
vamos pensar em como possibilitar que todo objeto de uma classe seja
inicializado com um valor pré-definido para um de seus atributos. Considere o
código a seguir.
Figura 4.12 – Inicialização de atributo com valor pré-definido
EADDCC031 – Linguagem de Programação II
64
Observe o construtor da linha 6. Esse construtor recebe apenas um dado
por parâmetro e o utiliza para inicializar o atributo nome. Note a instrução da linha
8. Perceba que o valor do atributo matrícula está sendo definido como
000000000. Isso quer dizer que todo objeto da classe Aluno terá, inicialmente, o
mesmo valor para matrícula ao ser instanciado. O valor para nome deverá ser
passado no momento da instanciação. Veja como fica o código para a criação de
um objeto de Aluno.
Figura 4.13 – Chamada ao construtor de Aluno
A linha 4 cria um objeto da classe Aluno. Esse objeto terá o nome
Fernando e matrícula com valor 000000000. Se outro objeto for criado, ele terá o
nome especificado pelo valor passado por parâmetro, mas valor 000000000 para
matrícula. Evidentemente, os valores de matrícula poderão ser alterados depois,
desde que haja métodos específicos para isto na classe Aluno. A questão é
entender porque se criar objetos com valores pré-definidos para alguns atributos.
A verdade é que cada sistema terá uma razão para fazê-lo. Pode ser somente
uma questão de se impedir que atributos sejam inicializados com valores-padrão,
ou pode ser uma necessidade imposta pela própria natureza do domínio do
problema. Suponha um cenário hipotético para cadastro de usuários de um
sistema. Para este caso, considere o código abaixo.
Figura 4.14 – Chamada ao construtor de Aluno
EADDCC031 – Linguagem de Programação II
65
O sistema deste cenário, inicialmente, cadastra todo novo usuário com uma
senha de acesso padrão, que deve ser alterada, posteriormente, pelo próprio
usuário. Pelo código da classe Usuario, podemos observar que o construtor (linha
6) recebe apenas um parâmetro, nome – utilizado para inicializar o atributo nome.
O atributo senha, por sua vez, é inicializado através de um valor padrão. Logo,
todo objeto de Usuario terá, inicialmente, a mesma senha. Certamente, métodos
para alteração da mesma deverão ser fornecidos pela classe, permitindo que
cada usuário altere sua senha pessoal.
4.2. Construtores Sobrecarregados
Quando tratamos de construtores, uma das possibilidades é a definição de
não apenas um, mas de dois ou mais construtores para uma mesma classe. Até o
momento, vimos classes com apenas um construtor, inicializando alguns ou todos
os seus atributos. Veremos, agora, casos em que especificamos mais de um
construtor para uma classe. Considere o seguinte cenário: um sistema realiza o
cadastro de pessoas. Para realizar este cadastro, ele precisa de, ao menos, um
dos seguintes documentos: RG e CPF. Considere o código a seguir.
Figura 4.15 – Construtor único em classe
No código acima, temos a classe Pessoa, com os atributos RG e CPF. Na
linha 6, especificamos um construtor para a mesma, recebendo dois parâmetros –
utilizados para inicializar ambos os atributos. O problema com esta
implementação é que ela obriga que seja passado tanto o RG quanto o CPF no
momento de instanciarmos um objeto de pessoa. Se a instanciação significar o
cadastro de uma pessoa, esta terá que ter ambos os documentos. Contudo,
EADDCC031 – Linguagem de Programação II
66
dizemos que ao menos um teria que ser utilizado – não necessariamente os dois.
Pode ser que, em um cenário real, a pessoa tenha apenas um dos dois
documentos em mãos. Para a pessoa que estiver com ambos, não teremos
problemas com o cadastro. Mas se a pessoa tiver apenas com um, pelo código
acima, não teremos como cadastrá-la. A solução para este impasse é possível:
basta especificarmos outros construtores para esta classe. A figura a seguir
apresenta o código para esta situação.
Sobrecarga Recurso usual em OO que consiste na definição de mais de uma implementação para um mesmo método em uma classe. A sobrecarga ocorre quando uma classe especifica métodos com mesmo nome, mas com assinaturas diferentes. Assim, mesmo que vários métodos tenham o mesmo nome, eles são distinguíveis pelo compilador. A assinatura de um método é uma combinação entre seu nome, tipo (retorno) e quantidade (e tipo) de argumentos que são passados para o mesmo.
Figura 4.16 – Construtores sobrecarregados
Observe a presença de três construtores no código acima: linhas 6, 11 e
15. O primeiro recebe dois parâmetros (RG e CPF) e inicializa os atributos RG e
CPF com estes valores. Já o segundo recebe apenas um valor (RG) e inicializa o
atributo RG. O terceiro, da mesma forma, recebe um único valor (CPF) e inicializa
do atributo CPF. A primeira questão a responder é porque não ocorre erro de
compilação nesta classe, visto que temos construtores com nomes iguais. Em
primeiro lugar, devemos nos lembrar de que um construtor é uma estrutura
semelhante a um método, diferenciando-se deste pelo fato de não retornarem
EADDCC031 – Linguagem de Programação II
67
valores (não podem especificar um tipo de retorno, nem mesmo void). Logo,
assim como métodos, construtores podem ser sobrecarregados (ter nomes iguais,
mas assinaturas diferentes). No caso dos construtores, a assinatura é a
composição de seu nome e quantidade (e tipo) de argumentos que ele possui.
Portanto, não ocorre erro de compilação na classe Pessoa pelo fato dos
construtores estarem sobrecarregados. Convém entendermos qual o benefício de
se especificar vários construtores em uma classe. Considerando o problema do
cadastro de pessoas, essa nova classe Pessoa permite que pessoas sejam
cadastradas utilizando-se somente CPF, ou RG ou mesmo ambos. Assim, se uma
pessoa precisar ser cadastrada e estiver apenas com o RG em mãos, ela o será.
Neste caso, basta usar o construtor da linha 11. Se ela estiver apenas com o
CPF, basta usar o construtor da linha 15. E se ela estiver com ambos os
documentos, pode-se utilizar qualquer um dos três construtores. Em resumo,
construtores sobrecarregados permitem que objetos sejam inicializados de
diferentes maneiras.
4.2. Referência THIS em Construtores
Já vimos em unidade anterior que a palavra-chave this é utilizada para se
evitar ambiguidade de nomes. De fato, ela evita que nomes de atributos de
classes sejam confundidos com nomes de variáveis locais. Em se tratando de
construtores, a palavra-chave this assume outra possibilidade de uso: reutilizar
código de inicialização fornecido por outro construtor da classe. Em outras
palavras, podemos - dentro de um construtor de uma classe - referenciar outro
construtor da mesma classe, de forma a reaproveitar o código deste último. Com
isso, evitamos ter que ficar repetindo código de inicialização quando temos vários
construtores em uma mesma classe. Analise o código a seguir.
EADDCC031 – Linguagem de Programação II
68
Figura 4.17 – Problema com construtores sobrecarregados
A classe Pessoa apresenta dois construtores, linhas 7 e 12. Observe que o
primeiro construtor recebe dois parâmetros: nome e telefone, utilizados para
inicializar os atributos nome e telefone. Note que os nomes dos parâmetros deste
construtor possuem os mesmos nomes dos atributos da classe. Para que seja
possível diferenciá-los, precisamos da palavra-chave this. A figura a seguir ilustra
a relação entre estes termos.
Figura 4.18 – Utilização de this para evitar ambiguidade de nomes
Com a figura acima, ilustramos o uso de this para se evitar ambiguidade de
nomes. Veja que o termo nome, recebido por parâmetro, se refere a uma variável
local e não ao atributo nome. O termo nome, ao lado da palavra-chave this, se
refere ao atributo da classe. Portanto, a palavra this é utilizada para referenciar
EADDCC031 – Linguagem de Programação II
69
entidades da classe (neste caso, atributo). Contudo, métodos também podem ser
referenciados por this. A figura 3.18, unidade 3, ilustra um exemplo (linha 7).
Agora que recordarmos esse primeiro uso da palavra-chave this, vamos voltar ao
caso dos construtores. Observe, novamente, o código da figura 4.17. Veja que o
segundo construtor, a exemplo do primeiro, recebe nome e telefone como
parâmetros, além de RG. Note que temos, dentro dos construtores, repetição de
código - nas linhas 8, 9, 13 e 14. Para este pequeno exemplo, isto não representa
um problema. Contudo, considere que a classe tenha vários atributos, e que esse
repetição de código seja da ordem de algumas dezenas de linhas. Neste caso, a
repetição começa a incomodar. Entretanto, podemos usar o this para resolver
essa questão. Observe o código a seguir.
Figura 4.19 – Utilização de this para evitar ambiguidade de nomes
Na figura 4.17, tanto o primeiro quanto o segundo construtores estão
inicializando nome e telefone, causando repetição de código. O segundo
construtor, em relação ao primeiro, apenas inicializa um atributo a mais: RG.
Portanto, o segundo construtor pode aproveitar o código de inicialização do
primeiro. É isto o que está sendo feito na figura 4.19. Observe que as linhas 13 e
14 da figura 4.17 foram substituídas pela linha 13 da figura 4.19. Nesta linha,
temos o a palavra this, seguida de parênteses e das variáveis locais nome e
telefone dentro dos mesmos. Quando disposta desta forma, a palavra-chave this
faz referência a outro construtor da classe Pessoa – neste caso, o primeiro
construtor. A linha 13 repassa ao construtor da linha 7 as variáveis nome e
telefone, recebido por parâmetro. O construtor da linha 7, então, inicializa os
EADDCC031 – Linguagem de Programação II
70
atributos nome e telefone com estas variáveis. Em seguida, a linha 14 inicializa o
atributo RG, com o parâmetro RG, recebido por parâmetro. Temos, portanto, a
inicialização dos atributos nome, telefone e RG. Agora, observe o código a seguir.
Figura 4.20 – Distinção entre vários construtores
Observe que, em relação ao código da figura 4.19, apenas acrescentamos
na figura 4.20 o construtor da linha 17. O que queremos verificar, aqui, é como a
palavra-reservada this (linha 13) sabe que deve fazer referência ao construtor da
linha 7 e não a qualquer outro construtor, como o da linha 17. A resposta é
simples: através da assinatura do construtor. Observe que a instrução da linha 13
realizada uma chamada a um construtor que receba duas strings como
parâmetro. O construtor da linha 17 recebe apenas uma e o construtor da linha 7,
duas. Logo, a instrução da linha 13 acaba fazendo referência ao primeiro
construtor, linha 7. Agora, podemos nos perguntar como o this faria a distinção
caso tivéssemos na classe Pessoa dois construtores recebendo suas strings.
EADDCC031 – Linguagem de Programação II
71
Figura 4.21 – Construtores com mesma assinatura
Observe pela figura 4.21 que os construtores das linhas 7 e 17 possuem a
mesma assinatura e, portanto, geram um erro de compilação. Logo, não pode
haver dois construtores com uma mesma assinatura e, assim, a palavra-chave
this sempre consegue distinguir, pela assinatura dos construtores, qual deve
referenciar.
EADDCC031 – Linguagem de Programação II
72
5. Pacotes As classes representam a forma básica de estruturação de um sistema
orientado a objetos. Embora elas sejam úteis, é necessário algo mais para a
estruturação de grandes e complexos sistemas, os quais podem conter centenas
ou milhares de classes. Podemos, então, dispor de um tipo de módulo que sirva
para agrupar classes relacionadas, tornando mais fácil a tarefa de se encontrar
uma determinada classe entre as várias pertencentes a um sistema. A ideia de
agrupamento de classes oferece um nível a mais de abstração (a habilidade de se
concentrar nos aspectos essenciais de um contexto qualquer) em relação à
abstração apresentada por uma classe isolada. Para apoiar a proposta de
agrupar classes relacionadas, a Orientação a Objetos oferece o conceito de
pacotes.
5.1. Pacotes
Colocar classes relacionadas em um pacote é, claramente, um mecanismo
conveniente para organização de classes. Entretanto, há uma razão mais
importante para os pacotes: evitar conflito de nomes. Em grandes projetos de
desenvolvimento de sistemas OO, em geral, é inevitável atribuir o mesmo nome
para diferentes elementos, por exemplo classes. Ainda que o programador
controle o desenvolvimento, evitando atribuir um mesmo nome para mais de uma
classe, devemos considerar que, em algum momento, o programador pode
precisar fazer uso de alguma biblioteca que contém uma classe com um nome
idêntico ao atribuído para uma de suas classes. Neste caso, o conflito de nomes é
inevitável. Entretanto, o conceito de pacotes cria seu próprio espaço de nomes,
evitando o conflito. É preciso considerar, também, que o desenvolvimento de
grandes sistemas engloba vários programadores participando da implementação
ao mesmo tempo. Neste caso, suponha que um deles precise adicionar uma
classe Conta no sistema, mas descobre que outro programador implementou uma
classe com esse mesmo nome, mas com implementação diferente. O primeiro
programador não ficará impossibilitado de utilizar a nomenclatura Conta desde
que as classes estejam em pacotes diferentes. Então, muito mais do que
EADDCC031 – Linguagem de Programação II
73
simplesmente organizar as inúmeras classes do sistema, os pacotes permitem
expandir o espaço de nomes. A princípio, qualquer grupo de classes pode ser
organizado dentro de um pacote. Porém, o mais natural é organizá-las em função
de alguma relação entre elas. Um exemplo seria organizar em um pacote as
classes relacionadas ao controle financeiro do sistema, classes que realizam
algum tipo de ordenação de dados, etc. Precisamos enfatizar, ainda, que mesmo
estando em pacotes diferentes, classes podem se relacionar umas com as outras.
A organização de pacotes não impede a comunicação entre as classes.
Evidentemente, em termos de implementação, precisamos informar ao compilador
que uma classe está fazendo uso de outra classe (localizada em outro pacote).
Sem essa informação, o compilador emite erros. O uso de pacotes é tão natural
em desenvolvimento de sistemas que podemos prover uma representação para
os pacotes de um sistema, bem como a relação existente entre eles. Observe a
figura a seguir.
Figura 5.1 – Exemplo de pacotes relacionados
Na figura acima, é possível vermos a especificação de três pacotes:
Faturamento, Financeiro e Cadastros. Dentro destes, há um conjunto de classes.
Observe que existe um relacionamento entre os pacotes. O relacionamento é
definido em função das relações entre as classes. Isso quer dizer que se existe
uma relação entre Cadastros e Financeiro, certamente alguma classe do primeiro
se relaciona com outra classe dos segundo. Contudo, o relacionamento não está
restrito às classes. Há outros elementos que compõem um sistema (por exemplo,
interfaces) que podem estar relacionadas com outros elementos. Embora não
EADDCC031 – Linguagem de Programação II
74
esteja representado neste exemplo, há relações entre as classes dentro de um
pacote. Observe outro exemplo na figura a seguir.
Figura 5.2 – Exemplo de pacotes relacionados
Podemos perceber, pela figura acima, os relacionamentos entre classes de
diferentes pacotes e entre classes de um mesmo pacote. Precisamos entender,
agora, como é definido o conceito de pacotes em código. Observe a figura a
seguir.
Figura 5.3 – Exemplo de pacotes relacionados
No exemplo da figura acima, definimos dois pacotes: Controle e Classe.
Além disso, definimos a classe Sistema, fora de ambos. Convém ressaltarmos
que embora não pareça, Sistema está dentro de um pacote, que chamamos de
padrão. Pense nos pacotes como se fossem diretórios em um sistema de
EADDCC031 – Linguagem de Programação II
75
arquivos (na verdade, são organizados exatamente como tais). Observe a figura
abaixo e note como fica a organização, no sistema de arquivos do computador,
para o exemplo da figura acima.
Figura 5.4 – Organização de classes e pacotes no computador
Para os elementos da figura 5.3, a organização em computador fica
conforme ilustrado na figura 5.4. Todos os arquivos mostrados nesta figura estão
dentro de uma pasta raiz, chamada SRC. Observe que temos duas pastas na
figura 5.4: Classe e Controle – que são os nossos pacotes. Há, ainda, a classe
sistema (na raiz). De certa forma, podemos dizer que Sistema está dentro de um
pacote padrão. Observe a figura abaixo.
Figura 5.5 – Árvore de arquivos do nosso projeto
Note que, dentro da pasta SRC, temos os nossos pacotes Classe e
Controle, além de um pacote padrão (default package). Esse pacote é a própria
raiz SRC. Na figura 5.4, perceba que ele não é representado como uma pasta. Na
verdade, ele não existe como pacote. Alguns ambientes de desenvolvimento
usam essa representação de pacote “padrão” (como na figura 5.5) para ajudar o
programador na visualização da organização dos arquivos do projeto. Outros,
EADDCC031 – Linguagem de Programação II
76
porém, não fazem referência ao mesmo - embora se considere que a raiz onde o
projeto está inserido seja o “pacote” padrão. Temos que ressaltar que cada
ambiente de desenvolvimento organiza os arquivos de certa forma. No nosso
exemplo, foi criada a pasta SRC. Em outros ambientes, por exemplo, ela não é
criada. Agora que entendemos essa organização de pacotes no sistema de
arquivos, vamos continuar com o exemplo. Dizemos que o uso de pacotes, além
de prover a organização do projeto, permite resolver problemas de conflitos de
nomes. Neste contexto, considere que o nosso projeto conte com duas classes,
ambas de nome Aluno, conforme a figura abaixo.
Figura 5.6 – Classes dentro dos pacotes Controle e Classe
Propositalmente, criamos as duas classes com o mesmo nome, simulando
um cenário que, em geral, ocorre no desenvolvimento de grandes sistemas.
Suponha que a classe sistema precise fazer uso dessas classes. Precisamos
verificar como ela pode utilizar ambas, sem que ocorram problemas. Observe a
figura a seguir.
Figura 5.7 – Implementação da classe Aluno do pacote Controle
A figura acima ilustra a implementação da classe Aluno, do pacote
Controle. Observe a linha 1. Toda vez que alguma classe (ou outro elemento) é
criada dentro de um pacote específico, este é identificado dentro do código. A
EADDCC031 – Linguagem de Programação II
77
linha 1 apenas informa que esta classe Aluno pertence ao pacote Controle. Se
esta classe estivesse no “pacote padrão”, não haveria esta linha. É o que
acontece com a classe sistema, ilustrada abaixo.
Figura 5.8 – Implementação da classe Sistema
Sistema está na pasta raiz do projeto. Embora a mesma seja considerada o
pacote padrão, não é representado como tal. Portanto, não se especifica sua
localização, como feito na linha 1 da figura 5.7. A figura a seguir ilustra a
implementação da classe Aluno, do pacote Classe.
Figura 5.9 – Implementação da classe Aluno do pacote Classe
Os nomes dos pacotes foram definidos aleatoriamente, apenas para título
de exemplificação. Note que as implementações de ambas as classes Aluno são
diferentes, simulando um cenário real. Se as implementações fossem iguais, não
haveria qualquer sentido em se criar as duas classes. Vamos considerar que a
classe Sistema, em um primeiro momento, precise utilizar a classe Aluno de
Controle. Observe a figura a seguir.
Figura 5.10 – Uso da classe Aluno pela classe Sistema
EADDCC031 – Linguagem de Programação II
78
Observe que temos erro de compilação na classe Sistema. A mensagem
emitida informa que o compilador não conhece o símbolo Aluno. De fato, o erro
era esperado. Lembre-se, pela figura 5.3, que Sistema não está no mesmo pacote
de Aluno. O compilador, ao compilar a classe Sistema, verifica se, na mesma
pasta (diretório) onde a mesma se encontra, existe uma classe chamada Aluno.
Se não existir, com é o caso acima, gera-se um erro. Precisamos, então, informar
ao compilador que a classe Sistema está fazendo uso da classe Aluno, do pacote
Controle. Veja a figura abaixo.
Figura 5.11 – Importação de pacote
Observe a linha 1: estamos importando a classe Aluno. Em outras
palavras, estamos dizendo ao compilador que a classe Sistema está fazendo uso
da classe Aluno. Note que não há mais o erro de compilação. Agora, o compilador
entende que o símbolo Aluno, linha 6, se refere à classe Aluno, localizada no
pacote Controle. Toda vez que uma classe precisar utilizar algum elemento
localizado em outro pacote, é necessário fazer uma importação, usando a
palavra-chave import. O uso do import é semelhante à instrução #include da
linguagem C. Suponha que, vem vez de utilizar a classe Aluno de Controle, seja
necessária a classe Aluno de Classe. Observe o que muda em Sistema.
Figura 5.12 – Importação de pacote
EADDCC031 – Linguagem de Programação II
79
Observe a linha 1. Note que, em relação à figura 5.11, apenas alteramos o
nome do pacote de onde a classe Aluno é importada. Agora, o Aluno da linha 6 se
refere à classe Aluno do pacote Classe. Suponha, agora, que seja necessário à
classe Sistema utilizar ambas as classes Aluno, dos pacotes Controle e Classe.
Analise o código da figura a seguir.
Figura 5.13 – Tentativa de importar duas classes Aluno
Suponha que queremos instanciar dois objetos alunos (aluno1 e aluno2,
linhas 7 e 8) – um da classe Aluno, do pacote Controle, e outro da classe Aluno,
do pacote Classe. Ao compilarmos o código acima, temos erro de compilação.
Basicamente, a mensagem emitida quer dizer que o compilador não consegue
distinguir de que classe os símbolos Aluno (linhas 7 e 8) pertencem – ou seja, se
são da classe Aluno, de Controle, ou da classe Aluno, de Classe. O compilador
não sabe qual importação (linhas 1 e 2) usar para definir o tipo Aluno destas
linhas 7 e 8. Temos, portanto, que resolver esse problema. Uma primeira solução
possível seria especificar, nas instruções de instanciação (linhas 7 e 8), de que
tipo são os objetos aluno1 e aluno2. Observe o código da figura 5.14. Observe
que alteramos as linhas 7 e 8, acrescentando os termos Controle e Classe,
respectivamente. Note que o mesmo erro de compilação foi emitido. Logo, nossa
tentativa de solução falhou. Podemos, no entanto, ser mais específicos, alterando
a segunda parte das mesmas instruções, após o símbolo “=”. Observe o código
da figura 5.15 e compare com a figura 5.14.
EADDCC031 – Linguagem de Programação II
80
Figura 5.14 – Primeira tentativa de solução
Figura 5.15 – Segunda tentativa de solução
Note que na figura 5.15, obtivemos o mesmo erro de compilação. Acontece
que fomos bem claros nas instruções das linhas 7 e 8, informando ao compilador
que o objeto aluno1 é do tipo Aluno, do pacote Controle, e que o objeto aluno2 é
do tipo Aluno, do pacote Classe. Se há um conflito nas instruções 1 e 2, vamos
tentar retirar as mesmas e ver o resultado. Observe a figura abaixo.
Figura 5.16 – Terceira tentativa de solução
Note que, agora, não temos mais o erro de compilação. O compilador
consegue entender que o objeto aluno1 é do tipo Aluno (Controle) e que aluno2 é
do tipo Aluno (Classe). Embora não seja feito uso da palavra-chave import, o
EADDCC031 – Linguagem de Programação II
81
compilador entende o caminho Controle.Aluno e Classe.Aluno, diferenciando
ambas as classes Aluno. Se quisermos, podemos fazer uso do import apenas
para uma delas. Veja o código abaixo.
Figura 5.17 – Importação do pacote Controle
Note que o código acima não gera erro de compilação. A linha 1 importa a
classe Aluno do pacote Controle. Na linha 7, esta classe é utilizada na
instanciação do objeto aluno2. O curioso está na linha 6. Note que não há
qualquer especificação do tipo do símbolo Aluno. Contudo, o compilador entende
que este Aluno se refere à classe Aluno, do pacote Controle. Isto porque este
pacote está sendo importado via import. Portanto, se quisermos utilizar qualquer
outra classe Aluno - de qualquer outro pacote que não o Controle -, teremos que
especificar de que pacote ela é. Na linha 7, se não especificássemos que aluno2
é do tipo Classe.Aluno, o compilador entenderia que aluno2 seria do tipo
Controle.Aluno. Toda vez que precisarmos fazer referência à classe Aluno do
pacote Classe, temos que fornecer o caminho completo: nome do pacote, seguido
do operador ponto (.) e o nome da classe. Agora, observe o código da figura
abaixo.
Figura 5.18 – Importação do pacote Classe
EADDCC031 – Linguagem de Programação II
82
Em relação à figura 5.17, o que alteramos na figura 5.18 foi a classe
importada via import. Antes, importávamos a classe do pacote Controle e, agora,
importamos do pacote Classe. Note que, em função dessa alteração, tivemos que
especificar, na linha 6, que o objeto aluno1 se refere à classe Aluno do pacote
Controle. Para a linha 7, note que retiramos a especificação de que o objeto é do
tipo Classe.Aluno.
5.2. Importação Implícita e Explícita
Quando utilizamos a palavra-chave import para importar elementos de um
pacote, podemos fazê-lo de suas formas: explicitamente ou implicitamente.
Embora ambas funcionem, há uma diferença pontual entre elas. Como exemplo,
vamos considerar as bibliotecas da linguagem Java. Java possui várias classes
com operações previamente definidas. Sempre que necessário, os
desenvolvedores podem fazer uso destas classes, importando-as – assim como
fizemos em nossos exemplos nessa unidade. A figura a seguir apresenta algumas
das várias bibliotecas da linguagem Java.
Figura 5.19– Exemplos de bibliotecas Java
EADDCC031 – Linguagem de Programação II
83
Note que elas estão organizadas na forma de pacotes. As bibliotecas são,
de fato, conjuntos de elementos (classes, interfaces, outros pacotes, etc.).
Observe na figura acima que temos um pacote denominado java.util. Vamos
acessar o mesmo e verificar seus elementos internos. A figura a seguir ilustra o
interior desse pacote.
Figura 5.20 – Elementos do pacote UTIL
Observe que, dentro do pacote UTIL, temos classes, interfaces e outros
pacotes, que podem conter mais classes, interfaces ou pacotes. Suponha que
nossa classe Sistema precise utilizar uma das classes específicas do pacote UTIL
para realizar alguma operação. Observe no código abaixo como podemos fazer a
importação.
Figura 5.21 – Componente específico do pacote UTIL
No exemplo da figura acima, estamos importando a classe ArrayList (linha
1). Se necessário, podemos importar outras classes, deste ou de outro pacote.
Observe o código da figura abaixo e perceba as duas importações.
EADDCC031 – Linguagem de Programação II
84
Figura 5.22 – Realizando mais de um import
Note que não temos erro de compilação ao especificar mais de um import.
A questão do erro no nosso exemplo da classe Aluno era o fato de importarmos
uma classe com o mesmo nome, mesmo sendo de pacotes diferentes. Neste
caso, sempre ocorre erro. Se importarmos – no código de uma mesma classe -
duas classes com o mesmo nome, teremos conflito e erro de compilação.
Observe o exemplo a seguir.
Figura 5.23 – Problema com conflito de nomes
Como dizemos, se dois (ou mais) elementos com mesmo nome forem
importados para uma mesma classe, teremos conflito de nomes e erro de
compilação. Já apresentamos a solução para este cenário nessa unidade.
Sabendo dessas questões, vamos continuar a explicação das importações
explícitas e implícitas. Observe a figura abaixo.
EADDCC031 – Linguagem de Programação II
85
Figura 5.23 – Importação implícita
Observe a instrução da linha 1. Note que estamos importando algo do
pacote UTIL. Para os exemplos anteriores, o que há de diferente nessa instrução
é o símbolo “*”. Essa é a chamada importação implícita. Ela, na verdade, importa
tudo (classes, interfaces, outros pacotes, etc.) que existe dentro do pacote UTIL.
Essa importação chama-se implícita, pois não estamos especificando,
explicitamente, que elemento está sendo importado. No caso da explícita, como
ilustrado na figura 5.21, por exemplo, estamos especificando que elemento está
sendo importado (a classe ArrayList). É sempre uma boa pratica o uso da
instrução import de forma explícita ao invés do uso da forma implícita. Essa é uma
boa pratica porque possibilita ao programador determinar, rapidamente, quais
classes foram importadas. Observe que na figura 5.23, não há qualquer
informação dos elementos que estão sendo importados. Pelo “*”, sabemos que
estamos importando tudo, mas não sabemos qual elemento está, de fato, sendo
utilizado na classe.
EADDCC031 – Linguagem de Programação II
86
6. Visibilidade
Se cada membro de uma classe/objeto fosse acessível a qualquer outra
classe/objeto, então o entendimento, a depuração (processo de encontrar e
reduzir defeitos, erros, em um sistema de software) e a manutenção de sistemas
seria uma tarefa quase impossível. O contrato apresentado por classes não seria
de confiança, uma vez que qualquer parte do código poderia acessar diretamente
um atributo e alterá-lo (modificar seu valor). Um dos pontos fortes da Orientação a
Objetos é seu suporte à visibilidade e restrição de acesso a membros (atributos e
métodos) de uma classe – consequentemente, de objetos. De fato, OO possibilita
controlar quem possui acesso aos membros de uma classe. Esse controle é
fundamental para qualquer sistema OO.
6.1. Visibilidade
O mecanismo para controle de acesso em Orientação a Objetos é simples,
sendo realizado por meio dos modificadores de acesso. Precisamos entender,
antes, que todos os membros de uma classe são sempre disponíveis ao código
da própria classe – ou seja, os métodos da classe podem acessar, sem qualquer
restrição, os atributos (ou outros métodos) declarados na mesma, seja para
acessar seus valores ou modificar estes. A questão, então, é controlar o acesso
aos membros da classe por outras classes. Sem o devido controle, o programa
está fadado aos problemas citados no início da unidade. Em Orientação a
Objetos, são definidos quatro níveis de acesso. A tabela abaixo descreve os
mesmos.
Tabela 6.1 – Modificadores de acesso
Todos os membros públicos de uma classe são acessíveis onde quer que o
programa tenha uma referência a um objeto dessa classe. É o nível de acesso
EADDCC031 – Linguagem de Programação II
87
menos restritivo. Se um atributo da classe X é declarado como público, ele pode
ser acessado pelos membros de objetos de outra classe. Por acessar, estamos
considerando a possibilidade de tanto visualizar seu valor quanto modificar o
mesmo. Os membros declarados como privados são acessíveis apenas por
membros pertencentes à própria classe. Assim, um método da classe pode
acessar os atributos privados da mesma, para visualizar seus valores ou para
alterá-los. O mesmo não pode ser realizado por métodos declarados em outras
classes. É o nível de acesso mais restritivo, e que garante maior segurança.
Utilizar acesso protegido provê um nível intermediário de restrição – entre os
níveis privado e público. É um modificador utilizado muito em hierarquias de
classes. Membros protegidos de uma superclasse podem ser acessados por
membros dessa superclasse, por membros de suas subclasses e por membros de
outras classes no mesmo pacote. Neste momento, não se preocupe com os
termos superclasse, subclasse e hierarquia de classes. Em unidades posteriores,
discutiremos os mesmos. Por fim, os membros de uma classe declarados como
padrão possuem nível de acesso similar ao protegido – isto é, podem ser
acessados por membros de outras classes no mesmo pacote. Para este
modificador, não se aplica o conceito de hierarquia de classes. A figura a seguir
ilustra o nível de acesso público.
Figura 6.1 – Classe Aluno
Observe o código da figura acima. Note que ambos os atributos da classe,
matrícula e nome (linhas 3 e 4), estão definidos como privados. Logo, apenas
membros da própria classe podem acessar os mesmos. Na linha 6, definimos um
método para alterar o valor do atributo matricula. Note que na linha 7, estamos
EADDCC031 – Linguagem de Programação II
88
acessando o mesmo através da referência this. Esse acesso é permitido, pois
embora o atributo esteja definido como privado, ele pertence à própria classe. O
acesso na linha 11 também é possível, pela mesma razão. Agora, observe se um
membro de outra classe tentar realizar este acesso.
Figura 6.2 – Acesso a atributo privado de Aluno
O código da figura acima especifica a classe Sistema. Dentro da mesma,
definimos o método main, no qual criamos um objeto da classe Aluno (linha 5) e
tentamos, com o mesmo, acessar seu atributo matricula (linha 6), atribuindo o
valor 200222015 para o mesmo. Observe que o compilador emitiu uma
mensagem (parte final da figura), dizendo que o atributo matricula possui acesso
privado em Aluno. Vamos discutir o que aconteceu. Embora matricula seja, de
fato, um atributo do objeto aluno, ele possui acesso privado. Observe que esta
tentativa de acesso ao atributo está sendo realizado da classe Sistema. Podemos
dizer, então, que a classe Sistema está tentando acessar, através de um objeto
Aluno que ela criou em seu método main, o atributo matricula deste objeto. Isso
não quer dizer que não possamos, de alguma forma, acessar este (ou qualquer
outro atributo privado de aluno). Em OO, contamos com meios para este tipo de
procedimento, o que discutiremos em unidade posterior. Observe, pelo código
abaixo, o que acontece quando alteramos o modificador de acesso do atributo
matricula (linha 4, figura 6.1) de privado para público.
EADDCC031 – Linguagem de Programação II
89
Figura 6.3 – Acesso a atributo público de Aluno
Note que não temos mais erro de compilação. E nem deveria haver, pois
dizemos que matricula teve o modificador de acesso alterado para público. Esse é
um grande problema de segurança, pois uma classe (Sistema) consegue acessar,
sem qualquer restrição, o atributo de outra classe (Aluno) e alterar seu valor. Há
um conceito em OO, chamado de Encapsulamento, que deve sempre ser utilizado
para se evitar este tipo de situação. Discutiremos sobre o mesmo em unidade
posterior. Agora, vamos analisar a tentativa de acesso a um método declarado
como privado. Observe o código abaixo.
Figura 6.4 – Exemplo de método privado
No código da figura acima, declaramos todos os membros da classe Aluno
como privados. Em teoria, nenhum deles poderá ser acessado por membros de
outras classes. Observe o código abaixo, o qual tenta realizar acesso ao método
privado de Aluno.
EADDCC031 – Linguagem de Programação II
90
Figura 6.5 – Acesso a método privado de Aluno
Observe que obtivemos o mesmo erro descrito na figura 6.2, mas, agora,
relativo ao método. Na parte inferior da figura 6.5, o compilador emite um aviso
dizendo que o método possui acesso privado. Portanto, não podemos - da classe
Sistema - invocar este método. É certo que é o objeto aluno quem está, de fato, o
invocando (linha 6). Mas quem está solicitando isto é a classe Sistema, através de
seu método main. Observe a figura abaixo.
Figura 6.6 – Acesso a método privado
Observe a linha 10 do código da figura 6.6. Nesta linha, definimos um
método chamado teste, cujo objetivo é apenas invocar o método privado definido
na linha 6. Observe que não há erro neste código, apesar do método de teste
estar acessando um método privado. Como o acesso está sendo feito dentro da
própria classe, não há erro de compilação. Membros privados da classe são
acessíveis por (e somente por) membros da própria classe. No código abaixo,
note o que acontece quando alteramos o método alterarMatricula (linha 6, figura
6.6) de privado para público.
EADDCC031 – Linguagem de Programação II
91
Figura 6.7 – Acesso a método público
Note que não há erro de compilação. Sendo o método alterarMatricula
público, seu acesso é liberado para qualquer classe externa. Vejamos, agora,
exemplos do modificador padrão. O código abaixo ilustra a definição de um
atributo com este nível de acesso. Observe que, para o caso do modificador
padrão, basta não especificarmos nada.
Figura 6.8 – Acesso a método privado de Aluno
O código acima especifica a classe Aluno, com apenas um atributo, nome
(linha 3). Note que nos exemplos anteriores, imediatamente antes do tipo do
atributo, definimos sua visibilidade (privativo ou público). No caso do modificador
de acesso padrão, como dito, basta não definir nenhum outro modificador – ou
seja, basta especificar o tipo do atributo e seu nome. Fazendo isto, o compilador
entende que a visibilidade do atributo é padrão (default). O mesmo vale para os
métodos. Vamos analisar o caso deste modificador. Observe o código da figura
abaixo.
EADDCC031 – Linguagem de Programação II
92
Figura 6.9 – Acesso a método privado de Aluno
No código acima, definimos todos os membros da classe Aluno como
padrão. Agora, considere a classe Sistema da figura 6.3. Considere, ainda, que
ambas as classes Aluno e Sistema estão dentro do mesmo pacote. Note que a
linha 6 da classe Sistema (figura 6.3) realiza um acesso direto ao atributo
matricula do objeto aluno. Vimos que não há problemas de compilação quando
este atributo está definido como público. Mas, obtemos erro quando ele é definido
como privado. Veja, na figura a seguir, o que acontece quando compilamos
ambas as classes.
Figura 6.10 – Acesso a atributo default de Aluno
Como esperávamos, não houve erro de compilação – uma vez que as
classes estão no mesmo pacote. Logo, neste caso, membros default de classes
podem ser acessados por outras classes. Vejamos o que ocorre quando estas
classes são inseridas em pacotes diferentes. Observe a figura abaixo.
EADDCC031 – Linguagem de Programação II
93
Figura 6.11 – Acesso a atributo default de Aluno
Na figura acima, estamos ilustrando o fato das duas classes, Aluno e
Sistema, estarem em pacotes diferentes. O código da classe Aluno é o mesmo
apresentados na figura 6.9. Apenas uma alteração foi necessária no código da
classe Sistema. A figura abaixo apresenta o novo código desta classe.
Figura 6.12 – Alteração na classe Sistema
Observe que no código cima, inserimos uma instrução (linha 2) para
importar a classe Aluno (que está em outro pacote). Utilizamos a palavra-chave
import, seguida do nome do pacote e do nome da classe de interesse. Sem esta
instrução, a classe Sistema não consegue identificar o tipo Aluno, definido na
linha 8. A instrução da linha 1 é inserida automaticamente ao criar a classe. O
mesmo vale para a classe Aluno (embora no código desta classe, esteja definido
que ela pertence ao pacote Classe – e não Controle, como é o caso da classe
Sistema). Vejamos, então, o que ocorre quando compilamos ambas as classes.
EADDCC031 – Linguagem de Programação II
94
Figura 6.13 – Acesso a membro default de outro pacote
Observe a mensagem de erro emitida pelo compilador, dizendo que o
atributo matrícula não é público na classe Aluno (pacote Classe) e que, por isso,
não pode ser acessado por classe de pacote externo. Em outras palavras, há erro
de compilação porque as classes não estão dentro do mesmo pacote. Se
alterarmos a visibilidade do atributo matrícula (linha 4, figura 6.9) de padrão para
privado, teremos o mesmo erro. Contudo, o erro será em função da visibilidade
estar como privativa e não pelo fato das classes estarem em pacotes separados.
Independente de estarem ou não em um mesmo pacote, classes (e objetos) não
conseguem acessar qualquer membro privado de outra classe. Observe o código
da figura abaixo. Para este caso, como dito, alteramos a visibilidade do atributo
matrícula (linha 4, figura 6.9) para private. Note o erro emitido pelo compilador,
avisando que matrícula possui visibilidade privativa em Aluno.
Figura 6.14 – Acesso a membro privado de outro pacote
EADDCC031 – Linguagem de Programação II
95
Vamos considerar, agora, que o atributo matricula (linha 4, figura 6.9) seja
definido como público. Na teoria, não há erro de compilação, uma vez que
membros públicos de uma classe podem ser acessados por qualquer outra
classe, desde que esta última faça uma referência à primeira. O código abaixo
ilustra o resultado da compilação após esta alteração.
Figura 6.15 – Acesso a membro público de outro pacote
Considere, agora, que o mesmo atributo matrícula (linha 4, figura 6.9)
tenha sua visibilidade alterada para protegida. A figura abaixo ilustra o resultado
da compilação da classe Sistema.
Figura 6.16 – Acesso a membro público de outro pacote
Observe que há erro de compilação. A mensagem de erro informa que o
atributo matricula tem visibilidade protegida. O erro era esperado, visto que as
classes continuam em pacotes separados. Lembre-se que membros protegidos
de classes só podem ser acessados por outras classes do mesmo pacote – além
EADDCC031 – Linguagem de Programação II
96
do caso das hierarquias (que não veremos por agora). Se as classes foram
inseridas no mesmo pacote, teremos o mesmo resultado obtido na figura 6.10 -
não haverá erro de compilação. Quando estudarmos hierarquias de classes,
voltaremos a ver sobre a visibilidade protegida.
6.2. Métodos Públicos e Privados
Vimos que atributos de classe devem ser declarados como privados e,
caso seja necessário acesso aos mesmos, devemos fazê-lo via da interface
pública da classe (métodos públicos). Os métodos devem, em geral, ser públicos.
A razão é que métodos correspondem à interface de comunicação entre
diferentes classes (consequentemente, objetos). Se declararmos métodos como
privados, eles não serão vistos por outras classes e, consequentemente, não
poderão ser invocados por elas. Os métodos públicos oferecem serviços aos
usuários de uma classe. Contudo, há casos em que podemos e precisamos
definir métodos como sendo privados. É o caso de quando precisamos dividir uma
tarefa maior em várias outras menores dentro da própria classe. Certamente,
estas tarefas menores não precisam ser visíveis aos usuários da classe, apenas a
tarefa maior. Neste caso, as tarefas menores são declaradas como privativas,
pois são de uso exclusivo da própria classe. Outra boa razão para termos um
método privado é uma tarefa que é necessária (como subtarefa) por vários
métodos de uma classe. Em vez de escrevermos o código desta subtarefa várias
vezes (repetição de código), podemos escrevê-lo uma única vez em um único
método privado. Assim, toda vez que outro método da classe precisar do mesmo,
basta invocá-lo. Por ser privado, ele será visto apenas pelos pela própria classe
em que está declarado – as demais classes do sistema não terão visibilidade ao
mesmo.
Fique Atento Métodos privados são criados para que possam ser utilizados apenas pela classe onde são declarados. Se precisar que um método não possa ser visto por classes externas, declare-o com o modificador de acesso private. Se o método é um serviço oferecido pela classe para outras classes, ele deve ser declarado como public – caso contrário, não poderá ser acessado. Atributos devem, sempre, ser declarados como privados. Isso permite que o objeto
mantenha mais controle sobre seu estado. Se o acesso para um atributo privado for realizado através de métodos, o objeto terá a capacidade de assegurar que o atributo nunca será configurado com um valor inconsistente pra o objeto. Esse nível de integridade não é possível se os atributos foram definidos como públicos.
EADDCC031 – Linguagem de Programação II
97
A figura 6.17 ilustra um exemplo de uso de método privado dentro de uma
classe. Para esta figura, estamos considerando o caso da classe conter um
método para efetuar o pagamento (de uma compra, por exemplo) via cartão de
crédito. Para que o pagamento seja possível, é necessário, antes, verificar se o
cartão está liberado para ser utilizado (se possui limite e se é válido). Essa
verificação é feita junto à operadora do cartão. Observe os dois métodos, nas
linhas 3 e 17. Para simplificação, não os implementamos. O método da linha 3 é
público e, portanto, pode ser requisitado por usuários da classe. Quando ele é
invocado, deve-se passar o número do cartão. Na linha 4, efetuamos uma
chamada a um método privado da classe, passando o número do cartão como
parâmetro. Este método (linha 17) recebe o número do cartão e verifica, junto à
operadora, se o mesmo é válido e se possui limite disponível para a compra. Em
caso positivo, ele é liberado e o método retorna um valor booleano true (cartão
liberado). Em caso negativo, o valor retornado é false (cartão não liberado). A
execução volta para linha 4. Recebendo valor true, segue-se para a
implementação dentro (linha 5). Em caso de valor false, segue-se para a linha 10.
EADDCC031 – Linguagem de Programação II
98
Figura 6.17 – Exemplo de uso de método privado
Observe que não há necessidade (e talvez seja inseguro) deixar que o
método da linha 17 seja acessível aos usuários da classe (externos). Este método
realiza uma operação complementar para o método da linha 3 e, portanto, deve
ser privado. Neste caso, a justificativa para criar este método (linha 17) poderia
ser que o método da linha 3 ficaria demasiadamente extenso se todo o código do
método 17 fosse inserido no mesmo.
EADDCC031 – Linguagem de Programação II
99
7. Encapsulamento
No mundo real, um objeto A pode interagir com outro objeto B sem
qualquer conhecimento do funcionamento interno de B. Uma pessoa, por
exemplo, geralmente utiliza um aparelho de TV sem saber, efetivamente, como é
a estrutura interna da TV ou com seus mecanismos são ativados. Para utilizá-la,
basta saber realizar algumas operações básicas, como ligar, desligar, mudar de
um canal para outro, regular do volume, etc. Como essas operações produzem
resultados satisfatórios, não interessa ao usuário entender como a TV funciona
internamente. O encapsulamento consiste na separação dos aspectos externos
de um objeto, acessíveis por outros objetos, de seus detalhes internos de
implementação, que ficam ocultos dos demais objetos. Encapsulamento está
relacionado ao propósito de ocultamento de informações, fator essencial para
qualquer sistema OO.
7.1. Encapsulamento
A interface de comunicação de um objeto deve ser definida de tal forma a
revelar o menos possível sobre seu funcionamento interno. Um objeto deve ter
conhecimento das operações que podem ser solicitadas, mas precisa estar ciente
do que as operações fazem, e não como elas fazem (ou seja, como estão
implementadas). Em linguagens OO, o interior de uma classe (sua
implementação) deve ser ocultado de outras classes. Há dois aspectos para isto:
não se deve ter permissão para conhecer detalhes internos das classes e não
deve ser necessário conhecer esses detalhes. O segundo princípio – necessidade
de conhecer – possui relação com o conceito de modularização. Se fosse
necessário conhecer o interior de todas as classes que precisamos utilizar, jamais
terminaríamos a implementação de grandes sistemas. Lembre-se que sistemas
grandes e complexos possuem várias classes. O primeiro princípio – não ter
permissão para conhecer – é diferente. Também possui relação com a
modularização, mas em um contexto diferente. A linguagem de programação não
permite acesso à seção privativa de uma classe. Isso assegura às classes não
EADDCC031 – Linguagem de Programação II
100
depender, exatamente, de como outras classes são implementadas. Isso é muito
importante para o trabalho de manutenção – isto é, alteração ou extensão da
implementação de uma classe visando melhorias ou correção de erros. De
maneira geral, a alteração da implementação de uma classe não deveria provocar
alterações em outras classes. Se uma classe X necessitar conhecer os detalhes
internos de outra classe Y – em outras palavras, se ela depender destes detalhes
internos – uma alteração na classe Y pode gerar problemas sérios para X, a ponto
de ela não mais funcionar corretamente. Logo, conhecer detalhes internos de
implementação das classes é perigoso, pois facilita a criação de outras classes
que dependam dessas implementações específicas, e não dos resultados que
elas podem gerar. Suponha uma a classe X dependa de um método de
verificação de CPF, implementado na classe Y. Considere, ainda, que X dependa,
especificamente, da forma como o método de Y esteja implementado. Imagine o
que pode ocorrer caso a forma como é feita a verificação de CPF precise ser
alterada por Y. Provavelmente, X não funcionaria da forma como deveria. Este é
um exemplo dos riscos de se conhecer detalhes de implementação das classes.
Por esta razão, o ocultamento de informações é importante no desenvolvimento
de sistemas OO. O intuito é garantir estabilidade aos sistemas computacionais.
Encapsulamento também é utilizado para facilitar a reutilização de código. Um
encapsulamento bem realizado pode servir de base para a localização de
decisões de projetos que necessitem ser alteradas – um exemplo seria uma
operação implementada de maneira ineficiente e que, agora, precise de um
algoritmo mais eficiente. Se a operação está encapsulada, apenas o objeto que a
define precisará ser modificado. Os demais que, por ventura, dependam dele, não
sofrerão qualquer alteração.
7.2. Utilizando Encapsulamento
A diretriz de encapsulamento sugere que somente as informações sobre o
que uma classe pode fazer devem ser visíveis externamente, não como ela é.
Isso representa uma grande vantagem: se nenhuma outra classe sabe como
nossas informações estão armazenadas e nem como nossas operações estão
implementadas, podemos facilmente alterar tanto a forma de armazenamento
EADDCC031 – Linguagem de Programação II
101
quanto a implementação sem “quebrar” outras classes. Sobre as operações,
podemos impor essa separação entre o que e como evitando criar métodos que
independam da forma como outro método é implementado. Se apenas soubermos
o que um método faz (e não como ele faz), ficaremos dependentes apenas da
execução do método. Assim, se ele for alterado futuramente, mas realizando o
que sempre realizou (retornando alguma informação, imprimindo algo, etc.), não
teremos problemas. No caso das informações em si, podemos impor a separação
tornando os atributos das classes privados e utilizando métodos de acesso para
obter seus valores e métodos de modificação para alterar os mesmos. Para as
operações, o cuidado com o encapsulamento depende unicamente do
programador. É ele quem deve programar suas classes de forma que elas não
dependam das implementações de outra. Para as informações, embora ainda
dependa do programador, o uso do encapsulamento é mais direto: um atributo
jamais deve ser acessado/modificado diretamente – ou seja, ele nunca deve ser
marcado como público. Os atributos sempre devem ser definidos como privativos
e o acesso/modificação deve ser realizado exclusivamente por meio de métodos.
A figura 7.1 ilustra uma representação para este cenário. Observe que os
métodos ficam “protegendo” os atributos. Não se consegue atingir qualquer
atributo sem seja através de algum dos métodos da classe.
Figura 7.1 – Representação do encapsulamento de atributos
O potencial de fazer alterações no código, sem interromper o código de
outras classes que o estiverem usando, é um benefício essencial do conceito de
encapsulamento. Percebemos, então, que encapsulamento está intimamente
relacionado com o conceito de visibilidade.
EADDCC031 – Linguagem de Programação II
102
7.3. Métodos de Acesso e Modificação
Até agora, entendemos o que é o encapsulamento e como podemos obtê-
lo ao implementar nosso código. Precisamos analisar isso na prática. Iremos focar
na questão da modificação e acesso aos atributos, já que para os métodos, não
existe um padrão de implementação para torna-los encapsulados. O que há,
neste sentido, é a necessidade de se implementar de forma a não depender de
outras implementações (ou seja, de como ela é feita, codificada). No caso dos
atributos, garantimos encapsulamento ao impedir que sejam acessados
diretamente. Em linguagem Java, existem métodos específicos para possibilitar o
acesso aos atributos de uma classe: há métodos para obter a informação
armazenada no mesmo e métodos para alterar essa informação. Note que o
intuito do encapsulamento não é impedir o acesso, mas controlar o mesmo.
Lembre-se que sistemas OO são fundamentados nas interações entre objetos, e
que estas interações dependem das operações (métodos) e dados (atributos)
oferecidos pelos próprios objetos. Assim, impedir acesso aos dados é impedir a
interação e o funcionamento dos sistemas. Observe a figura abaixo.
Figura 7.2 – Definição de atributo público
No código da figura acima, definimos a classe Aluno com dois atributos
públicos, matrícula e nome. Por serem públicos, sabemos que podem ser
acessados, sem qualquer restrição, por outras classes. Observe, agora, o código
da figura abaixo.
Figura 7.3 – Acesso a membros públicos da classe Aluno
EADDCC031 – Linguagem de Programação II
103
Note que o código da classe Sistema está criando um objeto de Aluno
(linha 4) e acessando seus atributos, atribuindo valores específicos aos mesmos
(linhas 5 e 6). Note que o acesso está sendo realizado de forma direta – ou seja,
especificando o nome do objeto, seguido da notação de ponto e do membro que
queremos acessar: nome na linha 5 e matricula na linha 6. Isso é acesso direto.
Portanto, os atributos não estão encapsulados: estão definidos como públicos
(linhas 3 e 4 – figura 7.2) e permitem acesso direto. O objeto que criamos no
código acima possui nome Fernando e matrícula 200222015. Se existe acesso
direto, na impede de qualquer outra classe, que tenha referência ao objeto aluno,
de alterar os valores de matrícula ou nome de aluno. Analise o código abaixo, na
definimos a classe Relatorio. Dentro da mesma, especificamos um método para
impressão de dados (linha 3). Este método recebe um aluno como parâmetro e
imprime seu nome (linha 4) e sua matrícula (linha 5). Até agora, não há qualquer
problema nisto.
Figura 7.4 – Alteração indevida de atributo do objeto aluno
A questão é a instrução da linha 7. Observe que, propositalmente, estamos
alterando o valor do nome do aluno, o que não poderia ocorrer. É um acesso
direto ao atributo nome de aluno e, por isto, não existe qualquer restrição,
qualquer verificação antes de o nome ser alterado. Isso é muito ruim. Se o objeto
aluno da classe Sistema (figura 7.3) fosse passado por parâmetro para o método
imprimir da classe Sistema, o aluno – de nome Fernando – teria ser nome
alterado para Maria – o que é uma inconsistência. Esse é o problema da falta de
encapsulamento. Precisamos, então, fazer com que a classe Aluno esteja
encapsulada. Para começar, vamos definir seus atributos nome e matrícula como
privados. Em seguida, temos que definir métodos para acesso aos mesmos.
Observe a figura abaixo.
EADDCC031 – Linguagem de Programação II
104
Figura 7.5 – Definição de atributo público
O código da figura acima é uma extensão do código da classe Aluno (figura
7.2). Continuamos com os atributos nome e matrícula, mas, agora, privados. A
maior diferença foi a especificação de métodos para acesso aos mesmos. Em
Java, métodos para acesso (leitura) aos atributos de uma classe, objetivando
captura de seus valores, são denominados GET. Já os métodos para alteração de
valores são denominados SET. Para cada atributo de uma classe, então, devem
ser definidos um método set e um método get – isso se o programador definir que
o atributo deve permitir tanto acesso quanto alteração por membros externos.
Observe a linha 6 do código acima. Por padrão, um método de captura de valor
possui o como tipo de retorno o mesmo tipo definido para o atributo, seguido do
termo get e do nome do atributo. Como este método é criado para obter valor de
atributo, ele apenas retorna o valor do mesmo (linha 7). Note o método da linha
10. Este é um método para alteração de valor de atributo. Por padrão, ele possui
tipo de retorno void (já que não precisa retornar nada, apenas alterar), seguido do
termo set e do nome do atributo. Além disso, se iremos alterar o valor do atributo,
temos que passar o nome valor por parâmetro - é o que está sendo realizado
entre parênteses na mesma linha 10. Como o atributo nome é do tipo String,
devemos passar um valor que também o seja. A linha 11 apenas atualiza o valor
EADDCC031 – Linguagem de Programação II
105
do atributo nome com o valor recebido por parâmetro. Observe que para o
atributo matricula, foram, igualmente, criados métodos get (linha 14) e set (linha
18). Podemos dizer, então, que os atributos da classe Aluno estão todos
encapsulados – ou seja, o acesso a eles não mais é permitido diretamente, mas
somente por meio da interface pública (dos métodos) da classe. Lembre-se que
métodos públicos podem ser acessados por qualquer outra classe do sistema.
Logo, métodos públicos correspondem à forma de ligação entre classes - são
serviços oferecidos por classes, que podem ser solicitados por outras classes.
Toda vez que uma classe X quiser acessar o atributo matrícula, seja para obter
seu valor ou alterá-lo, deverá fazer isto através dos métodos públicos de Aluno.
Note que se não haver métodos para acesso, o atributo jamais poderá ser
acessado – uma vez que ele está definido como privado. Vamos analisar como
fica a classe Sistema (figura 7.3) a partir da nova implementação da classe Aluno.
Antes, observe a figura abaixo.
Figura 7.6 – Erro ao tentar acessar atributo privado de Aluno
Como era de se esperar, Sistema não consegue mais acessar o atributo
nome de forma direta. Note o erro emitido pelo compilador, dizendo que nome
possui acesso privado em Aluno. Se quisermos acessar este atributo, passando o
valor Fernando para o mesmo, deveremos fazê-lo através dos métodos de Aluno,
se houver. No nosso caso, há. Observe a figura abaixo e veja a alteração do
código de Sistema.
EADDCC031 – Linguagem de Programação II
106
Figura 7.7 – Acesso a atributos de Aluno via métodos públicos
Note, pelo código acima, que o acesso aos atributos nome e matrícula é
realizado via os métodos públicos setNome e setMatricula (linhas 5 e 6). Após a
execução, o objeto aluno é criado, com os valores Fernando e 200222015,
respectivamente, para nome e matricula. Se quisermos verificar se estes valores
estão, de fato, inseridos em aluno, podemos utilizar os métodos de acesso get.
Observe a figura abaixo.
Figura 7.8 – Uso de métodos get de Aluno
No código acima, as linhas 8 e 9 imprimem os valores obtidos através dos
métodos getNome e getMatricula. Observe que ambos os métodos retornam um
string, que é impressa através da instrução de impressão de Java. Vamos
verificar o resultado da execução desta classe Sistema. Observe a figura abaixo.
Figura 7.9 – Resultado da execução
EADDCC031 – Linguagem de Programação II
107
A figura acima apresenta o resultado da execução da classe Sistema. Note
que tanto o nome quanto a matrícula foram impressas. Logo, os métodos
getNome e getMatricula executaram sem problemas. Suponha que seja
necessário alterar o valor da matrícula do aluno Fernando, de 200222015 para
200222001. Observe o código abaixo.
Figura 7.10 – Alteração de matricula de aluno
No código acima, em relação ao código da figura 7.8, apenas
acrescentamos as linhas 11, 13 e 14. Na linha 11, inserimos uma instrução para
alterar o valor do atributo matricula. As linhas 13 e 14 voltam a imprimir o nome e
a matricula do objeto aluno, para compararmos com os valores impressos pelas
linhas 8 e 9. Observe, abaixo, o resultado da execução desta classe.
Figura 7.11 – Resultado da execução após alteração de Sistema
As linhas 7 e 8 (figura 7.10) imprimem o nome Fernando e a matrícula
200222015. Já as linhas 13 e 14 imprimem o mesmo nome Fernando, mas um
novo valor para matrícula: 200222001. Note que, de fato, o valor de matrícula foi
alterado, conforme especificado na linha 11 da figura 7.8. Perceba, portanto, que
EADDCC031 – Linguagem de Programação II
108
os atributos de uma classe devem ser acessíveis somente através de métodos
públicos definidos pela própria classe. O que precisamos entender, agora, é: que
real vantagem existe ao se criar métodos set e get para os atributos das classes.
Observe que, na figura 7.8, embora não tenha sido feito um acesso direto, o
atributo matricula foi alterado. Então, podemos nos perguntar que diferença existe
entre acessar diretamente e acessar através de um método público. Para
começar, o acesso (para obter valor ou modifica-lo) só é possível se houver
métodos para isto. Logo, temos como prover segurança, pois se o desenvolvedor
entender que um atributo não pode ser alterado por membros externos, ele
simplesmente não irá implementar o método set para o mesmo. Da mesma forma,
se não for interessante permitir que o valor de um atributo seja acessado por
membros externos, basta não implementar o método get. Cabe ao programador,
com base nas necessidades do sistema, entender quais atributos terão ou não
métodos de modificação e acesso. Segundo, supondo que seja necessário
implementar tanto get quanto set - neste caso, podemos até achar que não existe
diferença, mas existe. Perceba pelo código da figura 7.4 (linha 7), que o acesso
direto não impõem qualquer restrição para a alteração – ou seja, bastou atribuir
um valor e a alteração é executada, pois o atributo matricula estava definido como
sendo público. Agora, considere que o mesmo atributo seja privado e que existam
métodos set e get para o mesmo, como ilustrado na figura 7.5. Note que a
alteração de matricula só pode ser executava através de um método set. Observe
a figura abaixo.
Figura 7.12 – Resultado da execução após alteração de Sistema
EADDCC031 – Linguagem de Programação II
109
A chave essencial do código acima está no método definido na linha 10.
Observe que este set não altera, simplesmente, o valor do atributo matricula.
Antes de executar a alteração, existe um código (não implementação – ilustrado
como comentário) para verificar se a mesma é possível. Em outras palavras, um
membro externo solicita a alteração do valor do atributo matrícula para um dado
objeto, mas antes de realizar essa alteração, o método set analise se ela, por
exemplo, não causaria inconsistências. O código entre as linhas 11 e 14 pode ser
definido conforme a necessidade do programador – que pode exigir uma série de
verificações antes de autorizar a mudança de valor de matricula. No caso do
acesso direto, não se realiza tais verificações, apenas altera-se o valor. Logo, há,
sim, muito sentido em se especificar get e seu em vez de possibilitar acesso
direto. Mesmo que, em princípio, o programador acredite que não haja
necessidade real de validação de dados, um bom projeto OO sempre preconiza o
planejamento – visando futuras modificações no código. Pode ser que, em
mudanças futuras, seja importante fazer alguma verificação antes de permitir
acesso e modificação de atributo. Para ficar seguro, é sempre viável exigir código
para capturar e modificar atributos, em vez de permitir acesso direto aos mesmos.
Fique Atento Cuidado com códigos que pareçam estar encapsulados, mas que, de fato, não estão. Em, primeiro lugar, o que define encapsulamento de atributos é o fato de estes estarem marcados como privados e não a simples presença de métodos get e set. Assim, de nada adianta a presença destes métodos se os atributos estiverem definidos como públicos. Sendo públicos, não há qualquer obrigação de acessá-los através dos métodos – podendo o acesso pode ser realizado diretamente.
Precisamos entender, então, que o encapsulamento dos atributos depende
de não se permitir o acesso direto aos mesmos. Assim, qualquer aplicação que
tente acessá-los, deverá fazê-lo mediantes métodos disponibilizados pela classe
em que se encontram. A figura abaixo ilustra esse cenário.
Figura 7.13 – Encapsulamento de atributos
EADDCC031 – Linguagem de Programação II
110
Apêndice A: Introdução à Tecnologia Java Java é uma linguagem de programação desenvolvida pela Sun
Microsystems e lançada em versão beta em 1995. Seu desenvolvimento foi
iniciado em 1991, visando o mercado de bens eletrônicos de consumo. Por esta
razão, foi projetada desde o início para ser independente de hardware, já que as
características arquiteturais dos equipamentos variam amplamente neste nicho de
desenvolvimento. Outro objetivo estabelecido desde sua concepção foi o de ser
uma linguagem segura. Segura tanto no sentido de evitar falhas comuns que os
programadores costumam cometer durante o desenvolvimento, como no sentido
de evitar ataques externos.
Estas características despertaram o interesse para utilização de Java em
outro ambiente que também necessitava de uma linguagem com este perfil: a
Internet. A Internet, também, é um ambiente constituído por equipamentos de
diferentes arquiteturas e necessita de linguagens que permitam a construção de
aplicativos seguros. Embora estas características possam ser encontradas em
outras linguagens, Java alcançou enorme sucesso entre os programadores. A
sintaxe semelhante às linguagens C e C++, sua filosofia de implementação, a
independência de plataforma, etc. são apenas algumas características que a
tornaram um das linguagens mais difundidas e utilizadas para desenvolvimento
de sistemas computacionais.
A.1. Máquina Virtual
Em uma linguagem de programação como C, dita ser linguagem
compilada, temos a seguinte figura quando vamos compilar um programa.
Figura A.1 – Processo de compilação
O código fonte (em programa C, por exemplo) é compilado para uma
plataforma e um sistema operacional específico. Não raro, o próprio código fonte
é desenvolvido visando uma única plataforma. O código executável (binário)
resultante será executado pelo sistema operacional e, por esta razão, é essencial
EADDCC031 – Linguagem de Programação II
111
que ele saiba “conversar” com o sistema em questão. Logo, temos um código
executável para cada sistema. Isso significa que se o programa for utilizado em
um sistema Windows, devemos compilar o mesmo para este sistema em
específico. Se seu uso será em um sistema Linux, outra compilação será
necessária. Em outras palavras, haverá uma compilação para cada tipo de
sistema. Além disso, na maioria das vezes, uma aplicação faz uso das bibliotecas
do sistema operacional (por exemplo, as bibliotecas de interface gráfica).
Acontece que as bibliotecas do Windows são diferentes daquelas especificadas
para Linux. Logo, o programador acaba tendo que reescrever o mesmo pedaço
de código da aplicação para diferentes sistemas operacionais, uma vez que eles
não são compatíveis.
Figura A.2 – Compilação para plataformas diferentes
Isso limita o desenvolvimento, além de torna-lo custoso. Já a linguagem
Java se utiliza do conceito de máquina virtual. Neste contexto, verifica-se a
existência de uma camada extra entre o sistema operacional e a aplicação, a qual
é responsável por traduzir o que a aplicação deseja fazer para as respectivas
chamadas do sistema operacional onde ela está rodando no momento.
Figura A.3 – Máquina virtual Java
Com a utilização de uma máquina virtual, ganhamos independência de
sistema operacional. Ou, melhor ainda, independência de plataforma em geral:
não é preciso se preocupar em qual sistema operacional a aplicação irá rodar,
nem em que tipo de máquina, configurações etc.
EADDCC031 – Linguagem de Programação II
112
Podemos dizer, por analogia, que uma máquina virtual é como um
computador, uma vez que é responsável por gerenciar memória, pilha de
execução, etc. A aplicação roda sem nenhum envolvimento com o sistema
operacional - sempre conversando apenas com a Java Virtual Machine (JVM).
Essa característica é interessante: como tudo passa pela JVM, ela pode tirar
métricas, decidir onde é melhor alocar a memória, entre outros. Uma JVM isola,
totalmente, a aplicação do sistema operacional. Se uma JVM termina
abruptamente, só as aplicações que estavam rodando nela irão terminar: isso não
afetará outras JVM que estejam rodando no mesmo computador, nem afetará o
sistema operacional.
A.2. Processo de Compilação e Interpretação
Em Java, o código fonte continua passando pelo processo de compilação.
Contudo, diferente de outras linguagens, o resultado deste processo não é um
código binário executável pelo SO, mas o que chamamos de “bytecode” (código
binário gerado pelo compilador Java). Bytecode é um formato de código
intermediário entre o código fonte (texto que o programador consegue manipular)
e o código de máquina (que o SO consegue executar). O curioso é perceber que
os bytecodes podem ser executados em diferentes SO, desde que haja uma JVM
instalada nestes SO. A JMV instalada em um SO irá receber os bytecodes e
traduzir os mesmos para serem usados naquele SO. Esta é a razão pela qual
Java é independente de plataforma. A figura abaixo ilustra o processo de
compilação e tradução (interpretação) relacionada à tecnologia Java.
Figura A.4 – Processo de compilação e interpretação
Resumidamente, o código fonte (escrito em Java) é compilado, gerando os
bytecodes – que, por sua vez, são entregues às maquinas virtuais instaladas nos
diferentes SO nos quais se deseja que o programa execute. Se precisarmos
executar os mesmos bytecodes em outro SO, basta instalar nele a JVM. Não é
EADDCC031 – Linguagem de Programação II
113
necessário compilar novamente o código fonte. Precisamos analisar, na prática,
como é este processo. Antes disso, entenda que existem dois processos
envolvidos para a execução de programas em Java: compilação e execução. A
compilação gera os arquivos com extensão class (bytecodes) e a execução,
realizada pela JVM, interpreta esses arquivos para instruções que são inteligíveis
para a máquina (SO). Em Java, para cada classe (unidade de programação
Java), é gerado um arquivo class. No ambiente de execução, alguns arquivos
class são utilizados pela JVM para execução do programa. Esses arquivos fazem
parte da Java API (biblioteca Java). A figura abaixo ilustra o processo completo de
execução de um programa Java.
Figura A.5 – Processo de compilação e interpretação
Observe os arquivos gerados após o processo de compilação. Estes são
os arquivos de classe (possuem extensão “.class”). O arquivo de classe contém
apenas a conversão das instruções que o programador escreveu. Na verdade,
isso não é suficiente para executar um programa. Para certas ações, como exibir
um texto em uma janela, é necessário o uso de arquivos de classe já
disponibilizados pela linguagem (ou escritos por outros programadores). No caso
dos arquivos da linguagem, as ações necessárias para executar algumas
operações (como imprimir um texto) foram implementadas e compiladas. Os
arquivos de classes (bytecodes) foram, então, organizados em uma biblioteca –
um conjunto de códigos, que podem, sempre, que necessário, ser utilizados.
Assim, para a execução de um programa, o interpretador Java carrega o bytecode
do programa escrito pelo programador, inicializa o mesmo e carrega os arquivos
necessários da biblioteca, à medida que são requeridos. O não uso dos arquivos
EADDCC031 – Linguagem de Programação II
114
para a execução de certas ações gera erro de compilação. É interessante o erro
ser informado em tempo de compilação, e não de execução, pois permite que o
programador acerte o problema, especificando as bibliotecas necessárias para o
bom funcionamento do seu programa antes que o mesmo seja colocado em
execução.
A.3. Execução de Programas - Exemplo
Vamos, agora, apresentar um exemplo completo de execução de um
programa simples em Java. Entenda que o desenvolvimento de sistemas em Java
é feito através de ambientes apropriados, chamados de IDE (Ambiente de
Desenvolvimento Integrado). Estes permitem que todo o programa seja compilado
e executado, sem que seja necessário realizar o exemplo a seguir. A proposta,
aqui, é apenas ilustrar como ocorrer a execução de um programa escrito em
linguagem Java. Não seria nada produtivo se tivéssemos que realizar o
procedimento deste exemplo para todos os nossos programas. Para começar,
observe a figura abaixo. Ela ilustra o código fonte de um simples programa em
Java.
Figura A.6 – Exemplo de programa em Java
A figura acima ilustra um programa simples, que imprime a mensagem “Oi
Mundo” ao ser executado. Não entraremos em detalhes sobre o código em si.
Nosso propósito, agora, é apenas ilustrar a geração do arquivo de bytecode e a
execução do mesmo. O programa acima foi salvo com o nome “HelloWord.java”.
Agora que temos o nosso arquivo com o código fonte, precisamos (i) compilá-lo e
(ii) executá-lo. Para realizar a compilação e a execução, iremos utilizar o prompt
de comando do Windows - o propósito é ilustrar as instruções de comando para
ser realizar ambos os procedimentos. Observe a figura abaixo.
EADDCC031 – Linguagem de Programação II
115
Figura A.7 – Compilação do programa Java
A figura acima ilustra o procedimento para compilação do arquivo
HelloWord.java. Usamos o termo javac para chamar o compilador e compilar
nosso programa. Note que precisamos especificar a localização do arquivo. Após
este procedimento, no arquivo está compilado. No mesmo local onde o arquivo se
encontra, há , agora, um arquivo denominado HelloWord.class – que é o arquivo
com os bytecodes. Em seguida, precisamos executar o nosso programa. Para
isto, usamos a JVM. Observe a figura abaixo.
Figura A.8 – Execução do programa Java
A figura acima ilustra a execução do programa. Observe a primeira
instrução mostrada no prompt. Utilizamos o termo java, seguido do nome do
programa (mas sem a extensão java). Esta instrução executa o programa, cujo
resultado (o texto Oi Mundo) pode ser visto na linha abaixo. Com isto, terminamos
a execução do nosso programa. A figura A.8 ilustra todo o processo que
acabamos de executar.
EADDCC031 – Linguagem de Programação II
116
Figura A.9 – Processo completo de execução do programa Java
Alguns programadores iniciantes em tecnologia Java - mas acostumados
com programas em C, por exemplo - acreditam que Java possui baixa
produtividade. Essa percepção é fomentada pelo fato de, segundo eles, ser mais
simples criar os pequenos programas utilizados no início do aprendizado nas
linguagens que já utilizam. É importante, entretanto, deixar claro que a premissa
do Java não é a de criar sistemas pequenos. O foco da plataforma é outro:
aplicações de médio a grande porte. Certamente, criar a primeira versão de uma
aplicação usando Java, mesmo utilizando IDE, é mais trabalhoso do que utilizar
outra linguagem, mas simples. Porém, com uma linguagem orientada a objetos e
madura como o Java, é extremamente mais fácil e rápido realizar alterações no
sistema - desde as boas práticas e recomendações sejam, de fato, seguidas.
Além disso, a quantidade enorme de bibliotecas gratuitas para realizar os mais
diversos trabalhos (tais como relatórios, gráficos, sistemas de busca, geração de
código de barra, manipulação de XML, tocadores de vídeo, manipuladores de
texto, persistência, impressão, etc.) é um ponto fortíssimo para adoção do Java.
Cada linguagem tem seu espaço e seu melhor uso. O uso do Java é interessante,
por exemplo, em aplicações que virão a crescer, em que a legibilidade do código
é importante, onde temos muita conectividade e onde há a necessidade de
integração/comunicação entre diferentes plataformas.
EADDCC031 – Linguagem de Programação II
117
Apêndice B: Método Main Quando aplicativos Java são desenvolvidos, deve haver um único ponto de
partida a partir do qual sua execução começa. Em Java, este ponto de partida é
um método, chamado de main (principal).
B.1. Método main
Para um aplicativo Java, exatamente um dos métodos deve ser chamado
de main e ser definido conforme a figura abaixo. Caso contrário, a JVM não
executará o aplicativo. O método main do Java é como se fosse a função main no
C e no C++. Quando a aplicação é executada pelo interpretador Java, o método
main é o primeiro método a ser chamado. Então, o método main chama os
demais métodos necessários para a execução completa da aplicação. A figura
abaixo apresenta a assinatura deste método.
Figura B.1– Método main ou principal
Podemos, pela figura, acima, observar a estrutura do método main. A
assinatura do método é formada pelo modificador de acesso public, seguido pela
palavra static, o tipo de retorno void e seu nome (main). Note que main é um
método. Logo, o seu nome é seguido de parênteses. No caso deste método, há
um argumento – especificamente, um vetor – do tipo String. A assinatura sempre
segue este padrão. O método main deve ser public para que seja possível invocá-
lo externamente. Ele precisa ser estático (static), pois não há nenhum objeto
criado no sistema quando o iniciamos (note que toda aplicação começa a
execução a partir deste método e, antes de se iniciar a execução do mesmo, não
EADDCC031 – Linguagem de Programação II
118
há objetos instanciados). O tipo de retorno é void, uma vez que o método não
retorna valor. O nome main é fixo (ou seja, deve possuir este nome). O
parâmetro, como dito, é um vetor String, que permite que os usuários passem
argumentos necessários para iniciar o método. Essa passagem é opcional – ou
seja, deve-se passar algum argumento se for necessário. Caso não seja, não
passamos e o vetor fica com comprimento zero. O corpo do método main,
teoricamente, pode conter quaisquer instruções. Contudo, um bom estilo dita que
o comprimento do método (a quantidade de linhas que o mesmo possui) deve ser
mantido em um mínimo possível. Especificamente, ele não deve conter nada que
faça parte da lógica da aplicação. Ele deve ser um método apenas para iniciar a
aplicação. Quando realizamos uma chamada para execução de um programa em
Java (por exemplo, no prompt de comando: Java NomedaClasse), o comando
java inicia a JVM. O sistema, então, procura um método na classe exatamente
com a assinatura da linha 5 (figura B.1). Sobre a passagem de parâmetro para a
execução do main, convém destacar que estamos trabalhando com um vetor do
tipo String. Assim, somente elementos desse tipo são passados. Entretanto, Java
fornece meios de transformarmos um numeral do tipo String em um numeral
inteiro, por exemplo. Lembre-se que é perfeitamente possível uma variável do tipo
String receber um valor “5”. As aspas são utilizadas para informar ao compilador
que 5 é uma String e não um valor inteiro. Observe o código abaixo
Figura B.2– Método main recebendo parâmetro
No código acima, temos a especificação da classe Exemplo, que possui um
método main. Note que este método, como dizemos, recebe um vetor de Strings,
nomeado como args. Na linha 4, estamos realizando quatro ações: (i) estamos
acessando o valor da posição 0 do vetor args, (ii) convertendo este valor em um
inteiro, (iii) multiplicando este valor, já convertido, por 10 e (iv) imprimindo o
resultado dessa multiplicação. Estamos considerando o fato de que, para
EADDCC031 – Linguagem de Programação II
119
executar o programa acima, temos que passar uma String que representa um
número inteiro. Observe a figura abaixo
Figura B.2– Método main recebendo parâmetro
Note que estamos realizando uma chamada ao programa Exemplo,
passando o valor 5 como parâmetro. O resultado, como esperado, será o valor
50. Cada palavra depois do nome da classe na primeira linha de comando na
figura acima será lida como uma String separada e passada para o método main
como um elemento do vetor de strings. Nesse nosso exemplo, o vetor args
contém um único elemento, 5 (String). Convém ressaltarmos que os parâmetros
de linha de comando não são frequentemente utilizados em Java.