Escolar Documentos
Profissional Documentos
Cultura Documentos
VINÍCIUS GODOY
Vinícius Godoy
IESDE
2019
© 2019 – IESDE BRASIL S/A.
É proibida a reprodução, mesmo parcial, por qualquer processo, sem autorização por escrito do autor e do detentor dos
direitos autorais.
Projeto de capa: IESDE BRASIL S/A.
Imagem da capa: IESDE BRASIL S/A.
Apresentação 7
1 Olá, Java! 9
1.1 Breve histórico 9
1.2 Arquitetura da plataforma 10
1.3 Instalando o ambiente 12
1.4 O primeiro programa Java 16
2 Conhecendo a linguagem 25
2.1 Variáveis e tipos de dados 25
2.2 Controle de fluxo: estruturas de decisão 34
2.3 Controle de fluxo: estruturas de repetição 36
2.4 Escopo de variáveis 39
3 Classes e objetos 43
3.1 Classes e objetos no mundo real 43
3.2 Sua primeira classe 45
3.3 A palavra-chave static 51
3.4. O valor especial null 54
4 Compondo objetos 57
4.1 Classificação no mundo real: o todo e suas partes 57
4.2 Associando objetos 59
4.3 Pacotes 61
4.4 Encapsulamento e modificadores de acesso 63
4.5 Referência a valor 66
5 Hierarquias de classes 71
5.1 Classificação no mundo real: biologia 71
5.2 Apresentando o problema 73
5.3 Herança 77
5.4 Polimorfismo 81
5.5 Classes e métodos abstratos 83
5.6 Interfaces 84
6 Generics e lambda 89
6.1 O que são generics 89
6.2 Generics 91
6.3 Lambda 94
7 A biblioteca de coleções 101
7.1 Listas 101
7.2 Conjuntos 105
7.3 Mapas 111
7.4 Streams 112
8 Tratamento de erros 117
8.1 Entendendo o problema 117
8.2 Disparando exceções 120
8.3 Capturando exceções 124
Gabarito 133
Apresentação
Prezado aluno,
Neste livro, você começará a desvendar o paradigma orientado a objetos. Ainda me lembro
quando, no ano de 1997, um dos meus professores da universidade nos mostrou "um novo recurso
de programação que talvez cole". Pela primeira vez, fui apresentado ao conceito de classes, descobri
uma nova forma de programar. Fiquei deslumbrado, em meio a colegas meio céticos, com como
aqueles recursos permitiam resolver problemas de uma maneira muito mais natural.
Foi nesse mesmo ano que conheci uma plataforma de programação que prometia se destacar
pela forte integração com outra tecnologia ascendente: a internet. Essa plataforma era o Java, que
apresentava uma máquina virtual, tornando-a compatível com vários sistemas operacionais ao
mesmo tempo. Seus críticos alegavam que isso tornava a linguagem muito lenta, que consumia
muita memória e que ela jamais ganharia força no mercado.
Ao escrever esse livro, além da orientação a objetos, procurei gerar um material atualizado,
com vários detalhes e recursos da última versão disponível do Java. Além disso, complementei o
material com dicas de programação, vindas tanto de livros e materiais de referência internacionais
quanto diretamente do cotidiano de um programador. Espero que você aprecie o material e, assim
como o jovem Vinícius, se deslumbre e transforme sua forma de programar.
Em nosso livro de programação orientada a objetos, iremos utilizar a plataforma Java. O Java
é a linguagem presente em servidores de internet e em celulares Android. Neste primeiro capítulo,
aprenderemos o que é a plataforma e como ela está organizada, instalaremos o ambiente e você
conseguirá compilar e executar seu primeiro programa em Java. Pronto para aprender um pouco mais?
Este material utilizará a versão 12 da linguagem Java, lançada pela Oracle em março de
2019, que, mesmo após tantos anos, ainda respeita os princípios básicos dos designs originais
(SUN MICROSYSTEMS, 1997):
• Simples, orientada a objetos e familiar;
• Robusta e segura;
• Neutralidade de arquitetura e portável;
• Alta performance;
• Interpretada, multithread1 e dinâmica.
É importante ter esses objetivos em mente, pois será mais fácil entender por que os projetistas
da linguagem criaram determinados recursos.
Todos esses elementos fornecem um ambiente poderoso, que permitirá que façamos
aplicações comerciais robustas.
1 Multithreading refere-se à capacidade de executar, em um único programa, várias tarefas ao mesmo tempo
(GOETZ, 2018).
Olá, Java! 11
• Por meio de um interpretador, que traduz o software linha a linha, à medida que o executa.
Como o processo de tradução ocorre enquanto o software é executado, a execução é mais
lenta, o programador analisa o software.
Para você entender melhor essas ferramentas, vamos fazer uma analogia. Vamos supor
que você seja o computador e que você só entenda a língua portuguesa. O programador escreveu
o software em inglês. Um compilador seria análogo a um tradutor. Ele pegaria um livro inteiro
(programa) e o traduziria para o português, de modo que você poderia ler o livro, sem a presença
do tradutor ou mesmo do livro original. Já o interpretador seria equivalente a um intérprete, ou
seja, à medida que ele lesse uma frase do livro, ele traduziria para você. Note que, para que isso seja
possível, o intérprete precisa do livro original em mãos e precisa sempre estar presente. Por outro
lado, esse processo é mais interativo, já que você pode tirar dúvidas com o intérprete.
Como um dos objetivos da plataforma Java sempre foi garantir uma execução segura e capaz
de rodar no maior número de dispositivos possível, uma abordagem híbrida foi implementada (SUN
MICROSYSTEMS, 1997). Nessa abordagem, o compilador Java (javac) transformará seu código no
bytecode Java. Esse bytecode é um arquivo muito mais próximo da linguagem de máquina do que o
código escrito em Java, mas ele ainda não pode ser diretamente executado pelo computador.
A execução final desse código, já otimizado, será realizada de maneira interpretada pela
Máquina Virtual Java (JVM) instalada. A JVM possui habilidades bastante poderosas, como a
capacidade de analisar trechos do bytecode mais utilizados e compilá-los de fato (compilação
just-in-time). Além disso, por conhecer a plataforma onde o código está realmente sendo
executado, a máquina virtual pode habilitar instruções específicas de modo a aproveitar ao
máximo seus recursos.
A máquina virtual também pode garantir um ambiente de execução seguro, proibindo que
programas maliciosos executem instruções que prejudiquem o computador (ORACLE, 1997).
Além disso, pode fornecer ao programador diversas ferramentas para análise do código e medições
de performance, tal como uma linguagem interpretada faria.
Figura 1 – Compilação e execução na plataforma Java
Compilação
Código-fonte Bytecode
Bytecodes
compilado
agrupados
programa.java Compilador Empacotador
cadastro.java programa.class
programa.jar
cadastro.class
Execução
Bytecodes
Máquina
agrupados Execução do código
Virtual Java
final
(JVM)
programa.jar
Fonte: Oracle.
Fonte: Oracle.
14 Programação orientada a objetos I
Na parte inferior da tela, clique em Accept License Agreement e então escolha o instalador
de acordo com sua plataforma, por exemplo, no caso do Windows:
Figura 4 – Aceitação do contrato de licença
Fonte: Oracle.
Nós instalaremos o último. Para isso, acesse o site da empresa Jetbrains (disponível em:
https://www.jetbrains.com/idea/) e clique em download.
Olá, Java! 15
Fonte: JetBrains.
Fonte: JetBrains.
Rode o programa e siga o passo a passo da instalação. Cabe reforçar que os IDEs estão para
o Java assim como o Word está para a língua portuguesa. Elas são apenas ferramentas para escrita
de código, mas a linguagem de programação Java será a mesma nos três ambientes. Por isso, todo
código presente neste livro poderá ser executado em qualquer um desses editores ou, até mesmo,
diretamente na linha de comando.
16 Programação orientada a objetos I
Em seguida, o IntelliJ confirmará a sua licença de usuário. Basta confirmar que leu e clicar
em Continue.
Figura 8 – Contrato de licença do IntelliJ
A próxima tela pergunta se você deseja ou não enviar estatísticas de uso para a Jetbrains.
O objetivo disso é tentar melhorar as futuras versões do IDE. Caso deseje, clique em Send Usage
Statistics. Caso não deseje, use o botão Don't send.
Figura 9 – Compartilhamento de dados
O próximo passo é escolher se você quer seu IDE no modo escuro ou claro. Muitos
programadores consideram o modo escuro menos cansativo e mais interessante, por isso, ele já
aparece selecionado no IDE. Escolha uma das opções e clique em Skip Remaining and Set Defaults.
Isso pulará as demais configurações que são mais adequadas a usuários avançados.
Caso você tenha baixado a versão de avaliação do IDE, e não a versão Community, o último
passo será fornecer suas credenciais. Faça isso caso você tenha uma conta Jetbrains ou escolha
Evaluate for free caso você queira usar o período de avaliação.
Figura 10 – Ativação da licença da versão de avaliação
No lado esquerdo, deixe selecionada a opção Java. Certifique-se também de que a versão do
JDK seja a 12. Caso não seja, clique em New e selecione a pasta em que você instalou seu JDK. Em
seguida, clique em Next. Não é necessário marcar nenhuma opção adicional.
Figura 12 – Escolha da linguagem
Na tela seguinte, desmarque a opção Create Project From Template, se estiver marcada, e
clique em Next.
Olá, Java! 19
Caso o menu esquerdo não se abra automaticamente, clique em 1: Project na barra lateral.
O código-fonte do seu programa será colocado na pasta src. Ela aparece selecionada na captura de
tela da Figura 15.
Observe que agora a pasta src contém um arquivo chamado Aula1. No disco, esse arquivo terá
a extensão .java. E é nele que o código-fonte da nossa aplicação iniciará. Uma das regras da linguagem
é que o nome do arquivo e da estrutura class, dentro dele, devem obrigatoriamente ser iguais.
Dentro desse arquivo, o seguinte código deve ter aparecido:
Altere-o para:
Você deve ter notado que, ao fazer isso, aparecem alguns botões verdes na barra lateral
esquerda, ao lado da primeira e segunda linhas. Clique em um desses botões e selecione
Run 'Aula1.main()'.
Olá, Java! 21
Pronto! Você acaba de escrever e executar seu primeiro programa! Perceba que o resultado
da execução já apareceu na parte inferior do IDE. É o texto "Olá mundo!".
Vamos agora analisar esse programa linha a linha:
O Java é uma linguagem orientada a objetos. Isso quer dizer que você sempre trabalhará
com o conceito de classes e objetos. Exploraremos em detalhes esses conceitos a partir do
Capítulo 3, mas note que, desde a linha 1, já fomos obrigados a criar uma classe em que iremos
trabalhar. Dentro dessa classe, definimos uma função importante, conhecida como função
principal (main). Trata-se do ponto de entrada do nosso programa, declarado na linha 2. Tanto
a classe quanto a função principal definem dois blocos de código delimitados pelas chaves {}.
Na linha 3, dentro da função main, imprimimos o texto "Olá mundo!", utilizando o
comando System.out.println. Como o programa só tem essa linha, ele imprime esse texto e
encerra. Observe que, logo após o comando, encontramos um comentário, criado por meio
das duas barras //. Comentários são completamente ignorados na linguagem e nos permitem
escrever anotações para nos acharmos no código. Poderíamos também criar comentários de
várias linhas, bastando delimitá-los por /* e */.
22 Programação orientada a objetos I
Observe que os comandos em Java são terminados pelo ponto e vírgula. Além dos
comentários, as quebras de linha, a tabulação e os espaçamentos são ignorados pelo compilador,
mas repare que os utilizamos para enfatizar os blocos de código. Essa prática é conhecida como
endentação e recomendamos que você a siga durante o código. Alguns programadores, de outras
linguagens inserem uma quebra de linha antes de abrir as chaves em cada bloco, o que deixaria
nosso programa assim:
Considerações finais
Neste capítulo, você aprendeu sobre a plataforma Java, sua importância e configurações.
Juntamente com o primeiro programa, esse foi o primeiro passo para um aprendizado mais profundo.
No próximo capítulo, iniciaremos o estudo da linguagem Java. Inicialmente, conheceremos
a estrutura básica, que será similar a qualquer outra linguagem que você já tenha estudado.
Aproveite essa oportunidade para exercitar a linguagem de forma prática. Em seguida, estudaremos
a orientação a objetos, uma forma de pensar em problemas na hora de escrever software.
A plataforma é um ótimo ambiente para esse aprendizado. Com um pouco de esforço e
dedicação, tenha certeza de que cada hora de estudo será muito recompensadora.
Atividades
1. O Java é uma linguagem híbrida. Por que isso é interessante? Quais são as desvantagens?
2. Na Seção 1.2, foi dito que o impacto de performance do Java é "difícil de medir". Discorra
sobre o porquê dessa afirmação.
Referências
BYOUS, J. Java technology: the early years. The Internet Archive, 20 abr. 2004. Disponível em: https://web.
archive.org/web/20050420081440/http://java.sun.com/features/1998/05/birthday.html. Acesso em: 4 jul.
2019.
GOSLING, J. A brief history of the green project. The Internet Archive, maio 1998. Disponível em: https://web.
archive.org/web/20050609085739/http://today.java.net/jag/old/green/. Acesso em: 4 jul. 2019.
SUN MICROSYSTEMS. Javasoft Ships Java 1.0. The Internet Archive. 23 jan. 1996. Disponível em:
https://web.archive.org/web/20070310235103/http://www.sun.com/smi/Press/sunflash/1996-01/
sunflash.960123.10561.xml. Acesso em: 4 ago. 2019.
SUN MICROSYSTEMS. Java Code Conventions. Oracle Technetwork, 12 set. 1997. Disponível em: https://
www.oracle.com/technetwork/java/codeconventions-150003.pdf. Acesso em: 4 ago. 2019.
2
Conhecendo a linguagem
Você só conseguirá executar esse exemplo caso inclua todo o código extra exibido no capítulo
anterior, ou seja, você precisaria criar o arquivo Aula2.java e, então, digitar dentro dele o seguinte:
Embora esse código extra possa parecer excessivo, você logo verá que ele ocorre de maneira
natural em programas maiores.
tipo nomeDaVariavel;
tipo nomeDaVariavel = valor;
Para definirmos uma variável textual que guardará um nome, por exemplo, poderíamos
fazer:
String nome;
Ou poderíamos definir uma variável inteira para indicar o número de páginas de um livro já
inicializada com o valor 100, na seguinte forma:
Também é possível declarar mais de uma variável de mesmo tipo em uma única linha,
separando-as por vírgula. Por exemplo:
Embora isso seja possível, não é muito usual, pois a sintaxe pode ser confusa (SIERRA;
BATES, 2010). A linha a seguir, por exemplo, declara a variável x sem valor e a variável y com o
valor 50, não as duas variáveis com valor 50, como pode parecer:
int x, y = 50;
Por isso, muitos programadores consideram uma boa prática declarar cada variável em sua
própria linha.
Além da maneira explícita, podemos declarar variáveis utilizando tipagem implícita, por
meio da instrução var. Nesse caso, seremos obrigados a indicar o valor, pois é por meio dele que
o Java determinará o tipo da variável. Note que a variável ainda tem um tipo, o qual não poderá
mudar.
Por exemplo, a variável páginas poderia ser declarada de maneira implícita:
De maneira geral, a forma implícita é preferível. Nela, não é possível declarar múltiplas
variáveis na mesma linha.
Além de variáveis, podemos declarar constantes com a palavra-chave final. Constantes não
podem mudar de valor.
Uma vez declaradas, podemos utilizar variáveis para realizar operações básicas, como
soma, subtração, comparações e concatenação. As operações básicas variam em cada tipo de dado.
Sem perceber, já estudamos a primeira operação básica, chamada de atribuição e realizada pelo
operador de =. Por exemplo:
1 var x = true;
2 var y = false;
3
4 System.out.println(x == y); //Imprime false
5 System.out.println(x != y); //Imprime true
É com esse operador que substituiremos o valor contido no interior das variáveis.
1 var x = true;
2 var y = false;
3
4 System.out.println(x == y); //Imprime false
5 System.out.println(x != y); //Imprime true
Além desses dois operadores relacionais, o tipo boolean admite três operadores condicionais
(também chamados de operadores lógicos). São eles: operador E (&&), OU (||) e NÃO (!).
O operador E retorna true somente se os dois valores comparados forem verdadeiros. Já o
operador OU retorna false apenas se os dois valores comparados forem falsos. Por fim, o operador
NÃO inverte o valor sendo comparado. Por exemplo:
28 Programação orientada a objetos I
Obviamente, você poderia combinar operadores para realizar operações complexas, como:
Qual seria o valor da variável complexa? Isso te obrigaria a avaliar a expressão parte a parte.
v1 && !(v2 || f2) → v2 || f2 é true
v1 && !true → !true é false
v1 && false → v1 && false é false
Para os tipos de dados de ponto flutuante, temos apenas os tipos float (32 bits) e double
(64 bits). Apesar do grande número de tipos, utilizaremos int e double na maior parte dos casos.
Conhecendo a linguagem 29
Literais numéricos
Quando os números aparecem no código sem especificarmos seus tipos, nós os chamamos
de literais numéricos (DEITEL; DEITEL, 2010). Podemos utilizar a letra L, para especificar que um
número deve ser encarado como long, ou a letra f, para que seja declarado como float. Em literais,
também podemos utilizar o _, para separar dígitos. Finalmente, podemos utilizar alguns prefixos
para alterar a base numérica em que o literal é fornecido, sendo 0x para hexadecimal, 0b para
binário e somente 0 para a base octal. Veja alguns exemplos:
Você também pode usar a letra L minúscula para o long, mas isso não é recomendado já que
ela é muito similar ao número 1.
Type casting
Quando atribuímos o valor de um tipo de dado a outro, o Java é obrigado a fazer a conversão.
Essa operação de conversão é chamada de type casting. Se o tipo de dado não acrescenta imprecisão,
a conversão é automática; porém, às vezes, a operação pode resultar em perda de informação e,
nesse caso, o Java exigirá que você indique explicitamente que a conversão deve ser feita (SIERRA;
BATES, 2010). Como exemplo, considere o código a seguir.
1 int y = 10;
2 short x = y;
3 System.out.println(x);
Esse código apresentará erro na linha 2. Como x é uma variável inteira, ela poderia conter
um valor muito maior ou menor do que o máximo permitido para um short.
A correção seria fazermos a operação de type casting, indicando o tipo de dado entre
parênteses.
1 int y = 10;
2 short x = (short) y;
3 System.out.println(x);
O que aconteceria se o valor em y fosse maior do que o permitido para um short (32.767)?
Se fosse 32.768, por exemplo? Se testarmos esse programa com o valor 32.768, obteremos como
resultado o valor -32.768. Isso ocorre porque, ao ultrapassarmos o último valor do short, o Java
retorna ao primeiro valor possível nesse tipo de dado, no caso -32.768, e continua somando valores,
a partir daí, sem apresentar qualquer tipo de erro. Tenha cuidado, portanto, antes de fazer esse tipo
de conversão.
30 Programação orientada a objetos I
Operadores
Para os tipos numéricos, o Java fornece os seguintes operadores de comparação: ==, !=, >, <,
<= e >=. Observe que o resultado de uma operação de comparação será um booleano, ou seja, um
valor true ou false. Além disso, o Java fornece os operadores matemáticos tradicionais para soma,
subtração, multiplicação, divisão e resto: +, -, *, / e %. Esses operadores respeitam a precedência
matemática, ou seja, multiplicações e divisões irão ocorrer primeiro, caso sejam misturadas a
somas e subtrações. Por exemplo:
var x = 5 + 10 * 3; //Atribui 35 a x
O sinal de menos também pode ser utilizado na frente da variável. Isso é chamado de negação
unária, que inverte o sinal da variável. Por exemplo: no código a seguir, a variável outroNumero
receberá o valor 12.5.
É possível atribuir a uma variável um valor com base em seu valor antigo, da seguinte forma:
1 var x = 10;
2 x = x + 5;
3 System.out.println(x); //Imprime 15
A operação de = será avaliada por último. Assim, na linha 2, o Java primeiro avaliará a
expressão x + 5 com base no atual valor de x, que é 10 (linha 1). Desse modo, 10 + 5 resulta em 15.
Somente após isso o valor de x será substituído.
Essa operação é tão comum que os operadores aritméticos podem ser combinados ao sinal
de = para realizá-la. Por exemplo:
1 var x = 10;
2 x += 5; //Equivalente a x = x + 5
3 System.out.println(x); //Imprime 15
Além desses, para os tipos inteiros, o Java também fornece os operadores ++ e -- para somar
ou subtrair 1 ao valor da variável. Ele pode ser usado antes ou depois da variável. Em ambos os
casos, o valor desta será modificado, porém, se usado antes dela, o operador retornará o valor antes
da modificação. Por exemplo:
1 var x = 10;
2 System.out.println(++x); //Muda o valor para 11 e imprime o resultado
3 System.out.println(x++); //Muda o valor para 12, mas imprime 11
4 System.out.println(x); //Imprime 12
Por fim, os tipos inteiros também fornecem operadores para manipulação de bits. A Tabela
2, a seguir, descreve seu funcionamento considerando x os últimos 8 bits de uma variável de valor
0010_1011.
Conhecendo a linguagem 31
OU | x | 1111_0000 1111_1011
Se você achou esses operadores muito complexos, não se preocupe, eles geralmente são
utilizados apenas em aplicações de baixo nível, como manipulação de protocolos de rede ou
programação de firmwares.
Por utilizar a codificação Unicode para representar caracteres, o Java não tem problemas
com acentuação, como ocorre em outras linguagens.
2.1.4 Texto
Além dos oito tipos primitivos vistos até agora, o Java também dá suporte ao texto por meio
de variáveis do tipo String. Textos literais são definidos utilizando aspas:
Podemos descobrir a quantidade de caracteres dentro de uma String por meio do comando
length(). No caso do código anterior, subtitulo.length() resultaria no valor 7.
Por fim, podemos ler o valor de um caractere dentro do texto por meio da função charAt.
Devemos informar a posição desse caractere, iniciando em 0. Por exemplo: titulo.charAt(3) nos
retornaria a quarta letra, ou seja, char "g".
32 Programação orientada a objetos I
2.1.5 Enumerações
Enumerações representam um conjunto fixo de valores. Imagine a seguinte situação: digamos
que você vai trabalhar com um sistema que utilize baralho e gostaria de um tipo de dado para
representar os naipes. Como sabemos, só existem quatro naipes possíveis: paus, ouros, espadas e
copas. Você poderia criar para isso uma enumeração chamada Naipes. A criação de enumerações
envolve dois passos. São eles:
1. Definir a enumeração: dar um nome para esse nosso tipo de dado e, em seguida,
especificar quais são os seus valores possíveis.
2. Utilizar a enumeração: declarar uma variável do tipo da enumeração e atribuir a ela
valores.
Observe que até agora não criamos nenhuma variável. Estamos apenas explicando para o
Java o que é um Naipe. Agora, podemos, em nosso main, utilizar esse novo tipo de dado:
As enumerações são muito mais poderosas do que isso, exploraremos vários outros recursos
em capítulos futuros.
O que acontece com o array declarado na linha 2? Nessa linha, foi criado um array com 10
variáveis do tipo double. Como não especificamos seus valores, o Java utilizou o valor padrão. Para
qualquer variável numérica, o valor padrão será 0. Então, nesse caso, há um array com 0 em todas
as suas posições.
A linha 3 é um caso diferente. Criamos uma variável chamada z, que conterá um array
bidimensional de caracteres; porém, essa variável não foi inicializada com nenhuma lista. Ela
ganha, então, o valor especial null. Se tentarmos imprimir um índice qualquer de z, obteremos
uma mensagem de erro. Null é também o valor padrão de Strings, enumerações e, como veremos
no próximo capítulo, objetos.
Similar às Strings, também é possível utilizar o comando length para descobrir o tamanho
de um array. No exemplo anterior, x.length resultaria no valor 4.
Diferentemente de outras linguagens, o Java não exige que arrays com mais de uma dimensão
sejam quadrados, ou seja, devemos imaginar uma lista bidimensional, como uma lista de listas, e
não como uma matriz. Como matrizes retangulares são extremamente comuns, o Java também
fornece uma forma direta de declaração desse tipo de estrutura. Vejamos alguns exemplos de
declaração de matrizes.
A declaração da linha 1 cria uma matriz quadrada de oito linhas e oito colunas, todas elas
contendo caracteres. Como não informamos exatamente quais caracteres estão lá dentro, o Java os
inicializou com o valor padrão 0.
34 Programação orientada a objetos I
Na linha 2, declaramos a matriz irregular com uma lista de um único inteiro na sua
primeira linha, dois inteiros na segunda linha, nenhum inteiro na terceira linha e cinco inteiros
na quarta linha.
Um detalhe importante sobre o código da linha 5: ele cria um array vazio, o que é diferente
do null que comentamos há pouco. Nesse caso, existe um vetor naquela posição, mas ela não possui
nenhum número dentro. O null indicaria que o vetor não existe, ou seja, não poderíamos nem
mesmo usar a função length sobre ele.
Na linha 9, declaramos uma matriz de quatro linhas, mas somente indicamos que as
colunas seriam formadas de arrays de ints. As quatro linhas foram criadas, mas contendo null
em seus valores.
Nas linhas seguintes, definimos quais arrays são esses, linha a linha. Sendo assim,
estabelecemos uma matriz exatamente com as mesmas dimensões da matriz irregular, porém
com todos os inteiros inicializados em seu valor padrão 0.
2.2.1 If e else
O if executa uma instrução ou bloco de código caso a condição seja verdadeira. Opcional-
mente, podemos usar a instrução else, que será executada caso o if seja falso. Por exemplo:
Esse programa imprimirá o texto "Menor de idade". Isso porque a condição idade < 18,
presente na linha 3, é verdadeira. Se você alterar o valor da variável idade para 18, ele passará a
imprimir "Maior de idade", afinal, a expressão idade < 18 agora é falsa.
Como temos apenas um comando dentro do if, as chaves são opcionais, porém constitui boa
prática utilizá-las, conforme recomendado nas convenções de código (SUN MICROSYSTEMS,
1997). A exceção disso é quando queremos colocar outro if logo após o else para testar múltiplas
condições, como no exemplo a seguir:
Conhecendo a linguagem 35
Operador ternário
Muitas vezes, queremos atribuir o valor de uma variável com base em uma condição. Para
isso, podemos utilizar o operador ternário na seguinte forma:
Vejamos um exemplo:
Então, a variável resultado receberá o valor "Não grávida". Como se trata de um operador, os
valores dos dois lados da expressão precisam ser, obrigatoriamente, do mesmo tipo.
2.2.2 Switch
Muitas vezes, precisamos desviar o fluxo com base em um conjunto de valores. Para isso,
utilizamos o comando switch.
1 var exemplo = 1;
2 switch (exemplo) {
3 case 1:
4 System.out.println("Primeira condição");
5 break;
6 case 2:
7 System.out.println("Segunda condição");
8 case 3:
9 System.out.println("Terceira condição");
10 break;
11 default:
12 System.out.println("Outra condição");
13 break;
14 }
36 Programação orientada a objetos I
Observe, na linha 2, que o comando switch utilizou como base a variável exemplo, que, nesse
caso, é numérica. Ela também poderia ser uma String ou uma enumeração.
Em cada case, colocamos o valor esperado seguido de dois pontos (:) e o código que queremos
executar, caso o valor da variável corresponda ao case. No exemplo, como o valor da variável é 1, o
código imprimirá "Primeira condição".
O código dentro de cada case executa até que o break seja executado. Caso não haja um
comando break, o código prosseguirá para dentro do próximo case. Desse modo, se alterássemos o
valor da variável exemplo para 2, o sistema imprimiria:
Segunda condição
Terceira condição
Isso ocorre porque não há break dentro do case 2 e é chamado de fallthrough. Como você
mesmo deve ter notado pelo exemplo, pode gerar um código confuso. Por fim, existe o bloco
opcional default, que é executado se nenhuma das condições anteriores for atendida.
1 var x = 1;
2 while (x <= 3) {
3 System.out.print(x + ", ");
4 x++;
5 }
6 System.out.println("indiozinhos");
1, 2, 3, indiozinhos
1 var x = 1;
2 do {
3 System.out.print(x + ", ");
4 x++;
5 } while (x <= 3);
6 System.out.println("indiozinhos");
Para deixar mais claro, vamos reescrever o exemplo dos indiozinhos utilizando o for:
O resultado é o mesmo:
1, 2, 3, indiozinhos
O comando for é muito utilizado para imprimir os elementos de um array. Por exemplo:
Laranja
Banana
Maçã
Há outras formas de fazê-lo. Uma delas é por meio do comando for each, introduzido na
versão 5.0 do Java. Veremos seu funcionamento a seguir.
38 Programação orientada a objetos I
For each
iterar: acessar cada Na verdade, iterar sobre os elementos de uma lista é uma tarefa tão comum que uma estrutura
um dos elementos
de um vetor em for foi criada inteiramente para isso. É chamado de for each, que significa "para cada" em tradução
ordem.
literal. O mesmo código anterior poderia ser reescrito assim:
Lemos esse for em português da seguinte forma: "para cada fruta no array de frutas". Essa
é a melhor forma de iterar: não só melhora a leitura, como é mais eficiente em coleções mais
avançadas (BLOCH, 2019).
O Capítulo 7, que trata da biblioteca de coleções do Java, mostrará outras formas de percorrer
listas de objetos.
Digamos que nós temos uma lista com números e queremos imprimir somente os números
pares, na ordem em que aparecem. Poderíamos escrever esse laço assim:
10
2
14
6
Conhecendo a linguagem 39
Caso haja um laço dentro do outro, esses comandos atuarão no mais interno, mas é possível
também incluir marcadores (labels), caso você precise abandonar os mais distantes. O código a
seguir imprime os números linha a linha, mas interrompe o laço imediatamente caso o número 0
seja encontrado.
O resultado será:
1 2 3 4 5
10 20 30 0
Observe o marcador externo na linha 7, que atua sobre o for da linha seguinte. É ele que faz
com que o break colocado na linha 13 interrompa completamente os dois laços, em vez de somente
o laço interno.
Caso tenha três ou mais comandos for, você poderá usar mais de um marcador, entretanto,
uma boa estruturação de código fará com que o uso de labels seja muito raro.
1 var x = 10;
2 for (var i = 0; i < 10; i++) {
3 var a = i*2;
4 System.out.println(a);
5 System.out.println(x);
6 }
7 System.out.println(a); //ERRO
A variável x pode ser utilizada na linha 5, pois está dentro do bloco da função main, assim
como o for. Observe que a variável a foi declarada na linha 3, no interior do bloco do for, portanto
não pode mais ser usada na linha 7. Como seu bloco deixou de existir, a variável também deixou de
existir. O bloco definido nas chaves das linhas 2 e 7 definem o escopo da variável. E a variável i? No
caso do comando for, o escopo dela é o mesmo da variável a. É uma boa prática manter as variáveis
no menor escopo possível (DEITEL; DEITEL, 2010).
Tenha sempre em mente que boas práticas, como restringir o escopo da variável, evitam que
você mesmo cometa erros, simplificando a programação.
• SIERRA, K.; BATES, B. Use a cabeça! Java. Rio de Janeiro: Alta Books, 2010.
O livro de Kathy Sierra e Bert Bates apresenta a linguagem de maneira bastante
extrovertida e, por isso, é uma boa referência.
• CURSO de Java 63: printf, 2016. 1 vídeo (21 min). Publicado pelo canal Loiane Groner.
Disponível em: https://www.youtube.com/watch?v=3Ie7VMJWoYo. Acesso em: 24 set.
2019.
O comando printf é uma poderosa forma de imprimir dados. Para saber mais sobre isso,
recomendamos a videoaula 63 do curso de Java, de Loiane Groner.
• CURSO de Java 12: lendo dados do teclado usando a classe Scanner, 2015. 1 vídeo (22
min). Publicado pelo canal Loiane Groner. Disponível em: https://www.youtube.com/
watch?v=Z6Y8zupCKfk. Acesso em: 24 set. 2019.
Ler dados do teclado também é muito importante. Para saber mais sobre o assunto, assista
à videoaula 12, de Loiane Groner.
Conhecendo a linguagem 41
Atividades
1. Escreva dois programas para imprimir todos os números pares de 2 até 20. O primeiro deve
usar o comando while e o segundo, o comando for. Não utilize o comando continue.
2. O que o programa a seguir imprime? Você pode executar o código mentalmente ou com a
ajuda de um papel.
var a = "a";
var b = 10L;
var c = -.2;
var d = 0f;
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
ORACLE. Primitive Data Types. The Java Tutorials, 2017a. Disponível em: https://docs.oracle.com/javase/
tutorial/java/nutsandbolts/datatypes.html. Acesso em: 24 set. 2019.
ORACLE. Learning the Java Language. The Java Tutorials, 2017b. Disponível em: https://docs.oracle.com/
javase/tutorial/java/TOC.html. Acesso em: 24 set. 2019.
SIERRA, K.; BATES, B. Use a cabeça, Java! Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
SUN MICROSYSTEMS. Java Code Conventions. Oracle Technetwork, 12 set. 1997. Disponível em: https://
www.oracle.com/technetwork/java/codeconventions-150003.pdf. Acesso em: 24 set. 2019.
3
Classes e objetos
Problemas são complexos. Pare para pensar alguns segundos em quantos detalhes estão
envolvidos na resolução do problema de transportar pessoas: segurança, cobrança, o melhor
caminho de um ponto ao outro (mesmo em meio ao trânsito) etc. A tecnologia vem como aliada
das empresas nesse ponto, e, dando suporte a tudo, está o software, portanto, não é surpresa que
ele seja igualmente complexo.
Felizmente, as linguagens de programação fornecem estruturas cada vez mais refinadas
para decompor o problema em partes menores. Neste capítulo, iniciaremos o estudo de um dos
principais paradigmas modernos que nos auxiliam nessa tarefa: a orientação a objetos. Aqui você
começará a desvendar esse mundo, entendendo o que são classes e objetos e como podemos utilizar
a linguagem para modelá-los.
altanaka/Shutterstock
Além dos atributos, também procuramos conhecer o conjunto de operações que os objetos
podem realizar. Por exemplo, pássaros e insetos são capazes de voar, um carro pode transportar
pessoas, um cachorro late etc.
44 Programação orientada a objetos I
maxim ibragimov/Shutterstock
o entendermos os objetos a nossa volta, os classificamos em diferentes tipos. Por exemplo, na
A
Figura 2, sabemos que a criança está com uma guitarra em mãos. O conceito da guitarra foi percebido
pela forma do instrumento, por sabermos que possui cordas, e pela maneira como ela está tocando.
Repare que, embora estejamos olhando para um único objeto concreto – isto é, aquela
guitarra, daquela menina – para nós, o conceito de guitarra engloba uma série de objetos similares
que podem variar em sua cor, tamanho e formato, mas que terão um conjunto de atributos e
operações em comum, como representado pela Figura 3.
Figura 3 – Guitarra: objetos diferentes, um só conceito
Christopher Hall/Shutterstock
Classes e objetos 45
A este conceito damos o nome de abstração, que é definido por Booch et al. (2006, p. 44)
como "as características essenciais de um objeto que os distinguem de todos os outros tipos de
objetos e, por consequência, fornecem fronteiras rígidas e bem definidas, relativas à perspectiva
do observador".
Vemos aqui dois conceitos distintos, mas correlacionados. O primeiro é o conceito de objeto,
também chamado de instância, que se refere a um objeto específico. O outro é o conceito de classe,
que se refere ao tipo desse objeto e, com base nele, sabemos que atributos e operações deveriam
estar presentes em cada uma de suas instâncias.
Como já mencionamos em outras ocasiões, o Java é uma linguagem orientada a objetos.
O que isso significa? Significa que a linguagem nos dará mecanismos para definir e criar nossas
próprias classes e, com base nelas, gerar os objetos que compõem os dados de nossos programas.
Programar em uma linguagem orientada a objetos envolve focar os esforços não tanto nos
algoritmos, mas em criar em software boas abstrações dos objetos do mundo real e, então,
modelar suas interações.
Esta estratégia nos permite lidar com a complexidade do software por meio da decomposição
dos vários problemas complexos em problemas menores. Na prática, cada classe se transformará
em um pequeno programa, com funções e dados bem isolados e definidos. O software como um
todo terá centenas ou milhares de classes cooperando entre si.
Nele, as palavras-chave public class indicam ao Java que uma nova classe será criada. Em
seguida, escrevemos o seu nome: Planeta.
Já definimos que os atributos da nossa classe são três: nome, massa e diâmetro.
Representaremos isso em Java, indicando que a classe Planeta possui internamente três variáveis,
que devem ser declaradas da maneira explícita:
46 Programação orientada a objetos I
Observe que, até agora, nós apenas descrevemos para Java o que é um planeta, ou seja, nós
modelamos um conceito, uma abstração, na forma de um tipo de dado que chamamos de Planeta.
Agora, gostaríamos de criar, em nosso programa, alguns planetas específicos. Por exemplo,
podemos descrever os planetas Mercúrio, Terra e Saturno. Estes são nossos objetos, ou seja,
instâncias (exemplos) de nossa classe.
Vamos fazer isso em um novo arquivo. Clique novamente na pasta src e crie uma nova classe
chamada Main. Nele, inclua a função main. Ao final, seu projeto deve se parecer com isso:
Figura 4 – Projeto com duas classes no IntelliJ
Observe que criamos três variáveis chamadas planeta1, planeta2 e planeta3. Essas variáveis
têm como tipo de dado a nossa classe Planeta. Além disso, perceba que elas se referem a três objetos
diferentes, os planetas Mercúrio, Terra e Saturno.
Como o Java é uma linguagem fortemente tipada, ele garantirá que variáveis de classes
diferentes não se misturem. Por exemplo, se colocássemos no método main o seguinte código e
tentássemos compilar o programa:
Isso significa que Cachorro e Planeta são coisas diferentes e não podemos misturá-los.
3.2.1 Métodos
Além de atributos, também podemos definir operações que os planetas podem realizar
no contexto do nosso sistema. A primeira operação que podemos inserir para nosso planeta é a
obtenção do raio. Como sabemos, o raio equivale à metade do diâmetro. Fazemos isso definindo
uma função no interior do objeto, chamada de método.
A declaração de função segue a seguinte sintaxe (DEITEL; DEITEL, 2010):
Note que o tipo de retorno dele é um double, já que é o tipo de dado do raio calculado. A
palavra-chave return indica qual valor será o resultado da execução desse método. É importante
saber que o método será imediatamente interrompido quando ela for atingida. Podemos usar esse
método em nosso main, da seguinte forma:
Como declararíamos o tipo de retorno de um método que, por exemplo, fizesse só impressão
de dados sem retornar qualquer tipo de valor? Bastaria declarar o método com o tipo de dado void
(vazio). Nesse caso, a palavra return não é mais obrigatória, mas pode ser usada para interromper o
método. Por exemplo, poderíamos programar o método imprimir na classe Planeta assim:
1 public class Planeta {
2 String nome = "";
3 int diametro;
4 double massa;
5
6 double raio() {
7 return diametro / 2.0;
8 }
9
10 void imprimir() {
11 System.out.println("Nome: " + nome);
12 System.out.println("Diâmetro: " + diametro);
13 System.out.println("Massa: " + massa);
14 }
15 }
Classes e objetos 49
Obviamente, um método pode executar outro. Por exemplo, vamos supor que também
quiséssemos calcular a área da superfície do planeta. Matematicamente, a área da superfície de
uma esfera é definida por 4πr2. Poderíamos implementar o método areaSuperficie() assim:
Observe, na linha 11, que o método areaSuperficie() define a variável local raioAoQuadrado
e utiliza a função raio() duas vezes. Também atualizamos nossa função imprimir() para incluir os
dados calculados.
Quando planejamos métodos, é importante pensar com cuidado em sua assinatura (BLOCH,
2019). Bons nomes podem melhorar muito a legibilidade do código, simplificando sua manutenção.
3.2.2 Construtores
Quando utilizamos a palavra-chave new para criar um novo objeto, estamos utilizando uma
função especial chamada construtor (SIERRA; BATES, 2010). Seu objetivo é inicializar o valor dos
atributos da classe quando uma nova instância for gerada.
Caso não criemos um construtor, o Java o criará automaticamente. Nesse caso, os atributos
serão inicializados com o valor padrão (0 para números, false para booleanos e null para objetos),
ou com os valores já indicados na declaração (como o valor de um texto vazio que utilizamos no
nome, indicado pelas aspas). Entretanto, podemos criar nossos próprios construtores e, inclusive,
passar para eles parâmetros. Vamos adicionar um construtor à classe Planeta para que possamos
informar o valor dos seus atributos diretamente. O construtor é declarado como na forma de uma
função com o próprio nome da classe e sem tipo de retorno, por exemplo:
50 Programação orientada a objetos I
Observe que nosso construtor, definido na linha 6, contém três parâmetros que chamamos
de n, d e m. Usamos o valor desses parâmetros para inicializar o nome, diâmetro e massa do
Planeta. Se tentarmos executar nosso código agora, obteremos o seguinte erro:
Main.java
Error:(3, 24) java: constructor Planeta in class Planeta cannot be applied to given
types;
required: java.lang.String,int,double
found: no arguments
reason: actual and formal argument lists differ in length
O que esse erro quer dizer? Significa que na linha 3 do arquivo Main.java estamos usando um
construtor sem parâmetros na classe Planeta, mas ele não existe. Isso ocorre porque, ao declararmos
nosso construtor, o Java não nos fornecerá mais o construtor padrão automaticamente. Podemos
corrigir a função no main utilizando nosso construtor:
Uma prática comum em construtores é declarar o nome dos parâmetros exatamente iguais
ao nome dos atributos, porém, com o conhecimento que temos até agora, isso geraria um código
ambíguo, observe:
Na linha 2, gostaríamos que a palavra nome, do lado esquerdo do sinal de igual, se referisse ao
atributo nome do objeto; e a palavra nome, do lado direito, se referisse ao parâmetro nome, declarado
Classes e objetos 51
na assinatura do método (antigamente chamado de n). Como resolver esse impasse? Para isso, o
Java define a palavra-chave this. Ela significa "o próprio objeto" e, por meio dela, referenciamos o
atributo, não o parâmetro do método. Assim, o código final do nosso construtor será:
E se quiséssemos que o construtor sem parâmetros ainda existisse? Nesse caso, bastaria
declararmos um segundo construtor sem parâmetros e inicializar nele as variáveis:
1 Planeta() {
2 this.nome = "";
3 this.diametro = 0;
4 this.massa = 0;
5 }
Há uma forma ainda mais fácil de fazer isso. A palavra-chave this pode ser utilizada na
primeira linha de um construtor como uma referência a outro construtor. Desse modo, poderíamos
reescrever esse construtor chamando nosso construtor de três parâmetros:
1 Planeta() {
2 this("", 0, 0.0);
3 }
Não há limites para o número de construtores que podemos criar. Observe que, na ausência
de um construtor padrão, podemos utilizar construtores para obrigar o usuário a informar
determinados atributos.
System.out.println(Planeta.descricao());
52 Programação orientada a objetos I
Essa chamada também seria possível por meio de uma variável do tipo Planeta, por exemplo:
1 var planeta1 = new Planeta("Mercurio", 4_878, 0.055);
2
3 //Valido, porém, confuso
4 System.out.println(planeta1.descricao());
Observe que, nesse código, a função descrição aparenta não ser estática e, portanto, é uma
má prática por levar a esse entendimento errôneo.
Um dos grandes usos de atributos estáticos é a definição de constantes. Por exemplo,
poderíamos definir a constante PI com:
final static double PI = 3.1415;
Como atributos e métodos estáticos pertencem à classe, não a uma instância específica, será
impossível acessar métodos e atributos não estáticos da mesma classe com base neles sem que
criemos pelo menos um objeto.
Muitos iniciantes em programação acabam criando somente atributos e métodos estáticos
ao perceber que não conseguem acessar atributos e métodos sem esse marcador com base no
método main. Esta não é uma prática correta e, para piorar, provavelmente será sugerida como
correção por sua IDE.
Caso você precise acessar métodos da classe em que o main está, prefira criar um objeto,
como no exemplo:
1 public class Main {
2 void exemplo() {
3 System.out.println("Chamando método não estático");
4 }
5
6 public static void main(String[] args) {
7 var main = new Main(); //Crie um objeto
8 main.exemplo(); //Chame o método não estático
9 }
10 }
Lembre-se sempre de que métodos e atributos estáticos são exceção, não regra. Desconfie de
sua IDE caso ela esteja sugerindo para marcar todos os lugares dessa forma.
54 Programação orientada a objetos I
at Main.main(Main.java:5)
Observe que ele indica que o tipo do erro é NullPointerException, ou seja, tentamos fazer
acesso a uma variável nula. Após o at, o Java indicará a classe e o método em que o problema
ocorreu, seguido do nome do arquivo: linha entre parênteses. Nesse caso, o erro ocorreu na classe
Main, no método main, que está no arquivo Main.java linha 5. O valor null é o valor padrão para
Não confunda o null com objetos que admitem valores vazios. Por
exemplo, é possível existir um objeto de texto sem nenhum caractere ""
ou mesmo um array com 0 elementos.
Quando estudamos as variáveis no capítulo 2, vimos três tipos de dados que também podiam
possuir valor null: Strings, arrays e enums. Isso porque o Java também considera esses três tipos
como casos especiais de classes.
Isso significa que as variáveis que representam esses tipos também são referências, o que
não é um problema para Strings ou enums, já que os valores dos objetos dessas classes, uma vez
construídos, não podem ser modificados, mas tome cuidado no caso dos arrays. Passar um array
por parâmetro não criará uma cópia de seus dados.
Quando você estiver começando a programar, provavelmente receberá muitas vezes o erro
NullPointerException, descrito acima. Quando isso ocorrer, não se assuste. Basta localizar a linha
do erro e verificar qual variável nunca foi inicializada com new. e, assim, você encontrará a falha
em seu código.
Classes e objetos 55
Considerações finais
A capacidade humana de classificar objetos e criar abstrações é realmente impressionante.
Quando lemos uma fase simples como "Fui de carro para a escola", não percebemos que as palavras
carro e escola representam abstrações para qualquer carro e qualquer escola.
Uma linguagem orientada a objetos, como o Java, permite-nos criar abstrações similares
em código. Dessa maneira, podemos criar conceitos em nossos programas, como o de Planeta,
usado em nossos exemplos. Um programador que utilize essa classe não precisará mais entender
os detalhes de sua lógica para saber a que ela se refere. É simples agora entender que uma chamada
como planeta.raio() resultará no raio daquele planeta específico.
Desse modo, dividimos nosso código em diversas partes menores e coesas, conseguindo lidar
mais facilmente com a complexidade inerente ao problema que nosso sistema ajudará a resolver.
Logo, se estivermos programando um sistema de mapas e tivermos um problema no cálculo da
distância, por exemplo, saberemos que poderemos procurar esse erro na classe Rota, e não na
classe Usuário – e provavelmente encontraremos um método da distância por lá.
Nossa jornada ainda não terminou. Podemos ir muito além de definir objetos. Também
seremos capazes de agrupar objetos similares, compor objetos complexos como o todo de objetos
menores, hierarquizar objetos similares. É essa jornada que continuaremos explorando nos
próximos capítulos.
• ORIENTAÇÃO a objetos com Java. 1 vídeo (4 min). Publicado pelo canal Instituto
Tecnológico da Aeronáutica. Disponível em: https://pt.coursera.org/lecture/orientacao-
a-objetos-com-java/comportamento-e-estado-das-classes-bixyH. Acesso em: 5 ago. 2019.
Esse vídeo, desenvolvido pelos professores Clovis Fernandes e Eduardo Guerra, do
Instituto Tecnológico da Aeronáutica (ITA) e disponibilizado no site da Coursera, explora
um pouco mais a fundo o conceito de classes e objetos, explicando o que é o estado de
uma classe. Vale a pena consultar!
56 Programação orientada a objetos I
Atividades
1. Uma farmácia pretende controlar seus medicamentos. Cada medicamento é composto de
um nome, número do lote e quantidade em estoque. Nenhum medicamento deveria ser
criado sem seu nome e o número de lote. Os medicamentos têm duas operações: retirar(),
em que se indica a quantidade para ser reduzida do estoque, e acabou(), que retorna false se
a quantidade em estoque for 0. Descreva o código dessa classe.
3. Qual a diferença de uma variável String nula e vazia? Dê exemplos de pelo menos uma
situação em que o código de ambas se comportaria de forma diferente.
4. Um programador estava criando uma classe para representar pontos, mas esbarrou no
seguinte problema: há duas formas de construí-los, uma contendo os valores de x e y e outra
com base no raio e na distância até o centro. Quando foi construir a classe, percebeu que isso
implicaria dois construtores idênticos. Veja:
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
BOOCH, G. et al. Object-Oriented Analysis and Design with Applications. 3. ed. Boston: Addison-Wesley,
2006.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson, 2010.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
4
Compondo objetos
No capítulo passado, exploramos o que são classes e objetos, bem como aprendemos a criar
nossas próprias classes em Java utilizando a palavra-chave class. As classes permitiram transformar
nossas abstrações do mundo real em tipos de dados e subdividir o problema em partes menores.
Embora definir classes já nos dê bastante poder de fogo, somente quando fazemos as classes
trabalharem juntas é que começamos a ter benefícios significativos. Neste capítulo, exploraremos
em detalhes como se dá essa interação e como podemos melhorar ainda mais nossas abstrações
nesse contexto. Vamos lá?
Agora, observe com atenção a imagem da Figura 1 e tente dizer o nome de todas as peças do
carro. A maior parte dos leitores deste livro não conseguirá essa façanha, entretanto, isso não nos
impede de identificar um carro na rua, certo? Para entender por que isso acontece, vamos relembrar
o conceito de abstração: "denota as características essenciais de um objeto que os distinguem de
todos os outros tipos de objetos e, por consequência, fornecem fronteiras rígidas e bem definidas,
relativas à perspectiva do observador" (BOOCH et al., 2006, p. 44).
Nesse contexto, o que podemos entender por "relativas à perspectiva do observador"? Quer
dizer que observadores diferentes podem entender os detalhes dos objetos de maneiras diferentes.
Por exemplo, para o passageiro de um carro, pouco importa o que esteja embaixo do capô, desde
que ele o leve de um ponto a outro. Por outro lado, um mecânico deve conhecer cada uma das
peças do carro e detalhes de como interagem (ver Figura 2).
Figura 2 – As fronteiras das abstrações dependem do observador
Lemusique/davooda/Happy Art/Shutterstock
Boas abstrações, dentro de um programa, devem levar em consideração essas duas figuras: a
do criador do objeto, equivalente ao mecânico – que entende vários detalhes sobre as peças do carro
e como suas interações geram o seu funcionamento – e a do usuário, que está mais interessado em
características externas do objeto, em sua funcionalidade geral.
Por fim, há uma terceira maneira de relacionarmos classes. Nós podemos simplesmente
agrupá-las de acordo com um critério qualquer. Por exemplo, o mecânico poderia ter em sua
oficina uma caixa em que está escrito Rodas e lá dentro colocar todo o conjunto de ferramentas
usadas para reparos nas rodas: de martelos a porcas e parafusos. A esses agrupamentos damos o
nome de pacotes e essa relação é chamada de empacotamento (packaging) (SIERRA; BATES, 2010).
Boas linguagens orientadas a objetos nos darão estruturas de programação para implementar
em código todos esses conceitos. Nos próximos capítulos, vamos estudar suas implementações
em Java.
Compondo objetos 59
A segunda será a sua turma, que também tem um nome e pode conter até 20 alunos. A
operação que queremos modelar da turma é a de matricular um aluno. Faremos essa operação
retornar um valor booleano verdadeiro, caso a matrícula seja possível, ou falso, caso a turma já
esteja cheia. Mas como indicar a relação de que "uma turma contém até 20 alunos" em programação?
Não há problema nenhum que um objeto seja declarado como atributo de outro. Isso quer
dizer que, dentro de uma classe, podemos usar objetos de outras classes como seus atributos. Veja,
por exemplo, como será a implementação da classe Turma:
Observe que a turma possui uma lista de alunos. Ela tem capacidade para 20 alunos, porém,
todos os seus valores inicialmente são null, pois nenhum aluno foi associado. Isso é indicado na
variável qtdeAlunos, que inicialmente indica 0.
Quando chamamos o método matricular, passamos para esse método um parâmetro: o
aluno que deve ser matriculado. Ele então é inserido ao final da lista, e a quantidade de alunos é
acrescida em 1. Como utilizaríamos essa classe? Vamos a um exemplo:
Observe que nossa classe Main criou inicialmente três alunos: Vinícius, Thais e Mariana.
Então, criamos a classe da TurmaA e a utilizamos para associar dois alunos: Vinicius e Thais. Já na
TurmaB, associamos o terceiro aluno, Mariana.
Veja que aqui ficam claros os dois pontos de vista comentados no início do capítulo.
Quando programamos a classe Aluno e a classe Turma, éramos os criadores das classes e estávamos
interessados em vários detalhes, como os códigos do método matricular.
Já na classe Main, agimos como programadores usuários das classes Aluno e Turma.
Mesmo que não tivéssemos programado essas classes, seria claro entender que a linha turmaA.
matricular(aluno1) está associando o aluno a uma turma e que a quantidade de alunos indicada
posteriormente deveria ser alterada, e pouco nos importa como o método matricular fez isso. O
programador da classe Turma poderia, por exemplo, ter declarado lá dentro 20 variáveis diferentes
do tipo Aluno, bem como ter controlado cada uma delas manualmente em vez de usar um vetor e,
para efeitos da classe Main, isso não faria qualquer diferença.
Compondo objetos 61
4.3 Pacotes
Já vimos que diferentes partes do nosso sistema se transformam em classes, porém, um
sistema grande terá classes para o mais diverso conjunto de funcionalidades. Considere, por
exemplo, nosso sistema da escola, ele pode conter um módulo financeiro, com classes para lidar
com pagamentos, contas bancárias, parcelas, centros de custo etc. Já no módulo acadêmico, conterá
a listagem de alunos, turmas, notas, professores, entre outras coisas. O sistema poderá, até, possuir
módulos mais internos, para lidar com banco de dados, comunicação em redes, cada um com seu
conjunto de classes.
Para garantir a organização do sistema, o Java define a palavra-chave package para indicar
que uma classe faz parte de um pacote (SIERRA; BATES, 2010). O nome do pacote é indicado no
topo da classe, como no exemplo a seguir:
package escola;
No disco, o pacote será representado por uma pasta. Todas os arquivos .java de classes de
um mesmo pacote devem obrigatoriamente estar na mesma pasta. Podemos utilizar o ponto para
agrupar pacotes. Portanto, caso tivéssemos os pacotes:
escola.financeiro
escola.academico
escola.academico.alunos
escola.rede
Há duas formas de utilizar uma classe de um pacote em outro. A primeira é por meio do
nome completo da classe, composto de nome do pacote e nome da classe separados por um ponto.
Por exemplo, se a classe Boleto do pacote financeiro precisasse ter um vínculo com a classe Aluno
do pacote acadêmico, o atributo poderia ser declarado assim:
1 package escola.financeiro;
2
3 public class Boleto {
4 escola.academico.Aluno aluno;
5 }
62 Programação orientada a objetos I
Como, normalmente, utilizamos com frequência classes de um mesmo pacote, podemos realizar
uma ação conhecida como importação. Para isso, colocamos no início do arquivo a palavra-chave
import seguida do nome completo da classe. A partir de agora, será possível referenciar a classe dentro
1 package escola.financeiro;
2
3
4 import escola.academico.Aluno;
5
6
7 public class Boleto {
Aluno aluno;
}
9. Na classe Main, logo após a linha do package, inclua a linha import escola.academico.*.
Abra o projeto no Windows Explorer e veja a estrutura de pastas criada pelo IntelliJ.
Também repare que o IntelliJ adicionou automaticamente a linha com o comando package no
início dos arquivos.
Porém, nosso programa não funciona mais. Agora, há uma série de erros em vermelho na
classe Main e eles ocorrem porque os atributos e métodos das classes Turma e Aluno não são mais
acessíveis. No próximo tópico, entenderemos o porquê.
• default (não utilizar nada): indica que a classe, atributo ou método é visível apenas
dentro do pacote em que foi declarado.
• private: indica que o atributo ou método é visível somente dentro da classe em que foi
declarado.
• protected: indica que o atributo ou método é visível dentro do pacote em que foi
declarado ou em classes filhas1 de qualquer pacote.
Isso explica por que ainda temos erros nas classes da escola, pois como as dividimos em
pacotes, seus atributos e métodos deixaram de ser visíveis. Portanto, vamos torná-los públicos
para eliminar os erros. Veja o exemplo na classe da turma e repita o processo por contra própria
na classe Aluno:
Note que agora os erros desapareceram, logo, encapsular corretamente nos dá uma série
de vantagens:
• Permite alterar a parte privada da classe sem causar impacto no resto do código.
• Evita que usuários da classe cometam erros, prejudicando o funcionamento do programa.
• Torna o código mais fácil de ser estudado, uma vez que se torna possível estudar a
funcionalidade pública da classe sem conhecer os detalhes de sua implementação.
Obviamente, deixar tudo público não é uma boa forma de encapsular. Por exemplo, o que
aconteceria se o usuário da classe Turma alterasse o valor da variável qtdeAlunos declarado na
linha 4? O que aconteceria na linha 7 do código abaixo?
E o que ele quer dizer? Que, após a modificação, o método matricular tentou acessar a posição
100 do vetor da Turma , que foi declarado com tamanho 20. Isso ocorre porque ninguém deveria alterar
a variável qtdeAlunos diretamente. Ela deveria ser gerenciada somente pela classe Turma e ser atualizada
automaticamente somente no método matricular. Podemos corrigir esse comportamento tornando os
atributos da classe privados e fornecendo métodos para acessá-los. Vejamos a classe da Turma reescrita:
1 package escola.academico;
2
3 public class Turma {
4 private String nome;
5 private Aluno alunos[] = new Aluno[20];
6 private int qtdeAlunos = 0;
7
8 public Turma(String nome) {
9 this.nome = nome;
10 }
11
12 public boolean matricular(Aluno aluno) {
13 //Testamos se a turma está cheia
14 if (qtdeAlunos == alunos.length) {
15 return false;
16 }
17
18 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
19 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
20 return true;
21 }
22
23 public void setNome(String nome) {
24 if (nome == null || nome.isBlank()) {
25 return;
26 }
27 this.nome = nome;
28 }
29
30 public String getNome() {
31 return nome;
32 }
33
34 public int getQtdeAlunos() {
35 return qtdeAlunos;
36 }
37 }
Observe que queremos permitir que o nome seja alterado. Por isso, incluímos também um
método chamado setNome, que realiza a operação. Nele, adotamos até mesmo a boa prática de
66 Programação orientada a objetos I
verificar a validade do parâmetro de entrada (BLOCH, 2019, p. 227), impedindo que nome nulo
ou em branco altere o nome da turma para um valor inválido.
Os métodos getNome e getQtdeAlunos permitem o acesso aos valores das propriedades privadas.
Métodos desse tipo têm o nome de métodos de acesso (ou, em inglês, getters e setters) e devemos dar
preferência a utilizá-los em vez de acessar diretamente os atributos (BLOCH, 2019, p. 78).
Observe também que não colocamos nenhum método desse tipo para o vetor de alunos.
Isso é intencional: não queremos que os usuários da classe Turma mexam nesse vetor diretamente,
só queremos que eles mexam por meio do método matricular. Obviamente, teríamos de incluir
métodos para a consulta dos alunos da turma, caso isso seja necessário em nosso sistema.
Note que agora poderíamos alterar o nome dos atributos ou até mesmo seu tipo sem causar
impacto no código externo – desde que não alteremos a assinatura dos seus getters ou setters. Por
causa dessas vantagens, é considerado uma boa prática de programação restringir ao máximo a
visibilidade de atributos e métodos da classe (BLOCH, 2019, p. 73).
Por que utilizamos os termos em inglês get e set? Isso não foi acidental. O Java também
fornece uma convenção para a criação de classes, chamada de Java Beans (ORACLE, 2017b).
Como toda convenção, ela não é obrigatória, mas a adotaremos neste livro por ser comum em toda
comunidade Java. Além desses dois prefixos, o prefixo is pode ser utilizado no lugar de get para
propriedades booleanas. Por exemplo, poderíamos criar o método isCheia para indicar se a turma
está cheia, em vez de chamá-lo de getCheia.
Por fim, observe que classes também possuem modificadores de acesso. Uma classe pode ter
o modificador default (só existente no pacote em que foi declarada) ou público (visível em todos os
pacotes). Embora um arquivo em Java contenha apenas uma única classe pública, ele pode conter
qualquer número de classes não públicas.
1 var x = 10;
2 var y = x;
3 y = y + 1;
4 System.out.println(y); //Imprime 11
5 System.out.println(x); //Imprime 10
Nesse código, o valor impresso de x na linha 5 permanece 10, isso porque na linha 2 houve a
cópia do valor 10 (conteúdo de x) na variável y. Assim, a alteração da variável y na linha 3 não teve
qualquer impacto sobre o valor original de x. As duas variáveis são independentes.
O mesmo não vale para objetos. Variáveis de objetos são chamadas de referências (DEITEL;
DEITEL, 2010), porque em vez de guardar todos os dados do objeto dentro, elas apenas apontam
o endereço de memória em que esses dados estão. Quando atribuímos uma referência a outra, esse
Compondo objetos 67
endereço de memória simplesmente é copiado e, portanto, as duas passam a apontar para o mesmo
objeto. Veja um exemplo:
A figura abaixo demonstra visualmente essas variáveis ao final dos dois códigos:
Figura 5 – Variáveis na memória
x
10 Matrícula: 1234
Nome: Bruno
y a1 a2
11
O problema desse método é que, como aprendemos, o vetor de alunos também é um objeto,
portanto, nós estamos retornando nesse método uma referência, o que permitiria ao usuário da
nossa classe alterar esse vetor livremente, como no exemplo a seguir:
Observe que o código da linha 2 ocasionaria um erro, pois acessa um índice inválido do
vetor. Pior do que isso, poderíamos fazer alterações em índices válidos, sem passar por qualquer
tipo de validação e sem utilizar o método matricular, e isso incluiria até a possibilidade de atribuir
null a um desses índices. Claramente, não desejamos essa situação.
Como corrigir essa violação do encapsulamento da classe? De maneira geral, há três
estratégias comuns:
1. Utilizar um objeto imutável, ou seja, sem nenhum tipo de método set, ou método que
altere o objeto. Nesse caso, não há com o que se preocupar. Strings e enums entram nessa
categoria, mas não é o caso do nosso vetor (GOETZ, 2003).
2. Criar mais métodos, de modo a se fazer acesso indireto aos objetos.
3. Copiar o objeto manualmente e utilizar a cópia (BLOCH, 2019, p. 231).
68 Programação orientada a objetos I
A estratégia 2 seria, por exemplo, criar o método getAluno(int indice), que acessaria o
aluno por meio do índice. Entretanto, isso não nos permitirá iterar sobre a lista com um for each,
como era nossa intenção original.
Outra abordagem seria usar a estratégia de número 3 e fazer uma cópia do vetor. Para isso, o Java
fornece um método conveniente chamado Arrays.copyOf. Ele permite inclusive alterar o tamanho do
array, assim, poderíamos retornar um vetor somente com os alunos realmente cadastrados:
Agora, graças à cópia, caso o usuário da classe Turma altere o array retornado na função
getAlunos(), não trará qualquer impacto para o vetor interno da classe Turma. Ou seja, a partir
de agora, mantemos a restrição de que só é possível adicionar alunos na classe Turma por meio do
método matricular.
E se um aluno de dentro desse array for alterado por meio de seus métodos, como no
exemplo a seguir?
Por ser uma referência, isso também não alteraria o aluno dentro do array interno da classe
Turma? A resposta é sim. Cabe a reflexão sobre esse comportamento, se ele é correto ou não. Como
Aluno e Turma são classes independentes, talvez seja exatamente isso que se almeja permitir. Afinal,
a classe Aluno também contém seu gets e sets e não deixa que modificações inválidas ocorram.
Agora, se isso não deveria ser permitido por qualquer motivo, seria necessário utilizar também
uma das três estratégias acima para os alunos dentro do vetor de turmas antes de retorná-lo.
Considerações finais
A criação de boas abstrações é uma tarefa complexa, central nos sistemas orientados a
objetos. É por meio delas que mantemos nossos sistemas modularizados e quebramos o problema
em partes menores.
No Capítulo 3, vimos que ela inicia por identificar quais características daqueles objetos são
relevantes para a solução do problema que queremos resolver com aquele software, construindo,
com isso, classes que expressem bem os conceitos que estamos modelando. Por exemplo, a classe
Usuario de um sistema de biblioteca provavelmente terá atributos e métodos muito diferentes da
classe Usuario em um sistema de um banco, mesmo que se refiram à mesma pessoa do mundo real.
Isso porque abstrações envolvem o ponto de vista do observador de um objeto.
Compondo objetos 69
Já neste capítulo, expandimos o conceito dos diferentes observadores, vendo que ele também
existe dentro de um mesmo sistema: sempre teremos o programador criador da classe, que
conhecerá seus detalhes e funcionamento interno, e o programador usuário – que está interessado
na sua interface externa, pública. Por meio do conceito de encapsulamento, implementado por
meio de mecanismos como os pacotes e modificadores de acesso, programamos classes com o uso
seguro e fáceis de serem estudadas.
Observe que, com isso, mudamos o enfoque sobre como pensar para resolver problemas:
agora, pensamos apenas em classes e suas interações, não mais em fluxos de dados. É por esse motivo
que chamamos a orientação a objetos de um paradigma de programação. Não se trata somente de um
comando ou recurso da linguagem, mas de toda uma abordagem para se desenvolver um software.
Mas isso não é tudo. No próximo capítulo, exploraremos mais uma forma de classificação
fundamental para o entendimento completo do paradigma: a relação de herança. Com ela,
poderemos criar abstrações mais poderosas e programar de maneira ainda mais flexível. Até lá!
• HANDS-ON: colaboração entre classes. 1 vídeo (17 min). Publicado pelo canal Instituto
Tecnológico da Aeronáutica. Disponível em: https://pt.coursera.org/lecture/orientacao-a-
objetos-com-java/hands-on-colaboracoes-entre-classes-IKCLa. Acesso em: 22 ago. 2019.
Recomendamos esse vídeo, também desenvolvido pelos professores Clovis Fernandes e
Eduardo Guerra, que mostra, na prática, os conceitos vistos nesse capítulo.
70 Programação orientada a objetos I
Atividades
1. Na Seção 4.5, explicamos que retornar uma referência de um objeto pode violar o
encapsulamento, ou seja, permitir que o usuário da classe altere indevidamente o seu
conteúdo. Também seria possível violar o encapsulamento em um set ou construtor?
Justifique.
2. Qual seria o impacto de alterar a variável qtdeAlunos utilizando a versão da classe Turma com
todos os atributos públicos, descrita no início da Seção 4.4, ou utilizando métodos de acesso,
como descrita ao final da seção?
3. A relação entre os alunos e a classe Turma é uma relação de agregação, já que é plausível imaginar
uma turma sem alunos ou situações em que alunos não estão matriculados em nenhuma
turma. Analise agora a classe Turma, há nela algum objeto em que a relação é de composição?
Explique.
5. Ajuste a classe Planeta do final do Capítulo 3 com os conceitos de encapsulamento que você
aprendeu neste capítulo.
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
BOOCH, G. et al. Object-Oriented Analysis and Design with Applications. 3. ed. Boston: Addison-Wesley,
2006.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
GOETZ, B. Java Theory and Practice: To mutate or not to mutate? IBM Developer Networks, 18 fev. 2003.
Disponível em: https://www.ibm.com/developerworks/library/j-jtp02183/index.html. Acesso em: 19 set. 2019.
ORACLE. Learning the Java Language. The Java Tutorials, 2017a. Disponível em: https://docs.oracle.com/
javase/tutorial/java/TOC.html. Acesso em: 19 set. 2019.
ORACLE. Java Beans. The Java Tutorials, 2017b. Disponível em: https://docs.oracle.com/javase/tutorial/
javabeans/TOC.html. Acesso em: 19 set. 2019.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
5
Hierarquias de classes
Nos capítulos anteriores, estudamos a diferença entre classes e objetos e vimos que criar
nossos próprios tipos de dados nos ajuda a resolver problemas complexos do mundo real. Essas
classes podem cooperar entre si, seja por meio da criação de um tipo mais complexo com base em
outros mais simples (composição), seja por meio de sua interação (agregação).
Neste capítulo, conheceremos mais uma forma de classificação: a relação de herança. Isso
nos permitirá criar abstrações ainda mais poderosas, o que resultará em mais flexibilidade em
nossos sistemas.
Dora Zett/Shutterstock
Eric Isselee/Shutterstock
Eric Isselee/Shutterstock
72 Programação orientada a objetos I
Essas duas raças são subdivisões de uma mesma espécie, a dos cães, chamada de Canis
lupus familiaris (UZUNIAN; BIRNER, 2012). A espécie agrupa uma série de características e
comportamentos que todas essas raças têm em comum. No caso do cão, estamos falando do fato
de serem dóceis com seres humanos, de latirem, terem quatro patas, serem capazes de farejar
coisas, aprender comandos etc.
Observe que, apesar de também possuírem muitas similaridades, não conseguimos classificar
cães e gatos juntos, na mesma espécie. Isso porque, apesar das características em comum – como
serem dóceis, terem quatro patas e pelo –, possuem diferenças significativas, como serem capazes
de subir em árvores, miarem e terem personalidade e inteligência muito diferentes das dos cães.
Por isso, são classificados em uma espécie única (Felis catus). Mas isso significa que cães e gatos
não possuem qualquer relação?
Não. Cães e gatos são agrupados em uma mesma ordem, a dos carnívoros (UZUNIAN;
BIRNER, 2012). Repare que as características de uma ordem já são bem mais genéricas, pois
animais muito diferentes, como ursos e texugos, também podem ser agrupados nessa categoria.
Figura 2 – Gato e cão. Diferentes espécies, ambos carnívoros
Africa Studio/Shutterstock
Essa classificação, por similaridade, permite-nos criar uma hierarquia de classes. No topo
dessa hierarquia, estão classes bastante gerais (como a dos animais e das plantas) e cada nível dessa
hierarquia define subclasses mais específicas. O diagrama a seguir mostra, de maneira resumida,
essa hierarquia. Por simplicidade, nele só incluímos os níveis descritos no texto e suprimimos
vários níveis existentes na biologia:
Hierarquias de classes 73
Seres vivos
Plantas Animais
Carnívoros
Pastor-alemão
Yorkshire
É importante notar que todas as classes de um mesmo nível hierárquico terão um conjunto
bem definido de atributos e operações em comum. Classes de um nível inferior terão todas as
características das classes superiores além de contar com um grupo extra de características e
comportamentos próprios (BOOCH et al., 2006).
Fazemos esse tipo de classificação naturalmente, o tempo todo – não só dentro da biologia.
Vejamos outro exemplo: quando falamos em dispositivos móveis, estamos pensando em uma
série de aparelhos similares, como tablets e celulares. Ao mesmo tempo, sabemos que dispositivos
móveis são uma categoria especial de equipamento eletrônico. Classificar objetos em seus similares
é um aspecto essencial da forma como nós, seres humanos, entendemos o universo a nossa volta.
E a classe dos círculos? Ela teria uma construção muito similar, observe:
1 package br.forma;
2
3 public class Circulo {
4 private double raio;
5 private Cor cor;
6
7 public Circulo(double raio, Cor cor) {
8 this.raio = raio;
9 this.cor = cor;
10 }
11
12 public double getRaio() {
13 return raio;
14 }
15
16 public double getDiametro() {
17 return 2 * raio;
18 }
19
20 public double getArea() {
21 return Math.PI * raio * raio;
22 }
23
24 public double getPerimetro() {
25 return 2 * Math.PI * raio;
26 }
27
28 public Cor getCor() {
29 return cor;
30 }
31 }
O que é o tipo de dado Cor, presente nas duas formas? Trata-se de um enum, definido como:
1 package br.forma;
2
3 public enum Cor {
4 Branco, Vermelho, Amarelo, Laranja, Verde, Azul, Violeta, Preto;
5 }
E como seria nossa classe Main? Iremos definir quatro formas, mas vamos colocá-las em
vetores, já que, no futuro, poderíamos querer ampliar o número de formas de maneira fácil. Como
sabemos até agora, Retangulo e Circulo são classes totalmente diferentes, portanto precisam estar
em vetores diferentes.
76 Programação orientada a objetos I
Vamos colocar aqui também funções para imprimir cor, tamanho e raio de qualquer uma
das formas.
1 package br.forma;
2
3 public class Main {
4 private Retangulo[] retangulos = {
5 new Retangulo(2, 5, Cor.Preto),
6 new Retangulo(3, 1, Cor.Branco)
7 };
8
9 private Circulo[] circulos = {
10 new Circulo(4, Cor.Azul),
11 new Circulo(5, Cor.Verde)
12 };
13
14 private void imprimir(Circulo c) {
15 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
16 c.getCor(), c.getArea(), c.getPerimetro());
17 }
18
19 private void imprimir(Retangulo r) {
20 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
21 r.getCor(), r.getArea(), r.getPerimetro());
22 }
23
24 public void run() {
25 System.out.println("Imprimindo formas");
26 for (Retangulo x : retangulos) {
27 imprimir(x);
28 }
29
30 for (Circulo x : circulos) {
31 imprimir(x);
32 }
33 }
34
35 public static void main(String[] args) {
36 new Main().run();
37 }
38 }
5.3 Herança
Felizmente, o Java permite que também agrupemos classes similares por meio da relação de
herança. Sabemos que um círculo é uma forma geométrica, assim como um retângulo também é
uma forma geométrica.
Analisemos as classes Retangulo e Circulo. O que as duas têm em comum? Vimos que
o atributo cor, além do método getCor(), é idêntico nas duas classes, portanto vamos iniciar
definindo uma classe Forma contendo esse atributo:
1 package br.forma;
2
3 public class Forma {
4 private Cor cor;
5
6 public Forma(Cor cor) {
7 this.cor = cor;
8 }
9
10 public Cor getCor() {
11 return cor;
12 }
13 }
O Java nos permite dizer que as classes Circulo e Retangulo são subclasses da classe Forma.
Assim, elas herdarão todos os atributos e métodos dessa classe, não sendo necessário recriá-los.
Fazemos isso por meio da palavra-chave extends colocada na declaração da classe (SIERRA;
BATES, 2010).
78 Programação orientada a objetos I
como a cor e a largura da linha. Bastaria acrescentá-los na classe Forma. Ademais, incluir uma
nova forma, como um triângulo, seria muito menos sujeito a erros, uma vez que você jamais se
esqueceria de incluir um dos atributos necessários a todas as formas.
Por fim, é importante que você saiba que o Java contém uma superclasse padrão, da qual
todas as classes derivam. É a superclasse Object. Ela define alguns métodos, como toString() e
equals(). Estudaremos essa classe com mais detalhes no Capítulo 7.
Como as variáveis f1 e f2 são do tipo Forma, poderemos utilizar apenas o que as duas
formas têm em comum, ou seja, o método getCor().
Agora, como testar se uma variável do tipo Forma possui em seu interior um objeto do tipo
Circulo ou Retangulo? Podemos fazer isso por meio do operador instanceof (SIERRA; BATES,
No caso acima, como a variável f2 foi inicializada com um Retangulo, o if não executará.
Ao descobrirmos se um objeto tem ou não um círculo, poderíamos querer imprimir um dado
específico do círculo, como o raio. Sabemos que qualquer círculo é uma forma, mas nem toda
forma é um círculo. Por isso, o Java exigirá que a conversão de uma variável de uma superclasse
para uma de uma subclasse seja explicitamente feita pelo programador. Fazemos isso por meio de
uma operação de type casting (SIERRA; BATES, 2010):
1 Forma f1 = new Circulo(4, Cor.Azul);
2 //Type casting: Converte a variável f1 para um círculo
3 Circulo c = (Circulo)f1;
4 System.out.println("O raio do circulo é:" + c.getRaio());
Lembre-se que, por guardarem referências, tanto a variável f1 quanto a variável c apontam
para o mesmo objeto criado na linha 1.
Mas o que aconteceria se fizéssemos o type casting para círculo em uma forma que
não contém um círculo? O Java dispararia um erro em tempo de execução, conhecido como
ClassCastException (DEITEL; DEITEL, 2010).
80 Programação orientada a objetos I
Que tal utilizarmos o que aprendemos para alterar a classe Main de modo a termos um único
vetor de formas?
1 package br.forma;
2
3 public class Main {
4 private Forma[] formas = {
5 new Retangulo(2, 5, Cor.Preto),
6 new Retangulo(3, 1, Cor.Branco),
7 new Circulo(4, Cor.Azul),
8 new Circulo(5, Cor.Verde)
9 };
10
11 private void imprimir(Circulo c) {
12 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
13 c.getCor(), c.getArea(), c.getPerimetro());
14 }
15
16 private void imprimir(Retangulo r) {
17 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
18 r.getCor(), r.getArea(), r.getPerimetro());
19 }
20
21 public void run() {
22 System.out.println("Imprimindo formas");
23 for (Forma f : formas) {
24 if (f instanceof Circulo) {
25 Circulo c = (Circulo)f;
26 imprimir(c);
27 } else if (f instanceof Retangulo) {
28 Retangulo r = (Retangulo)f;
29 imprimir(r);
30 }
31 }
32 }
33
34 public static void main(String[] args) {
35 new Main().run();
36 }
37 }
Dentro do método run(), tivemos de usar o operador instanceof para decidir qual versão
do método imprimir chamar. A pergunta que surge é: será que não há uma maneira mais eficiente
de se fazer isso?
Hierarquias de classes 81
5.4 Polimorfismo
Nosso código já está bem mais interessante, mas ainda temos o que melhorar. Algumas
operações, como a área e o perímetro, são comuns a todas as formas geométricas, porém a maneira
como são feitas difere.
É possível definir uma operação na superclasse e sobrescrever seu comportamento
em suas subclasses. Por exemplo, vamos definir os métodos getArea() e getPerimetro() na
classe Forma:
1 package br.forma;
2
3 public class Forma {
4 private Cor cor;
5
6 public Forma(Cor cor) {
7 this.cor = cor;
8 }
9
10 public Cor getCor() {
11 return cor;
12 }
13
14 public double getArea() {
15 return 0;
16 }
17
18 public double getPerimetro() {
19 return 0;
20 }
21 }
Você poderia esperar que o resultado desse código fosse 0, certo? Porém, ao executar, o
resultado é 6. Isso porque quando fizemos:
Forma f = new Retangulo(3,2, Cor.Verde);
Indicamos que a variável f é uma Forma, mas o objeto dentro dela é da classe Retangulo.
Como a classe Retangulo possui uma versão própria do método getArea(), essa foi a versão
utilizada. Essa capacidade é chamada de polimorfismo (do grego poli = muitas, morfos = formas),
pois um mesmo método poderá se comportar de várias maneiras diferentes, de acordo com a
classe específica do objeto criado (Retangulo), e não com seu tipo de referência (Forma), desde
que seja feita sua sobreposição1 (override) na classe filha (BOOCH et al., 2006).
2 As classes de nível superior também são chamadas de superclasses ou, simplesmente, classes pai. Já as classes
de níveis inferiores são chamadas de subclasses ou classes filhas.
3 Na verdade, caso o tipo de retorno seja um objeto, ele também poderá ser de uma classe filha do tipo de retorno da
superclasse. Isso é chamado de tipo de retorno covariante (DEITEL; DEITEL, 2010).
Hierarquias de classes 83
Todo código duplicado foi eliminado! Observe também que se programássemos o código
de uma nova forma – por exemplo, a classe Triangulo –, só precisaríamos adicioná-lo ao vetor da
classe Main e todo o resto do código, inclusive o método imprimir, já sairia funcionando.
Além de definir constantes, a palavra-chave final tem outro significado. Uma classe pode
ser marcada como final para indicar que ela não poderá mais ter classes filhas. Além disso,
um método pode ser marcado como final para indicar que ele não poderá mais ser sobreposto
(DEITEL; DEITEL, 2010).
Esse código não dá erro, mas também não faz qualquer sentido. Que forma exatamente
seria essa? Por que as operações de área e perímetro retornam 0? Essa confusão ocorre porque
o conceito de forma não é concreto, e sim abstrato. Isto é, sabemos que uma forma qualquer
tem uma cor, uma área e um perímetro, mas não faz sentido pensar em como essa operação é
realizada sem pensarmos em uma forma específica, como um retângulo ou um círculo.
No Java, utilizamos a palavra-chave abstract para indicar que uma classe é abstrata (SIERRA;
BATES, 2010). Uma classe abstrata não pode ser instanciada com o comando new e pode conter
métodos abstratos (sem implementação). Vejamos a classe Forma corrigida:
1 package br.forma;
2
3 public abstract class Forma {
4 private Cor cor;
5
6 public Forma(Cor cor) {
7 this.cor = cor;
8 }
9
10 public Cor getCor() {
11 return cor;
12 }
13
14 public abstract double getArea();
15
16 public abstract double getPerimetro();
17 }
Note que agora conseguimos indicar para o Java que todas as formas possuem os métodos
getArea() e getPerimetro(), mas que não é a classe Forma que define como eles funcionarão.
Assim, ainda será possível chamar esses métodos com base em uma variável do tipo Forma, mas
84 Programação orientada a objetos I
essa variável terá de conter uma instância mais específica da classe (como Retangulo ou Circulo)
em seu interior.
Outro ponto interessante é que agora qualquer programador que queira criar um novo
filho da classe Forma será obrigado a implementar os métodos getArea() e getPerimetro(). Esse
padrão de projeto, de se criar métodos em uma classe pai para servirem de base em classes filhas,
se tornou tão comum que ganhou um nome: Template Method (GAMMA et al., 2007, p. 301).
5.6 Interfaces
Uma das limitações da herança é que cada subclasse pode ter uma, e apenas uma, superclasse4
(SIERRA; BATES, 2010), pois a herança cria um compromisso fortíssimo entre a classe e suas
subclasses (BOOCH et al., 2006).
Uma alternativa bastante flexível são as interfaces. Uma interface é similar a uma classe
abstrata, porém com as seguintes características (DEITEL; DEITEL, 2010):
• Não pode conter atributos.
• Todos os seus métodos são públicos.
• Todos os seus métodos são abstratos.
• Uma mesma classe pode implementar várias interfaces.
A ausência de atributos faz com que classes que implementem interfaces não se
comprometam com uma implementação específica, mas somente com um comportamento
esperado. Por exemplo, poderíamos definir uma interface Colorivel para qualquer coisa que
possa ser colorida em nosso programa.
1 package br.forma;
2
3 public interface Colorivel {
4 Cor getCor();
5 }
Note que, embora possível, não precisamos indicar que o método getCor() é public ou
abstract, pois essa informação seria redundante. Poderíamos também definir uma interface
chamada Poligono para qualquer coisa que tenha área:
1 package br.forma;
2
3 public interface Poligono {
4 double getArea();
5 double getPerimetro();
6 }
4 Na verdade, a orientação a objetos considera a existência do conceito de herança múltipla, em que uma classe
contém mais de uma superclasse, porém pouquíssimas linguagens o implementam, sendo uma delas o C++ e, mesmo
nesta linguagem, seu uso é muito sujeito a erros e exige muita cautela.
Hierarquias de classes 85
Agora, podemos criar variáveis do tipo Colorivel ou Poligono, exatamente igual fizemos
com a classe Forma. Por fim, interfaces também podem realizar herança de outras interfaces, por
meio da palavra-chave extends:
1 package br.forma;
2
3 public interface Figura extends Poligono, Colorivel {
4 }
Note que, no caso de interfaces, pode haver mais de um pai. Como os métodos de interfaces
não contêm implementação, não há conflitos caso haja métodos de mesmo nome nas duas
interfaces pais.
O que aconteceria caso a interface Colorivel também possuísse o método padrão imprimir?
Perceba que, como a classe Forma implementa as duas interfaces, haveria conflito. Nesse caso,
será necessário fazer uma sobreposição, indicando qual dos métodos será usado ou fornecendo
ainda uma terceira implementação:
86 Programação orientada a objetos I
1 package br.forma;
2
3 public abstract class Forma implements Colorivel, Poligono {
4 private Cor cor;
5
6 public Forma(Cor cor) {
7 this.cor = cor;
8 }
9
10 public Cor getCor() {
11 return cor;
12 }
13
14 @Override
15 public void imprimir() {
16 Poligono.super.imprimir();
17 }
18 }
Por fim, é importante saber que esse é um recurso da linguagem Java. Métodos padrão não
fazem parte do paradigma orientado a objetos "puro". Outras linguagens, como o C#, utilizam
outro mecanismo, conhecido como mecanismo de extensão para obter o mesmo resultado.
Considerações finais
Você pode estar um pouco assustado com a quantidade de opções de classificação e
organização de código que tem até agora: herança, interfaces, agregação, composição e pacotes.
Obviamente, decompor um problema em um grupo coeso de classes exigirá tempo e
maturidade, e você provavelmente errará um bocado antes de chegar a boas abstrações. Entretanto,
algumas regras podem auxiliá-lo nesse processo:
1. Utilize os termos é um ou tem para diferenciar entre herança e composição. Por exemplo:
um círculo é uma forma, por isso, a relação é de herança em que Forma é superclasse de
Circulo. Uma turma tem alunos, portanto a relação entre os dois é de composição .
2. Saiba que evitar duplicação de código é um efeito colateral da herança, não um objetivo
em si. Se você está criando uma superclasse só por causa disso, unindo classes que de
outra forma seriam pouco relacionadas, pense duas vezes.
3. Prefira composição a herança.
4. Não subestime as interfaces: procure estar atento em como elas são usadas dentro do
próprio Java e em outras bibliotecas em que você venha a trabalhar. Elas são preferíveis
às classes abstratas na maioria das vezes.
Utilizar bem todo esse arcabouço permitirá que você crie sistemas bastante coesos e fáceis
de ler. Acredite, a orientação a objetos não é tão popular à toa.
Hierarquias de classes 87
• VENNERS, B. Design principles from design patterns: a conversation with Erich Gamma,
Part III. Artima, 6 jun. 2005. Disponível em: https://www.artima.com/lejava/articles/
designprinciples.html. Acesso em: 22 ago. 2019.
Nessa entrevista, Erich Gamma, um dos autores do livro Design Patterns e uma das mentes
por trás da IDE Eclipse e do JUnit, explica para Bill Venners por que deveríamos preferir o
uso de interfaces a implementações com classes abstratas. A entrevista está em inglês, mas
você pode utilizar o recurso de tradução do seu navegador para passá-la para o português.
Vale a leitura.
Atividades
1. Analise as operações e seus resultados e, com base nelas, escreva a hierarquia das classes A,
B, C, D e E.
2. Descreva as classes e seus atributos para um sistema automotivo. Nesse sistema, há o interesse
de cadastrar carros e motos. Todos os veículos possuem uma placa e um chassi. Além disso,
cada veículo é associado a um motor, que possui uma potência, tipo de combustível (inteiro)
e número de válvulas. O motor poderá ser cadastrado em outro ponto do sistema. Para os
carros, também é importante descrever o número de portas. Já para as motos, é importante
incluir a informação das cilindradas. Para esse exercício, não é necessário descrever os
métodos das classes.
3. Considere um vetor de inteiros. Agora, considere a existência de uma função que realize
uma operação sobre cada elemento desse vetor. Como você poderia utilizar o polimorfismo
para que o programador que utiliza essa função possa escolher qual operação será realizada?
88 Programação orientada a objetos I
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
BOOCH, G. et al. Object Oriented Analysis and Design with Applications. 3. ed. Boston: Addison-Wesley,
2006.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
GAMMA, E. et al. Padrões de Projeto: soluções reutilizáveis de software orientado a objetos. Trad. de L. A. M.
Salgado. Porto Alegre: Bookman, 2007.
ORACLE. Default Methods. The Java Tutorials, 2017. Disponível em: https://docs.oracle.com/javase/tutorial/
java/IandI/defaultmethods.html. Acesso em: 4 ago. 2019.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
SUN MICROSYSTEMS. Java Code Conventions. Oracle Technetwork, 12 set. 1997. Disponível em: https://
www.oracle.com/technetwork/java/codeconventions-150003.pdf. Acesso em: 4 ago. 2019.
UZUNIAN, A.; BIRNER, E. Biologia: volume único. 4. ed. São Paulo: Harbra, 2012.
6
Generics e lambda
Neste capítulo, abordaremos alguns recursos presentes na linguagem Java que não
fazem parte do paradigma orientado a objetos. O primeiro deles é o generics, que permitirá
ainda mais abstração ao longo da leitura deste livro. Trata-se de uma implementação bastante
peculiar que ocorreu somente na versão 5 da plataforma, gerando uma série de preocupações
com compatibilidade.
O segundo recurso é o das expressões lambda, que nos permite tratar funções como se fossem
dados. Esse recurso introduz na linguagem Java um novo paradigma de programação, o funcional.
Não aprofundaremos os conceitos e práticas desse paradigma, mas estudaremos o recurso, pois ele
nos dá uma forma prática de utilizar funções em que implementações rápidas sejam necessárias.
Veremos vários exemplos de uso das expressões lambda nas próximas sessões.
(Continua)
1 Observe que essa classe será muito similar à classe Turma, do Capítulo 4.
90 Programação orientada a objetos I
Tudo parece bem até aqui, certo? Temos um método get, que retorna nulo caso o elemento
não exista na lista. Temos um método adicionar, que adiciona objetos ao fim da lista, ou retorna
false caso isso não seja possível.
Agora, vejamos o que acontece ao usarmos essa lista. Que tal guardarmos alguns objetos da
classe Planeta, criada no Capítulo 3?
1 var sistemaSolar = new Lista(9);
2 sistemaSolar.adicionar(new Planeta("Mercurio", 4_878, 0.055));
3 sistemaSolar.adicionar("Venus");
4 sistemaSolar.adicionar(new Planeta("Terra", 12_742, 1.0));
5 sistemaSolar.adicionar(new Planeta("Saturno", 120_536, 95.2));
6
7 //Imprimindo o planeta
8 for (var i = 0; i < sistemaSolar.getQtde(); i++) {
9 Planeta p = (Planeta) sistemaSolar.get(i);
10 p.imprimir();
11 }
você tentou executar esse programa deve ter recebido a seguinte mensagem de erro:
Generics e lambda 91
Por que ele ocorre? Porque, na linha 3, inserimos por acidente um texto no interior da lista,
e não um objeto da classe Planeta.
6.2 Generics
Os tipos genéricos, também chamados de generics, são classes que nos permitem parametrizar
tipos de dados. Assim, quando criamos um objeto dessa classe, podemos especificar quais tipos de
dados serão utilizados (SIERRA; BATES, 2010).
Quando criamos uma classe genérica ou declaramos uma variável em uma classe
genérica, utilizamos os sinais de < e > para especificar parâmetros formais de tipo, que nada
mais são do que "variáveis" que representam um tipo de dado, em vez de um valor. Uma vez
criado, podemos utilizar o parâmetro formal em qualquer lugar em que o tipo de dado possa
ser usado (GOETZ, 2004). Por exemplo, poderíamos reescrever nossa classe de lista assim:
1 public class Lista<T> {
2 private int qtde;
3 private T elementos[];
4
5 public Lista(int capacidade) {
6 this.elementos = (T[]) new Object[capacidade];
7 }
8
9 public T get(int indice) {
10 return indice >= qtde ? null: elementos[indice];
11 }
12
13 public boolean adicionar(T objeto) {
14 if (qtde == getCapacidade()) {
15 return false;
16 }
17 elementos[qtde++] = objeto;
18 return true;
19 }
20
21 public int getCapacidade() {
22 return elementos.length;
23 }
24
25 public int getQtde() {
26 return qtde;
27 }
28 }
92 Programação orientada a objetos I
E como usaríamos essa lista? Como toda variável, poderíamos declará-la da maneira
explícita:
Lista<Planeta> sistemaSolar = new Lista<>(9);
Ou implícita:
var sistemaSolar = new Lista<Planeta>(9);
Observe que o main seria similar ao que já programamos, mas, agora, o cast (após o uso
do método get) não será mais necessário, e o compilador apresentará imediatamente um erro se
tentarmos incluir a String na lista:
1 var sistemaSolar = new Lista<Planeta>(9);
2 sistemaSolar.adicionar(new Planeta("Mercurio", 4_878, 0.055));
3 //sistemaSolar.adicionar("Venus"); Não funciona mais
4 sistemaSolar.adicionar(new Planeta("Terra", 12_742, 1.0));
5 sistemaSolar.adicionar(new Planeta("Saturno", 120_536, 95.2));
6
7
8 //Imprimindo o planeta
9 for (var i = 0; i < sistemaSolar.getQtde(); i++) {
10 sistemaSolar.get(i).imprimir(); //O get já retorna um planeta
11 }
Por fim, o uso dos tipos genéricos também impede que listas contendo tipos diferentes no
seu interior sejam misturadas. Por exemplo, se possuirmos uma função que aceite como parâmetro
de entrada uma Lista<Planeta>, não poderemos chamá-la acidentalmente de Lista<Aluno>.
Permitirá qualquer tipo de lista em seu interior. Isso possibilita que programadores atualizem
seu código para uma versão genérica, sem forçá-los a reescrever todas as classes que utilizam esse
código – o que é especialmente relevante para fabricantes de bibliotecas para terceiros. Obviamente,
nos pontos em que esse uso perigoso é realizado, o compilador gerará um aviso durante a compilação
(SIERRA; BATES, 2010).
Generics e lambda 93
Outra consequência é o fato de não ser possível chamar o construtor do tipo T. Também, por
questões de compatibilidade, é possível fazer o cast do vetor de objetos para o tipo T[], mas essa
prática é insegura e deve ser usada com cuidado (BLOCH, 2019). Nós a utilizamos na linha 6 do
código da lista:
this.elementos = (T[]) new Object[capacidade];
Por fim, outra consequência importante do type erasure é que não é possível fazer sobrecarga
de métodos quando a única coisa que muda é o parâmetro T, ou seja, esses dois métodos, quando
o tipo T for removido, vão se tornar idênticos:
public static void print(Lista<Planeta> lista)
public static void print(Lista<String> lista)
Isso poderia gerar um erro de compilador de que um método está duplicado. Nesse caso,
seríamos obrigados a contornar o problema, criando métodos com nomes diferentes.
Agora, vamos supor que seja necessário criar um método utilitário em outra classe, para
fazer a comparação de dois objetos diferentes do tipo Par. Ele poderia ser feito assim:
1 public class Util {
2 public static <K, V> boolean compare(Par<K, V> p1, Par<K, V> p2) {
3 return p1.getChave().equals(p2.getChave()) &&
4 p1.getValor().equals(p2.getValor());
5 }
6 }
Observe, nesse caso, o uso do método equals, da classe Object, para testar se os conteúdos
dos dois objetos são iguais. Como chamaríamos esse método? A forma completa seria:
1 var p1 = new Par<String, String>("Raffs", "João");
2 var p2 = new Par<String, String>("Imai", "Bruno");
3 boolean iguais = Util.<String, String>compare(p1, p2);
94 Programação orientada a objetos I
O Java também é capaz de deduzir o parâmetro do método com base em sua chamada, o que
viabilizaria a forma resumida a seguir, idêntica à de um método comum:
boolean iguais = Util.compare(p1, p2);
Assim como os tipos genéricos de p1 e p2 estão utilizando duas Strings, o Java utilizará o
método compare, também com duas Strings.
6.2.3 Wildcards
Muitas vezes, queremos uma classe genérica como parâmetro de um método. Nesse caso,
acabamos surpresos ao descobrir que um método, como o apresentado a seguir, não aceita como
parâmetro de entrada uma Lista<String> – mesmo sendo String um filho direto de Object:
public static void print(Lista<Object> lista)
Para entender o porquê, vamos pensar em um caso mais amplo. O método print, como
usa uma Lista<Object>, poderia ter em seu interior uma chamada ao método adicionar da lista,
utilizando como tipo de entrada um objeto da classe Planeta. Isso deveria ser possível para uma
List<Object>, mas não para um List<String>, o que explica a proibição do Java.
Para resolver esse problema, os wildcards foram introduzidos. Assim, o método print
O sinal de interrogação (?) indica que o tipo é desconhecido e a única certeza que teremos
é de que ele é um filho de Object. Com isso, o Java permitirá chamar somente os métodos da lista
em que o tipo T seja um valor de retorno, mas não um parâmetro – em nosso caso, o método get,
não mais o método adicionar. Isso torna o uso da lista segura, mesmo que uma lista filha de Object
seja fornecida (SIERRA; BATES, 2010).
Os wildcards não são limitados a objetos da classe Object. Podemos torná-los mais
específicos, utilizando a palavra-chave extends:
public static void print(Lista<? extends Planeta> lista)
Na verdade, wildcards podem ter até o compromisso inverso (DEITEL; DEITEL 2010). Se
quiséssemos que o método print recebesse um objeto do tipo Planeta ou qualquer um dos seus
pais, poderíamos fazer:
public static void print(Lista<? super Planeta> lista)
Também de forma inversa, seríamos proibidos de chamar qualquer método da lista que
retornasse um parâmetro do tipo Planeta, ou seja, poderíamos chamar o método adicionar, mas
não mais o método get.
6.3 Lambda
Funções lambda foram introduzidas no Java a partir da versão 8, lançada em março de 2014
(ORACLE, 2017a), e representaram um marco na linguagem, pois introduziram a possibilidade de
se trabalhar mais fortemente com conceitos do paradigma funcional. Vamos entender como elas
funcionam e como podemos utilizá-las para melhorar nosso código.
Generics e lambda 95
Aqui, criamos na linha 2 uma nova turma, que será retornada. Então, percorremos cada
aluno da turma, buscando aqueles cujo nome inicie com o parâmetro criado e os adicionamos
nessa nova turma. Portanto, o código a seguir retornaria a uma nova turma, contendo todos os
alunos com nomes iniciados pela letra A:
var alunosComA = turma.coletarPorNome("A");
Agora, para fazer uma função similar na classe lista, enfrentamos o seguinte problema: qual
seria o critério utilizado no if da função coletar, uma vez que sequer sabemos qual classe está
contida no tipo T da lista? A solução desse problema está em fornecer uma interface que permita
ao programador testar elemento por elemento:
public interface Criterio<T> {
boolean atende(T elemento);
}
Com base nessa interface, poderíamos então fazer o método coletar genérico:
1 public Lista coletar(Criterio<T> criterio) {
2 Lista coletados = new Lista(elementos.length);
3 for (T elemento : elementos) {
4 if (criterio.atende(elemento)) {
5 coletados.adicionar(elemento);
6 }
7 }
8 return coletados;
9 }
96 Programação orientada a objetos I
Como utilizaríamos essa função? Para exemplificar, vamos coletar todos os planetas cujo
nome se inicia com determinada letra (igual fizemos para os alunos). O primeiro passo seria criar
uma classe que implementasse esse critério:
1 public class NomeIniciaCom implements Criterio<Planeta> {
2 private String nome;
3
4 public NomeIniciaCom(String nome) {
5 this.nome = nome;
6 }
7
8 @Override
9 public boolean atende(Planeta elemento) {
10 return elemento.getNome().startsWith(nome);
11 }
12 }
Ganhamos muita flexibilidade com essa implementação: agora, é possível utilizar a função
coletar para qualquer tipo de critério (como a massa ou até dois critérios juntos), bastando, para
isso, criar classes que implementam a interface Criterio. Efetivamente, transformamos a função
de critério em um tipo de dado, que pode ser passado como parâmetro para o método coletar
(ORACLE, 2017a).
Muitas vezes, iremos utilizar critérios em situações específicas, simples e apenas uma vez.
Nesse caso, parece muito código para pouco resultado, não? Até o Java 7, poderíamos simplificar
um pouco essa situação utilizando classes anônimas (DEITEL; DEITEL, 2010):
1 var planetasComA = planetas.coletar(new Criterio<Planeta>() {
2 @Override
3 public boolean atende(Planeta elemento) {
4 elemento.getNome().startsWith("A");
5 }
6 });
Ainda assim, parece haver uma quantidade significativa de código. A solução para o
problema? Lambda.
Como utilizaríamos o lambda para implementar, na nossa lista de planetas, o filtro por
nome? Primeiro, vamos prestar atenção na interface Criterio:
public interface Criterio<T> {
boolean atende(T elemento);
}
Ela possui o método atende, que precisa receber como parâmetro um elemento do tipo T
(Planeta, no caso da nossa lista), portanto, nosso lambda também terá como parâmetro um objeto
com o elemento. O resultado, utilizando a forma completa do lambda, seria uma chamada assim:
1 var planetasComA = planetas.coletar((Planeta elemento) -> {
2 return elemento.getNome().startsWith("A");
3 });
Agora, vamos utilizar as regras de simplificação. Vamos deixar o Java deduzir o tipo do dado
do parâmetro e, como só temos um único parâmetro, omitiremos também os parênteses. Além
disso, podemos remover as chaves e o return, já que nossa implementação consiste em uma única
expressão. Como nosso código ficará muito curto, podemos até simplificar o nome da variável
elemento para simplesmente p (de planeta). O resultado é uma linha simples:
1 var planetasComA = planetas.coletar(p -> p.getNome().startsWith("A"));
Para esses casos, é mais limpo utilizarmos referências a métodos, por meio do operador
(ORACLE, 2017b). Veja um exemplo:
var habitabeis = planetas.coletar(Planeta::isHabitavel);
98 Programação orientada a objetos I
Construtor NomeDaClasse::new
Observe que, em nosso exemplo, utilizamos o terceiro tipo. Outro ponto interessante é o fato
de que construtores também podem ser referenciados por meio do nome new.
Referências a métodos não estão limitadas a métodos sem parâmetros. Qualquer método cuja
chamada seja direta, utilizando todos os parâmetros do lambda pode ser substituído. Isso gera uma
sintaxe muito mais limpa e inteligível, principalmente quando o lambda possuir muitos parâmetros.
Além de nos poupar a escrita de interfaces simples, essas interfaces podem já vir turbinadas
com alguns métodos padrão úteis, o que torna seu uso preferível a criar seus próprios tipos
(BLOCH, 2019).
No caso da interface Predicate, já estariam disponíveis os métodos negate, or e and, que
aplicam operações lógicas sobre o resultado do predicado. Por exemplo, poderíamos coletar os
planetas não habitáveis com o seguinte código:
Predicate<Planeta> p = Planeta::ehHabitavel;
var habitabeis = planetas.coletar(p.negate());
Observe que aqui o negate foi usado para gerar automaticamente uma versão invertida
(negada) do método ehHabitavel. Além disso, por serem mantidas pela Oracle, é possível que mais
métodos padrão como esses sejam incluídos no futuro.
Generics e lambda 99
Considerações finais
Com os recursos vistos neste capítulo, descobrimos maneiras ainda mais flexíveis de escrever
nosso código. É importante notar que a escrita de boas abstrações nos permite o reuso do código.
Reutilizar código é muito importante, pois:
• Aumenta a modularidade: veja o exemplo da classe da lista, feita neste capítulo. Ela não
precisaria ser duplicada caso, em vez de planetas, precisássemos criar uma lista de Alunos.
Além disso, uma classe mais geral, como essa, poderia ser usada para simplificar o código
de uma classe mais específica (como a classe Turma, do Capítulo 4), que possuísse regras
mais complexas.
• Não parte do zero: imagine se, a cada novo projeto, formos obrigados a criar novas classes
para listas, como as que criamos neste capítulo. Boas abstrações permitem que criemos
nossas próprias bibliotecas de classes.
• Aumenta a robustez do código: classes reutilizadas em mais projetos passam a ser
testadas em uma gama maior de situações. Quando seus bugs são corrigidos, podem
imediatamente ser aplicados em todos os projetos que as utilizam. Ao longo do tempo,
isso garante robustez e performance.
• Compartilha código: podemos compartilhar boas abstrações com outros programadores.
Podemos baixar classes prontas de outros programadores ou disponibilizar nossas
próprias classes para uso de terceiros. De fato, uma biblioteca de classes robusta pode ser
um negócio tão rentável quanto uma aplicação, como foi o caso do Hibernate.
Por esses motivos, linguagens orientadas a objetos e suas plataformas, como o próprio Java, se
tornaram tão populares. Se pesquisarmos na internet, veremos que já existem bibliotecas prontas para
inúmeras situações, como acesso a bancos de dados, redes e processamento avançado de imagens.
Por isso, mais do que estudar a linguagem Java, concentre-se em codificar pensando no quão
flexíveis, simples de entender e reutilizáveis suas classes são. Reflita se o código parece expressar
um bom idioma para o programador que o utilizar. Pense se suas classes reforçam o uso correto,
evitando erros de programação indesejados. Tudo isso lhe permitirá a criação de sistemas cada vez
maiores, mais robustos e com qualidade.
Atividades
1. Escreva um método converter na classe lista, para gerar uma nova lista de mesmo tamanho
com os elementos da lista convertidos em outro tipo de dado. Por exemplo, você poderia ter
uma Lista<Planeta> e querer chamar o método converter para gerar uma Lista<String>
contendo apenas uma descrição dos planetas. Tente utilizar generics para tornar seu método
o mais flexível possível.
2. Gere uma lista de planetas e a converta em uma lista de descrições de planetas. Utilize
lambda.
3. Qual modificação poderia ser aplicada para o método coletar deste capítulo se tornar ainda
mais abrangente?
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
GOETZ, B. Introduction to generic types in JDK 5.0. IBM DeveloperWorks, 7 dez. 2004. Disponível em:
https://www.ibm.com/developerworks/java/tutorials/j-generics/j-generics.html. Acesso em: 21 set. 2019.
ORACLE. Lambda Expressions. The Java Tutorials, 2017a. Disponível em: https://docs.oracle.com/javase/
tutorial/java/javaOO/lambdaexpressions.html. Acesso em: 21 set. 2019.
ORACLE. Method References. The Java Tutorials, 2017b. Disponível em: https://docs.oracle.com/javase/
tutorial/java/javaOO/methodreferences.html. Acesso em: 21 set. 2019.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
7
A biblioteca de coleções
Além de possuir uma linguagem poderosa, a plataforma Java conta com uma série de classes
prontas, que facilitam enormemente a tarefa do programador. Dentre essas classes, encontra-se a
biblioteca de coleções, chamada Java Collections Framework, disponível no pacote java.util.
Essa biblioteca fornece dois tipos básicos de coleções. O primeiro tipo implementa a
interface Collection, agrupamentos de objetos em que é possível percorrer elemento a elemento
(iteráveis) – isso inclui filas, listas e conjuntos de objetos sem repetição. O segundo tipo se refere
a coleções que implementam a interface Map e, como o nome indica, realizam mapeamento, isto
é, classes capazes de associar dois objetos entre si. Além das operações básicas, como adicionar
e remover elementos, as coleções fornecem, por meio do recurso de streams, capacidades
avançadas de ordenação e filtro.
Como você viu nos capítulos anteriores, associar objetos é parte essencial da orientação
a objetos. Por esses motivos, conhecer a biblioteca de coleções é fundamental para qualquer
programador Java. Vamos desvendá-la?
7.1 Listas
O tipo de coleção mais intuitivo certamente é a lista. Afinal, listas fazem um paralelo direto
com os vetores: são coleções em que um elemento pode ser acessado pelo índice. A biblioteca de
coleções fornece dois padrões de implementações para listas (SIERRA; BATES, 2010):
1. ArrayList: representa uma lista sequencial, ou seja, a classe utilizará um vetor
internamente para armazenar seus elementos. Isso permite que operações como o acesso
a um elemento sejam feitas em um tempo constante, enquanto operações como adição e
remoção gastem um tempo linear amortizado.
2. LinkedList: representa uma lista ligada, em que os elementos ficam dispersos na
memória, em uma estrutura conhecida como nó. Qualquer acesso que envolva índices
fará com que essa lista percorra o elemento a partir do primeiro até alcançar aquele
desejado, tendo alto impacto na performance. A operação de adição é muito veloz, assim
como a remoção de um elemento já encontrado.
Ambas as listas são dinâmicas, o que singifica que a quantidade máxima de elementos será
limitada apenas pela memória do computador, e as duas classes implementam a interface List. É
essa interface que especifica quais operações uma lista deve possuir e como cada operação precisa
se comportar. Este padrão, o de conter uma interface descrevendo um tipo de coleção e classes
concretas com diferentes implementações, é recorrente na biblioteca de coleções.
É uma boa prática utilizar as listas sempre por meio de sua interface principal (BLOCH,
2019). Por exemplo, declaramos um List da seguinte forma:
102 Programação orientada a objetos I
Isso faz com que possamos alterar facilmente a implementação da lista no futuro. É pertinente
mencionar que você não pode utilizar um generic com um tipo primitivo – criando um List<int>,
por exemplo. Esse problema é resolvido por meio de classes cujo único papel é armazenar um valor
primitivo, chamadas de wrappers (como a classe Integer). Veja mais detalhes sobre essas classes nos
vídeos recomendados na seção Ampliando seus conhecimentos deste capítulo.
frutas.add("Maçã");
frutas.add("Laranja");
Outra possibilidade é indicar para o método em que índice o elemento será inserido.
Os índices iniciam em zero, portanto, para inserir a palavra mamão na segunda posição da
lista, utilizaríamos:
frutas.add(1, "Mamao");
Além do método add, podemos utilizar o método addAll para adicionar uma coleção
inteira ao final ou com base em determinado índice de nossa lista. A coleção recebida não precisa
necessariamente ser uma lista.
1 List<String> legumes = new ArrayList<>();
2 legumes.add("Beringela");
legumes.add("Couve-flor");
3
4
List<String> feira = new ArrayList<>();
5
feira.addAll(frutas);
6
feira.addAll(0, legumes);
7
Também é possível substituir elementos por meio do método set. Esse método aceita como
parâmetro o índice e o elemento a ser trocado.
Por fim, há ainda uma poderosa operação de substituição que permite utilizar um lambda
para realizar uma operação sobre todos os elementos da lista. Esse recurso é chamado replaceAll.
Para exemplificar, o comando a seguir poderia ser usado para tornar minúsculas todas as palavras
da lista feira:
feira.replaceAll(String::toLowerCase);
Fique atento apenas para o fato de que todos os métodos que utilizam índices podem disparar
um erro caso o índice fornecido seja menor do que 0 ou maior do que o último índice disponível
na lista.
A biblioteca de coleções 103
Você também pode remover itens da lista com base em uma condição, utilizando, para
isso, o método removeIf. Basta passar um lambda que retorna true sempre que o item precisar ser
removido (ORACLE, 2017). O exemplo a seguir removeria qualquer item da lista iniciado com a
letra R:
frutas.removeIf(e -> e.startsWith("R"));
Se a lista tiver sido modificada após sua chamada, todos os métodos retornam true. Assim,
caso um elemento não exista, o método remove retornará false (SIERRA; BATES, 2010). De forma
similar, a lista também possui o método removeAll para eliminar todos os objetos presentes em
uma coleção da lista. Outra possibilidade é utilizar o método retainAll, que apagará todos os
elementos da lista que não estiverem na coleção fornecida.
A segunda maneira é iterar elemento por elemento. A forma mais simples e direta de se
iterar sobre uma lista é aplicar o comando for each (BLOCH, 2019):
Também é possível iterar utilizando um objeto do tipo Iterator, que se trata da implementação
de um padrão de projetos de mesmo nome (GAMMA et al., 2007). Para isso, basta chamar a função
iterator, presente na maioria das coleções, e um while. Uma das vantagens do iterador sobre o
for é que ele possui o método remove, que permite remover o último objeto retornado (DEITEL;
DEITEL, 2010). Exceto por esse método, você não poderá alterar a lista durante sua iteração e, se o
fizer, fará com que o erro ConcurrentModificationException seja disparado.
Refaçamos o exemplo do removeIf, que removia todas as frutas iniciadas com R, usando
esse recurso:
104 Programação orientada a objetos I
ocorre porque esses comandos surgiram em versões mais novas da linguagem e, assim, substituíram
essa prática.
O interessante é que as operações feitas nessa sublista têm efeito na lista original, portanto,
se um comando clear for dado na variável lista, a sublista ficará vazia, mas isso também excluirá
os respectivos elementos da lista feira (DEITEL; DEITEL, 2010).
É possível, ainda, copiar os elementos da lista em um vetor por meio do comando toArray.
Há duas versões desse método: uma delas sem parâmetros, que retornará um vetor da classe Object;
e outra que recebe como parâmetro um vetor do mesmo tipo da lista e o retorna preenchido. Esse
vetor precisa ter um tamanho igual ou superior ao da lista para ser preenchido diretamente, pois,
do contrário, um novo vetor do tamanho correto será criado. Um idioma comum é utilizar esse
método com um vetor de tamanho 0:
var array = feira.toArray(new String[0]);
Observe que um vetor também poderia ter sido usado no interior do método asList.
A biblioteca de coleções 105
7.2 Conjuntos
Conjuntos representam um grupo de elementos sem repetição (SIERRA; BATES, 2010), e,
diferentemente das listas, não podem ser acessados por índices, nem a ordem desses elementos
corresponderá necessariamente à ordem de sua inserção (DEITEL; DEITEL, 2010).
No Java, todos os conjuntos são filhos da interface Set, filha de Collection. O Java fornece
três implementações padrão de conjuntos:
• HashSet: para conjuntos em que não seja necessária uma ordem específica;
• LinkedHashSet: para conjuntos em que a ordem de inserção deva ser respeitada;
• TreeSet: em que os elementos se encontrarão ordenados. A interface SortedSet, filha de
Set, é implementada por essa classe.
Os conjuntos, assim como as listas, são filhos de Collection, por isso eles também possuirão
as operações clear, add, addAll, contains, containsAll, isEmpty, remove, removeAll, removeIf,
retainAll, size e toArray. Além disso, todos podem ser iterados utilizando-se o for each ou um
Fazendo uso do método descendingSet, você também pode retornar um SortedSet com
a ordem contrária. De forma parecida com o que ocorre com a função subset, alterações nesse
conjunto terão impacto no conjunto original. Há, ainda, métodos convenientes para a busca de um
único elemento:
• first e last: retornam o primeiro ou o último elemento do conjunto;
• lowere higher: retornam o elemento imediatamente inferior ou superior ao elemento
passado por parâmetro. Se o elemento não estiver no conjunto, retorna nulo;
• floor e ceiling: similar ao lower e higher, mas considera que o elemento também pode
ser igual ao passado por parâmetro.
Como os conjuntos sabem se dois elementos são iguais ou como ordená-los? É o que veremos
a seguir.
(Continua)
A biblioteca de coleções 107
Ele também será usado se você imprimir uma coleção inteira, como uma lista ou conjunto.
Caso você não forneça uma implementação de toString, o Java utilizará a padrão que imprime o
nome da classe, seguido de @, seguida de um identificador único do objeto – um número único
para a máquina virtual, sem muito sentido para nós, seres humanos (DEITEL; DEITEL, 2010).
Outra ferramenta importante é o método equals, usado quando precisamos testar se dois
objetos são iguais. É importante ressaltar que igualdade e identidade são conceitos diferentes
(BOOCH et al., 2006):
• Identidade: propriedade que está no fato de o objeto existir e ser único, ou seja, testar por
identidade é determinar se duas variáveis contêm o mesmo objeto. No Java, testamos a
identidade por meio do operador ==;
• Igualdade: conceito relativo, que precisa ser implementado caso a caso. De maneira geral,
representa dizer que os objetos possuem um conjunto de valores iguais, tal que possam
ser considerados exemplos de um mesmo objeto.
Vamos ver um exemplo dos dois conceitos:
1 String nome1 = "Vinicius";
2 String nome2 = nome1;
3 String nome3 = new String("Vinicius");
4
5 System.out.println(nome1); //Imprime Vinicius
6 System.out.println(nome2); //Imprime Vinicius
7 System.out.println(nome3); //Imprime Vinicius
8
9 //True, são o mesmo objeto
10 System.out.println(nome1 == nome2);
11
12 //False: O nome é igual, mas não é o mesmo objeto
13 System.out.println(nome1 == nome3);
14
15 //True: Os valores de nome1 e nome3 são iguais
16 System.out.println(nome1.equals(nome3));
108 Programação orientada a objetos I
Observe que nome1 e nome3 contêm valores iguais, mas a execução no new na linha 3 forçou a
criação de um novo objeto de texto. Por isso, o operador de == na linha 13 retornou false, já que,
apesar dos valores, são objetos diferentes (identidade). A igualdade foi testada pelo método equals,
na linha 16.
Para implementar o conceito de igualdade em nossos próprios objetos, precisamos
sobrescrever o método equals. Essa implementação precisa respeitar cinco regras (BLOCH, 2019):
1. Reflexividade: para um objeto x, x.equals(x) deve sempre retornar true.
2. Simetria: se x.equals(y) for verdadeiro, então y.equals(x) deve ser verdadeiro.
3. Transitividade: se x.equals(y) for verdadeiro e y.equals(z) for verdadeiro, então
x.equals(z) também deve ser verdadeiro.
4. Consistência: se x.equals(y)for verdadeiro e nenhum valor for modificado, então
x.equals(y) deve se manter verdadeiro.
5. Não nulidade: se x for um objeto, então x.equals(null) deve ser sempre falso.
Por exemplo, para nossa classe do aluno, poderíamos considerar iguais dois alunos com
mesmo nome e número de matrícula (descartando a idade):
1 @Override
2 public boolean equals(Object obj) {
3 if (obj == null) return false; //Não nulidade
4 if (obj == this) return true; //Reflexividade
5
6 if (!(obj instanceof Aluno)) return false;
7
8 Aluno o = (Aluno) obj;
9 return matricula == matricula && nome.equals(o.nome);
10 }
Caso o hash code não seja implementado, o Java retornará o identificador do objeto para a
VM (o mesmo usado no toString).
É por meio desse método que as coleções com Hash no nome trabalham, como o HashSet.
Felizmente, o Java implementa uma forma fácil de calcular hash codes com a classe Objects. Para
executar o método hashCode da classe Aluno, faríamos:
1 @Override
2 public int hashCode() {
3 return Objects.hash(matricula, nome);
4 }
Esses não são os únicos métodos da classe Object. Ela ainda possui o método clone,
responsável por retornar uma cópia do objeto. Para implementá-lo, você deve, também, fazer
sua classe implementar a interface Cloneable. Além disso, ele possui métodos para lidar com
programação concorrente, que não estudaremos nesse livro.
Coleções com base em hash necessitarão dos métodos hashCode e equals implementados
corretamente para funcionar. No caso dos mapas, isso vale tanto para as chaves quanto para
os valores.
Observe que, para um número inteiro como a matrícula, uma forma mais sucinta de
implementar esse método seria com uma subtração, o que pode ser escrito em uma única linha
(BLOCH, 2019):
return matricula - a.matricula;
Agora, vejamos um exemplo de Comparator que ordena os alunos por nome, o que podemos
fazer utilizando um lambda. Criaremos como uma constante da classe Aluno, embora esse objeto
pudesse ser criado em qualquer lugar:
1 public class Aluno implements Comparable<Aluno> {
2 public static final Comparator<Aluno> POR_NOME =
3 (a1, a2) -> a1.getNome().compareTo(a2.getNome());
Embora as listas não sejam naturalmente ordenadas, como acontece nos sets, o método
Collections.sort permite ordená-las, como mostra o exemplo a seguir:
É o caso do nosso exemplo: caso dois alunos de mesma matrícula sejam inseridos em um
TreeSet, um deles será descartado, pois o nome não é levado em conta pela nossa implementação
7.3 Mapas
Mapas associam objetos entre si. Um dos objetos atua como uma chave, que é utilizada para
localizar rapidamente o segundo objeto (SIERRA; BATES, 2010). Uma analogia válida para um
mapa é considerá-lo uma espécie de vetor, no qual a chave não precisa ser necessariamente um
número nem precisa ser contínua.
Mapas não implementam a interface Collection, porém, os métodos clear, isEmpty e size
se comportam exatamente igual aos métodos das demais coleções. De forma semelhante aos sets,
os mapas contêm as implementações HashMap, LinkedHashMap e TreeMap, e você as utiliza de acordo
com a importância de ele ser ordenado em relação às chaves.
A inserção de um objeto em um mapa é feita pelo método put. São necessários dois valores,
a chave e o objeto. Para exemplificar, criamos, a seguir, um mapa que associa os alunos criados na
lista do exemplo anterior a seu próprio nome:
1 Map<String, Aluno> alunoMap = new HashMap<>();
2 for (Aluno aluno : alunos) {
3 alunoMap.put(aluno.getNome(), aluno);
4 }
O mapa possui dois métodos para testar seu conteúdo. O método containsKey, que testa
se uma chave existe no mapa, e o containsValue, que busca por um valor. O primeiro método é
bastante veloz, enquanto o segundo fará uma busca por todo o mapa (DEITEL; DEITEL, 2010).
Para ler um dado do mapa, podemos usar a função get, que retorna o objeto associado à
chave, ou null se o objeto não existir. Outra possibilidade é usar o método padrão getOrDefault,
similar ao get, mas que nos permite definir que valor será retornado caso o objeto não exista.
Vejamos alguns exemplos:
1 if (alunoMap.containsKey("Ana")) {
2 System.out.println("Ana está no mapa");
3 }
4
5 Aluno a1 = alunoMap.get("Juca");
6 System.out.println(a1); //Imprime nulo
7
8 Aluno a2 = alunoMap.getOrDefault("Juca", new Aluno(0, "?", 0));
9 System.out.println(a2); //Imprime: ?(0)
10
11 Aluno a3 = alunoMap.get("Pedro");
12 System.out.println(a3); //Imprime: Pedro(314)
112 Programação orientada a objetos I
Para percorrer os valores de um mapa por meio de um for each, podemos utilizar três métodos
diferentes: keySet, values ou entrySet. Tudo depende de querermos imprimir, respectivamente,
somente as chaves, os valores ou os dois ao mesmo tempo (SIERRA; BATES, 2010). Como os nomes
indicam, os métodos retornarão conjuntos contendo os valores. No caso do método entrySet, o
par chave/valor será retornado dentro de um objeto da classe Entry, que contém os métodos getKey
e getValue. Observe um exemplo:
1 for (var entry : alunoMap.entrySet()) {
2 System.out.println("Nome:" + entry.getKey());
3 System.out.println("Aluno:" + entry.getValue());
4 }
Para percorrer as chaves e valores, o mapa também fornece o método forEach, que pode ser
implementado com um lambda (ORACLE, 2017):
1 alunoMap.forEach((k, v) -> {
2 System.out.println("Nome:" + k);
3 System.out.println("Aluno:" + v);
4 });
Para remover um item do mapa, utilize o método remove, indicando a chave que deseja
remover. O mapa fornece uma versão adicional do método, em que você também poderá indicar o
valor, e a remoção será feita apenas se o valor fornecido estiver mapeado naquela chave.
Além do método put, você pode utilizar o método replace para substituir o valor em um
mapa. A diferença é que esse método não incluirá novos valores no mapa, só alterará valores já
existentes. Similarmente ao método remove, há uma versão do replace em que é possível indicar
também o par chave/valor, que só substituirá o valor caso o par esteja correto.
O mapa também fornece o método replaceAll, que permite substituir todos os objetos com
base em um lambda. O recurso fornece como entrada a chave e o valor antigos e permite que se
retorne um novo valor a ser associado àquela chave. O exemplo a seguir altera todos os alunos de
um mapa, incluindo o nome da turma no nome de cada um deles:
1 alunoMap.replaceAll((k, v) -> new Aluno(
2 v.getMatricula(), "Turma1: " + v.getNome(), v.getIdade())
3 );
7.4 Streams
Frequentemente precisamos processar os dados dentro de uma coleção, seja para filtrá-
-los, calcular totais, médias etc. Os streams fornecem maneiras poderosas e eficientes para realizar
essa tarefa (URMA, 2014). Utilizamos streams por meio do método stream ou parallelStream.
O segundo utilizará processamento paralelo, aproveitando os vários núcleos de processamento
de um computador.
A biblioteca de coleções 113
Em seguida, possuímos uma série de métodos que podem ser utilizados de forma encadeada.
Vejamos alguns deles a seguir (URMA, 2014):
• filter: filtra a coleção com base em algum critério;
• map e flatMap: convertem cada elemento da coleção para outro valor, com base em uma
função de mapeamento. Utilizamos o map quando queremos indicar quais atributos das
classes deverão fazer parte do resultado. Caso o valor seja numérico, há também as versões
mapToInt, mapToLong e mapToDoublem, que retornam streams numéricos;
Observe que utilizamos, aqui, a função filter para aplicar o critério de alunos maiores de
9 anos. Em seguida, sobre o resultado do filtro, chamamos a função map para separar no resultado
somente o nome do aluno, não os demais campos. Ordenamos o resultado disso com a função
sorted, para finalmente iterar no resultado com a função forEach e imprimir os textos. Ou, em vez
de imprimir, talvez fosse mais útil ter esses nomes em uma lista, bastando, para isso, substituir o
forEach pelo collect:
Obviamente, pode-se demorar um tempo para dominar todos os métodos presentes nos
streams, mas observe como realizamos várias operações poderosas de maneira eficiente sem a
necessidade de ifs ou loops, facilitando enormemente operações complexas.
Considerações finais
Associar objetos, por meio de agregação ou composição, é uma das principais formas de
elaborar boas abstrações. Por ser tão essencial ao paradigma orientado a objetos, uma biblioteca
de coleções fácil de usar, com diversas opções, implementações eficientes e operações poderosas
torna-se parte essencial de qualquer sistema.
Além de fornecer algoritmos e implementações com base no estado da arte da computação,
a biblioteca de coleções também é extensível – o que significa que você poderá encontrar outras
coleções, criadas com propósitos mais específicos. Exemplo disso é a classe CopyOnWriteArrayList,
disponível no pacote de concorrência do Java, que estende a biblioteca de coleções para facilitar a
implementação de processamento paralelo. Você poderia até mesmo criar suas próprias coleções
ou buscar por classes criadas por terceiros para outros propósitos específicos.
• CURSO Java Completo – Aula 131: Coleções pt 16 Queue e PriorityQueue, 2016. 1 vídeo
(8 min.). Publicado pelo canal DevDojo. Disponível em: https://www.youtube.com/
watch?v=Ehjgp084GtI. Acesso em: 8 out. 2019.
Além de listas, conjuntos e mapas, a biblioteca de coleções contém outro tipo: o das filas
(Queue). Elas são similares às demais coleções, por isso indicamos esse vídeo, também da
série Curso Java Completo, do canal DevDojo, para você aprender mais sobre elas.
A biblioteca de coleções 115
Atividades
1. Imagine um programa no qual você deve indicar quantas palavras diferentes um livro possui.
Será necessário imprimir a lista dessas palavras em ordem alfabética e sua quantidade. Qual
coleção você utilizaria e por quê?
3. É possível gerar uma versão imutável de qualquer coleção por meio dos métodos da classe
Collections, por exemplo:
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. M. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
BOOCH, G. et al. Object Oriented Analysis and Design with Applications. 3. ed. Boston: Addison-Wesley,
2006.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
GAMMA, E. et al. Padrões de projeto: soluções reutilizáveis de software orientado a objetos. Trad. de L. A.
Salgado. Porto Alegre: Bookman, 2007.
ORACLE. Lambda Expressions. The Java Tutorials, 2017. Disponível em: https://docs.oracle.com/javase/
tutorial/java/javaOO/lambdaexpressions.html. Acesso em: 8 out. 2019.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
URMA, R.-G. Processing Data with Java SE 8 Streams, Part 1. Oracle Technology Network, abr. 2014.
Disponível em: https://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html.
Acesso em: 8 out. 2019.
8
Tratamento de erros
2. Retornar um valor: alguns métodos fornecem determinado valor de retorno para indicar
se funcionaram ou não. Por exemplo, considere que um usuário está tentando remover
um objeto que não está na lista. Deste modo, o método remove, também da classe List,
retornaria false para indicar que o comando foi ignorado e a remoção não ocorreu.
3. Sinalizar o problema: algumas situações exigem que o objeto impeça a ação, mas
também sinalize qual problema ocorreu e, até mesmo, interrompa a execução do código.
Por exemplo, digamos que, em um sistema acadêmico, exista um método para matricular
um aluno. Às vezes, essa matrícula não poderá ser feita por uma série de motivos: o
aluno pode estar desligado, estar em outra turma no mesmo horário, o usuário pode
estar tentando matricular um aluno em uma turma já encerrada etc. Nesse caso, além de
impedir a ação, o objeto deveria deixar claro qual problema ocorreu.
Observe que os problemas nem sempre se referem a erros de programação ou erros de
lógica. Podem ser situações em que é simplesmente o momento errado de se usar um método. Por
isso, chamamos problemas relacionados a regras de uso das nossas classes de exceções (SIERRA;
BATES, 2010).
118 Programação orientada a objetos I
Nem sempre as linguagens forneceram um mecanismo para lidar com a terceira alternativa
(sinalizar o problema). Programadores eram obrigados a recorrer a códigos de erro, indicando qual
problema ocorreu no decorrer da função. Esses códigos de erro eram retornados na função e nada
mais eram do que constantes numéricas.
Por exemplo, vamos supor que possuíssemos um sistema de biblioteca, em que os
livros fossem lidos de um arquivo. Quem faria essa leitura seria a classe Estante. O código que
imaginaríamos para isso seria:
1 public static Estante carregar(String arquivo) {
2 BancoDeDados bd = new BancoDeDados();
3 Estante estante = new Estante();
4 bd.abrir(arquivo);
5 estante.livros = bd.lerArquivo();
6 bd.fechar();
7 return e;
8 }
Observe que a lista de livros, que antes era retornada pelo método, precisou ser passada
como um parâmetro de entrada e, por isso, a assinatura da função ficou poluída. Isso porque a
função lerArquivo não pôde mais criar a lista de livros, pois o valor de retorno foi "gasto" com
o código de erro, tornando menos claro o que a função retorna. Veja que o mesmo se repete na
própria função carregar da classe Estante. Além disso, tivemos de incluir um if para testar se a
função abrir funcionou, para não chamar a função lerArquivo em caso negativo.
Por fim, como temos de garantir que o banco de dados fechou, fomos obrigados a duplicar a
chamada a bd.fechar, que agora aparece nas linhas 6 e 11 do código. Se, ao invés de uma só função
lerArquivo, tivéssemos várias funções de leitura diferentes na classe BancoDeDados, teríamos vários
Veja o que ocorreu: os erros foram gerados na classe BancoDeDados. Essa classe é usada pela
classe Estante, mas quem irá exibi-los para o usuário é a classe TelaLivros, que tem o método
buttonListarClicked. Esta é uma situação comum: a classe geradora do erro pode estar muito
distante da classe que exibe e trata esse erro. Além disso, considere que em um código mais
complexo entre as duas telas, outros erros, de outras classes, poderiam ter sido gerados. Com a
evolução do sistema, novos códigos de erro poderiam surgir e talvez a mensagem pouquíssimo
específica de "Ocorreu um problema" ficasse cada vez mais frequente.
Além desses problemas, os códigos de erro apresentam outros inconvenientes (ORACLE, 2017):
• São pouco descritivos. Observe que há poucas informações sobre o que gerou o problema,
o que dificulta seu diagnóstico.
• Um programador pode simplesmente ignorar os códigos de erro, mesmo que por acidente,
gerando um código instável.
• É difícil filtrar grupos de código de erro. Por exemplo, poderíamos querer lidar com
todos os erros "de arquivo", independentemente de ser um acesso inválido ou de arquivo
não encontrado.
120 Programação orientada a objetos I
• Bibliotecas dispares poderiam ter códigos de erro iguais para problemas diferentes.
• Caso um erro ocorra em decorrência de outro, fica difícil rastrear a causa original.
• É difícil garantir a realização de uma ação obrigatória (como fecharArquivo).
O mecanismo de exceções surgiu para resolver de maneira bastante intuitiva todos esses
problemas. Vamos estudá-lo mais a fundo neste capítulo.
inválido de um vetor. Note que fazer isso é geralmente um erro de programação, que
sequer deveria ocorrer.
O diagrama a seguir descreve essa hierarquia e se as exceções são verificadas ou não:
Figura 1 – Hierarquia das exceções
Throwable
Verificada
Exception Error
Filhas de RuntimeException
Não verificadas
Por padrão, todos os Throwables podem conter uma mensagem, descrevendo o problema e,
opcionalmente, outro Throwable com a causa do problema. Essa mensagem será impressa caso o
erro não seja capturado. Nós já estudamos sua estrutura no Capítulo 3.
Além de exceções específicas em cada classe, o Java também fornece algumas
RuntimeExceptions de uso geral, que podemos utilizar em nossas próprias classes, sendo as mais
Vamos agora criar um código que saca e realiza depósitos aleatoriamente, o código sacará
valores cinco vezes maiores do que depositará:
1 public class Main {
2 public void run() {
3 Random random = new Random();
4 ContaCorrente cc = new ContaCorrente();
5
6 for (int i = 0; i < 10; i++) {
7 int valor = 250 - random.nextInt(500);
8 if (random.nextBoolean()) {
9 cc.depositar(valor);
10 } else {
11 cc.sacar(valor * 5);
12 }
13 }
14 }
15
16 public static void main(String[] args) {
17 new Main().run();
18 }
19 }
Tratamento de erros 123
Porém:
• Não deveria ser possível sacar caso não houvesse saldo.
• Não deveria ser possível depositar ou sacar valores negativos.
• Não deveria ser possível fazer nenhuma das operações com a conta encerrada,
independentemente do saldo.
Disparamos exceções utilizando a palavra throw, seguido do objeto de um objeto com a exceção.
Este comando, tal como o return, abandonará a função imediatamente (DEITEL; DEITEL, 2010).
Vamos incluir validações para esses casos, inicialmente utilizando a exceção
IllegalArgumentException, que indica que um parâmetro inválido foi informado ao método,
Observe que tomamos o cuidado de gerar uma mensagem bastante descritiva, indicando o que
o método tentou fazer e o motivo pelo erro ter ocorrido. Embora isso não seja obrigatório, é altamente
recomendável (BLOCH, 2019), pois facilita o diagnóstico e a correção de problemas no futuro.
Tente rodar o código novamente. Poderemos obter agora uma execução com erro:
3. Agora, usar uma classe em comum nem sempre é desejável. Às vezes, ela vai capturar
muito mais exceções do que gostaríamos. Uma alternativa a isso é utilizar o | para
capturar várias exceções ao mesmo tempo (ORACLE, 2017).
Vamos alterar nosso código para incluir tanto a segunda quanto a terceira alternativa:
1 public class Main {
2 public void run() {
3 Random random = new Random();
4 ContaCorrente cc = new ContaCorrente();
5
6 for (int i = 0; i < 10; i++) {
7 try {
8 int valor = 250 - random.nextInt(500);
9 if (random.nextBoolean()) {
10 cc.depositar(valor);
11 } else {
12 cc.sacar(valor * 5);
13 }
14 } catch (IllegalArgumentException | IllegalStateException e) {
15 System.out.println("Não realizado: " + e.getMessage());
16 }
17 }
18 }
19
20 public static void main(String[] args) {
21 try {
22 new Main().run();
23 } catch (Exception e) {
24 System.out.println("Um erro inesperado ocorreu:");
25 e.printStackTrace();
26 }
27 }
28 }
Atenção: nosso método run agora trata todas as exceções do tipo IllegalStateException e
IllegalArgumentException, já nosso método main ficou responsável por capturar qualquer outra
de saque quando o saldo da conta é insuficiente? Para isso, vamos criar uma exceção verificada
SaldoInsuficienteException. Para tanto, basta criar uma classe filha de Exception:
Perceba que, ao fazer isso, o IntelliJ notifica que há erro na linha do throw. Isso ocorre
porque, como essa exceção é verificada, somos obrigados a tratá-la, porém, não queremos isso,
128 Programação orientada a objetos I
mas, sim, disparar essa exceção adiante. Para isso, precisamos indicar explicitamente, na assinatura
do método sacar, que ele pode disparar a SaldoInsuficienteException. Basta alterar uma cláusula
throw e colocar, separado por vírgulas, as classes das exceções verificadas que o método dispara.
Agora, o erro se deslocou para o método run. Aqui, temos duas opções: incluir também o
throw ou capturar a exceção no catch. Vamos utilizar a segunda opção, mas utilizaremos a classe
pai Exception. Isso permitiria que o método run disparasse outras exceções verificadas diferentes,
à medida que o código evoluísse:
Uma dica é criar uma boa hierarquia de classes para suas exceções (BLOCH, 2019),
preferencialmente, criando uma superclasse para toda a sua aplicação. No caso dessa aplicação,
poderíamos criar a seguinte hierarquia:
Figura 2 – Exceções do sistema bancário
Exception
BancoException RunTimeException
ContaException BancoRunTimeException
SaldoInsuficienteException ContaRunTimeException
TransacaoInvalidaException ContaFechadaException
Para você, deixamos como exercício alterar o programa a fim de utilizar essas classes. Assim,
o método run poderia disparar uma exceção um pouco mais específica no lugar de Exception (como
a ContaCorrenteException ou a BancoException), mantendo a clareza da natureza de erro que ele
dispara, mas não se tornando tão específico a ponto de impedir a manutenção do código no futuro.
Tratamento de erros 129
8.3.3 Finally
Algumas vezes, precisamos executar algum código independentemente de uma exceção ocorrer
ou não. Também podemos incluir no bloco try um bloco chamado finally, que sempre executará. O
bloco finally pode ocorrer mesmo que o try não possua nenhum catch (DEITEL; DEITEL, 2010).
Vejamos o exemplo inicial da classe Estante, caso exceções e o método finally existissem:
Veja que esse código é muito próximo da nossa intenção original. Também note como o
bloco finally foi usado para realizar a tarefa de finalização de fechar o banco de dados antes que
o método terminasse.
Outra situação ainda mais comum é não querermos propagar uma exceção tão específica,
como a exceção de banco de dados BDException para os usuários da classe Estante. Nesse caso,
geramos uma exceção filha de EstanteException (por exemplo, FalhaCarregarLivrosException) e
a disparamos no catch, incluindo a BDException como causa:
1 public static Estante carregar(String arquivo) throws EstanteException {
2 try {
3 BancoDeDados bd = new BancoDeDados();
4 Estante estante = new Estante();
5 bd.abrir(arquivo);
6 estante.livros = bd.lerArquivo();
7 return estante;
8 } catch (BDException e) {
9 throw new FalhaCarregarLivrosException(e);
10 } finally {
11 bd.fechar();
12 }
13 }
Dessa maneira, no futuro, poderíamos alterar a exceção BDException por outra, sem impactar
o código de quem usa o método carregar. Um exemplo disso seria se decidíssemos alterar o sistema
para utilizar um arquivo em vez de um banco de dados e passássemos a ter como causa uma
FileException.
Considerações finais
No decorrer deste livro, aprendemos sobre o paradigma orientado a objetos. Vimos
que a principal chave do paradigma está em criar boas abstrações, ou seja, modelar versões
simplificadas de objetos do mundo real por meio de código. Vimos como agrupar os atributos e
ações desses objetos dentro das classes, como combinar classes por meio de agregação (inclusive
uma biblioteca de coleções que dá um suporte avançado a essa operação) e como agrupar classes
hierarquicamente com a herança, modelar contratos com interfaces ou agrupar objetos similares
utilizando o recurso de pacotes.
Todos esses recursos permitem-nos escrever um código modular, separado e robusto.
Finalizamos este capítulo mostrando um mecanismo que, combinado ao encapsulamento, permite
que as classes impeçam seu uso incorreto, tornando nosso código mais robusto.
Aprender a dominar corretamente todos esses recursos exigirá prática e tempo. Não há
outra maneira de aprender, senão programando, errando e corrigindo código. Dominar um novo
paradigma de programação não se trata apenas de aprender novos comandos, mas de pensar uma
maneira diferente de solucionar problemas.
Saiba que esse esforço vale a pena. Ao dominar esse paradigma, um novo leque de linguagens
e tecnologias se abrirá para você. O Java, visto neste livro, é apenas uma delas. Outros exemplos são
as linguagens C++, C#, Swift, Kotlin, PHP e Python, todas implementam o paradigma em maior e
menor grau e estão presentes na programação de aplicações tradicionais, websites, celulares e tablets.
Tratamento de erros 131
Usar a orientação a objetos torna a programação uma espécie de jogo avançado de lego, em
que você tem um número infinito de combinações, pode criar suas próprias peças e cujo resultado
é realmente útil. Quem sabe o novo app do momento não será programado por você? Sua jornada
apenas começou e, a partir de agora, tudo é realmente possível.
• ORACLE. Unchecked Exceptions – The Controversy. The Java Tutorials, 2017. Disponível
em: https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html. Acesso
em: 3 out. 2019.
Esse artigo, presente nos tutoriais oficiais da linguagem Java, também apresenta a visão
dos criadores sobre o assunto exceções verificadas. O texto também está em inglês, mas
você pode utilizar o tradutor do seu navegador para convertê-lo para o português.
• MAGALHÃES, E. O novo try no Java 7, por uma linguagem mais simples. Global Coders,
26 out. 2011. Disponível em: http://blog.globalcode.com.br/2011/10/o-novo-try-no-java-
7-por-uma-linguagem.html. Acesso em: 3 out. 2019.
Até o Java 7, uma das principais funções do finally era chamar o método close de
diversas classes que precisavam encerrar o uso de algum recurso. Por exemplo, a classe
FileInputStream, que lida com arquivos, imde que outra aplicação faça uso do arquivo
enquanto ele está sendo lido e exige que se chame o método close para que o arquivo
seja liberado após o uso. A partir do Java 8, um recurso mais inteligente para isso foi
criado, chamado de try with resources. Este tutorial, escrito por Eder Magalhães, no site
GlobalCoders, mostra em detalhes como você pode utilizar esse recurso.
Atividades
1. Nas situações a seguir, seria melhor disparar uma exceção verificada, não verificada ou um
erro? Justifique.
a) Uma função que converte texto em número, quando o valor informado não é um número.
b) A memória do seu programa é subitamente tornada inacessível pelo sistema operacional.
c) Em um método de conexão, para indicar que o endereço de internet que o usuário
informou não existe.
132 Programação orientada a objetos I
3. Altere a classe Aluno a seguir para disparar exceções quando necessário. Considere se tratar
de uma classe para uma escola de ensino fundamental.
Referências
BLOCH, J. Java Efetivo: as melhores práticas para a plataforma Java. Trad. de C. R. Ravaglia. 3. ed. Rio de
Janeiro: Alta Books, 2019.
BOOCH, G. et al. Object Oriented Analysis and Design with Applications. 3. ed. Boston: Addison-Wesley,
2006.
DEITEL, H. M.; DEITEL, P. Java: como programar. 8. ed. Trad. de E. Furmankiewicz. São Paulo: Pearson,
2010.
ORACLE. Lesson: Exceptions. The Java Tutorials, 2017. Disponível em: https://docs.oracle.com/javase/
tutorial/essential/exceptions/index.html. Acesso em: 22 set. 2019.
SIERRA, K.; BATES, B. Use a cabeça! Java. Trad. de A. J. Coelho. Rio de Janeiro: Alta Books, 2010.
Gabarito
1 Olá, Java!
1. As linguagens híbridas têm as facilidades de linguagens interpretadas, mas sem incorrer
em todo impacto de performance de um interpretador. Isso é interessante porque a
existência de uma virtual machine permite que o mesmo bytecode rode em diferentes
plataformas sem a necessidade de recompilação. As desvantagens são que o usuário final
deverá instalar a VM e o código fonte pode ser facilmente revertido.
2. Como a virtual machine está na máquina que executará o código, ela pode procurar por
otimizações específicas daquela plataforma. Além disso, a JVM utiliza uma estratégia
conhecida como compilação just-in-time, em que ela analisa os trechos do código mais
usados e os compila durante a execução, evitando que o processo de interpretação
ocorra sempre. O impacto de performance do Java é difícil de medir, porque nunca se
sabe quais serão as otimizações ou quando ocorrerá a compilação, o que gera incerteza
na performance do código final.
2 Conhecendo a linguagem
1. Resolução com while:
1 var numero = 2;
2 while (numero <= 20) {
3 System.out.println(numero);
4 numero += 2;
5 }
3. a: String, b: long, c: double, d: float. Cuidado com a letra a. As aspas definem variáveis de
texto (String), não importando se tem uma ou mais letras. Para declarar um char, deveríamos
ter usado aspas simples:
var a = 'a';
3 Classes e objetos
1. Como o nome e número do lote são obrigatórios, vamos reforçar isso removendo o construtor
padrão e acrescentando o construtor com esses dois valores.
2. Devemos lembrar que atributos estáticos pertencem à classe, e não a objetos. Podemos
armazenar o último id gerado em um atributo estático e, então, somar 1 em seu valor a cada
novo objeto construído.
3. Uma variável nula não contém valor. Isso quer dizer que uma variável do tipo String não
possuirá texto, o que é diferente de um texto vazio, que é um valor especial. O código a
seguir exemplifica isso:
1 String v1 = "";
2 String v2 = null;
3
4 //Ok: O texto dessa variável tem 0 caracteres
5 System.out.println(v1.length());
6
7 //Erro: Sem nenhum texto associado, não podemos perguntar o tamanho.
8 System.out.println(v2.length());
4. É possível resolver esse impasse utilizando um método estático, que chame o construtor do
objeto. Chamamos esse padrão de projeto de método fábrica e é considerado, inclusive, uma
boa prática (BLOCH, 2019, p. 5). Veja:
4 Compondo objetos
1. É possível também violar o encapsulamento em setters e construtores. Basta que associemos
o objeto vindo externamente a nossa classe. Por exemplo, considere o construtor:
Por isso, é importante tomarmos as mesmas precauções que teríamos ao retornar objetos.
Neste caso, por exemplo, poderíamos fazer uma cópia do array.
quantidade de alunos da turma (e, em um programa grande, todas as classes que utilizassem
esse atributo). No segundo caso, poderíamos alterar o nome da variável sem alterar o nome
do seu get e set, assim a mudança ficaria restrita só à classe Turma.
Matrícula: 1234
Nome: Alice Matrícula: 5555
Nome: Bruno
aluno 1
aluno 3 aluno 2
Gabarito 137
Observe que as variáveis de referência aluno1 e aluno3 apontam para o mesmo objeto,
portanto, se fizéssemos aluno1.setNome("Carla"), imprimiríamos o nome Carla ao imprimir
o conteúdo de aluno2.getNome(), e não Alice.
Deixando essa suposição de lado, após executar a linha 4, a situação passa a ser:
Matrícula: 1234
Nome: Alice Matrícula: 5555
Nome: Bruno
aluno 1
aluno 3 aluno 2
Ou seja, alteramos o valor do local para onde aluno1 aponta, mas não o seu conteúdo. Por
isso, ao imprimir o nome de aluno3, ainda imprimiremos Alice, e não Bruno.
1 package astronomia;
2
3 public class Planeta {
4 private final static double PI = 3.1415;
5
6 //Atributos
7 private String nome;
8 private int diametro;
9 private double massa;
10
11 //Construtores
12 public Planeta(String nome, int diametro, double massa) {
13 this.nome = nome;
14 this.diametro = diametro;
15 this.massa = massa;
16 }
17
18 public Planeta() {
19 this("", 0, 0.0);
20 }
21
22 public static String descricao() {
23 return "Um corpo celeste esférico que orbita uma estrela";
24 }
25
26 //Getters e setters
27 public void setNome(String nome) {
28 this.nome = nome;
29 }
30
(Continua)
138 Programação orientada a objetos I
5 Hierarquias de classes
1. Analisaremos primeiro as afirmações a seguir:
Se é ok que C v1 = new D(); então D é filho de C
Se é ok que A v2 = v1; então C (tipo de v1) é filho de A
Se é ok que A v3 = new B(); então B é filho de A
Se é ok que A v4 = new E(); então E é filho de A
Se é ok que C v5 = (C) v4; então E (conteúdo de v4) é filho de C
Se dá erro em C v6 = (C)v3; então B não é filho de C
B C
a) Um carro é um veículo, assim como uma moto é um veículo. Isso indica relação de
herança (extends).
b) Um veículo tem um motor, portanto indica composição.
c) O motor pode ser cadastrado em outros pontos do sistema, isso indica que ele deve existir
como uma classe própria, e a relação dele com o veículo é de agregação.
Classe do motor:
1 package carros;
2
3 public class Motor {
4 private double potencia;
5 private int tipoCombustivel;
6
7 private int valvulas;
}
Classe do veículo:
1 package carros;
2
3 public class Veiculo {
4 private String placa;
5 private String chassi;
6
7 private Motor motor; //Composição, "tem um"
8 }
140 Programação orientada a objetos I
Classe do carro:
1 package carros;
2
3 public class Carro extends Veiculo {
4 private int portas;
5 }
Classe da moto
1 package carros;
2
3 public class Moto extends Veiculo {
4 private int cilindradas;
5 }
Para utilizá-lo, bastaria criar uma classe filha de operação. Por exemplo, caso o programador
quisesse multiplicar os números por 10, faríamos:
1 public class Vezes10 implements Operacao {
2 @Override
3 public int aplicar(int valor) {
4 return valor * 10;
5 }
6 }
Dica:
Também seria possível utilizar uma classe anônima, como apresentado no vídeo indicado na
seção Ampliando seus conhecimentos deste capítulo:
1 processar(valores, new Operacao() {
2 @Override
3 public int aplicar(int valor) {
4 return valor * 10;
5 }
6 });
6 Generics e lambda
1.
public <R> Lista<R> converter(Function<T, R> operacao) {
Lista<R> resultado = new Lista<R>(elementos.length);
for (T elemento : elementos) {
resultado.adicionar(operacao.apply(elemento));
}
return resultado;
}
2.
var sistema = new Lista<Planeta>(3);
sistema.adicionar(new Planeta("Mercurio", 4_878, 0.055));
sistema.adicionar(new Planeta("Terra", 12_742, 1.0));
sistema.adicionar(new Planeta("Saturno", 120_536, 95.2));
Imprime:
Planeta Mercurio
Planeta Terra
Planeta Saturno
3.
Seria possível ampliar a classe utilizando um critério que aceitasse também objetos da classe
pai de Planeta. O método deveria ser reescrito como:
public Lista<T> coletar(Predicate<? super T> criterio) {
142 Programação orientada a objetos I
7 A biblioteca de coleções
1. O ideal seria utilizar um TreeSet, visto que ele descartaria automaticamente as palavras
duplicadas ao mesmo tempo em que o resultado estaria ordenado. O tamanho do set
representaria quantas palavras há no livro.
2. O Map deve ser utilizado quando o valor numérico não é contínuo – se fosse um número de
matrícula, por exemplo. Tanto o List quanto o vetor de objetos Object[] exigem que os índices
sejam contínuos. De maneira geral, o List será sempre preferível ao vetor primitivo. Uma
exceção a essa regra será quando o programa possuir restrições extremas de performance,
como aplicações tipicamente encontradas em desafios de programação.
3. Isso seria interessante para a classe Turma, que utiliza em seu interior uma coleção para seus
alunos. Para incluir um aluno na turma, você pode criar um método matricular, que faz uma
série de validações, e também outros métodos para remover. Se você quiser dar aos usuários
de sua classe uma forma conveniente de iterar sobre a lista de alunos ou mesmo utilizar
sobre elas os streams, poderia parecer uma boa ideia implementar um método getAlunos(),
que retorna diretamente a lista de alunos. Lembre-se de que a lista é um objeto e, por usar
referências, permitirá que os métodos add e remove dessa lista sejam chamados diretamente,
ignorando suas validações, como no exemplo a seguir:
turma.getAlunos().remove(3);
return Collections.unmodifiableList(alunos);
Com a lista retornada dessa forma, uma chamada ao remove dispararia um erro, embora
iterações e streams de consulta ainda se mantenham possíveis.
8 Tratamento de erros
1.
a) Exceção não verificada: essa situação está mais relacionada a um erro de programação.
Isso ocorre no Java: a função Integer.parseInt, e similares, que converte um texto em
um número dispara a NumberFormatException não verificada.
b) Erro: trata-se de algo que a aplicação nunca poderá recuperar. Provavelmente, será um
erro disparado pela máquina virtual.
c) Exceção verificada: sempre que se tenta uma conexão a um site de internet, o endereço pode
não existir. É uma ação que todo e qualquer programador deverá levar em consideração.
Gabarito 143
2.
Classes de exceção:
1 public class BancoException extends Exception {
2 public BancoException(String message) {
3 super(message);
4 }
5 }
27 }
28
29 saldo -= valor;
30 }
31
33 return saldo;
34 }
35
36 public void fechar() {
37 this.aberta = false;
38 }
39 }
Gabarito 145
13 this.matricula = matricula;
14 this.nome = nome;
15 this.idade = idade;
16 }
VINÍCIUS GODOY