Baixe o app para aproveitar ainda mais
Prévia do material em texto
Interfaces de BD Aula 3: Coleções e entidades Apresentação As exceções são o mecanismo pelo qual um sistema detecta erros em tempo de execução, iniciando rotinas de recuperação apropriadas, algo inerente à própria arquitetura do computador, re�etindo no sistema operacional e nas linguagens. Ao contrário de algumas linguagens que apenas permitem o tratamento das exceções, no Java, o compilador exige esse tratamento para a maioria delas, algo que torna obrigatório o conhecimento acerca da de�nição e do uso de exceções. Outro assunto de grande relevância é o uso de coleções na gerência de dados, algo muito mais adequado para o ambiente orientado a objetos que o simples uso de vetores, como era prática comum no modelo estruturado. A grande vantagem do uso de coleções é que não estamos restritos a uma quantidade �xa de elementos, e o Java Collection Framework (JCF) oferece diversos tipos de coleções, adaptáveis aos mais diversos contextos. Aliando os conhecimentos acerca de coleções e o uso de exceções, seremos capazes de representar relacionamentos entre classes de entidade e efetuar todas as operações de manipulação de dados de uma forma robusta, conforme veremos nesta aula, fundamentando diversos passos posteriores da disciplina. Objetivos Explicar o funcionamento e o uso de exceções; Explicar o uso de coleções com Java Collections Framework (JCF); Utilizar entidades e coleções na representação de dados e relacionamentos. Exceções Muitas vezes temos que preparar nossos sistemas para a possibilidade de ocorrência de erros de execução, talvez pela indisponibilidade de recursos, como rede ou espaço em disco, ou pela formatação incorreta da entrada de dados, entre diversas outras situações. Esses erros, quando não tratados, fazem a execução do sistema ser interrompida e até podem travar a máquina, levando à necessidade de ferramentas para lidar com eles. Na aula anterior, já fomos obrigados a trabalhar com exceções. Essa é uma necessidade natural de qualquer ambiente em que são executadas as rotinas, pois mesmo as operações mais básicas podem gerar erros, como a divisão por zero ocorrendo na Unidade Lógica e Aritmética (ULA), no nível do processador. Fonte: Shuttershock por Profit_Image Cada ambiente poderá trazer uma forma diferente de lidar com as exceções e oferecer meios para a recuperação do contexto, e normalmente esses modelos de tratamento apresentarão uma complexidade equivalente à do ambiente no qual executam. Partindo do meio mais básico, a linguagem Assembly precisa lidar com as exceções por meio de interrupções, registradores e endereços de memória especí�cos. Logo acima do Assembly, temos o sistema operacional, com um conjunto de rotinas para armazenar o contexto, identi�car o erro, direcionar para o tratamento necessário, recuperar o contexto e selecionar o processo seguinte a ser executado. Em linguagens mais básicas, como ANSI C, não temos ferramental próprio para o tratamento das exceções, e o resultado mais comum é o retorno ao sistema operacional, abortando a execução do programa e apresentando o console de execução. Já em linguagens derivadas do C++, como Java e C#, assim como outras plataformas que permitem a orientação a objetos, as exceções devem ser encapsuladas em classes próprias, trazendo código e mensagem do erro, além de informações complementares, a depender da especialização que foi utilizada para representar o erro ocorrido. Na linguagem Java temos o encapsulamento das exceções em uma família de classes próprias, descendentes de Throwable. Na classe inicial temos apenas a descrição básica, sendo o detalhamento para cada caso capturado no nível dos descendentes. Fonte: Oracle. A classe Exception deriva diretamente de Throwable, e serve de base para quase todos os tipos de erro, sendo a primeira exceção checada, ou seja, que tem o tratamento exigido na compilação, comportamento diferente de outras linguagens, nas quais o tratamento de exceções é opcional. As exceções não checadas do Java, ou seja, que não exigem o tratamento para permitir a compilação, são uma minoria, e derivam de Error, �lha de Throwable, ou RuntimeException, descendente de Exception. Enquanto a primeira é voltada para problemas sérios demais para serem tratados pelo mecanismo padrão, como um erro da máquina virtual, a segunda é utilizada para situações muito comuns, as quais amentariam demasiadamente o código com a exigência do tratamento, como divisão por zero e objeto nulo. Além de utilizar as diversas classes de exceções disponíveis na plataforma, podemos criar nossas próprias classes, derivando de uma das três classes de base, com o objetivo de personalizar o tratamento de erros em eventos especí�cos de nossos sistemas. Exceções são tão importantes para o Java que fazem parte da documentação padrão dos pacotes, em uma divisão própria, da mesma forma que as interfaces e as classes. public class ErroCalc extends Exception{ private final int a,b; public ErroCalc(int a, int b){ super("Erro com os numeros "+a+" e "+b); this.a = a; this.b = b; } public int getA() { return a; } public int getB() { return b; } } Nesse exemplo temos a de�nição de uma exceção customizada que poderia ser utilizada para indicar a ocorrência de erro em um cálculo que envolva dois números inteiros. A nova exceção deriva de Exception, indicando tratamento obrigatório para a compilação, e de�ne um construtor com dois números inteiros, que são utilizados na formação da mensagem do erro. A mensagem é repassada ao ancestral com a chamada ao construtor correto, via super, e ainda temos atributos para a armazenagem dos valores envolvidos, com getters para o acesso externo em modo de leitura. Agora que vimos como são encapsuladas as exceções no Java, devemos analisar as instruções de controle de �uxo necessárias para lidar com esse per�l de classes. Estruturas para �uxo de exceção Enquanto na programação estruturada temos uma veri�cação de erros com a constante antecipação de problemas, baseada em estruturas de decisão e na de�nição de �ags, um pouco da herança do Assembly, na orientação a objetos lidamos com exceções, e as estruturas tradicionais de �uxo não seriam su�cientes. Na linguagem Java temos uma estrutura própria para controlar o �uxo de exceção, a qual pode utilizar os comandos try (tentar), cacth (capturar) e �nally (execução obrigatória). try { // Bloco de comandos protegido } catch (IOException e1) { // Tratamento de erro de IO } catch (Exception e2) { // Tratamento de erro genérico } finally { // Execução obrigatória, com ou sem erros } A instrução try de�ne um bloco protegido, impedindo que o programa seja interrompido caso ocorra uma exceção naquele trecho de código. Funciona de forma similar ao tratamento efetuado pelo sistema operacional, já que guarda o contexto para qualquer necessidade de recuperação. Com a ocorrência de uma exceção, o �uxo é direcionado, inicialmente, para uma instrução catch voltada para o tipo de exceção ocorrida. Se for uma exceção do tipo IOException, ela é capturada na variável e1, executando o primeiro bloco de captura, e se for qualquer outra será capturada em e2, com a execução do segundo bloco. Devemos lembrar que as regras da orientação a objetos são plenamente aplicáveis às classes de exceção. Como um descendente pode ser utilizado no lugar do original, e temos a classe Exception como base da hierarquia padrão, qualquer exceção do grupo checado será capturada no segundo bloco de tratamento, enquanto uma exceção do tipo FileException seria capturada no primeiro catch. Atenção Se invertermos a ordem dos blocos catch ocorrerá um erro de compilação, pois o fato de Exception ser mais genérico impediria a captura de quaisquer outras exceções. Ao término do tratamento do erro, ou na �nalização do bloco protegido sem erros, o �uxo é direcionado para o bloco �nally. Este executa mesmo na ausência de exceções, mas não efetua o tratamento do erro, ou seja, sem um comando catch, embora seja forçada a execução do bloco�nally, a responsabilidade de tratamento seria de quem invocou o método. Um exemplo prático em que utilizaríamos essas instruções seria na inserção de dados em um banco, sendo que devemos abrir o banco, inserir os registros e fechar o banco. Ocorrendo ou não erros durante a inserção, o banco deve ser fechado. ABRIR_BANCO_DE_DADOS(); try { INSERIR(); INSERIR(); // Diversos comandos de inserção } catch (Exception e) { // Tratamento de erro genérico } finally { FECHAR_BANCO_DE_DADOS(); } Para qualquer exceção checada, o compilador irá considerar como erro uma possibilidade de ocorrência dela que não seja tratada. Nesses casos precisamos utilizar uma estrutura try..catch, ou avisar ao compilador que a exceção não será tratada, mas sim ecoada para quem invocou o método, por meio da assinatura com throws (ecoar). public class Calculadora { public int somar(int a, int b){ return a+b; } public int dividir(int a, int b) throws ErroCalc { if(b==0) throw new ErroCalc(a, b); return a/b; } } A marcação com throws sinaliza para o compilador que poderá ocorrer uma exceção daquele tipo na chamada ao método. Na classe Calculadora temos o método somar, em que não é gerada exceção, e dividir, que lança ErroCalc quando o parâmetro b tem valor zero. A instrução throw (lançar) serve para sinalizar o modo de falha e emitir o objeto de exceção no ambiente. Note que no método dividir, além de alocar um objeto do tipo ErroCalc com new, foi necessário o uso do comando throw para lançar a exceção. Caso tente compilar a classe Calculadora sem assinar o método com throws, verá que o compilador indica erro, sugerindo o uso de try..catch, mas esse não é o nosso objetivo, já que criamos a classe ErroCalc justamente para permitir a sinalização da falha de modo personalizado. Queremos emitir o erro para que seja tratado por quem invocou o método, ou seja, estamos no papel do fornecedor da biblioteca e não do programador de aplicativos. public class TesteCalc { public static void main(String[] args) { Calculadora c1 = new Calculadora(); try { System.out.println(c1.somar(2, 3)); System.out.println(c1.dividir(6, 3)); System.out.println(c1.dividir(6, 0)); } catch (ErroCalc e) { System.out.println(e.getMessage()); } } } Nesse exemplo, temos uma instância de Calculadora e algumas operações simples. Quando o método dividir é invocado em meio ao código, a IDE nos avisa imediatamente que um erro pode ocorrer, devido à assinatura com throws, e oferece a possibilidade de delimitar automaticamente o bloco em uma estrutura do tipo try..catch. Nas atuais versões do Java, contamos ainda com o try with resources, que simpli�ca a escrita com a desalocação automática do recurso alocado, sendo voltado para operações de entrada e saída. Ele evita a necessidade, por exemplo, de um bloco �nally para o fechamento de um arquivo. static String inicioArquivo(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } No método de exemplo temos, como recurso, um objeto BufferedReader apontando para o arquivo descrito no parâmetro path, e a primeira linha do arquivo é retornada sem a necessidade de fechá-lo. Outra opção adicionada foi a possibilidade de utilização do operador binário or na captura de múltiplas exceções simultaneamente, evitando que haja replicação de código em tratamentos que são idênticos. try { dao.inserir(produto); } catch (IOException|SQLException ex) { logger.log(ex); throw ex; } Coleções no Java Na programação estruturada é comum o uso de vetores e matrizes para a manipulação de conjuntos de dados de um mesmo tipo, e se quisermos que a estrutura de armazenamento trabalhe com dimensões variáveis, precisaremos de ponteiros, como na criação de listas e árvores. Embora sejam soluções viáveis, exigem cuidados com relação à liberação da memória, e muitos programadores não gostam da complexidade envolvida. Saiba mais Nos ambientes com elementos de orientação a objetos, a exemplo do Java, é comum a presença de repositórios alocados dinamicamente, com as listas e seus ponteiros já encapsulados, recebendo o nome de coleções. As coleções são de grande importância para o Java, sendo organizadas pela Oracle em um grupo denominado Java Collections Framework (JCF), o qual foi todo implementado com o uso de classes genéricas. A relevância dessa família de classes é comprovada pelo fato de ter sido adicionada uma funcionalidade para a estrutura for, no estilo foreach, baseada no uso de coleções, a qual, posteriormente, foi expandida para vetores. for(Pessoa p: pessoaDAO.obterTodos()){ System.out.println(p.getNome()); } A leitura que devemos fazer para um comando for nesse estilo é para cada elemento pertencente à coleção faça, ou seja, supondo que o comando do objeto DAO de exemplo retorne a coleção de entidades do tipo Pessoa, foi de�nido um objeto p, do tipo Pessoa, que percorrerá toda a coleção, permitindo a impressão do nome de cada entidade englobada. Comentário Em várias outras linguagens existem estruturas com o comando foreach, que apresentam funcionamento similar ao for descrito aqui. O núcleo do JCF é composto por um grupo de interfaces, de�nindo aspectos abstratos das coleções que serão implementadas. São duas famílias, uma com base em Collection, que permitirá a de�nição de listas e conjuntos, e outra baseada em Map, voltada para mapeamentos do tipo chave-valor. Em termos arquiteturais, o JCF utiliza modelagem comportamental em todos os níveis, com interfaces e classes baseadas em elementos genéricos. Essa abordagem permitiu a criação de coleções de qualquer tipo de objeto, sem a necessidade de type cast, o que era uma fragilidade encontrada na versão antiga dessa biblioteca. Todas as interfaces englobadas são voltadas para a gerência de grupos de objetos, mas cada especialização tem alguma peculiaridade, como podemos observar no quadro seguinte. Interface Característica da Coleção Collection Base para coleções sem mapeamento Set Conjuntos de objetos (não permitem duplicidade) SortedSet Conjuntos ordenados de objetos List Listas de objetos (permitem duplicidade) Queue Filas de objetos FIFO (first-in-first-out) Deque Filas com inserções e remoções em ambos os lados Map Base para mapeamentos chave-valor SortedMap Mapeamentos chave-valor ordenados pela chave As classes concretas do JCF devem implementar alguma dessas interfaces, de�nindo as funcionalidades previstas por meio de algoritmos próprios. Estão presentes também algumas classes abstratas, que trazem rotinas comuns na implementação dessas interfaces, como AbstractList e AbstractSet, as quais também implementam Iterable, para viabilizar a utilização do for no estilo foreach. Listas e conjuntos Clique no botão acima. As coleções no estilo de lista permitem a organização dos dados na forma de uma lista encadeada, e podem ser percorridas pelos ponteiros que de�nem a ligação entre os nós de objetos. A utilização é transparente, sem a necessidade de mais conhecimentos acerca da estrutura interna. Uma lista é o meio mais comum para a criação de repositórios de entidades e mapeamento de dados obtidos a partir de bancos relacionais. Por ter uma utilização simples, e não se preocupar com indexação, pode ser preenchida de uma forma muito e�ciente. As principais descendências de AbstractList são Vector e ArrayList, sendo que a primeira de�ne elementos de suporte ao acesso concorrente, o que a deixa menos ágil. Em termos práticos é mais comum o uso de ArrayList, já que permite acesso e manipulação mais e�cientes. Nesse exemplo de lista temos uma coleção do tipo ArrayList, com elementos do tipo String, sendo adicionados três elementos na coleção pelo método add. Depois de adicionar os elementos, percorremos a lista com uso do for, e a variável x receberá os valores adicionados sequencialmente, imprimindo um nó de valor a cada rodada. Os principais métodos das coleções, no estilo de lista, são apresentadosno quadro seguinte. import java.util.ArrayList; public class ExemploLista001 { public static void main(String[] args) { ArrayList lista = new ArrayList<>(); lista.add("Primeiro"); lista.add("Segundo"); lista.add("Terceiro"); for(String x: lista){ System.out.println(x); } } } Método Funcionalidade clear Remove todos os elementos da lista contains Retorna true quando o objeto pertence à lista get Retorna o elemento na posição selecionada add Adiciona o elemento ao final da lista remove Remove o objeto na posição e retorna o objeto removido size Retorna o tamanho da lista O uso de conjuntos é indicado para grupos de dados menores, que exigem acesso rápido e constante durante a execução do programa. Temos várias implementações, como EnumSet, para a formação de conjuntos sobre tipos enumerados, e HashSet, que detecta duplicidades pelo hash do objeto. O uso de enumerados é viável apenas em grupos muito pequenos, em que os valores são �xos, como conceitos de avaliação e indicadores de status. Para grupos de porte um pouco maior, que podem ser modi�cados em algum momento, mesmo que raro, seria melhor a adoção de HashSet. Observe que nesse exemplo utilizamos o operador funcional forEach, uma alternativa ao uso do for. Um operador funcional, ou lambda, captura cada elemento de trabalho na variável interna e a utiliza no escopo do bloco de execução, sendo útil na simpli�cação de escrita para percorrer coleções ou para responder a eventos em interfaces grá�cas. Note que o HashSet permite o acréscimo de elementos de qualquer tipo, e se quisermos utilizar com uma classe própria devemos sobrescrever equals e hashCode na classe. import java.util.EnumSet; enum Avaliacao{RUIM, REGULAR, BOM, OTIMO} public class ExemploConjunto001 { public static void main(String[] args) { EnumSet conjunto = EnumSet.allOf(Avaliacao.class); conjunto.forEach((a) -> { System.out.println(a); }); } } import java.util.HashSet; public class ExemploConjunto002 { public static void main(String[] args) { HashSet conjunto = new HashSet<>(); conjunto.add("RJ"); conjunto.add("SP"); conjunto.add("MG"); conjunto.add("ES"); if(conjunto.contains("RJ")){ // Execução condicionada } } } Nesse exemplo sobrescrevemos o método toString, que é utilizado para a conversão implícita do objeto em um texto representativo, além de equals, para a comparação do conteúdo de dois objetos, e hashCode, que de�ne um identi�cador único para o objeto. public class Cachorro { private final String nome; private final String raca; public Cachorro(String nome, String raca) { this.nome = nome; this.raca = raca; } @Override public String toString() { return nome+"::"+raca; } @Override public int hashCode() { return toString().hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Cachorro other = (Cachorro) obj; return toString().equals(other.toString()); } } Mapeamento Os mapeamentos no estilo chave-valor permitem a de�nição de grupos de objetos acessíveis de forma rápida a partir de uma chave de indexação. Eles são muito utilizados em repositórios de propriedades ou grupos de ações no padrão Command para o sistema. A classe concreta de maior utilização para esse tipo de coleção é HashMap, baseada em um tipo para a chave e outro para o objeto. import java.util.HashMap; public class ExemploMapeamento { public static void main(String[] args) { HashMap<Integer,String> mapa = new HashMap<>(); mapa.put(1,"UM"); mapa.put(2,"DOIS"); System.out.println(mapa.get(2)); } } Esse é um exemplo simples, com um mapeamento de inteiro para texto, em que são adicionadas duas ocorrências pelo método put, e temos a impressão do texto "DOIS", obtido a partir da chave de valor 2, com o uso do método get. Os principais métodos presentes na classe HashMap podem ser observados no quadro seguinte. Método Funcionalidade clear Remove todos os elementos do mapeamento Remove todos os elementos do mapeamento Retorna true quando a chave pertence ao mapeamento containsValue Retorna true quando o objeto pertence ao mapeamento get Retorna o objeto indexado pela chave put Adiciona o objeto indexado pela chave remove Remove o objeto indexado pela chave size Retorna o tamanho do mapeamento keySet Retorna um conjunto com as chaves do mapeamento values Retorna uma coleçao com as objetos do mapeamento As estruturas de coleções podem ser combinadas, representando relações muito mais complexas, por exemplo, um mapeamento com uma lista de departamentos para cada setor da empresa. HashMap> mapaSetores = new HashMap<>(); Entidades e relacionamentos Em termos de bancos de dados, um tipo de entidade é representado como uma tabela, e as diversas instâncias armazenadas nos registros expressam os valores de seus atributos nos campos de�nidos para a tabela. Para que ocorra a individualização de cada instância é necessário um campo que não permite valores repetidos, ou seja, a chave primária. Se queremos representar esse ambiente em termos de objetos, precisamos seguir os mesmos princípios utilizados em nossos bancos de dados, ou seja, devemos criar classes de dados, sem métodos de negócios, e contendo um atributo que permita a individualização das instâncias. Em geral é utilizado um campo inteiro, que permita a recepção de valores gerados pelo banco, mas qualquer estrutura pode ser utilizada, inclusive campos compostos. Para a de�nição de uma entidade não podemos pensar em cadastro, mas na simples expressão dos dados. As entidades devem apresentar somente os atributos de dados, devidamente encapsulados, sendo que um deles identi�ca de forma unívoca cada instância, além de alguns construtores. import java.util.Date; public class Funcionario { private String matricula; private String nome; private String identidade; private Long cpf; private Date dataNascimento; public Funcionario() { } public Funcionario(String matricula) { this.matricula = matricula; } // getters/setters } Nesse exemplo temos a entidade Funcionario, em que o atributo matricula representa o identi�cador da instância, com todos os atributos encapsulados por meio de getters e setters. Também foram de�nidos dois construtores, um vazio, ou construtor padrão, e outro baseado no atributo de identi�cação, equivalente a uma chave primária. Após de�nir Funcionario, vamos criar a entidade Empresa, em que teremos um claro relacionamento de um para muitos. Como uma empresa está associada a diversos funcionários, uma forma simples de implementar a relação será com o uso de uma lista. import java.util.ArrayList; import java.util.List; public class Empresa { // Relação 1xN private final List funcionarios = new ArrayList<>(); public List getFuncionarios(){ return funcionarios; } private String CNPJ; private String nome; public Empresa() { } public Empresa(String CNPJ) { this.CNPJ = CNPJ; } // getters/setters } A classe Empresa segue os mesmos moldes de Funcionario, com a de�nição de atributos internos, getters e setters, além de um construtor vazio e outro baseado no identi�cador único. A relação de um para muitos foi de�nida no atributo funcionarios, uma coleção do tipo ArrayList. Mas temos que pensar na implementação correta das relações, e se criamos na empresa um atributo que nos leva aos funcionários, devemos modi�car a classe Funcionario para adicionar uma referência para Empresa, no caso apenas um atributo simples, do tipo da classe, além do getter e o setter. public class Funcionario { // Relação Nx1 private Empresa empresa = null; public Empresa getEmpresa(){ return empresa; } public void setEmpresa(Empresa empresa){ this.empresa = empresa; } // Todo o código anterior é mantido } Com as classes de entidade criadas, vamos implementar um repositório, e assim como nos bancos de dados temos tabelas e chaves primárias, aqui teremos mapeamentosdo tipo chave-valor. Vamos de�nir um repositório apropriado para as entidades, com o uso de atributos HashMap estáticos. import java.util.HashMap; public class Repositorio { public static HashMap funcionarios = new HashMap<>(); public static HashMap empresas = new HashMap<>(); } Como temos duas entidades, nosso repositório contará com dois mapas, um para funcionários e outro para empresas, algo similar a um banco de dados com duas tabelas. Atenção Elementos estáticos pertencem à classe, e não às instâncias, o que os torna globais, mas os níveis de acesso ainda são aplicáveis. Os repositórios, de forma geral, são elementos passivos, voltados apenas para a armazenagem de dados, o que nos leva à necessidade de gestores para as consultas e modi�cações dos dados. Os processos envolvidos são bastante repetitivos, con�gurando uma boa oportunidade para a utilização de elementos genéricos. import java.util.Collection; import java.util.HashMap; public abstract class Gestor { protected abstract K extractKey(E entidade); protected abstract HashMap<K,E> getMapeamento(); public void criar(E entidade){ getMapeamento().put(extractKey(entidade),entidade); } public void remover(K chave){ getMapeamento().remove(chave); } public E obter(K chave){ return getMapeamento().get(chave); } public Collection obterTodos(){ return getMapeamento().values(); } } A classe genérica Gestor apresenta os parâmetros E, do tipo da entidade, e K, representando a chave. Ela é abstrata, pois não tem como de�nir qual o mapeamento para armazenagem, nem como extrair a chave da entidade, o que é previsto nos métodos abstratos getMapeamento e extractKey, que deverão ser implementados pelos descendentes. O método criar acrescenta a entidade no mapa do repositório, via método put, baseado na chave extraída a na própria entidade, enquanto remover elimina a entidade do repositório pelo método remove e o valor da chave passada como parâmetro. Os métodos de consulta são bastante similares, sendo que obter utiliza get com o valor da chave para recuperar uma entidade individual, e obterTodos se baseia na coleção fornecida por meio do método values, retornando todo o conjunto de entidades. Com a estrutura básica pronta, precisamos especializar a classe Gestor para os domínios de Funcionario e Empresa. import java.util.HashMap; public class GestorFuncionario extends Gestor{ @Override protected String extractKey(Funcionario entidade) { return entidade.getMatricula(); } @Override protected HashMap getMapeamento() { return Repositorio.funcionarios; } } Como podemos observar em GestorFuncionario, foi necessário apenas implementar os métodos para a extração da chave, no caso a matrícula, e obtenção do HashMap correto (Repositorio.funcionarios). As operações herdadas se adaptarão às classes especi�cadas, e os processos de consulta e modi�cação serão disponibilizados de forma automática. import java.util.HashMap; public class GestorEmpresa extends Gestor<String,Empresa> { @Override protected String extractKey(Empresa entidade) { return entidade.getCNPJ(); } @Override protected HashMap<String, Empresa> getMapeamento() { return Repositorio.empresas; } public void adicionarFuncionario(Empresa empresa, Funcionario funcionario){ empresa.getFuncionarios().add(funcionario); funcionario.setEmpresa(empresa); } public void removerFuncionario(Empresa empresa, Funcionario funcionario){ empresa.getFuncionarios().remove(funcionario); funcionario.setEmpresa(null); } @Override public void remover(String chave) { for(Funcionario f: super.obter(chave).getFuncionarios()) f.setEmpresa(null); super.remover(chave); } } Para GestorEmpresa temos uma implementação similar, com o retorno do CNPJ na extração da chave, e o uso de Repositorio.empresas. Porém, o método remover precisou ser sobrescrito para desassociar Funcionario de Empresa, colocando o valor nulo no atributo referente para cada elemento da lista de funcionários, antes de remover a empresa por meio do super. Foram adicionados dois métodos especí�cos, com o objetivo de adicionar e remover funcionários, cuidando sempre de ambos os lados da relação, ou seja, ao adicionar o funcionário na lista da empresa, a empresa envolvida é con�gurada no atributo do funcionário, e quando o funcionário é removido da lista da empresa, o atributo referente é con�gurado com valor nulo. Observando a necessidade de modi�cação em GestorEmpresa, é bastante simples compreender que não seria diferente para GestorFuncionario, e devemos excluir o funcionário da lista da empresa quando ele estiver para ser removido do repositório. import java.util.HashMap; public class GestorFuncionario extends Gestor<String,Funcionario>{ @Override protected String extractKey(Funcionario entidade) { return entidade.getMatricula(); } @Override protected HashMap<String, Funcionario> getMapeamento() { return Repositorio.funcionarios; } @Override public void remover(String chave) { Funcionario funcionario = super.obter(chave); Empresa empresa = funcionario.getEmpresa(); if(empresa!=null) empresa.getFuncionarios().remove(funcionario); super.remover(chave); } } Neste ponto temos todo o nosso sistema para representação e gerência de informações �nalizado, e só precisamos criar a interface de usuário. Vamos utilizar uma interface simples, em modo texto, iniciando com a de�nição de alguns atributos de uso geral. public class Cadastro { static GestorEmpresa gEmpresa = new GestorEmpresa(); static GestorFuncionario gFuncionario = new GestorFuncionario(); static BufferedReader teclado = new BufferedReader( new InputStreamReader(System.in)); } Adicionaremos à nossa classe Cadastro o método gerirFuncionarios, com as operações necessárias para adicionar e listar os funcionários de uma determinada empresa. static void gerirFuncionarios(Empresa empresa){ int opcao; do { System.out.println("EMPRESA > "+empresa.getCNPJ()+"::"+ empresa.getNome()); System.out.println("FUNCIONARIO > 1-Listar Funcionarios "+ "2-Adicionar Funcionario 0-Retornar"); opcao = Integer.parseInt(teclado.readLine()); switch(opcao){ case 1: empresa.getFuncionarios().forEach((f) -> { System.out.println(f.getMatricula()+"::"+f.getNome()); }); break; case 2: System.out.println("Matricula:"); Funcionario f = new Funcionario(teclado.readLine()); System.out.println("Nome:"); f.setNome(teclado.readLine()); System.out.println("Identidade:"); f.setIdentidade(teclado.readLine()); gFuncionario.criar(f); gEmpresa.adicionarFuncionario(empresa, f); break; } }while(opcao!=0); } Esse método tem uma empresa como parâmetro, e um loop que se repete até que o usuário digite o valor 0. A cada rodada são apresentados os dados da empresa, e exibidas as opções de listar funcionários (1), adicionar (2) ou interromper o loop (0). Sendo escolhida a opção de valor 1, serão apresentados matrícula e nome de cada funcionário da empresa. A obtenção dos funcionários ocorre a partir da lista da empresa, percorrida por meio de um operador funcional. Já na opção de valor 2, um objeto do tipo Funcionario é instanciado com o uso da matrícula fornecida pelo usuário, sendo preenchidos também alguns campos da entidade. Ao �nal, o funcionário é adicionado ao repositório com o uso de gFuncionario, da classe GestorFuncionario, além de associado à empresa corrente por meio de gEmpresa, da classe GestorEmpresa. No próximo passo podemos de�nir o nosso método principal, com a simples apresentação do menu com as operações disponíveis para empresas, dentro de um loop que termina com a opção de valor 0. public static void main(String[] args) throws Exception{ int opcao; do{ System.out.println("EMPRESA > 1-Listar 2-Criar "+ "3-Remover 4-Obter 0-Sair "); opcao = Integer.parseInt(teclado.readLine()); Empresa atual = gerirEmpresa(opcao); if(atual!=null) gerirFuncionarios(atual); }while(opcao!=0); } Esse loop apenas captura a opção escolhida e repassa para gerirEmpresa, que será criadoainda, e se o retorno for diferente de nulo, o ciclo de gestão de dados de funcionários é iniciado, com a chamada a gerirFuncionarios, tendo como parâmetro a empresa retornada. static Empresa gerirEmpresa(int opcao){ Empresa atual=null; switch(opcao){ case 1: gEmpresa.obterTodos().forEach((e) -> { System.out.println(e.getCNPJ()+"::"+e.getNome()); }); break; case 2: System.out.println("CNPJ:"); String CNPJ = teclado.readLine(); System.out.println("Nome:"); String nome = teclado.readLine(); Empresa e = new Empresa(CNPJ); e.setNome(nome); gEmpresa.criar(e); break; case 3: System.out.println("CNPJ"); gEmpresa.remover(teclado.readLine()); break; case 4: System.out.println("CNPJ"); atual = gEmpresa.obter(teclado.readLine()); break; } return atual; } O método gerirEmpresa é bastante simples, apresentando as opções para listar todas as empresas (1), criar a empresa (2), remover a empresa (3), e retornar uma empresa já criada (4). A listagem das empresas se resume à impressão do CNPJ e nome de cada empresa da coleção obtida a partir da chamada ao método obterTodos do gestor. Quanto à criação e à remoção, são solicitados os dados ao usuário e efetuadas as chamadas corretas ao gestor de empresas, sendo instanciada uma empresa na criação e repassada a chave diretamente na remoção. Quanto à obtenção individual, ela apenas solicita o CNPJ ao usuário, e altera o valor de atual para a empresa obtida na chamada ao método obter. Essa empresa será retornada para a rotina principal para iniciar o ciclo de gestão de funcionários, não precisando da impressão de dados nesse nível. Com o código de Cadastro completo, podemos executar o programa e efetuar alguns testes. Em algumas situações temos relacionamentos do tipo um para um, em que a con�guração se resume a um atributo do tipo da ponta oposta em ambos os lados. Por exemplo, poderíamos separar os dados de endereço em uma entidade própria, e relacionar com Funcionario. public class Endereco { // Relação 1X1 - entidade fraca private final Funcionario funcionario; public Funcionario getFuncionario() { return funcionario; } private String logradouro; private String Bairro; private String cidade; public Endereco(Funcionario funcionario) { this.funcionario = funcionario; } // getters/setters } Em relacionamentos que ocorrem nesse formato, temos uma entidade forte, cuja existência é independente, e uma fraca, que necessita da outra para existir. Devido à dependência descrita, o construtor necessita da entidade forte, e o atributo de relacionamento pode ser apenas lido, como podemos observar no código de Endereco. A entidade forte desse exemplo é Funcionario, que deve ter uma pequena alteração no código para comportar o novo relacionamento. Basta de�nir o atributo privado do tipo Endereco, além dos getters e setters. public class Funcionario { // Relação 1X1 - entidade forte private Endereco endereco = null; public Endereco getEndereco(){ return endereco; } public void setEndereco(Endereco endereco){ this.endereco = endereco; } // Todo o código anterior é mantido } O último tipo de relacionamento seria muitos para muitos, e assim como no banco de dados precisamos de uma tabela de relacionamento, teremos em meio ao código uma classe com esse �m. Em termos práticos, uma relação desse tipo é convertida em dois relacionamentos de um para muitos. Vamos criar uma entidade para representar projetos, nos quais funcionários participam, podendo atuar em mais de um projeto. Como o relacionamento será de muitos para muitos, além da entidade Projeto, criaremos a classe de relacionamento FuncionarioProjeto. public class Projeto { // Relação NxN private final List funcionarios = new ArrayList<>(); public List<FuncionarioProjeto> getFuncionarios(){ return funcionarios; } private Integer codigo; private String nome; private Date inicio; private Date fim; public Projeto() { } public Projeto(Integer codigo) { this.codigo = codigo; } // getters/setters } Podemos observar que a nova entidade não traz nada de especial, a não ser pela lista de objetos de ligação com entidades do tipo Funcionario. Devemos criar a classe de relacionamento para que seja possível a compilação. public class FuncionarioProjeto { // Relacionamento NxN - entidade fraca de ligação private final Funcionario funcionario; public Funcionario getFuncionario() { return funcionario; } private final Projeto projeto; public Projeto getProjeto() { return projeto; } public FuncionarioProjeto(Funcionario funcionario, Projeto projeto) { this.funcionario = funcionario; this.projeto = projeto; } @Override public int hashCode() { return funcionario.hashCode()+projeto.hashCode(); } @Override public boolean equals(Object obj) { try { final FuncionarioProjeto other = (FuncionarioProjeto) obj; return (other.getFuncionario().getMatricula().equals( funcionario.getMatricula())) && (other.getProjeto().getCodigo().equals( projeto.getCodigo())); }catch(Exception e){ return false; } } } Essa classe representa um relacionamento simples, sem atributos, apenas com as referências para as entidades envolvidas. Em um banco de dados, seria uma tabela contendo os valores das chaves primárias de ambas as entidades fortes envolvidas no relacionamento. No repositório os objetos desse tipo de classe serão armazenados em um HashSet, e por causa disso precisamos do hashCode e, principalmente, de um teste de igualdade com equals. Pelo teste de�nido, serão considerados iguais dois objetos dessa classe cujos funcionários tenham matrículas iguais e os projetos tenham o mesmo código. Precisamos alterar a classe Funcionario novamente, de forma a acrescentar a relação com Projeto por meio dos objetos de ligação, assim como ocorre na segunda em relação à primeira. public class Funcionario { // Relação NxN private final List projetos = new ArrayList<>(); public List<FuncionarioProjeto> getProjetos(){ return projetos; } // Todo o código anterior é mantido } O repositório deve ser modi�cado para comportar as novas entidades e os objetos de ligação, estes últimos sem a necessidade de gestores próprios, já que não correspondem a entidades de dados reais. Com isso, teremos o uso de HashMap para Projeto e Endereco, enquanto objetos de ligação do tipo FuncionarioProjeto são armazenados em estruturas mais simples, como HashSet, já que não precisam de busca pela chave. public class Repositorio { public static HashMap<String, Funcionario> funcionarios = new HashMap<>(); public static HashMap<String, Empresa> empresas = new HashMap<>(); public static HashMap<Funcionario, Endereco> enderecos = new HashMap<>(); public static HashMap<Integer, Projeto> projetos = new HashMap<>(); public static HashSet<FuncionarioProjeto> relFuncProj = new HashSet<>(); } A criação de um gestor para Projeto envolve, além das operações básicas, os métodos para a inclusão e a remoção de funcionários, em que temos o teste da operação no nível do HashSet, antes de incluir ou remover o objeto utilizado na ligação, sempre em ambos os lados (Projeto e Funcionario). Para uma implementação mais robusta, deveríamos modi�car o método de remoção, tanto no gestor de projetos quanto no de funcionários, de forma a remover os relacionamentos que �cariam pendentes. Também poderíamos testar a presença de ligações, por meio de size, e impedir a remoção quando o tamanho da lista é maior que zero. public class GestorProjeto extends Gestor{ @Override protected Integer extractKey(Projeto entidade) { return entidade.getCodigo(); } @Override protected HashMap getMapeamento() { return Repositorio.projetos; } public void adicionarFuncionario(Projeto projeto, Funcionario funcionario){ FuncionarioProjeto fp = new FuncionarioProjeto(funcionario, projeto); if(Repositorio.relFuncProj.add(fp)){ projeto.getFuncionarios().add(fp); funcionario.getProjetos().add(fp); } } public void removerFuncionario(Projeto projeto, Funcionario funcionario){FuncionarioProjeto fp = new FuncionarioProjeto(funcionario, projeto); if(Repositorio.relFuncProj.remove(fp)){ projeto.getFuncionarios().remove(fp); funcionario.getProjetos().remove(fp); } } } Com esse conjunto de classes, demonstramos os tipos de relacionamentos mais comuns entre as entidades, e vimos que os modelos relacionais podem ser expressos de forma concisa ao trabalhar com classes e coleções. Não podemos esquecer que as coleções de entidades, assim como tabelas, são passivas, voltadas apenas para a armazenagem dos dados, assim como os repositórios assemelham-se à instância do banco de dados. Da mesma forma que as operações de consulta e manipulação de dados são externas às tabelas, com uso de SQL, procedimentos armazenados e gatilhos, temos na programação o uso de classes próprias para a gestão dos dados contidos nas instâncias de entidades. Ao longo da disciplina, essas semelhanças serão exploradas de forma cada vez mais profunda, algo que �cará evidente quando abordarmos os meios para mapeamento objeto-relacional em Java. Ao �nal, veremos que a interpretação dos dados poderá ser feita exclusivamente por elementos de programação, transformando o banco em um repositório de dados passivo, e a linguagem SQL não será utilizada de forma direta, �cando transparente para o desenvolvedor. Atividades 1. Na arquitetura para gerência de exceções do Java, um método pode avisar para o compilador que estará propenso a emitir determinados tipos de exceções. Qual será a palavra reservada necessária para implementar esse comportamento? a) Throw b) Try c) Catch d) Finally e) Throws 2. Com o uso do Java Collections Framework (JCF), nós temos diversas classes para a criação de coleções de objetos, assemelhando-se ao uso de listas encadeadas para os modelos de programação relacionais. No entanto, os tipos de coleções do JCF apresentam funcionalidades diferenciadas, e um desses tipos caracteriza-se por não aceitar instâncias com valores duplicados. Qual é o tipo de coleção descrito? a) ArrayList b) HashSet c) Vector d) Collection e) Queue 3. Em sistemas que utilizam o padrão Front Control, uma estratégia muito interessante é o encapsulamento das funcionalidades diversas em classes no padrão Command, e as instâncias desses executores são armazenadas em repositórios que permitem sua recuperação a partir de um identi�cador, normalmente em modo texto. Qual classe do JCF seria adequada para a implementação desse repositório? a) ArrayList b) Queue c) HashMap d) Deque e) HashSet 4. Considere os comandos SQL para a criação de uma tabela especí�ca de um banco de dados. Ela participa de um relacionamento de um para muitos com uma segunda tabela, expresso por meio de uma chave estrangeira. CREATE TABLE DEPENDENTE ( ID INTEGER NOT NULL PRIMARY KEY, NOME VARCHAR2(50), MATRICULA VARCHAR2(10)); ALTER TABLE DEPENDENTE ADD CONSTRAINT DEP_FUNCIONARIO FOREIGN KEY (MATRICULA) REFERENCES FUNCIONARIO (MATRICULA); 5. Considere a classe de entidade apresentada a seguir, que não tem relacionamentos com outras entidades, sendo voltada para o log de mensagens texto recebidas via mensageria em um sistema corporativo, e a classe de repositório. Implemente um método que receba um parâmetro assunto, do tipo String, e retorne todas as mensagens em que o título contém o assunto. public class Mensagem { private Long id; private String titulo; private String mensagem; private Date data; public Mensagem() { } public Mensagem(Long id) { this.id = id; } // getters/setters } public class Repositorio { public static HashMap mensagens = new HashMap<>(); } Notas Título modal 1 Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Título modal 1 Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográ�ca e de impressos. Referências CORNELL, G.; HORSTMANN, C. Core Java. 8. ed. São Paulo: Pearson, 2010. DEITEL, P.; DEITEL, H. Java: como programar. 8. ed. São Paulo: Pearson, 2010. FONSECA, E. Desenvolvimento de software. Rio de Janeiro: Estácio, 2015. SANTOS, F. Programação I. Rio de Janeiro: Estácio, 2017. Próxima aula Criação de interfaces grá�cas; Gerência de eventos em sistemas com janelas; Persistência de dados em arquivos. Explore mais Leia os textos: Exceptions (Oracle) Collections in Java – 13 Things You MUST Know Keeping ArrayLists safe across threads in Java . javascript:void(0); javascript:void(0); javascript:void(0);
Compartilhar