Apostila Prog II - 2º Ano
Apostila Prog II - 2º Ano
PROGRAMAÇÃO ORIENTADA À
OBJETOS
CAPÍTULO 1
ORIENTAÇÃO A OBJETOS
Objetivos:
cpf = formulario-
>campo_cpf valida(cpf)
Alguém o obriga a sempre validar esse CPF? Você pode, inúmeras vezes, esquecer de chamar esse
validador. Mas: considere que você tem 50 formulários e precise validar em todos o CPF. Se sua
equipe tem três programadores trabalhando nesses formulários, quem fica responsável por essa
validação? Todos!
Considerando que você não erre nesse ponto e a sua equipe tenha uma comunicação muito boa
(perceba que comunicação excessiva pode ser prejudicial e atrapalhar o andamento), ainda temos
outro problema: imagine que, em todo formulário, você também queira que a idade do cliente seja
validada - o cliente precisa ter mais de 18 anos. Teríamos de colocar um if ... Mas onde? Espalhado
por todo seu código e, mesmo que se crie outra função para validar, precisaríamos incluir isso nos
nossos 50 formulários já existentes. Qual é a chance de esquecermos um deles? É muito grande.
A responsabilidade de verificar se o cliente tem ou não 18 anos ficou espalhada por todo o seu
código. Seria interessante poder concentrar essa responsabilidade em um lugar só para não ter chances
de se esquecer disso.
Melhor ainda seria se conseguíssemos mudar essa validação e os outros programadores nem
precisassem ficar sabendo disso. Em outras palavras, eles criariam formulários, e um único
programador seria responsável pela validação: os outros nem saberiam da existência desse trecho de
código. Impossível? Não, o paradigma da orientação a objetos facilita tudo isso.
O problema do paradigma procedural é que não existe uma forma simples de criar conexão forte
entre dados e funcionalidades. No paradigma orientado a objetos, é muito fácil ter essa conexão por
meio dos recursos da própria linguagem.
QUAIS AS VANTAGENS?
Orientação a objetos irá ajudá-lo bastante a se organizar e escrever menos, além de concentrar
as responsabilidades nos pontos certos, flexibilizando sua aplicação e encapsulando na lógica
de negócios.
Nos próximos capítulos, conseguiremos enxergar toda essa vantagem. Mas, primeiramente, é
necessário conhecer um pouco mais da sintaxe e criação de tipos e referências em Java.
Número da conta;
Nome do titular da conta;
Saldo.
O que toda conta faz que é importante para nós? Isto é, o que gostaríamos de "pedir à conta"?
Com isso, temos o projeto de uma conta bancária. Podemos pegar esse projeto e acessar seu saldo?
Não. O que temos ainda é o projeto. Antes, precisamos construir uma conta para poder acessar o
que ela tem e pedir a ela que faça algo.
Página |4
Repare na figura. Apesar de o papel do lado esquerdo especificar uma Conta, essa especificação
é uma Conta? Nós depositamos e sacamos dinheiro desse papel? Não. Utilizamos a especificação da
Conta para poder criar instâncias que realmente são contas, nas quais podemos realizar as operações
que criamos.
Apesar de declararmos que toda conta tem um saldo, um número e uma agência no pedaço de
papel (como, à esquerda, na figura), são nas instâncias desse projeto em que realmente há espaço para
armazenar esses valores.
Ao projeto da conta, isto é, à definição da conta, damos o nome de classe. Ao que podemos
construir a partir desse projeto; às contas de verdade, damos o nome de objetos.
A palavra classe vem da taxonomia da biologia. Todos os seres vivos de uma mesma classe
biológica têm uma série de atributos e comportamentos em comum, mas não são iguais, pois podem
variar nos valores desses atributos e como realizam esses comportamentos.
Homo Sapiens define um grupo de seres que possuem características em comum. Porém, a
definição (a ideia, o conceito) de um Homo Sapiens é um ser humano? Não. Tudo está especificado
na classe Homo Sapiens, mas se quisermos mandar alguém correr, comer e pular, precisaremos de
uma instância de Homo Sapiens, ou então de um objeto do tipo Homo Sapiens.
Um outro exemplo: uma receita de bolo. A pergunta é certeira: você come uma receita de bolo?
Não. Precisamos instanciá-la e fazer um objeto bolo a partir dessa especificação (a classe) para
utilizá-la. Podemos criar centenas de bolos com base nessa classe (a receita, no caso). Eles podem ser
bem semelhantes, alguns até idênticos, mas são objetos diferentes.
Podemos fazer milhares de analogias parecidas. A planta de uma casa é uma casa?
Definitivamente, não. Não podemos morar dentro da planta de uma casa nem podemos abrir sua porta
ou pintar suas paredes. Precisamos, antes, construir instâncias a partir dessa planta. Essas instâncias,
sim, podemos pintar, decorar ou morar dentro.
Pode parecer óbvio, mas a dificuldade inicial do paradigma da orientação a objetos é justamente
saber distinguir classe de objeto. É comum o iniciante utilizar, obviamente de forma errada, essas
duas palavras como sinônimos.
Página |5
Começaremos apenas com o que uma Conta tem, e não com o que ela faz (veremos isso logo
em seguida).
Um tipo desses, como o especificado de Conta acima, pode ser facilmente traduzido para Java:
class Conta {
int numero;
String titular;
double saldo;
// ..
}
STRING
String é uma classe em Java. Ela guarda uma cadeia de caracteres, uma frase completa. Como
estamos ainda aprendendo o que é uma classe, entenderemos, com detalhes, a classe String
Por enquanto, declaramos o que toda conta deve ter. Esses são os atributos que as contas quando
criadas terão. Repare que essas variáveis foram declaradas fora de um bloco, diferente do que
fazíamos quando tinha aquele main. Quando uma variável é declarada diretamente dentro do escopo
da classe, é chamada de variável de objeto ou atributo.
Já temos uma classe em Java que especifica o que todo objeto dessa classe deve ter. Mas como
usá-la? Além dessa classe, ainda teremos o Programa.java e, a partir dele, utilizaremos a classe
conta.
Para criar (construir, instanciar) uma Conta, basta usar a palavra-chave new. Devemos
utilizar também os parênteses, que descobriremos o que fazem exatamente no próximo capítulo:
Página |6
class Programa {
public static void main(String[] args) {
new Conta();
}
}
Bem, o código acima cria um objeto do tipo Conta. Mas como acessar esse objeto que foi criado?
Precisamos ter alguma forma de nos referenciarmos a esse objeto. Precisamos de uma variável:
class Programa {
public static void main(String[] args) {
Conta minhaConta;
minhaConta = new Conta();
}
}
Pode parecer estranho escrevermos duas vezes Conta: uma vez na declaração da variável e,
outra vez, no uso do new. Mas há um motivo que, em breve, entenderemos.
class Programa {
public static void main(String[] args) {
Conta minhaConta;
minhaConta = new Conta();
minhaConta.titular = "Duke";
minhaConta.saldo = 1000.0;
1.5 MÉTODOS
Dentro da classe, também declararemos o que cada conta faz e como isso é feito - os
comportamentos que cada classe tem. Por exemplo, de que maneira uma Conta saca dinheiro?
Especificaremos isso dentro da própria classe conta, e não em um local desatrelado das
informações da própria Conta. É, por isso, que essas funções são chamadas de métodos, pois é a
maneira de fazer uma operação com um objeto.
Queremos criar um método que saca uma determinada quantidade e não devolve nenhuma
informação para quem acionar esse método:
class Conta {
double salario;
// ... outros atributos ...
A palavra-chave void diz que quando você pedir para a conta sacar uma quantia, nenhuma
informação será enviada de volta a quem pediu.
Quando alguém pedir para sacar, ela também dirá quanto quer sacar. Por isso, precisamos declarar
o método com algo dentro dos parênteses - o que vai aí dentro é chamado de argumento do método
(ou parâmetro). Essa variável é uma variável comum, chamada também de temporária ou local, pois,
ao final da execução desse método, ela deixa de existir.
Dentro do método, estamos declarando uma nova variável. Essa variável, assim como o
argumento, irá morrer no fim do método, porque esse é seu escopo. No momento em que vamos
acessar nosso atributo, usamos a palavra-chave this para mostrar que esse é um atributo, e não
uma simples variável
(veremos depois que é opcional).
Repare que, nesse caso, a conta poderia estourar um limite fixado pelo banco. Mais para frente,
evitaremos essa situação de uma maneira muito elegante.
class Conta {
// ... outros atributos e métodos ...
Página |8
Para mandar uma mensagem ao objeto e pedir que ele execute um método, também usamos o ponto.
O termo usado para isso é invocação de método.
O código a seguir saca dinheiro e depois deposita outra quantia na nossa conta:
class TestaAlgunsMetodos {
public static void main(String[] args) {
// criando a conta Conta minhaConta;
minhaConta = new Conta();
class Conta {
// ... outros métodos e atributos...
}
else {
this.saldo = this.saldo - valor;
return true;
}
}
}
A declaração do método mudou! O método saca não tem void na frente. Isso quer dizer que
quando é acessado, ele devolve algum tipo de informação – no caso, um boolean. A palavra-
chave return indica que o método terminará ali, retornando tal informação.
Exemplo de uso:
minhaConta.saldo = 1000;
boolean consegui = minhaConta.saca(2000);
if (consegui) {
System.out.println("Consegui sacar");
} else {
System.out.println("Não consegui sacar");
}
minhaConta.saldo = 1000;
if (minhaConta.saca(2000)) {
System.out.println("Consegui sacar");
} else {
System.out.println("Não consegui sacar");
}
Mais adiante, veremos que, algumas vezes, é mais interessante lançar uma exceção (exception)
nesses casos.
class TestaDuasContas {
P á g i n a | 10
Conta minhaConta;
minhaConta = new Conta();
minhaConta.saldo = 1000;
Conta meuSonho;
meuSonho = new Conta();
meuSonho.saldo = 1500000;
}
}
P á g i n a | 44
CAPÍTULO 2
OBJETOS
É por esse motivo que, diferente dos tipos primitivos como i n t e l o n g , precisamos dar n e w
depois de declarada a variável:
Conta c2;
c2 = new Conta();
}
O correto aqui é dizer que c1 se refere a um objeto. Não é certo dizer que c1 é um objeto,
pois c1 é uma variável referência apesar de, depois de um tempo, os programadores Java falarem:
"tenho um objeto c do tipo Conta" como um modo para encurtar a frase: "tenho uma referência c a
um objeto do tipo Conta".
Basta lembrar que, em Java, uma variável nunca é um objeto. Não há, no Java, uma maneira de
criarmos o que é conhecido como objeto pilha ou objeto local, pois todo objeto, nessa linguagem,
sem exceção, é acessado por uma variável referência.
Conta c1;
c1 = new Conta();
Conta c2;
c2 = new Conta();
P á g i n a | 45
Para quem conhece, é parecido com um ponteiro. Porém, você não pode manipulá-lo como um
número nem utilizá-lo para aritmética, pois ela é tipificada (usa variáveis com tipos específicos).
Um outro exemplo:
class TestaReferencias {
public static void main(String[] args) {
Conta c1 = new Conta();
c1.deposita(100);
System.out.println(c1.saldo);
System.out.println(c2.saldo);
}
}
Qual é o resultado do código acima? O que aparece ao rodar?
O que acontece aqui? O operador = copia o valor de uma variável. Mas qual é o valor da variável
c1 ? É o objeto? Não. Na verdade, o valor guardado é a referência (endereço) ao local onde o objeto
se encontra na memória principal.
Quando fizemos c2 = c1 , c2 passa, nesse instante, a fazer referência ao mesmo objeto referenciado
por c1 .
Atenção: não estamos discutindo aqui a utilidade de fazer uma referência apontar para o mesmo
objeto que outra. Essa utilidade ficará mais evidente quando passarmos variáveis do tipo referência
como argumento a métodos.
NEW
Mas, a fim de melhor entender as referências no Java, imagine que o new , depois de alocar a
memória para esse objeto, devolve uma flecha, isto é, um valor de referência. Quando você atribui
isso a uma variável, essa variável passa a se referir a esse mesmo objeto.
if (c1 == c2) {
System.out.println("Contas iguais");
}
}
P á g i n a | 46
O operador == compara o conteúdo das variáveis, mas essas variáveis não guardam o objeto, e
sim o endereço em que ele se encontra. Como em cada uma dessas variáveis guardamos duas contas
criadas diferentemente, elas estão em espaços distintos da memória, o que faz o teste if valer
false . As contas podem ser equivalentes no nosso critério de igualdade, porém elas não são o
mesmo objeto. Quando se trata de objetos, pode ficar mais fácil pensar que o == compara se os
objetos (referências, na verdade) são o mesmo, e não se são iguais.
Para saber se dois objetos têm o mesmo conteúdo, você precisa comparar atributo por atributo.
Veremos uma solução.
A ideia é que, quando chamarmos o método transfere, já teremos um objeto do tipo Conta
(o this). Portanto, o método recebe apenas um parâmetro do tipo Conta, isto é, a Conta destino
(além do valor):
P á g i n a | 48
class Conta {
// atributos e métodos...
Para deixar o código mais robusto, poderíamos verificar se a conta tem a quantidade a ser
transferida disponível. Com o intuito de ficar ainda mais interessante, você pode chamar os métodos
deposita e saca já existentes para fazer essa tarefa:
class Conta {
// atributos e métodos...
if (retirou == false) {
else {
destino.deposita(valor);
return true;
P á g i n a | 49
Quando passamos uma Conta como argumento, o que será que acontece na memória? Será que o
objeto é clonado?
No Java, a passagem de parâmetro funciona como uma simples atribuição tal qual no uso do "=".
Então, esse parâmetro copiará o valor da variável do tipo Conta que for passado como argumento.
E qual é o valor de uma variável dessas? Seu valor é um endereço ou uma referência, mas nunca um
objeto. Por isso, não há cópia de objetos aqui.
Esse último código poderia ser escrito com uma sintaxe muito mais sucinta. Como?
TRANSFERE PARA
conta1.transferePara(conta2, 50);
class Conta {
int numero = 1234;
String titular = "Duke";
double saldo = 1000.0;
}
Nesse caso, quando você criar uma conta, seus atributos já estarão "populados" com esses valores
colocados.
Imagine que iniciamos a aumentar nossa classe Conta e adicionamos nome, sobrenome
e CPF do titular da conta. Começaríamos a ter muitos atributos. E se você pensar direito, uma
Conta não tem nome , nem sobrenome , nem CPF . Quem tem esses atributos é um Cliente
. Desse modo, podemos criar uma nova classe e fazer uma composição.
Seus atributos também podem ser referências às outras classes. Suponha a seguinte classe
Cliente :
class Cliente {
String nome;
String sobrenome;
String cpf;
}
class Conta {
int numero;
double saldo;
Cliente titular;
// ..
class Teste {
public static void main(String[] args) {
Conta minhaConta = new Conta();
Cliente c = new Cliente();
minhaConta.titular = c;
// ...
}
P á g i n a | 51
Aqui, simplesmente houve uma atribuição. O valor do variável c é copiado para o atributo titular
do objeto ao qual minhaConta se refere. Em outras palavras, minhaConta tem uma referência
ao mesmo Cliente a que c se refere, e este pode ser acessado por meio de
minhaConta.titular.
Você pode realmente navegar sobre toda essa estrutura de informação sempre usando o ponto:
Ou ainda pode fazer isso de uma forma mais direta e até mais elegante:
minhaConta.titular.nome = "Duke";
Mas e se dentro do meu código eu não desse new em Cliente e tentasse acessá-lo diretamente?
class Teste {
public static void main(String[] args) {
Conta minhaConta = new Conta();
minhaConta.titular.nome = "Manoel";
// ...
}
}
Quando damos new em um objeto, ele o inicializa com seus valores default: 0 para números,
false para boolean e null para referências. null é uma palavra-chave em Java que indica
uma referência a nenhum objeto.
P á g i n a | 50
Se, em algum caso, você tentar acessar um atributo ou método de alguém que está se referenciando a
null, receberá um erro durante a execução (NullPointerException, que veremos mais à frente).
Percebe-se, então, que o new não apresenta um efeito cascata, a menos que você dê um valor default
(ou use construtores, que também veremos mais à frente):
class Conta {
Com esse código, toda nova Conta criada já terá um novo Cliente associado sem necessidade
de instanciá-lo logo em seguida da instanciação de uma Conta. Qual alternativa você deve usar?
Depende do caso: para toda nova Conta, você precisa de um novo Cliente? É essa pergunta a ser
respondida. Nesse caso, a resposta é não, mas depende do nosso problema.
Atenção: para quem não está acostumado com referências, pode ser bastante confuso pensar
sempre em como os objetos estão na memória para poder tirar as conclusões de o que ocorrerá ao
executar determinado código, por mais simples que ele seja. Com tempo, você adquire a habilidade
de rapidamente saber o efeito de atrelar as referências sem ter de gastar muito tempo com isso. É
importante, nesse começo, você estar sempre pensando no estado da memória. E realmente lembrar-
se de que no Java, "uma variável nunca carrega um objeto, e sim uma referência a ele", pois isso
facilita muito.
P á g i n a | 51
class TestaCarro {
public static void main(String[] args) {
Carro meuCarro;
meuCarro = new Carro();
meuCarro.cor = "Verde";
meuCarro.modelo = "Fusca";
meuCarro.velocidadeAtual = 0;
meuCarro.velocidadeMaxima = 80;
// liga o carro
meuCarro.liga();
P á g i n a | 52
// acelera o carro
meuCarro.acelera(20);
System.out.println(meuCarro.velocidadeAtual);
}
}
class TestaCarro {
public static void main(String[] args) {
Carro meuCarro;
meuCarro = new Carro();
meuCarro.cor = "Verde";
class Motor {
int potencia; String tipo;
}
class Carro {
String cor; String modelo;
double velocidadeAtual;
double velocidadeMaxima;
Motor motor;
// ..
}
Podemos criar diversos carros e mexer com seus atributos e métodos, assim como fizemos no
exemplo do Banco.
Como você pode ter reparado, sempre damos nomes às variáveis com letras minúsculas. É que
existem convenções de código, dadas pela Oracle, para facilitar a legibilidade do código entre
programadores. Essa convenção é muito seguida. Saiba mais pesquisando por java code
conventions.
É necessário usar a palavra-chave this quando for acessar um atributo? Para que, então,
utilizá-la?
Existe um padrão para representar suas classes em diagramas que é amplamente utilizado,
P á g i n a | 53
chamado
UML. Pesquise sobre ele.
Modele uma conta. A ideia aqui é apenas modelar, isto é, identificar quais informações são importantes.
Desenhe no papel tudo o que uma Conta tem e tudo o que ela faz. Ela deve ter o nome do titular (
String ), o número ( int ), a agência ( String ), o saldo ( double ) e uma data de abertura (
String ). Além disso, a conta deve fazer as seguintes ações: sacar para retirar um valor do saldo;
depositar a fim de adicionar um valor ao saldo; calculaRendimento para devolver o seu ganho a classe
Conta deve conter, além dos atributos mencionados anteriormente, pelo menos os seguintes métodos:
Lembre-se de seguir a convenção Java, isso é importantíssimo. Preste atenção nas maiúsculas e
minúsculas, seguindo o seguinte exemplo: nomeDeAtributo , nomeDeMetodo ,
nomeDeVariavel , NomeDeClasse , etc.
P á g i n a | 54
Você até pode colocar todas as classes no mesmo arquivo e apenas compilá-lo. Ele vai
gerar um .class para cada classe presente nele.
Porém, por uma questão de organização, uma boa prática é criar um arquivo .java para
cada classe. Em capítulos posteriores, veremos também determinados casos nos quais você
será obrigado a declarar cada classe em um arquivo separado.
javac *.java
Veremos, mais à frente, o método toString , que é uma solução muito mais elegante para
mostrar a representação de um objeto como String , além de não jogar tudo para o
System.out (só se você o desejar).
P á g i n a | 55
2. Na classe de teste dentro do bloco main, construa duas contas com o new e compare-as com o
==. E se eles tiverem os mesmos atributos? Para isso, você precisará criar outra referência:
Conta c1 = new Conta();
c1.titular = "Danilo";
c1.saldo = 100;
if (c1 == c2) {
System.out.println("iguais");
} else {
System.out.println("diferentes");
}
3. Agora, crie duas referências para a mesma conta e compare-as com o == . Tire suas conclusões.
A fim de criar duas referências para a mesma conta:
Conta c1 = new Conta():
c1.titular = "Hugo";
c1.saldo = 100;
c2 = c1;
4. (Opcional) Em vez de utilizar uma String para representar a data, crie uma outra classe
chamada Data . Ela tem três campos int para dia, mês e ano. Faça com que sua conta passe a
usá-la (é parecido com o último exemplo da explicação, em que a Conta passou a ter referência
a um Cliente ).
class Conta {
Data dataDeAbertura; // qual é o valor default aqui?
// seus outros atributos e métodos
}
class Data {
int dia; int mes; int ano;
}
Modifique sua classe TestaConta para que você crie uma Data e a atribua à Conta :
Conta c1 = new Conta();
//...
Data data = new Data(); // ligação!
c1.dataDeAbertura = data;
o valor da
dataDeAbertura daquela Conta :
class Conta {
String recuperaDadosParaImpressao() {
String dados = "\nTitular: " + this.titular;
// imprimir aqui os outros atributos...
6. (Opcional) O que acontece se você tentar acessar um atributo diretamente na classe? Por exemplo:
Conta.saldo = 1234;
Conta.calculaRendimento();
7. (Opcional e avançado) Crie um método na classe Data que devolva o valor formatado da data,
isto é, devolva uma String com "dia/mês/ano". Tudo isso para que o método
recuperaDadosParaImpressao da classe Conta possa ficar assim:
class Conta {
// atributos e metodos
String recuperaDadosParaImpressao()
{
// imprime outros atributos...
dados += "\nData de abertura: " +
this.dataDeAbertura.formatada(); return dados;
}
}
P á g i n a | 57
2.4 DESAFIOS
1. Um método pode se chamar a si mesmo. Chamamos isso de recursão. Você pode resolver a série
de Fibonacci usando um método que se chama a si mesmo. O objetivo é você criar uma classe
que possa ser usada da seguinte maneira:
Fibonacci fibonacci = new
Fibonacci(); for (int i =
1; i <= 6; i++) {
int resultado = fibonacci.calculaFibonacci(i);
System.out.println(resultado);
}
Esse método calculaFibonacci não pode ter nenhum laço e só pode chamar-se a si mesmo
sendo método. Pense nele como uma função que usa a si própria para calcular o resultado.
2. Por que o modo acima é extremamente mais lento para calcular a série do que o modo iterativo
(que se usa um laço)?
3. Escreva o método recursivo novamente usando apenas uma linha. Para isso, pesquise sobre
o
operador condicional ternário (ternary operator).
P á g i n a | 58
Se você está com dificuldade em alguma parte desse capítulo, aproveite e treine tudo o que vimos
nos pequenos programas abaixo:
Programa 1
Classe: Pessoa
Atributos: nome, idade. Método:
void fazAniversario()
Crie uma pessoa. Coloque o nome e a idade inicial dela, faça alguns aniversários (aumentando
a idade) e imprima o seu nome e a idade.
Programa 2
Classe: Porta
Atributos:
aberta, cor, dimensaoX,
dimensaoY, dimensaoZ
Métodos:
void abre()
void fecha()
void pinta(String s)
boolean estaAberta()
Crie uma porta, abra-a e feche-a. Pinte-a de diversas cores, altere suas dimensões e use o método
estaAberta para verificar se ela está aberta.
Programa 3
Classe: Casa
Atributos:
cor, porta1, porta2, porta3
Método:
void pinta(String s),int quantasPortasEstaoAbertas()
P á g i n a | 59
Crie uma casa e pinte-a. Faça três portas e coloque-as na casa; abra-as e feche-as como desejar.
Utilize o método quantasPortasEstaoAbertas para imprimir o número de portas
abertas.
CAPÍTULO 3
MODIFICADORES DE ACESSO E ATRIBUTOS DE CLASSE
"A marca do homem imaturo é que ele quer morrer nobremente por uma causa, enquanto a marca do
homem maduro é querer viver modestamente por uma."--J. D. Salinger
Controlar o acesso aos seus métodos, atributos e construtores por meio dos modificadores private
e public;
Escrever métodos de acesso a atributos do tipo getters e setters;
Escrever construtores para suas classes;
Utilizar variáveis e métodos estáticos.
class Conta {
String titular;
int numero;
double saldo;
// ..
void saca(double valor) {
this.saldo = this.saldo - valor;
}
}
A classe a seguir mostra como é possível ultrapassar o limite de saque usando o método saca:
class TestaContaEstouro1 {
public static void main(String[] args) {
Conta minhaConta = new Conta();
P á g i n a | 60
minhaConta.saldo = 1000.0;
minhaConta.saca(50000); // saldo é só 1000!!
}
}
Podemos incluir um if dentro do nosso método saca() para evitar a situação que resultaria em
uma conta em estado inconsistente, com seu saldo abaixo de 0. Fizemos isso no capítulo de orientação
a objetos básica.
Apesar de melhorar bastante, ainda temos um problema mais grave: ninguém garante que o
usuário da classe utilizará sempre o método para alterar o saldo da conta. O código a seguir faz isso
diretamente:
class TestaContaEstouro2 {
public static void
main(String[] args) {
Conta minhaConta = new
Conta();
minhaConta.saldo = -200; //saldo está abaixo de 0
}
}
Como evitar isso? Uma ideia simples seria testar se não estamos sacando um valor maior do que o
saldo toda vez que formos alterá-lo:
class TestaContaEstouro3 {
// testa se o
novoSaldo é válido if
(novoSaldo < 0) { //
System.out.println("Não posso mudar para esse saldo");
} else {
minhaConta.saldo = novoSaldo;
}
}
}
P á g i n a | 61
Esse código iria se repetir ao longo de toda nossa aplicação e, pior, alguém pode esquecer de fazer
essa comparação em algum momento, deixando a conta na situação inconsistente. A melhor forma de
resolver isso seria forçar quem usa a classe Conta a invocar o método saca e não permitir o
acesso direto ao atributo. É o mesmo caso da validação de CPF.
Para fazer isso no Java, basta declarar que os atributos não podem ser acessados de fora da classe
por meio da palavra-chave private :
class Conta {
private double saldo;
// ...
}
Marcando um atributo como privado, fechamos o seu acesso em relação a todas as outras classes e
fazemos com que o seguinte código não compile:
class TestaAcessoDireto {
public static void main(String[] args) {
Conta minhaConta = new Conta();
//Não compila! Você não pode acessar o atributo privado de outra classe.
minhaConta.saldo = 1000;
}
}
TesteAcessoDireto.java:5 saldo has private access in Conta
minhaConta.saldo = 1000;
Na orientação a objetos, é prática quase que obrigatória proteger seus atributos com private
(discutiremos outros modificadores de acesso em outros capítulos).
Cada classe é responsável por controlar seus atributos, portanto ela deve julgar se aquele novo
valor é válido ou não. Essa validação não deve ser controlada por quem está usando a classe, e sim
por ela mesma, centralizando essa responsabilidade e facilitando futuras mudanças no sistema. Muitas
outras vezes, nem mesmo queremos que outras classes saibam da existência de determinado atributo,
escondendo-o por completo, já que ele diz respeito ao funcionamento interno do objeto.
Repare: quem invoca o método saca não faz a menor ideia de que existe uma verificação para o
valor do saque. Para quem for usar essa classe, basta saber o que o método faz e não como exatamente
ele o faz (o que um método faz é sempre mais importante do que como ele faz: mudar a implementação
é fácil, já mudar a assinatura de um método gerará problemas).
P á g i n a | 62
A palavra-chave private também pode ser usada a fim de modificar o acesso a um método. Tal
funcionalidade é utilizada em diversos cenários, os mais comuns são: quando existe um método que
serve apenas para auxiliar a própria classe e quando há código repetido dentro de dois métodos da
classe. Sempre devemos expôr o mínimo possível de funcionalidades com o intuito de criar um baixo
acoplamento entre as nossas classes.
Da mesma maneira que temos o private, temos o modificador public, que permite a todos
acessarem um determinado atributo ou método:
class Conta {
//...
public void saca(double valor) {
//posso sacar até saldo if (valor > this.saldo){
System.out.println("Não posso sacar um valor maior do que o saldo!");
} else {
this.saldo = this.saldo - valor;
}
}
}
Até agora, tínhamos declarado variáveis e métodos sem nenhum modificador como private e
public . Quando isso acontece, o seu método ou atributo fica em um estado de visibilidade
intermediário entre o private eo public , que veremos mais para frente, no capítulo de pacotes.
É muito comum e faz todo sentido que seus atributos sejam private , e quase todos seus
métodos sejam public (não é uma regra!). Desta forma, toda conversa de um objeto com outro é
feita por troca de mensagens, isto é, acessando seus métodos. Algo muito mais educado que mexer
diretamente em um atributo que não é seu.
Melhor ainda! O dia em que precisarmos mudar como é realizado um saque na nossa classe
Conta , adivinhe o local onde precisaríamos modificar? Apenas no método saca , o que faz pleno
sentido. Por exemplo, imagine cobrar CPMF de cada saque: basta você modificar ali, e nenhum outro
código, fora a classe Conta , precisará ser recompilado. Além do mais: as classes as quais usam
esse método nem precisam ficar sabendo de tal modificação. Você precisa apenas recompilar aquela
classe e substituir aquele arquivo .class . Ganhamos muito em esconder o funcionamento do nosso
método na hora de fazer manutenção e modificações.
P á g i n a | 63
3.2 ENCAPSULAMENTO
O que começamos a ver nesse capítulo é a ideia de encapsular, isto é, ocultar todos os membros
de uma classe (como vimos acima), além de esconder como funcionam as rotinas (no caso, métodos)
do nosso sistema.
Encapsular é fundamental para seu sistema ser suscetível a mudanças: não precisaremos mudar uma
regra de negócio em vários lugares, mas, sim, em apenas um único lugar, já que essa regra está
encapsulada (veja o caso do método saca).
O conjunto de métodos públicos de uma classe é também chamado de interface da classe, pois essa é
a única maneira pela qual você se comunica com objetos dessa classe.
É sempre bom programar pensando na interface da sua classe, em como seus usuários estarão
utilizando-a, e não somente em como ela funcionará.
A implementação em si, o conteúdo dos métodos, não tem tanta importância para o usuário dessa
classe, pois ele só precisa saber o que cada método pretende fazer, e não como ele o faz, porque isso
pode mudar com o tempo.
Essa frase vem do livro Design Patterns, de Eric Gamma et al., que é cultuado no meio da
orientação a objetos.
Sempre que acessamos um objeto, utilizamos sua interface. Existem diversas analogias fáceis no
P á g i n a | 64
mundo real:
Quando você dirige um carro, o que lhe importa são os pedais e o volante (interface), e não o motor
o qual você está usando (implementação). É claro: um motor diferente pode lhe dar melhores
resultados, mas o que ele faz é o mesmo que um motor menos potente, a diferença está em como
ele faz. Para trocar um carro a álcool por um a gasolina, você não precisa reaprender a dirigir
(trocar a implementação dos métodos não precisa mudar a interface, fazendo com que as outras
classes continuem usando-os da mesma maneira).
Todos os celulares fazem a mesma coisa (interface). Eles têm maneiras (métodos) de discar,
ligar, desligar, atender, etc. O que muda é como eles o fazem (implementação). Mas repare que,
para efetuar uma ligação, pouco importa se o celular é iPhone ou Android, visto que isso fica
encapsulado na implementação (aqui são os circuitos).
class Cliente {
private String nome; private String endereco; private String cpf;
private int idade;
// ..
}
Se alguém tentar criar um Cliente e não usar o mudaCPF para alterar um CPF
diretamente,
receberá um erro de compilação, já que o atributo CPF é privado. E quando você não precisar
verificar o CPF de quem tem mais de 60 anos? Seu método fica o seguinte:
public void mudaCPF(String cpf) {
if (this.idade <= 60) {
validaCPF(cpf);
}
this.cpf = cpf;
}
O controle sobre o CPF está centralizado: ninguém consegue acessá-lo sem passar por aí. A
classe Cliente é a única responsável pelos seus próprios atributos!
P á g i n a | 65
Precisamos, então, arranjar uma maneira de fazer esse acesso. Sempre que precisamos arrumar
uma forma de fazer alguma coisa com um objeto, utilizamos os métodos! Assim, criemos um
método, digamos pegaSaldo, para realizar essa simples tarefa:
class Conta {
private double saldo; // outros atributos omitidos
public double pegaSaldo() {
return this.saldo;
}
// deposita() e saca() omitidos
}
Para acessarmos o saldo de uma conta, podemos fazer:
class TestaAcessoComPegaSaldo {
public static void main(String[] args) {
Conta minhaConta = new Conta();
minhaConta.deposita(1000);
System.out.println("Saldo: " + minhaConta.pegaSaldo());
}
}
A fim de permitir o acesso aos atributos (já que eles são private) de uma maneira controlada, a
prática mais comum é criar dois métodos, um que retorna o valor, e outro o qual muda o valor.
A convenção para esses métodos é de colocar a palavra get ou set antes do nome do atributo.
Por exemplo, a nossa conta com saldo, limite e titular fica assim caso desejarmos dar o
acesso da leitura e escrita a todos os atributos:
class Conta {
É uma má prática criar uma classe e, logo em seguida, fazer getters e setters para todos seus
atributos. Você só deve criar um getter ou setter se tiver a real necessidade. Repare que, nesse exemplo,
setSaldo não deveria ter sido criado, pois queremos que todos usem deposita() e saca()
.
Outro detalhe importante: um método getX não, necessariamente, retorna o valor de um atributo
que chama X do objeto em questão. Isso é interessante para o encapsulamento. Imagine a situação:
queremos que o banco sempre mostre, como saldo, o valor do limite somado ao saldo (uma prática
comum dos bancos que costumam iludir seus clientes). Poderíamos sempre chamar
c.getLimite() + c.getSaldo() , mas isso geraria uma situação de replace all quando
precisássemos mudar como o saldo é mostrado. Podemos encapsular isso em um método e, por que
não, dentro do próprio getSaldo? Repare:
class Conta {
O código acima não possibilita a chamada do método g e t L i m i t e () , posto que ele não existe. E
nem deve existir enquanto não houver essa necessidade. O método getSaldo() não devolve
simplesmente o saldo , e sim o que queremos que seja mostrado como se fosse o saldo . Utilizar
getters e setters não só ajuda você a proteger seus atributos como também possibilita ter de mudar
algo em um só lugar; chamamos isso de encapsulamento, pois esconde a maneira pela qual os objetos
guardam seus dados. É uma prática muito importante.
Nossa classe está totalmente pronta? Isto é, existe a chance de ela ficar com saldo menor que 0?
Pode parecer que não, mas e se depositarmos um valor negativo na conta? Ficaríamos com menos
dinheiro que o permitido embora não esperássemos por isso. A fim de nos proteger disso, basta
mudarmos o método deposita() para que ele verifique se o valor é necessariamente positivo.
Depois disso, precisaríamos mudar mais algum outro código? A resposta é não, graças ao
encapsulamento dos nossos dados.
3.4 CONSTRUTORES
Quando usamos a palavra-chave new, estamos construindo um objeto. Sempre quando o new é
chamado, ele executa o construtor da classe. O construtor da classe é um bloco declarado com o
mesmo nome que a classe:
class Conta {
String titular; int numero; double saldo;
// construtor
Conta() {
System.out.println("Construindo uma conta.");
}
// ..
}
A mensagem "construindo uma conta" aparecerá. É como uma rotina de inicialização que é chamada
sempre que um novo objeto é criado. Um construtor pode parecer, mas não é um método.
P á g i n a | 68
O CONSTRUTOR DEFAULT
Até agora, as nossas classes não tinham nenhum construtor. Então, como é que era possível dar
new se todo new chama um construtor obrigatoriamente?
Quando você não declara nenhum construtor na sua classe, o Java cria um para você. Esse
construtor é o construtor default. Ele não recebe nenhum argumento e o seu corpo é vazio.
A partir do momento que você declara um construtor, o construtor default não é mais fornecido.
O interessante é que um construtor pode receber um argumento, inicializando, assim, algum tipo de
informação:
class Conta {
String titular;
int numero;
double saldo;
// construtor
Conta(String titular) {
this.titular = titular;
}
// ..
}
Esse construtor recebe o titular da conta. Desta maneira, quando criarmos uma conta, ela já terá
um
determinado titular.
A ideia é bem simples. Se toda conta precisa de um titular, como obrigar todos os objetos que
forem criados a ter um valor desse tipo? É só criar um único construtor que receba essa String!
O construtor se resume a isso! Dar possibilidades ou obrigar o usuário de uma classe a passar
argumentos para o objeto durante o seu processo de criação.
P á g i n a | 69
Por exemplo, não podemos abrir um arquivo para leitura sem dizer qual é o nome do arquivo que
desejamos ler. Portanto, nada mais natural que passar uma String representando o nome de um
arquivo na hora de criar um objeto do tipo de leitura de arquivo, e que isso seja obrigatório.
Você pode ter mais de um construtor na sua classe, e, no momento do new, o construtor apropriado
será escolhido.
Um construtor só pode rodar durante a construção do objeto, isto é, você nunca conseguirá chamar
o construtor em um objeto já construído. Porém, durante a construção de um objeto, você pode fazer
com que um construtor chame outro para não ter de ficar copiando e colando:
class Conta {
String
titular;
int
numero;
double
saldo;
// construtor
Conta (String titular) {
// faz mais uma série de
inicializações e configurações
this.titular = titular;
}
//..
}
Você pode ter mais de um construtor na sua classe, e, no momento do new, o construtor apropriado será
escolhido.
Existe um outro motivo, o outro lado dos construtores: facilidade. Às vezes, criamos um
construtor que recebe diversos argumentos para não obrigar o usuário de uma classe a chamar diversos
métodos do tipo 'set’. No nosso exemplo do CPF, podemos forçar que a classe Cliente receba
no mínimo o CPF. Dessa maneira, um Cliente já será construído e terá um CPF válido.
Um construtor só pode rodar durante a construção do objeto, isto é, você nunca conseguirá chamar
o construtor em um objeto já construído. Porém, durante a construção de um objeto, você pode fazer
com que um construtor chame outro para não ter de ficar copiando e colando:
class Conta {
String
titular;
int
numero;
double
saldo;
// construtor
Conta (String titular) {
// faz mais uma série de
inicializações e configurações
this.titular = titular;
}
//..
}
class Conta {
private int totalDeContas;
//...
Conta() {
this.totalDeContas = this.totalDeContas + 1;
}
}
Quando criarmos duas contas, qual será o valor do totalDeContas de cada uma delas? Será um,
pois cada uma tem essa variável. O atributo é de cada objeto.
Seria interessante, então, que essa variável fosse única, compartilhada por todos os objetos dessa
classe. À vista disso, quando mudasse por meio de um objeto, o outro enxergaria o mesmo valor. Para
fazer isso em Java, declaramos a variável como static.
Quando declaramos um atributo como static, ele passa a não ser mais um atributo de cada
objeto, e sim um atributo da classe. A informação fica guardada pela classe e não é mais individual
para cada objeto.
Para acessarmos um atributo estático, não usamos a palavra-chave this, mas, sim, o nome da
classe:
class Conta {
Conta() {
Conta.totalDeContas = Conta.totalDeContas + 1;
}
}
Já que o atributo é privado, como podemos acessar essa informação a partir de outra classe?
P á g i n a | 73
class Conta {
private static int totalDeContas;
//...
Conta() {
Conta.totalDeContas = Conta.totalDeContas + 1;
}
Public int getTotalDeContas() {
return Conta.totalDeContas;
}
}
Precisamos criar uma conta antes de chamar o método. Isso não é legal, pois gostaríamos de saber
quantas contas existem sem precisar ter acesso a um objeto-conta. A ideia aqui é a mesma, transformar
esse método que todo objeto-conta tem em um método de toda a classe. Usamos a palavra static
de novo, mudando o método anterior.
Repare que estamos não chamando um método com uma referência a uma Conta, e sim usando
o nome da classe.
Métodos e atributos estáticos só podem acessar outros métodos e atributos estáticos da mesma
classe, o que faz todo sentido, dado que dentro de um método estático, não temos acesso à referência
this, pois um método estático é chamado por meio da classe, e não de um objeto.
Se uma classe só tem atributos e métodos estáticos, que conclusões podemos tirar? O que
lhe parece um método estático em casos como esses?
No caso de atributos booleanos, pode-se usar, no lugar do get, o sufixo is. Desse modo,
caso tivéssemos um atributo booleano ligado em vez de getLigado, poderíamos ter
isLigado.
2. Após deixar os atributos da classe Conta com acesso restrito (privado), tente criar uma Conta
na
3. classe TestaConta dentro do main e modificar ou ler os atributos da conta criada. O
que acontece?
4. Crie apenas os getters e setters necessários na sua classe Conta . Pense sempre se é preciso criar
cada um deles.
5. Não copie e cole! Aproveite para praticar a sintaxe. Logo, passaremos a usar o Eclipse e aí, sim,
teremos procedimentos mais simples destinados a esse tipo de tarefa.
6. Repare que o método calculaRendimento parece também um getter. Aliás, seria comum
alguém nomeá-lo de getRendimento . Getters não precisam apenas retornar atributos, eles
podem trabalhar com esses dados.
7. Altere suas classes que acessam e modificam atributos de uma Conta para utilizar os getters e
setters recém-criados.
8. Faça com que sua classe Conta possa receber, opcionalmente, o nome do titular da Conta
durante a criação do objeto.
9. (Opcional) Adicione um atributo, na classe Conta de tipo int, que se chama identificador.
Este deve ter um valor único para cada instância do tipo Conta . A primeira Conta instanciada
P á g i n a | 75
tem identificador 1, a segunda, 2, e assim por diante. Você deve utilizar os recursos aprendidos
aqui na resolução desse problema.
10. (Opcional) Como garantir que datas como 31/2/2021 não sejam aceitas pela sua classe Data ?
11. (Opcional) Imagine que tenhamos a classe PessoaFisica a qual tem um CPF como
atributo. Como garantir que alguma pessoa física tenha CPF inválido e tampouco seja criada uma
PessoaFisica sem CPF inicial? (Suponha que já exista um algoritmo de validação de CPF:
este deve passar por um método valida(String x) )
3.7 DESAFIOS
1. Por que esse código não compila?
class Teste {
int x = 37;
public static void main(String [] args) {
System.out.println(x);
}
}
Imagine que haja uma classe FabricaDeCarro, e quero garantir que só exista um objeto
desse tipo em toda a memória. Não há uma palavra-chave especial para isso em Java. Então,
teríamos de fazer nossa classe de tal maneira que ela respeitasse essa nossa necessidade. Como
faríamos isso? (Pesquise: singleton design pattern.)
CAPÍTULO 4
HERANÇA E POLIMORFISMO
4.1 HERANÇA
// outros métodos
}
P á g i n a | 77
Poderíamos ter deixado a classe Funcionario mais genérica, mantendo nela a senha de acesso
e o número de funcionários gerenciados. Caso o funcionário não fosse um gerente, deixaríamos esses
atributos vazios.
Essa é uma possibilidade, porém, com o tempo, podemos passar a ter muito atributos opcionais,
e a classe ficaria estranha. E em relação aos métodos? A classe Gerente tem o método
autentica, que não faz sentido existir em um funcionário o qual não é gerente.
Além disso, se um dia precisarmos adicionar uma nova informação a todos os funcionários,
precisaremos passar por todas as classes de funcionário e adicionar esse atributo. O problema acontece
novamente por não centralizarmos os dados principais do funcionário em um único lugar.
Existe um jeito, em Java, de relacionarmos uma classe de tal maneira que uma delas herda tudo que a
outra tem. Isso é uma relação de classe mãe e classe filha. No nosso caso, gostaríamos de fazer com
que o Gerente tivesse tudo que um Funcionario tem, gostaríamos que ela fosse uma extensão
de Funcionario . Fazemos isso por meio da palavra-chave extends
Em todo momento que criarmos um objeto do tipo Gerente , este terá também os
atributos definidos na classe Funcionario , pois um Gerente é um Funcionario :
consegue acessá-los diretamente. Para acessar um membro privado na filha indiretamente, seria
necessário que a mãe expusesse um outro método visível que invocasse esse atributo ou método privado.
E se precisamos acessar os atributos que herdamos? Não gostaríamos de deixar os atributos de
Funcionario , public , pois, dessa maneira, qualquer um poderia alterar os atributos dos
objetos desse tipo. Existe um outro modificador de acesso, o protected , o qual fica entre o
private e o public . Um atributo protected só pode ser acessado (visível) pela própria
classe, suas subclasses e classes encontradas no mesmo pacote.
Então por que usar private ? Depois de um tempo programando orientado a objetos, você
começará a sentir que nem sempre é uma boa ideia deixar a classe filha acessar os atributos da classe
mãe, pois isso quebra um pouco a percepção de que só aquela classe deveria manipular seus atributos.
Essa é uma discussão um tanto mais avançada.
Além disso, não só as subclasses, mas também as outras classes que se encontram no mesmo
pacote podem acessar os atributos protected . Veja outras alternativas ao protected no exercício
de discussão em sala de aula juntamente com o instrutor.
Uma classe pode ter várias filhas, mas apenas uma mãe. É a chamada herança simples do Java.
P á g i n a | 80
4.3 POLIMORFISMO
O que guarda uma variável do tipo Funcionario? Uma referência para um Funcionario,
nunca o objeto em si.
Na herança, vimos que todo Gerente é um Funcionario, pois é uma extensão deste.
Podemos nos referir a um Gerente como sendo um Funcionario. Se alguém precisa falar
com um Funcionario do banco, pode falar com um Gerente! Por quê? Pois, Gerente é
um Funcionario. Essa é a semântica da herança.
Qual é o retorno desse método? 500 ou 750? No Java, a invocação de método sempre será decidida
em tempo de execução. O Java procurará o objeto na memória e, aí sim, decidirá qual método deve
ser chamado, sempre relacionando com sua classe de verdade, e não com a que estamos usando para
referenciá-lo. Apesar de estarmos nos referenciando a esse Gerente como sendo um
Funcionario, o método executado é o do Gerente. O retorno é 750.
Parece estranho criar um gerente e referenciá-lo como apenas um funcionário. Por que faríamos
isso? Na verdade, a situação que costuma aparecer é a que temos um método que recebe um argumento
do tipo Funcionario:
P á g i n a | 81
class ControleDeBonificacoes {
private double totalDeBonificacoes = 0;
Repare que conseguimos passar um Gerente para um método que recebe um Funcionario
como argumento. Pense em uma porta na agência bancária com o seguinte aviso: "Permitida a entrada
apenas de funcionários". Um gerente pode passar nessa porta? Sim, pois Gerente é um
Funcionario.
Qual será o valor resultante? Não importa que dentro do método registra o
ControleDeBonificacoes receba Funcionario. Quando ele receber um objeto que
realmente é um Gerente , o seu método reescrito será invocado. Reafirmando: não importa como
nos referenciamos a um objeto, o método a ser invocado é sempre o do próprio objeto.
No dia em que criarmos uma classe Secretaria, por exemplo, que é filha de Funcionario ,
precisaremos mudar a classe de ControleDeBonificacoes ? Não. Basta a classe
S e c r e t a r i a reescrever os métodos que lhe parecerem necessários. É exatamente esse o poder
do polimorfismo juntamente com a reescrita de método: diminuir o acoplamento entre as classes para
evitar que novos códigos resultem em modificações em inúmeros lugares.
Repare que quem criou ControleDeBonificacoes pode nunca ter imaginado a criação da
classe Secretaria ou Engenheiro. Contudo, não será necessário reimplementar esse
controle em cada nova classe: reaproveitamos aquele código.
P á g i n a | 82
Note que o uso de herança aumenta o acoplamento entre as classes, isto é, o quanto uma
classe depende de outra. A relação entre as classes mãe e filha é muito forte e isso acaba fazendo
com que o programador das classes filhas tenha de conhecer a implementação da classe mãe, e
vice-versa – fica difícil fazer uma mudança pontual no sistema.
Por exemplo, imagine se mudássemos algo na nossa classe Funcionario, mas não
quiséssemos que todos os funcionários sofressem a mesma mudança. Precisaríamos passar por
cada uma das filhas de Funcionario, verificando se ela se comporta como deveria, ou se
deveríamos sobrescrever o tal método modificado.
Esse é um problema da herança, e não do polimorfismo, que resolveremos mais tarde com a
ajuda de Interfaces.
Como tiramos proveito do polimorfismo? Imagine que tenhamos uma classe de relatório:
Uma discussão muito atual é sobre o abuso no uso da herança. Algumas pessoas usam herança
apenas para reaproveitar o código, quando poderiam ter feito uma composição. Procure sobre
herança versus composição.
Mesmo depois de reescrever um método da classe mãe, a classe filha ainda pode acessar o método
antigo. Isso é feito por meio da palavra-chave super.método() . Algo parecido ocorre entre
os construtores das classes, o quê?
1. Teremos mais de um tipo de conta no nosso sistema, então precisaremos de uma nova
tela para cadastrar os diferentes tipos de conta. Essa tela já está pronta, e, para utilizá-la, só
precisamos alterar a classe que estamos chamando no método main() no TestaContas.java :
package br.com.caelum.contas.main;
import br.com.caelum.javafx.api.main.SistemaBancario;
SistemaBancario.mostraTela(false);
// TelaDeContas.main(args);
Entraremos na tela de criação de contas com o objetivo de verificar o que precisaremos implementar
para o sistema funcionar. Desse modo, clique no botão Nova Conta. A seguinte tela aparecerá:
Podemos perceber que, além das informações que já tínhamos na conta, temos agora o tipo: se queremos
uma conta-corrente ou uma conta poupança. Então, criemos as classes correspondentes.
Crie a classe ContaCorrente no pacote br.com.caelum.contas.modelo e faça com que ela seja filha da
P á g i n a | 85
classe Conta .
Crie a classe ContaPoupanca no pacote br.com.caelum.contas.modelo e faça com que ela seja filha da
classe Conta .
Dicas: a seguir, algumas informações importantes sobre a classe Evento (a qual é responsável
pela tela Nova Conta):
• Observe, na figura anterior, que agora precisamos escolher que tipo de conta queremos
criar (conta-corrente ou conta poupança) e, portanto, teremos de recuperar o tipo da
conta escolhido e criar a conta correspondente.
Para isso, ao invés de criar um objeto do tipo 'Conta', usaremos o método getSelecionadoNoRadio
do objeto evento com o intuito de pegar o tipo, fazer um if para verificar e só depois criar o objeto
do tipo correspondente. A seguir, um trecho do código:
//complete o código
4. Apesar de já conseguirmos criar os dois tipos de contas, nossa lista não consegue exibir
o tipo de cada conta na lista da tela inicial. Para resolver isso, podemos criar um método getTipo
em cada uma das classes que representam nossas contas, fazendo com que a conta-corrente
devolva a string "Conta Corrente" e a conta poupança devolva a string "Conta Poupança".
if (this.conta.getTipo().equals("Conta Corrente")){
//complete o código
Dica: ao tentarmos chamar o método getTipo , o Eclipse reclamou que este não existe na classe
Conta, apesar de existir nas classes filhas. O que fazer para resolver isso?
7. O código compila, mas temos um outro problema. A lógica do nosso saque vazou para
a classe ManipuladorDeContas . Se algum dia precisarmos alterar o valor da taxa no saque,
teríamos que mudar em todos os lugares onde fazemos uso do método saca . Essa lógica deveria
estar encapsulada dentro do método saca de cada conta. Como resolver isso?
Dica: repare que, a fim de acessar o atributo saldo herdado da classe Conta , você precisará mudar
o modificador de visibilidade de saldo para protected .
Agora que a lógica está encapsulada, você precisa corrigir o método saca da classe
8. Rode a classe TestaContas , adicione uma conta de cada tipo e veja se o tipo é
apresentado corretamente na lista de contas da tela inicial.
Agora, clique na conta-corrente apresentada na lista para abrir a tela de detalhes de contas. Teste
as operações de saque e depósito e perceba que a conta apresenta o comportamento de uma conta-
corrente, conforme o esperado.
• O método transfere da classe Conta deve receber, como parâmetro, duas variáveis, uma
referente ao valor a ser transferido, e outra para a conta de destino.
P á g i n a | 87
• No corpo do método, por meio do objeto do tipo Evento, deve-se invocar o método
getSelecionadoNoCombo("destino");
• Em seguida, por meio do objeto do tipo Conta, deve-se chamar o método transfere da
classe Conta para que a transferência seja, de fato, realizada. Rode de novo a aplicação e
teste a operação de transferência.
Se o mudarmos para:
Compila? Roda? O que muda? Qual é a utilidade disso? Realmente, essa não é a maneira mais
útil do polimorfismo. Porém, existe uma utilidade ao declararmos uma variável de um tipo menos
específico do que o objeto realmente é, como fazemos na classe ManipuladorDeContas .
É extremamente importante perceber que não importa como nos referimos a um objeto, o método
a ser invocado é sempre o mesmo! A JVM descobrirá, em tempo de execução, qual deve ser
invocado, dependendo do tipo daquele objeto, e não considerando como fazemos referência a
ele.
11. (Opcional) A nossa classe Conta devolve a palavra "Conta" no método getTipo . Use a
palavra- chave super nos métodos getTipo reescritos nas classes filhas para não ter de reescrever
a palavra "Conta" ao devolver os textos "Conta Corrente" e "Conta Poupança".
2. (Opcional) Se você precisasse criar uma classe ContaInvestimento , e seu método saca fosse
complicadíssimo, precisaria alterar a classe ManipuladorDeContas ?
P á g i n a | 88
Referências Bibliográficas