Exercícios 3.3: variáveis e tipos primitivos
Na empresa em que trabalhamos, há tabelas com o quanto foi gasto em cada mês. Para fechar o balanço do primeiro trimestre, precisamos somar o gasto total. Sabendo que, em janeiro, foram gastos 15.000 reais, em fevereiro, 23.000 reais, e, em março, 17.000 reais, faça um programa que calcule e imprima o gasto total no trimestre e a média mensal de gastos. Siga esses passos:
- Crie uma classe chamada
BalancoTrimestral
com um bloco main, como nos exemplos anteriores; - Dentro do
main
(o miolo do programa), declare uma variável inteira chamadagastosJaneiro
e inicialize-a com 15.000; - Crie também as variáveis
gastosFevereiro
egastosMarco
, inicializando-as com 23.000 e 17.000, respectivamente. Utilize uma linha para cada declaração; - Crie uma variável chamada
gastosTrimestre
e inicialize-a com a soma das outras três variáveis; - Crie uma variável chamada
mediaPorMes
e inicialize-a com a divisão degastosTrimestre
por três. - Imprima a variável
gastosTrimestre
.
Abaixo o código completo:
class BalancoTrimestral { public static void main(String[] args) { int gastosJaneiro = 15000; int gastosFevereiro = 23000; int gastosMarco = 17000; int gastosTrimestre = gastosJaneiro + gastosFevereiro + gastosMarco; System.out.println("Gasto do trimestre: R$" + gastosTrimestre); int mediaPorMes = gastosTrimestre / 3; System.out.println("Média mensal: R$" + mediaPorMes); } }
- Crie uma classe chamada
Exercícios 3.13: fixação de sintaxe
Mais exercícios de fixação de sintaxe. Para quem já conhece um pouco de Java, pode ser muito simples. Mas recomendamos fortemente que você faça os exercícios a fim de se acostumar com erros de compilação, mensagens do javac, convenção de código, etc.
Apesar de extremamente simples, precisamos praticar a sintaxe que estamos aprendendo. Para cada
exercício, crie um novo arquivo com extensão .java e declare aquele estranho cabeçalho, dando nome
a uma classe e com um bloco main
dentro dele:
class ExercicioX {
public static void main(String[] args) {
// Seu exercício vai aqui.
}
}
Não copie e cole de um exercício já existente! Aproveite para praticar.
Imprima todos os números de 150 a 300.
class ImprimeIntervalo { public static void main(String[] args) { int i = 150; while (i<=300){ System.out.println(i); i++; } } }
ou
class ImprimeIntervalo { public static void main(String[] args) { for (int i = 150; i<=300; i++){ System.out.println(i); } } }
Imprima a soma de 1 até 1000.
class ImprimeSoma { public static void main(String[] args) { int soma = 0; int i = 1; while (i<=1000){ soma = soma + i; i++; } System.out.println(i); } }
ou
class ImprimeSoma { public static void main(String[] args) { soma = 0; for (int i = 1; i<=1000; i++){ soma = soma + i; } System.out.println(i); } }
Imprima todos os múltiplos de 3, entre 1 e 100.
class MultiplosDeTresAteCem { public static void main (String[] args) { for (int i = 1; i < 100; i++ ){ if (i % 3 == 0) { System.out.println(i); } } } }
ou, entre outras tantas opções, a mais simples:
class MultiplosDeTresAteCem { public static void main (String[] args) { for (int i = 1; i < 100; i += 3 ){ System.out.println(i); } } }
Imprima os fatoriais de 1 a 10.
O fatorial de um número n é n * (n-1) * (n-2) * ... * 1. Lembre-se de utilizar os parênteses.
O fatorial de 0 é 1
O fatorial de 1 é (0!) * 1 = 1
O fatorial de 2 é (1!) * 2 = 2
O fatorial de 3 é (2!) * 3 = 6
O fatorial de 4 é (3!) * 4 = 24
Faça um for que inicie uma variável
n
(número) como 1, efatorial
(resultado) como 1, variandon
de 1 até 10:int fatorial = 1; for (int n = 1; n <= 10; n++) { }
class Fatorial { public static void main (String[] args) { int fatorial = 1; for (int n = 1; n <= 10; n++) { fatorial = fatorial * n; System.out.println("fat(" + n + ") = " + fatorial); } } }
No código do exercício anterior, aumente a quantidade de números que terão os fatoriais impressos: até 20, 30 e 40. Em um determinado momento, além desse cálculo demorar, começará a mostrar respostas completamente erradas. Por quê?
Mude de
int
paralong
a fim de ver alguma mudança.Resposta:
Isso acontece porque, a partir de 16!, o valor ultrapassa o limite superior do tipo
int
. O tipolong
consegue armazenar o cálculo dos fatoriais até 21!. Teste!(Opcional) Imprima os primeiros números da série de Fibonacci até passar de 100. A série de Fibonacci é a seguinte: 0, 1, 1, 2, 3, 5, 8, 13, 21, etc. Para calculá-la, o primeiro elemento vale 0, o segundo vale 1, daí por diante, o n-ésimo elemento vale o (n-1)-ésimo elemento somado ao (n-2)-ésimo elemento (ex: 8 = 5 + 3):
class Fibonacci { public static void main(String[] args) { int anterior = 0; int atual = 1; while (atual < 100) { System.out.println(atual); int proximo = anterior + atual; anterior = atual; atual = proximo; } System.out.println(atual); } }
(Opcional) Escreva um programa no qual, dada uma variável
x
com algum valor inteiro, temos um novox
de acordo com a seguinte regra:- Se
x
é par,x = x / 2
. - Se
x
é impar,x = 3 * x + 1
. - Imprime
x
. - O programa deve parar quando
x
tiver o valor final de 1. Por exemplo, parax = 13
, a saída será:
40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1
Imprimindo sem pular linha
Um detalhe importante: uma quebra de linha é impressa toda vez que chamamos
println
. Para não pular uma linha, usamos o código a seguir:System.out.print(variavel);
class TresNMaisUm { public static void main(String[] args) { int x = 13; System.out.println("Iniciando...\n" + x); while (x != 1) { if (x % 2 == 0) { x = x / 2; } else { x = 3 * x + 1; } System.out.println(x); } } }
- Se
(Opcional) Imprima a seguinte tabela usando
for
s encadeados:1 2 4 3 6 9 4 8 12 16 n n*2 n*3 .... n*n
class Triangulo { public static void main(String[] args) { int numero = 5; for (int linha = 1; linha <= numero; linha++) { for (int coluna = 1; coluna <= linha; coluna++) { System.out.print(linha * coluna + " "); } System.out.println(); } } }
Desafios 3.14: Fibonacci
Faça o exercício da série de Fibonacci usando apenas duas variáveis.
class Desafio { public static void main(String[] args) { int anterior = 0; int atual = 1; while (atual < 100) { System.out.println(atual); atual = anterior + atual; anterior = atual - anterior; } System.out.println(atual); } }
Exercícios 4.12: orientação a objetos
O modelo da conta a seguir será utilizado para os exercícios dos próximos capítulos.
O objetivo aqui é criar um sistema para gerenciar as contas de um Banco
. Os exercícios desse capítulo são extremamente importantes.
Modele uma conta. A ideia nesse momento é 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, ela deve fazer as seguintes ações:saca
, para retirar um valor do saldo;deposita
, para adicionar um valor ao saldo;calculaRendimento
para devolver o rendimento mensal dessa conta, que é de 10% sobre o saldo.Resposta:
Modelando uma conta
Toda conta tem:
- String titular;
- int numero;
- String agencia;
- double saldo;
- String dataDeAbertura;
Toda conta faz:
saca
: retira uma determinada quantia do saldo da conta;deposita
: adiciona uma determinada quantia ao saldo da conta;calculaRendimento
: devolve o quanto essa conta rende por mês.
Transforme o modelo acima em uma classe Java. Teste a classe usando uma outra classe que tenha o
main
. Você deve criar a classe da conta com o nomeConta
, mas pode nomear como quiser a classe de testes. Por exemplo, pode chamá-laTestaConta
. Contudo, ela deve necessariamente ter o métodomain
.A classe Conta deve conter, além dos atributos mencionados anteriormente, pelo menos os seguintes métodos:
saca
, que recebe umvalor
como parâmetro e o retira do saldo da conta;deposita
, que recebe umvalor
como parâmetro e o adiciona ao saldo da conta;calculaRendimento
, que não recebe parâmetro algum e devolve o valor do saldo multiplicado por 0.1.
Um esboço da classe:
class Conta { double saldo; // Seus outros atributos e métodos. void saca(double valor) { // O que fazer aqui dentro? } void deposita(double valor) { // O que fazer aqui dentro? } double calculaRendimento() { // O que fazer aqui dentro? } }
Você pode (e deve) compilar seu arquivo Java sem que ainda tenha terminado sua classe
Conta
. Isso evitará que você receba dezenas de erros de compilação de uma vez só. Crie a classeConta
, ponha seus atributos e, antes de colocar qualquer método, compile o arquivo Java. O arquivoConta.class
será gerado, mas não podemos executá-lo, visto que essa classe não tem ummain
. De qualquer forma, verificamos, assim, que nossa classeConta
já está tomando forma e está escrita em sintaxe correta.Esse é um processo incremental. Procure desenvolver seus exercícios assim para não descobrir só no fim do caminho que algo estava muito errado.
Um esboço da classe que tem o
main
:class TestaConta { public static void main(String[] args) { Conta c1 = new Conta(); c1.titular = "Hugo"; c1.numero = 123; c1.agencia = "45678-9"; c1.saldo = 50.0; c1.dataDeAbertura = "04/06/2015"; c1.deposita(100.0); System.out.println("saldo atual:" + c1.saldo); System.out.println("rendimento mensal:" + c1.calculaRendimento()); } }
Incremente essa classe. Faça outros testes, imprima outros atributos e invoque os métodos que você criou a mais.
Lembre-se de seguir a convenção Java, isso é importantíssimo. Preste atenção nas maiúsculas e minúsculas, seguindo este exemplo:
nomeDeAtributo
,nomeDeMetodo
,nomeDeVariavel
,NomeDeClasse
, etc.Todas as classes no mesmo arquivo?
Você até pode colocar todas as classes no mesmo arquivo e compilar apenas esse arquivo. Ele gerará um
.class
para cada classe presente nele.Porém, por uma questão de organização, é 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.Essa separação não é importante nesse momento do aprendizado, mas se quiser praticar sem ter que compilar classe por classe, você pode dizer para o
javac
compilar todos os arquivos Java de uma vez:javac *.java
Abaixo a resposta completa desse item:
class Conta { String titular; int numero; String agencia; double saldo; String dataDeAbertura; void saca (double valor) { this.saldo -= valor; } void deposita (double valor) { this.saldo += valor; } double calculaRendimento() { return this.saldo * 0.1; } }
Na classe
Conta
, crie um métodorecuperaDadosParaImpressao()
que não recebe parâmetro, mas devolve o texto com todas as informações da nossa conta para efetuarmos a impressão.Dessa maneira, você não precisa ficar copiando e colando um monte de
System.out.println()
para cada mudança e teste que fizer com os seus funcionários, você simplesmente fará:Conta c1 = new Conta(); // brincadeiras com c1.... System.out.println(c1.recuperaDadosParaImpressao());
Veremos, mais à frente, o método
toString
, que é uma solução muito mais elegante para mostrar a representação de um objeto comoString
, além de não jogar tudo para oSystem.out
(somente se você o desejar).O esqueleto do método ficaria assim:
class Conta { // Seus outros atributos e métodos. String recuperaDadosParaImpressao() { String dados = "Titular: " + this.titular; dados += "\nNúmero: " + this.numero; // Imprimir aqui os outros atributos. // Também pode imprimir this.calculaRendimento() return dados; } }
Abaixo está a resposta completa desse item:
class Conta { // Outros atributos e métodos. String recuperaDadosParaImpressao() { String dados = "Titular: " + this.titular; dados += "\nNúmero: " + this.numero; dados += "\nAgência: " + this.agencia; dados += "\nSaldo: R$" + this.saldo; dados += "\nData de abertura: " + this.dataDeAbertura; return dados; } }
Na classe de teste dentro do bloco
main
, construa duas contas com onew
e compare-as com o==
. E se elas tiverem os mesmos atributos? Para isso, você precisará criar outra referência:Conta c1 = new Conta(); c1.titular = "Danilo"; c1.saldo = 100; Conta c2 = new Conta(); c2.titular = "Danilo"; c2.saldo = 100; if (c1 == c2) { System.out.println("iguais"); } else { System.out.println("diferentes"); }
Em ambos os casos, temos
false
como resposta. Isso é porque variáveis guardam apenas as referências! Por mais que dois objetos diferentes tenham as mesmas informações, cada um deles é um objeto à parte.Você pode ver isso de uma forma simples: se alterar o
c1
, note que oc2
não é alterado junto. Cada um é um objeto diferente, e cada variável (c1
ec2
) referencia a um deles.Agora crie duas referências à mesma conta e compare-as com o
==
. Tire suas conclusões. Para criar duas referências à mesma conta:Conta c1 = new Conta(): c1.titular = "Hugo"; c1.saldo = 100; c2 = c1;
O que acontece com o
if
do exercício anterior?Resposta:
Agora, sim, obtemos
true
. Isso porque, de fato, ambas as variáveis têm referências ao mesmo objeto. Verifique: mude o titular dac1
para Mariana e imprimac2.titular
. Você notará que o nome mudou!(Opcional) Em vez de utilizar uma
String
para representar a data, crie uma outra classe chamadaData
. Ela deve ter três camposint
: 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 aConta
passou a ter referência a umCliente
).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 umaData
e atribua-a àConta
:Conta c1 = new Conta(); //... Data data = new Data(); // ligação! c1.dataDeAbertura = data;
Faça o desenho do estado da memória quando criarmos um
Conta
. Uma possibilidade é o arquivoData.java
ficar assim:class Data { int dia; int mes; int ano; void preencheData (int dia, int mes, int ano) { this.dia = dia; this.mes = mes; this.ano = ano; } }
No arquivo
Conta.java
, altere o atributo para a data:class Conta { // outros atributos Data dataDeAbertura; // metodos }
Finalmente, no arquivo
TestaConta.java
:class TestaConta { public static void main(String[] args) { Conta c1 = new Conta(); c1.titular = "Hugo"; c1.saldo = 50; c1.deposita(100); // adicionando a data como tipo c1.dataDeAbertura = new Data(); c1.dataDeAbertura.preencheData(1, 7, 2009); System.out.println(c1.recuperaDadosParaImpressao()); } }
(Opcional) Modifique seu método
recuperaDadosParaImpressao
para que ele devolva o valor dadataDeAbertura
daquelaConta
:class Conta { // Seus outros atributos e métodos. Data dataDeAbertura; String recuperaDadosParaImpressao() { String dados = "\nTitular: " + this.titular; // Imprimir aqui os outros atributos. dados += "\nDia: " + this.dataDeAbertura.dia; dados += "\nMês: " + this.dataDeAbertura.mes; dados += "\nAno: " + this.dataDeAbertura.ano; return dados; } }
Teste-o. O que acontece se chamarmos o método
recuperaDadosParaImpressao
antes de atribuir uma data a essaConta
?Resposta: dia, mês e ano serão apresentados com o valor 0.
class TestaConta { public static void main(String[] args) { Conta c1 = new Conta(); c1.titular = "Hugo"; c1.agencia = "12-x"; c1.numero = 557890; c1.saldo = 50; c1.deposita(100); // Adicionando a data como tipo c1.dataDeAbertura = new Data(); //Chamando o método `recuperaDadosParaImpressao` antes de atribuirmos uma data para essa `Conta` System.out.println(c1.recuperaDadosParaImpressao()); c1.dataDeAbertura.preencheData(1, 7, 2009); } }
(Opcional) O que acontece se você tentar acessar um atributo diretamente na classe? Por exemplo:
Conta.saldo = 1234;
Esse código faz sentido? E este:
Conta.calculaRendimento();
Faz sentido perguntar para o esquema da Conta seu valor anual?
(Opcional e avançado) Crie um método na classe
Data
que devolva o valor formatado da data, isto é, devolva uma String no formato "dia/mês/ano". Tudo isso para que o métodorecuperaDadosParaImpressao
da classeConta
possa ficar assim:class Conta { // Atributos e métodos. String recuperaDadosParaImpressao() { // Imprime outros atributos. dados += "\nData de abertura: " + this.dataDeAbertura.formatada(); return dados; } }
Resposta:
class Data { // Atributos e método preencheData String formatada() { return this.dia + "/" + this.mes + "/" + this.ano; } }
Desafios 4.13
Um método pode chamar-se 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); }
Aqui imprimirá a sequência de Fibonacci até a sexta posição, isto é: 1, 1, 2, 3, 5, 8.
Este método
calculaFibonacci
não pode ter nenhum laço, só pode chamar-se a si mesmo como método. Pense nele como uma função que usa a própria função para calcular o resultado.Resposta:
class Fibonacci{ public int calculaFibonacci(int n) { if (n <= 2) { return 1; } else { return calculaFibonacci(n-1) + calculaFibonacci(n-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)?
Resposta:
Dessa forma, o código fica muito mais lento, porque ele não consegue aproveitar os Fibonaccis já calculados anteriormente. E, pior ainda, ele abre o cálculo de Fibonaccis exponencialmente, uma vez que, para calcular o Fibonacci de um número, é preciso somar os dois anteriores.
Escreva o método recursivo novamente usando apenas uma linha. Para isso, pesquise sobre o operador condicional ternário (ternary operator).
Resposta:
public static int calculaFibonacci(int n) { return (n <= 2) ? 1 : calculaFibonacci(n-1) + calculaFibonacci(n-2); }
Exercícios 5.8: encapsulamento, construtores e static
Adicione o modificador de visibilidade (
private
, se necessário) para cada atributo e método da classeConta
. Tente criar umaConta
nomain
e modificar ou ler um de seus atributos privados. O que acontece?public class Conta { private String titular; private int numero; private String agencia; private double saldo; private Data dataDeAbertura; public void saca(double valor) {...} public void deposita(double valor) {...} public double calculaRendimento() {...} public String recuperaDadosParaImpressao() {...} }
Crie apenas os getters e setters necessários da sua classe
Conta
. Pense sempre se é preciso criar cada um deles. Por exemplo:class Conta { private String titular; // ... public String getTitular() { return this.titular; } public void setTitular(String titular) { this.titular = titular; } }
Não copie e cole! Aproveite para praticar sintaxe. Logo passaremos a usar o Eclipse e, aí sim, teremos procedimentos mais simples destinados a esse tipo de tarefa.
Repare que o método
calculaRendimento
parece também um getter. Aliás, seria comum alguém nomeá-lo degetRendimento
. Getters não precisam apenas retornar atributos, eles podem trabalhar com esses dados.Resposta completa para esse item:
public class Conta { private String titular; private int numero; private String agencia; private double saldo; private String dataDeAbertura; public double calculaRendimento() { return this.saldo * 0.1; } public String getTitular() { return this.titular; } public void setTitular (String titular) { this.titular = titular; } public int getNumero() { return this.numero; } public void setNumero (int numero) { this.numero = numero; } public String getAgencia() { return this.agencia; } public void setAgencia (String agencia) { this.agencia = agencia; } public double getSaldo() { return this.saldo; } public void deposita (double valor) { this.saldo += valor; } public void saca (double valor) { this.saldo -= valor; } public String getDataDeAbertura() { return this.dataDeAbertura; } public void setDataDeAbertura (String dataDeAbertura) { this.dataDeAbertura = dataDeAbertura; } }
Altere suas classes que acessam e modificam atributos de uma
Conta
para utilizar os getters e setters recém-criados.Por exemplo, onde você encontra:
c.titular = "Batman"; System.out.println(c.titular);
passa para:
c.setTitular("Batman"); System.out.println(c.getTitular());
Resposta completa para esse item:
class TestaConta { public static void main(String[] args) { Conta c1 = new Conta("Hugo"); c1.setNumero(123); c1.deposita(50); c1.setDataDeAbertura(new Data(1, 7, 2009)); System.out.println(c1.recuperaDadosParaImpressao()); } }
Faça com que sua classe
Conta
possa receber, opcionalmente, o nome do titular daConta
durante a criação do objeto. Utilize construtores para obter esse resultado.Dica: utilize um construtor sem argumentos também, pensando no caso em que a pessoa não queira passar o titular da
Conta
.Seria algo como:
class Conta { public Conta() { // Construtor sem argumentos. } public Conta(String titular) { // Construtor que recebe o titular. } }
Por que você precisa do construtor sem argumentos para que a passagem do nome seja opcional?
Resposta: a partir do momento em que declaramos um construtor, o construtor default não é mais fornecido, por isso, se quisermos ter a passagem do nome como opcional, teremos de ter duas versões de construtores: uma que não exige nada como parâmetro, e outra que exige uma String.
public class Conta { // atributos public Conta() {} public Conta(String titular) { this.titular = titular; } // métodos }
(Opcional) Adicione um atributo na classe
Conta
de tipoint
que se chamaidentificador
; este deve ter um valor único para cada instância do tipoConta
. A primeiraConta
instanciada tem identificador 1, a segunda, 2, e assim por diante. Você deve utilizar os recursos aprendidos aqui para resolver esse problema.Crie um getter para o identificador. Devemos ter um setter?
Resposta:
public class Conta { private int identificador; private static int proximoIdentificador; public Conta(String titular) { this.titular = titular; this.identificador = proximoIdentificador++; } public int getIdentificador() { return this.identificador; } // restante da classe }
Não faz sentido que o identificador tenha um setter, já que, pela lógica da aplicação, o
identificador
é um número único para cada funcionário no sistema.(Opcional) Como garantir que datas como 31/2/2021 não sejam aceitas pela sua classe
Data
?Resposta: você pode definir um construtor na classe
Data
que exija dia, mês e ano. Esse construtor invoca o métodopreencheData
, o qual invoca o métodoisDataViavel
que, por sua vez, faz a validação das datas válidas. Nesse momento, a única forma que você aprendeu de indicar se houve um erro é imprimir uma mensagem no terminal avisando sobre o erro, mas, mais para a frente, veremos uma forma muito mais elegante de tratar esses casos.public class Data { private int dia; private int mes; private int ano; public Data(int dia, int mes, int ano) { this.preencheData(dia, mes, ano); } private boolean isDataViavel(int dia, int mes, int ano) { if (dia <= 0 || mes <= 0) { return false; } int ultimoDiaDoMes = 31; // por padrao são 31 dias if (mes == 4 || mes == 6 || mes == 9 || mes == 11) { ultimoDiaDoMes = 30; } else if (mes == 2) { if (ano % 4 == 0) { ultimoDiaDoMes = 29; } else { ultimoDiaDoMes = 28; } } if (dia > ultimoDiaDoMes) { return false; } return true; } void preencheData(int dia, int mes, int ano) { if (!isDataViavel(dia, mes, ano)) { System.out.println("A data " + dia + "/" + mes + "/" + ano + " não existe!"); } else { this.dia = dia; this.mes = mes; this.ano = ano; } } String formatada() { return this.dia + "/" + this.mes + "/" + this.ano; } }
Faça testes com datas válidas e inválidas:
class TestaDataAberturaDaConta { public static void main(String[] args) { Conta c1 = new Conta(); c1.setTitular("Hugo"); ; c1.setAgencia("12-x");; c1.setNumero(557890); ; c1.deposita(50);; c1.deposita(100); // Adicionando a data como tipo c1.setDataDeAbertura(new Data(31, 2, 2021)); // Teste também com datas válidas System.out.println(c1.recuperaDadosParaImpressao()); } }
(Opcional) Suponha que tenhamos a classe
PessoaFisica
, a qual tem um CPF como atributo. Como garantir que pessoa física alguma tenha CPF inválido e tampouco seja criadaPessoaFisica
sem CPF inicial?Resposta: (Considere que já exista um algoritmo de validação de CPF: basta passá-lo por um método
valida(String x)...
)Você pode fazer a validação ser chamada no construtor e, por ora, imprimir a mensagem no console. No capítulo 12, veremos uma forma de realmente impedir a criação do objeto caso essa validação não passe.
Desafios 5.9
Por que esse código não compila?
class Teste { int x = 37; public static void main(String [] args) { System.out.println(x); } }
Resposta: O
main
é um método estático, isto é, ele não é do objeto, é da classe. Já o atributox
não tem a palavrastatic
e, portanto, é do objeto.Para rodar o
main
, não há necessidade nem garantia de termos um objeto do tipoTeste
, por isso não conseguimos garantir que ox
sequer existirá.Imagine a situação: há 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 teremos de fazer nossa classe de tal maneira que ela respeite nossa necessidade. Como fazê-lo (pesquise: Singleton Design Pattern)?
Exercícios 8.9: mostrando os dados da conta na tela
Crie a classe
ManipuladorDeContas
dentro do pacotebr.com.caelum.contas
. Repare que os pacotesbr.com.caelum.contas.main
ebr.com.caelum.contas.modelo
são subpacotes debr.com.caelum.contas
, portanto o pacotebr.com.caelum.contas
já existe. Para criar a classe nesse pacote, basta selecioná-lo na janela de criação da classe:A classe
ManipuladorDeContas
fará a ligação daConta
com a tela, por isso precisaremos declarar um atributo do tipoConta
.Crie o método
criaConta
, que recebe como parâmetro um objeto do tipoEvento
. Instancie uma conta para o atributoconta
e coloque os valores denumero
,agencia
etitular
. Algo como:public class ManipuladorDeContas { private Conta conta; public void criaConta(Evento evento){ this.conta = new Conta(); this.conta.setTitular("Batman"); // faça o mesmo para os outros atributos }
Com a conta instanciada, agora podemos implementar as funcionalidades de saque e depósito. Crie o método
deposita
; este recebe umEvento
, que é a classe a qual retorna os dados da tela nos tipos que precisamos. Por exemplo, se quisermos o valor a depositar, sabemos que ele é do tipodouble
, e o nome do campo na tela évalor
, então, podemos fazer:public void deposita(Evento evento){ double valorDigitado = evento.getDouble("valor"); this.conta.deposita(valorDigitado); }
Crie agora o método
saca
. Ele também deve receber umEvento
nos mesmos moldes dodeposita
.public void saca(Evento evento){ double valorDigitado = evento.getDouble("valor"); this.conta.saca(valorDigitado); }
Testemos nossa aplicação. Crie a classe
TestaContas
dentro do pacotebr.com.caelum.contas
com ummain
. Nela chamaremos omain
da classeTelaDeContas
, que mostrará a tela de nosso sistema. Não se esqueça de fazer o import dessa classe!import br.com.caelum.javafx.api.main.TelaDeContas; public class TestaContas { public static void main(String[] args) { TelaDeContas.main(args); } }
Rode a aplicação, crie a conta e tente fazer as operações de saque e depósito. Tudo deve funcionar normalmente.
Exercícios 9.7: herança e polimorfismo
Teremos mais de um tipo de conta no nosso sistema, dessa maneira, 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()
doTestaContas.java
:package br.com.caelum.contas.main; import br.com.caelum.javafx.api.main.SistemaBancario; public class TestaContas { public static void main(String[] args) { SistemaBancario.mostraTela(false); // TelaDeContas.main(args); } }
Ao rodar a classe
TestaContas
agora, teremos a tela abaixo:Entraremos na tela de criação de contas com o intuito de ver o que precisamos implementar para que o sistema funcione. Assim, 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. Vamos então criar as classes correspondentes.
- Crie a classe
ContaCorrente
no pacotebr.com.caelum.contas.modelo
e faça com que ela seja filha da classeConta
; - Crie a classe
ContaPoupanca
no pacotebr.com.caelum.contas.modelo
e faça com que ela seja filha da classeConta
.
- Crie a classe
Precisamos pegar os dados da tela para conseguirmos criar a conta correspondente. No
ManipuladorDeContas
, alteraremos o métodocriaConta
. Atualmente, apenas criamos uma nova conta com os dados direto no código. Façamos com que agora os dados sejam recuperados da tela para colocarmos na nova conta. Faremos isso utilizando o objetoevento
:public void criaConta(Evento evento) { this.conta = new Conta(); this.conta.setAgencia(evento.getString("agencia")); this.conta.setNumero(evento.getInt("numero")); this.conta.setTitular(evento.getString("titular")); }
Mas precisamos dizer qual o tipo de conta que queremos criar. Devemos, então, 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 objetoevento
a fim de pegar o tipo. Faremos umif
para verificar esse tipo e, só depois, criaremos o objeto do tipo correspondente. Após essas mudanças, o métodocriaConta
ficará como abaixo:public void criaConta(Evento evento) { String tipo = evento.getSelecionadoNoRadio("tipo"); if (tipo.equals("Conta Corrente")) { this.conta = new ContaCorrente(); } else if (tipo.equals("Conta Poupança")) { this.conta = new ContaPoupanca(); } this.conta.setAgencia(evento.getString("agencia")); this.conta.setNumero(evento.getInt("numero")); this.conta.setTitular(evento.getString("titular")); }
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
nas nossas contas fazendo com que a conta-corrente devolva a string "Conta Corrente", e a conta poupança devolva a string "Conta Poupança":public class ContaCorrente extends Conta { public String getTipo() { return "Conta Corrente"; } } public class ContaPoupanca extends Conta { public String getTipo() { return "Conta Poupança"; } }
Altere os métodos
saca
edeposita
para buscarem o campovalorOperacao
ao invés de apenasvalor
na classeManipuladorDeContas
.Mudaremos o comportamento da operação de saque de acordo com o tipo de conta que estiver sendo utilizada. Na classe
ManipuladorDeContas
, alteremos o métodosaca
para tirar dez centavos de cada saque em uma conta-corrente:public void saca(Evento evento) { double valor = evento.getDouble("valorOperacao"); if (this.conta.getTipo().equals("Conta Corrente")){ this.conta.saca(valor + 0.10); } else { this.conta.saca(valor); } }
Ao tentarmos chamar o método
getTipo
, o Eclipse reclamou que esse método não existe na classeConta
, apesar de existir nas classes filhas. Como estamos tratando todas as contas genericamente, só conseguimos acessar os métodos da classe mãe. Vamos então colocá-lo na classeConta
:public class Conta { public String getTipo() { return "Conta"; } }
Agora 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, teremos de mudar em todos os lugares nos quais fazemos uso do métodosaca
. Essa lógica deveria estar encapsulada dentro do métodosaca
de cada conta. Dessa forma, sobrescrevamos o método dentro da classeContaCorrente
:public class ContaCorrente extends Conta { @Override public void saca(double valor) { this.saldo -= (valor + 0.10); } // restante da classe }
Repare que, para acessar o atributo saldo herdado da classe
Conta
, você precisará mudar o modificador de visibilidade de saldo paraprotected
.Agora que a lógica está encapsulada, podemos corrigir o método
saca
da classeManipuladorDeContas
:public void saca(Evento evento) { double valor = evento.getDouble("valorOperacao"); this.conta.saca(valor); }
Perceba que agora tratamos a conta de forma genérica!
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.
E se tentarmos realizar uma transferência da conta-corrente para a conta poupança? O que acontece?
Comecemos implementando o método
transfere
na classeConta
:public void transfere(double valor, Conta conta) { this.saca(valor); conta.deposita(valor); }
Também precisamos implementar o método
transfere
na classeManipuladorDeContas
para fazer o vínculo entre a tela e a classeConta
:public void transfere(Evento evento) { Conta destino = (Conta) evento.getSelecionadoNoCombo("destino"); conta.transfere(evento.getDouble("valorTransferencia"), destino); }
Rode de novo a aplicação e teste a operação de transferência.
Considere o código abaixo:
Conta c = new Conta(); ContaCorrente cc = new ContaCorrente(); ContaPoupanca cp = new ContaPoupanca();
Se mudarmos esse código para:
Conta c = new Conta(); Conta cc = new ContaCorrente(); Conta cp = new ContaPoupanca();
Compila? Roda? O que muda? Qual é a utilidade disso? Realmente, essa não é a maneira mais útil do polimorfismo. Porém, existe uma utilidade se 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 que será invocado é sempre o mesmo! A JVM descobrirá, em tempo de execução, qual deve ser invocado, dependendo de que tipo é aquele objeto, e não importando como nos referimos a ele.
(Opcional) A nossa classe
Conta
devolve a palavra "Conta" no métodogetTipo
. Use a palavra-chavesuper
nos métodosgetTipo
reescritos nas classes filhas para não ter de reescrever a palavra "Conta" ao devolver os textos "Conta Corrente" e "Conta Poupança" para cada tipo.class ContaCorrente extends Conta { public String getTipo() { return super.getTipo() + " Corrente"; } }
E também
class ContaPoupanca extends Conta { public String getTipo() { return super.getTipo() + " Poupança"; } //... }
(Opcional) Se você precisasse criar uma classe
ContaInvestimento
, e seu métodosaca
fosse complicadíssimo, você precisaria alterar a classeManipuladorDeContas
?Resposta: Não! Essa é a vantagem do polimorfismo: qualquer coisa que seja uma Conta pode ser passada para o método
saca
. A complexidade fica isolada dentro de cada classe.
Exercícios 11.5: interfaces
Nosso banco precisa tributar dinheiro de alguns bens que nossos clientes possuem. Para isso, criaremos uma interface no pacote
br.com.caelum.contas.modelo
do nosso projetofj11-contas
já existente:public interface Tributavel { public double getValorImposto(); }
Lemos essa interface da seguinte maneira: "todos que quiserem ser tributável precisam saber retornar o valor do imposto, devolvendo um double".
Alguns bens são tributáveis, e outros não.
ContaPoupanca
não é tributável. Já paraContaCorrente
, você precisa pagar 1% da conta, e oSeguroDeVida
tem uma taxa fixa de 42 reais mais 2% do valor do seguro.Aproveite o Eclipse! Quando você escrever
implements Tributavel
na classeContaCorrente
, o quickfix do Eclipse sugerirá que você reescreva o método. Escolha essa opção e depois preencha o corpo do método adequadamente:public class ContaCorrente extends Conta implements Tributavel { // outros atributos e métodos public double getValorImposto() { return this.getSaldo() * 0.01; } }
Crie a classe
SeguroDeVida
, aproveitando novamente do Eclipse para obter:public class SeguroDeVida implements Tributavel { private double valor; private String titular; private int numeroApolice; public double getValorImposto() { return 42 + this.valor * 0.02; } // Getters e setters para os atributos. }
Além disso, escreva o método
getTipo
para que o tipo do produto apareça na interface gráfica:public String getTipo(){ return "Seguro de Vida"; }
Criemos a classe
ManipuladorDeSeguroDeVida
dentro do pacotebr.com.caelum.contas
para vincular a classeSeguroDeVida
à tela de criação de seguros. Essa classe deve ter um atributo do tipoSeguroDeVida
.Crie também o método
criaSeguro
que deve receber um parâmetro do tipoEvento
a fim de conseguir obter os dados da tela. Você deve pegar os parâmetrosnumeroApolice
do tipoint
,titular
do tipoString
evalor
do tipodouble
.O código final deve ficar parecido com este:
package br.com.caelum.contas; import br.com.caelum.contas.modelo.SeguroDeVida; import br.com.caelum.javafx.api.util.Evento; public class ManipuladorDeSeguroDeVida { private SeguroDeVida seguroDeVida; public void criaSeguro(Evento evento){ this.seguroDeVida = new SeguroDeVida(); this.seguroDeVida.setNumeroApolice(evento.getInt("numeroApolice")); this.seguroDeVida.setTitular(evento.getString("titular")); this.seguroDeVida.setValor(evento.getDouble("valor")); } }
Execute a classe
TestaContas
e tente cadastrar um novo seguro de vida. O seguro cadastrado deve aparecer na tabela de seguros de vida.Queremos agora saber qual o valor total dos impostos de todos os tributáveis. Criemos, então, a classe
ManipuladorDeTributaveis
dentro do pacotebr.com.caelum.contas
. Crie também o métodocalculaImpostos
que recebe um parâmetro do tipoEvento
:package br.com.caelum.contas; import br.com.caelum.javafx.api.util.Evento; public class ManipuladorDeTributaveis { public void calculaImpostos(Evento evento){ // Aqui calcularemos o total. } }
Agora que criamos o tributavel, habilitaremos a última aba de nosso sistema. Altere a classe
TestaContas
para passar o valortrue
na chamada do métodomostraTela
. Observe: agora que temos o seguro de vida funcionando, a tela de relatório já consegue imprimir o valor dos impostos individuais de cada tipo de Tributavel.No método
calculaImpostos
, precisamos buscar os valores de impostos de cadaTributavel
e somá-los. Para saber a quantidade de tributáveis, a classeEvento
tem um método chamadogetTamanhoDaLista
, que deve receber o nome da lista desejada, no caso "listaTributaveis". Existe também um outro método que retorna umTributavel
de uma determinada posição de uma lista, em que precisamos passar o nome da lista e o índice do elemento. Devemos percorrer a lista inteira passando por cada posição. Logo, utilizaremos umfor
para isso.package br.com.caelum.contas; import br.com.caelum.contas.modelo.Tributavel; import br.com.caelum.javafx.api.util.Evento; public class ManipuladorDeTributaveis { private double total; public void calculaImpostos(Evento evento){ total = 0; int tamanho = evento.getTamanhoDaLista("listaTributaveis"); for (int i = 0; i < tamanho; i++) { Tributavel t = evento.getTributavel("listaTributaveis", i); total += t.getValorImposto(); } } public double getTotal() { return total; } }
Repare que, de dentro do
ManipuladorDeTributaveis
, você não pode acessar o métodogetSaldo
, por exemplo, pois você não tem a garantia de que oTributavel
o qual será passado como argumento tem esse método. Você tem a certeza de que esse objeto tem os métodos declarados na interfaceTributavel
.É interessante enxergar que as interfaces (como aqui, no caso,
Tributavel
) costumam ligar classes muito distintas, unindo-as por uma característica que elas têm em comum. No nosso exemplo,SeguroDeVida
eContaCorrente
são entidades completamente distintas, porém ambas têm a característica de serem tributáveis.Se amanhã o governo começar a tributar até mesmo
PlanoDeCapitalizacao
, basta que essa classe implemente a interfaceTributavel
. Repare no grau de desacoplamento que temos: a classeGerenciadorDeImpostoDeRenda
nem imagina que trabalhará comoPlanoDeCapitalizacao
. Para ela, o importante é que o objeto respeite o contrato de um tributável, isto é, a interfaceTributavel
. Novamente: programe voltado à interface, não à implementação.Quais os benefícios de manter o código com baixo acoplamento?
Resposta:
Quanto menos acoplado o código, mais fácil é sua manutenção, já que alterar uma classe não deve atrapalhar o funcionamento das outras. Note que o uso de interfaces cria uma ligação entre tipos que permite o polimorfismo, mas é bem menos intrusivo do que a herança: não é possível reaproveitar código da mãe.
Por um lado, isso pode parecer negativo e, por vezes, teremos um trecho de código repetido. Mas a certeza de que, ao mudar uma classe, não afetaremos as outras é muito confortável. Para usar interfaces e evitar a repetição, procure pelo conceito de composição.
(Opcional) Crie a classe
TestaTributavel
com um métodomain
para testar o nosso exemplo:public class TestaTributavel { public static void main(String[] args) { ContaCorrente cc = new ContaCorrente(); cc.deposita(100); System.out.println(cc.getValorImposto()); // testando polimorfismo: Tributavel t = cc; System.out.println(t.getValorImposto()); } }
Tente chamar o método
getSaldo
por meio da referênciat
. O que ocorre? Por quê?A linha em que atribuímos
cc
a umTributavel
é apenas para você enxergar que é possível fazê-lo. Nesse nosso caso, isso não tem uma utilidade. Essa possibilidade foi útil no exercício anterior.Resposta: apesar de ser um objeto do tipo
ContaCorrente
, ao chamarmos ele deTributavel
, apenas garantimos ao compilador que aquele objeto dispõe dos métodos que todoTributavel
tem. E como o compilador do Java só trabalha com certezas, ele só permite chamar os métodos definidos no tipo da variável.
Exercícios opcionais 11.6
Atenção: caso você faça esse exercício, faça-o em um projeto à parte
conta-interface
, já que usaremos a Conta
como classe em exercícios futuros.
(Opcional) Transforme a classe
Conta
em uma interface.public interface Conta { public double getSaldo(); public void deposita(double valor); public void saca(double valor); public void atualiza(double taxaSelic); }
Adapte
ContaCorrente
eContaPoupanca
para essa modificação:public class ContaCorrente implements Conta { // ... }
public class ContaPoupanca implements Conta { // ... }
Algum código terá de ser copiado e colado? Isso é tão ruim?
Ao fim desse exercício, você terá os seguintes códigos:
public interface Conta { public abstract void deposita(double valor); public abstract void saca(double valor); public abstract double getSaldo(); public abstract void atualiza(double taxa); }
E as classes que implementam
Conta
:public class ContaCorrente implements Conta, Tributavel { private double saldo; @Override public void deposita(double valor) { this.saldo += valor; } @Override public void saca(double valor) { this.saldo -= valor; } @Override public double getSaldo() { return this.saldo; } @Override public void atualiza(double taxa) { this.saldo += this.saldo * taxa * 2; } @Override public double calculaTributos() { return this.saldo * 0.01; } }
public class ContaPoupanca implements Conta { private double saldo; @Override public void atualiza(double taxa) { this.saldo += this.saldo * taxa * 3; } @Override public void deposita(double valor) { this.saldo += (valor - 0.1); } @Override public void saca(double valor) { this.saldo -= valor; } @Override public double getSaldo() { return this.saldo; } }
(Opcional) Às vezes, é interessante criarmos uma interface que herde de outras interfaces: aquela é chamada subinterfaces; estas nada mais são do que um agrupamento de obrigações para a classe que a implementar.
public interface ContaTributavel extends Conta, Tributavel { }
Dessa maneira, quem for implementar essa nova interface precisa executar todos os métodos herdados das suas superinterfaces (e talvez ainda novos métodos declarados dentro dela):
public class ContaCorrente implements ContaTributavel { // métodos } Conta c = new ContaCorrente(); Tributavel t = new ContaCorrente();
Repare que o código pode parecer estranho, pois a interface não declara método algum, só herda os métodos abstratos declarados nas outras interfaces.
Ao mesmo tempo que uma interface pode herdar de mais de uma outra, classes só podem ter uma classe mãe (herança simples).
Resposta:
Podemos criar a interface
ContaTributavel
, que é umaConta
e também umTributavel
. Como as definições dos métodos já estão nas duas interfaces originais, a declaração da nova fica simplesmente:public interface ContaTributavel extends Conta, Tributavel { }
E então, alteramos também a ContaCorrente, que passa a implementar apenas essa nova interface:
public class ContaCorrente implements ContaTributavel { // restante da classe // exatamente igual à do exercício anterior }
Exercícios 12.11: exceções
Na classe
Conta
, modifique o métododeposita(double x)
: ele deve lançar uma exception chamadaIllegalArgumentException
, a qual já faz parte da biblioteca do Java, sempre que o valor passado como argumento for inválido (por exemplo, quando for negativo).public void deposita(double valor) { if (valor < 0) { throw new IllegalArgumentException(); } else { this.saldo += valor; } }
Rode a aplicação, cadastre uma conta e tente depositar um valor negativo. O que acontece?
Resposta: Uma
IllegalArgumentException
é lançada quando tentamos depositar um valor inválido, ou seja, o próprio métododeposita
se defende de alguém que queira fazer algo errado.Ao lançar a
IllegalArgumentException
, passe via construtor uma mensagem a ser exibida. Lembre-se de que aString
recebida como parâmetro é acessível depois por intermédio do métodogetMessage()
, herdado por todas asExceptions
.public void deposita(double valor) { if (valor < 0) { throw new IllegalArgumentException("Você tentou depositar" + " um valor negativo"); } else { this.saldo += valor; } }
Rode a aplicação novamente e veja que agora a mensagem aparece na tela.
Faça o mesmo para o método
saca
da classeContaCorrente
, afinal o cliente também não pode sacar um valor negativo!Validaremos também a situação em que o cliente não pode sacar um valor maior do que o saldo disponível em conta. Faça sua própria
Exception
,SaldoInsuficienteException
. Para isso, você precisa criar uma classe com esse nome que seja filha deRuntimeException
.public class SaldoInsuficienteException extends RuntimeException { }
No método
saca
da classeContaCorrente
, utilizaremos esta novaException
:@Override public void saca(double valor) { if (valor < 0) { throw new IllegalArgumentException("Você tentou sacar um valor negativo"); } if (this.saldo < valor) { throw new SaldoInsuficienteException(); } this.saldo -= (valor + 0.10); }
Atenção: nem sempre é interessante criarmos um novo tipo de exception, depende do caso. Neste aqui, seria melhor ainda utilizarmos
IllegalArgumentException
. É boa prática preferir usar as já existentes do Java sempre que possível.(Opcional) Coloque um construtor na classe
SaldoInsuficienteException
que receba o valor o qual ele tentou sacar (isto é, ele receberá umdouble valor
).Quando estendemos uma classe, não herdamos seus construtores, mas podemos acessá-los por meio da palavra-chave
super
de dentro de um construtor. As exceções do Java têm uma série de construtores úteis que podem populá-las já com uma mensagem de erro. Então, criaremos um construtor emSaldoInsuficienteException
que delegue ao construtor de sua mãe; esta guardará essa mensagem para poder mostrá-la quando o métodogetMessage
for invocado:public class SaldoInsuficienteException extends RuntimeException { public SaldoInsuficienteException(double valor) { super("Saldo insuficiente para sacar o valor de: " + valor); } }
Dessa maneira, na hora de dar o
throw new SaldoInsuficienteException
, você precisará passar esse valor como argumento:if (this.saldo < valor) { throw new SaldoInsuficienteException(valor); }
Atenção: você pode se aproveitar do Eclipse para isso: comece já passando o
valor
como argumento ao construtor da exception, e o Eclipse reclamará que não existe tal construtor. O quickfix (ctrl + 1) recomendará que ele seja construido, poupando-lhe tempo!E agora, como fica o método
saca
da classeContaCorrente
?Resposta:
@Override public void saca(double valor) { if (valor < 0) { throw new IllegalArgumentException("Valor menor do que 0"); } if (this.saldo < valor) { throw new SaldoInsuficienteException(valor); } this.saldo -= (valor + 0.10); }
(Opcional) Declare a classe
SaldoInsuficienteException
como filha direta deException
em vez deRuntimeException
. Ela passa a ser checked. Em que isso resulta?Você precisará avisar que o seu método
saca()
throws SaldoInsuficienteException
, pois ela é uma checked exception. Além disso, quem chama esse método tomará uma decisão entretry-catch
outhrows
. Faça uso do quickfix do Eclipse novamente!Depois, retorne a exception para unchecked, isto é, para ser filha de
RuntimeException
, pois a utilizaremos assim em exercícios dos capítulos posteriores.Resposta: A mudança na classe
SaldoInsuficienteException
é apenas na classe mãe:public class SaldoInsuficienteException extends Exception { //... }
E, por conta disso, o método
saca
da classeContaCorrente
precisa avisar que pode, eventualmente, lançar essa exceção:public void saca(double valor) throws SaldoInsuficienteException { if (valor < 0) { throw new IllegalArgumentException(); } if (this.saldo < valor) { throw new SaldoInsuficienteException(valor); } this.saldo -= (valor + 0.10); }
Desafios 12.12
O que acontece se acabar a memória da Java Virtual Machine?
Resposta: O que sucede é um
java.lang.OutOfMemoryError
, que é umError
em vez de umaException
(http://docs.oracle.com/javase/7/docs/api/java/lang/OutOfMemoryError.html).O código para fazer esse erro é:
public class TestaError { public static void main(String[] args) { String[] ss = new String[Integer.MAX_VALUE]; } }
Exercícios 13.5: java.lang.Object
Como verificar se a classe
Throwable
, que é a superclasse deException
, também reescreve o métodotoString
?Resposta: A maioria das classes do Java que são muito utilizadas terão seus métodos
equals
etoString
reescritos convenientemente.Há algumas formas de verificar a sobrescrita de um método:
- Olhar o Javadoc: se o método estiver sobrescrito, seu novo comportamento estará documentado ali;
- Abrir a classe e olhar: no Eclipse, se você tiver adicionado o src.zip nas suas configurações, pode abrir a classe com Ctrl + shift + T e olhar se o método foi sobrescrito;
- Bom e velho Syso: outra possibilidade é criar objetos iguais, comparar
com o
equals
e ver se funciona. Os outros métodos são, no entanto, bem mais eficientes.
Utilize-se da documentação do Java e descubra de que classe é o objeto referenciado pelo atributo
out
daSystem
.Repare que, com o devido
import
, poderíamos escrever:// falta a declaração da saída ________ saida = System.out; saida.println("ola");
De que tipo a variável
saida
precisa ser declarada? É isso que você precisa descobrir. Se digitar esse código no Eclipse, ele irá sugerir-lhe um quickfix e declarará a variável por você.Estudaremos essa classe em um capítulo futuro.
Resposta: A variável pública e estática
out
é do tipoPrintStream
.Rode a aplicação e cadastre duas contas. Na tela de detalhes de conta, verifique o que aparece na caixa de seleção de conta para transferência. Por que isso acontece?
Resposta: Nesse primeiro momento, algo parecido com isso deve ser mostrado:
br.com.caelum.contas.modelo.ContaCorrente@f34a08
Porque o
toString
ainda não foi sobrescrito.Reescreva o método
toString
da sua classeConta
fazendo com que uma mensagem mais explicativa seja devolvida. Lembre-se de aproveitar dos recursos do Eclipse para isso: digitando apenas o começo do nome do método a ser reescrito e pressionando Ctrl + espaço. Ele recomendará reescrever o método e, assim, irá poupar-lhe do trabalho de escrever a assinatura do método e cometer algum engano.public abstract class Conta { protected double saldo; @Override public String toString() { return "[titular=" + titular + ", numero=" + numero + ", agencia=" + agencia + "]"; } // restante da classe }
Rode a aplicação novamente, cadastre duas contas e verifique, de novo, a caixa de seleção da transferência. O que aconteceu?
Resposta: Dessa vez, o resultado foi mais agradável. Deve ter aparecido algo como:
[titular=Batman, numero=123, agencia=345]
Reescreva o método
equals
da classeConta
para que duas contas com o mesmo número e agência sejam consideradas iguais. Esboço:public abstract class Conta { public boolean equals(Object obj) { if (obj == null) { return false; } Conta outraConta = (Conta) obj; return this.numero == outraConta.numero && this.agencia.equals(outraConta.agencia); } }
Você pode usar o Ctrl + espaço do Eclipse para escrever o esqueleto do método
equals
, basta digitar dentro da classeequ
e pressionar Ctrl + espaço.Rode a aplicação e tente adicionar duas contas com o mesmo número e agência. O que acontece?
Exercícios 13.7: java.lang.String
Queremos que as contas apresentadas na caixa de seleção da transferência apareçam com o nome do titular em letras maiúsculas. Com o objetivo de fazer isso, alteraremos o método
toString
da classeConta
. Utilizemos o métodotoUpperCase
daString
para tal.Resposta:
@Override public String toString() { return "[titular=" + titular.toUpperCase() + ", numero=" + numero + ", agencia=" + agencia + "]"; }
Após alterarmos o método
toString
, aconteceu alguma mudança com o nome do titular que é apresentado na lista de contas? Por quê?Resposta: Não mudou nada, pois os métodos da
String
sempre retornam uma novaString
, mantendo o titular da conta inalterado.Teste os exemplos desse capítulo para ver que uma
String
é imutável.Resposta: Exemplos de teste:
public class TestaString { public static void main(String[] args) { String s = "fj11"; s.replaceAll("1", "2"); System.out.println(s); } }
Como fazer para ele imprimir fj22? Resposta:
public class TestaString { public static void main(String[] args) { String s = "fj11"; String outra = s.replaceAll("1", "2"); System.out.println(s); System.out.println(outra); } }
Como sabemos se uma
String
se encontra dentro de outra? Como tiramos os espaços em branco das pontas de umaString
? Como sabemos se umaString
está vazia e quantos caracteres tem umaString
?Tome como hábito sempre pesquisar o Javadoc! Conhecer a API, aos poucos, é fundamental para que você não precise reescrever a roda!
Abra a página da documentação da classe String da versão do Java que você utiliza. http://docs.oracle.com/javase/7/docs/api/java/lang/String.html
Resposta: Os exemplos dessa questão são:
contains
: devolvetrue
se aString
contém a sequência de caracteres passada;trim
: devolve uma novaString
sem caracteres brancos do início e do fim;isEmpty
: devolvetrue
se aString
está vazia. Surgiu no Java 6;length
: devolve a quantidade de caracteres daString
.
(Opcional) Escreva um método que usa os métodos
charAt
elength
de umaString
para imprimi-la caractere a caractere, com cada um em uma linha diferente.Resposta:
public void imprimeLetraPorLetra(String texto) { for (int i = 0; i < texto.length(); i++) { System.out.println(texto.charAt(i)); } }
(Opcional) Reescreva o método do exercício anterior, mas modificando-o para que imprima a
String
de trás para a frente e em uma linha só. Teste-o para "Socorram-me, subi no ônibus em Marrocos" e "anotaram a data da maratona".Resposta:
public void inverte(String texto) { for (int i = texto.length() - 1; i >= 0; i--) { System.out.print(texto.charAt(i)); } System.out.println(""); }
(Opcional) Pesquise a classe
StringBuilder
(ou StringBuffer no Java 1.4). Ela é mutável. Por que usá-la em vez daString
? Quando usá-la?Como você poderia reescrever o método que imprime a
String
de trás para a frente usando umStringBuilder
? Resposta:public void inverteComStringBuilder(String texto) { System.out.print("\t"); StringBuilder invertido = new StringBuilder(texto).reverse(); System.out.println(invertido); }
Desafio 13.8
Converta uma
String
para um número sem usar as bibliotecas do Java que já o fazem. Isto é, umaString x = "762"
deve gerar umint i = 762
.Para ajudar, saiba que um
char
pode ser transformado emint
com o mesmo valor numérico fazendo:char c = '3'; int i = c - '0'; // i vale 3!
Aqui estamos nos aproveitando do conhecimento da tabela unicode: os números de 0 a 9 estão em sequência! Você poderia usar o método estático
Character.getNumericValue(char)
em vez disso.Resposta:
public class DesafioConversaoDeNumeros { public static void main(String[] args) { String numero = "762"; System.out.println("Imprimindo a string: " + numero); int resultado = converteParaInt(numero); System.out.println("Imprimindo o int: " + resultado); } private static int converteParaInt(String numero) { int resultado = 0; while (numero.length() > 0) { char algarismo = numero.charAt(0); resultado = resultado * 10 + (algarismo - '0'); numero = numero.substring(1); } return resultado; } }
Exercícios 14.5: arrays
Para consolidarmos os conceitos sobre arrays, faremos alguns exercícios que não interferem em nosso projeto.
Crie uma classe
TestaArrays
e, no métodomain
, crie uma array de contas de tamanho 10. Em seguida, faça um laço para criar dez contas com saldos distintos e colocá-las na array. Por exemplo, você pode utilizar o índice do laço e multiplicá-lo por cem para gerar o saldo de cada conta:Conta[] contas = new Conta[10]; for (int i = 0; i < contas.length; i++) { Conta conta = new ContaCorrente(); conta.deposita(i * 100.0); // escreva o código para guardar a conta na posição i do array }
A seguir a resposta completa para esse item:
public class TestaArrays { public static void main(String[] args) { Conta[] contas = new Conta[10]; for (int i = 0; i < contas.length; i++) { Conta conta = new ContaCorrente(); conta.deposita(i * 100.0); contas[i] = conta; } } }
Ainda na classe
TestaArrays
, faça um outro laço para calcular e imprimir a média dos saldos de todas as contas da array.Resposta:
public class TestaArrays { public static void main(String[] args) { Conta[] contas = new Conta[10]; for (int i = 0; i < contas.length; i++) { Conta conta = new ContaCorrente(); conta.deposita(i * 100.0); contas[i] = conta; } double soma = 0.0; for (int i = 0; i < contas.length; i++) { soma += contas[i].getSaldo(); } double media = soma / contas.length; System.out.println("A média dos saldos é: " + media); } }
(Opcional) Crie uma classe
TestaSplit
que reescreva uma frase com as palavras na ordem invertida. "Socorram-me, subi no ônibus em Marrocos" deve retornar "Marrocos em ônibus no subi Socorram-me,". Utilize o métodosplit
daString
para auxiliá-lo. Esse método divide umaString
de acordo com o separador especificado e devolve as partes em uma array deString
, por exemplo:String frase = "Uma mensagem qualquer"; String[] palavras = frase.split(" "); // Agora só basta percorrer a array na ordem inversa, imprimindo as palavras.
A seguir a resposta completa para esse item:
public void invertePalavrasDaFrase(String texto) { String[] palavras = texto.split(" "); for (int i = palavras.length - 1; i >= 0; i--) { System.out.print(palavras[i] + " "); } System.out.println(""); }
(Opcional) Crie uma classe
Banco
dentro do pacotebr.com.caelum.contas.modelo
OBanco
deve ter um nome, um número (obrigatoriamente) e uma referência a uma array deConta
de tamanho 10, além de outros atributos que você julgar necessário.Resposta:
public class Banco { private String nome; private int numero; private Conta[] contas; // Outros atributos que você achar necessário. public Banco(String nome, int numero) { this.nome = nome; this.numero = numero; this.contas = new ContaCorrente[10]; } // Getters para nome e número. Não colocar os setters, pois já recebemos no // construtor }
(Opcional) A classe
Banco
deve ter um métodoadiciona
, que recebe uma referência àConta
como argumento e guarda essa conta.Resposta: Você deve inserir a
Conta
em uma posição da array que esteja livre. Existem várias maneiras para você fazer isso: guardar um contador para indicar qual a próxima posição vazia, ou procurar por uma posição vazia toda vez. O que seria mais interessante?Se quiser verificar qual a primeira posição vazia (nula) e adicionar nela, poderia ser feito algo como:
public void adiciona(Conta c) { for(int i = 0; i < this.contas.length; i++){ // verificar se a posição está vazia // adicionar no array } }
É importante reparar que o método adiciona não recebe titular, agência, saldo, etc. Essa não seria uma maneira estruturada nem orientada a objetos de se trabalhar. Você, primeiramente, cria uma
Conta
e já passa a referência dela, que dentro do objeto tem titular, saldo, etc.Resposta:
public void adiciona(Conta c) { for(int i = 0; i < this.contas.length; i++){ if(this.contas[i] == null) { this.contas[i] = c; break; } } }
(Opcional) Crie uma classe
TestaBanco
que terá um métodomain
. Dentro dele, crie algumas instâncias deConta
e passe para o banco pelo métodoadiciona
.Resposta:
Banco banco = new Banco("CaelumBank", 999); // ....
Crie algumas contas e passe como argumento para o
adiciona
do banco:Resposta:
ContaCorrente c1 = new ContaCorrente(); c1.setTitular("Batman"); c1.setNumero(1); c1.setAgencia(1000); c1.deposita(100000); banco.adiciona(c1); ContaPoupanca c2 = new ContaPoupanca(); c2.setTitular("Coringa"); c2.setNumero(2); c2.setAgencia(1000); c2.deposita(890000); banco.adiciona(c2);
Você pode criar essas contas dentro de um loop e dar a cada uma delas valores diferentes de depósitos:
for (int i = 0; i < 5; i++) { ContaCorrente conta = new ContaCorrente(); conta.setTitular("Titular " + i); conta.setNumero(i); conta.setAgencia(1000); conta.deposita(i * 1000); banco.adiciona(conta); }
Repare que temos de instanciar
ContaCorrente
dentro do laço. Se a instanciação deContaCorrente
ficasse acima do laço, estaríamos adicionado cinco vezes a mesma instância deContaCorrente
nesseBanco
e apenas mudando seu depósito a cada iteração, que, neste caso, não é o efeito desejado.Opcional: o método
adiciona
pode gerar uma mensagem de erro indicando quando a array já está cheia.public class TestaBanco { public static void main (String[] args) { Banco banco = new Banco("CaelumBank", 999); ContaCorrente c1 = new ContaCorrente(); c1.setTitular("Batman"); c1.setNumero(1); c1.setAgencia(1000); c1.deposita(100000); banco.adiciona(c1); ContaPoupanca c2 = new ContaPoupanca(); c2.setTitular("Coringa"); c2.setNumero(2); c2.setAgencia(1000); c2.deposita(890000); banco.adiciona(c2); } }
(Opcional) Percorra o atributo
contas
da sua instância deBanco
e imprima os dados de todas as suas contas. Para fazer isso, você pode criar um método chamadomostraContas
dentro da classeBanco
:public void mostraContas() { for (int i = 0; i < this.contas.length; i++) { System.out.println("Conta na posição " + i); // Preencher para mostrar outras informações da conta. } }
Cuidado ao preencher esse método: alguns índices da sua array podem não conter referência a uma
Conta
construída, isto é, ainda se referirem anull
. Se preferir, use ofor
novo do Java 5.0.Então, por meio do seu
main
, depois de adicionar algumas contas, basta fazer:banco.mostraContas();
public void mostraContas() { for (int i = 0; i < this.contas.length; i++) { Conta conta = this.contas[i]; if (conta != null) { System.out.println("Conta na posição: " + i); System.out.println("Saldo da conta: " + conta.getSaldo()); } } }
E também altere a classe
TestaBanco
:public class TestaBanco { public static void main (String[] args) { // criação das contas... banco.mostraContas(); } }
(Opcional) Em vez de mostrar apenas o salário de cada funcionário, você pode usar o método
toString()
de cadaConta
da sua array.Resposta:
public void mostraContas() { for (int i = 0; i < this.contas.length; i++) { Conta conta = this.contas[i]; if (conta != null) { System.out.println("Conta na posição: " + i); System.out.println("Dados da conta: " + conta); } } }
(Opcional) Crie um método para verificar se uma determinada
Conta
se encontra ou não como conta desse banco:public boolean contem(Conta conta) { // ... }
Você precisará fazer um
for
em sua array e verificar se a conta passada como argumento se encontra dentro da array. Evite ao máximo usar números hard-coded, isto é, use o.length
.public boolean contem(Conta conta) { for (int i = 0; i < this.contas.length; i++) { if (contas.equals(this.contas[i])) { return true; } } return false; }
(Opcional) Caso a array já esteja cheia no momento de adicionar uma outra conta, crie uma nova com uma capacidade maior e copie os valores da array atual. Isto é, faremos a realocação dos elementos da array, já que Java não tem isso: uma array nasce e morre com o mesmo length.
Usando o this para passar argumento
Dentro de um método, você pode usar a palavra
this
para referenciar a si mesmo e pode passar essa referência como argumento.public class Banco { // atributos public void adiciona(Conta c) { for(int i = 0; i < this.contas.length; i++){ if(this.contas[i] == null) { this.contas[i] = c; return; } } this.aumentaArray(); } public void aumentaArray() { int novoTamanho = this.contas.length * 2; Conta[] maior = new Conta[novoTamanho]; for (int i = 0; i < this.contas.length; i++) { maior[i] = this.contas[i]; } this.contas = maior; } // Outros métodos }
Exercícios 15.6: ordenação
Ordenaremos o campo de destino da tela de detalhes da conta para que as contas apareçam em ordem alfabética de titulares.
Faça sua classe
Conta
implementar a interfaceComparable<Conta>
. Utilize o critério de ordenar pelo titular da conta.public class Conta implements Comparable<Conta> { ... }
Resposta: Deixe o seu método
compareTo
parecido com este:public class Conta implements Comparable<Conta> { // ... todo o código anterior fica aqui public int compareTo(Conta outraConta) { return this.titular.compareTo(outraConta.titular); } }
Queremos que as contas apareçam no campo de destino ordenadas pelo titular. Então, criemos o método
ordenaLista
na classeManipuladorDeContas
. Use oCollections.sort()
para ordenar a lista recuperada doEvento
:Resposta:
public class ManipuladorDeContas { // outros métodos public void ordenaLista(Evento evento) { List<Conta> contas = evento.getLista("destino"); Collections.sort(contas); } }
Rode a aplicação, adicione algumas contas e verifique se as elas aparecem ordenadas pelo nome do titular no campo destino, na parte da transferência. Para ver a ordenação, é necessário acessar os detalhes de uma conta.
Atenção especial: repare que escrevemos um método
compareTo
em nossa classe, e nosso código nunca o invoca!! Isso é muito comum. Reescrevemos (ou implementamos) um método, e quem o invocará será um outro conjunto de classes (nesse caso, quem está chamando ocompareTo
é oCollections.sort
, que o usa como base para o algoritmo de ordenação). Isso cria um sistema extremamente coeso e, ao mesmo tempo, com baixo acoplamento: a classeCollections
nunca imaginou que ordenaria objetos do tipoConta
, mas já que eles sãoComparable
, o seu métodosort
está satisfeito.O que teria acontecido se a classe
Conta
não implementasseComparable<Conta>
, mas tivesse o métodocompareTo
?Faça um teste: remova temporariamente a sentença
implements Comparable<Conta>
. Não retire o métodocompareTo
e veja o que acontece. Basta ter o método sem assinar a interface?Resposta: Não basta! A interface é como um contrato e, sem assiná-lo, a existência do método é só uma coincidência e não dá a certeza à JVM de que a intenção era mesmo assinar aquele contrato.
Como posso inverter a ordem de uma lista? E embaralhar todos os elementos de uma lista? Como rotaciono os elementos de uma lista?
Investigue a documentação da classe
Collections
dentro do pacotejava.util
.Resposta: Olhando na documentação da classe
Collections
(http://docs.oracle.com/javase/7/docs/api/java/util/Collections.html), encontramos o métodoreverse()
, que recebe umaList
e altera a ordem dos seus elementos, invertendo-os.(Opcional) Em uma nova classe
TestaLista
, crie umaArrayList
e insira novas contas com saldos aleatórios usando um laço (for
). Adivinhe o nome da classe para colocar saldos aleatórios?Random
, do pacotejava.util
. Consulte sua documentação para usá-la (utilize o métodonextInt()
passando o número máximo a ser sorteado).Resposta:
public class TestaLista { public static void main(String[] args) { List<Conta> contas = new ArrayList<Conta>(); Random random = new Random(); ContaPoupanca c1 = new ContaPoupanca(random.nextInt(2000), "Caio"); c1.deposita(random.nextInt(10000) + random.nextDouble()); contas.add(c1); ContaPoupanca c2 = new ContaPoupanca(random.nextInt(2000), "Adriano"); c2.deposita(random.nextInt(10000) + random.nextDouble()); contas.add(c2); ContaPoupanca c3 = new ContaPoupanca(random.nextInt(2000), "Victor"); c3.deposita(random.nextInt(10000) + random.nextDouble()); contas.add(c3); } }
Modifique a classe
TestaLista
para utilizar umaLinkedList
em vez deArrayList
:List<Conta> contas = new LinkedList<Conta>();
Precisamos alterar mais algum código para que essa substituição funcione? Rode o programa. Alguma diferença?
Resposta: Essa mudança simplesmente funciona! O legal de chamar as coleções pelas suas interfaces é isso: não importa a implementação. Como ambas são uma
List
, é possível trocar entre elas e continuar tratando comoList
.É mais uma aplicação do polimorfismo!
(Opcional) Imprima a referência a essa lista. O
toString
de umaArrayList
/LinkedList
é reescrito?System.out.println(contas);
Resposta: Sim! Ele mostra os elementos da lista entre colchetes e separados por vírgulas.
Exercícios 15.15: collections
Crie um código que insira 30 mil números numa
ArrayList
e pesquise-os. Usemos um método deSystem
para cronometrar o tempo gasto:public class TestaPerformance { public static void main(String[] args) { System.out.println("Iniciando..."); Collection<Integer> teste = new ArrayList<>(); long inicio = System.currentTimeMillis(); int total = 30000; for (int i = 0; i < total; i++) { teste.add(i); } for (int i = 0; i < total; i++) { teste.contains(i); } long fim = System.currentTimeMillis(); long tempo = fim - inicio; System.out.println("Tempo gasto: " + tempo); } }
Troque a
ArrayList
por umHashSet
e verifique o tempo que levará:Collection<Integer> teste = new HashSet<>();
O que é mais lento? A inserção de 30 mil elementos ou as 30 mil buscas? Descubra computando o tempo gasto em cada
for
separadamente.A diferença é mais que evidente. Se você passar de 30 mil para um número maior, como 50 ou 100 mil, verá que isso inviabiliza por completo o uso de uma
List
, caso queiramos utilizá-la essencialmente em pesquisas.Resposta: No caso das listas (
ArrayList
eLinkedList
), a inserção é bem rápida e a busca muito lenta!Para os conjuntos (
TreeSet
eHashSet
), a inserção ainda é rápida, embora um pouco mais lenta do que a das listas. E a busca é muito rápida!(Conceitual e importante) Repare que se você declarar a coleção e der
new
assim:Collection<Integer> teste = new ArrayList<>();
em vez de:
ArrayList<Integer> teste = new ArrayList<>();
É garantido que terá de alterar só essa linha para substituir a implementação por
HashSet
. Estamos aqui usando o polimorfismo a fim de nos proteger que mudanças de implementação nos obriguem a alterar muito o código. Mais uma vez: programe voltado à interface, e não à implementação!Resposta: Esse é um excelente exemplo de bom uso de interfaces, afinal de que importa como a coleção funciona? O que queremos é uma coleção qualquer, isso é suficiente para os nossos propósitos! Nosso código está com baixo acoplamento em relação a estrutura de dados utilizada: podemos trocá-la facilmente.
Esse é um código extremamente elegante e flexível. Com o tempo, você reparará que as pessoas tentam programar sempre se referindo a essas interfaces menos específicas, na medida do possível: métodos costumam receber e devolver
Collection
s,List
s eSet
s em vez de referenciar diretamente uma implementação. É o mesmo que ocorre com o uso deInputStream
eOutputStream
: eles são o suficiente, não há um porquê de forçar o uso de algo mais específico.Obviamente, algumas vezes, não conseguimos trabalhar dessa forma e precisamos usar uma interface mais específica ou mesmo nos referir ao objeto pela sua implementação para poder chamar alguns métodos. Por exemplo,
TreeSet
tem mais métodos que os definidos emSet
, assim comoLinkedList
em relação aList
.Dê um exemplo de um caso em que não poderíamos nos referir a uma coleção de elementos como
Collection
, mas necessariamente por interfaces mais específicas comoList
ouSet
.Quando precisamos colocar a semântica de que uma coleção não pode ter repetição, por exemplo, precisamos de um
Set
. Se precisamos necessariamente de ordem, necessitamos de umaList
.Pense na preparação de um mochilão pela Europa. Se eu estou interessado em contar para meus amigos por quais países eu passarei, a repetição não importa, então eu escolheria um
Set
.Agora se eu quero planejar as passagens de um local a outro dessa viagem, não só a repetição de locais importa, como também a ordem. Então, preciso de uma
List
.Faça testes com o
Map
, como visto nesse capítulo:public class TestaMapa { public static void main(String[] args) { Conta c1 = new ContaCorrente(); c1.deposita(10000); Conta c2 = new ContaCorrente(); c2.deposita(3000); // cria o mapa Map mapaDeContas = new HashMap(); // adiciona duas chaves e seus valores mapaDeContas.put("diretor", c1); mapaDeContas.put("gerente", c2); // qual a conta do diretor? Conta contaDoDiretor = (Conta) mapaDeContas.get("diretor"); System.out.println(contaDoDiretor.getSaldo()); } }
Depois, altere o código para usar o generics e não haver a necessidade do casting, além da garantia de que nosso mapa estará seguro em relação à tipagem usada.
Você pode utilizar o quickfix do Eclipse para que ele conserte isso: na linha em que você está chamando o
put
, use o Ctrl + 1. Depois de mais um quickfix (descubra qual!), seu código deve ficar como segue:// cria o mapa Map<String, Conta> mapaDeContas = new HashMap<>();
Que opção do Ctrl + 1 você escolheu para que ele adicionasse o generics?
Resposta: Há duas opções válidas aqui:
- Podemos usar o Add type arguments to Map e, depois, novamente Add type arguments to HashMap;
- Podemos escolher direto a opção Infer generic type arguments, que já fará tudo com apenas um comando.
(Opcional) Assim como no exercício 1, crie uma comparação entre
ArrayList
eLinkedList
para ver qual é a mais rápida ao adicionar elementos na primeira posição (list.add(0, elemento)
), por exemplo:public class TestaPerformanceDeAdicionarNaPrimeiraPosicao { public static void main(String[] args) { System.out.println("Iniciando..."); long inicio = System.currentTimeMillis(); // trocar depois por ArrayList List<Integer> teste = new LinkedList<>(); for (int i = 0; i < 30000; i++) { teste.add(0, i); } long fim = System.currentTimeMillis(); double tempo = (fim - inicio) / 1000.0; System.out.println("Tempo gasto: " + tempo); } }
Seguindo o mesmo raciocínio, você pode ver qual é a mais rápida para se percorrer usando o
get(indice)
(sabemos que o correto seria utilizar o enhanced for ou oIterator
). Para isso, insira 30 mil elementos e depois percorra-os usando cada implementação deList
.Perceba que, aqui, o nosso intuito não é que você aprenda qual é o mais rápido, o importante é entender que podemos tirar proveito do polimorfismo para nos comprometer apenas com a interface. Depois, quando necessário, podemos trocar e escolher uma implementação mais adequada às nossas necessidades.
Qual das duas listas foi mais rápida para adicionar elementos à primeira posição?
Resposta: A
LinkedList
é bem mais rápida para fazer a inserção na primeira posição do que aArrayList
. Isso é uma característica dos algoritmos dessas listas, e é estudada sob o tópico de Análise de algoritmos na literatura.(Opcional) No pacote
br.com.caelum.contas.modelo
, crie a classeBanco
(caso não tenha sido criada anteriormente) que tem umaList
deConta
chamadacontas
. Repare que, em uma lista deConta
, você pode colocar tantoContaCorrente
quantoContaPoupanca
por causa do polimorfismo.Crie um método
void adiciona(Conta c)
, um métodoConta pega(int x)
e outroint pegaQuantidadeDeContas()
. Basta usar a sua lista e delegar essas chamadas aos métodos e às coleções que estudamos.Como ficou a classe
Banco
?Resposta:
public class Banco { private List<Conta> contas = new ArrayList<>();; public void adiciona(Conta conta) { contas.add(conta); } public Conta pega(int posicao) { return contas.get(posicao); } public int getQuantidadeDeContas() { return contas.size(); } }
(Opcional) No
Banco
, crie um métodoConta buscaPorTitular(String nome)
que procura por umaConta
cujotitular
sejaequals
aonomeDoTitular
dado.Você pode implementar esse método com um
for
na sua lista deConta
, porém não tem uma performance eficiente.Adicionando um atributo privado do tipo
Map<String, Conta>
, terá um impacto significativo. Toda vez que o métodoadiciona(Conta c)
for invocado, você deve invocar.put(c.getTitular(), c)
no seu mapa. Dessa maneira, quando alguém invocar o métodoConta buscaPorTitular(String nomeDoTitular)
, basta você fazer oget
no seu mapa, passandonomeDoTitular
como argumento.Note que isso é somente um exercício! Desse jeito você não poderá ter dois clientes com o mesmo nome nesse banco, o que não é legal.
Como ficaria sua classe
Banco
com esseMap
?Resposta:
public class Banco { private List<Conta> contas = new ArrayList<>(); private Map<String, Conta> indexadoPorNome = new HashMap<>(); public void adiciona(Conta conta) { contas.add(conta); indexadoPorNome.put(conta.getTitular(), conta); } public Conta buscaPorTitular(String nomeDoTitular) { return indexadoPorNome.get(nomeDoTitular); } }
(Opcional e avançado) Crie o método
hashCode
para a sua conta de forma que ele respeite oequals
, considerando que duas contas sãoequals
quando tem o mesmo número e agência. Felizmente para nós, o próprio Eclipse já vem com um criador deequals
ehashCode
, que os faz de forma consistente.Na classe
Conta
, use o Ctrl + 3 e comece a escrever hashCode para achar a opção de gerá-los. Então, selecione os atributosnumero
eagencia
e mande gerar ohashCode
e oequals
.Como ficou o código gerado?
Resposta:
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((agencia == null) ? 0 : agencia.hashCode()); result = prime * result + numero; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Conta other = (Conta) obj; if (agencia == null) { if (other.agencia != null) return false; } else if (!agencia.equals(other.agencia)) return false; if (numero != other.numero) return false; return true; }
(Opcional e avançado) Crie uma classe de teste e verifique se sua classe
Conta
funciona agora corretamente em umHashSet
, isto é, se ela não guarda contas com número e agência repetidos. Depois remova o métodohashCode
. Continua funcionando?Resposta: Dominar o uso e o funcionamento do
hashCode
é fundamental para o bom programador.Sem o
hashCode
, o critério para definir o que são contas iguais e o que são contas diferentes se perde e, assim, oHashSet
não consegue garantir a aparição única de uma conta.A classe para fazer essa verificação fica mais ou menos assim:
public class TestaHashSetDeConta { public static void main(String[] args) { HashSet<Conta> contas = new HashSet<>(); ContaCorrente c1 = new ContaCorrente(); c1.setNumero(1); c1.setAgencia(1000); c1.setTitular("Batman"); ContaCorrente c2 = new ContaCorrente(); c2.setNumero(1); c2.setAgencia(1000); c2.setTitular("Robin"); ContaCorrente c3 = new ContaCorrente(); c3.setNumero(2); c3.setAgencia(1000); c3.setTitular("Coringa"); contas.add(c1); contas.add(c2); contas.add(c3); System.out.println("Total de contas no HashSet: " + contas.size()); } }
Desafios 15.16
Gere todos os números entre 1 e 1000 e organize-os em ordem decrescente utilizando um
TreeSet
. Como ficou seu código?Resposta:
public class TestaTreeSetDecrescente { public static void main(String[] args) { TreeSet<Integer> conjunto = new TreeSet<>(); for (int i = 1; i <= 1000; i++) { conjunto.add(i); } for (Integer i : conjunto.descendingSet()) { System.out.print(i + " "); } } }
Gere todos os números entre 1 e 1000 e organize-os em ordem decrescente utilizando uma
ArrayList
. Como ficou seu código? Resposta:public class TestaArrayListDecrescente { public static void main(String[] args) { List<Integer> lista = new ArrayList<>(); for (int i = 1; i <= 1000; i++) { lista.add(i); } Collections.sort(lista); Collections.reverse(lista); for (Integer i : lista) { System.out.print(i + " "); } } }
Exercícios 17.11: Apêndice Java I/O
Salvemos as contas cadastradas em um arquivo para não precisar ficar adicionando-as a todo momento.
Na classe
ManipuladorDeContas
, crie o métodosalvaDados
, que recebe umEvento
do qual obteremos a lista de contas:Resposta:
public void salvaDados(Evento evento){ List<Conta> contas = evento.getLista("listaContas"); // Aqui salvaremos as contas em arquivo. }
Para não colocarmos todo o código de gerenciamento de arquivos dentro da classe
ManipuladorDeContas
, criaremos uma nova classe cuja responsabilidade será lidar com a escrita/leitura de arquivos. Crie a classeRepositorioDeContas
dentro do pacotebr.com.caelum.contas
e declare o métodosalva
que deverá receber a lista de contas a serem guardadas. Nesse método, você deve percorrer a lista de contas e salvá-las separando as informações detipo
,numero
,agencia
,titular
esaldo
com vírgulas. O código ficará parecido com:Resposta:
public class RepositorioDeContas { public void salva(List<Conta> contas) { PrintStream stream = new PrintStream("contas.txt"); for (Conta conta : contas) { stream.println(conta.getTipo() + "," + conta.getNumero() + "," + conta.getAgencia() + "," + conta.getTitular() + "," + conta.getSaldo()); } stream.close(); } }
O compilador reclamará que você não está tratando algumas exceções (como
java.io.FileNotFoundException
). Utilize o devidotry
/catch
e relance a exceção comoRuntimeException
. Utilize o quickfix do Eclipse para facilitar (Ctrl + 1).Vale lembrar que deixar todas as exceptions passarem despercebidas não é uma boa prática. Você pode usá-la aqui, pois estamos focando apenas no aprendizado da utilização do
java.io
.Quando trabalhamos com recursos que falam com a parte externa da nossa aplicação, é preciso que avisemos quando acabarmos de usar esses recursos. Por isso, é importantíssimo lembrar de fechar os canais com o exterior os quais abrimos, utilizando o método
close
!Voltando à classe
ManipuladorDeContas
, completemos o métodosalvaDados
para que utilize a nossa nova classeRepositorioDeContas
criada.Resposta:
public void salvaDados(Evento evento){ List<Conta> contas = evento.getLista("listaContas"); RepositorioDeContas repositorio = new RepositorioDeContas(); repositorio.salva(contas); }
Rode sua aplicação, cadastre algumas contas e veja se aparece um arquivo chamado
contas.txt
dentro do diretóriosrc
de seu projeto. Talvez seja necessário dar F5 nele para que o arquivo apareça.(Opcional e difícil) Façamos com que, além de salvar os dados em um arquivo, nossa aplicação também consiga carregar as informações das contas a fim de exibi-las na tela. Para o funcionamento da aplicação, é necessário que a nossa classe
ManipuladorDeContas
tenha um método chamadocarregaDados
, o qual devolva umaList<Conta>
. Façamos o mesmo que anteriormente e encapsulemos a lógica de carregamento dentro da classeRepositorioDeContas
:public List<Conta> carregaDados() { RepositorioDeContas repositorio = new RepositorioDeContas(); return repositorio.carrega(); }
Faça o código referente ao método
carrega
, que devolve umaList
dentro da classeRepositorioDeContas
, utilizando a classeScanner
. Para obter os valores de cada atributo, você pode utilizar o métodosplit
daString
. Lembre-se de que os atributos das contas são carregados na seguinte ordem:tipo
,numero
,agencia
,titular
esaldo
. Exemplo:String linha = scanner.nextLine(); String[] valores = linha.split(","); String tipo = valores[0];
Além disso, a conta deve ser instanciada de acordo com o conteúdo do
tipo
obtido. Fique atento, pois os dados lidos virão sempre lidos em forma deString
e, para alguns atributos, será necessário transformar o dado nos tipos primitivos correspondentes. Por exemplo:String numeroTexto = valores[1]; int numero = Integer.parseInt(numeroTexto);
A seguir a resposta completa para esse item:
public List<Conta> carrega() { List<Conta> contas = new ArrayList<>(); try (Scanner scanner = new Scanner(new File("contas.txt"))) { while (scanner.hasNextLine()) { Conta conta; String linha = scanner.nextLine(); String[] valores = linha.split(","); String tipo = valores[0]; int numero = Integer.parseInt(valores[1]); String agencia = valores[2]; String titular = valores[3]; double saldo = Double.parseDouble(valores[4]); if (tipo.equals("Conta Corrente")) { conta = new ContaCorrente(numero, agencia, titular, saldo); } else { conta = new ContaPoupanca(numero, agencia, titular, saldo); } contas.add(conta); } } catch (FileNotFoundException e) { System.out.println("Não tem arquivo ainda"); } return contas; }
(Opcional) A classe
Scanner
é muito poderosa! Consulte seu Javadoc para saber sobre odelimiter
e os outros métodosnext
.(Opcional) Crie uma classe
TestaInteger
, e façamos comparações comInteger
s dentro domain
:Integer x1 = new Integer(10); Integer x2 = new Integer(10); if (x1 == x2) { System.out.println("igual"); } else { System.out.println("diferente"); }
E se testarmos com o
equals
? O que podemos concluir?Resposta: A conclusão é aquela mesma do capítulo de orientação a objeto do curso. Não importa se todos as informações são exatamente iguais: quando usamos o
==
, estamos comparando as variáveis, isto é, a referência a objetos.Se demos
new
duas vezes, cada referência aponta para um objeto diferente, e, portanto não são iguais. Já oequals
doInteger
, que sobrescreve o doObject
, compara o conteúdo dos objetos.(Opcional) Um
double
não está sendo suficiente para guardar a quantidade de casas necessárias em uma aplicação. Preciso guardar um número decimal muito grande. O que poderia usar?O
double
também tem problemas de precisão ao fazer contas por causa de arredondamentos da aritmética de ponto flutuante definido pela IEEE 754:http://en.wikipedia.org/wiki/IEEE_754
Ele não deve ser usado se você precisa realmente de muita precisão (casos que envolvam dinheiro, por exemplo).
Consulte a documentação, tente adivinhar em que lugar você pode encontrar um tipo que o ajudaria a resolver esses casos e veja como é intuitivo. Qual é a classe que resolveria esses problemas?
Lembre-se: no Java, há muito já feito. Seja na biblioteca padrão, seja em bibliotecas open source, que você pode encontrar pela internet.
Resposta: A classe que nos ajudará a evitar arredondamentos e armazenar números decimais bem grandes é a
java.math.BigDecimal