UENP - UNIVERSIDADE ESTADUAL DO NORTE DO PARANÁ CAMPUS LUIZ MENEGHEL Centro de Ciências Tecnológicas Programação II Notas de Aula Prof. José Reinaldo Merlin Bandeirantes – 2017 Sumário 0 1 2 3 Introdução 0.1 Ementa . . . . . . . . . . . . . . . . . . . . . . . . . . . . 0.2 Bibliografia . . . . . . . . . . . . . . . . . . . . . . . . . . 0.3 Livros recomendados . . . . . . . . . . . . . . . . . . . . . 0.4 Orientação a Objetos e Java . . . . . . . . . . . . . . . . . . 0.4.1 História da linguagem Java . . . . . . . . . . . . . . 0.4.2 Plataformas Java . . . . . . . . . . . . . . . . . . . 0.4.3 Máquina Virtual Java (JVM - Java Virtual Machine) 0.4.4 O que vamos precisar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 0 0 0 0 0 1 1 1 1 Orientação a Objetos 1.1 Classes e objetos . . . . . . 1.1.1 Atributos e métodos 1.1.2 Herança . . . . . . . 1.2 Identificando classes . . . . 1.3 Considerações finais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 4 5 5 Classes e Objetos em Java 2.1 Estrutura de uma classe . . . . . . . . . . . . . 2.1.1 Atributos . . . . . . . . . . . . . . . . 2.1.2 Métodos . . . . . . . . . . . . . . . . 2.1.3 Métodos sobrecarregados (overloaded) 2.2 Instanciando objetos . . . . . . . . . . . . . . 2.2.1 Construtores . . . . . . . . . . . . . . 2.2.2 Sobrecarga de construtores . . . . . . . 2.3 Ciclo de vida de um objeto . . . . . . . . . . . 2.4 Classes wrappers . . . . . . . . . . . . . . . . 2.5 Exercício . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 7 7 8 8 9 10 11 12 Modificadores e Encapsulamento 3.1 Pacotes (Packages) . . . . . . . . 3.1.1 Instrução import . . . . . 3.2 Modificadores de acesso . . . . . 3.2.1 Modificador public . . . . 3.2.2 Modificador private . . . 3.2.3 Modificador protected . . 3.2.4 Acesso default (de pacote) 3.3 Modificadores final e static . . . . 3.3.1 Modificador final . . . . . 3.3.2 Modificador static . . . . 3.4 Encapsulamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 14 15 16 16 16 17 17 17 17 18 19 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 4 Getters e setters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 22 23 24 5 Classes Abstratas e Interfaces 5.1 Classes abstratas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 25 26 6 Programação Genérica 6.1 Definindo uma classe genérica simples 6.2 Métodos genéricos . . . . . . . . . . 6.3 Limites para variáveis de tipo . . . . . 6.4 A classe ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 28 29 29 30 Exceções 7.1 Um exemplo: divisão por zero . . . . 7.2 Tratamento de exceções . . . . . . . . 7.3 Princípios do tratamento de exceções . 7.3.1 Tipos de exceções . . . . . . 7.4 Criando as próprias classes de exceção 7.5 Instrução try-with-resources . . . . . 7.6 Múltiplos catch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 32 32 33 34 34 35 36 7 Herança 4.1 Implementando herança . . . . . . . 4.2 Atributos e métodos sobrescritos . . 4.3 Polimorfismo e vinculação dinâmica 4.4 A classe Object . . . . . . . . . . 19 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Referências Bibliográficas 37 A Entrada e Saída em Java A.1 Entrada de dados via teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.2 Saída de dados via monitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 38 38 B Arrays em Java B.1 Arrays unidimensionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2 Arrays multidimensionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 40 41 C Classes String e StringBuilder C.1 Criando e manipulando Strings . . C.1.1 Concatenação . . . . . . . C.1.2 Imutabilidade . . . . . . . C.1.3 Métodos da classe String . C.2 StringBuilder . . . . . . . . . . . 42 42 42 43 43 44 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Capítulo 0 Introdução O objetivo desta disciplina é introduzir o estudo de programação orientada a objetos. 0.1 Ementa Estudo do paradigma orientado a objetos e tópicos relacionados. Ambientes de Desenvolvimento. 0.2 Bibliografia Bibliografia da disciplina: Deitel, D. Java, Como Programar. Porto Alegre: Bookman, 2001. Sierra, K; Bates, B. Use a cabeça!Java. Alta Books, 2005. Deitel, D. C++ Como Programar. Porto Alegre: Bookman, 2001. 0.3 Livros recomendados Estes são alguns dos livros úteis para a disciplina. Gupta, Mala. OCA Java SE 7 Programmer I Certification Guide: Prepare for 1ZO-803 Exam. Shelter Island: Manning, 2013. Pressman, R. S. Engenharia de Software: uma abordagem profissional. Porto Alegre: McGraw Hill, 2011. (Apêndice 2: Conceitos Orientados a Objetos). 0.4 Orientação a Objetos e Java Orientação a objetos é um paradigma de programação. Um paradigma é um “jeito de pensar” a estruturação dos programas. Existem diversas linguagens que podem ser utilizadas para programação orientada a objetos, tais como PHP, C#, Java, Python, entre outras. Neste curso será utilizada a linguagem Java, no entanto, os conceitos trabalhados são válidos para qualquer linguagem orientada a objetos. 0 0.4.1 História da linguagem Java Java foi desenvolvida pela Sun Microsystems em 1991, com a finalidade de programar dispositivos eletrônicos inteligentes, tais como cafeteiras e controladores de TV a cabo. O projeto não obteve sucesso na época, no entanto, tomou um rumo diferente. Em 1993 a web começou a se tornar popular e a Sun resolver focar este segmento. Isto deu um impulso à linguagem tornando–a uma das mais utilizadas no mundo. Em 2009, a Sun foi comprada pela Oracle, sendo esta a atual proprietária do Java. 0.4.2 Plataformas Java Java pode ser executada em diversos tipos de dispositivos, desde computadores de grande porte até celulares. Existem duas plataformas principais: Java Plataform, Standard Edition: contém o ambiente necessário para criação de aplicações Java. Java Plataforma, Enterprise Edition: inclui capacidades avançadas de programação, como desenvolvimento distribuído, programação para web, manipulação de banco de dados e várias outras. Além disso, existem outras plataformas, como Java ME (celulares), Java Card (cartões com chip) , Java TV... 0.4.3 Máquina Virtual Java (JVM - Java Virtual Machine) Tradicionalmente, os programas devem ser escritos e compilados para um sistema operacional específico. Um programa escrito em C para Windows, por exemplo, deve ser compilado para Windows. O binário deste programa não pode ser executado em Linux, o código fonte teria que ser compilado novamente para Linux e, o que é pior, possivelmente algumas bibliotecas não são compatíveis. Java foi concebida para que os programas possam ser executados em vários sistemas operacionais sem necessidade de mudança. Ao invés de compilar para binário, o compilador Java produz um código intermediário chamado de bytecode. Este bytecode pode ser executado na máquina virtual específica de cada sistema operacional, como ilustrado na Figura 1. 0.4.4 O que vamos precisar Para acompanhar esta disciplina é necessário ter instalado: • JDK/JRE: Java Development Kit/ Java Runtime Environment • JEE: Java Enterprise Edition • Apache Tomcat • NetBenas A forma mais fácil de ter tudo funcionando é instalar o NetBeans com tudo integrado, inclusive o servidor Tomcat. 1 Figura 1: Máquina Virtual Java. 2 Capítulo 1 Orientação a Objetos Orientação a objetos é uma paradigma de desenvolvimento. Bom, mas isto não explica muita coisa. O objetivo deste capítulo é introduzir os conceitos deste paradigma, especialmente Classe e Objeto. O conceito de orientação a objetos é bastante antigo. Simula 67 (1967) geralmente é considerada a primeira linguagem que apresenta características deste paradigma. Já os mais puritanos consideram que Smalltalk (1972) é a primeira linguagem genuinamente orientada a objetos. A orientação a objetos se tornou popular durante as décadas de 1980 e 1990. Nesta disciplina será utilizada a linguagem Java, mas lembre-se: o objetivo não é aprender a linguagem, e sim o paradigma. 1.1 Classes e objetos Para começar a entender os conceitos de classe e objeto considere o jogo Robocode 1 (Figura 1.1), um jogo de programação cujo objetivo é programar um tanque robótico de batalha para competir com outros robôs em uma arena 2 . Durante o jogo, os programadores não interagem com os robôs, eles agem de acordo com sua programação anterior que pode, inclusive, incorporar conceitos de inteligência artificial. Figura 1.1: Tela do jogo Robocode. No jogo, os personagens principais andam pela arena e atiram contra outros competidores. Podemos dizer que no jogo existe a “categoria” robô, com características e comportamentos comuns a todo “ser” deste tipo. Em orientação a objetos, chamamos esta “categoria” de classe. Na definição de Horstmann 1 2 Download em http://http://robocode.sourceforge.net/ http://robocode.sourceforge.net/docs/ReadMe.html 3 e Cornell [5] , uma classe é “o modelo ou esquema a partir do qual os objetos são criados”. Assim, a classe Robô é uma estrutura que permite a criação de muitos objetos robôs, todos com as mesmas características e comportamentos. A criação de um objeto é chamada de instanciação (criação de uma instância). Classes existem em tempo de compilação. Em tempo de execução, o que existem são objetos executando ações e trocando mensagens entre si. Em programação orientada a objetos (POO), um programa é um conjunto de classes. O desenvolvimento de um programa é feito por meio da utilização de classes existentes e da criação de classes específicas. A biblioteca padrão linguagem Java já fornece milhares de classes prontas para utilização. Por exemplo, a classe GregorianCalendar, que traz um conjunto de funcionalidades para manipulação de datas. O programador não precisa saber como esta classe está implementada, basta saber como utilizá–la (para isso, basta consultar a documentação). 1.1.1 Atributos e métodos Em POO, classes definem atributos e métodos. Atributos são características que os objetos da classe terão. Por exemplo, a classe Robô poderia definir um atributo numeroDeVidas. Métodos são comportamentos que os objetos poderão ter (ou operações que o objeto realiza). A classe Robô tem o método atirar. Todos os objetos instanciados a partir da classe Robô terão o atributo numeroDeVidas e o método atirar. Um objeto possui um estado, que é o conjunto de valores que seus atributos possuem em determinado instante. O estado do objeto é mutável e, geralmente, é alterado pelos próprios métodos do objeto. Um objeto da classe Robô poderia ter um método morrer, que diminuiria o numeroDeVidas. 1.1.2 Herança Um característica específica da POO é a herança. Herança é a possibilidade de se construir classes derivadas de outras já existentes, reaproveitando todos os atributos e métodos sem necessidade de reescrevê– los. Claro que as classes derivadas adicionam ou modificam atributos e comportamentos, pois de outra forma não haveria utilidade em se criar uma classe derivada de outra. Voltando ao nosso jogo, os robôs não são construídos do zero, eles são derivados de uma classe base Robot. Esta classe tem os atributos e comportamentos básicos de todo robô. No exemplo mostrado na Figura 1.1, há dois tipos de robôs: Crazy e Fire. Esses robôs tem algumas características comuns, por exemplo, a cor e vivo (true ou false). Também possuem comportamentos comuns, como andar e atirar. No entanto, a forma como um Crazy se move é diferente da forma como Fire se move. No contexto da POO, poderíamos ter uma classe Robô que teria os atributos comuns (cor e vivo) e o método comum (atirar). A classe Fire seria herdeira de Robô e teria o método andar básico. A classe Crazy também seria herdeira de Robô e teria o método andar implementado de forma diferente da classe Fire. Em POO, chamamos a classe Robô de superclasse, classe mãe ou classe base e as classes Crazy e Fire de subclasses, classes filhas ou classes derivadas. Diz–se, também que Robô é uma generalização de Crazy e Fire e que Crazy e Fire são especializações de Robô. Nos próximos capítulos estes conceitos serão detalhados. 4 1.2 Identificando classes Um método simples para identificar classes é localizar substantivos na análise do problema. Métodos são identificados a partir de verbos. Se fôssemos descrever textualmente nosso jogo, poderíamos dizer que ... O jogo possui dois ou mais robôs que se movem pela arena escaneando a presença de inimigos. Quando localiza o inimigo, atira contra ele. Quando um robô bate na parede, ele gira para caminhar em outra direção. Substantivos: arena, robôs, parede... Verbos: andar, escanear, atirar, girar... Naturalmente, o projetista deve utilizar sua experiência para decidir quais substantivos e verbos são importantes para criar suas classes [5]. 1.3 Considerações finais Neste capítulo foram apresentados, de maneira introdutória, alguns conceitos de orientação a objetos. Nos próximos capítulos, os principais conceitos serão vistos com mais detalhes. 5 Capítulo 2 Classes e Objetos em Java Neste capítulo será mostrado como escrever classes em Java e instanciar objetos. Pressupõe–se que o leitor tenha conhecimentos básicos de programação em Linguagem C, tais como sobre variáveis, tipos, estruturas de seleção e repetição. 2.1 Estrutura de uma classe Em Java, todo código deve estar dentro de uma classe. A estrutura básica de uma classe é: public class Carro { // atributos // métodos } A definição de uma classe começa com a palavra reservada class seguida do nome da classe. Por padrão, o nome da classe se inicia com letra maiúscula. (Não se preocupe ainda com o modificador public). Observe a chave de abertura e de fechamento da classe. Uma classe em Java deve ser salva em um arquivo com o mesmo nome da classe seguido da extensão .java. 2.1.1 Atributos Atributos são declarados na forma <tipo> + <nome>. Um atributo pode ser de um tipo primitivo (int, boolean) bem como ser de tipo classe (String). Atributos geralmente são declarados logo após a chave de abertura, mas nada impede que você declare os atributos no final da classe. Por padrão, nomes de atributos são escritos em minúsculas. public class Carro { int velocidade; boolean motorLigado; String marca; // métodos } 6 Atributos também são chamados de variáveis de instância. Cada objeto possui suas próprias variáveis de instância. Se o valor de uma variável de instância de um objeto for alterado, em nada altera o valor das variáveis dos outros objetos. 2.1.2 Métodos Métodos definem comportamentos dos objetos. Geralmente, atuam sobre os próprios atributos do objeto, modificando seu estado. No exemplo a seguir são mostrados os métodos ligar( ), desligar( ), getMarca() e setVelocidade() da classe Carro: public class Carro { int velocidade; boolean motorLigado; String marca; void ligar(){ motorLigado = true; } void desligar(){ motorLigado = false; } String getMarca(){ return marca; } void setVelocidade(int velocidade){ this.velocidade = velocidade; } } Métodos podem receber parâmetros e retornar valor. O método getMarca() retorna o valor do atributo marca, enquanto que o método setVelocidade() recebe o novo valor para o atributo velocidade. Um comentário sobre a palavra reservada this. Observe que o nome do parâmetro (velocidade) é o mesmo do atributo. Para que não haja confusão (velocidade = velocidade ?), a palavra this identifica que o que vem depois do operador de ponto é o atributo da classe. 2.1.3 Métodos sobrecarregados (overloaded) Sobrecarga de método é uma facilidade oferecida pela linguagens orientadas a objetos. Consiste na escrita de dois ou mais métodos com o mesmo nome mas com assinaturas diferentes. Em outras palavras, a quantidade, a ordem ou o tipo dos parâmetros devem ser diferentes. Considere o exemplo a seguir, em que nossa classe Carro tem um método acelerar() sobrecarregado. O primeiro método não recebe parâmetros enquanto o segundo recebe um inteiro indicando quantas vezes o carro tem que acelerar. Qual método será executado será decidido em tempo de execução. Se o método for chamado sem parâmetros, o primeiro será executado; se for chamado com um inteiro, o segundo será executado. 7 public class Carro { // atributos e demais métodos... void acelerar(){ velocidade++; } void acelerar(int vezes){ for (int i=0; i < vezes, i++){ velocidade++; } } } 2.2 Instanciando objetos Uma classe, como já foi dito, é uma estrutura para se criar objetos. Para a execução da aplicação, objetos devem ser criados. A criação de objetos é feita com o comando new, na forma: Carro carro = new Carro(); Por padrão, nomes de objetos são escritos em minúsculas. Uma vez instanciado o objeto, o acesso aos atributos e métodos é feito por meio do operador de ponto 1 : carro.ligar(); //chama o método ligar() do objeto carro System.out.println(carro.motorLigado); // exibe o valor do atributo 2.2.1 Construtores Quando um objeto é instanciado por meio do operador new(), um método especial é chamado na classe. Este método é chamado de construtor. Alguns autores dizem que o construtor não é um método propriamente, mas vamos considerá–lo um “método especial”. É ele que se encarrega de criar um novo objeto e inicializar os campos. Existem dois tipos de construtores: default e criado pelo programador. O construtor default é criado pelo compilador quando o programador não cria um construtor para a classe. Se o programador escreve seu próprio construtor, o construtor default NÃO é criado. A classe Carro do nosso exemplo não tem um construtor. Isto significa que, quando um objeto é criado, o construtor padrão é chamado. Este construtor inicializa os tipos primitivos numéricos com 0 (zero), os tipos booleanos com false e os tipos objetos com null. Null pode ser entendido como um ponteiro apontado para lugar nenhum. XLembre–se: quando aparecer a mensagem “null pointer exception” é porque o programa tentou acessar um objeto não instanciado. Construtores são escritos, preferencialmente, no início da classe. Informalmente falando, eles servem para deixar o objeto no estado desejado no momento da criação. Como os construtores são chamados no momento de criação dos objetos, o programadores podem utilizá–los para associar valores padrão para as variáveis de instância. No exemplo da classe Carro, o programador pode desejar que os objetos sejam 1 Ver Apêndice A sobre entrada e saída em Java 8 criados com velocidade = 0, motorLigado = false e marca “em branco”. A maneira de se fazer isso é criando um construtor da seguinte forma: public class Carro { int velocidade; boolean motorLigado; String marca; Carro(){ velocidade = 0; motorLigado = false; marca = ""; } //demais métodos } Construtores tem o mesmo nome da classe e NÃO tem tipo de retorno, nem mesmo void. Os construtores retornam um objeto do tipo da classe da qual fazem parte. Se você especificar um tipo para um construtor ele deixa de ser um construtor e passa a ser um método comum. 2.2.2 Sobrecarga de construtores Construtores, assim como os demais métodos, podem ser sobrecarregados. Um construtor sobrecarregado é um construtor com uma lista de parâmetros diferente dos demais. Exemplo: public class Carro { int velocidade; boolean motorLigado; String marca; Carro(){ velocidade = 0; motorLigado = false; marca = ""; System.out.println("Chamou o construtor 1"); } Carro(int vel, boolean lig, String marc){ velocidade = vel; motorLigado = lig; marca = marc; System.out.println("Chamou o construtor 2"); } Carro(String marc){ velocidade = 0; motorLigado = false; marca = marc; System.out.println("Chamou o construtor 3"); } } 9 Mas, ao instanciar um objeto, qual construtor será chamado? Depende dos parâmetros passados no momento da chamada... Se a chamada for Carro c = new Carro(); será chamado o primeiro construtor, pois não foi passado nenhum argumento. Se a chamada for Carro c = new Carro(“Ford”); o construtor 3 será chamado e o argumento passado será atribuído à variável de instância fabricante. Uma classe pode ter quantos construtores forem necessários (naturalmente com uma lista de parâmetros diferentes para cada um). Será executado o construtor que “combinar” com a chamada. Por outro lado, a seguinte chamada ao construtor Carro c = new Carro(20, “Bazinga”); resultará em erro de compilação, pois não há um construtor que receba int, String. Construtores podem chamar outros construtores da mesma classe. O código a seguir é válido: ... Carro(){ this(0, false, ""); } Carro(int vel, boolean lig, String marc){ velocidade = vel; motorLigado = lig; marca = marc; } ... Um detalhe a ser observado é que, quando um construtor chama outro por meio da palavra reservada this, a chamada deve ser a primeira instrução no método. O código a seguir não compila: ... Carro(){ System.out.println("Executou o construtor 1"); this(0, false, ""); } ... 2.3 Ciclo de vida de um objeto Existe uma diferença entre objeto e variável de referência que muitas vezes não é percebida. A instrução Carro carro = new Carro(); declara uma variável chamada carro. O operador new cria uma objeto e associa este objeto à variável carro. Poderíamos, inclusive, dividir as instruções em duas: 10 Carro carro; carro = new Carro(); A declaração de uma variável é uma operação distinta da inicialização da variável. Para ilustrar este conceito, Gupta compara a declaração de variável ao nome de uma bebê ainda não nascido e o objeto com o bebê real [4] mostrado na Figura 2.1. Embora seja possível criar um objeto sem associá–lo à uma variável, isto só é feito em situações específicas que não serão tratadas por enquanto, pois o objeto ficaria inacessível. Figura 2.1: Diferença entre declarar uma variável e inicializar a variável. Fonte: [4]. Uma vez que o objeto foi criado, ele pode ser acessado por meio da variável de referência. Ele permanece acessível até que fique fora de escopo ou a seja atribuído null explicitamente à variável. Outra situação que deixa o objeto inacessível é quando outro objeto for atribuído à outra variável, como no exemplo a seguir. ... Carro c1 = new Carro(); Carro c2 = new Carro(); c2 = c1; ... (1) (2) (3) Em (1), o primeiro objeto foi criado e associado à variável c1. Em (2), o segundo objeto foi criado e atribuído à variável c2. Em (3), o primeiro objeto foi associado à variável c2. Agora o primeiro objeto não tem mais como ser acessado... (No exemplo, ao final, c1 e c2 referenciam o mesmo objeto). 2.4 Classes wrappers Em Java, podemos dividir os tipos de dados em tipos primitivos e classes. Wrappers são classes especiais que possuem métodos capazes de lidar com tipos primitivos. Existe uma classe para cada tipo primitivo. Grosso modo, a classe “guarda” o tipo primitivo (wrap, em inglês, significa “envelopar”). Na Tabela 2.1 são mostradas as classes wrapper correspondente a cada tipo primitivo. Classes wrapper são usadas quando é necessário tratar um tipo primitivo como objeto. Por exemplo, classes e métodos genéricos não aceitam métodos primitivos, no entanto, aceitam a classe wrapper correspondente. 11 Tabela 2.1: Classes wrapper correspondentes a cada tipo primitivo. Tipo Primitivo Classe wrapper boolean Boolean byte Byte char Character short Short long Long int Integer float Float double Double A forma de instanciar um objeto do tipo Integer é mostrada a seguir. Há dois construtores na classe: um que recebe um int e outro que recebe uma string. Integer integer1 = new Integer(1); Integer integer2 = new Integer("2"); Para se recuperar o tipo primitivo, chamamos o método intValue(): int i = integer1.intValue(); Autoboxing Autoboxing é o processo pelo qual um tipo primitivo é automaticamente encapsulado (boxed) no seu tipo wrapper equivalente. As duas linhas de código a seguir executam exatamente a mesma operação: Integer integer1 = new Integer(1); Integer integer2 = 1; Auto-unboxing Auto-unboxing é o processo pelo qual o valor de um objeto é automaticamente extraído do tipo wrapper. Basta atribuir o objeto ao tipo primitivo: int i = integer1; Aqui foram mostradas algumas características das classes wrapper. Para saber mais, consulte a documentação Java de cada classe. 2.5 Exercício Considere a descrição seguinte. A UENP possui vários veículos que são utilizados por várias pessoas. Pode ser que apareça uma multa para determinado veículo. É necessário saber quem foi o motorista que cometeu a infração. Carros, basicamente, possuem marca, modelo, cor e placa. Os usuários podem ser professores, agentes ou alunos. Pretende–se que, quando um carro for entregue, sejam registrados o motorista, data e hora 12 da retirada e quilometragem atual. Quando o carro for devolvido, devem ser registradas a data e hora da devolução bem como a quilometragem. O registro das informações é feito por funcionário administrador mediante login e senha. Analise a descrição e crie classes com atributos e métodos adequados para projetar um programa. 13 Capítulo 3 Modificadores e Encapsulamento Neste capítulo serão mostrados os diferentes modificadores que podem se aplicados a atributos, métodos e classes. Também será mostrado o conceito de encapsulamento. Inicialmente, trataremos de pacote, que tem relação com modificadores. 3.1 Pacotes (Packages) Todas classes Java são parte de um pacote. Se um pacote não for declarado, a classe fará parte do pacote default (padrão). Um pacote é uma “pasta” onde são mantidas uma ou mais classes relacionadas. Pacotes são importantes para organizarem a aplicação. Por padrão, o nome de pacote se inicia com letra minúscula. Se uma classe definir explicitamente um pacote, a declaração do pacote deve ser a primeira declaração no arquivo 1 . package carros; class Carro { // atributos // métodos } Também é possível criar pacotes dentro de pacotes. Suponha uma aplicação para gerenciamento de uma frota de veículos. Poderíamos ter o pacote veiculos e dentro deste pacote, os pacotes carros e caminhoes. O acesso a pacotes dentro de pacotes é feito por meio do operador de ponto. Veja o exemplo modificado. package veiculos.carros; public class Carro { // atributos // métodos } 1 Geralmente as organizações possuem suas regras para nomear pacotes. Exemplo: br.edu.uenp.aplicação.pacote 14 Não é possível ter duas classes com o mesmo nome dentro de um mesmo pacote. Por outro lado, se as classes estiverem em pacotes distintos, não há problema em terem o mesmo nome. 3.1.1 Instrução import Classes não “enxergam” outras classes que não estão no mesmo pacote. Considere as classes e respectivos pacotes apresentados na Figura 3.1. Figura 3.1: Exemplo de classes em pacotes distintos. Vamos imaginar que a classe Master queira instanciar um objeto da classe Carro. Como a classe Carro está em outro pacote, é necessário “incluir” esta classe por meio do comando import. package com; import veiculos.carro.Carro; // <-- import da classe Carro public class Master { void metodoX(){ Carro c = new Carro(); } } Caso queiramos utilizar mais de uma classe do pacote veiculos.carros, utilizamos o “*”: import vaiculos.carros.*; O comando anterior realiza o import de todas as classes do pacote carros. No entanto, o “*” não importa subpacotes, apenas classes. A instrução: import vaiculos.*; não é equivalente à anterior. É possível utilizar uma classe de outro pacote sem o comando import. Para isto, devemos utilizar o caminho completo, ou “nome completamente qualificado”, tecnicamente falando: 15 package com; public class Master { void metodoX(){ veiculos.carros.Carro c = new Carro(); } } // <-- sem import Esta opção é menos prática, mas pode ser necessária quando uma classe que tenha o mesmo nome de outra classe em outro pacote. 3.2 Modificadores de acesso Modificadores de acesso alteram a visibilidade de uma classe e seus membros dentro de um pacote ou em pacotes separados. Há quatro modificadores de acesso em Java: public, protected, private e “nenhum”. Este último ocorre quando o programador não especifica o modificador, então a classe ou membro assume o acesso default, também chamado de acesso de pacote (package access). A forma de aplicar um modificador é colocando a palavra correspondente antes da classe, método ou variável de instância: public class MinhaClasse { ... } public void meuMetodo () { ... } private String meuAtributo; Modificadores de acesso não são aplicados a variáveis locais ou a parâmetros de método. 3.2.1 Modificador public O modificador public, quando aplicado a uma classe, a torna acessível à qualquer classe no universo Java [4], quer estejam no mesmo pacote ou em pacotes diferentes, quer sejam classes derivadas ou classes sem qualquer relação. O modificador public aplicado a métodos permite acesso ao método em qualquer classe do universo Java. Em outras palavras, o método pode ser chamado por um objeto que esteja em qualquer lugar. Um atributo public pode ser acessado por qualquer classe. 3.2.2 Modificador private O modificador private pode ser aplicado à classes internas (classes definidas dentro de outras). Este modificador torna a classe acessível apenas à classe em que estão contidas. Um método definido como private é acessível apenas pela própria classe e não é herdado por classes derivadas. Um método private faz sentido quando, por exemplo, queremos um método que execute uma tarefa para outro método da própria classe. Assim, os métodos da própria classe, e apenas eles, chamam o método private quando precisarem. Um atributo private só pode ser acessado por métodos da própria classe. No caso de classes derivadas, os atributos private são herdados mas não são diretamente acessíveis. Eles podem ser acessados por métodos públicos da superclasse (que são herdados). 16 3.2.3 Modificador protected O modificador protected não é aplicado à classes. Quando o modificador protected é aplicado a método, faz com que o método seja acessível apenas no mesmo pacote ou por classes derivadas, mesmo que estejam em pacotes diferentes. Atributos protected seguem a mesma regra: são acessíveis pela classe, são herdados e acessíveis pela subclasse. 3.2.4 Acesso default (de pacote) O acesso de pacote (default), que ocorre quando não há nenhum modificador, permite que a classe seja acessível apenas às outras classes do mesmo pacote. Em outras palavras, estas classes não permitem import por outras classes. Lembre–se que classes no mesmo pacote não necessitam de import para serem utilizadas. Um método com acesso de pacote (default) não pode ser chamado por uma classe que esteja fora do pacote. Os atributos com acesso default são acessíveis apenas dentro do pacote em que as classes às quais pertencem estão. Mesmo as classes derivadas, se estiverem em pacotes diferentes, não podem acessar estes atributos. 3.3 Modificadores final e static Os modificadores de acesso controlam a visibilidade, enquanto os demais modificadores alteram as propriedades default das classes Java e seus membros [4]. Os principais modificadores são static, final e abstract. Este último será tratado no Capítulo 5. 3.3.1 Modificador final O modificador final modifica o comportamento default de classes, variáveis e métodos. Classes Uma classe final não pode ser estendida por outra (não pode haver herdeiras da classe). Para tornar a classe final, acrescentamos a palavra reservada final na definição da classe: public final class Carro {... } Variáveis Uma variável final não pode ter seu valor alterado. Em outras palavras, seu valor só pode ser atribuído uma única vez. Por padrão, escrevemos o nome das variáveis final inteiramente em maiúsculas. Observe o seguinte exemplo : public class Carro { final int VEL_MAX; 17 public Carro(){ VEL_MAX = 240; } } Métodos Um método final, por sua vez, não pode ser sobrescrito por uma classe derivada. Por outro lado, uma classe derivada pode redefinir o método (mesmo nome, assinatura diferente). Suponha que a classe Carro tenha o método: public void metodoX(){ ... } uma classe derivada CarroLuxo poderia ter o método: public void metodoX(int y){ ... 3.3.2 } Modificador static O modificador static pode ser aplicado a classes, variáveis e métodos. Variáveis Uma variável static é uma variável de classe e não de objeto. Em outras palavras, ela é compartilhada entre todas as instâncias de uma classe. Ela existe e pode ser acessada mesmo que nenhum objeto da classe seja instanciado. Uma variável static pode ser acessada utilizando o nome da variável de referência ou o nome da classe. Os modificadores static e final podem ser usados para definir constantes. No exemplo a seguir, a classe Carro define uma constante VEL_MAX: public class Carro { public static final int VEL_MAX = 240; } Embora seja possível definir uma constante não–estática, é uma prática comum definir as constantes como membros estáticos. Assim, as constantes podem ser acessadas pela classe ou pelos objetos. Métodos Métodos static não estão associados a objetos e não podem acessar as variáveis de instância de uma classe (a não ser que a variável seja static). Um exemplo de método static é o método pow( ), que efetua a exponenciação e pode ser encontrado na classe Math: public static double pow(double a, double b) A forma de chamar o método é por meio da classe: double resultado = Math.pow( 5, 3); 18 Geralmente, métodos utilitários são static. Métodos utilitários são aqueles usados para manipular os parâmetros do métodos, realizar alguma computação e retornar um resultado, sem relação com um objeto específico. Embora métodos static possam ser chamados pela classe ou pelo objeto, o comum é chamar por meio da classe, uma vez que o método não pertence ao objeto. Classes Em Java, não é possível definir uma classe como static, a menos que ela seja uma classe interna. 3.4 Encapsulamento Encapsulamento é um princípio da orientação a objetos segundo o qual uma classe não deve expor suas partes internas para o mundo exterior [4]. Em outras palavras, os atributos de uma classe devem ser privados e a classe deve fornecer métodos públicos para acessar estes atributos. Uma classe também pode ter métodos encapsulados (private). Tomando como exemplo a classe Carro, vamos supor que um objeto execute a seguinte instrução: carro.velocidade = -10; Isto deixaria nosso objeto em um estado inválido, pois não queremos uma velocidade negativa. Uma forma de contornar isso e tornando o atributo velocidade privado e criando métodos públicos para acessálo: public class Carro { private int velocidade; public void setVelocidade(int vel){ if (vel >= 0 && vel < 240){ this.velocidade = vel; } public int getVelocidade(){ return this.velocidade; } } Desta forma, a funcionalidade do objeto é exposta utilizando métodos públicos, ao mesmo tempo em que as informações ficam protegidas. O conceito de encapsulamento, muitas vezes, é usado como sinônimo de ocultamento de informação (information hiding). Encapsulamento, a princípio, é a definição de variáveis e métodos juntos em uma classe. O ocultamento de informação é uma consequência do encapsulamento. 3.4.1 Getters e setters Um padrão utilizados pelos desenvolvedores Java é criar um método get+atributo para acesso às propriedades privadas da classe e um método set+atributo para modificar as propriedades privadas de uma 19 classe. No código do exemplo anterior, há a propriedade velocidade (private) e os métodos públicos getVelocidade() para obter o valor da propriedade e o método setVelocidade() para modificar este valor. 20 Capítulo 4 Herança Herança é um conceito fundamental em orientação a objetos. O mecanismo de herança permite criar novas classes com base nas classes existentes [5]. A classe “herdeira” reutiliza os métodos e atributos e adiciona novas propriedades e comportamentos. 4.1 Implementando herança Imagine uma aplicação que defina uma classe Carro, da seguinte forma: public class Carro { int velocidade; boolean motorLigado; String marca; public public public public void void void void ligar(){...} desligar(){...} acelerar(){...} frear(){...} } Agora considere que seja necessária a criação de outra classe, CarroLuxo, que tem o atributo adicional arCondicionado e os métodos ligarAr( ) e desligarAr( ). A nova classe difere pouco da classe Carro já existente. Então, por que não aproveitar o que já está pronto e testado? Obviamente, para classes muito simples a vantagem não é muito visível. A forma de se implementar a herança é criar a nova classe com a palavra reservada extends, como a seguir, em que CarroLuxo herda de Carro (construtores omitidos para simplificação). public class CarroLuxo extends Carro{ boolean arLigado; public void ligarAr(){...} public void desligarAr(){...} } 21 Ao herdar de uma classe, a classe derivada, também chamada de subclasse ou classe filha, possui todos os campos e métodos da classe base, também chamada de superclasse ou classe mãe, sem a necessidade de reescrevê–los. Agora, vamos imaginar que queremos criar uma nova classe, CarroInvisivel, que tem todas as características de CarroLuxo e mais a possibilidade de ficar invisível. Poderíamos fazer a nova classe herdar de CarroLuxo, da seguinte forma: public class CarroInvisivel extends CarroLuxo{ boolean estaInvisivel; public void ficarInvisivel(){...} public void ficarVisivel(){...} } Observe que CarroInvisivel especializa (extends) CarroLuxo, mas CarroLuxo é subclasse de Carro, portanto, CarroInvisivel tem todos os campos e comportamentos de Carro também, tais como ligarMotor() e acelerar(). XLembre–se: construtores não são herdados, mas eles podem ser chamados a partir da classe derivada. Uma regra para saber se uma classe deve ser projetada como subclasse ou uma classe independente é perguntar “A é um B? ”. No exemplo, CarroLuxo pode ser projetada como subclasse de Carro, pois um CarroLuxo é um Carro. 4.2 Atributos e métodos sobrescritos Quando uma classe derivada define um atributo com o mesmo nome da classe base, somente o novo atributo é visível na classe derivada. Quando a classe derivada define um método com o mesmo nome da classe base, naturalmente com corpo diferente, este método é chamado de sobrescrito (overridden). XLembre–se: sobrescrita (overridden) é diferente de sobrecarga (overloaded). Considere que a classe Carro tenha o método desligar(), que torna o motor = false. Vamos imaginar que o projetista da classe CarroLuxo queira, também, desligar o ar condicionado quando o motor for desligado. Agora, o método desligar( ) herdado já não serve mais ao nosso propósito. Devemos, então, sobrescrever o método desligar( ) da classe base na classe derivada. Nossas classes ficariam assim (demais métodos omitidos): 22 public class Carro { int velocidade; boolean motorLigado; String marca; public class CarroLuxo{ boolean arCondicionado; public void desligar(){ motorLigado = false; this.desligarAr(); } public void desligar motorLigado = false; } public void desligarAr(){ arCondicionado = false; } } } Observe que a classe CarroLuxo está acessando o atributo motorLigado que é definido na classe base. Uma alternativa é chamar o método da classe base para que ela execute a operação. Isto é especialmente necessário quando o atributo da classe base é private (ver capítulo sobre Modificadores). A palavra reservada super faz com que o método da superclasse seja chamado. Sempre que o comando super for usado, ele deve ser a primeira instrução do método. public class CarroLuxo{ boolean arCondicionado; public void desligar(){ super.desligar(); this.desligarAr(); } //chama o método da superclasse } 4.3 Polimorfismo e vinculação dinâmica Polimorfismo é um conceito muitas vezes confundido com sobrecarga. Polimorfismo é o fato de uma variável de referência poder se referir a múltiplos tipos reais. Selecionar automaticamente o método apropriado em tempo de execução é chamado de vinculação dinâmica [5]. Vamos por partes. Antes precisamos entender que o tipo da variável de referência pode ser diferente do tipo do objeto. A seguinte construção é válida Carro carro = new CarroLuxo(); pois CarroLuxo é um Carro. Naturalmente, o inverso não é possível. O que temos é uma variável do tipo Carro e um objeto do tipo CarroLuxo. Duas observações importantes: • Não podemos chamar o método ligarAr() na variável carro. • Se o método desligar() for chamado, será executado o método da classe filha, uma vez que o objeto é do tipo CarroLuxo. Considere o seguinte exemplo: 23 Carro[] frota = new Carro[3]; frota[0] = new Carro(); frota[1] = new CarroLuxo(); frota[2] = new Carro(); ... for (int i = 0; i < 3; i++){ frota[i].desligar(); } A vinculação dinâmica ocorre quando, em tempo de execução, há mais de um método possível de ser executado. No exemplo anterior, a chamada a frota[1].desligar() deixa 2 possibilidades: executar o método correspondente ao tipo da variável ou ao tipo do objeto. Neste caso, vai ser executado o método na subclasse, uma vez que ela tem o método correspondente. Se não tivesse, o método seria buscado na superclasse. 4.4 A classe Object Todas as classes, automaticamente, herdam da superclasse ancestral Object. Não é necessário a palavra extends para que a classe seja uma subclasse de Object. Por isso, o código seguinte é válido Carro carro = new Carro(); carro.toString(); Mesmo que nossa classe Carro não tenha o método toString(), ela herda de Object. Portanto, qualquer objeto, de qualquer tipo é um Object. Isto torna a construção a seguir possível, embora de discutível utilidade. Object[] lista[0] lista[1] lista[2] lista = new = new = new = new Object[3]; Carro(); Pessoa(); Abobora(); XLembre–se: tipos primitivos não são objetos, portanto não herdam de Object. 24 Capítulo 5 Classes Abstratas e Interfaces Classes abstratas são classes que, por algum motivo, não podem ser instanciadas. Interfaces não são classes, mas sim um conjunto de requisitos aos quais as classes precisam se adequar. Neste capítulo serão mostradas as diferenças entre esses dois tipos de estruturas. 5.1 Classes abstratas Algumas vezes as classes estão tão altas na hierarquia de herança que, na prática, não se criam instancias delas. Outras são genéricas a ponto de não ser possível instanciar objetos. Considere o exemplo mostrado na Figura 5.1. Figura 5.1: Exemplo de hierarquia de herança. O exemplo mostra uma hierarquia de herança muito simples, mas permite algumas análises. Não faz sentido instanciar objetos da classe Veiculo, uma vez que, no contexto da aplicação, só existem objetos do tipo Carro, Caminhao e Moto. Não faz sentido, também, implementar o método calcularIPVA() na classe Veiculo, pois como a maneira de calcular o IPVA é diferente em cada classe, todas as classes derivadas sobrescrevem o método. No entanto, o projetista da aplicação quer obrigar que todas as classes implementem este método. Por outro lado, todas as classes derivadas tem o atributo anoFabricacao. Nesta situação, pode–se criar a classe Veiculo como abstrata (abstract). Uma classe abstrata é uma classe que não pode ser instanciada, mas serve de base para criação de classes derivadas. No nosso exemplo, vamos definir o método calcularIPVA() como abstrato na classe Veiculo. 25 public abstract class Veiculo { private int anoFabricacao; public abstract double calcularIPVA(); public int getAnoFabricacao() { return anoFabricacao; } public void setAnoFabricacao(int anoFabricacao) { this.anoFabricacao = anoFabricacao; } } Uma classe abstract pode ter métodos abstract e métodos concretos. Métodos abstract não tem corpo. Uma classe que tem ao menos um método abstract é, obrigatoriamente, abstract. No entanto, uma classe abstract não precisa ter métodos abstract, pode ter somente métodos concretos. Neste caso, o que a torna abstract é o modificador antecedendo a palavra reservada class. Continuando com o exemplo, a classe Carro pode estender a classe Veiculo. Ao fazer isso, ela é obrigada a implementar todos os métodos abstract da classe base, caso contrário, Carro também se tornará uma classe abstract. public class Carro extends Veiculo { @Override public double calcularIPVA() { //corpo do método } } XLembre–se: uma variável nunca pode ser marcada como abstract. 5.2 Interfaces Interfaces são uma maneira de especificar o que as classes devem fazer sem dizer como devem fazer [5]. Interfaces não possuem variáveis de instância, mas podem ter constantes. Vamos a um exemplo. Suponha um jogo que tenha vários personagens (marcianos, terráqueos, cachorros, cavalos, bolas, grama). Suponha que o (objeto) jogo precise, em dados momentos, desenhar os objetos na tela. Acontece que cada objeto tem um jeito diferente de se desenhar. A solução é criar uma interface com o método desenhar( ) e obrigar todos as classes assumir esta interface. Desta forma, garante–se que todos os objetos presentes no jogo saibam desenhar a si mesmas. Para criar uma interface, ao invés da palavra class, usa–se a palavra interface. A nossa interface poderia ser definida da seguinte maneira: 26 public interface IJogavel { public void desenhar(); } Agora, podemos ter classes concretas que implementam a interface IJogavel. Estas classes, obrigatoriamente, devem implementar o método desenhar(), pois ele é abstrato na interface. Naturalmente, a classe pode ter outros métodos próprios. Uma classe Marciano, para implementar a interface IJogavel, utiliza a palavra reservada implements. public class Marciano implements IJogavel { @Override public void desenhar() { //corpo do método } //métodos próprios } Em Java, não é possível herança múltipla (uma classe herdar de duas classes base ao mesmo tempo), mas uma classe pode implementar mais de uma interface: public class Marciano implements IJogavel, IDesenhavel {...} Até a versão 7 de Java, interfaces não tinham métodos concretos. A partir da versão 8, passaram a poder ter métodos default e static. 27 Capítulo 6 Programação Genérica Programação genérica é uma maneira de escrever código que pode ser utilizado para objetos de muitos tipos diferentes [5]. Programadores de aplicativos escrevem pouco código genérico. No entanto, é importante conhecer sobre genéricos para utilizar as classes genéricas da linguagem. 6.1 Definindo uma classe genérica simples Considere o código não genérico a seguir: public class Caixa{ private Integer i; public Caixa(){ i = new Integer(0); } //gets e sets } A classe possui um atributo do tipo Integer. O que acontece se for necessário utilizar a classe com o tipo String? Teríamos que reescrever a classe... A programação genérica pode resolver este problema. Uma classe genérica é uma classe com uma ou diversas variáveis de tipo [5]. public class Caixa <T> { private T objeto; public Caixa(){} public Caixa(T objeto){ this.objeto = objeto } public T getObjeto() { return objeto; } 28 public void setObjeto(T objeto) { this.objeto = objeto; } } Pronto, agora a classe Caixa pode ser instanciada com qualquer tipo de objeto. A forma de fazer isso é, no momento da instanciação, definir qual o tipo de objeto será utilizado. Caixa<String> caixa1 = new Caixa<>(); Caixa<Carro> caixa2 = new Caixa<>(new Carro()); No exemplo, o objeto referenciado por caixa1 manipula um objeto do tipo String, uma vez que na definição da variável de referência estipulamos este tipo. Já caixa2 opera com objetos do tipo Carro. Em resumo, uma classe genérica pode ser instanciada com qualquer tipo, mas uma vez instanciada, só opera com aquele tipo. Seria um erro tentar a seguinte atribuição: caixa2.setObjeto("blábláblá"); 6.2 Métodos genéricos Podemos ter métodos genéricos em classes comuns. Para tornar um método genérico colocamos a variável de tipo (<T>) depois dos modificadores e antes do tipo de retorno. O método a seguir recebe um objeto de qualquer tipo e retorna uma String com o nome da classe do parâmetro: public static <T> String metodoGenerico(T t){ return t.getClass().getName(); } 6.3 Limites para variáveis de tipo Pelo que vimos até aqui, classes e métodos genéricos são construídos para operar com qualquer tipo de classe. Pode ser que, em alguns casos, seja necessário limitar o tipo de classe que nossa classe ou método genérico aceite. Isto é feitos introduzindo o limite para tipo: public class Coisa <T extends Serializable> { ... } Neste caso, T pode ser de qualquer classe, desde que esta classe implemente a interface Serializable. Detalhe: embora interfaces sejam implementadas, a palavra utilizada é extends. No caso de métodos, também podemos limitar os tipos aceitáveis: public static <T extends Number> Double getDobro(T num) { return num.doubleValue() * num.doubleValue(); } 29 O método anterior retorna o dobro do parâmetro recebido. Por razões óbvias, o método deve aceitar somente números. Assim, limitamos o tipo para classes derivadas de Number. Todas as classes wrapper ( BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short) são derivadas de Number. 6.4 A classe ArrayList A classe ArrayList é uma classe genérica muito utilizada. Ela oferece uma combinação entre arrays e a estrutura de dados tipo lista. As operações mais comuns com listas são: adicionar itens, modificar itens, excluir itens e percorrer a lista [4]. Ao contrário dos arrays que têm tamanho fixo, listas podem crescer de tamanho quando adicionamos itens e diminuir quando excluímos itens. Criando um ArrayList Para criar um ArrayList especifica–se o tipo de objeto que ela vai conter dentro dos colchetes angulares. Por exemplo, para criar uma lista de strings, a instrução é: ArrayList<String> listaNomes = new ArrayList<>(); Adicionando elementos à lista Há duas formas de se adicionar um elemento: no final ou especificando–se a posição. Chamar o método add() faz com que o objeto seja adicionado ao final da lista. Exemplo: listaNomes.add("Machado de Assis"); Se quisermos adicionar um objeto em uma posição específica, chamamos o método add() e passamos a posição, além do objeto a ser inserido. Por exemplo, para inserir na posição 1 (segundo elemento, pois o primeiro está na posição 0), a chamada é: listaNomes.add(1, “Rigby”); Neste caso, o elemento existente na posição e os seguintes serão “empurrados” para frente. Acessando elementos da lista Para percorrer uma lista podemos usar uma estrutura for (enhanced for): for (String s : listaNomes) { System.out.println(s); } Opcionalmente, podemos usar um ListIterator para percorrer a lista: ListIterator<String> i = listaNomes.listIterator(); while (i.hasNext()){ System.out.println(i.next()); } 30 As duas maneiras percorrem os elementos, do primeiro ao último, na ordem em que foram inseridos. A diferença é que usando ListIterator é possível remover algum elemento enquanto se percorre a lista. Substituindo elementos de um ArrayList Para substituir um elemento de uma ArrayList utiliza–se o método set(). Por exemplo, para substituir o primeiro elemento, a instrução é: listaNomes.set(0, “Mordecai”); Removendo elementos Para remover um elemento de uma posição específica, a instrução é: listaNomes.remove(pos); onde pos é o índice do elemento a ser removido. 31 Capítulo 7 Exceções Uma exceção é a indicação de que algum problema ocorreu durante a execução de um programa, impedindo a continuação normal. Tratar exceções é escrever algum código adicional que lide com essas situações previsíveis que podem ser gerenciadas. Logicamente, existem situações que não podem ser contornadas por meio de programação, como se o processador “queimar” durante a execução. Neste capítulo veremos como tratar as exceções em um programa Java. 7.1 Um exemplo: divisão por zero Como é sabido, não podemos ter divisão por zero em um programa. Mas o que acontece se, por exemplo, um usuário digitar 0 em uma entrada e ela for usada em uma divisão? Neste caso, ocorre um erro em tempo de execução e o programa termina de forma não desejada. Este é um exemplo de situação que pode ser tratada e, ao invés de o programa terminar, uma solução alternativa poderia ser oferecida. Considere o trecho de código a seguir, em que são inseridos dois números e efetuada a divisão do primeiro pelo segundo 1 . Scanner teclado = new Scanner(System.in); int dividendo = teclado.nextInt(); int divisor = teclado.nextInt(); int resultado = dividendo/divisor; System.out.println(resultado); Se o usuário digitar 0 para o dividendo, o programa será encerrado com a seguinte mensagem de erro: Exception in thread "main"java.lang.ArithmeticException: / by zero 7.2 Tratamento de exceções Em Java, uma exceção é um tipo de objeto “lançado” quando ocorre um erro. O objeto lançado pode ser capturado e tratado de maneira apropriada. A forma de tratar exceções é por meio de blocos try/catch. 1 Ver Apêndice A sobre a classe Scanner. 32 O código que pode gerar a exceção é escrito dentro do bloco try. Se a exceção ocorrer, ela será tratado dentro do bloco catch. Sintaxe: try { // código que pode gerar exceção } catch (tipo da exceção) { // código para tratar a exceção } Agora vamos reescrever o exemplo anterior, da divisão de dois números de forma a tratar a exceção. ... Scanner teclado = new Scanner(System.in); int dividendo, divisor, resultado=0; boolean ok = false; while (! ok){ try{ dividendo = teclado.nextInt(); divisor = teclado.nextInt(); resultado = dividendo/divisor; ok = true; } catch(Exception e){ System.out.println("Erro: divisão por zero."); } } System.out.println(resultado); Agora, caso seja digitado 0 para o valor do dividendo, o programa exibe uma mensagem e volta a solicitar os números. Em outras palavras, não será encerrado. (Ok, você jamais fará um código assim, mas este é apenas um exemplo para fins didáticos.) É importante ressaltar que a execução não retorna ao ponto em que a exceção foi disparada O tratamento de exceção funciona da seguinte maneira: quando ocorre um erro dentro do método, o método cria um objeto do tipo Exception. Este objeto contém informações sobre o tipo de erro e o estado do programa. A isto dá–se o nome de disparar uma exceção. Em um programa bem projetado, o código de tratamento de erros fica separado do código “normal”. 7.3 Princípios do tratamento de exceções O tratamento de exceções foi projetado para situações em que o método que detecta um erro é incapaz de lidar com ele [2]. Esse método dispara uma exceção. Se houver um método projetado para tratar esta exceção, ela será capturada e tratada. Para tratar a exceção, o programador inclui em um bloco try o código que pode gerar a exceção. O bloco try é seguido por um ou mais blocos catch. Cada catch especifica o tipo de exceção que pode capturar e que contém o tratador de exceção. Depois do último bloco catch, um bloco finally opcional 33 fornece o código que sempre é executado independentemente de uma exceção ocorrer ou não. O bloco finally é o lugar ideal para código que libera recursos. Se não houver blocos catch, o bloco finally é obrigatório [2]. Quando uma exceção é lançada, o controle do programa deixa o bloco try e os blocos catch são pesquisados para encontrar o bloco apropriado (aquele cujo parâmetro corresponde ao tipo de exceção lançada). Se nenhuma exceção for disparada, os blocos catch são pulados. Se o bloco finally existir, será executado sendo ou não disparada a exceção. A superclasse Exception pode ser considerada a “mãe” de todas as exceções. Por isso, muitas vezes, um parâmetro deste tipo é utilizado no último bloco catch. Considere o exemplo a seguir: int lista[] = new int[3]; try{ lista[3] = 30; } catch (ArrayIndexOutOfBoundsException ae){ System.out.println("Capturada no primeiro catch"); } catch (Exception e){ System.out.println("Capturada no segundo catch"); } O código tenta acessar uma posição do array que não existe. Quando a exceção é disparada, ela é capturada no primeiro bloco catch, pois o tipo lançado “bate” com o parâmetro. 7.3.1 Tipos de exceções Em Java, as exceções se dividem em verificadas (checked) e não verificadas (unchecked). O programa deve, obrigatoriamente, tratar as exceções verificadas. Já para as exceções não verificadas, o tratamento é opcional, como por exemplo ArrayIndexOutOfBoundsException. Neste último caso, o código que potencialmente gera a exceção não precisa estar dentro de um bloco try. O ideal é que este tipo de erro seja resolvido de outra maneira. 7.4 Criando as próprias classes de exceção Durante a construção de um programa, podemos utilizar as classes de exceção próprias da linguagem e, se necessário, criar nossas próprias classes de exceção, como no exemplo a seguir, que trata da divisão por zero. public class ExcecaoDivisaoPorZero extends Exception{ public ExcecaoDivisaoPorZero(String exc){ super("Exceção: tentativa de dividir por zero"); } } 34 Considere o caso de divisão por zero. Como vimos, não é possível realizar a divisão por um inteiro igual a zero. No entanto, se o denominador for do tipo double, a divisão é possível. Suponha que não queremos que isso aconteça. Podemos tratar isto no método que efetua a divisão: public class Calculadora { public double dividir(double a, double b) throws ExcecaoDivisaoPorZero{ if (b == 0){ throw new ExcecaoDivisaoPorZero(" dividendo: " + b); } return (a/b); } } Agora estamos tratando o caso do dividendo igual a zero e disparando a nossa exceção por meio do comando throw. As exceções criadas pelo programador devem ser do tipo verificadas. Nossa classe ExcecaoDivisaoPorZero, por herdar de Exception, automaticamente obriga que a exceção seja do tipo verificada. Agora observe que a classe Calculadora lança uma exceção no método dividir(). Este método deve declarar no cabeçalho os tipos de exceção que pode lançar por meio da cláusula throws. A cláusula throws transfere a responsabilidade pelo tratamento da exceção para a classe cliente do método. Sempre que alguma classe for utilizar o método, a chamada deve estar dentro de um bloco try, como a seguir: Calculadora c = new Calculadora(); try{ c.dividir(x, y); } catch (ExcecaoDivisaoPorZero exc){ System.out.println(exc.getMessage()); } Se a chamada ao método não estiver dentro de um bloco try, isto resulta em um erro de compilação. 7.5 Instrução try-with-resources Um recurso é um objeto que precisa ser fechado. A instrução try-with-resources assegura que todos os recursos sejam fechados ao final da utilização. Um arquivo é um exemplo de recurso, bem como todos os objetos que implementam a interface Closeable. No trecho de código a seguir, um arquivo é aberto e um objeto é gravado nele. Após a gravação o arquivo é fechado (out.close()). 35 public void escrever(Cachorro cachorro){ try { FileOutputStream fos = new FileOutputStream("cachorros.dat"); ObjectOutput out = new ObjectOutputStream(fos ); out.writeObject(cachorro); out.close(); // fechando o recurso } catch (IOException ex) { System.out.println("Falha em escrever no arquivo "+ ex); } } O trecho a seguir representa a mesma operação de escrita no arquivo, sendo que a instrução que abriu o recurso que precisa ser fechado foi colocada no try. Assim, o programa “sabe” que precisa fechar o recurso ao final, sem a necessidade de chamar explicitamente o método close(). try (ObjectOutput out = new ObjectOutputStream (new FileOutputStream("cachorros.dat"))) { out.writeObject(cachorro); } catch (IOException ex) { System.out.println("Falha em escrever no arquivo "+ ex); } 7.6 Múltiplos catch Muitas vezes somos “obrigados” a tratar mais de um tipo de exceção para um mesmo try, como no trecho de código a seguir, em que capturamos NumberFormatException e IllegalArgumentException em dois catch separados. try{ Integer i = Integer.parseInt(s); } catch(NumberFormatException e){ System.out.println("Entrada inválida para converter"); } catch(IllegalArgumentException e){ System.out.println("Entrada inválida para converter"); } Nos casos em que se quer dar o mesmo tratamento para mais de um tipo de exceção, pode–se usar múltiplos catch, como no trecho a seguir. Observe o caractere “|” entre os tipos de exceção. try{ Integer i = Integer.parseInt(s); } catch(NumberFormatException | IllegalArgumentException e){ System.out.println("Entrada inválida para converter"); } 36 Referências Bibliográficas [1] Rogers Cadenhead e Laura Lemay. Sams Teach Yourself Java 6 in 21 Days. Sams Publishing, Indianapolis, 2007. [2] H. M. Deitel e P. J. Deitel. JAVA Como Programar. Bookman, Porto Alegre, 3 edition, 2001. [3] Jeanne Boyarsky e Scott Selikoff. OCA: Oracle Certified Associate Java SE 8 Programmer I. Study Guyide. Exam 1Zo-808. John Wiley & Sons, Indianapolis, 2015. [4] Mala Gupta. OCA Java SE 7 Programmer I Certification Guide: Prepare for 1ZO-803 Exam. Shelter Island, Manning, 2013. [5] Cay S. Horstmann and Garry Cornell. Core Java, volume I: fundamentos. Pearson Education, São Paulo, 2011. 37 Apêndice A Entrada e Saída em Java Este apêndice não tem o objetivo de apresentar streams em Java, apenas mostrar de forma prática, como se realizam entradas e saídas de programas por meio dos dispositivos padrão (teclado e monitor). A.1 Entrada de dados via teclado A entrada de dados via teclado pode ser feita utilizando a classe Scanner. No trecho de código a seguir é mostrado como ler um inteiro e uma String em um programa: public class EntradaSaida{ 1 2 3 4 5 6 7 8 9 public static void main(String[] args) { Scanner teclado = new Scanner(System.in); int i = teclado.nextInt(); teclado.nextLine(); String nome = teclado.nextLine(); } } Na linha 4 um objeto java.util.Scanner é instanciado e atribuído à variável de referência teclado. Na linha 5, um inteiro é lido do teclado e atribuído à variável i por meio do método nextInt(). Na linha 7, uma cadeia de caracteres (String) é lida do teclado e atribuída à variável nome por meio do método nextLine(). Observe a linha 6 do programa que, aparentemente, não faz nada. Bom, ela é necessária sempre que um nextLine() vier depois de nextInt(). Explicando a grosso modo, pode–se dizer que toda entrada é uma cadeia de caracteres finalizada pelo delimitador (caractere de fim de linha). Acontece que o método nextInt() “pega” o inteiro e deixa o delimitador no buffer. Esse delimitador “deixado para trás” é consumido na linha 7. O que é feito na linha 6, na prática, é o descarte do delimitador. Consulte a documentação se precisar saber mais sobre a classe Scanner. A.2 Saída de dados via monitor A saída de dados pode ser feita por meio do método print() ou println() do objeto out da classe System: System.out.println(30); System.out.println(idade); System.out.println(nome); 1 2 3 38 System.out.println("Machado de Assis"); System.out.println("Nome: "+ nome); System.out.println("Idade: " + idade +" anos."); 4 5 6 O método println() é sobrecarregado cerca de 10 vezes. Em (1 e 4) exibe constantes, em (2), o conteúdo de uma variável inteira, em (3) o conteúdo de um objeto String. Em (5) e (6) há dois exemplos de concatenação. Em Java, qualquer coisa concatenada à uma cadeia de caracteres passa a ser uma cadeia de caracteres. 39 Apêndice B Arrays em Java Arrays são estruturas de dados capazes de armazenar uma lista de itens do mesmo tipo, sejam eles: • Tipos primitivos de dados. • Objetos da mesma classe. • Objetos que tenham classe base comum. Java implementa arrays de uma forma um pouco diferente de outras linguagens. Em Java eles são tratados como objetos. Para criar um array em Java, devemos [1]: • Declarar a variável para referenciar o array. • Criar um objeto do tipo array e atribuí–lo à variável. • Armazenar informação no array. B.1 Arrays unidimensionais Para declarar uma variável que referencia um array, devemos acrescentar um par de colchetes ([ ]) após o tipo ou após o nome da variável: int[] numeros; int pontos[]; String[] nomes; String cidades[]; Para criar o array, utiliza–se o operador new: int[] pontos = new int[10]; // array de 10 posições Ao se criar um array com o operador new, todas as posições serão automaticamente preenchidas com um valor inicial padrão. Este valor é 0 (zero) para arrays de números, false para arrays de booleans e null para arrays de objetos. Como arrays de objetos são preenchidos com uma referência nula, é preciso atribuir referências válidas antes de fazer uso deles. Por exemplo, um array de strings seria “preenchido” da seguinte forma: String[] nomes[0] nomes[1] nomes[2] nomes = new = new = new = new String[3]; String(); String(); String(); A chamada a new String() cria uma String vazia. Arrays também podem ser criados e inicializados ao mesmo tempo envolvendo os elementos em chaves, separados por vírgulas: 40 String[] nomes = {"Huguinho", "Luizinho", "Zezinho"}; Criar um array desta maneira faz com que ele tenha uma quantidade de elementos igual à quantidade de elementos declarados entre as chaves. A seguinte declaração e inicialização também é válida: String[] nomes = new String[] {"Huguinho", "Luizinho", "Zezinho"}; No entanto, o código seguinte não compila: String[] nomes = new String[3]{"Huguinho", "Luizinho", "Zezinho"}; B.2 Arrays multidimensionais A quantidade de dimensões de um array é Java é dada pela quantidade de colchetes presentes na declaração. Os comandos seguintes declaram arrays de duas dimensões: int[] arrayInteiros[]; int[] [] arrayInteiros; int arrayInteiros[] []; A alocação de memória para o array multidimensional é feita por meio do operador new: arrayInteiros = new int[2][3]; Embora seja possível criar arrays de mais de duas dimensões, o mais comum é se trabalhar com arrays de uma ou duas dimensões. 41 Apêndice C Classes String e StringBuilder Dados do tipo texto, em Java, são manipulados por objetos das classes String e StringBuilder. Essas classes possuem características específicas, sendo que algumas delas serão analisadas neste apêndice. C.1 Criando e manipulando Strings A classe String é fundamental em qualquer programa. Nem mesmo o método main() pode ser escrito sem se valer desta classe [3]. Uma string é basicamente uma sequência de caracteres, como por exemplo: String texto = “Bazinga”; No exemplo, String é o tipo, texto a variável de referência e Bazinga o objeto associado à variável. Certo, e o operador new necessário para se instanciar objetos? Bem, no caso da classe String, o operador new não é necessário. As formas a seguir são válidas: String texto = “Bazinga”; String texto = new String(“Bazinga”); C.1.1 Concatenação Concatenação é a operação de unir duas ou mais strings por meio do operador “+”. Este operador é sobrecarregado, pois 1 + 2 resulta 3, no entanto, “1” + “2” resulta “12”. O operador “+” pode ser usado de duas formas na mesma linha [3]: 1. Se ambos operandos são numéricos, “+” significa adição; 2. Se ao menos um operador é String, “+” significa concatenação; 3. A expressão é avaliada da esquerda para direita. Alguns exemplos: System.out.println(1 + System.out.println("a" System.out.println("a" System.out.println(1 + System.out.println("a" 2); + "b"); + "b" + 3); 2 + "a"); + 3 + 2 + "b"); 42 // // // // // 3 ab ab3 3a a32b C.1.2 Imutabilidade Uma vez que um objeto String é criado, ele se torna imutável (ele não pode ser alterado). Não podemos nem ao menos alterar uma simples letra do conteúdo. Considere o seguinte exemplo: String texto = "um "; texto = texto + "dois"; System.out.println(texto); // um dois Criamos uma string com o valor inicial “um ” e, em seguida, concatenamos com o texto “dois”. Fazendo isso, alteramos o conteúdo da string, certo? Errado. O que acontece é que Java trabalha com o conceito de string pool, para evitar strings duplicadas na memória durante a execução de um programa. Assim, quando criamos uma string na forma: String texto = “Bazinga”; a JVM 1 verifica se já existe alguma string “Bazinga” no pool. Se existir, ela faz a variável referenciar esta string. Se não existir, ela cria uma string “Bazinga” e faz a variável referenciar este novo objeto. Voltando ao exemplo, quando declaramos: String texto = “um ”; a JVM cria um objeto string com o conteúdo “um ” (se não existir). Em seguida, quando fazemos: texto = texto + “dois ”; a JVM cria um objeto string com o conteúdo “dois” (se não existir), depois cria outro objeto com o conteúdo “um dois” (se não existir), aí sim faz texto referenciar este terceiro objeto. Os dois objetos anteriores permanecem vivos no pool. É importante lembrar que a instrução: String texto = new String(“texto”); obriga a JVM a criar um novo objeto, existindo ou não outro objeto igual no pool. C.1.3 Métodos da classe String A classe String possui dezenas de métodos que não serão comentados aqui. Veja a documentação da classe para conhecê–los. Vamos analisar apenas os métodos equals() e equalsIgnoreCase(). equals() e equalsIgnoreCase() O operador de igualdade ( == ) não pode ser usado para comparar se duas strings são iguais. Ao invés deste operador, utiliza–se o método equals(), que retorna verdadeiro se forem iguais, ou falso, caso contrário. Exemplo: String s1 = "1"; String s2 = "2"; if (s1.equals(s2)){ System.out.println("São iguais"); } O método equalsIgnoreCase() é semelhante ao anterior, apenas ignora as diferenças entre maiúsculas e minúsculas que podem existir. 1 Java Virtual Machine 43 C.2 StringBuilder Ao contrário de objetos da classe String, objetos da classe StringBuilder são mutáveis. Isto quer dizer que podemos alterar um objeto (concatenar, substituir caracteres) e o objeto continuará sendo o mesmo objeto. Isto tem influência no desempenho do programa. A forma de instanciar um objeto StringBuilder é bastante similar à instanciação de um objeto String: StringBuilder sb1 = new StringBuilder(); StringBuilder sb2 = new StringBuilder("Bazinga"); Para objetos StringBuilder não se usa o operador de atribuição ( = ). Ao invés disso, utiliza–se o método append(): StringBuilder sb = new StringBuilder(); sb.append("Big"); sb.append("Bang"; System.out.println(sb); // Big Bang Um objeto StringBuilder pode ser convertido em String por meio do método toString(): String str = sb.toString(); A decisão sobre usar String ou StringBuilder depende do contexto do programa. 44