Você está na página 1de 150

Programação Orientada a Objetos I

A orientação a objetos representou uma revolução na


forma de construir softwares. Desde que
se popularizou, em torno dos anos 1990, se
tornou parte das principais linguagens de
programação utilizadas no mercado, como
Java, C++ e C#. Ela permitiu a divisão
de programas em partes menores e
reutilizáveis e a construção de
sistemas cada vez mais complexos.
Não é à toa que hoje é utilizada
em programas não só em computadores e
laptops, mas também em celulares, tablets e
até mesmo em carros e geladeiras.

VINÍCIUS GODOY

Código Logístico Fundação Biblioteca Nacional


ISBN 978-85-387-6531-8

58878 9 788538 765318


Programação orientada
a objetos I

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.

CIP-BRASIL. CATALOGAÇÃO NA PUBLICAÇÃO


SINDICATO NACIONAL DOS EDITORES DE LIVROS, RJ
M497p
Godoy, Vinícius
Programação orientada a objetos I / Vinícius Godoy. - 1. ed. - Curitiba [PR] :
IESDE Brasil, 2019.
146 p. : il.
Inclui bibliografia
ISBN 978-85-387-6531-8
1. Java (Linguagem de programação de computador). 2. Programação orientada a
objetos (Computação). I. Título.
CDD: 005.117
19-60075
CDU: 004.43

Todos os direitos reservados.

IESDE BRASIL S/A.


Al. Dr. Carlos de Carvalho, 1.482. CEP: 80730-200
Batel – Curitiba – PR
0800 708 88 88 – www.iesde.com.br
Vinícius Godoy
Mestre em Visão Computacional pela Pontifícia Universidade Católica do Paraná (PUCPR),
especialista em Desenvolvimento de Jogos de Computadores pela Universidade Positivo (UP)
e graduado em Tecnologia em Informática pela Universidade Tecnológica Federal do Paraná
(UTFPR). Trabalha na área de informática desde 1997, tendo participado de grandes projetos,
como a edição de 100 anos do Dicionário Aurélio Eletrônico. Também atua como moderador do
GUJ, o maior fórum de tecnologia Java do Brasil.
Sumário

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.

Hoje, a orientação a objetos se tornou o padrão da indústria e aquela plataforma de


programação sobreviveu em meio a vários concorrentes. Isso mostrou o quão acertada estava a
visão de seus criadores, que miraram em necessidades do futuro, prevendo a evolução tecnológica
do hardware e da rede mundial de computadores. A cada ano, a relevância da plataforma cresceu:
conquistou os servidores de internet e os dispositivos móveis Android e passou a ser parte da ementa
de praticamente todas os cursos de informática. Sua máquina virtual tornou-se extremamente veloz
e moderna, um conjunto de classes e recursos bastante robustos foi apresentado e uma comunidade
se organizou para propor melhorias na plataforma e mantê-la elegante e fiel a seus princípios.

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.

Pronto?! Hora de embarcar nessa jornada...


1
Olá, Java!

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?

1.1 Breve histórico


Em dezembro de 1990, um time de 13 engenheiros da Sun Microsystems, liderado por James
Gosling, Mike Sheridan e Patrick Naughton, iniciou o desenvolvimento do Projeto Green, com o
objetivo de desenvolver uma plataforma que representaria a próxima onda tecnológica do mundo.
Na visão deles, pessoas poderiam controlar diversos dispositivos, como TVs e telefones, de maneira
integrada (GOSLING, 1998).
O time focou-se no desenvolvimento de um PDA, uma espécie de tablet antigo, sem fio, capaz de
executar animações e controlar vários dispositivos. Esse produto foi chamado de *7 (Star 7) e, embora
não tenha chegado ao mercado, levou ao desenvolvimento de diversas tecnologias interessantes.
Uma delas foi a linguagem Oak, capaz de executar em diversos dispositivos, que
posteriormente se tornaria a linguagem Java. Além da linguagem, uma grande biblioteca de
software permitia que seus desenvolvedores fizessem animações, comunicassem via rede e
realizassem várias operações no dispositivo de maneira mais fácil.
Quando o projeto começou a crescer, os projetistas logo perceberam o potencial da plataforma
para a internet. O time trabalhou em um clone do navegador Mosaic, chamado WebRunner, que
demonstrava o poder da tecnologia Java. Nele, era possível combinar e executar aplicações Java de
maneira segura, em meio a páginas HTML e CSS. A tecnologia foi demonstrada na Conferência
de Tecnologia, Design e Entretenimento, no início de 1995, em Monterey, capturando a atenção da
audiência ao fazer o desenho de uma molécula em 3D se mover controlada pelo mouse (BYOUS,
2004). Tal tipo de interação, comum hoje em dia, não era possível na internet daquela época.
Em junho de 1996, a primeira versão da plataforma Java foi oficialmente disponibilizada para
o público, com a promessa de "escrever uma vez e rodar em todo lugar" (SUN MICROSYSTEMS,
1996). Em 1997, a Sun tentou formalizar o Java, mas desistiu do processo, entretanto, isso fez com
que ela criasse o Java Community Process (JCP), permitindo que modificações fossem propostas
de forma pública e transparente.
De novembro de 2006 até maio de 2007, a Sun tornou pública boa parte do código da
Máquina Virtual Java, por meio da licença GPL. Nos anos de 2009 e 2010, a Oracle comprou a Sun
Microsystems e passou a ser dona da tecnologia.
10 Programação orientada a objetos I

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.

1.2 Arquitetura da plataforma


Quando falamos em Java, é importante entender que não estamos nos referindo única e
simplesmente a uma linguagem de programação. O Java é uma plataforma que inclui:
• Bibliotecas de classes: que permitem que você trabalhe facilmente com arquivos, interface
gráfica, redes, entre outras funcionalidades.
• Ferramentas e utilidades: que envolvem ampla documentação, analisadores de
performance e um empacotador de arquivos.
• Linguagem Java: que cobriremos intensamente nos demais capítulos do livro.
• Máquina Virtual Java: que explicaremos a seguir.

Todos esses elementos fornecem um ambiente poderoso, que permitirá que façamos
aplicações comerciais robustas.

1.2.1 A Máquina Virtual Java (JVM)


Para que um programa de computador funcione, é necessário converter comandos em
forma de textos como este:
print "Olá mundo!"

Em um código de máquina, muito específico e pouco legível, formado de milhares de


instruções como estas:
AE 1F 91 AA 85 FF C4 1B 32 ...

Há duas formas de realizar essa tarefa (MENDONÇA, 2018):


• Por meio de um compilador, que traduz o programa inteiro, gerando um arquivo
executável. Como esse programa já está na linguagem que o computador entende, sua
execução é muito rápida.

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: Mendonça, 2018, p. 110.


12 Programação orientada a objetos I

A desvantagem dessa abordagem é que o usuário final do programa precisa instalar a


máquina virtual mesmo sem entender exatamente o que ela é. Outra desvantagem é que geralmente
bytecodes são facilmente reversíveis em seu código-fonte original.
Finalmente, embora sejam consideravelmente mais rápidas do que as linguagens puramente
interpretadas, esse processo tem impacto na performance que pode ser difícil de medir. Isso
dificulta a criação de aplicações de tempo real, tais como reprodutores de vídeo ou jogos.

1.2.2 Como a plataforma é distribuída


A plataforma Java é distribuída em dois pacotes:
• Java Runtime Environment (JRE): é o pacote de execução. Contém apenas a JVM e o
código compilado das bibliotecas padrão. Qualquer pessoa interessada em executar um
programa Java deverá ter a JRE instalada em sua máquina.
• Java Development Kit (JDK): é o pacote de desenvolvimento. Contém o JRE em conjunto
com o compilador Java, códigos-fonte, documentação e ferramentas. Deve ser instalado
pelo desenvolvedor para criar projetos.
Você pode estar se perguntando: o que é o JEE? Não seria também um pacote, só que
mais poderoso? JEE é a sigla de Java Enterprise Edition. Embora soe como uma versão "mais
completa" do Java, não é disso que se trata. Ele é uma especificação sobre como diversos serviços
adequados a aplicações de rede empresariais (como persistência, mensageria etc.) devem
funcionar (ORACLE, 2019a).
A implementação desses serviços é feita por mais de 20 empresas terceiras (ORACLE, 2019b).
Por exemplo, a JEE descreve a JPA (Java Persistence API), que descreve classes para uso de banco
de dados. Tanto o Hibernate (Red Hat) quanto o Toplink (Oracle) são projetos que implementam
essa especificação.
Graças ao JEE, é possível montar um servidor web com diversas tecnologias, de diversos
fabricantes, comunicando entre si. Porém, o estudo do JEE está fora do escopo da nossa disciplina.

1.3 Instalando o ambiente


Antes de começarmos a desenvolver, precisaremos instalar todo o ambiente de
desenvolvimento. Isto é, precisamos instalar o JDK, contendo compilador, Máquina Virtual Java,
documentações e um editor de códigos, também chamado de ambiente integrado de desenvolvimento
(IDE). Com esse ambiente em mãos, você poderá executar e testar os exemplos deste livro e resolver
os exercícios propostos ao final de cada capítulo.
Olá, Java! 13

1.3.1 Instalação do JDK


Antes de começarmos a desenvolver, precisamos instalar o JDK. Para isso, entre no site
Oracle (disponível em: https://www.oracle.com/technetwork/java/index.html). Você também
pode usar o antigo site da Sun, que é muito mais fácil de decorar (disponível em: java.sun.com).
Nele, clique em Java SE.
Figura 2 – Tela inicial do site Oracle

Fonte: Oracle.

Em seguida, clique no ícone do JDK.


Figura 3 – Página Java SE Downloads

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.

Siga o passo a passo do instalador e aguarde a conclusão do processo de instalação.

1.3.2 Instalando um IDE


Nós podemos escrever todo o nosso código em editores de textos e executar a compilação
manualmente, porém, esse é um procedimento tedioso, comparável a tentar escrever um livro
usando somente o bloco de notas.
Ao longo dos anos, programadores criaram ambientes que nos auxiliam na codificação,
chamados de Integrated Development Environment (IDEs). Esses ambientes fornecem ferramentas
para análise do código, colorização de comandos, recurso de autocompletar inteligente, execução
automática do código etc.
No caso do Java, existem três IDEs poderosos e gratuitos:
• Netbeans IDE, da Oracle;
• Eclipse, da Eclipse Foundation;
• IntelliJ IDEA, da Jetbrains.

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

Figura 5 – Página do IntelliJ IDEA

Fonte: JetBrains.

Dentre as opções, selecione a versão Community, que é gratuita:


Figura 6 – Escolha da versão

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

1.4 O primeiro programa Java


Vamos agora tentar criar e executar nosso primeiro programa em Java. Para isso, precisaremos
abrir o IntelliJ e compreender a estrutura de um código bastante simples.
Na primeira vez que você abrir o IntelliJ, ele realizará algumas perguntas para a configuração
básica. A primeira pergunta refere-se à importação das suas configurações. Como isso é possível
somente se você já tiver instalado uma versão antiga do IntelliJ, simplesmente escolha a opção
Do not import settings e siga em frente.
Figura 7 – Importação de configurações antigas

Fonte: IntelliJ IDEA.

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

Fonte: IntelliJ IDEA.


Olá, Java! 17

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

Fonte: IntelliJ IDEA.

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

Fonte: IntelliJ IDEA.


18 Programação orientada a objetos I

1.4.1 Criando o projeto


É hora de testar se tudo foi instalado corretamente escrevendo nosso primeiro programa
Java. Para isso, abra o IntelliJ IDEA e clique em Create New Project.
Figura 11 – Criação de novo projeto

Fonte: IntelliJ IDEA.

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

Fonte: IntelliJ IDEA.

Na tela seguinte, desmarque a opção Create Project From Template, se estiver marcada, e
clique em Next.
Olá, Java! 19

Figura 13 – Criação de projeto em branco

Fonte: IntelliJ IDEA.

Nomeie o projeto como Aula1 e clique em Finish. O IntelliJ provavelmente perguntará se


pode criar a pasta do projeto, clique em OK.
Figura 14 – Nome e caminho do projeto

Fonte: IntelliJ IDEA.

Uma tela como esta deverá se abrir:


Figura 15 – Seu primeiro projeto

Fonte: IntelliJ IDEA.


20 Programação orientada a objetos I

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.

1.4.2 Escrevendo o código


Clique com o botão direito sobre a pasta src, clique em New e depois em Java Class. Dê o
nome de Aula1 e clique em OK.
Figura 16 – New Java Class

Fonte: IntelliJ IDEA.

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:

public class Aula1 {

Altere-o para:

Figura 17 – O primeiro programa

Fonte: elaborada pelo autor.

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

Figura 18 – Execução do primeiro programa

Fonte: IntelliJ IDEA.

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:

1 public class Aula1 {


2 public static void main(String[] args) {
3 System.out.println("Olá mundo!"); //Imprime Olá mundo
4 }
5 }

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:

1 public class Aula1


2 {
3 public static void main(String[] args)
4 {
5 System.out.println("Olá mundo!"); //Imprime Olá mundo
6 }
7 }

Isso é perfeitamente permitido em Java, porém, embora sintaticamente correto, fere a


convenção de código oficial (SUN MICROSYSTEMS, 1997), por isso, daremos preferência para a
primeira forma.

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.

Ampliando seus conhecimentos


• INTELLIJ IDEA. Default keymap. 2019. Disponível em: https://resources.jetbrains.
com/storage/products/intellij-idea/docs/IntelliJIDEA_ReferenceCard.pdf. Acesso em:
4 ago. 2019.
Você pode conhecer um pouco mais sobre o IDE IntelliJ, com o qual iremos trabalhar. É
bastante útil conhecer configurações e teclas de atalho. Por isso, consulte o cheat sheet,
que contém um resumo das principais funções. Tenha esse documento em mãos, pois,
com o tempo, esse uso será cada vez mais natural.
Olá, Java! 23

• MEYER, Maximiliano. Os melhores salários por linguagem de programação, 2018. Oficina


da Net, 1 mar. 2018. Disponível em: https://www.oficinadanet.com.br/post/14518-qual-a-
linguagem-de-programacao-e-mais-bem-remunerada. Acesso em: 17 set. 2019.
Esse artigo dá um panorama do mercado de trabalho em 2018, envolvendo a linguagem
Java e outras linguagens de programação. Observe que, embora o Java não tenha a
melhor média salarial, apresenta o maior número de vagas disponibilizadas. Além disso,
cabe lembrar que ele também é a linguagem do Android. Por fim, outras linguagens
citadas no artigo, como o C# da plataforma .Net, também são orientadas a objetos. Por
isso, ao aprender os conceitos deste livro, será fácil migrar para essas linguagens, caso
você precise.

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.

3. Quais são os cinco pilares de projeto da linguagem Java?

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.

GOETZ, B. Java concorrente na prática. Rio de Janeiro: Alta Books, 2018.

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.

MENDONÇA, V. G. Introdução à computação. Curitiba: IESDE Brasil, 2018.

ORACLE. The Java Language Environment. 1997. Disponível em: https://www.oracle.com/technetwork/java/


intro-141325.html. Acesso em: 4 ago. 2019.

ORACLE. Java EE Compatibility. 2019a. Disponível em: https://www.oracle.com/technetwork/java/javaee/


overview/compatibility-jsp-136984.html. Acesso em: 4 jul. 2019.

ORACLE. Java EE at a Glance. 2019b. Disponível em: https://www.oracle.com/technetwork/java/javaee/


overview/index.html. 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

Neste capítulo, iremos explorar rapidamente a sintaxe básica da linguagem Java.


Entenderemos como as declarações de variável funcionam e veremos as principais estruturas
de controle para tomada de decisão e repetição. Ainda veremos a linguagem do ponto de vista
estruturado, sem focar na parte orientada a objetos – essa segunda parte será tema do próximo
capítulo. Apesar de ser um capítulo longo, você não terá dificuldades para acompanhar o assunto
se já conhecer outras linguagens de programação.
Considere que todos os exemplos que veremos adiante estão contidos no interior do bloco
de código da função principal. Por exemplo, se mostrarmos o código:
1 int x = 10;
2 System.out.printf("O valor de x é: %d%n", x);

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:

public class Aula2 {


public static void main(String[] args) {
var x = 10;
System.out.printf("O valor de x é: %d%n", x);
}
}

Embora esse código extra possa parecer excessivo, você logo verá que ele ocorre de maneira
natural em programas maiores.

2.1 Variáveis e tipos de dados


Java é uma linguagem fortemente tipada. Isso quer dizer que as variáveis estarão associadas
a um único tipo de dado durante toda a sua existência. Esse tipo de dado indica a informação que a
variável armazenará e quais operações poderemos realizar sobre ela. Os principais tipos de dados,
de acordo com Oracle (2017a), são:
• Tipos primitivos:
• Booleano (verdadeiro/falso): boolean.
• Inteiros: byte, short, int e long.
• Numéricos decimais: float e double.
• Texto: char.
26 Programação orientada a objetos I

• Tipos não primitivos:


• String.
• Enumerações.
• Arrays.
• Referências para objetos: declaradas sempre que usamos uma classe.

Há duas formas de declarar variáveis, a primeira é com a tipagem explícita, em que


indicamos o tipo diretamente nas formas:

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:

int paginas = 100;

Também é possível declarar mais de uma variável de mesmo tipo em uma única linha,
separando-as por vírgula. Por exemplo:

int x = 10, y = 20;

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:

var paginas = 100;


Conhecendo a linguagem 27

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.

final var paginas = 100;

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.

2.1.1 O tipo de dado booleano


O tipo de dado booleano, ou lógico, admite apenas dois valores: verdadeiro (true) ou falso
(false). O valor padrão para variáveis desse tipo é false.
Podemos comparar se duas variáveis booleanas são iguais, por meio dos operadores
relacionais ==, e se são diferentes, utilizando o operador ! =. 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

Importante: não confunda a atribuição (=) com a igualdade (==).

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

1 boolean v1 = true, v2 = true;


2 boolean f1 = false, f2 = false;
3
4 System.out.println("Comparação com E (&&)");
5 System.out.println(v1 && v2); //Imprime true, ambos verdadeiros
6 System.out.println(v1 && f2); //Imprime false, f2 é falso
7 System.out.println(f1 && v2); //Imprime false, f1 é falso
8 System.out.println(f1 && f2); //Imprime false, ambos falsos
9
10 System.out.println("Comparação com OU (&&)");
11 System.out.println(v1 || v2); //Imprime true, ambos verdadeiros
12 System.out.println(v1 || f2); //Imprime true, v1 é verdadeiro
13 System.out.println(f1 || v2); //Imprime true, v2 é verdadeiro
14 System.out.println(f1 || f2); //Imprime false, ambos falsos
15
16 System.out.println("Negação com !");
17 System.out.println(!v1); //Imprime false, pois v1 é true
18 System.out.println(!f1); //Imprime true, pois f1 é false

Obviamente, você poderia combinar operadores para realizar operações complexas, como:

var complexo = v1 && !(v2 || f2);

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

Portanto, o valor da variável complexa será false.

2.1.2 Tipos numéricos


Os tipos numéricos são divididos em números inteiros, sem casa decimal, e números de
ponto flutuante. A Tabela 1 descreve os diferentes tipos inteiros, os valores máximos e mínimos
que podem ser representados e quantos bytes ocupam na memória.
Tabela 1 – Tipos inteiros

Tipo Máximo / Mínimo Bits


byte –128 até 127 8

short –32.768 até 32.767 16

int –2.147.483.648 até 2.147.483.647 32

long –9.223.372.036.854.775.808 até 9.223.372.036.854.775.807 64


Fonte: ORACLE, 2017b.

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:

var longNum = 12L; //Essa variável é do tipo long


var intNum = 1_250; //intNum vale 1250
var doze = 0xB; //B em hexadecimal equivale a 12 em decimal
var nove = 011; //11 em octal equivale a 9 em decimal
var floatNum = .5f; //O tipo é float e o valor 0.5
var doubleNum = 1_2___5.; //O tipo é double e o valor é 125.0

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.

1 var numero = -12.5;


2 var outroNumero = -numero;

É 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

Tabela 2 – Operadores para manipulação de bits

Operação Símbolo Exemplo Resultado


Right-Shift << x << 1 0101_0110

Left-Shift >> x >> 2 0000_1010

E & x & 1111_0000 0011_0000

OU | x | 1111_0000 1111_1011

OU Exclusivo ^ x ^ 1111_0000 0001_1011


Fonte: Elaborada pelo autor.

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.

2.1.3 O tipo char


O tipo char representa um caractere de texto. No fundo, cada caractere é representado por
um valor numérico que varia de 0 até 65.535; portanto, ocupa 2 bytes na memória. Por causa dessa
conversão, é possível utilizar qualquer operador numérico em variáveis do tipo char.
Definimos um char, de maneira literal, por meio de aspas simples.

var letra = 'x';

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:

var nome = "Programação"

Variáveis de texto suportam a operação de concatenação por meio dos operadores + e


+=. A concatenação une duas Strings. Por exemplo, a linha 3 deste código imprime o texto
"Programação OO em Java":

1 var titulo = "Programação OO";


2 var subtitulo = "em java";
3 System.out.println(titulo + subtitulo);

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.

Para o caso do exemplo, iniciaríamos criando o arquivo Naipes.java e colocando nele o


seguinte conteúdo:

1 public enum Naipes {


2 Paus, Ouros, Copas, Espadas
3 }

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:

1 var valor = Naipes.Paus;


2 System.out.println(valor); //Imprime Paus

As enumerações contêm várias operações interessantes. É possível chamar Naipes.values()


para obtermos um vetor com todos os valores de Naipes possíveis dentro, na ordem em que foram
declarados. Além disso, podemos obter um naipe por meio do texto, com o comando valueOf().
Mas atenção: o Java disparará um erro caso o valor não exista. Se quisermos obter o valor do enum
em forma de texto, podemos usar a função name(). Por fim, podemos saber qual a ordem do naipe,
iniciada em 0, utilizando a função ordinal(). Veja no exemplo a seguir.

1 var valor = "Ouros";


2 Naipes naipe = Naipes.valueOf(valor);
3 System.out.println(valor.ordinal()); //Imprime 1
4 System.out.println(valor.name()); //Imprime Ouros

As enumerações são muito mais poderosas do que isso, exploraremos vários outros recursos
em capítulos futuros.

2.1.6 Vetores e matrizes


Muitas vezes, precisamos trabalhar com listas de valores. Para isso, o Java permite definir
estruturas conhecidas como vetores (arrays). Elas associam um conjunto de valores a um índice
iniciado em 0. Podemos declarar um array utilizando o operador de []. Veja alguns exemplos:
Conhecendo a linguagem 33

1 int[] x = new int[] {10,2,30,-5};


2 var y = new double[10];
3 char[][] z;
4
5 System.out.println(x[0]); //Imprime 10
6 System.out.println(x[2]); //Imprime 30

Observe, nas linhas 5 e 6, como os índices são usados.

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.

1 var xadrez = new char[8][8];


2 var irregular = new int[][] {
3 {1},
4 {1}, {2},
5 {},
6 {1}, {2}, {3}, {4}, {5}
7 };
8
9 var irregular2 = new int[4][];
10 irregular2[0] = new int[1];
11 irregular2[1] = new int[2];
12 irregular2[2] = new int[0];
13 irregular2[3] = new int[5];

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 Controle de fluxo: estruturas de decisão


Muitas vezes, precisamos executar ou não um trecho de código de acordo com uma condição.
O Java apresenta duas estruturas de decisão importantes, o if e o switch. Além disso, ele também
fornece um operador com base em decisão: o operador ternário. Vamos estudar essas estruturas.

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:

1 var idade = 10;


2
3 if (idade < 18) {
4 System.out.println("Menor de idade");
5 } else {
6 System.out.println("Maior de idade");
7 }

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

1 if (idade < 14) {


2 System.out.println("Criança");
3 } else if (idade < 18){
4 System.out.println("Adolescente");
5 } else {
6 System.out.println("Adulto");
7 }

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:

valor = condição ? valor caso verdadeiro : valor caso falso;

Vejamos um exemplo:

var hcg = 4.0;


var resultado = hcg >= 5.0 ? "Grávida" : "Não grávida";

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.

2.3 Controle de fluxo: estruturas de repetição


É frequente também a necessidade de repetir um trecho de código várias vezes. Essa operação
é conhecida como iteração, loop ou laço. Para isso, o Java fornece quatro comandos: while, do
while, for e for each. Que tal aprendermos sobre cada um deles?

2.3.1 Repetição com while e do while


Os comandos while e do while repetem um comando ou bloco de comandos enquanto uma
condição for verdadeira. Observe o código a seguir.

1 var x = 1;
2 while (x <= 3) {
3 System.out.print(x + ", ");
4 x++;
5 }
6 System.out.println("indiozinhos");

Ele tem como resultado o seguinte texto:

1, 2, 3, indiozinhos

O do while é similar, porém a condição será testada ao final da repetição, e o código


executará pelo menos uma vez. O código a seguir tem o mesmo resultado:
Conhecendo a linguagem 37

1 var x = 1;
2 do {
3 System.out.print(x + ", ");
4 x++;
5 } while (x <= 3);
6 System.out.println("indiozinhos");

Porém, com x = 4, o primeiro exemplo imprimiria "indiozinhos", enquanto o segundo


exemplo imprimiria "4, indiozinhos".

2.3.2 A instrução for


A instrução for é formada por três partes:
a. Inicialização: permite a criação e inicialização de variáveis.
b. Condição: mantém o for executando enquanto for verdadeira.
c. Operação: é executada a cada iteração do for.

Para deixar mais claro, vamos reescrever o exemplo dos indiozinhos utilizando o for:

1 for (var x = 1; x <= 3; x++) {


2 System.out.print(x + ",");
3 }
4 System.out.println("indiozinhos");

O resultado é o mesmo:

1, 2, 3, indiozinhos

O comando for é muito utilizado para imprimir os elementos de um array. Por exemplo:

1 var frutas = new String[] {"Laranja, Banana, Maçã"};


2 for (var i = 0; i < frutas.length; i++) {
3 var fruta = frutas[i];
4 System.out.println(fruta);
5 }

Esse programa imprime, em ordem:

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:

1 var frutas = new String[] {"Laranja, Banana, Maçã"};


2 for (var fruta : frutas) {
3 System.out.println(fruta);
4 }

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.

2.3.3 Interrompendo laços com break e continue


Muitas vezes, precisamos interromper um laço de um loop ou mesmo a iteração inteira,
dependendo de alguma condição. Para isso, o Java disponibiliza dois comandos:
• Continue: que interrompe imediatamente aquela iteração, fazendo com que a condição
do laço seja novamente testada.
• Break: que interrompe completamente o laço.

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:

1 var numeros = new int[] {10,2,23,11,14,17,6,13};


2 for (var numero : numeros) {
3 //Se o número é impar
4 if (numero % 2 == 1) {
5 continue; //Pule para o próximo número

6 }
7 System.out.println(numero);
8 }

Esse programa imprime:

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.

1 var matrizQuadrada = new int[][] {


2 {1,2,3,4,5},
3 {10,20,30,0,40},
4 {100, 200, 300, 400, 500}
5 };
6
7 externo:
8 for (int i = 0; i < matrizQuadrada.length; i++) {
9 System.out.println();
10 for (var numero : matrizQuadrada[i]) {
11 System.out.print(numero + " ");
12 if (numero == 0) {
13 break externo;
14 }
15 }
16 }

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.

2.4 Escopo de variáveis


Observe que todos os programas que vimos até aqui utilizaram as chaves para criar blocos
de código. É importante saber que as variáveis só existem a partir da linha em que forem declaradas
e no interior de blocos em que foram criadas (SIERRA; BATES, 2010).
No caso de um bloco estar dentro do outro, ele conseguirá utilizar variáveis do bloco em que
está contido. Por exemplo, no código a seguir:
40 Programação orientada a objetos I

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.

Ampliando seus conhecimentos


• ORACLE. Lesson: language basics. The Java Tutorials, 2019. Disponível em: https://docs.
oracle.com/javase/tutorial/java/nutsandbolts/index.html. Acesso em: 24 set. 2019.
Caso você domine o inglês, é sempre importante consultar a documentação oficial da
Oracle. Nela, você encontrará tutoriais e a descrição detalhada de todos os elementos da
linguagem.

• 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.

1 var letras = new char[]{'a','b','e','j','m','o','u','v','z',' '};


2 var idx = new int[]{2, 6, 9, 0, 4, 5, 9, 5, 9, 3, 0, 7, 0};
3 for (var i : idx) {
4 System.out.print(letras[i]);
5 }

3. Identifique os tipos declarados a seguir.

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.

3.1 Classes e objetos no mundo real


Desde criança, procuramos entender os objetos do mundo real. Quando os analisamos,
prestamos atenção em cada um dos seus atributos, como sua cor, forma, altura e peso.
Figura 1 – Cor, tamanho, forma são atributos dos objetos

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

Figura 2 – A operação de um instrumento é fazer som

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.

3.2 Sua primeira classe


Em Java, definimos uma nova classe por meio da palavra-chave class. Uma classe definirá um
novo tipo de dado. Além disso, cada classe em Java será geralmente definida em seu próprio arquivo.
Por exemplo, vamos supor que estejamos programando um sistema astronômico e gostaríamos
de descrever os planetas. Sabemos que os planetas, no mundo real, têm milhares de atributos: nome,
massa, diâmetro, vegetação, composição mineral, densidade, distância até o Sol, entre outros, mas
iremos incluir no sistema somente aqueles que nos interessam. Em nosso caso, poderia ser um texto
com o nome, a massa (medida em "Terras") e o diâmetro (medido em quilômetros).
Para isso, crie um novo projeto, vá até a pasta src, clique no botão direito e selecione New e
Java Class. Dê o nome para essa classe de Planeta e clique em OK. Observe que o IntelliJ criou um
arquivo chamado Planeta.java e, dentro dele, colocou o seguinte código:

1 public class Planeta {


2 }

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

1 public class Planeta {


2 String nome = "";
3 int diametro;
4 double massa;
5 }

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

Fonte: Elaborada pelo autor.

Criaremos objetos utilizando a palavra-chave new, da seguinte forma:


1 var planeta1 = new Planeta();
2 planeta1.nome = "Mercurio";
3 planeta1.diametro = 4_878;
4 planeta1.massa = 0.055;
5
6 var planeta2 = new Planeta();
7 planeta2.nome = "Terra";
8 planeta2.diametro = 12_742;
9 planeta2.massa = 1.0;
10
11 var planeta3 = new Planeta();
12 planeta3.nome = "Saturno";
13 planeta3.diametro = 120_536;
14 planeta3.massa = 95.2;
Classes e objetos 47

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:

1 public class Main {


2 public static void main(String[] args) {
3 var planeta = new Planeta();
4 var cachorro = new Cachorro();
5 planeta = cachorro;
6 }
7 }

Receberíamos do compilador a seguinte mensagem de erro:

Error:(5, 19) java: incompatible types: Cachorro cannot be converted to Planeta

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):

tipo nomeDaFunção(tipo parametro1, tipo parametro2) {


código
return valor;
}

O tipo de retorno, nome do método e parâmetros de retorno definem o que chamamos de


assinatura do método. Observe que o método também define um bloco de código, dentro da classe,
portanto, elas podem utilizar os valores dos atributos da classe e, ao mesmo tempo, podem conter
novas variáveis restritas ao seu interior, chamadas de variáveis locais.
48 Programação orientada a objetos I

Voltemos ao exemplo do método do raio. Ele seria implementado da seguinte forma:

1 public class Planeta {


2 String nome = "";
3 int diametro;
4 double massa;
5
6 double raio() {
7 return diametro / 2.0;
8 }
9 }

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:

1 var planeta1 = new Planeta();


2 planeta1.nome = "Mercurio";
3 planeta1.diametro = 4_878;
4 planeta1.massa = 0.055;
5
6 System.out.println(planeta1.nome);
7 System.out.println(planeta1.raio());

Esse código imprimirá:


Mercurio
2439.0

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:

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 double areaSuperficie() {
11 var raioAoQuadrado = raio() * raio();
12 return 4 * 3.1415 * raioAoQuadrado;
13 }
14
15 void imprimir() {
16 System.out.println("Nome: " + nome);
17 System.out.println("Diâmetro: " + diametro);
18 System.out.println("Massa: " + massa);
19 System.out.println("Raio: " + raio());
20 System.out.println("Area da superfície:" + areaSuperficie());
21 }
22 }

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

1 public class Planeta {


2 String nome;
3 int diametro;
4 double massa;
5
6 public Planeta(String n, int d, double m) {
7 nome = n;
8 diametro = d;
9 massa = m;
10 }

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:

1 var planeta1 = new Planeta("Mercurio", 4_878, 0.055);


2 planeta1.imprimir();
3
4 var planeta2 = new Planeta("Terra", 12_742, 1.0);
5 var planeta3 = new Planeta("Saturno", 120_536, 95.2);
6

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:

1 Planeta(String nome, int diametro, double massa) {


2 nome = nome;
3 diametro = diametro;
4 massa = massa;
5 }

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á:

1 Planeta(String nome, int diametro, double massa) {


2 this.nome = nome;
3 this.diametro = diametro;
4 this.massa = massa;
5 }

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.

3.3 A palavra-chave static


Às vezes, precisamos criar um método ou atributo que se refere à classe como um todo, e não
a determinada instância. Damos a isso o nome de atributo ou método estático (DEITEL; DEITEL,
2010). A diferença deles é que não precisarão da palavra-chave new para funcionar. Além disso, o
valor de um atributo estático é compartilhado por todos os objetos da classe (SIERRA; BATES, 2010).
Definimos um atributo ou método como estático com a palavra-chave static no momento
de sua declaração. Por exemplo, vamos definir o método estático para a descrição para a classe
Planeta, que descreve o que um planeta é:

1 static String descricao() {


2 return "Um corpo celeste esférico que orbita uma estrela";
3 }

Podemos utilizar esse método na função main por meio do comando:

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;

Dessa forma, a versão final da nossa classe Planeta seria:

1 public class Planeta {


2 final static double PI = 3.1415;
3
4 String nome;
5 int diametro;
6 double massa;
7
8 static String descricao() {
9 return "Um corpo celeste esférico que orbita uma estrela";
10 }
11
12 Planeta(String nome, int diametro, double massa) {
13 this.nome = nome;
14 this.diametro = diametro;
15 this.massa = massa;
16 }
17
18 Planeta() {
19 this("", 0, 0.0);
20 }
21
22 double raio() {
23 return diametro / 2.0;
24 }
25
26 double areaSuperficie() {
27 var raioAoQuadrado = raio() * raio();
28 return 4 * PI * raioAoQuadrado;
29 }
30
31 void imprimir() {
32 System.out.println("Nome: " + nome);
33 System.out.println("Diâmetro: " + diametro);
34 System.out.println("Massa: " + massa);
35 System.out.println("Raio: " + raio());
36 System.out.println("Area da superfície:" + areaSuperficie());
37 }
38 }
Classes e objetos 53

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 }

Se a linha 8 contivesse somente a chamada exemplo() em vez de main.exemplo(), você receberia


o erro Non-static method 'exemplo()' cannot be referenced from a static context, que pode ser
traduzido como "o método não estático exemplo não pode ser acessado de um contexto estático".
Por fim, uma prática comum é criar um método não estático cujo único papel é substituir o
main e chamá-lo por meio de um objeto anônimo:
1 public class Main {
2 void run() {
3 //Esse método substituirá o main
4 }
5
6 public static void main(String[] args) {
7 new Main().run(); //Cria o objeto e já chama run()
8 }
9 }

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

3.4. O valor especial null


Podemos utilizar o valor especial null para indicar que uma variável não contém
nenhum objeto associado. Se tentarmos utilizar qualquer atributo ou método de uma variável
nula, obteremos um erro conhecido como NullPointerException. O código a seguir simula
essa condição:

1 Planeta terra = null;


2 System.out.println(terra.raio());

Se tentarmos executá-lo, receberemos um erro como este:

Exception in thread "main" java.lang.NullPointerException

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

variáveis que guardam objetos.

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.

Ampliando seus conhecimentos


• ZINA, G. Como funcionam os inicializadores em Java? High5Devs. 24 dez. 2014.
Disponível em: http://high5devs.com/2014/12/como-funcionam-os-inicializadores-em-
java/. Acesso em: 5 ago. 2019.
Além dos construtores, o Java possui duas outras estruturas chamadas blocos de
inicialização. Há dois tipos de bloco, os estáticos e os não estáticos. Embora seu uso não
seja comum, eles são frequentemente cobrados em provas de certificação da linguagem.
Leia esse artigo de Gustavo Zina, do site High5Devs, para aprender sobre eles.

• 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.

2. Gostaríamos de criar uma classe em que cada objeto recebesse um número de


Cliente

identificação. O número inicia-se em 1, e é acrescido em 1 a cada novo objeto criado. Pense


um pouco: como poderíamos implementar isso utilizando atributos estáticos? Escreva o
código da sua solução.

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:

1 public class Ponto {


2 float x;
3 float y;
4
5 Ponto(float x, float y) {
6 this.x = x;
7 this.y = y;
8 }
9
10 Ponto(float angulo, float distancia) {
11 this(Math.cos(angulo) * distancia,
12 Math.sin(angulo) * distancia);
13 }
14 }

É possível resolver esse impasse? O que o programador deve fazer?

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á?

4.1 Classificação no mundo real: o todo e suas partes


O mundo é formado por milhares de objetos diferentes. Para entendê-los, além de criarmos
um vasto vocabulário com seus tipos, também analisamos a forma como os objetos se associam.
Pense, por exemplo, em um carro. Além de sabermos seus atributos, como cor e tamanho,
sabemos que ele transporta pessoas (passageiro(s) e o motorista, que terá a responsabilidade de
conduzir o carro de um ponto a outro). Os passageiros não são partes do carro, formando uma
associação conhecida como agregação (BOOCH et al., 2006). Nela, apesar de os objetos cooperarem
entre si, é possível imaginar situações perfeitamente plausíveis em que um existe sem o outro. Um
carro pode, por exemplo, estar estacionado ou ser vendido sem que existam passageiros dentro. E
um passageiro pode estar em casa, aguardando o veículo com o motorista chegar para buscá-lo.
Sabe-se também que um carro pode ter duas ou quatro portas, possui quatro rodas etc.
Portas e rodas são outros objetos e são partes do que se entende por carro. Essa relação, chamada
de composição, indica objetos que atuam de maneira interdependente (BOOCH et al., 2006). Isto
é, não faz sentido pensarmos em um carro sem um motor, assim como não faz muito sentido
imaginarmos um motor sem um carro.
Figura 1 – Composição: um carro é composto de várias peças
Vlad Kochelaevskiy/Shutterstock
58 Programação orientada a objetos I

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

4.2 Associando objetos


Para efeitos de exemplo, vamos imaginar um sistema acadêmico com duas classes. A primeira
refere-se ao aluno, que contém um número de matrícula e nome. Como já sabemos, poderíamos
programar a classe Aluno da seguinte forma:

1 public class Aluno {


2 int matricula;
3 String nome;
4
5 Aluno(int matricula, String nome) {
6 this.matricula = matricula;
7 this.nome = nome;
8 }
9 }

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:

1 public class Turma {


2 String nome;
3 Aluno alunos[] = new Aluno[20];
4 int qtdeAlunos = 0;
5
6 Turma(String nome) {
7 this.nome = nome;
8 }
9
10 boolean matricular(Aluno aluno) {
11 //Testamos se a turma está cheia
12 if (qtdeAlunos == alunos.length) {
13 return false;
14 }
15
16 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
17 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
18 return true;
19 }
20 }
60 Programação orientada a objetos I

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:

1 public class Main {


2 public static void main(String[] args) {
3 var aluno1 = new Aluno(1234, "Vinícius");
4 var aluno2 = new Aluno(5555, "Thais");
5 var aluno3 = new Aluno(4321, "Mariana");
6
7 var turmaA = new Turma("TurmaA");
8 turmaA.matricular(aluno1);
9 turmaA.matricular(aluno2);
10
11 var turmaB = new Turma("TurmaB");
12 turmaB.matricular(aluno3);
13
14 System.out.println(turmaA.qtdeAlunos); //Imprime 2
15 System.out.println(turmaB.qtdeAlunos); //Imprime 1
16 }
17 }

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

Teríamos a seguinte estrutura de pastas no disco:


Figura 3 – Pastas dos pacotes da escola

Fonte: Elaborada pelo autor.

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

do arquivo somente pelo seu nome simples:

1 package escola.financeiro;
2
3
4 import escola.academico.Aluno;
5
6
7 public class Boleto {
Aluno aluno;
}

Também podemos importar todas as classes de um pacote utilizando asterisco (*) no


lugar do nome da classe. Classes de um mesmo pacote são automaticamente visíveis entre si, não
havendo necessidade de importá-las. Observe que, apesar de o Java permitir "subpacotes" no disco,
na prática, cada pacote é tratado de maneira totalmente independente (SIERRA; BATES, 2010).
E o que acontece quando não especificamos nenhum pacote? As classes vão para o pacote
padrão. Apesar de possível, essa prática não é recomendada, pois não será possível importar essas
classes com base em outros pacotes (BLOCH, 2019).
Vamos agrupar as classes criadas no tópico 4.2 em pacotes? Para isso, siga os seguintes passos:
1. Vá até a pasta src e clique com o botão direito;
2. Selecione new, package e digite escola;
3. Clique sobre a classe Main e arraste-a até a pasta escola;
4. Clique no botão refactor e em seguida em Continue. Ignore os erros na classe Main, por
enquanto;
5. Vá até o pacote escola e clique com o botão direito;
6. Clique em new, package e digite academico;
7. Selecione as classes Aluno e Turma e mova para dentro do pacote;
8. Clique em Refactor e na parte inferior, Do Refactor;

9. Na classe Main, logo após a linha do package, inclua a linha import escola.academico.*.

Note que parte dos erros desapareceu.


A estrutura final do seu projeto deve ter ficado similar a esta:
Compondo objetos 63

Figura 4 – Estrutura final do projeto da escola

Fonte: Elaborada pelo autor.

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ê.

4.4 Encapsulamento e modificadores de acesso


Boas abstrações precisam contemplar tanto a visão do criador dos objetos quanto a dos usuários.
Fazemos isso "escondendo" a parte interna da classe (sua lógica e tipos de dados exatos de seus atributos)
da parte externa, chamada de interface da classe. Chamamos essa propriedade de encapsulamento e, em
Java, ela é implementada por meio de um conjunto de modificadores de acesso (ORACLE, 2017a), que
são palavras-chave da linguagem e podem ser utilizados antes de classes, atributos ou métodos.
• public: indica que a classe, atributo ou método é visível em qualquer pacote.

• 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:

1 Veremos em detalhes o conceito de classes filhas no próximo capítulo.


64 Programação orientada a objetos I

1 public class Turma {


2 public String nome;
3 public Aluno alunos[] = new Aluno[20];
4 public int qtdeAlunos = 0;
5
6 public Turma(String nome) {
7 this.nome = nome;
8 }
9
10 public boolean matricular(Aluno aluno) {
11 //Testamos se a turma está cheia
12 if (qtdeAlunos == alunos.length) {
13 return false;
14 }
15
16 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
17 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
18 return true;
19 }
20 }

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?

1 var aluno1 = new Aluno(1234, "Vinícius");


2 var aluno2 = new Aluno(5555, "Thais");
3
4 var turmaA = new Turma("TurmaA");
5 turmaA.matricular(aluno1);
6 turmaA.qtdeAlunos = 100;
7 turmaA.matricular(aluno2);

A resposta é que obteríamos o seguinte erro:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 100 out


of bounds for length 20
at escola.academico.Turma.matricular(Turma.java:18)
at escola.Main.main(Main.java:14)
Compondo objetos 65

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.

4.5 Referência a valor


Variáveis de tipos primitivos (variáveis numéricas, char ou booleanas) guardam em seu
interior o valor. Isso significa que, ao atribuirmos uma variável a outra, há uma cópia desse valor,
portanto podemos alterar a variável copiada sem interferir na variável original. Veja um exemplo:

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:

1 var a1 = new Aluno(1234, "Vinícius");


2 var a2 = a1;
3 a2.setNome("Bruno");
4 System.out.println(a1.getNome()); //Imprime Bruno

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

Fonte: Elaborada pelo autor.

Observe que x e y contêm diretamente os valores 10 e 11. Já as variáveis a1 e a2 somente


apontam para a área de memória contendo o objeto da classe Aluno. Isso tem implicações
importantes. Por exemplo, digamos que há o desejo de permitir que nosso usuário itere sobre os
alunos da classe Turma utilizando um for each. Pode parecer uma boa ideia incluir um método
getAlunos() implementado da seguinte forma:

1 public Aluno[] getAlunos() {


2 return alunos;
3 }

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:

1 var alunos = turmaA.getAlunos();


2 alunos[100] = new Aluno(1111, "Erro");

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:

1 public Aluno[] getAlunos() {


2 return Arrays.copyOf(alunos, qtdeAlunos);
3 }

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?

1 var alunos = turmaA.getAlunos();


2 alunos[1].setNome("João");

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á!

Ampliando seus conhecimentos


• SENAGA, M. O reflexo da imutabilidade do código limpo. DevMedia. 2014. Disponível
em: https://www.devmedia.com.br/o-reflexo-da-imutabilidade-no-codigo-limpo/30697.
Acesso em: 22 ago. 2019.
Falamos brevemente sobre objetos imutáveis no decorrer do capítulo. O artigo O reflexo
da imutabilidade no código limpo, escrito por Marcelo Senaga para o portal DevMedia em
2014, dá mais detalhes sobre esse tipo de objeto e explica alguns motivos por que pode ser
interessante utilizá-lo.

• RELACIONAMENTO entre classes. 1 vídeo (8 min). Publicado pelo canal Instituto


Tecnológico da Aeronáutica. Disponível em: https://pt.coursera.org/lecture/orientacao-
a-objetos-com-java/relacionamento-entre-classes-pnkcn. Acesso em: 22 ago. 2019.
Esse vídeo, desenvolvido pelos professores Clovis Fernandes e Eduardo Guerra, do
Instituto Tecnológico da Aeronáutica (ITA), fala sobre o relacionamento entre as classes.
O vídeo faz parte de um curso que aborda os princípios de orientação a objetos por meio
da linguagem Java.

• 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.

4. Considere o código a seguir:

1 Aluno aluno1 = new Aluno(1234, "Alice");


2 Aluno aluno2 = new Aluno(5555, "Bruno");
3 Aluno aluno3 = aluno1;
4 aluno1 = aluno2;
5 System.out.println(aluno3.getNome());

Qual nome será impresso na linha 5? Alice ou Bruno? 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.

5.1 Classificação no mundo real: biologia


Quando estudamos Biologia na escola, descobrimos que os cientistas possuem uma
forma bastante interessante de classificar os seres vivos: eles analisam sua estrutura em busca de
similaridades e então os agrupam hierarquicamente em reinos, filos, classes, ordens, entre outros
(UZUNIAN; BIRNER, 2012).
Dois animais de uma mesma espécie contêm um grupo enorme de características em
comum, podendo até mesmo se reproduzir entre si. Por exemplo, ao pensarmos em uma raça de
cão, como o pastor-alemão, saberemos que se trata de um animal de pelagem marrom e preta,
pelo semilongo, orelhas pontudas e porte médio. Além disso, saberemos algumas características
comportamentais, como o fato de serem inteligentes e obedientes. Quando pensamos em outra
raça, como o Yorkshire, lembraremos que se trata de um animal de pelagem longa e porte pequeno,
muito dócil.
Figura 1 – Diferentes raças de cães
Eric Isselee/Shutterstock

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

Figura 3 – Hierarquia de alguns seres vivos

Seres vivos

Plantas Animais

Carnívoros

Urso Texugo Canídeos Felinos

Pastor-alemão

Yorkshire

Fonte: Elaborada pelo autor

É 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.

5.2 Apresentando o problema


Antes de estudar o conceito de herança, vamos analisar um grupo de classes em que seria
interessante utilizá-lo. Vamos supor que um programa trabalhe com desenhos. Vamos definir
classes para representar círculos e retângulos. Queremos colocar em nossas classes atributos como
a cor e o tamanho, assim como operações interessantes, como o cálculo da área e do perímetro. Já
na classe Main, iremos criar quatro formas geométricas e listá-las.
74 Programação orientada a objetos I

Nossa classe Retangulo poderia ser definida assim:


1 public class Retangulo {
2 private double lado1;
3 private double lado2;
4 private Cor cor;
5
6 public Retangulo(double lado1, double lado2, Cor cor) {
7 this.lado1 = lado1;
8 this.lado2 = lado2;
9 this.cor = cor;
10 }
11
12 public double getLado1() {
13 return lado1;
14 }
15
16 public double getLado2() {
17 return lado2;
18 }
19
20 public boolean isQuadrado() {
21 return lado1 == lado2;
22 }
23
24 public double getArea() {
25 return lado1 * lado2;
26 }
27
28 public double getPerimetro() {
29 return 2 * (lado1 + lado2);
30 }
31
32 public Cor getCor() {
33 return cor;
34 }
35 }
Hierarquias de classes 75

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 }

Ao executar o programa, obteríamos o seguinte resultado:


• Cor: PRETO Area: 10,00 Perimetro: 14,00

• Cor: BRANCO Area: 3,00 Perimetro: 8,00


Hierarquias de classes 77

• Cor: AZUL Area: 50,27 Perimetro: 25,13

• Cor: VERDE Area: 78,54 Perimetro: 31,42

Observe que esse programa tem uma série de problemas:


1. As classes Circulo e Retangulo têm a cor em comum. Por isso, duplicam a definição do tipo
cor e o método getCor().
2. Tivemos de definir duas sobrecargas para a função imprimir na classe Main, de
comportamento praticamente idêntico – uma para cada classe.
3. A classe Main também teve de definir dois vetores diferentes e percorrê-los.
O que aconteceria no futuro, quando quiséssemos utilizar 20 ou 30 formas geométricas em
vez de duas? Ou quando incluíssemos uma nova forma geométrica? Mais código seria duplicado,
tornando a programação extremamente repetitiva.

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

Vamos reescrever a classe do círculo utilizando esse conceito?


1 package br.forma;
2
3 public class Circulo extends Forma {
4 private double raio;
5
6 public Circulo(double raio, Cor cor) {
7 super(cor);
8 this.raio = raio;
9 }
10
11 public double getRaio() {
12 return raio;
13 }
14
15 public double getDiametro() {
16 return 2 * raio;
17 }
18
19 public double getArea() {
20 return Math.PI * raio * raio;
21 }
22
23 public double getPerimetro() {
24 return 2 * Math.PI * raio;
25 }
26 }

Removemos a declaração do atributo cor e o método getCor(), entretanto ambos estão


presentes na classe Circulo – pois eles serão herdados da classe Forma.
Além disso, incluímos na primeira linha do construtor um elemento extra – a palavra-chave
super. Essa palavra é similar à palavra-chave this, porém, em vez de referenciar elementos da própria
classe, indica os da superclasse (DEITEL; DEITEL, 2010). Nesse caso, estamos utilizando o comando
super para chamar o construtor da classe Forma, que aceita como parâmetro a cor da forma. Apenas

poderíamos omitir a chamada super no construtor se possuíssemos um construtor sem parâmetros


definido na classe Forma e, nesse caso, esse construtor seria automaticamente chamado (DEITEL;
DEITEL, 2010).
Deixamos como exercício para você fazer o mesmo para a classe Retangulo. Perceba que o
código da classe Main continuará funcionando normalmente.
Já ganhamos vantagens em organizar dessa maneira. Além de escrever menos código,
considere o que aconteceria se você precisasse incluir mais atributos comuns a todas as formas,
Hierarquias de classes 79

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.

5.3.1 Tipos, casting e instanceof


Quando utilizamos a herança, também estamos criando uma relação de tipos entre as
classes. Uma vez que tanto um círculo quanto um retângulo são formas geométricas, podemos
utilizar uma variável do tipo Forma para guardar os dois tipos. Ou seja, as duas declarações a
seguir são válidas:
1 Forma f1 = new Circulo(4, Cor.Azul); //A forma f1 é um círculo
2 Forma f2 = new Retangulo(3, 1, Cor.Branco); //A forma f2 é um retângulo

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,

2010). Por exemplo:


1 //A forma f2 é um círculo?
2 if (f2 instanceof Circulo) {
3 System.out.println("A cor do circulo é:" + f2.getCor());
4 }

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 }

Agora, vamos testar o que acontece quando fazemos:


1 Forma f = new Retangulo(3,2, Cor.Verde);
2 System.out.println(f.getArea());

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).

1 A sobreposição também pode ser chamada de sobrescrita (SIERRA; BATES, 2010).


82 Programação orientada a objetos I

Para que a sobreposição de um método ocorra e o polimorfismo seja possível, sua


assinatura precisa ser idêntica na classe pai e na classe filha2, ou seja, seu nome, parâmetros e
tipo de retorno3 devem ser iguais (SIERRA; BATES, 2010).
É uma boa prática marcar, na classe filha, os métodos cuja intenção era fazer uma
sobreposição com a anotação @Override. Assim, o compilador gerará um erro caso não haja na
superclasse um método de mesmo nome (SUN MICROSYSTEMS, 1997).
Por exemplo, o método getArea() da classe Circulo seria escrito da seguinte forma para
usarmos esse recurso:
1 @Override
2 public double getArea() {
3 return Math.PI * raio * raio;
4 }

Agora que temos o polimorfismo, podemos simplificar significativamente a classe Main.


Não precisamos mais de duas versões do método imprimir, nem mesmo testar o tipo da forma no
método run(), pois agora todas as formas possuem os métodos getArea() e getPerimetro().
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 void imprimir(Forma f) {
12 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
13 f.getCor(), f.getArea(), f.getPerimetro());
14 }
15
16 public void run() {
17 for (Forma f : formas) {
18 imprimir(f);
19 }
20 }
21
22 public static void main(String[] args) {
23 new Main().run();
24 }
25 }

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).

5.5 Classes e métodos abstratos


Você já deve estar impressionado com a simplificação do código e talvez esteja até imaginando
a possibilidade de extensão que ele trará, porém não faz sentido algum criarmos um objeto da
classe Forma diretamente, como no exemplo a seguir:
Forma f = new Forma(Cor.Azul);

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 }

E, então, fazer com que uma forma implemente as duas interfaces:

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

1 public abstract class Forma implements Colorivel, Poligono {


2 private Cor cor;
3
4 public Forma(Cor cor) {
5 this.cor = cor;
6 }
7
8 public Cor getCor() {
9 return cor;
10 }
11 }

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.

5.6.1 Métodos padrão


Uma interface também pode definir métodos padrão, existentes em todas as classes que as
implementam. Para definir um método padrão, basta marcá-lo com a palavra-chave default. Eles
também são obrigatoriamente públicos (ORACLE, 2017).
Por exemplo, poderíamos definir o método padrão imprimir para os polígonos:
1 public interface Poligono {
2 double getArea();
3 double getPerimetro();
4
5 default void imprimir() {
6 System.out.printf("Area: %5.2f Perimetro: %5.2f%n",
7 getArea(), getPerimetro());
8 }
9 }

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

Ampliando seus conhecimentos


• CURSO de Java 64: classes aninhadas: internas, locais e anônimas. 2016. 1 vídeo (10
min). Publicado pelo canal Loiane Groner. Disponível em: https://www.youtube.com/
watch?v=OQKV3dCKzSI. Acesso em: 22 ago. 2019.
Há dois outros tipos de classes interessantes que você deve conhecer: classes internas e
classes anônimas. O vídeo, parte da aula 64 do curso de Java da Loiane Groner, apresenta
esses conceitos de forma didática.

• 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.

C v1 = new D(); //Ok


A v2 = v1; //Ok
A v3 = new B(); //Ok
A v4 = new E(); //OK
C v5 = (C) v4; //OK
C v6 = (C)v3; //Erro

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.

6.1 O que são generics


Como vimos nos capítulos anteriores, o Java é uma linguagem fortemente tipada. Assim,
variáveis são associadas a tipos de dados, que são verificados pelo compilador. Mas o que ocorre
em classes em que o tipo de dado não pode ser conhecido de antemão?

6.1.1 Apresentando o problema


Para introduzir o problema, vamos imaginar a construção de uma classe representando uma
lista de objetos, para usar em todos os projetos que vamos fazer.
A lista gerenciará um vetor e um contador de quantos objetos já foram inseridos em seu
interior1. Aqui temos o primeiro problema: qual será a classe dos objetos que serão colocados
na lista? Como não sabemos de antemão essa informação, vamos utilizar a classe pai de todas as
classes em Java, a classe Object:
1 public class Lista {
2 private int qtde;
3 private Object elementos[];
4
5 public Lista(int capacidade) {
6 this.elementos = new Object[capacidade];
7 }
8

(Continua)

1 Observe que essa classe será muito similar à classe Turma, do Capítulo 4.
90 Programação orientada a objetos I

9 public Object get(int indice) {


10 return indice >= qtde ? null : elementos[indice];
11 }
12
13 public boolean adicionar(Object 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 }

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 }

A primeira coisa que notamos de inconveniente é a necessidade de cast, na linha 8. Ao


adicionar um objeto à lista, ele não é necessário, pois a classe Planeta é filha de Object. Na hora
de imprimir, ele passa a ser necessário, pois o método get retornará uma referência a uma classe
Object, e o compilador não tem como saber de antemão que esse objeto se refere a um planeta. Se

você tentou executar esse programa deve ter recebido a seguinte mensagem de erro:
Generics e lambda 91

Exception in thread "main" java.lang.ClassCastException: class java.lang.String


cannot be cast to class br.cap6.Planeta at br.cap6.Main.main(Main.java:14)

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>.

6.2.1 Compatibilidade: type erasure e raw types


A implementação dos generics no Java ocorreu apenas na versão 5 da plataforma. Para que a
linguagem se mantivesse compatível, tomou-se como decisão de design o fato de que esse recurso
ocorreria apenas em tempo de compilação. Isto é, antes de o código ser compilado, o compilador
substitui o tipo Lista<Aluno> por uma lista de objetos, idêntica à que implementamos inicialmente,
e faz todos os casts e operações necessárias, literalmente excluindo do resultado final o tipo Aluno.
Esta característica é chamada de type erasure (GOETZ, 2004) e gera várias consequências. A
primeira é que ainda será possível criar o tipo "bruto" da lista (raw type), utilizando o comando:
var sistemaSolar = new Lista(9);

Da mesma forma, uma variável criada como:


Lista lista;

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.

6.2.2 Métodos genéricos


Além das classes, métodos estáticos, não estáticos e até mesmo construtores podem conter
seus próprios tipos genéricos (GOETZ, 2004). Por exemplo, digamos que se tenha uma classe
chamada Par, que associa dois objetos:
1 public class Par<K, V> {
2 private K chave;
3 private V valor;
4
5 public Par(K chave, V valor) {
6 this.chave = chave;
7 this.valor = valor;
8 }
9
10 public K getChave() { return chave; }
11 public V getValor() { return valor; }
12 }

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

poderia ser declarado como:


public static void print(Lista<?> lista)

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

6.3.1 Funções como tipo de dado


Muitas vezes, precisamos dar aos programadores, usuários de nossas classes, formas
de fornecer algum tipo de lógica, que servirá de base para algum algoritmo mais complexo,
implementado por nós.
Por exemplo, vamos incluir na classe Turma, do Capítulo 4, uma função para gerar uma nova
turma, com os alunos cujo nome se inicie com uma letra fornecida:
1 public Turma coletarPorNome(String nome) {
2 var coletados = new Turma("Coletados");
3
4 for (Aluno aluno : alunos) {
5 if (aluno.getNome().startsWith(nome)) {
6 coletados.matricular(aluno);
7 }
8 }
9 return coletados;
10 }

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 }

Em seguida, poderíamos chamar a classe da lista utilizando:


var planetasComA = planetas.coletar(new NomeIniciaCom("A"));

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.

6.3.2 Expressões lambda


Expressões lambda fornecem uma sintaxe simples para especificar funções desse tipo.
Elas automaticamente implementarão interfaces com um único método, como a nossa interface
Criterio. Sua sintaxe refere-se a esse método. O formato completo de uma expressão lambda é

apresentado a seguir (ORACLE, 2017a):


(parametros) -> {
//Codigo
return valor;
};
Generics e lambda 97

Tenha em mente o seguinte:


1. Apesar de permitido, não é necessário especificar os tipos de dados dos parâmetros. O
Java deduzirá automaticamente com base na interface que o lambda está implementando.
2. Caso haja apenas um único parâmetro, os parênteses podem ser omitidos.
3. Caso exista apenas um comando no bloco de código, as chaves podem ser omitidas.
Além disso, ao remover as chaves, se esse comando for uma expressão, o comando return
também poderá ser omitido, pois o valor da expressão será automaticamente retornado.

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"));

Muito melhor, não?

6.3.3 Referência a métodos


Muitas vezes, um lambda não faz nada além do que chamar um método existente de uma classe.
Por exemplo, digamos que a classe Planeta contivesse um método isHabitavel() que retornasse se o
planeta é ou não habitável. Para coletar todos os planetas habitáveis em uma lista, faríamos:
var habitaveis = planetas.coletar(p -> p.isHabitavel());

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

Há quatro tipos de referências a métodos, apresentados na tabela a seguir.


Tabela 1 – Tipos de método de referência

Tipo de referência Exemplo


Métodos estáticos NomeDaClasse::nomeMetodoEstatico

Instância de um objeto objeto::nomeDoMetodo

Método de instância de um tipo específico NomeDaClasse::nomeDoMetodo

Construtor NomeDaClasse::new

Fonte: Elaborada pelo autor com base em Oracle, 2017b.

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.

6.3.4 Interfaces lambda padrão


Algumas interfaces, como a Criterio, que criamos em nosso exemplo, são comuns em uma série
de situações. Por isso, o próprio Java já definiu uma série de interfaces no pacote java.util.function.
A interface Predicate<T> substitui com perfeição nossa interface de critério. Para a utilizarmos
em nossa lista, bastaríamos alterar de Criterio para Predicate e utilizar seu método test:
1 public Lista<T> coletar(Predicate<T> criterio) {
2 var coletados = new Lista<T>(elementos.length);
3 for (T elemento : elementos) {
4 if (criterio.test(elemento)) {
5 coletados.adicionar(elemento);
6 }
7 }
8 return coletados;
9 }

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.

Ampliando seus conhecimentos


• PROGRAMA funcional // Dicionário do Programador, 2019. 1 vídeo (8 min).
Publicado pelo canal Código Fonte TV. Disponível em: https://www.youtube.com/
watch?v=BxbHGPivjdc. Acesso em: 21 set. 2019.
Neste capítulo, falamos brevemente sobre programação funcional. Esse vídeo do
Dicionário do Programador, publicado no canal Código Fonte TV, explica um pouco
mais os conceitos por trás desse paradigma. Que tal assistir a ele para entender mais
sobre o assunto?
100 Programação orientada a objetos I

• ORACLE. Package java.util.function, 2019. Disponível em: https://docs.oracle.com/


javase/8/docs/api/java/util/function/package-summary.html. Acesso em: 21 set. 2019.
Outro ponto que vale a pena explorar são as interfaces do pacote java.util.function,
citada neste capítulo. Por meio dos Oracle JavaDocs, você pode consultar uma breve
descrição de todas as interfaces e classes da API padrão do Java. O texto está em inglês,
mas você pode utilizar o recurso de tradução do seu navegador para verificar a descrição
de cada classe. Explore as interfaces Function, Predicate e Consumer, verificando seus
métodos padrão e tentando imaginar onde você poderia utilizá-las.

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

List<String> frutas = new ArrayList<>();

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.

7.1.1 Adicionando elementos


Podemos adicionar um elemento ao final da lista por meio do método add:
frutas.add("Banana");

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

7.1.2 Removendo elementos


Caso você queira limpar a lista inteira, basta chamar o método clear. A lista também fornece
duas versões do método remove, sendo que uma funciona por meio do índice e a outra permite
passar por parâmetro o objeto que será removido, como no exemplo a seguir:

1 frutas.remove(0); //Remove a primeira fruta da lista


2
3 //Remove a primeira ocorrência da laranja da lista
4 frutas.remove("Laranja");

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.

7.1.3 Acessando elementos


Há duas formas de acessar elementos em uma lista. A primeira é por meio do método get,
que recebe como parâmetro o índice do elemento que será acessado. Exemplificando, o comando
a seguir imprime o primeiro item da feira:
System.out.println(feira.get(0));

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):

1 for (String legume : legumes) {


2 System.out.println(legume);
3 }

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

1 var iterador = feira.iterator();


2 while (iterador.hasNext()) {
3 String item = iterador.next();
4 if (item.startsWith("R")) {
5 iterador.remove();
6 }
7 }

O comando for each utiliza automaticamente os iteradores. Um detalhe interessante é que


você pode tornar suas classes iteráveis com o for each se implementar a interface Iterable e seu
próprio iterador (SIERRA; BATES, 2010).
Você pode estar se perguntando quando usaria iteradores, uma vez que o for each e o
removeIf parecem resolver de maneira mais clara e com menos códigos esse mesmo problema. Isso

ocorre porque esses comandos surgiram em versões mais novas da linguagem e, assim, substituíram
essa prática.

7.1.4 Outras operações


Além das operações descritas, pode-se, por meio dos métodos contains ou containsAll,
testar se um elemento existe na lista Além disso, caso você esteja de posse de um elemento da lista,
é possível descobrir seu índice utilizando os métodos indexOf ou lastIndexOf, visto que ambos
retornam -1 se o elemento não estiver na lista.
Você pode testar se uma lista está ou não vazia fazendo uso do método isEmpty ou utilizando
o método size, caso precise saber seu tamanho exato. Também é possível gerar uma sublista,
contendo apenas partes dos elementos da lista original com o comando sublist, no qual você deve
fornecer o índice inicial e o índice posterior ao final dos elementos a serem incluídos. Por exemplo,
o comando a seguir gerará uma sublista com o segundo e terceiro elementos da lista da feira:
var lista = feira.subList(1,3);

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]);

Já a operação inversa pode ser feita por meio do método Arrays.asList:


var verduras = Arrays.asList("Alface", "Couve", "Agrião");

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

iterador. Na verdade, esses são praticamente os únicos métodos do HashSet.


Já os conjuntos ordenados, como o TreeSet, que implementam a interface SortedSet,
possuem uma série de métodos adicionais (DEITEL; DEITEL, 2010). Por exemplo, há os métodos
subset, headset e tailset para a criação de subconjuntos. Vejamos alguns exemplos:

1 var pessoas = Arrays.asList("Ana", "Pedro", "Thiago", "Ana", "Lucas",


2 "Mateus", "Pedro", "Ricardo");
3
4 //Imprime: [Ana, Pedro, Thiago, Ana, Lucas, Mateus, Pedro, Ricardo]
5 System.out.println(pessoas);
6
7 //Imprime: [Mateus, Ricardo, Ana, Thiago, Lucas, Pedro]
8 var hashSet = new HashSet<>(pessoas);
9 System.out.println(hashSet);
10
11 //Imprime: [Ana, Pedro, Thiago, Lucas, Mateus, Ricardo]
12 var linkedHashSet = new LinkedHashSet<>(pessoas);
13 System.out.println(linkedHashSet);
14
15 //Imprime: [Ana, Lucas, Mateus, Pedro, Ricardo, Thiago]
16 var treeSet = new TreeSet<>(pessoas);
17 System.out.println(treeSet);
18
19 //Imprime: [Ana, Lucas]
20 System.out.println(treeSet.headSet("Mateus"));
21
22 //Imprime: [Mateus, Pedro, Ricardo, Thiago]
23 System.out.println(treeSet.tailSet("Mateus"));
24
25 //Imprime: [Mateus, Pedro]
26 System.out.println(treeSet.subSet("Mateus", "Ricardo"));
106 Programação orientada a objetos I

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.

7.2.1 Revisitando a classe Object


Como já sabemos, a classe Object é a superclasse comum a todas as classes em Java – mesmo
as classes que você cria (SIERRA; BATES, 2010). O que ainda não explicamos é que ela contém
um conjunto de métodos padrão que são usados em diversos pontos da API Java, inclusive em
conjuntos e mapas da biblioteca de coleções. Vamos entendê-los?
O primeiro método que estudaremos é o toString. Ele será usado sempre que você tentar
converter um objeto em um texto – ao imprimir o objeto usando o comando System.out.println
ou ao concatenar o objeto a uma String, por exemplo. Vamos criar uma classe chamada Aluno com
o método toString implementado:
1 public class Aluno implements Comparable<Aluno> {
2 private int matricula;
3 private String nome;
4 private int idade;
5
6 public Aluno(int matricula, String nome, int idade) {
7 this.matricula = matricula;
8 this.nome = nome;
9 this.idade = idade;
10 }
11
12 public int getMatricula() {
13 return matricula;
14 }
15
16 public String getNome() {
17 return nome;
18 }
19

(Continua)
A biblioteca de coleções 107

20 public int getIdade() {


21 return idade;
22 }
23 @Override
24
25 public String toString() {
26 return String.format("%s(%d)", nome, matricula);
27 }
28 }

O que nos permitiria fazer, a partir de agora:


1 var pedro = new Aluno(314, "Pedro", 10);
2 System.out.println(pedro); //Imprime: Pedro(314)

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 }

Observe que, na linha 4, usamos um if específico para a regra de reflexividade ao testar


o objeto contra this. Embora isso não seja estritamente necessário, trata-se de uma otimização
interessante, já que evitará que testes sejam feitos em vários atributos caso o objeto seja exatamente
o mesmo (BLOCH, 2019). Além disso, é preciso tomar cuidado quando o equals é usado entre
classes pais e filhas, pois incluir regras adicionais em classes filhas pode quebrar as regras de
simetria e transitividade.
A implementação padrão do equals testa apenas pela identidade, ou seja, só retorna true
para um objeto x, se x == x.
O terceiro método que estudaremos é o método hashCode. Um hash code é um número que
deve obedecer a três regras (BLOCH, 2019), são elas:
• Se x.equals(y) for verdadeiro, então x.hashCode() == y.hashCode() também precisa
ser verdadeiro.
• Se os atributos de um objeto usados no equals não mudarem, seu hash code também não
pode mudar.
• Dois objetos diferentes não precisam ter hash codes diferentes, entretanto um bom hash
code terá altas chances de ter números muito diferentes para objetos diferentes.
A biblioteca de coleções 109

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.

7.2.2 Ordem dos objetos


O Java fornece duas formas de indicarmos qual é a ordem de dois objetos. Quando os
objetos possuem uma ordenação natural, podemos implementar a interface Comparable em sua
classe para indicar essa ordem (SIERRA; BATES, 2010). Outra possibilidade é criar uma classe que
implemente a interface Comparator. Isso nos permite criar outro objeto, responsável por comparar
elementos e, assim, ter vários critérios de comparação.
Em ambos os casos, será obrigatório fornecer um método de comparação que retorne
um número negativo, caso o objeto A deva vir antes do objeto B; zero, se a ordem deles
for a mesma; ou positivo, caso o objeto A deva vir após o objeto B. Vejamos como seria a
implementação desse método na classe Aluno (omitimos a implementação do resto da classe
por uma questão de brevidade):
1 public class Aluno implements Comparable<Aluno> {
2
3 ...
4
5 @Override
6 public int compareTo(Aluno a) {
7 if (matricula < a.matricula) {
8 return -1;
9 } else if (matricula > a.matricula) {
10 return 1;
11 }
12
13 return 0;
14 }
110 Programação orientada a objetos I

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:

1 var alunos = new ArrayList<Aluno>();


2 alunos.add(new Aluno(314, "Pedro", 10));
3 alunos.add(new Aluno(900, "Ana", 9));
4 alunos.add(new Aluno(80, "Xavier", 11));
5 alunos.add(new Aluno(120, "Maria",10));
6
7
8 //Imprime: [Pedro(314), Ana(900), Xavier(80), Maria(120)]
9 System.out.println(alunos);
10
11 //Usa a ordem definida pela interface Comparable
12 Collections.sort(alunos);
13
14 //Imprime: [Xavier(80), Maria(120), Pedro(314), Ana(900)]
15 System.out.println(alunos);
16
17 //Usa o comparator
18 Collections.sort(alunos, Aluno.POR_NOME);
19
20 //Imprime: [Ana(900), Maria(120), Pedro(314), Xavier(80)]
21 System.out.println(alunos);

Além do método sort, coleções baseadas em ordenação (TreeSet, TreeMap) dependerão


dessas implementações para funcionar.
Note que algumas classes do Java, como a String, já possuem implementações completas dos
métodos equals, hashcode e até da interface Comparable. Além disso, chaves de mapas e conjuntos
com base em ordem consideram, para efeitos de descarte, objetos em que os comparadores
retornarem 0, mesmo que seu método equals retorne false.
A biblioteca de coleções 111

É 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

do método compare. Para evitar confusão, recomenda-se manter a implementação do Comparable


consistente com o método equals.

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 );

Observe como, novamente, apesar de ligeiramente diferente de outras coleções, o princípio


de funcionamento desse método é o mesmo. Essa consistência torna o uso da biblioteca muito
mais fácil.

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;

• sorted e sort: ordenam a coleção;


• forEach: aplica uma operação sobre cada elemento da coleção;
• collect: copia o resultado para outra coleção;
• distinct: elimina duplicatas;
• limit: restringe o resultado a um tamanho máximo;
• toArray: copia o resultado para um vetor;
• allMatch: testa se todos os elementos se enquadram em uma condição;
• anyMatch: testa se pelo menos um elemento se encaixa em uma coleção;
• average: calcula a média dos elementos;
• findFirst: retorna o primeiro elemento da coleção;
• findAny: retorna um elemento qualquer da coleção;
• sum e average: em streams numéricos, calcula a soma ou média dos valores;
• min e max: em um stream numérico, retornam o menor ou maior valor do stream.
Vejamos alguns exemplos. Suponhamos que queremos exibir, em ordem alfabética, o nome
de todos os alunos maiores de 9 anos. Com os streams, podemos fazer:
1 alunos.stream()
2 .filter(a -> a.getIdade() > 9)
3 .map(Aluno::getNome)
4 .sorted()
5 .forEach(System.out::println);

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:

1 var lista = alunos.stream()


2 .filter(a -> a.getIdade() > 9)
3 .map(Aluno::getNome)
4 .sorted()
5 .collect(Collectors.toList());
114 Programação orientada a objetos I

E que tal calcular a média de idade de todos os alunos da lista?


1 var media = alunos.parallelStream()
2 .mapToInt(Aluno::getIdade)
3 .average()
4 .getAsDouble();

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.

Ampliando seus conhecimentos


• CURSO Java completo – Aula 78: Classes Wrapper pt 01, 2016. 1 vídeo (13 min). Publicado
pelo canal DevDojo. Disponível em: https://www.youtube.com/watch?v=MBm7iyYt6NQ.
Acesso em: 8 out. 2019.
• CURSO Java completo – Aula 79: Classes Wrapper pt 02, 2016. 1 vídeo (9 min). Publicado
pelo canal DevDojo. Disponível em: https://www.youtube.com/watch?v=s5AEuHhR2PY.
Acesso em: 8 out. 2019.
Nesses dois vídeos da série Curso Java Completo, do canal DevDojo, você aprenderá mais
sobre os wrappers de tipo primitivo e como o Java camufla sua existência por meio de
operações de boxing e unboxing.

• 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ê?

2. Descreva as situações em que você deveria usar um Map<Integer, Objeto>, um List<Objeto>


ou um Objeto[]. Observe que, nos três casos, o objeto será recuperado por meio de um
valor numérico.

3. É possível gerar uma versão imutável de qualquer coleção por meio dos métodos da classe
Collections, por exemplo:

var listaFinal = Collections.unmodifiableList(feira);

Em quais situações isso é interessante? Discorra a respeito.

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

Como vimos anteriormente, uma das principais características da orientação a objetos é


permitir a criação de boas abstrações, fáceis e intuitivas de usar. Por meio do encapsulamento e dos
métodos de acesso, garantimos que os atributos não sejam modificados diretamente.
Porém, somente com o encapsulamento não é possível assegurar que o uso da classe seja feito
de modo correto. Em uma boa implementação, nossos objetos serão capazes de se manter em um
estado válido e sinalizar problemas, mesmo na presença de um código ruim (BOOCH et al., 2006).
Neste capítulo, estudaremos como podemos utilizar a linguagem Java para identificar e
tratar problemas de uso em nossas classes. Esse conceito, chamado de tratamento de erros nos
permitirá criar códigos ainda mais robustos, ajudando-nos a prevenir e diagnosticar problemas
antes que se propaguem.

8.1 Entendendo o problema


Considere, por exemplo, que um método é chamado em um momento indevido ou com
parâmetros inválidos. Para lidar com isso, geralmente usamos uma destas três abordagens:
1. Ignorar: em alguns casos, a ação inválida pode ser seguramente ignorada, sem nenhum
tipo de erro ou aviso – isso porque seu funcionamento nessa situação não alteraria
significativamente o resultado do programa. Por exemplo, o método clearAll da classe
List não precisa sinalizar que foi incapaz de limpar uma lista já vazia.

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 }

Para sinalizar problemas, a função abrir da classe BancoDeDados poderia retornar os


códigos: 0 para OK, 1 para arquivo inexistente, 2 para arquivo inacessível. Já a função lerArquivo
poderia também retornar 3 para arquivo incorreto, mas veja o que acontece com o código,
quando tratamos esse erro:
1 public static int carregar(String arquivo, Estante estante) {
2 BancoDeDados bd = new BancoDeDados();
3
4 int retVal = bd.abrir(arquivo);
5 if (retVal != ARQUIVO_OK) {
6 bd.fechar();
7 return retVal;
8 }
9
10 retVal = bd.lerArquivo(estante.livros);
11 bd.fechar();
12 return retVal;
13 }

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

ifs dessa maneira e várias chamadas a bd.fechar.


Tratamento de erros 119

Agora, vamos ver o que ocorreria no momento do uso:


1 public class TelaLivros {
2
3 public void buttonListarClicked() {
4 Estante e = new Estante();
5 int retVal = Estante.carregar(txtCodigo.getText(), e);
6
7 switch (retVal) {
8 case ARQUIVO_OK:
9 txtTitulo.setText(e.getTitulo());
10 return;
11 case ARQUIVO_INEXISTENTE:
12 showMessage("Arquivo inexistente!");
13 break;
14 case ARQUIVO_INACESSIVEL:
15 showMessage("Arquivo inacessível");
16 break;
17 case ARQUIVO_ARQUIVO_INCORRETO:
18 showMessage("Arquivo incorreto");
19 break;
20 default:
21 showMessage("Ocorreu um problema");
22 break;
23 }
24 }
25 }
26

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.

8.2 Disparando exceções


O Java fornece um poderoso mecanismo para lidarmos com exceções. Por meio dele,
podemos sinalizar, a qualquer momento, que um problema ocorreu. Poderemos verificar se um
problema vai ou não ocorrer em determinado trecho de código e, caso ocorra, tratar esse problema
exibindo uma mensagem para o usuário, por exemplo. Também poderemos analisar informações
caso um problema ocorra e não seja tratado, para identificar e resolver bugs em nosso código.
Nesse mecanismo, exceções nada mais são do que objetos que possuem atributos e métodos
com a descrição do problema. Sua hierarquia e seu nome descrevem que tipo de erro ocorreu e nos
permitem filtrar corretamente os erros que pretendemos tratar. Antes de disparar nossas próprias
exceções, vamos entender as classes básicas do Java.

8.2.1 Tipos de exceções


A classe base de qualquer exceção em Java é a classe Throwable. Ela representa qualquer coisa
que possa ser usada pelo mecanismo e tem duas classes filhas, que lidam com situações diferentes
(SIERRA; BATES, 2010):
• Classe Exception: representa problemas que a aplicação poderá tratar e lidar para se
recuperar do problema. Por exemplo, o erro de "arquivo inexistente" pode ser tratado
com uma mensagem de erro para o usuário, permitindo que ele escolha um novo arquivo.
• Classe Error: representa erros com os quais a aplicação não é capaz de lidar, de modo
que provavelmente terá sua execução imediatamente interrompida (abortada). Um
exemplo é o erro de falta de memória. Não há muito o que a aplicação possa fazer nesse
caso. Dificilmente criaremos um filho dessa classe, mas pode ser interessante capturar e
registrar o erro em algum arquivo para auxiliar em seu diagnóstico e correção.
A classe Exception possui uma filha, chamada RuntimeException. Ela representa os erros
de lógica, que normalmente não seriam tratados pelo programador, pois eles não ocorrem
ordinariamente em um código bem escrito (ORACLE, 2017).
As classes de exceção definirão também seu tipo. Há dois tipos de exceção em Java (SIERRA;
BATES, 2010):
• Exceções verificadas (Checked): são as que o programador é obrigado a dar atenção, pois
sinalizam problemas bastante comuns. Caso não o faça, o código não compilará. Ele
pode capturar e escrever código para lidar com o problema ou indicar que sua própria
função também propaga essa exceção. Um exemplo de situação desse tipo é a exceção
FileNotFoundException, que pode ocorrer sempre que se tenta abrir um arquivo.
Tratamento de erros 121

• Exceções não verificadas (Unchecked): embora seja possível capturá-las e tratá-las, o


Java não impedirá a compilação nem sinalizará qualquer erro, caso o programador não
o faça. Qualquer exceção que seja filha de RuntimeException ou Error se enquadrará,
nesse caso, às demais, no caso anterior. Um exemplo de exceção desse caso é a
ArrayIndexOutOfBoundsException, que indica que o programador acessou um índice

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

Verificada Não verificada

RuntimeException Filhas de Exception Filhas de Error

Não verificada Verificadas Não verificadas

Filhas de RuntimeException

Não verificadas

Fonte: Elaborada pelo autor.

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

comuns a IllegalArgumentException e a IllegalStateException. A primeira é usada para sinalizar


que um parâmetro inválido foi informado a um método; a segunda para sinalizar que um método
foi invocado no momento errado.
Por fim, podemos também criar nossas próprias exceções, bastando para isso criar uma
classe filha de Exception, RuntimeException ou qualquer outra exceção.
122 Programação orientada a objetos I

8.2.2 O comando throw


Que tal um pouco de ação? Vamos ver na prática como disparar uma exceção. Para isso,
considere inicialmente a implementação de uma classe ContaCorrente, como demostrado a
seguir. Ela permite saques e depósitos:
1 public class ContaCorrente {
2 private double saldo = 1000;
3 private boolean aberta = true;
4
5 public void depositar(double valor) {
6 saldo += valor;
7 System.out.printf("Deposito: %.2f Saldo: %.2f%n",
8 valor, saldo);
9 }
10 public void sacar(double valor) {
11 saldo -= valor;
12 System.out.printf(" Saque: %.2f Saldo: %.2f%n",
13 valor, saldo);
14 }
15
16 public double getSaldo() {
17 return saldo;
18 }
19
20 public void fechar() {
21 this.aberta = false;
22 }
23 }"

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

Veja o que ocorreu em uma de suas execuções:


Deposito: 1000,00 Saldo: 1000,00
Saque: 540,00 Saldo: 460,00
Deposito: -114,00 Saldo: 346,00
Saque: 1005,00 Saldo: -659,00
Deposito: 237,00 Saldo: -422,00
Deposito: 88,00 Saldo: -334,00
Deposito: 149,00 Saldo: -185,00
Saque: 295,00 Saldo: -480,00
Saque: -855,00 Saldo: 375,00
Saque: -890,00 Saldo: 1265,00
Saque: 180,00 Saldo: 1085,00

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,

e a exceção IllegalStateException, quando eles forem usados no momento em que a conta


estiver fechada:
1 public void depositar(double valor) {
2 if (!aberta) {
3 throw new IllegalStateException("Depósito em conta fechada!");
4 }
5 if (valor < 0) {
6 throw new IllegalArgumentException(
7 "Depósito negativo. Valor: " + valor + " Saldo: " + saldo);
8 }
9 saldo += valor;
10 System.out.printf("Deposito: %.2f Saldo: %.2f%n", valor, saldo);
11 }
12 public void sacar(double valor) {
13 if (!aberta) {
14 throw new IllegalStateException("Saque em conta fechada!");
15 }
16 if (valor < 0) {
17 throw new IllegalArgumentException(
18 "Saque negativo. Valor: " + valor + " Saldo: " + saldo);
19 }
20 if (saldo - valor < 0) {
(Continua)
124 Programação orientada a objetos I

21 throw new IllegalArgumentException(


22 "Saldo insuficiente. Valor: " + valor + " Saldo: " + saldo);
23 }
24
25 saldo -= valor;
26 System.out.printf(" Saque: %.2f Saldo: %.2f%n", valor, saldo);
27 }

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:

Deposito: 1000,00 Saldo: 2000,00


Deposito: 143,00 Saldo: 2143,00
Deposito: 128,00 Saldo: 2271,00
Exception in thread "main" java.lang.IllegalArgumentException: Depósito negativo.
Valor: -98.0 Saldo: 2271.0
at br.cap8.ContaCorrente.depositar(ContaCorrente.java:12)
at br.cap8.Main.run(Main.java:14)
at br.cap8.Main.main(Main.java:22)

Note que, como descrevemos, o comando throw interrompeu imediatamente a execução do


código quando foi atingido, neste caso, no método depositar. O que o Java faz, então, é desviar o
código para o método run, em que os métodos estavam sendo chamados. Como naquele ponto
também não havia nenhum tratamento de erro, o método foi abandonado imediatamente e
desviou-se para a função main. Lá, outra vez, não havia tratamento nenhum para o erro, portanto,
o programa encerrou e exibiu a mensagem descrevendo a exceção. Observe que as linhas da
mensagem de erro acima, destacada em negrito, descrevem esse fluxo.
Perceba que, graças ao encapsulamento provido pelos métodos de acesso, torna-se impossível
utilizar a classe incorretamente (BOOCH et al., 2006). Quando ocorrer, ela sinalizará o problema
imediatamente, mesmo quando várias classes diferentes usarem esse objeto, em pontos muito
diversos do código.

8.3 Capturando exceções


E se quiséssemos tratar esse erro? Por exemplo, dizer ao usuário que o saque ou depósito é
impossível, mas seguir permitindo que ele faça outras operações? Veremos isso a seguir.

8.3.1 Try, catch


Para responder à pergunta anterior, delimitamos o bloco de código em que o erro pode
ocorrer dentro do comando try. Após o comando, incluímos uma cláusula catch, na qual podemos
capturar o objeto da exceção em que ocorreu o erro. Por exemplo, altere o método run da classe
Main para:
Tratamento de erros 125

1 public void run() {


2 Random random = new Random();
3 ContaCorrente cc = new ContaCorrente();
4
5 for (int i = 0; i < 10; i++) {
6 try {
7 int valor = 250 - random.nextInt(500);
8 if (random.nextBoolean()) {
9 cc.depositar(valor);
10 } else {
11 cc.sacar(valor * 5);
12 }
13 } catch (IllegalArgumentException e) {
14 System.out.println("Não realizado:" + e.getMessage());
15 }
16 }
17 }

Ao executar o código novamente, obtemos o seguinte resultado:

Deposito: 1000,00 Saldo: 2000,00


Deposito: 150,00 Saldo: 2150,00
Não realizado: Saque negativo. Valor: -575.0 Saldo: 2150.0
Deposito: 114,00 Saldo: 2264,00
Não realizado: Saque negativo. Valor: -900.0 Saldo: 2264.0
Saque: 745,00 Saldo: 1519,00
Não realizado: Saque negativo. Valor: -135.0 Saldo: 1519.0
Saque: 1080,00 Saldo: 439,00
Não realizado: Saldo insuficiente. Valor: 585.0 Saldo: 439.0
Não realizado: Depósito negativo. Valor: -197.0 Saldo: 439.0
Deposito: 92,00 Saldo: 531,00

Agora, quando um erro ocorre, o bloco de código é abandonado e é desviado imediatamente


para o catch. Como o objeto do erro foi capturado em uma variável chamada e, pudemos tratá-lo
(imprimindo sua mensagem) e o código seguiu executando na próxima linha após o catch.
Observe que esse catch captura erros apenas da classe IllegalArgumentException. O que
aconteceria se o IllegalStateException ocorresse? O erro seria propagado, como se o try sequer
existisse. Há várias maneiras de tratar esse problema (SIERRA; BATES, 2010):
1. Se o tratamento dos dois erros for diferente, pode-se simplesmente adicionar uma
segunda cláusula catch e realizá-lo dentro dela.
2. O bloco catch é capaz de pegar não só objetos da classe da exceção, mas também todas as
suas classes filhas, portanto, você poderia alterar o catch para RuntimeException, capturar
as duas exceções e tratá-las igualmente. Caso haja mais de um catch, o catch das classes
filhas deve ficar acima do catch da classe pai, com prioridade sobre ele.
126 Programação orientada a objetos I

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

classe filha de Exception que seja disparada.


Organizando dessa maneira, nosso main passou a ter um nível mais alto de verificação, que
poderia tomar uma ação (como gravar o texto da exceção em um arquivo de registro para análise
posterior) antes de abortar o programa. Neste caso, apenas escrevemos uma mensagem indicando
que um erro inesperado ocorreu e usamos o comando printStackTrace para imprimir a mesma
mensagem que o Java teria impresso se a exceção não tivesse sido capturada.
Experimente alterar o código para disparar, dentro do método run, uma RuntimeException e
ver sua captura ocorrer no método main.
Tratamento de erros 127

8.3.2 Exceções verificadas


Até o momento, lidamos apenas com exceções não verificadas, ou seja, filhas de
RuntimeException. E se considerarmos importante validar uma exceção para o caso de tentativa

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:

1 public class SaldoInsuficienteException extends Exception {


2 private double valor;
3 private double saldo;
4 public SaldoInsuficienteException(double saldo, double valor) {
5 super("Saldo insuficiente. Valor: " + valor + " Saldo: " + saldo);
6 }
7
8 public double getValor() {
9 return valor;
10 }
11
12 public double getSaldo() {
13 return saldo;
14 }
15 }

Observe que o saldo e o valor se tornaram atributos de nossa exceção e já aproveitamos


para cadastrar aqui sua mensagem padrão. Agora, vamos disparar essa exceção no caso do saque,
utilizando a cláusula throw:

1 public void sacar(double valor) {


2 if (!aberta) {
3 throw new IllegalStateException("Saque em conta fechada!");
4 }
5 if (valor < 0) {
6 throw new IllegalArgumentException(
7 "Saque negativo. Valor: " + valor + " Saldo: " + saldo);
8 }
9 if (saldo - valor < 0) {
10 throw new SaldoInsuficienteException(valor, saldo);
11 }
12
13 saldo -= valor;
14 System.out.printf(" Saque: %.2f Saldo: %.2f%n", valor, saldo);
15 }

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.

public void sacar(double valor) throws SaldoInsuficienteException {

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:

public void run() throws Exception {

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

Fonte: Elaborada pelo autor.

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:

1 public static Estante carregar(String arquivo) throws BDException {


2 try {
3 BancoDeDados bd = new BancoDeDados();
4 Estante estante = new ContaCorrente();
5 bd.abrir(arquivo);
6 estante.livros = bd.lerArquivo();
7 return cc;
8 } finally {
9 bd.fechar();
10 }
11 }

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.

8.3.4 Relançando exceções


Nem sempre nosso tratamento de erro elimina o problema causado pela exceção. Por
exemplo, podemos simplesmente registrar em alguma classe que o erro ocorreu para facilitar o
diagnóstico, mas ainda querer propagá-lo. Nesse caso, podemos relançar a exceção novamente com
o comando throw:

1 public static Estante carregar(String arquivo) throws BDException {


2 try {
3 BancoDeDados bd = new BancoDeDados();
4 Estante estante = new Estante();
5 bd.abrir(arquivo);
6 estante.livros = bd.lerArquivo();
7 return cc;
8 } catch (BDException e) {
9 System.out.println(e.getMessage());
10 throw e;
11 } finally {
12 bd.fechar();
13 }
14 }
130 Programação orientada a objetos I

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.

Ampliando seus conhecimentos


• ECKEL, B.; VENNERS, B. The Trouble with Checked Exceptions. Artima, 18 ago. 2003.
Disponível em: https://www.artima.com/intv/handcuffs.html. Acesso em: 3 out. 2019.
Exceções verificadas são um recurso muito bacana, mas o Java é uma das poucas linguagens
a implementá-las. Nessa entrevista publicada pelo site Artima, o criador da linguagem
C#, Anders Hejlsberg, explica por que tomou a decisão de não as incluir na linguagem
criada por ele. O texto está em inglês, mas você pode utilizar o tradutor do seu navegador.

• 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

2. Implemente as classes descritas na Figura 2 apresentada neste capítulo.

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.

1 public class Aluno {


2 private int matricula;
3 private String nome;
4 private int idade;
5
6 public Aluno(int matricula, String nome, int idade) {
7 this.matricula = matricula;
8 this.nome = nome;
9 this.idade = idade;
10 }
11
12 public int getMatricula() {
13 return matricula;
14 }
15
16 public String getNome() {
17 return nome;
18 }
19
20 public int getIdade() {
21 return idade;
22 }
23 }

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.

3. Os cinco pilares são:

1. Simples, orientada a objetos e familiar;


2. Robusta e segura;
3. Neutralidade de arquitetura e portável;
4. Alta performance;
5. Interpretada, multithread e dinâmica.

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 }

Resolução com for:


1 for (var numero = 2; numero <= 20; numero += 2) {
2 System.out.println(numero);
3 }

2. O resultado é "eu amo o java".


134 Programação orientada a objetos I

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.

1 public class Medicamento {


2 String nome;
3 int lote;
4 int quantidade = 0;
5
6 Medicamento(String nome, int lote) {
7 this.nome = nome;
8 this.lote = lote;
9 }
10
11 void retirar(int quantidade) {
12 this.quantidade -= quantidade;
13 }
14
15 boolean emFalta() {
16 return quantidade <= 0;
17 }
18 }

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.

1 public class Cliente {


2 static int ultimoId;
3
4 int id;
5
6 Cliente() {
7 ultimoId += 1;
8 this.id = ultimoId;
9 }
10 }
Gabarito 135

Teste o código com:


1 Cliente c1 = new Cliente();
2 Cliente c2 = new Cliente();
3 System.out.println(c1.id); //Imprime 1
4 System.out.println(c2.id); //Imprime 1

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:

1 public class Ponto {


2 double x;
3 double y;
4
5 Ponto(double x, double y) {
6 this.x = x;
7 this.y = y;
8 }
9
10 static Ponto porAnguloDistancia(float angulo, float distancia) {
11 return new Ponto(Math.cos(angulo) * distancia,
12 Math.sin(angulo) * distancia);
13 }
14 }

Esse método poderia ser usado assim:


var ponto = Ponto.porAnguloDistancia(45, 100);
136 Programação orientada a objetos I

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:

1 public Turma(Aluno[] alunos) {


2 this.alunos = alunos;
3 }

Ele permitiria um uso do tipo:


1 var alunos = new Aluno[20];
2 var problema = new Turma(alunos);
3 //Sem usar o método matricular!
4 alunos[0] = new Aluno(1234, "Vinícius");

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.

2. No primeiro caso, ao alterar o nome, teríamos de editar a classe que imprimia a


Main

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.

3. O vetor de Alunos é um exemplo de objeto em composição. Embora os alunos dentro dele


sejam associados à turma apenas por agregação, o vetor em si, que guarda esses alunos, é
criado pela classe Turma e só faz sentido dentro dela (na verdade, ele sequer é visível fora
dela). Por isso, pode ser encarado como uma parte da classe Turma.

4. Já vimos que variáveis de objetos são referências e armazenam o endereço do objeto na


memória, porém, temos de tomar um cuidado: esses endereços são passados por valor.
O que isso quer dizer? Que, ao atribuirmos um novo objeto a uma variável de referência
existente, estamos desvinculando o seu objeto original e vinculando um novo. Confuso?
Vamos estudar o código em detalhes:

1 Aluno aluno1 = new Aluno(1234, "Alice");


2 Aluno aluno2 = new Aluno(5555, "Bruno");
3 Aluno aluno3 = aluno1;
4 aluno1 = aluno2;
5 System.out.println(aluno3.getNome());

Após executar a linha 1 até 3, temos a seguinte situação:

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.

5. Classe Planeta ajustada:

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

31 public String getNome() {


32 return nome;
33 }
34
35 public double getMassa() {
36 return massa;
37 }
38
39 public void setMassa(double massa) {
40 this.massa = massa;
41 }
42
43 public int getDiametro() {
44 return diametro;
45 }
46
47 public void setDiametro(int diametro) {
48 this.diametro = diametro;
49 }
50
51 //Métodos
52 public double raio() {
53 return diametro / 2.0;
54 }
55
56 public double areaSuperficie() {
57 var raioAoQuadrado = raio() * raio();
58 return 4 * PI * raioAoQuadrado;
59 }
60
61 public void imprimir() {
62 System.out.println("Nome: " + nome);
63 System.out.println("Diâmetro: " + diametro);
64 System.out.println("Massa: " + massa);
65 System.out.println("Raio: " + raio());
66 System.out.println("Area da superfície:" + areaSuperficie());
67 }
68 }
Gabarito 139

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

2. Observe pelo enunciado que:

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 }

3. Podemos definir uma interface com a operação:


1 public interface Operacao {
2 public int aplicar(int valor);
3 }

E então escrever nosso método:


1 public int[] processar(int[] elementos, Operacao operacao) {
2 var resultado = new int[elementos.length];
3 for (var i = 0; i < elementos.length; i++) {
4 resultado[i] = operacao.aplicar(elementos[i]);
5 }
6
return resultado;
7
}

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 }

E chamaríamos o método da seguinte forma:


1 var valores = new int[] {1,2,3,4,5};
2 processar(valores, new Vezes10());
Gabarito 141

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));

var descricoes = sistema.converter(p -> "Planeta" + p.getNome());


for (int i = 0; i < descricoes.getQtde(); i++ ) {
System.out.println(descricoes.get(i));
}

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);

O Collections.unmodifiableList impede isso, permitindo manter a classe conveniente,


bastando fazer:

public List<Aluno> getAlunos() {

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 }

1 public class ContaException extends BancoException {


2 public ContaException(String msg, double valor, double saldo) {
3 super(String.format("%s Valor: %.2f, Saldo: %.2f",
4 msg, valor, saldo));
5 }
6 }

1 public class SaldoInsuficienteException extends ContaException {


2 public SaldoInsuficienteException(double valor, double saldo) {
3 super("Saldo insuficiente", valor, saldo);
4 }
5 }

1 public class BancoRuntimeException extends RuntimeException {


2 public BancoRuntimeException(String message) {
3 super(message);
4 }
5 }

1 public class ContaRuntimeException extends BancoRuntimeException {


2 public ContaRuntimeException(String msg, double valor, double saldo) {
3 super(String.format(
4 "%s Valor: %.2f, Saldo: %.2f", msg, valor, saldo));
5 }
6 }
7

1 public class TransacaoInvalidaException extends ContaRuntimeException {


2 public TransacaoInvalidaException(String msg, double valor,
3 double saldo) {
4 super(msg, valor, saldo);
5 }
6 }
144 Programação orientada a objetos I

1 public class ContaFechadaException extends ContaRuntimeException {


2 public TransacaoInvalidaException(double valor, double saldo) {
3 super("Conta fechada", valor, saldo);
4 }
5 }

Classe da conta corrente alterada:


1 public class ContaCorrente {
2 private double saldo = 1000;
3 private boolean aberta = true;
4
5 public void depositar(double valor) {
6 if (!aberta) {
7 throw new ContaFechadaException(valor, saldo);
8 }
9
10 if (valor < 0) {
11 throw new TransacaoInvalidaException("Depósito", valor, saldo);
12 }
13 saldo += valor;
14 }
15
16 public void sacar(double valor) throws SaldoInsuficienteException {
17 if (!aberta) {
18 throw new ContaFechadaException(valor, saldo);
19 }
20
21 if (valor < 0) {
22 throw new TransacaoInvalidaException("Saque", valor, saldo);
23 }
24
25 if (saldo - valor < 0) {

26 throw new SaldoInsuficienteException(valor, saldo);

27 }

28
29 saldo -= valor;

30 }

31

32 public double getSaldo() {

33 return saldo;

34 }

35
36 public void fechar() {

37 this.aberta = false;

38 }

39 }
Gabarito 145

Programa principal utilizando as classes:


1 import java.util.Random;
2
3 public class Main {
4 public void run() {
5 Random random = new Random();
5 ContaCorrente cc = new ContaCorrente();
6
7 for (int i = 0; i < 10; i++) {
8 try {
9 int valor = 250 - random.nextInt(500);
10 if (random.nextBoolean()) {
11 cc.depositar(valor);
12 } else {
13 cc.sacar(valor * 5);
14 }
15 } catch (ContaRuntimeException | ContaException e) {
16 System.out.println("Não realizado: " + e.getMessage());
17 }
18 }
19 }
20
21 public static void main(String[] args) {
22 try {
23 new Main().run();
24 } catch (Exception e) {
25 System.out.println("Um erro inesperado ocorreu:");
26 e.printStackTrace();
27 }
28 }
29 }

3. Bastaria adicionar exceções ao construtor da classe Aluno:


1 public Aluno(int matricula, String nome, int idade) {
2 if (matricula < 0) {
3 throw new IllegalArgumentException("Matrícula inválida!");
4 }
5
5 if (nome == null || nome.isBlank()) {
6 throw new IllegalArgumentException("O nome é obrigatório!");
7 }
8
9 if (idade < 3 || idade > 20) {
10 throw new IllegalArgumentException("Idade inválida!");
11 }
12
(Continua)
146 Programação orientada a objetos I

13 this.matricula = matricula;
14 this.nome = nome;
15 this.idade = idade;
16 }

Observação: a idade do ensino fundamental é, normalmente, de 6 a 14 anos. O método


setIdade não é tão rígido, pois isso poderia resultar em barrar totalmente exceções à regra

(superdotados, atrasados ou crianças reprovadas).


Programação Orientada a Objetos I
A orientação a objetos representou uma revolução na
forma de construir softwares. Desde que
se popularizou, em torno dos anos 1990, se
tornou parte das principais linguagens de
programação utilizadas no mercado, como
Java, C++ e C#. Ela permitiu a divisão
de programas em partes menores e
reutilizáveis e a construção de
sistemas cada vez mais complexos.
Não é à toa que hoje é utilizada
em programas não só em computadores e
laptops, mas também em celulares, tablets e
até mesmo em carros e geladeiras.

VINÍCIUS GODOY

Código Logístico Fundação Biblioteca Nacional


ISBN 978-85-387-6531-8

58878 9 788538 765318

Você também pode gostar