Baixe o app para aproveitar ainda mais
Prévia do material em texto
INSTITUTO FEDERAL DE MATO GROSSO DO SUL Tecnologia em Sistemas para Internet Disciplina: Linguagem de Programação II Prof.º Msc. Sidney Roberto de Sousa Apostila – Linguagem de Programação II NOTA: Para a boa interpretação do conteúdo desta apostila é altamente recomendável que o aluno tenha concluído a leitura da apostila da disciplina Linguagem de Programação I, assim como a resolução de seus exercícios. Desta forma, o professor presume tais fatos sobre o aluno, ou ao menos que este tenha domínio sobre os assuntos abordados naquela apostila. 1 – Conteúdo • O que são classes e objetos? • Criando uma classe ◦ Criando atributos ◦ Criando métodos • Instanciando objetos • Matrizes de objetos • Construtores • Herança • Métodos estáticos • Classes com construtor privado • Tratamento de exceções • A palavra reservada finally • Criando exceções • Manipulação de arquivos • Leitura em arquivos textuais • Gravação em arquivos textuais • Gravação e leitura em arquivos binários 2 – O que são classes e objetos É bem possível que neste ponto de seus estudos você já tenha ouvido falar em “orientação a objetos”. Mas enfim, o que vem a ser orientação a objetos? A orientação a objetos é um paradigma de análise, projeto e programação de sistemas que visa a organização de componentes de software como objetos que interagem entre si. É incorreto dizer que a orientação a objetos é apenas um paradigma de programação, pois como já enfatizado anteriormente ela também pode ser aplicada nas fases de análise e projeto, conforme descrito abaixo: • Análise orientada a objetos: Tudo o que existe no mundo real pode ser classificado. Os filósofos antigos perceberam isto e criaram modelos como as ontologias para a classificação de entidades do mundo real. Por exemplo, analisando um supermercado, podemos levantar classes como Cliente, Produto, Atendente de Caixa, entre outras. A classe Cliente possui instâncias (os objetos), isto é, os clientes que frequentam o mercado. Assim, os clientes do mercado podem ser classificados por meio da classe Cliente. • Projeto orientado a objetos: Classes e objetos levantados na fase de análise podem ser modelados como componentes de software por meio de uma linguagem de modelagem orientada a objetos. A linguagem mais popular para tal finalidade é a UML (Unified Modeling Language). A UML possui diagramas para a modelagem de classes e objetos, além de seus comportamentos e suas interações entre si. • Programação orientada a objetos: A programação orientada a objetos é realizada utilizando-se uma linguagem orientada a objetos. No nosso caso, utilizaremos a linguagem Java para resolver problemas utilizando o paradigma de orientação a objetos. Nesta disciplina, deixaremos (não totalmente) de lado as fases de análise e projeto e focaremos na programação orientada a objetos. Assim, espera-se que no final deste curso você seja capaz de criar programas que utilizem o paradigma de orientação a objetos para resolver problemas diversos. 3 – Criando uma classe Na linguagem Java, uma classe pode ser criada por meio da palavra reservada class. O código abaixo cria uma classe Java chamada Cliente, situada no pacote br.edu.ifms.lp2: package br.edu.ifms.lp2; public class Cliente { } Note que o modificador public foi utilizado para definir que a classe Cliente é pública, isto é, pode ser utilizada por qualquer outra classe, esteja esta no mesmo pacote que a classe Cliente ou não. Os modificadores protected e private não são permitidos na definição de classes. Apesar disso, a omissão do modificador public na definição de uma classe a torna protegida, isto é, acessível apenas para as classes definidas dentro de seu pacote. Talvez você esteja se perguntando: é possível criar uma classe privada? Falaremos sobre isto mais tarde. Além do modificador public para definir o seu acesso, uma classe pode conter também os modificadores final ou abstract. Também falaremos sobre isto em um momento posterior! 3.1 – Criando atributos Os atributos de uma classe definem as características referentes aos objetos de uma classe. Por exemplo, um cliente possui um nome e um endereço. Dependendo do contexto, ele também pode possuir uma data de nascimento. Assim, podemos definir atributos para especificar tais características do cliente. O código abaixo modifica a classe Cliente, adicionando os atributos nome, endereco e dataNascimento: package br.edu.ifms.lp2; import java.util.Date; public class Cliente { private String nome; private String endereco; private Date dataNascimento; } Assim como os métodos, atributos também possuem modificadores de acesso. Se um atributo é definido como public (público), qualquer outro trecho de código – seja este de sua classe ou não - pode utilizá-lo. Se um atributo é definido como protected (protegido), apenas os trechos de código escritos em sua classe, nas classes que residem no mesmo pacote de sua classe ou que herdam a sua classe podem utilizá-lo. Por fim, se um atributo é definido como private (privado), apenas os trechos de código escritos em sua própria classe podem invocá-lo. A tabela abaixo resume estas informações. A omissão de um modificador de acesso torna o atributo protegido. Permissão de acesso Característica public Atributo visível por qualquer outra classe protected Atributo visível apenas por sua classe, pelas classes filhas de sua classe e pelas classes do mesmo pacote de sua classe private Atributo visível apenas por sua classe No nosso exemplo, os três atributos foram definidos como privados. Isto é uma prática comum na programação orientada a objetos, chamada de encapsulamento. Ao definir atributos como privados, estamos definindo que eles representam dados que dizem respeito apenas à própria classe; assim, eles não são visíveis diretamente a outras classes. 3.2 – Criando métodos Como dito anteriormente, o encapsulamento de atributos serve para proteger dados privados à classe. Este tipo de encapsulamento não é obrigatório, porém é uma prática bem aceita na comunidade de desenvolvimento de software. Porém, é extremamente comum que classes precisem consultar valores de atributos alheios entre si. Por exemplo, digamos que uma classe necessite acessar os dados dos atributos da classe Cliente. Devido ao fato de que tais atributos foram definidos como privados, eles não estarão acessíveis diretamente para a outra classe. Desta forma, precisamos criar métodos para fornecer acesso para a consulta de tais atributos. Além disso, é possível que a outra classe necessite modificar valores dos atributos da classe Cliente a fim de realizar alguma operação. Métodos que oferecem a consulta a atributos de uma classe são chamados de getters. Métodos que permitem a modificação de atributos são chamados de setters. A seguir, a classe Cliente com métodos getters e setters para o atributo nome: package br.edu.ifms.lp2; import java.util.Date; public class Cliente { private String nome; private String endereco; private Date dataNascimento; public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } } Agora, é possível que outras classes possam acessar e modificar o valor do atributo nome. Observe as nomenclaturas dos métodos. É uma convenção que um método getter seja nomeado com a palavra get seguida do nome do atributo ao qual o método se refere, utilizando o padrão Camel Case. Analogamente, é uma convenção que um método setter seja nomeado com a palavra set seguida do nome do atributo ao qual o método se refere, também utilizando o padrão Camel Case. Observe agora o método setNome. Em sua única linha de código podemos ver o uso da palavra reservada this: this.nome = nome; A palavra this (em português, “isto” ou “este”) é utilizada paracriar uma auto referência, isto é, this representa a própria classe Cliente. No caso, this.nome referencia o atributo nome da classe Cliente, enquanto que a variável nome (do outro lado da atribuição) é um parâmetro de entrada do método setNome. Obviamente, uma classe pode conter outros métodos que não sejam getters ou setters. Por exemplo, vamos adicionar um método na classe Cliente para imprimir os dados do cliente: package br.edu.ifms.lp2; import java.util.Date; public class Cliente { private String nome; private String endereco; private Date dataNascimento; public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public void imprimeDados() { System.out.println("Nome: " + nome); } } Para saber mais sobre métodos e suas características, por favor, leia as seções 14.1 e 14.2 da apostila da disciplina Linguagem de Programação I. Exercício: a) Estenda a classe Cliente, criando os métodos getter e setter para os atributos endereco e dataNascimento. 4 – Instanciando objetos Uma vez que a classe exista, é possível instanciar objetos dela. Instanciar um objeto de uma classe significa criar uma cópia de tal classe, onde o conjunto de atributos do objeto possuirá valores específicos a alguma necessidade e seu conjunto de métodos operarão de acordo com algum comportamento específico. Como exemplo, imagine que desejamos criar uma classe para preencher dados de clientes. Para tanto, utilizaremos a classe CadastroCliente, conforme o código abaixo: package br.edu.ifms.lp2; import java.util.Date; public class CadastroCliente { private Cliente cliente; public Cliente getCliente() { return cliente; } public void setCliente(String nome, String endereco, Date dataNascimento) { cliente = new Cliente(); cliente.setNome(nome); cliente.setEndereco(endereco); cliente.setDataNascimento(dataNascimento); } } Note que a classe CadastroCliente possui um único atributo, o qual é um objeto da classe Cliente. No momento em que o atributo cliente é definido na classe CadastroCliente ele apenas foi declarado e seu valor é null. Sua instanciação só ocorre de fato no instante em que a primeira linha do método setCliente é executada. Nesta linha, o construtor da classe Cliente é invocado para que seja criada uma cópia da classe e então esta cópia seja associada ao objeto cliente. Falaremos mais sobre construtores em uma seção posterior. Dentro do método setCliente o objeto cliente é instanciado e seus atributos são preenchidos indiretamente por meio de seus métodos setter, com os valores contidos nos parâmetros do método setCliente. Para mostrar as mudanças de valores no objeto cliente, utilizaremos a classe CadastroCliente para acessar e manipular seu valor. Observe o código da classe TesteCadastroCliente abaixo: package br.edu.ifms.lp2; import java.util.Date; public class TesteCadastroCliente { public static void main(String[] args) { CadastroCliente cadastro = new CadastroCliente(); // Imprime: null System.out.println(cadastro.getCliente()); cadastro.setCliente("Sidney Sousa", "Rua Cinco, Vila Ycarai", new Date()); // Imprime: br.edu.ifms.lp2.Cliente@5563d208 (pode variar) System.out.println(cadastro.getCliente()); } } Dentro do método main, o valor do objeto cliente é impresso em dois momentos distintos. Note que o acesso ao valor do objeto foi feito de forma indireta por meio de seu método getter. No primeiro momento em que o valor do objeto cliente é impresso o objeto ainda não havia sido instanciado. Assim, o valor null foi impresso, uma vez que de fato o valor do objeto é nulo neste instante. Porém, no segundo momento em que o valor do objeto cliente é impresso o objeto já havia sido instanciado - assim como os seus campos já haviam sido preenchidos. Toda vez que a impressão do valor de um objeto (lembre-se: do objeto e não de um ou mais de seus atributos) que já foi instanciado é solicitada, a máquina virtual do Java procura na classe do objeto a existência do seguinte método: public String toString() Caso este método exista, o valor retornado por este método é impresso. Caso contrário, o método toString da classe Object é invocado, o qual retorna o identificador do objeto. Isto acontece pois toda classe criada em Java é filha da classe Object, a qual faz parte do núcleo do Java. Agora, observe o código modificado da classe TesteCadastroCliente abaixo: package br.edu.ifms.lp2; public class TesteCadastroCliente { public static void main(String[] args) { CadastroCliente cadastro = new CadastroCliente(); Cliente cliente = cadastro.getCliente(); // A linha abaixo vai causar uma exceção (erro) quando executada System.out.println(cliente.getNome()); } } Como você já deve ter visto no comentário dentro do método main, a última linha de código do método causa uma exceção. Mas por que isto ocorre? O fato é que no instante em que o valor do objeto cliente é solicitado por meio de seu getter, ele ainda não havia sido instanciado dentro da classe CadastroCliente; assim, o seu valor é null. Nenhum objeto cujo valor seja null pode ter os seus atributos ou métodos acessados, pois isto executaria a exceção NullPointerException (Exceção de Ponteiro Nulo). Desta forma, lembre-se sempre que para que um objeto possa ter seus atributos e métodos acessíveis ele deve ser instanciado anteriormente! Exercícios: a) Sobrecarregue o método setCliente da classe CadastroCliente de tal forma que ao invés de receber os valores de nome, endereço e data de nascimento separadamente, ele receba um outro objeto da classe Cliente cujos valores dos atributos devem ser copiados. b) Inclua na classe CadastroCliente um método toString (conforme a assinatura explicada nesta seção). Ele deve retornar uma string contendo todos os valores dos atributos do objeto, formatados em uma única linha. 5 – Matrizes de objetos Da mesma forma que podemos criar matrizes de tipos primitivos, também podemos criar matrizes para armazenar objetos. A sintaxe para a criação de matrizes de objetos é similar à sintaxe para a criação de matrizes de valores primitivos. Analogamente, podemos percorrer uma matriz de objetos da mesma forma que percorremos uma matriz de valores primitivos: package br.edu.ifms.lp2; import java.util.Calendar; import java.util.Date; public class ExemploMatrizObjetos { public static void main(String[] args) { Cliente[] vetorClientes = new Cliente[10]; for (Cliente cliente : vetorClientes) { System.out.print(cliente + "\t"); } System.out.println("\n"); Cliente[][] matrizBidimensionalClientes = new Cliente[10][10]; for (Cliente[] clientes : matrizBidimensionalClientes) { for (Cliente cliente : clientes) { System.out.print(cliente + "\t"); } System.out.println(); } System.out.println(); Calendar calendario = Calendar.getInstance(); calendario.set(1985, 7, 22); Date dataNascimento = calendario.getTime(); Cliente cliente1 = new Cliente(); cliente1.setDataNascimento(dataNascimento); cliente1.setEndereco("Rua das Oliveiras, 22, Vila Perdizes"); cliente1.setNome("Joana Andrade Dutra"); Cliente cliente2 = new Cliente(); calendario.set(1976, 3, 7); dataNascimento = calendario.getTime(); cliente2.setDataNascimento(dataNascimento); cliente2.setEndereco("Av. das Bandeiras, 1542, Centro"); cliente2.setNome("Ubiratan Vieira"); vetorClientes = new Cliente[2]; vetorClientes[0] = cliente1; vetorClientes[1] = cliente2; for (Cliente cliente : vetorClientes) { System.out.print(cliente.getNome() + "\t"); } Cliente[] vetorClientes2 = { cliente1, cliente2 }; for (Cliente cliente : vetorClientes2) { System.out.print(cliente.getNome() + "\t"); } } } Exercícios: a) Escreva um programa que leia os dados de dez clientes, armazenando estes dados emuma matriz unidimensional do tipo Cliente. Após isto, o programa deve informar quantos clientes possuem nome iniciado com vogal. b) Escreva um programa que leia os dados de dez clientes, armazenando estes dados em uma matriz unidimensional do tipo Cliente. Apos isto, o programa deve informar quantos clientes são maiores de idade. c) Escreva um programa que leia um valor inteiro n e então leia uma matriz bidimensional n x n do tipo Cliente. Após isto, o programa deve exibir a matriz formatada de clientes e a matriz transposta formatada de clientes (exibir apenas o nome de cada cliente na matriz). d) (*) Escreva um programa que leia um valor inteiro n e então leia uma matriz unidimensional de clientes de tamanho n. Após isto, o programa deve informar qual é o número máximo de pessoas que possuem endereço em comum (ignorar casos quando realizar a verificação), além dos nomes e datas de nascimento de tais pessoas. 6 – Construtores Um construtor é um método cuja função é instanciar e inicializar o objeto. A implementação do construtor não é obrigatória. Por exemplo, a implementação da classe Cliente exibida anteriormente não possui um construtor. Mesmo assim, nós utilizamos o construtor da classe Cliente quando instanciamos seus objetos: Cliente cliente = new Cliente(); Neste caso, o objeto é instanciado e é alocado um espaço de memória para ele. Porém, os seus atributos são inicializados com valores default. Atributos de tipos primitivos são inicializados com os valores default de seus respectivos tipos. Por exemplo, atributos do tipo int, short e long são inicializados com o valor 0, enquanto que atributos do tipo float e double são inicializados com o valor 0.0. Atributos de tipos complexos, ou seja, atributos que são instâncias de objetos são inicializados com o valor null, como por exemplo atributos do tipo String e Date. Uma boa utilidade para um construtor é incluir dentro dele a inicialização dos atributos do objeto. Desta forma, podemos garantir que os atributos possuam valores default mais apropriados. No exemplo a seguir, incluímos um construtor à classe Cliente para inicializar seus atributos: package br.edu.ifms.lp2; import java.util.Calendar; import java.util.Date; public class Cliente { private String nome; private String endereco; private Date dataNascimento; public Cliente() { nome = "NOME NÃO INFORMADO"; endereco = "ENDEREÇO NÃO INFORMADO"; Calendar calendario = Calendar.getInstance(); calendario.set(1900, 0, 1); dataNascimento = calendario.getTime(); } public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public String getEndereco() { return endereco; } public void setEndereco(String endereco) { this.endereco = endereco; } public Date getDataNascimento() { return dataNascimento; } public void setDataNascimento(Date dataNascimento) { this.dataNascimento = dataNascimento; } public void imprimeDados() { System.out.println("Nome: " + nome); } } Agora, toda vez que instanciarmos um objeto da classe Cliente, automaticamente seus atributos serão inicializados conforme definido no construtor da classe. A classe TesteConstrutor exibe o novo construtor da classe Cliente em uso: package br.edu.ifms.lp2; public class TesteConstrutor { public static void main(String[] args) { Cliente cliente = new Cliente(); System.out.println("Nome: " + cliente.getNome()); System.out.println("Endereco: " + cliente.getEndereco()); System.out.println("Data de nascimento: " + cliente.getDataNascimento()); } } Observe que diferentemente dos outros métodos, o construtor não possui valor de retorno. Normalmente, construtores são definidos com acesso público. Definir um construtor com acesso privado faz com que a classe não possa ter objetos, isto é, não possa ser instanciada. Construtores também são utilizados para simplificar a inicialização de atributos de forma programática. Por exemplo, podemos criar um construtor que receba como parâmetros os valores a serem definidos nos atributos da classe. Para ilustrar este exemplo, incluiremos um construtor deste tipo na classe Cliente: package br.edu.ifms.lp2; import java.util.Calendar; import java.util.Date; public class Cliente { private String nome; private String endereco; private Date dataNascimento; public Cliente() { nome = "NOME NÃO INFORMADO"; endereco = "ENDEREÇO NÃO INFORMADO"; Calendar calendario = Calendar.getInstance(); calendario.set(1900, 0, 1); dataNascimento = calendario.getTime(); } public Cliente(String nome, String endereco, Date dataNascimento) { this.nome = nome; this.endereco = endereco; this.dataNascimento = dataNascimento; } public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public String getEndereco() { return endereco; } public void setEndereco(String endereco) { this.endereco = endereco; } public Date getDataNascimento() { return dataNascimento; } public void setDataNascimento(Date dataNascimento) { this.dataNascimento = dataNascimento; } public void imprimeDados() { System.out.println("Nome: " + nome); } } Note que agora a classe possui dois construtores. Desta forma, no momento de criar um novo objeto da classe Cliente devemos escolher qual o construtor a ser utilizado para instanciar o objeto: Cliente cliente = new Cliente(); System.out.println("Nome: " + cliente.getNome()); System.out.println("Endereco: " + cliente.getEndereco()); System.out.println("Data de nascimento: " + cliente.getDataNascimento()); Calendar calendario = Calendar.getInstance(); calendario.set(1990, 10, 15); Date dataNascimento = calendario.getTime(); Cliente outroCliente = new Cliente("Ana Maria Cardoso", "Rua 7 de Setembro, 22", dataNascimento); System.out.println("Nome: " + outroCliente.getNome()); System.out.println("Endereco: " + outroCliente.getEndereco()); System.out.println("Data de nascimento: " + outroCliente.getDataNascimento()); Um fato deve ser esclarecido neste ponto. Se o primeiro construtor da classe Cliente (o que não recebe parâmetros) for removido, não será mais possível instanciar novos objetos utilizando o construtor sem parâmetros, como abaixo: Cliente cliente = new Cliente(); Isto se deve ao fato de que mesmo removendo o primeiro construtor, a classe ainda possui um construtor definido de forma explícita. Assim, toda vez que um novo objeto for criado ele deverá ser instanciado obrigatoriamente utilizando-se o construtor com parâmetros. Caso a classe não possua nenhum construtor, então é possível utilizar a instanciação padrão, isto é, utilizando-se o construtor implícito sem parâmetros. Exercícios: a) Escreva uma classe que modele um retângulo e que possua atributos para armazenar os dados de sua largura e altura, além dos getters e setters de tais atributos. A classe deve possuir um construtor padrão, o qual deve inicializar os atributos de largura e altura com o valor zero (0) e também um construtor que receba como parâmetros os valores a serem definidos para a altura e a largura do retângulo. Por fim, a classe deve conter um método que ofereça o cálculo da área do retângulo. b) Escreva uma classe que modele um triângulo retângulo e que possua atributos para armazenar os dados de sua base e altura, além dos getters e setters de tais atributos. A classe deve possuir um construtor padrão, o qual deve inicializar os atributos de base e altura com o valor zero (0) e também um construtor que receba como parâmetros os valores a serem definidos para a base e a altura do triângulo retângulo. Por fim, a classe deve conter um método que ofereça o cálculo daárea do triângulo retângulo. c) Escreva um programa que ofereça um menu ao usuário no qual seja possível para este escolher se ele deseja calcular a área de um retângulo ou de um triângulo retângulo. O programa deve utilizar as classes implementadas nos exercícios a e b em sua implementação. O programa deve conter métodos para a leitura dos dados de entrada para os cálculos. 7 – Herança Na orientação a objetos é possível escrever uma classe que reaproveite atributos e métodos de uma outra classe, estendendo assim esta primeira classe a fim de adicionar atributos ou métodos mais específicos. Este conceito é chamado de herança. Por exemplo, imagine que desejamos estender a classe Cliente de tal forma a definir uma segunda classe para modelar um cliente na categoria pessoa física. Imagine que desejamos também definir uma terceira classe, desta vez para modelar um cliente na categoria pessoa jurídica. Vamos chamar estas classes de ClientePessoaFisica e ClientePessoaJuridica. Bem, neste ponto vale a pena fazer uma análise a fim de levantar quais devem ser os atributos destas classes. Vamos começar pelo cliente pessoa física. Um cliente desta categoria possui diversas características. Porém, para fins de simplicidade da análise, diremos que este tipo de cliente possui um nome, um sexo, um número de CPF, um número de RG, um endereço, uma data de nascimento e um código de cliente. Da mesma forma, o cliente pessoa jurídica também possui diversas características. Porém, para o nosso exemplo, um cliente deste tipo possui um nome fantasia, uma razão social, um número de CNPJ, um endereço e um código de cliente. Se observarmos as listas de atributos que cada classe deve possuir, podemos perceber que as duas classes possuem atributos em comum. No caso, nome, endereço e código de cliente. Desta forma, seria redundante incluir estes três campos nas definições das duas classes. Assim, iremos deixar estes campos inclusos na classe Cliente. Abaixo, a nova definição da classe Cliente: package br.edu.ifms.lp2; public class Cliente { private String nome; private String endereco; private String codigoCliente; public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public String getEndereco() { return endereco; } public void setEndereco(String endereco) { this.endereco = endereco; } public String getCodigoCliente() { return codigoCliente; } public void setCodigoCliente(String codigoCliente) { this.codigoCliente = codigoCliente; } } Uma vez que a classe Cliente foi definida conforme as novas especificações, podemos criar as classes ClientePessoaFisica e ClientePessoaJuridica. A palavra reservada extends da linguagem Java deve ser utilizada nestas classes a fim de definir que elas são especializações da classe Cliente. Isto significa que elas herdam as características originais da classe Cliente, porém estendendo o modelo da classe e adicionando novas características (atributos e métodos) inerentes a cada modelo. A seguir, as definições das duas classes: package br.edu.ifms.lp2; import java.util.Date; public class ClientePessoaFisica extends Cliente { // F (feminino) ou M (masculino) private char sexo; private String cpf; private String rg; private Date dataNascimento; public char getSexo() { return sexo; } public void setSexo(char sexo) { this.sexo = sexo; } public String getCpf() { return cpf; } public void setCpf(String cpf) { this.cpf = cpf; } public String getRg() { return rg; } public void setRg(String rg) { this.rg = rg; } public Date getDataNascimento() { return dataNascimento; } public void setDataNascimento(Date dataNascimento) { this.dataNascimento = dataNascimento; } } package br.edu.ifms.lp2; public class ClientePessoaJuridica extends Cliente { private String razaoSocial; private String cnpj; public String getRazaoSocial() { return razaoSocial; } public void setRazaoSocial(String razaoSocial) { this.razaoSocial = razaoSocial; } public String getCnpj() { return cnpj; } public void setCnpj(String cnpj) { this.cnpj = cnpj; } } Desta forma, todo objeto da classe ClientePessoaFisica possuirá os atributos nome, endereco, codigoCliente, sexo, cpf, rg e dataNascimento, todos acessíveis por meio de seus getters e setters. Por sua vez, todo objeto da classe ClientePessoaJuridica possuirá os atributos nome, endereco, codigoCliente, razaoSocial e cnpj, também todos acessíveis por meio de seus getters e setters. package br.edu.ifms.lp2; import java.util.Calendar; import java.util.Date; public class TesteClientes { public static void main(String[] args) { ClientePessoaFisica clientePF = new ClientePessoaFisica(); clientePF.setCodigoCliente("19320716"); clientePF.setCpf("18516967506"); Calendar calendario = Calendar.getInstance(); calendario.set(1982, 2, 21); Date dataNascimento = calendario.getTime(); clientePF.setDataNascimento(dataNascimento); clientePF.setEndereco("Av. Rebouças, Centro, 2053"); clientePF.setNome("Ana Maria Velasquez"); clientePF.setRg("911225341"); clientePF.setSexo('F'); ClientePessoaJuridica clientePJ = new ClientePessoaJuridica(); clientePJ.setCnpj("78671357000199"); clientePJ.setCodigoCliente("00183837"); clientePJ.setEndereco("Av. das Indústrias, Vila Autonomista, 23"); clientePJ.setNome("Atacado dos Tecidos"); clientePJ.setRazaoSocial("Grupo Sousa & Oliveira"); } } Quando utilizamos o conceito de herança em Java, devemos nos atentar a algumas particularidades referentes a construtores. Se um construtor sem parâmetros for definido na classe mãe, não necessariamente devemos implementar (definir explicitamente) construtores nas classes filhas. Porém, se um construtor com parâmetros for definido na classe mãe, devemos obrigatoriamente definir um construtor na classe filha de tal forma que este construtor invoque o construtor da classe mãe. A questão é: como invocar o construtor da classe mãe sem definir um objeto. Para tanto, é necessário utilizar a palavra reservada super da linguagem Java. Considere o exemplo a seguir: criaremos um construtor com parâmetros na classe Cliente, a fim de inicializar os atributos da classe: public Cliente(String nome, String endereco, String codigoCliente) { this.nome = nome; this.endereco = endereco; this.codigoCliente = codigoCliente; } Uma vez que este construtor foi definido, devemos obrigatoriamente implementar construtores nas classes ClientePessoaFisica e ClientePessoaJuridica que invoquem o construtor da classe Cliente. Desta forma, quando um objeto da classe ClientePessoaFisica ou da classe ClientePessoaJuridica for instanciado, tanto o construtor da classe referente quanto o construtor da classe Cliente serão invocados. A seguir, os construtores das classes ClientePessoaFisica e ClientePessoaJuridica: public ClientePessoaFisica(String nome, String endereco, String codigoCliente) { super(nome, endereco, codigoCliente); } public ClientePessoaJuridica(String nome, String endereco, String codigoCliente) { super(nome, endereco, codigoCliente); } A invocação do construtor da classe mãe por meio da palavra reservada super deve ser realizada obrigatoriamente na primeira linha do construtor da classe filha. Alternativamente, o construtor da classe filha pode conter mais parâmetros ou mesmo mais linhas de código após a invocação do construtor da classe mãe. Para entender tal fato, considere as seguintes versões alternativas dos construtores das classes ClientePessoaFisica e ClientePessoaJuridica: public ClientePessoaFisica(String nome, String endereco, String codigoCliente, char sexo, String cpf, String rg, Date dataNascimento){ super(nome, endereco, codigoCliente); this.sexo = sexo; this.cpf = cpf; this.rg = rg; this.dataNascimento = dataNascimento; } public ClientePessoaJuridica(String nome, String endereco, String codigoCliente, String razaoSocial, String cnpj) { super(nome, endereco, codigoCliente); this.razaoSocial = razaoSocial; this.cnpj = cnpj; } Vale a pena salientar que caso a classe mãe não possua um construtor implementado explicitamente, podemos construir construtores específicos nas classes filhas naturalmente. Assim, a implementação de um construtor na classe filha não demanda a implementação de um construtor na classe mãe. Exercícios: a) Reescreva os setters e os construtores das classes Cliente, ClientePessoaFisica e ClientePessoaJuridica a fim de validar os atributos necessários. b) Implemente a classe ContaBancaria, a qual deve conter os seguintes atributos/métodos: • Atributos: ◦ cliente ◦ numeroConta ◦ saldo • Métodos: ◦ sacar → deve receber o valor a ser sacado (o saldo não pode ficar negativo!) e retornar o novo saldo da conta ◦ depositar → deve receber o valor a ser depositado e retornar o novo saldo da conta Após, implemente as classes ContaPoupanca e ContaCorrente, as quais devem herdar a classe ContaBancaria. As classes devem conter os seguintes atributos/métodos: • ContaPoupanca: ◦ Atributo: diaDeRendimento ◦ Método: calcularNovoSaldo → recebe a taxa de rendimento da poupança e atualiza o saldo, caso o dia atual seja o dia de rendimento • ContaCorrente: ◦ Atributo: limite ◦ Método: sacar1 → recebe o valor a ser sacado; deve permitir saldo negativo até o valor do limite 1 Ao implementar este método, você está realizando uma sobrecarga. A sobrecarga de métodos ocorrente em caso de herança será explicada pelo professor em sala. 8 – Métodos estáticos Quando um método é assinado como estático ele pode ser acessado sem a necessidade de se instanciar um objeto. Como exemplo, considere a classe FuncoesMatematicas abaixo: package br.edu.ifms.lp2; public class FuncoesMatematicas { public static double delta(double a, double b, double c) { return Math.pow(b, 2) - 4 * a * c; } public static int fatorial(int n) { int resultado = 1; for (int i = 2; i <= n; i++) { resultado *= i; } return resultado; } public static void main(String[] args) { System.out.println(fatorial(3)); } } No exemplo, a classe possui dois métodos estáticos que implementam funções matemáticas. Observe que o método fatorial é invocado dentro do método main sem a necessidade de se instanciar um objeto da classe FuncoesMatematicas para acessá-lo. Opcionalmente, o método pode ser acessado por meio de um objeto, caso haja necessidade. Porém, vale a pena ressaltar que isto não é uma boa prática de programação. Quando um método estático é invocado dentro de outro método da mesma classe (como no exemplo), tal invocação é realizada de forma simples, ou seja, apenas informando o nome do método. Porém, quando um método estático é invocado dentro de um método que pertence a outra classe, não podemos realizar a invocação simplesmente informando o nome do método pois, quando realizamos este tipo de invocação, o compilador supõe que o método pertence à mesma classe do método dentro do qual a linha de código de invocação foi escrita. Desta forma, a invocação de um método estático dentro de um método de outra classe deve ser realizada informando-se o nome da classe a qual o método estático pertence, seguida de um ponto (.) e do nome do método, como no exemplo abaixo: package br.edu.ifms.lp2; public class ChamadaMetodoEstatico { public static void main(String[] args) { double resultado = FuncoesMatematicas.delta(4, 5, 6); System.out.println(resultado); } } Vale a pena ressaltar que métodos estáticos estão sujeitos às mesmas regras de permissão de acesso que os métodos não estáticos. Isto significa que, por exemplo, o método delta não poderia ser acessado pela classe acima caso ele fosse definido como privado. Os métodos estáticos possuem uma limitação importante. Um método estático não pode utilizar atributos ou métodos de sua classe que não sejam estáticos. Isto se deve ao fato de que todos os métodos e atributos não estáticos são assim definidos para que sejam obrigatoriamente utilizáveis por meio da instanciação de objetos. Assim, se um método estático pudesse acessá-los haveria uma violação de acesso, uma vez que os métodos estáticos podem ser acessados sem a necessidade da instanciação de um objeto. Desta forma, definir um método como estático ou não é uma questão de design que exige uma análise atenciosa. Porém, a regra geral é: defina como não estático todo método que seja influenciado pelo comportamento do objeto; caso o método não necessite ser influenciado pelo comportamento de um objeto específico, então o defina como estático. Por exemplo, considere a classe abaixo: package br.edu.ifms.lp2; public class Retangulo { private double base; private double altura; public Retangulo(double base, double altura) { this.base = base; this.altura = altura; } public double calculaArea() { return base * altura; } public double calculaArea(double base, double altura) { return base * altura; } // Getters e setters... } A classe Retangulo possui uma sobrecarga no método calculaArea. Se observarmos as duas implementações do método, podemos perceber que na primeira o cálculo da área é realizado utilizando-se os valores da base e altura definidos no objeto, enquanto que na segunda os valores são informados via parâmetros de entrada. Podemos perceber então que na segunda implementação o cálculo da área não depende do comportamento de um objeto específico, pois os valores da base e altura são informados via parâmetros. Assim, a segunda implementação do método calculaArea poderia ser implementado como um método estático: package br.edu.ifms.lp2; public class Retangulo { private double base; private double altura; public Retangulo(double base, double altura) { this.base = base; this.altura = altura; } public double calculaArea() { return base * altura; } public static double calculaArea(double base, double altura) { return base * altura; } // Getters e setters... } A linguagem Java também permite a criação de atributos estáticos. As regras de invocação de atributos estáticos – assim como as de permissão de acesso – são as mesmas às dos métodos estáticos. Existem alguns casos em que criar atributos estáticos pode ser um artifício útil. Uma das maiores aplicações para atributos estáticos é a definição de constantes de uma classe específica que possam ser úteis em outras classes. Por exemplo, podemos incluir na classe FuncoesMatematicas algumas contantes matemáticas para que possam ser utilizadas por outras classes que as necessitarem: package br.edu.ifms.lp2; public class FuncoesMatematicas { public static double PI = 3.141592653589793; public static double NUMERO_DE_EULER = 2.718281828459045235360287; public static double NUMERO_DE_OURO = 1.61803398874989484820458683436563811; public static double delta(double a, double b, double c) { return Math.pow(b, 2) - 4 * a * c; } public static int fatorial(int n) { int resultado = 1; for (int i = 2; i <= n; i++) { resultado *= i; } return resultado; } } Como você já deve possivelmente ter observado há algum tempo, o método main é um método estático. Porém, por ser reservado para bootstrapping (inicialização) do programa Java, ele não pode ser invocado dentro de outros métodos. Existem mais alguns outros métodos e atributos estáticos “populares” dentro da JDK: • O atributo out da classe System; • O método pow da classe Math; • O método parseIntda classe Integer; • O método parseDouble da classe Double; • O método parseFloat da classe Float; • O método createInstance da classe Calendar; • … dentre muitos outros. 9 - Classes com construtor privado Um outro artifício possível na linguagem Java é definir o construtor default de uma classe como privado a fim de bloquear a instanciação de objetos desta classe. A questão é: para que (ou por quê) fazer isto? Na verdade, existem algumas situações em que se torna necessário impedir o acesso aos métodos (ou atributos) de uma classe por meio de objetos. Por exemplo, se uma classe possuir apenas métodos estáticos, não existe a necessidade de se instanciar objetos desta classe a fim de utilizar tais métodos. De fato, esta prática deve ser evitada – como já dito anteriormente. Assim, a solução para este caso seria definir o construtor desta classe como privado. Tal ação bloqueia a instanciação de objetos desta classe, dado que uma vez que um construtor tenha sido criado explicitamente, a máquina virtual do Java não criará um construtor (público) default para a classe e, como o construtor explícito foi definido como privado, qualquer outro método fora desta classe não possui acesso a ele. A fim de ilustrar o uso de construtores privados, poderíamos definir um construtor deste tipo na classe FuncoesMatematicas para evitar que um objeto desta classe seja instanciado. Ela é uma boa candidata a um construtor privado, visto que todos os seus métodos e atributos são estáticos. package br.edu.ifms.lp2; public class FuncoesMatematicas { public static double PI = 3.141592653589793; public static double NUMERO_DE_EULER = 2.718281828459045235360287; public static double NUMERO_DE_OURO = 1.61803398874989484820458683436563811; private FuncoesMatematicas() { } public static double delta(double a, double b, double c) { return Math.pow(b, 2) - 4 * a * c; } public static int fatorial(int n) { int resultado = 1; for (int i = 2; i <= n; i++) { resultado *= i; } return resultado; } } O uso de construtores privados deve ser reservado a casos específicos. Vale ressaltar que ao se definir o construtor de uma classe como privado, o acesso aos seus métodos e atributos públicos não estáticos ficará restrito às classes que a herdarem, não podendo ser acessados por objetos nem da classe mãe ou das classes filhas. Quando uma classe herda uma classe cujo construtor é privado, tal classe filha deve implementar explicitamente um construtor caso se deseje permitir a instanciação de seus objetos. Alguns desenvolvedores consideram a prática de definir o construtor de uma classe como privado a fim de se evitar a instanciação de seus objetos como uma solução “não totalmente” eficaz. Isto se deve ao fato de que existe um caso em que, mesmo com o seu construtor definido como privado, é possível instanciar objetos de uma determinada classe. Tal brecha ocorre no método main da classe. Como o método main faz parte da classe, então ele possui total acesso a todos os métodos de tal classe2. Inclusive ao seu construtor, independente ao fato de que este seja privado. Uma alternativa mais eficaz para evitar a instanciação de objetos de uma determinada classe é defini-la como abstrata. Classes abstratas serão discutidas futuramente. Exercícios: a) Implemente a classe LeituraDados. Ela deve conter os seguintes métodos estáticos para a leitura de dados de tipos distintos: • lerInteiro(String mensagemUsuario) • lerFloat(String mensagemUsuario) • lerDouble(String mensagemUsuario) • lerChar(String mensagemUsuario) • lerString(String mensagemUsuario) 2 Lembrando que métodos não estáticos são acessíveis neste ponto por meio de objetos (instâncias) da classe, enquanto que métodos estáticos são acessíveis diretamente. 10 - Tratamento de exceções Existem alguns tipos de operações em Java que podem ocasionar erros inesperados pelo programador. Tais erros não são sintáticos, ou seja, erros de sintaxe da linguagem Java. Assim, não são identificados no momento da compilação do código. De fato, tais erros são semânticos, isto é, comportamentos inesperados ao programa. Em Java, erros inesperados que causam a interrupção abrupta do programa são chamados de exceções. Um tipo de operação que é extremamente passível a exceções é o acesso a dispositivos secundários, como memórias ou dispositivos de I/O. Como um primeiro exemplo, considere a leitura de dados do usuário. Neste tipo de leitura, o usuário informa os seus dados por meio da entrada padrão do sistema – comumente, o teclado. Vamos analisar a leitura de um número inteiro do usuário utilizando a classe Scanner do pacote java.util: package br.edu.ifms.lp2; import java.util.Scanner; public class LeituraInteiro { public static void main(String[] args) { System.out.println("Digite um numero inteiro:"); Scanner leitor = new Scanner(System.in); int numero = leitor.nextInt(); System.out.println("Numero informado: " + numero); } } Ao se desenvolver um programa, é esperado um comportamento específico de seu usuário. No exemplo acima, é esperado que o usuário digite um número inteiro no teclado e ao final digite a tecla ENTER a fim de finalizar a leitura. Porém, caso o usuário digite um valor não inteiro (por exemplo, um texto), o programa será interrompido abruptamente e a máquina virtual do Java irá informar o seguinte na tela: Exception in thread "main" java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:909) at java.util.Scanner.next(Scanner.java:1530) at java.util.Scanner.nextInt(Scanner.java:2160) at java.util.Scanner.nextInt(Scanner.java:2119) at br.edu.ifms.lp2.LeituraInteiro.main(LeituraInteiro.java:10) O texto informado acima representa um stack trace de erro, ou seja, um “caminho” de invocações de métodos desde a origem da exceção até o código que iniciou todo o processo. No exemplo acima, a exceção foi apontada na linha 909 da classe Scanner, a qual fica dentro do método throwFor. Analisando o restante do stack trace, podemos verificar que o método throwFor foi invocado na linha 1530 da classe Scanner, exatamente dentro do método next. Por sua vez, o método next foi invocado na linha 2160, pelo método nextInt. Continuando a análise, podemos verificar que o método nextInt foi invocado na linha 2119, por outra versão do método nextInt (uma sobrecarga). Por fim, o método nextInt foi invocado na linha 10 do método main da nossa classe do último exemplo. Assim, pudemos rastrear a exceção causada desde a linha de código que iniciou todo o processo até a sua linha causadora. Além disso, o stack trace nos fornece outra informação importante: qual o tipo de exceção causada. No nosso exemplo, a exceção causada chama-se InputMismatchException. Note que a exceção é na verdade uma classe, situada no pacote java.util. De fato, toda exceção em Java é uma classe. Por convenção, o nome de toda classe que representa uma exceção deve possuir o sufixo “Exception”. Uma vez identificada a exceção causada, devemos tentar entender o que ela representa. Se olharmos a documentação da exceção InputMismatchException, iremos encontrar a seguinte definição: “Thrown by a Scanner to indicate that the token retrieved does not match the pattern for the expected type, or that the token is out of range for the expected type.” Ou seja, a exceção ocorre quando a informação lida do usuário não é compatível com o tipo de dados desejado ou o valor lido está fora dos limites de tal tipo de dados. No caso, quando o usuário digita um texto e espera-se que ele digite um valor inteiro, a exceção é lançada graças a esta incompatibilidade de tipos. Agora que sabemos qual exceção foi lançada e o motivo pelo qual ela foi lançada, devemos tratá-la. “Tratar” uma exceção significa preparar o código passível a ela de tal formaque, caso a exceção ocorra, ela possa ser capturada e assim alguma tratativa possa ser aplicada, evitando assim o encerramento abrupto do programa. Abaixo, a classe LeituraInteiro com a tratativa à exceção: package br.edu.ifms.lp2; import java.util.InputMismatchException; import java.util.Scanner; public class LeituraInteiro { public static void main(String[] args) { System.out.println("Digite um numero inteiro:"); Scanner leitor = new Scanner(System.in); try { int numero = leitor.nextInt(); System.out.println("Numero informado: " + numero); } catch (InputMismatchException e) { System.out.println("O valor informado nao e inteiro!"); } } } Em Java, uma exceção é capturada por meio do uso das palavras reservadas try e catch. Observe o exemplo acima. A linha que pode vir a causar a exceção InputMismatchException foi colocada dentro do bloco try. Com isto, deseja-se informar à maquina virtual do Java: “Tente executar este trecho de código”. Imediatamente após o bloco try foi colocado um bloco catch, passando como parâmetro um objeto da exceção InputMismatchException. Com este bloco, deseja-se informar à máquina virtual do Java: “Caso o trecho de código do bloco try lance a exceção InputMismatchException, capture esta exceção, coloque todas as informações a seu respeito dentro do objeto e e execute o seguinte trecho de código”. No caso, se a exceção for lançada, a mensagem “O valor informado nao e inteiro” será impressa. Podemos reescrever a classe LeituraInteiro de tal forma a permitir que o usuário possa digitar novamente o número caso o valor anterior não seja inteiro. Abaixo, a nova versão da classe, a qual utiliza um laço de repetição para forçar uma nova leitura caso o valor lido não seja válido: package br.edu.ifms.lp2; import java.util.InputMismatchException; import java.util.Scanner; public class LeituraInteiro { public static void main(String[] args) { System.out.println("Digite um numero inteiro:"); boolean numeroOk = false; do { Scanner leitor = new Scanner(System.in); try { int numero = leitor.nextInt(); System.out.println("Numero informado: " + numero); numeroOk = true; } catch (InputMismatchException e) { System.out.println("O valor informado nao e inteiro!"); System.out.println("Digite novamente o numero:"); } } while (!numeroOk); } } Para tratar uma exceção, não necessariamente é necessário conhecer o seu tipo. Toda exceção em Java herda a classe Exception. Graças a isto, opcionalmente, podemos capturar a exceção Exception ao invés da exceção InputMismatchException: package br.edu.ifms.lp2; import java.util.Scanner; public class LeituraInteiro { public static void main(String[] args) { System.out.println("Digite um numero inteiro:"); boolean numeroOk = false; do { Scanner leitor = new Scanner(System.in); try { int numero = leitor.nextInt(); System.out.println("Numero informado: " + numero); numeroOk = true; } catch (Exception e) { System.out.println("O valor informado nao e inteiro!"); System.out.println("Digite novamente o numero:"); } } while (!numeroOk); } } Existem algumas operações (ou sequência de operações) que podem vir a lançar mais de uma exceção. Neste caso, cada exceção pode ser capturada separadamente, conforme ilustrado abaixo, ou pode ser criado um único bloco catch que capture a exceção Exception. try { // Código a ser tratado } catch(Excecao1 e1) { // Código para tratar a exceção } catch(Excecao2 e2) { // Código para tratar a exceção } catch(Excecao3 e3) { // Código para tratar a exceção } ... Para simular tal situação, escreveremos um trecho de código que realize duas operações que podem lançar exceções distintas. Primeiramente, o programa lerá um número real do usuário utilizando o método nextDouble da classe Scanner. Após isto, o programa lerá outro número real do usuário, porém utilizando o método next da classe Scanner para ler o número como uma string e depois converteremos a string obtida em um número real utilizando o método parseDouble da classe Double: package br.edu.ifms.lp2; import java.util.InputMismatchException; import java.util.Scanner; public class ExemploConversao { public static void main(String[] args) { System.out.println("Digite um numero real:"); try { Scanner leitor = new Scanner(System.in); double numero = leitor.nextDouble(); String numeroStr = String.valueOf(numero); numero = Double.parseDouble(numeroStr); } catch (InputMismatchException e1) { System.out.println("Valor nao corresponde a um numero real."); } catch (NumberFormatException e2) { System.out.println("Ocorreu um erro ao converter a string."); } } } No programa acima, caso o primeiro número lido do usuário não corresponda a um número real ou caso tal número possua casas decimais separadas por um ponto (ao invés de uma vírgula, como espera o método nextDouble), então o programa lançará a exceção InputMismatchException. Além disso, caso o segundo valor lido do usuário não corresponda a um número real, a exceção NumberFormatException é lançada pelo método parseDouble. Note que não precisamos importar a exceção NumberFormatException pois ela reside no pacote java.lang, o qual faz parte do core da JDK. Neste último exemplo, poderíamos capturar somente a exceção Exception a fim de tratar indiretamente todas as exceções possíveis no trecho de código. Porém, ao se fazer isto, não é possível (ao menos de forma simples e direta) lidar com cada exceção individualmente, ou seja, realizar um tratamento específico a cada exceção. A versão abaixo da classe ExemploConversao trata individualmente cada uma das duas exceções do exemplo anterior, porém capturando-as indiretamente: package br.edu.ifms.lp2; import java.util.InputMismatchException; import java.util.Scanner; public class ExemploConversao { public static void main(String[] args) { try { System.out.println("Digite um numero real:"); Scanner leitor = new Scanner(System.in); double primeiro = leitor.nextDouble(); System.out.println("Digite outro numero real:"); String numeroStr = leitor.next(); double segundo = Double.parseDouble(numeroStr); System.out.println("Primeiro: " + primeiro); System.out.println("Segundo: " + segundo); } catch (Exception e) { if (e instanceof InputMismatchException) { System.out.println("Valor nao corresponde a um numero real."); } else if (e instanceof NumberFormatException) { System.out.println("Ocorreu um erro ao converter a string."); } else { System.out.println("Ocorreu um erro nao identificado"); } } } } Exercícios: a) Reescreva a classe LeituraDados de tal forma a tratar todas as possíveis exceções que a classe venha a lançar. 11 - A palavra reservada finally Como visto nos exemplos da seção anterior, utilizamos o bloco try-catch para tentar executar um bloco de código passível a exceções e, caso alguma exceção esperada seja lançada, capturar tal exceção e redirecionar o fluxo de execução do programa para um bloco de código alternativo. Existem algumas situações em que ambos os blocos de código possuem desfechos em comum, ou seja, linhas de código em comum que devem ser executadas no final do bloco e que dependem do processamento resultante do bloco. Como exemplo, considere que desejamos escrever um programa que leia um número real do usuário e imprima o quadrado deste número. Porém, queremos que o programa permite que o usuário digite o seu número real desejado utilizando um ponto ou uma vírgula para separar a parte inteira do número das casas decimais. Note que neste exemplo que, independentemente à forma a qual o usuário digitará o número desejado, o cálculo deve ser realizado. Porém, dependendo desta forma de representação do número real, uma tratativaadequada deve ser aplicada para a leitura correta do número. Para resolver este problema, utilizaremos um bloco try-catch-finally. A palavra reservada finally da linguagem Java é utilizada para se definir um bloco de código que deve ser executado logo após um bloco try-catch, independentemente ao fato de uma exceção ter sido capturada ou não. A seguir, a solução proposta: Scanner leitor = new Scanner(System.in); System.out.println("Digite um número real"); System.out.println("(utilizando ponto ou vírgula para separar as casas decimais): "); String numeroStr = leitor.next(); Double numero = 0D; try { numero = Double.parseDouble(numeroStr); } catch (NumberFormatException e) { numeroStr = numeroStr.replace(",", "."); numero = Double.parseDouble(numeroStr); } finally { System.out.println("Quadrado do valor informado: " + Math.pow(numero, 2)); leitor.close(); } No exemplo acima, o número do usuário é lido como uma string, utilizando-se o método next da classe Scanner. No bloco try, é realizada a tentativa de converter a string em um número real por meio do método parseDouble da classe Double. Caso este número tenha casas decimais separadas da parte inteira por uma vírgula, a exceção NumberFormatException é lançada e capturada no bloco catch. Neste bloco, a string contendo o número do usuário tem a sua vírgula substituída por um ponto, fazendo com que assim o número possa ser convertido adequadamente pelo método parseDouble. Por fim, o código do bloco finally é executado, não importando se a exceção tenha sido lançada ou não. 12 - Criando exceções Nos últimos exemplos pudemos ver algumas das exceções mais populares da linguagem Java. O JDK (Java Development Kit) contém uma série de classes de exceção que abordam diversos tipos de erros comuns. Porém, quando desenvolvemos uma aplicação, é comum que ela contenha diversas regras de negócio específicas, as quais devem ser garantidas para a boa execução das funcionalidades da aplicação. A violação de muitas destas regras não faz com que exceções do JDK sejam lançadas, devido à sua especificidade. A fim de tratar tais violações, é possível escrever exceções customizadas as quais podem ser lançadas e tratadas futuramente na sua aplicação. Para entender como é possível implementar este processo, considere como exemplo que você foi incumbido de implementar dentro de um sistema um módulo de cadastro de clientes. Para tanto, você decide iniciar a implementação deste módulo criando um método que receba os dados de um determinado cliente e então retorne um objeto que encapsule tais dados. Abaixo, o método resultante: public Cliente cadastraCliente(String cpf, String nome, String endereco, Date dataNascimento) { if (dataNascimento != null && !dataNascimento.after(new Date())) { Cliente cliente = new Cliente(); cliente.setCpf(cpf); cliente.setNome(nome); cliente.setEndereco(endereco); cliente.setDataNascimento(dataNascimento); return cliente; } return null; } Analisando o método acima é possível perceber que ele possui uma regra de negócios clara. Se a data de nascimento informada ao cliente não for informada ou se ela for informada mas for posterior à data atual, então o objeto de cliente não é criado e o método retorna o valor null. Apesar do fato de que a validação é feita, ao se pretender invocar este método é preciso saber de antemão que, caso o método retorne o valor null, isto ocorre devido ao fato da data de nascimento pretendida ao cliente ser inválida. Uma alternativa seria retornar uma mensagem informando de forma mais clara tal fato. Porém, o método cadastraCliente deve retornar um objeto de cliente e não uma string de mensagem. Uma boa alternativa a este problema seria criar uma exceção customizada que representasse a tentativa de cadastro de uma data de nascimento inválida. Assim, caso a data de nascimento passada ao método cadastraCliente fosse inválida, o método poderia lançar tal exceção, a qual poderia conter uma mensagem clara a respeito da violação cometida. Assim, utilizaremos esta abordagem por meio de três passos: • Criar uma exceção customizada à violação de regra de negócio • Inserir na exceção uma mensagem adequada e informativa a respeito da violação • Lançar esta exceção caso a violação seja cometida Para realizar o primeiro passo, é preciso criar uma classe de exceção. A forma mais comum de criar uma classe de exceção é criar uma classe comum e fazer com que ela herde a classe Exception do pacote java.lang. Abaixo, a exceção que utilizaremos para o nosso exemplo: package br.edu.ifms.lp2.excecao; public class DataInvalidaException extends Exception { public DataInvalidaException() { super("A data de nascimento deve ser anterior ou igual à data atual."); } } Existe uma convenção entre os programadores profissionais de utilizar o sufixo “Exception” na nomenclatura de classes de exceção. Para fazer com que uma mensagem customizada à violação referente à inserção de datas inválidas seja retornada pela exceção DataInvalidaException, a classe repassa tal mensagem à classe Exception por meio de um construtor que esta fornece para tal finalidade. Assim, de forma simples, é possível propagar a mensagem customizada para o código infrator. Desta forma, com a classe DataInvalidaException foi possível realizar os dois primeiros passos do processo. Agora, é necessário fazer com que o método cadastraCliente lance esta exceção caso ele receba uma data de nascimento inválida: public Cliente cadastraCliente(String cpf, String nome, String endereco, Date dataNascimento) throws DataInvalidaException { if (dataNascimento != null && !dataNascimento.after(new Date())) { Cliente cliente = new Cliente(); cliente.setCpf(cpf); cliente.setNome(nome); cliente.setEndereco(endereco); cliente.setDataNascimento(dataNascimento); return cliente; } else { throw new DataInvalidaException(); } } Para lançar a exceção, foram necessários dois passos. Primeiramente, foi preciso informar no final da assinatura do método que ele possivelmente lança a exceção, utilizando a palavra reservada throws da linguagem Java. Ao realizar isto, o método “obriga” que qualquer código que invocá-lo trate a exceção DataInvalidaException. O último passo consiste em efetivamente lançar a exceção caso a data seja inválida, utilizando a palavra reservada throw (sem o 's', não confunda!) para lançar uma nova instância da exceção. Note que além do retorno do objeto de cliente, caso a data de nascimento seja válida e o objeto de cliente seja criado, o método não possui outra instrução de retorno. Isto se deve ao fato de que, dentro do bloco de código da instrução else quando a exceção é lançada, a execução do método é interrompida instantaneamente. Assim, se uma instrução de retorno fosse colocada após a instrução throw ou mesmo fora do bloco else, tal linha de código seria inalcançável (o que é chamado em Java de Unreachable Code). Após a implementação destes três passos, a regra de negócios sobre a data de nascimento do cliente foi efetivamente garantida. Assim, todo código que invocar o método cadastraCliente deve tratar a exceção DataInvalidaException. Por exemplo, observe a classe a seguir: package br.edu.ifms.lp2; import java.util.Calendar; import java.util.Date; import br.edu.ifms.lp2.excecao.DataInvalidaException; public class TesteCadastroCliente { public static void main(String[] args) { RegrasCliente regras = new RegrasCliente(); String cpf = "11111111111"; String nome = "João da Silva"; String endereco = "Rua 14 de agosto, 23, Vila Inara"; Calendar calendario = Calendar.getInstance(); calendario.set(2015, 7, 28); Date dataNascimento = calendario.getTime(); try { Cliente cliente = regras.cadastraCliente(cpf, nome, endereco, dataNascimento);} catch (DataInvalidaException e) { e.printStackTrace(); } } } No classe acima, a data de nascimento enviada ao método cadastraCliente é posterior à data atual3. Assim, ao executar este programa, a mensagem abaixo será impressa: br.edu.ifms.lp2.excecao.DataInvalidaException: A data de nascimento deve ser anterior ou igual à data atual. at br.edu.ifms.lp2.RegrasCliente.cadastraCliente(RegrasCliente.java:19) at br.edu.ifms.lp2.TesteCadastroCliente.main(TesteCadastroCliente.java:19) Exercícios: a) Escreva uma classe de exceção para cada uma das seguintes regras de negócio abaixo: • O número de CPF deve conter exatamente onze dígitos • O endereço informado não pode ser nulo ou vazio • O cliente deve ter no mínimo 18 anos b) Após implementar as exceções do exercício a, valide o método cadastraCliente de tal forma a lançar tais exceções quando necessário. 13 - Manipulação de arquivos Manipular arquivos programaticamente é uma atividade comum no dia a dia de desenvolvimento de software. Na linguagem Java, a classe File do pacote java.io é utilizada para se realizar operações básicas sobre arquivos. Como um primeiro exemplo, vamos considerar que desejamos escrever um programa capaz de abrir um arquivo existente em memória secundária. Para tanto, iremos implementar um método que seja capaz de abrir um arquivo, utilizando a classe File para tanto. Abaixo, o método correspondente: public File abreArquivo(String caminhoArquivo) { File arquivo = new File(caminhoArquivo); if (arquivo.exists()) { return arquivo; } return null; } O método abreArquivo recebe como parâmetro de entrada o caminho do arquivo a ser aberto. Vale a pena salientar que o caminho de um arquivo é composto pelo caminho desde a raiz da unidade de memória secundária até o diretório onde o arquivo se encontra, seguido do 3 A seção 12 desta apostila foi escrita no dia 31 de outubro de 2013. nome do arquivo. Se o programa estiver sendo executado no sistema operacional Windows, é preciso substituir cada ocorrência do caractere '\' na string do caminho especificado por duas ocorrências do mesmo caractere (“\\”). Por exemplo, se o arquivo está situado no caminho abaixo: C:\Users\Sidney\arquivo.txt Então, a string correspondente para o parâmetro de entrada seria: “C:\\Users\\Sidney\\arquivo.txt” Opcionalmente, o mesmo caminho poderia ser definido substituindo-se cada ocorrência do caractere '\' pelo caractere '/'. Desta forma, o caminho do exemplo acima ficaria como abaixo: “C:/Users/Sidney/arquivo.txt” Na primeira linha de código do método abreArquivo é realizada a instanciação de um objeto da classe File. Observe que a instanciação é realizada utilizando-se um construtor que recebe como parâmetro o caminho do arquivo a ser aberto. Quando um objeto da classe File é instanciado desta forma, instantaneamente o arquivo desejado é aberto e alocado para o programa que realizou tal abertura. Assim, no nosso método, o arquivo desejado é aberto e todos os seus metadados são armazenadas dentro do objeto arquivo. Bem, se logo na primeira linha do método o arquivo já é aberto, então o que o restante do método realiza? Quando instanciamos um objeto da classe File conforme feito no método abreArquivo, podem ocorrer duas situações. A primeira situação ocorre quando o arquivo já existe no caminho especificado; neste caso, o arquivo é aberto e seus metadados armazenados no objeto. Por sua vez, a segunda situação ocorre quando o arquivo ainda não existe no caminho especificado. Neste caso, o construtor da classe File interpreta que o solicitante deseja criar o arquivo no caminho especificado; assim, o arquivo é “criado” em memória primária e espera-se que seja adicionado algum tipo de conteúdo a ele posteriormente. Desta forma, após a instanciação do objeto o programa não sabe se o arquivo foi aberto ou se na realidade ele foi recém criado. Para verificar o que de fato ocorreu, o programa verifica se o arquivo já existe por meio do método exists() da classe File. Este método retorna true caso o arquivo já exista no caminho especificado ou false caso contrário. Assim, caso o arquivo já exista, o método abreArquivo retorna o objeto referente ao arquivo desejado. Por outro lado, caso o arquivo não exista ainda, o método retorna null. Ao entender como o construtor da classe File se comporta quanto à preexistência do arquivo desejado, podemos implementar um método capaz de criar um novo arquivo. O método abaixo possui o propósito de gerar um novo arquivo no caminho especificado: public File criaArquivo(String caminhoArquivo) { return abreArquivo(caminhoArquivo) == null ? new File(caminhoArquivo) : null; } O método criaArquivo utiliza o método abreArquivo para tentar abrir o (suposto) arquivo inexistente no caminho especificado. Se o arquivo ainda não existir no caminho especificado, o método abreArquivo retorna o valor null e, consequentemente, o método criaArquivo cria o arquivo desejado em memória primária e retorna uma nova instância da classe File contendo os metadados do arquivo recém criado. Caso o arquivo já exista no caminho especificado, o método abreArquivo retorna uma instância da classe File contendo os metadados do arquivo já existente, o que faz com que o método criaArquivo retorne o valor null. A classe File possui uma série de métodos para verificar os metadados de um determinado arquivo. Por exemplo, o método getPath() retorna o caminho do arquivo referenciado pelo objeto e o método getName() retorna o nome de tal arquivo. O método abaixo utiliza o método getName() para identificar a extensão de um determinado arquivo: public String pegaExtensaoArquivo(File arquivo) { if (arquivo != null && arquivo.exists()) { String nomeArquivo = arquivo.getName().trim(); String[] aux = nomeArquivo.split("\\."); return aux.length > 0 ? aux[aux.length - 1] : ""; } return ""; } O método length() da classe File retorna o tamanho do arquivo em bytes. Assim, podemos escrever um método para calcular o tamanho de um determinado arquivo em megabytes, como abaixo: public Double calculaTamanhoArquivoEmMegaBytes(File arquivo) { if (arquivo != null && arquivo.exists()) { long tamanhoArquivoEmBytes = arquivo.length(); return tamanhoArquivoEmBytes / 1048576D; } // Caso o arquivo não exista, retorna um valor de tamanho inválido return -1D; } Como um último exemplo, utilizaremos os métodos isDirectory() e list() da classe File para varrer recursivamente todos os subdiretórios com raiz em um determinado diretório. O método isDirectory() retorna true caso o arquivo represente um diretório ou false caso contrário. O método list() deve ser invocado somente em arquivos que representem diretórios. Este método retorna uma lista contendo os caminhos de todos os arquivos ou diretórios existentes dentro do diretório referente. Os métodos abaixo recebem o caminho de um determinado diretório e listam todos os diretórios e arquivos alcançáveis a partir dele: public void varreDiretorios(String caminhoDiretorio) { File arquivo = abreArquivo(caminhoDiretorio); if (arquivo != null && arquivo.exists() && arquivo.isDirectory()) { varreDiretorios(arquivo, ""); } } private void varreDiretorios(File diretorio, String tabulacoes) { String[] listaDiretorios = diretorio.list(); if (listaDiretorios != null && listaDiretorios.length > 0) { for (String nomeSubDiretorio : diretorio.list()) { nomeSubDiretorio = diretorio.getPath() + "/" + nomeSubDiretorio; File subDiretorio = abreArquivo(nomeSubDiretorio); if (subDiretorio != null && subDiretorio.exists()) { if (subDiretorio.isDirectory()) { varreDiretorios(subDiretorio, tabulacoes + "\t"); } else { System.out.println(tabulacoes + subDiretorio.getPath()); } } } }} 14 - Leitura em arquivos textuais Apesar dos vários tipos de arquivos existentes atualmente, podemos categorizá-los em duas categorias básicas: arquivos textuais e arquivos binários. Um arquivo textual é totalmente composto por caracteres textuais e deve ser manipulado por meio de um editor de textos. Por sua vez, um arquivo binário possui uma estrutura mais complexa, podendo ser composto por vários tipos de dados. Assim, para cada tipo de arquivo binário existe um software específico para manipulá-lo. Como vimos na última seção, a classe File possui métodos para manipular arquivos e diretórios. Apesar disso, não é possível realizar a leitura de um arquivo utilizando somente a classe File. Existem classes específicas para a realização da leitura de arquivos textuais, assim como existem classes específicas para a leitura de arquivos binários. Nesta seção, aprenderemos as duas formas básicas para realizar a leitura de arquivos textuais. A forma mais básica de realizar a leitura programática de um arquivo textual é utilizar a classe Scanner para tal tarefa. A classe Scanner é responsável por realizar a leitura de dados oriundos de fontes diversas. Por exemplo, você já utilizou a classe Scanner para realizar a leitura de dados que o usuário informa por meio do teclado. Vamos nos recordar de como o objeto de leitura é preparado para realizar este tipo de leitura: Scanner leitor = new Scanner(System.in); Note que na instanciação do objeto é passado ao construtor da classe Scanner o valor do objeto in da classe System. Ao fazer isto, estamos informando à classe Scanner que desejamos realizar a leitura dos dados provenientes da entrada padrão do sistema. Em um dispositivo computacional padrão, a entrada de dados padrão do sistema é o teclado. Desta forma, o valor passado ao construtor da classe Scanner é que define a fonte dos dados a serem lidos. Para informar à classe que os dados a serem lidos são provenientes de um arquivo, basta passar como parâmetro ao construtor o objeto da classe File referente ao arquivo desejado, como exibido abaixo: File arquivo = new File(caminhoArquivo); Scanner leitor = new Scanner(arquivo); Uma vez que o objeto de leitura esteja preparado, é possível realizar a leitura do arquivo desejado. A leitura dos dados do arquivo é feita de forma análoga à leitura de dados provenientes da entrada padrão do sistema. Mas antes de saber como ler os dados de um arquivo, é preciso conhecer a dinâmica da leitura de um arquivo textual. A leitura do arquivo é sempre realizada do começo ao fim do arquivo. Além disso, é possível determinar se a leitura será realizada token a token (palavra a palavra) ou linha a linha. O método abaixo realiza a leitura token a token de um arquivo por completo: public String leArquivoTokenAToken(File arquivo) { if (arquivo != null) { try { String textoArquivo = ""; Scanner leitor = new Scanner(arquivo); while (leitor.hasNext()) { textoArquivo += leitor.next(); } leitor.close(); return textoArquivo; } catch (FileNotFoundException e) { System.out.println("Arquivo nao pode ser lido!"); e.printStackTrace(); } } return ""; } Não é necessário tratar nenhuma exceção nas operações de manipulação básica de arquivos vistas na seção anterior. Porém, ao se tentar realizar a leitura de um arquivo, a exceção FileNotFoundException do pacote java.io pode ser lançada, o que obriga ao programador tratá-la. A possível ocorrência desta exceção se deve ao fato de que, caso se tente abrir um arquivo que não existe, a classe File criará este arquivo em memória e assim não existe o risco de lançamento de uma exceção. Porém, é fácil prever que, caso o arquivo ainda não exista em memória secundária, a leitura do seu conteúdo é impossível. Portanto, se o arquivo ainda não existe em memória secundária, a exceção FileNotFoundException é lançada (lembrando que “file not found” corresponde – em português – a “arquivo não encontrado”). Note também que o leitor deve ser encerrado após a operação de leitura, a fim de desalocar o arquivo do programa. Para entender o funcionamento do método leArquivoTokenAToken, considere que desejamos realizar a leitura do arquivo exemplo.txt. O conteúdo deste arquivo é exibido abaixo: Este é um texto de exemplo. 2 3,57 Ele possui 5 linhas de informação. Esta é a última linha. Se abrirmos este arquivo e passarmos ele como parâmetro ao método leArquivoTokenAToken, o método retornará o conteúdo do arquivo formatado da seguinte forma: Esteéumtextodeexemplo.23,57Elepossui5linhasdeinformação.Estaéaúltimalinha. Isto ocorre devido a forma como a leitura do arquivo é realizada. Para ler cada token do conteúdo do arquivo por vez, foi utilizado o método next() da classe Scanner. Este método faz a leitura do próximo token do arquivo, isto é, a próxima palavra do arquivo, ignorando os espaços em branco, tabulações ou pulos de linha entre o token atual e o próximo token. Assim, como o resultado a ser retornado pelo método leArquivoTokenAToken consiste na concatenação de todos os tokens lidos, a string resultante é composta por todos os tokens sem qualquer separação entre eles. Observe que existe um laço de repetição para controlar que a leitura do arquivo seja realizada até que não haja um próximo token a ser lido, utilizando o método hasNext() da classe Scanner para realizar tal verificação. Este método retorna true caso haja um próximo token a ser lido ou false caso contrário. A versão abaixo do método leArquivoTokenAToken retorna todos os tokens lidos do arquivo, porém separados em linhas distintas: public String leArquivoTokenAToken(File arquivo) { if (arquivo != null) { try { String textoArquivo = ""; Scanner leitor = new Scanner(arquivo); while (leitor.hasNext()) { textoArquivo += leitor.next() + "\n"; } leitor.close(); return textoArquivo; } catch (FileNotFoundException e) { System.out.println("Arquivo nao pode ser lido!"); e.printStackTrace(); } } return ""; } Alternativamente, é possível realizar a leitura linha a linha do arquivo. Para tanto, basta utilizar o método hasNextLine() da classe Scanner para realizar a leitura dos dados do arquivo. Para garantir que todas as linhas do arquivo sejam lidas, podemos criar um laço de repetição que seja executado enquanto haja uma nova linha a ser lida, utilizando o método hasNextLine() para realizar tal verificação. De forma análoga ao hasNext(), este método retorna true caso haja uma nova linha a ser lida ou false caso contrário. O método abaixo utiliza esta estratégia para ler linha por linha do arquivo desejado: public String leArquivoLinhaALinha(File arquivo) { if (arquivo != null) { try { String textoArquivo = ""; Scanner leitor = new Scanner(arquivo); while (leitor.hasNextLine()) { textoArquivo += leitor.nextLine() + "\n"; } leitor.close(); return textoArquivo; } catch (FileNotFoundException e) { System.out.println("Arquivo nao pode ser lido!"); e.printStackTrace(); } } return ""; } Ainda utilizando a classe Scanner, é possível realizar leituras com conversões embutidas, da mesma forma que é realizado na leitura de dados da entrada padrão. Por exemplo, analisando o arquivo exemplo.txt, é possível verificar que algumas linhas do arquivo possuem informações numéricas. Assim, pode-se utilizar os métodos nextInt() e nextDouble() da classe Scanner de forma estratégica para ler estes valores e, indiretamente, convertê-los no momento da leitura. O programa abaixo realiza a leitura do arquivo exemplo.txt, coletando seus dados e armazenando-os diretamente em variáveis apropriadas aos seus tipos: package br.edu.ifms.lp2.arquivo; import java.io.File; import java.io.FileNotFoundException; import
Compartilhar