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" javalangIllegalArgumentException: Depósito negativo Valor: -980 Saldo: 22710 at brcap8ContaCorrentedepositar(ContaCorrentejava:12) at brcap8Mainrun(Mainjava:14) at brcap8Mainmain(Mainjava: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