Logo Passei Direto
Buscar
Material
páginas com resultados encontrados.
páginas com resultados encontrados.
left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

Prévia do material em texto

Programação Orientada a Objetos I
VINÍCIUS GODOY
Código Logístico
58878
ISBN 978-85-387-6531-8
9 7 8 8 5 3 8 7 6 5 3 1 8
Programação orientada 
a objetos I
IESDE
2019
Vinícius Godoy
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
© 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.
19-60075 CDD: 005.117
CDU: 004.43
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ências e valores 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.
Vídeo
Programação orientada a objetos I10
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 eportá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).
Vídeo
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
Compilador Empacotador
Execução do código 
final
Bytecode
compilado
programa.class
cadastro.class
Bytecodes
agrupados
programa.jar
Bytecodes
agrupados
programa.jar
Código-fonte 
programa.java 
cadastro.java
Execução
Compilação
Máquina 
Virtual Java
(JVM)
Fonte: Mendonça, 2018, p. 110.
Programação orientada a objetos I12
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.
Vídeo
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.
Programação orientada a objetos I14
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, existemtrê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.
https://www.jetbrains.com/idea/
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.
Programação orientada a objetos I16
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.
Vídeo
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.
Programação orientada a objetos I18
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.
Programação orientada a objetos I20
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
2
3
4
5
public class Aula1 {
 public static void main(String[] args) {
 System.out.println("Olá mundo!"); //Imprime Olá mundo
 }
}
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 */.
Programação orientada a objetos I22
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
2
3
4
5
6
7
public class Aula1
{
 public static void main(String[] args)
 {
 System.out.println("Olá mundo!"); //Imprime Olá mundo
 }
}
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
• INTELLIJIDEA. 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.
https://resources.jetbrains.com/storage/products/intellij-idea/docs/IntelliJIDEA_ReferenceCard.pdf
https://resources.jetbrains.com/storage/products/intellij-idea/docs/IntelliJIDEA_ReferenceCard.pdf
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
2
int x = 10;
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:
• boolean: booleano (verdadeiro/falso): boolean.
• byte, short, int e long: inteiros.
• float e double: numéricos decimais.
Vídeo
Programação orientada a objetos I26
• char: texto.
• 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
2
3
4
5
var x = true;
var y = false;
System.out.println(x == y); //Imprime false
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
2
3
4
5
var x = true;
var y = false;
System.out.println(x == y); //Imprime false
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 retornatrue 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:
Programação orientada a objetos I28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
boolean v1 = true, v2 = true;
boolean f1 = false, f2 = false;
System.out.println("Comparação com E (&&)");
System.out.println(v1 && v2); //Imprime true, ambos verdadeiros
System.out.println(v1 && f2); //Imprime false, f2 é falso
System.out.println(f1 && v2); //Imprime false, f1 é falso
System.out.println(f1 && f2); //Imprime false, ambos falsos
System.out.println("Comparação com OU (||)");
System.out.println(v1 || v2); //Imprime true, ambos verdadeiros
System.out.println(v1 || f2); //Imprime true, v1 é verdadeiro
System.out.println(f1 || v2); //Imprime true, v2 é verdadeiro
System.out.println(f1 || f2); //Imprime false, ambos falsos
System.out.println("Negação com !");
System.out.println(!v1); //Imprime false, pois v1 é true
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 complexo 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
var onze = 0b1010; //1011 em binário equivale a 11 em decimal
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
2
3
int y = 10;
short x = y;
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
2
3
int y = 10;
short x = (short) y;
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, 
Programação orientada a objetos I30
a partir daí, sem apresentar qualquer tipo de erro. Tenha cuidado, portanto, antes de fazer esse tipo 
de conversão.
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
2
var numero = -12.5;
var outroNumero = -numero;
É possível atribuir a uma variável um valor com base em seu valor antigo, da seguinte forma:
1
2
3
var x = 10;
x = x + 5;
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
2
3
var x = 10;
x += 5; //Equivalente a x = x + 5
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
2
3
4
var x = 10;
System.out.println(++x); //Muda o valor para 11 e imprime o resultado
System.out.println(x++); //Muda o valor para 12, mas imprime 11
System.out.println(x); //Imprime 12
Conhecendo a linguagem 31
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.
Tabela 2 – Operadores para manipulação de bits
Operação Símbolo Exemplo Resultado
Left-Shift << x << 1 0101_0110
Right-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 definidosutilizando 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
2
3
var titulo = "Programação OO ";
var subtitulo = "em java";
System.out.println(titulo + subtitulo);
Programação orientada a objetos I32
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’.
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
2
3
public enum Naipes {
 Paus, Ouros, Copas, Espadas
}
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
2
var valor = Naipes.Paus;
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
2
3
4
var valor = "Ouros";
Naipes naipe = Naipes.valueOf(valor);
System.out.println(valor.ordinal()); //Imprime 1
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.
Conhecendo a linguagem 33
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:
1
2
3
4
5
6
int[] x = new int[] {10,2,30,-5};
var y = new double[10];
char[][] z;
System.out.println(x[0]); //Imprime 10
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
2
3
4
5
6
7
8
9
10
11
12
13
var xadrez = new char[8][8];
var irregular = new int[][] {
 {1},
 {1}, {2},
 {},
 {1}, {2}, {3}, {4}, {5}
};
var irregular2 = new int[4][];
irregular2[0] = new int[1];
irregular2[1] = new int[2];
irregular2[2] = new int[0];
irregular2[3] = new int[5];
Programação orientada a objetos I34
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.
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
2
3
4
5
6
7
var idade = 10;
if (idade < 18) {
 System.out.println("Menor de idade");
} else {
 System.out.println("Maior de idade");
}
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, 
Vídeo
Conhecendo a linguagem 35
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:
1
2
3
4
5
6
7
if (idade < 14) {
 System.out.println("Criança");
} else if (idade < 18){
 System.out.println("Adolescente");
} else {
 System.out.println("Adulto");
}
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, precisamosdesviar o fluxo com base em um conjunto de valores. Para isso, 
utilizamos o comando switch.
Programação orientada a objetos I36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var exemplo = 1;
switch (exemplo) {
 case 1:
 System.out.println("Primeira condição");
 break;
 case 2:
 System.out.println("Segunda condição");
 case 3:
 System.out.println("Terceira condição");
 break;
 default:
 System.out.println("Outra condição");
 break;
}
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.
Vídeo
Conhecendo a linguagem 37
1
2
3
4
5
6
var x = 1;
while (x <= 3) {
 System.out.print(x + ", ");
 x++;
}
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:
1
2
3
4
5
6
var x = 1;
do {
System.out.print(x + ", ");
x++;
} while (x <= 3);
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
2
3
4
for (var x = 1; x <= 3; x++) {
 System.out.print(x + ",");
}
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:
Programação orientada a objetos I38
1
2
3
4
5
var frutas = new String[] {"Laranja, Banana, Maçã"};
for (var i = 0; i < frutas.length; i++) {
 var fruta = frutas[i];
 System.out.println(fruta);
}
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.
For each
Na verdade, iterar sobre os elementos de uma lista é uma tarefa tão comum que uma estrutura 
for foi criada inteiramente para isso. É chamado de for each, que significa "para cada" em tradução 
literal. O mesmo código anterior poderia ser reescrito assim:
1
2
3
4
var frutas = new String[] {"Laranja, Banana, Maçã"};
for (var fruta : frutas) {
 System.out.println(fruta);
}
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:
iterar: acessar cada 
um dos elementos 
de um vetor em 
ordem.
Conhecendo a linguagem 39
1
2
3
4
5
6
7
8
var numeros = new int[] {10,2,23,11,14,17,6,13};
for (var numero : numeros) {
 //Se o número é impar
 if (numero % 2 == 1) {
 continue; //Pule para o próximo número
 }
 System.out.println(numero);
}
Esse programa imprime:
10
2
14
6
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var matrizQuadrada = new int[][] {
 {1,2,3,4,5},
 {10,20,30,0,40},
 {100, 200, 300, 400, 500}
};
externo:
for (int i = 0; i < matrizQuadrada.length; i++) {
 System.out.println();
 for (var numero : matrizQuadrada[i]) {
 System.out.print(numero + " ");
 if (numero == 0) {
 break externo;
 }
 }
} 
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.
Programação orientada a objetos I40
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:
1
2
3
4
5
6
7
var x = 10;
for (var i = 0; i < 10; i++) {
 var a = i*2;
 System.out.println(a);
 System.out.println(x);
}
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 linguagemde 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.
Vídeo
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/index.html
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/index.html
https://www.youtube.com/watch?v=3Ie7VMJWoYo
Conhecendo a linguagem 41
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.
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
2
3
4
5
var letras = new char[]{'a','b','e','j','m','o','u','v','z',' '};
var idx = new int[]{2, 6, 9, 0, 4, 5, 9, 5, 9, 3, 0, 7, 0};
for (var i : idx) {
 System.out.print(letras[i]);
}
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.
https://www.youtube.com/watch?v=Z6Y8zupCKfk
https://www.youtube.com/watch?v=Z6Y8zupCKfk
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
al
ta
na
ka
/S
hu
tte
rs
to
ck
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.
Vídeo
Programação orientada a objetos I44
Figura 2 – A operação de um instrumento é fazer som
m
ax
im
 ib
ra
gi
m
ov
/S
hu
tte
rs
to
ck
 Ao entendermos os objetos a nossa volta, os classificamos em diferentes tipos. Por exemplo, na 
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
Ch
ris
to
ph
er
 H
al
l/S
hu
tte
rs
to
ck
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
2
public class Planeta {
}
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:
Vídeo
Programação orientada a objetos I46
1
2
3
4
5
public class Planeta {
 String nome = "";
 int diametro;
 double massa;
}
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. Estessã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
2
3
4
5
6
7
8
9
10
11
12
13
14
var planeta1 = new Planeta();
planeta1.nome = "Mercurio";
planeta1.diametro = 4_878;
planeta1.massa = 0.055;
var planeta2 = new Planeta();
planeta2.nome = "Terra";
planeta2.diametro = 12_742;
planeta2.massa = 1.0;
var planeta3 = new Planeta();
planeta3.nome = "Saturno";
planeta3.diametro = 120_536;
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
2
3
4
5
6
7
public class Main {
 public static void main(String[] args) {
 var planeta = new Planeta();
 var cachorro = new Cachorro();
 planeta = cachorro;
 }
}
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.
Programação orientada a objetos I48
Voltemos ao exemplo do método do raio. Ele seria implementado da seguinte forma:
1
2
3
4
5
6
7
8
9
public class Planeta {
 String nome = "";
 int diametro;
 double massa;
 double raio() {
 return diametro / 2.0;
 }
}
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
2
3
4
5
6
7
var planeta1 = new Planeta();
planeta1.nome = "Mercurio";
planeta1.diametro = 4_878;
planeta1.massa = 0.055;
System.out.println(planeta1.nome);
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Planeta {
 String nome = "";
 int diametro;
 double massa;
 double raio() {
 return diametro / 2.0;
 }
 void imprimir() {
 System.out.println("Nome: " + nome);
 System.out.println("Diâmetro: " + diametro);
 System.out.println("Massa: " + massa);
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Planeta {
 String nome = "";
 int diametro;
 double massa;
 double raio() {
 return diametro / 2.0;
 }
 double areaSuperficie() {
 var raioAoQuadrado = raio() * raio();
 return 4 * 3.1415 * raioAoQuadrado;
 }
 void imprimir() {
 System.out.println("Nome: " + nome);
 System.out.println("Diâmetro: " + diametro);
 System.out.println("Massa: " + massa);
 System.out.println("Raio: " + raio());
 System.out.println("Area da superfície:" + areaSuperficie());
 }
}
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:
Programação orientada a objetos I50
1
2
3
4
5
6
7
8
9
10
public class Planeta {
String nome;
int diametro;
double massa;
Planeta(String n, int d, double m) {
nome = n;
diametro = d;
massa = m;
}
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
2
3
4
5
6
var planeta1 = new Planeta("Mercurio", 4_878, 0.055);
planeta1.imprimir();
var planeta2 = new Planeta("Terra", 12_742, 1.0);
var planeta3 = new Planeta("Saturno", 120_536, 95.2);
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
2
3
4
5
Planeta(String nome, int diametro, double massa) {
 nome = nome;
 diametro = diametro;
 massa = massa;
}
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 
na assinatura do método(antigamente chamado de n). Como resolver esse impasse? Para isso, o 
Classes e objetos 51
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
2
3
4
5
Planeta(String nome, int diametro, double massa) {
 this.nome = nome;
 this.diametro = diametro;
 this.massa = massa;
}
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
2
3
4
5
Planeta() {
 this.nome = "";
 this.diametro = 0;
 this.massa = 0;
}
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
2
3
Planeta() {
 this("", 0, 0.0);
}
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
2
3
static String descricao() {
 return "Um corpo celeste esférico que orbita uma estrela";
}
Podemos utilizar esse método na função main por meio do comando:
System.out.println(Planeta.descricao());
Vídeo
Programação orientada a objetos I52
Essa chamada também seria possível por meio de uma variável do tipo Planeta, por exemplo:
1
2
3
4
var planeta1 = new Planeta("Mercurio", 4_878, 0.055);
//Valido, porém, confuso
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:
static final double PI = 3.1415;
Dessa forma, a versão final da nossa classe Planeta seria:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Planeta {
 static final double PI = 3.1415;
 String nome;
 int diametro;
 double massa;
 static String descricao() {
 return "Um corpo celeste esférico que orbita uma estrela";
 }
 Planeta(String nome, int diametro, double massa) {
 this.nome = nome;
 this.diametro = diametro;
 this.massa = massa;
 }
 Planeta() {
 this("", 0, 0.0);
 }
 double raio() {
 return diametro / 2.0;
 }
 double areaSuperficie() {
 var raioAoQuadrado = raio() * raio();
 return 4 * PI * raioAoQuadrado;
 }
 void imprimir() {
 System.out.println("Nome: " + nome);
 System.out.println("Diâmetro: " + diametro);
 System.out.println("Massa: " + massa);
 System.out.println("Raio: " + raio());
 System.out.println("Area da superfície:" + areaSuperficie());
 }
}
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
2
3
4
5
6
7
8
9
10
public class Main {
 void exemplo() {
 System.out.println("Chamando método não estático");
}
 public static void main(String[] args) {
 var main = new Main(); //Crie um objeto
 main.exemplo(); //Chame o método não estático
 }
}
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
2
3
4
5
6
7
8
9
public class Main {
 void run() {
 //Esse método substituirá o main
 }
 public static void main(String[] args) {
 new Main().run(); //Cria o objeto e já chama run()
 }
}
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.
Programação orientada a objetos I54
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
2
Planeta terra = null;
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.
Vídeo
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ódigoem 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!
http://high5devs.com/2014/12/como-funcionam-os-inicializadores-em-java/
http://high5devs.com/2014/12/como-funcionam-os-inicializadores-em-java/
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/comportamento-e-estado-das-classes-bixyH
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/comportamento-e-estado-das-classes-bixyH
Programação orientada a objetos I56
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 Cliente em que cada objeto recebesse um número de 
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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Ponto {
 float x;
 float y;
 Ponto(float x, float y) {
 this.x = x;
 this.y = y;
 }
 Ponto(float angulo, float distancia) {
 this(Math.cos(angulo) * distancia,
 Math.sin(angulo) * distancia);
 }
}
É 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
Vl
ad
 K
oc
he
la
ev
sk
iy
/S
hu
tte
rs
to
ck
Vídeo
Programação orientada a objetos I58
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
Le
m
us
iq
ue
/d
av
oo
da
/H
ap
py
 A
rt
/S
hu
tte
rs
to
ck
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 ferramentasusadas 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.
Vídeo
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
2
3
4
5
6
7
8
9
public class Aluno {
 int matricula;
 String nome;
 Aluno(int matricula, String nome) {
 this.matricula = matricula;
 this.nome = nome;
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Turma {
 String nome;
 Aluno alunos[] = new Aluno[20];
 int qtdeAlunos = 0;
 Turma(String nome) {
 this.nome = nome;
 }
 boolean matricular(Aluno aluno) {
 //Testamos se a turma está cheia
 if (qtdeAlunos == alunos.length) {
 return false;
 }
 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
 return true;
 }
}
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
Le
m
us
iq
ue
/d
av
oo
da
/H
ap
py
 A
rt
/S
hu
tte
rs
to
ck
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.
Vídeo
Programação orientada a objetos I60
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
 public static void main(String[] args) {
 var aluno1 = new Aluno(1234, "Vinícius");
 var aluno2 = new Aluno(5555, "Thais");
 var aluno3 = new Aluno(4321, "Mariana");
 var turmaA = new Turma("TurmaA");
 turmaA.matricular(aluno1);
 turmaA.matricular(aluno2);
 var turmaB = new Turma("TurmaB");
 turmaB.matricular(aluno3);
 System.out.println(turmaA.qtdeAlunos); //Imprime 2
 System.out.println(turmaB.qtdeAlunos); //Imprime 1
 }
}
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:
Vídeo
Programação orientada a objetos I62
1
2
3
4
5
package escola.financeiro;
public class Boleto {
 escola.academico.Aluno aluno;}
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
2
3
4
5
6
7
package escola.financeiro;
import escola.academico.Aluno;
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.
Compondo objetos 63
A estrutura final do seu projeto deve ter ficado similar a esta:
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.
1 Veremos em detalhes o conceito de classes filhas no próximo capítulo.
Vídeo
Programação orientada a objetos I64
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Turma {
 public String nome;
 public Aluno alunos[] = new Aluno[20];
 public int qtdeAlunos = 0;
 public Turma(String nome) {
 this.nome = nome;
 }
 public boolean matricular(Aluno aluno) {
 //Testamos se a turma está cheia
 if (qtdeAlunos == alunos.length) {
 return false;
 }
 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
 return true;
 }
}
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
2
3
4
5
6
7
var aluno1 = new Aluno(1234, "Vinícius");
var aluno2 = new Aluno(5555, "Thais");
var turmaA = new Turma("TurmaA");
turmaA.matricular(aluno1);
turmaA.qtdeAlunos = 100;
turmaA.matricular(aluno2);
A resposta é que obteríamos o seguinte erro:
Compondo objetos 65
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)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package escola.academico;
public class Turma {
 private String nome;
 private Aluno alunos[] = new Aluno[20];
 private int qtdeAlunos = 0;
 public Turma(String nome) {
 this.nome = nome;
 }
 public boolean matricular(Aluno aluno) {
 //Testamos se a turma está cheia
 if (qtdeAlunos == alunos.length) {
 return false;
 }
 alunos[qtdeAlunos] = aluno; //Associa o aluno a turma
 qtdeAlunos = qtdeAlunos + 1; //Adiciona um aluno
 return true;
}
public void setNome(String nome) {
 if (nome == null || nome.isBlank()) {
 return;
 }
 this.nome = nome;
}
 public String getNome() {
 return nome;
 }
 public int getQtdeAlunos() {
 return qtdeAlunos;
 }
}
Programação orientada a objetos I66
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 
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, é consideradouma 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ências e valores
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
2
3
4
5
var x = 10;
var y = x;
y = y + 1;
System.out.println(y); //Imprime 11
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.
Vídeo
Compondo objetos 67
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 
endereço de memória simplesmente é copiado e, portanto, as duas passam a apontar para o mesmo 
objeto. Veja um exemplo:
1
2
3
4
var a1 = new Aluno(1234, "Vinícius");
var a2 = a1;
a2.setNome("Bruno");
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
10
11
a1 a2
x
y
Matrícula: 1234
Nome: Bruno
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
2
3
public Aluno[] getAlunos() {
 return alunos;
}
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
2
var alunos = turmaA.getAlunos();
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).
Programação orientada a objetos I68
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).
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
2
3
public Aluno[] getAlunos() {
 return Arrays.copyOf(alunos, qtdeAlunos);
}
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
2
var alunos = turmaA.getAlunos();
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 imutabilidadeno 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.
https://www.devmedia.com.br/o-reflexo-da-imutabilidade-no-codigo-limpo/30697
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/relacionamento-entre-classes-pnkcn
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/relacionamento-entre-classes-pnkcn
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/hands-on-colaboracoes-entre-classes-IKCLa
https://pt.coursera.org/lecture/orientacao-a-objetos-com-java/hands-on-colaboracoes-entre-classes-IKCLa
Programação orientada a objetos I70
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
2
3
4
5
Aluno aluno1 = new Aluno(1234, "Alice");
Aluno aluno2 = new Aluno(5555, "Bruno");
Aluno aluno3 = aluno1;
aluno1 = aluno2;
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
Er
ic
 Is
se
le
e/
Sh
ut
te
rs
to
ck
Er
ic
 Is
se
le
e/
Sh
ut
te
rs
to
ck
Er
ic
 Is
se
le
e/
Sh
ut
te
rs
to
ck
Do
ra
 Z
et
t/
Sh
ut
te
rs
to
ck
Vídeo
Programação orientada a objetos I72
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
Af
ric
a 
St
ud
io
/S
hu
tte
rs
to
ck
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
Carnívoros
Urso Texugo Canídeos Felinos
Plantas Animais
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 tabletse 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.
Nossa classe Retangulo poderia ser definida assim:
Vídeo
Programação orientada a objetos I74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Retangulo {
 private double lado1;
 private double lado2;
 private Cor cor;
 
 public Retangulo(double lado1, double lado2, Cor cor) {
 this.lado1 = lado1;
 this.lado2 = lado2;
 this.cor = cor;
 }
 
 public double getLado1() {
 return lado1;
 }
 
 public double getLado2() {
 return lado2;
 }
 
 public boolean isQuadrado() {
 return lado1 == lado2;
 }
 
 public double getArea() {
 return lado1 * lado2;
 }
 
 public double getPerimetro() {
 return 2 * (lado1 + lado2);
 }
 
 public Cor getCor() {
 return cor;
 }
}
E a classe dos círculos? Ela teria uma construção muito similar, observe:
Hierarquias de classes 75
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package br.forma;
public class Circulo {
 private double raio;
 private Cor cor;
 
 public Circulo(double raio, Cor cor) {
 this.raio = raio;
 this.cor = cor;
 }
 public double getRaio() {
 return raio;
 }
 
 public double getDiametro() {
 return 2 * raio;
 }
 
 public double getArea() {
 return Math.PI * raio * raio;
 }
 
 public double getPerimetro() {
 return 2 * Math.PI * raio;
 }
 
 public Cor getCor() {
 return cor;
 }
}
O que é o tipo de dado Cor, presente nas duas formas? Trata-se de um enum, definido como:
1
2
3
4
5
package br.forma;
public enum Cor {
 Branco, Vermelho, Amarelo, Laranja, Verde, Azul, Violeta, Preto;
}
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.
Programação orientada a objetos I76
Vamos colocar aqui também funções para imprimir cor, tamanho e raio de qualquer uma 
das formas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package br.forma;
public class Main {
 private Retangulo[] retangulos = {
 new Retangulo(2, 5, Cor.Preto),
 new Retangulo(3, 1, Cor.Branco)
 };
 private Circulo[] circulos = {
 new Circulo(4, Cor.Azul),
 new Circulo(5, Cor.Verde)
 };
 private void imprimir(Circulo c) {
 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
 c.getCor(), c.getArea(), c.getPerimetro());
 }
 
 private void imprimir(Retangulo r) {
 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
 r.getCor(), r.getArea(), r.getPerimetro());
 }
 
 public void run() {
 System.out.println("Imprimindo formas");
 for (Retangulo x : retangulos) {
 imprimir(x);
 }
 
 for (Circulo x : circulos) {
 imprimir(x);
 }
 }
 public static void main(String[] args) {
 new Main().run();
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
package br.forma;
public class Forma {
 private Cor cor;
 
 public Forma(Cor cor) {
 this.cor = cor;
 }
 
 public Cor getCor() {
 return cor;
 }
}
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).
Vídeo
Programação orientada a objetos I78
Vamos reescrever a classe do círculo utilizando esse conceito?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package br.forma;
public class Circulo extends Forma {
 private double raio;
 
 public Circulo(double raio, Cor cor) {
 super(cor);
 this.raio = raio;
 }
 
 public double getRaio() {
 return raio;
 }
 
 public double getDiametro() {
 return 2 * raio;
 }
 
 public double getArea() {
 return Math.PI * raio * raio;
 }
 
 public double getPerimetro() {
 return 2 * Math.PI * raio;
 }
}
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
2
Forma f1 = new Circulo(4, Cor.Azul); //A forma f1 é um círculo
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
2
3
4
//A forma f2 é um círculo?
if (f2 instanceof Circulo) {
 System.out.println("A cor do circulo é:" + f2.getCor());
}
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
2
3
4
Forma f1 = new Circulo(4, Cor.Azul);
//Type casting: Converte a variável f1 para um círculo
Circulo c = (Circulo)f1;
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).
Programação orientada a objetos I80
Que tal utilizarmos o que aprendemos para alterar a classe Main de modo a termos um único 
vetor de formas?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package br.forma;
public class Main {
 private Forma[] formas = {
 new Retangulo(2, 5, Cor.Preto),
 new Retangulo(3, 1, Cor.Branco),
 new Circulo(4, Cor.Azul),
 new Circulo(5, Cor.Verde)
 };
 
 private void imprimir(Circulo c) {
 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
 c.getCor(), c.getArea(), c.getPerimetro());
 }
 private void imprimir(Retangulo r) {
 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
 r.getCor(), r.getArea(), r.getPerimetro());
 }
 public void run() {
 System.out.println("Imprimindo formas");
 for (Forma f : formas) {
 if (f instanceof Circulo) {
 Circulo c = (Circulo)f;
 imprimir(c);
 } else if (f instanceof Retangulo) {
 Retangulo r = (Retangulo)f;
 imprimir(r);
 }
 }
 }
 
 public static void main(String[] args) {
 new Main().run();
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package br.forma;
public class Forma {
 private Cor cor;
 
 public Forma(Cor cor) {
 this.cor = cor;
 }
 
 public Cor getCor() {
 return cor;
 }
 
 public double getArea() {
 return 0;
 }
 
 public double getPerimetro() {
 return 0;
 }
}
Agora, vamos testar o que acontece quando fazemos:
1
2
Forma f = new Retangulo(3,2, Cor.Verde);
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).
Vídeo
Programação orientada a objetos I82
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
2
3
4
 @Override
 public double getArea() {
 return Math.PI * raio * raio;
 }
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package br.forma;
public class Main {
 private Forma[] formas = {
 new Retangulo(2, 5, Cor.PRETO),
 new Retangulo(3, 1, Cor.BRANCO),
 new Circulo(4, Cor.AZUL),
 new Circulo(5, Cor.VERDE)
 };
 
 void imprimir(Forma f) {
 System.out.printf("Cor: %8s Area: %5.2f Perimetro: %5.2f%n",
 f.getCor(), f.getArea(), f.getPerimetro());
 }
 
 public void run() {
 for (Forma f : formas) {
 imprimir(f);
 }
 }
 
 public static void main(String[] args) {
 new Main().run();
 }
}
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 é, sabemosque 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package br.forma;
public abstract class Forma {
 private Cor cor;
 public Forma(Cor cor) {
 this.cor = cor;
 }
 public Cor getCor() {
 return cor;
 }
 
 public abstract double getArea();
 public abstract double getPerimetro();
}
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. 
Vídeo
Programação orientada a objetos I84
Assim, ainda será possível chamar esses métodos com base em uma variável do tipo Forma, mas 
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
2
3
4
5
package br.forma;
public interface Colorivel {
 Cor getCor();
}
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
2
3
4
5
6
package br.forma;
public interface Poligono {
 double getArea();
 double getPerimetro();
}
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.
Vídeo
Hierarquias de classes 85
E, então, fazer com que uma forma implemente as duas interfaces:
1
2
3
4
5
6
7
8
9
10
11
public abstract class Forma implements Colorivel, Poligono {
 private Cor cor;
 
 public Forma(Cor cor) {
 this.cor = cor;
 }
 
 public Cor getCor() {
 return cor;
 }
}
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
2
3
4
package br.forma;
public interface Figura extends Poligono, Colorivel {
}
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
2
3
4
5
6
7
8
9
public interface Poligono {
 double getArea();
 double getPerimetro();
 
 default void imprimir() {
 System.out.printf("Area: %5.2f Perimetro: %5.2f%n",
 getArea(), getPerimetro());
 }
}
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:
Programação orientada a objetos I86
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package br.forma;
public abstract class Forma implements Colorivel, Poligono {
 private Cor cor;
 
 public Forma(Cor cor) {
 this.cor = cor;
 }
 
 public Cor getCor() {
 return cor;
 }
 
 @Override
 public void imprimir() {
 Poligono.super.imprimir();
 }
}
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?
https://www.youtube.com/watch?v=OQKV3dCKzSI
https://www.youtube.com/watch?v=OQKV3dCKzSI
https://www.artima.com/lejava/articles/designprinciples.html
https://www.artima.com/lejava/articles/designprinciples.html
Programação orientada a objetos I88
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
2
3
4
5
6
7
8
public class Lista {
 private int qtde;
 private Object elementos[];
 
 public Lista(int capacidade) {
 this.elementos = new Object[capacidade];
 }
(Continua)
1 Observe que essa classe será muito similar à classe Turma, do Capítulo 4.
Vídeo
Programação orientada a objetos I90
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 public Object get(int indice) {
 return indice >= qtde ? null : elementos[indice];
 }
 public boolean adicionar(Object objeto) {
 if (qtde == getCapacidade()) {
 return false;
 }
 elementos[qtde++] = objeto;
 return true;
 }
 
 public int getCapacidade() {
 return elementos.length;
 }
 public int getQtde() {
 return qtde;
 }
}
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
2
3
4
5
6
7
8
9
10
11
var sistemaSolar = new Lista(9);
sistemaSolar.adicionar(new Planeta("Mercurio", 4_878, 0.055));
sistemaSolar.adicionar("Venus");
sistemaSolar.adicionar(new Planeta("Terra", 12_742, 1.0));
sistemaSolar.adicionar(new Planeta("Saturno", 120_536, 95.2));
//Imprimindo o planeta
for (var i = 0; i < sistemaSolar.getQtde(); i++) {
 Planeta p = (Planeta) sistemaSolar.get(i);
 p.imprimir();
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Lista<T> {
 private int qtde;
 private T elementos[];
 public Lista(int capacidade) {
 this.elementos = (T[]) new Object[capacidade];
 }
 public T get(int indice) {
 return indice >= qtde ? null: elementos[indice];
 }
 public boolean adicionar(T objeto) {
 if (qtde == getCapacidade()) {
 return false;
 }
 elementos[qtde++] = objeto;
 return true;
 }
 public int getCapacidade() {
 return elementos.length;
 }
 public int getQtde() {
 return qtde;
 }
}
Vídeo
Programação orientada a objetos I92
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
2
3
4
5
6
7
8
9
10
11
var sistemaSolar = new Lista<Planeta>(9);sistemaSolar.adicionar(new Planeta("Mercurio", 4_878, 0.055));
//sistemaSolar.adicionar("Venus"); Não funciona mais
sistemaSolar.adicionar(new Planeta("Terra", 12_742, 1.0));
sistemaSolar.adicionar(new Planeta("Saturno", 120_536, 95.2));
//Imprimindo o planeta
for (var i = 0; i < sistemaSolar.getQtde(); i++) {
 sistemaSolar.get(i).imprimir(); //O get já retorna um planeta
}
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
2
3
4
5
6
7
8
9
10
11
12
public class Par<K, V> {
 private K chave;
 private V valor;
 public Par(K chave, V valor) {
 this.chave = chave;
 this.valor = valor;
 }
 public K getChave() { return chave; }
 public V getValor() { return valor; }
}
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
2
3
4
5
6
public class Util {
 public static <K, V> boolean compare(Par<K, V> p1, Par<K, V> p2) {
 return p1.getChave().equals(p2.getChave()) &&
 p1.getValor().equals(p2.getValor());
 }
}
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
2
3
var p1 = new Par<String, String>("Raffs", "João");
var p2 = new Par<String, String>("Imai", "Bruno");
boolean iguais = Util.<String, String>compare(p1, p2);
Programação orientada a objetos I94
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.
Vídeo
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
2
3
4
5
6
7
8
9
10
public Turma coletarPorNome(String nome) {
 var coletados = new Turma("Coletados");
 for (Aluno aluno : alunos) {
 if (aluno.getNome().startsWith(nome)) {
 coletados.matricular(aluno);
 }
 }
 return coletados;
}
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 interfaceCriterio<T> {
 boolean atende(T elemento);
}
Com base nessa interface, poderíamos então fazer o método coletar genérico:
1
2
3
4
5
6
7
8
9
public Lista coletar(Criterio<T> criterio) {
 Lista coletados = new Lista(elementos.length);
 for (T elemento : elementos) {
 if (criterio.atende(elemento)) {
 coletados.adicionar(elemento);
 }
 }
 return coletados;
}
Programação orientada a objetos I96
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
2
3
4
5
6
7
8
9
10
11
12
public class NomeIniciaCom implements Criterio<Planeta> {
 private String nome;
 public NomeIniciaCom(String nome) {
 this.nome = nome;
 }
 @Override
 public boolean atende(Planeta elemento) {
 return elemento.getNome().startsWith(nome);
 }
}
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
2
3
4
5
6
var planetasComA = planetas.coletar(new Criterio<Planeta>() {
 @Override
 public boolean atende(Planeta elemento) {
 elemento.getNome().startsWith("A");
 }
});
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
2
3
var planetasComA = planetas.coletar((Planeta elemento) -> {
 return elemento.getNome().startsWith("A");
});
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);
Programação orientada a objetos I98
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
2
3
4
5
6
7
8
9
public Lista<T> coletar(Predicate<T> criterio) {
 var coletados = new Lista<T>(elementos.length);
 for (T elemento : elementos) {
 if (criterio.test(elemento)) {
 coletados.adicionar(elemento);
 }
 }
 return coletados;
}
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 naoHabitaveis = 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 seusbugs 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?
https://www.youtube.com/watch?v=BxbHGPivjdc
https://www.youtube.com/watch?v=BxbHGPivjdc
Programação orientada a objetos I100
• 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.
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
https://www.ibm.com/developerworks/java/tutorials/j-generics/j-generics.html
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
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.
Vídeo
Programação orientada a objetos I102
É uma boa prática utilizar as listas sempre por meio de sua interface principal (BLOCH, 
2019). Por exemplo, declaramos um List da seguinte forma:
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 omé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
2
3
4
5
6
7
List<String> legumes = new ArrayList<>();
legumes.add("Beringela");
legumes.add("Couve-flor");
List<String> feira = new ArrayList<>();
feira.addAll(frutas);
feira.addAll(0, legumes);
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
2
3
4
frutas.remove(0); //Remove a primeira fruta da lista
//Remove a primeira ocorrência da laranja da lista
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
2
3
for (String legume : legumes) {
 System.out.println(legume);
}
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:
Programação orientada a objetos I104
1
2
3
4
5
6
7
var iterador = feira.iterator();
while (iterador.hasNext()) {
 String item = iterador.next();
 if (item.startsWith("R")) {
 iterador.remove();
 }
}
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.
Vídeo
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var pessoas = Arrays.asList("Ana", "Pedro", "Thiago", "Ana", "Lucas", 
"Mateus", "Pedro", "Ricardo");
//Imprime: [Ana, Pedro, Thiago, Ana, Lucas, Mateus, Pedro, Ricardo]
System.out.println(pessoas);
//Imprime: [Mateus, Ricardo, Ana, Thiago, Lucas, Pedro]
var hashSet = new HashSet<>(pessoas);System.out.println(hashSet);
//Imprime: [Ana, Pedro, Thiago, Lucas, Mateus, Ricardo]
var linkedHashSet = new LinkedHashSet<>(pessoas);
System.out.println(linkedHashSet);
//Imprime: [Ana, Lucas, Mateus, Pedro, Ricardo, Thiago]
var treeSet = new TreeSet<>(pessoas);
System.out.println(treeSet);
//Imprime: [Ana, Lucas]
System.out.println(treeSet.headSet("Mateus"));
//Imprime: [Mateus, Pedro, Ricardo, Thiago]
System.out.println(treeSet.tailSet("Mateus"));
//Imprime: [Mateus, Pedro]
System.out.println(treeSet.subSet("Mateus", "Ricardo"));
1
2
3
4
5
6
7
var iterador = feira.iterator();
while (iterador.hasNext()) {
 String item = iterador.next();
 if (item.startsWith("R")) {
 iterador.remove();
 }
}
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.
Vídeo
Programação orientada a objetos I106
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;
• lower e 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Aluno implements Comparable<Aluno> {
 private int matricula;
 private String nome;
 private int idade;
 public Aluno(int matricula, String nome, int idade) {
 this.matricula = matricula;
 this.nome = nome;
 this.idade = idade;
 }
 public int getMatricula() {
 return matricula;
 }
 public String getNome() {
 return nome;
 }
(Continua)
A biblioteca de coleções 107
20
21
22
23
24
25
26
27
28
 public int getIdade() {
 return idade;
 }
 @Override
 public String toString() {
 return String.format("%s(%d)", nome, matricula);
 }
}
O que nos permitiria fazer, a partir de agora:
1
2
var pedro = new Aluno(314, "Pedro", 10);
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String nome1 = "Vinicius";
String nome2 = nome1;
String nome3 = new String("Vinicius");
System.out.println(nome1); //Imprime Vinicius
System.out.println(nome2); //Imprime Vinicius
System.out.println(nome3); //Imprime Vinicius
//True, são o mesmo objeto
System.out.println(nome1 == nome2);
//False: O nome é igual, mas não é o mesmo objeto
System.out.println(nome1 == nome3);
//True: Os valores de nome1 e nome3 são iguais
System.out.println(nome1.equals(nome3));
Programação orientada a objetos I108
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, paranossa classe do aluno, poderíamos considerar iguais dois alunos com 
mesmo nome e número de matrícula (descartando a idade):
1
2
3
4
5
6
7
8
9
10
@Override
public boolean equals(Object obj) {
 if (obj == null) return false; //Não nulidade
 if (obj == this) return true; //Reflexividade
 if (!(obj instanceof Aluno)) return false;
 Aluno o = (Aluno) obj;
 return matricula == matricula && nome.equals(o.nome);
}
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
2
3
4
@Override
public int hashCode() {
 return Objects.hash(matricula, nome);
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Aluno implements Comparable<Aluno> {
 ...
 @Override
 public int compareTo(Aluno a) {
 if (matricula < a.matricula) {
 return -1;
 } else if (matricula > a.matricula) {
 return 1;
 }
 return 0;
 }
Programação orientada a objetos I110
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
2
3
public class Aluno implements Comparable<Aluno> {
 public static final Comparator<Aluno> POR_NOME =
 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var alunos = new ArrayList<Aluno>();
alunos.add(new Aluno(314, "Pedro", 10));
alunos.add(new Aluno(900, "Ana", 9));
alunos.add(new Aluno(80, "Xavier", 11));
alunos.add(new Aluno(120, "Maria",10));
//Imprime: [Pedro(314), Ana(900), Xavier(80), Maria(120)]
System.out.println(alunos);
//Usa a ordem definida pela interface Comparable
Collections.sort(alunos);
//Imprime: [Xavier(80), Maria(120), Pedro(314), Ana(900)]
System.out.println(alunos);
//Usa o comparator
Collections.sort(alunos, Aluno.POR_NOME);
//Imprime: [Ana(900), Maria(120), Pedro(314), Xavier(80)]
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
2
3
4
Map<String, Aluno> alunoMap = new HashMap<>();
for (Aluno aluno : alunos) {
 alunoMap.put(aluno.getNome(), aluno);
}
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
2
3
4
5
6
7
8
9
10
11
12
if (alunoMap.containsKey("Ana")) {
 System.out.println("Ana está no mapa");
}
Aluno a1 = alunoMap.get("Juca");
System.out.println(a1); //Imprime nulo
Aluno a2 = alunoMap.getOrDefault("Juca", new Aluno(0, "?", 0));
System.out.println(a2); //Imprime: ?(0)
Aluno a3 = alunoMap.get("Pedro");
System.out.println(a3); //Imprime: Pedro(314)
Vídeo
Programação orientada a objetos I112
Para percorrer os valores de um mapa por meio de um for each, podemosutilizar 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
2
3
4
for (var entry : alunoMap.entrySet()) {
 System.out.println("Nome:" + entry.getKey());
 System.out.println("Aluno:" + entry.getValue());
}
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
2
3
4
alunoMap.forEach((k, v) -> {
 System.out.println("Nome:" + k);
 System.out.println("Aluno:" + v);
});
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
2
3
alunoMap.replaceAll((k, v) -> new Aluno(
 v.getMatricula(), "Turma1: " + v.getNome(), v.getIdade())
);
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.
Vídeo
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
2
3
4
5
alunos.stream()
 .filter(a -> a.getIdade() > 9)
 .map(Aluno::getNome)
 .sorted()
 .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
2
3
4
5
var lista = alunos.stream()
 .filter(a -> a.getIdade() > 9)
 .map(Aluno::getNome)
 .sorted()
 .collect(Collectors.toList());
Programação orientada a objetos I114
E que tal calcular a média de idade de todos os alunos da lista?
1
2
3
4
var media = alunos.parallelStream()
 .mapToInt(Aluno::getIdade)
 .average()
 .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.
https://www.youtube.com/watch?v=MBm7iyYt6NQ
https://www.youtube.com/watch?v=s5AEuHhR2PY
https://www.youtube.com/watch?v=Ehjgp084GtI
https://www.youtube.com/watch?v=Ehjgp084GtI
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.
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
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 clear 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).
Vídeo
Programação orientada a objetos I118
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
2
3
4
5
6
7
8
public static Estante carregar(String arquivo) {
 BancoDeDados bd = new BancoDeDados();
 Estante estante = new Estante();
 bd.abrir(arquivo);
 estante.livros = bd.lerArquivo();
 bd.fechar();
 return e;
}
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
2
3
4
5
6
7
8
9
10
11
12
13
public static int carregar(String arquivo, Estante estante) {
 BancoDeDados bd = new BancoDeDados();
 int retVal = bd.abrir(arquivo);
 if (retVal != ARQUIVO_OK) {
 bd.fechar();
 return retVal;
}
 retVal = bd.lerArquivo(estante.livros);
 bd.fechar();
 return retVal;
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TelaLivros {
 public void buttonListarClicked() {
 Estante e = new Estante();
 int retVal = Estante.carregar(txtCodigo.getText(), e);
 switch (retVal) {
 case ARQUIVO_OK:
 txtTitulo.setText(e.getTitulo());
 break;
 case ARQUIVO_INEXISTENTE:
 showMessage("Arquivo inexistente!");
 break;
 case ARQUIVO_INACESSIVEL:
 showMessage("Arquivo inacessível");
 break;
 case ARQUIVO_ARQUIVO_INCORRETO:
 showMessage("Arquivo incorreto");
 break;
 default:
 showMessage("Ocorreu um problema");
 break;
 }
 }
}
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 acessoinválido ou de arquivo 
não encontrado.
• Bibliotecas dispares poderiam ter códigos de erro iguais para problemas diferentes.
Programação orientada a objetos I120
• 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.
Vídeo
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
Filhas de RuntimeException
Não verificadas
RuntimeException
Não verificada
Filhas de Error
Não verificadas
Filhas de Exception
Verificadas
Exception
Verificada
Error
Não verificada
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.
Programação orientada a objetos I122
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ContaCorrente {
 private double saldo = 1000;
 private boolean aberta = true;
 public void depositar(double valor) {
 saldo += valor;
 System.out.printf("Deposito: %2f Saldo: %2f%n",
 valor, saldo);
 }
 public void sacar(double valor) {
 saldo -= valor;
 System.out.printf(" Saque: %2f Saldo: %2f%n",
 valor, saldo);
 }
 public double getSaldo() {
 return saldo;
 }
 public void fechar() {
 this.aberta = false;
 }
}"
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
 public void run() {
 Random random = new Random();
 ContaCorrente cc = new ContaCorrente();
 
 for (int i = 0; i < 10; i++) {
 int valor = 250 - random.nextInt(500);
 if (random.nextBoolean()) {
 cc.depositar(valor);
 } else {
 cc.sacar(valor * 5);
 }
 }
 }
 public static void main(String[] args) {
 new Main().run();
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void depositar(double valor) {
 if (!aberta) {
 throw new IllegalStateException("Depósito em conta fechada!");
 }
 if (valor < 0) {
 throw new IllegalArgumentException(
 "Depósito negativo Valor: " + valor + " Saldo: " + saldo);
 }
 saldo += valor;
 System.out.printf("Deposito: %2f Saldo: %2f%n", valor, saldo);
}
public void sacar(double valor) {
 if (!aberta) {
 throw new IllegalStateException("Saque em conta fechada!");
 }
 if (valor < 0) {
 throw new IllegalArgumentException(
 "Saque negativo Valor: " + valor + " Saldo: " + saldo);
 }
 if (saldo - valor < 0) {
(Continua)
Programação orientada a objetos I124
21
22
23
24
25
26
27
 throw new IllegalArgumentException(
 "Saldo insuficiente Valor: " + valor + " Saldo: " + saldo);
}
 saldo-= valor;
 System.out.printf(" Saque: %2f Saldo: %2f%n", valor, saldo);
}
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:
Vídeo
Tratamento de erros 125
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void run() {
 Random random = new Random();
 ContaCorrente cc = new ContaCorrente();
for (int i = 0; i < 10; i++) {
 try {
 int valor = 250 - random.nextInt(500);
 if (random.nextBoolean()) {
 cc.depositar(valor);
 } else {
 cc.sacar(valor * 5);
 }
 } catch (IllegalArgumentException e) {
 System.out.println("Não realizado:" + e.getMessage());
 }
 }
}
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.
Programação orientada a objetos I126
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Main {
 public void run() {
 Random random = new Random();
 ContaCorrente cc = new ContaCorrente();
 
 for (int i = 0; i < 10; i++) {
 try {
 int valor = 250 - random.nextInt(500);
 if (random.nextBoolean()) {
 cc.depositar(valor);
 } else {
 cc.sacar(valor * 5);
 }
 } catch (IllegalArgumentException | IllegalStateException e) {
 System.out.println("Não realizado: " + e.getMessage());
 }
 }
 }
 public static void main(String[] args) {
 try {
 new Main().run();
 } catch (Exception e) {
 System.out.println("Um erro inesperado ocorreu:");
 e.printStackTrace();
 }
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SaldoInsuficienteException extends Exception {
 private double valor;
 private double saldo;
public SaldoInsuficienteException(double saldo, double valor) {
 super("Saldo insuficiente Valor: " + valor + " Saldo: " + saldo);
}
 public double getValor() {
 return valor;
 }
 public double getSaldo() {
 return saldo;
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void sacar(double valor) {
 if (!aberta) {
 throw new IllegalStateException("Saque em conta fechada!");
 }
 if (valor < 0) {
 throw new IllegalArgumentException(
 "Saque negativo Valor: " + valor + " Saldo: " + saldo);
 }
 if (saldo - valor < 0) {
 throw new SaldoInsuficienteException(valor, saldo);
 }
 saldo -= valor;
 System.out.printf(" Saque: %2f Saldo: %2f%n", valor, saldo);
}
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, 
Programação orientada a objetos I128
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
ContaException
SaldoInsuficienteException
TransacaoInvalidaException
RunTimeException
BancoRunTimeException
ContaRunTimeException
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
2
3
4
5
6
7
8
9
10
11
public static Estante carregar(String arquivo) throws BDException {
 try {
 BancoDeDados bd = new BancoDeDados();
 Estante estante = new ContaCorrente();
 bd.abrir(arquivo);
 estante.livros = bd.lerArquivo();
 return cc;
 } finally {
 bd.fechar();
 }
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Estante carregar(String arquivo) throws BDException {
 try {
 BancoDeDados bd = new BancoDeDados();
 Estante estante = new Estante();
 bd.abrir(arquivo);
 estante.livros = bd.lerArquivo();
 return cc;
 } catch (BDException e) {
 System.out.println(e.getMessage());
 throw e;
 } finally {
 bd.fechar();
 }
}
Programação orientada a objetos I130
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
2
3
4
5
6
7
8
9
10
11
12
13
public static Estante carregar(String arquivo) throws EstanteException {
 try {
 BancoDeDados bd = new BancoDeDados();
 Estante estante = new Estante();
 bd.abrir(arquivo);
 estante.livros = bd.lerArquivo();
 return estante;
 } catch (BDException e) {
 throw new FalhaCarregarLivrosException(e);
 } finally {
 bd.fechar();
 }
}
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, quandoo 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.
Programação orientada a objetos I132
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Aluno {
 private int matricula;
 private String nome;
 private int idade;
 public Aluno(int matricula, String nome, int idade) {
 this.matricula = matricula;
 this.nome = nome;
 this.idade = idade;
 }
 public int getMatricula() {
 return matricula;
 }
 public String getNome() {
 return nome;
 }
 public int getIdade() {
 return idade;
 }
}
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
2
3
4
5
var numero = 2;
while (numero <= 20) {
 System.out.println(numero);
 numero += 2;
}
Resolução com for:
1
2
3
for (var numero = 2; numero <= 20; numero += 2) {
 System.out.println(numero);
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Medicamento {
 String nome;
 int lote;
 int quantidade = 0;
 Medicamento(String nome, int lote) {
 this.nome = nome;
 this.lote = lote;
 }
 void retirar(int quantidade) {
 this.quantidade -= quantidade;
 }
 boolean emFalta() {
 return quantidade <= 0;
 }
}
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
2
3
4
5
6
7
8
9
10
public class Cliente {
 static int ultimoId;
 int id;
 Cliente() {
 ultimoId += 1;
 this.id = ultimoId;
 }
}
Gabarito 135
Teste o código com:
1
2
3
4
Cliente c1 = new Cliente();
Cliente c2 = new Cliente();
System.out.println(c1.id); //Imprime 1
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
2
3
4
5
6
7
8
String v1 = "";
String v2 = null;
//Ok: O texto dessa variável tem 0 caracteres
System.out.println(v1.length());
//Erro: Sem nenhum texto associado, não podemos perguntar o tamanho.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Ponto {
 double x;
 double y;
 Ponto(double x, double y) {
 this.x = x;
 this.y = y;
 }
 static Ponto porAnguloDistancia(float angulo, float distancia) {
 return new Ponto(Math.cos(angulo) * distancia,
 Math.sin(angulo) * distancia);
 }
}
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
2
3
public Turma(Aluno[] alunos) {
 this.alunos = alunos;
}
Ele permitiria um uso do tipo:
1
2
3
4
var alunos = new Aluno[20];
var problema = new Turma(alunos);
//Sem usar o método matricular!
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 Main que imprimia a 
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
2
3
4
5
Aluno aluno1 = new Aluno(1234, "Alice");
Aluno aluno2 = new Aluno(5555, "Bruno");
Aluno aluno3 = aluno1;
aluno1 = aluno2;
System.out.println(aluno3.getNome());
Após executar a linha 1 até 3, temos a seguinte situação:
Matrícula: 5555
Nome: Bruno
Matrícula: 1234
Nome: Alice
aluno 3
aluno 1
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: 5555
Nome: Bruno
Matrícula: 1234
Nome: Alice
aluno 3
aluno 1
aluno 2
Ou seja, alteramos o valor do local para onde aluno1 aponta, mas não o seu conteúdo. Por 
isso, ao imprimiro nome de aluno3, ainda imprimiremos Alice, e não Bruno.
5. Classe Planeta ajustada:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package astronomia;
public class Planeta {
 private final static double PI = 3.1415;
 //Atributos
 private String nome;
 private int diametro;
 private double massa;
 //Construtores
 public Planeta(String nome, int diametro, double massa) {
 this.nome = nome;
 this.diametro = diametro;
 this.massa = massa;
 }
 public Planeta() {
 this("", 0, 0.0);
 }
 public static String descricao() {
 return "Um corpo celeste esférico que orbita uma estrela";
 }
 //Getters e setters
 public void setNome(String nome) {
 this.nome = nome;
 }
(Continua)
138 Programação orientada a objetos I
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 public String getNome() {
 return nome;
 }
 public double getMassa() {
 return massa;
 }
 public void setMassa(double massa) {
 this.massa = massa;
 }
 public int getDiametro() {
 return diametro;
 }
 public void setDiametro(int diametro) {
 this.diametro = diametro;
 }
 //Métodos
 public double raio() {
 return diametro / 2.0;
 }
 public double areaSuperficie() {
 var raioAoQuadrado = raio() * raio();
 return 4 * PI * raioAoQuadrado;
 }
 public void imprimir() {
 System.out.println("Nome: " + nome);
 System.out.println("Diâmetro: " + diametro);
 System.out.println("Massa: " + massa);
 System.out.println("Raio: " + raio());
 System.out.println("Area da superfície:" + areaSuperficie());
 }
}
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
A
B C
D
E
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
2
3
4
5
6
7
package carros;
public class Motor {
 private double potencia;
 private int tipoCombustivel;
 
private int valvulas;
}
Classe do veículo:
1
2
3
4
5
6
7
8
package carros;
public class Veiculo {
 private String placa;
 private String chassi;
 
private Motor motor; //Composição, "tem um"
}
140 Programação orientada a objetos I
Classe do carro:
1
2
3
4
5
package carros;
public class Carro extends Veiculo {
 private int portas;
}
Classe da moto
1
2
3
4
5
package carros;
public class Moto extends Veiculo {
 private int cilindradas;
}
3. Podemos definir uma interface com a operação:
1
2
3
public interface Operacao {
 public int aplicar(int valor);
}
E então escrever nosso método:
1
2
3
4
5
6
7
public int[] processar(int[] elementos, Operacao operacao) {
 var resultado = new int[elementos.length];
 for (var i = 0; i < elementos.length; i++) {
 resultado[i] = operacao.aplicar(elementos[i]);
 }
 return resultado;
}
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
2
3
4
5
6
public class Vezes10 implements Operacao {
 @Override
 public int aplicar(int valor) {
 return valor * 10;
 }
}
E chamaríamos o método da seguinte forma:
1
2
var valores = new int[] {1,2,3,4,5};
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
2
3
4
5
6
processar(valores, new Operacao() {
 @Override
 public int aplicar(int valor) {
 return valor * 10;
 }
});
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
2
3
4
5
public class BancoException extends Exception {
 public BancoException(String message) {
 super(message);
 }
}
1
2
3
4
5
6
public class ContaException extends BancoException {
 public ContaException(String msg, double valor, double saldo) {
 super(String.format("%s Valor: %.2f, Saldo: %.2f",
 msg, valor, saldo));
 }
}
1
2
3
4
5
public class SaldoInsuficienteException extends ContaException {
 public SaldoInsuficienteException(double valor, double saldo) {
 super("Saldo insuficiente", valor, saldo);
 }
}
1
2
3
4
5
public class BancoRuntimeExceptionextends RuntimeException {
 public BancoRuntimeException(String message) {
 super(message);
 }
}
1
2
3
4
5
6
7
public class ContaRuntimeException extends BancoRuntimeException {
 public ContaRuntimeException(String msg, double valor, double saldo) {
 super(String.format(
 "%s Valor: %.2f, Saldo: %.2f", msg, valor, saldo));
 }
}
1
2
3
4
5
6
public class TransacaoInvalidaException extends ContaRuntimeException {
 public TransacaoInvalidaException(String msg, double valor,
 double saldo) {
 super(msg, valor, saldo);
 }
}
144 Programação orientada a objetos I
1
2
3
4
5
public class ContaFechadaException extends ContaRuntimeException {
 public TransacaoInvalidaException(double valor, double saldo) {
 super("Conta fechada", valor, saldo);
 }
}
Classe da conta corrente alterada:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ContaCorrente {
 private double saldo = 1000;
 private boolean aberta = true;
public void depositar(double valor) {
 if (!aberta) {
 throw new ContaFechadaException(valor, saldo);
 }
 
 if (valor < 0) {
 throw new TransacaoInvalidaException("Depósito", valor, saldo);
 }
 saldo += valor;
 }
 
public void sacar(double valor) throws SaldoInsuficienteException {
 if (!aberta) {
 throw new ContaFechadaException(valor, saldo);
 }
 if (valor < 0) {
 throw new TransacaoInvalidaException("Saque", valor, saldo);
 }
 if (saldo - valor < 0) {
 throw new SaldoInsuficienteException(valor, saldo);
 }
 saldo -= valor;
 }
 
 public double getSaldo() {
 return saldo;
 }
 public void fechar() {
 this.aberta = false;
 }
}
Gabarito 145
Programa principal utilizando as classes:
1
2
3
4
5
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.Random;
public class Main {
 public void run() {
 Random random = new Random();
 ContaCorrente cc = new ContaCorrente();
 
 for (int i = 0; i < 10; i++) {
 try {
 int valor = 250 - random.nextInt(500);
 if (random.nextBoolean()) {
 cc.depositar(valor);
 } else {
 cc.sacar(valor * 5);
 }
 } catch (ContaRuntimeException | ContaException e) {
 System.out.println("Não realizado: " + e.getMessage());
 }
 }
 }
 public static void main(String[] args) {
 try {
 new Main().run();
 } catch (Exception e) {
 System.out.println("Um erro inesperado ocorreu:");
 e.printStackTrace();
 }
 }
}
3. Bastaria adicionar exceções ao construtor da classe Aluno:
1
2
3
4
5
5
6
7
8
9
10
11
12
public Aluno(int matricula, String nome, int idade) {
 if (matricula < 0) {
 throw new IllegalArgumentException("Matrícula inválida!");
 }
 
 if (nome == null || nome.isBlank()) {
 throw new IllegalArgumentException("O nome é obrigatório!");
 }
 if (idade < 3 || idade > 20) {
 throw new IllegalArgumentException("Idade inválida!");
 }
(Continua)
146 Programação orientada a objetos I
13
14
15
16
 this.matricula = matricula;
 this.nome = nome;
 this.idade = idade;
 }
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
VINÍCIUS GODOY
Código Logístico
58878
ISBN 978-85-387-6531-8
9 7 8 8 5 3 8 7 6 5 3 1 8
	Página em branco
	Página em branco

Mais conteúdos dessa disciplina