Relacionamentos Bi-Direcionais e 1..n

Como vimos, é muito simples criar relacionamentos com cardinalidade 1..1, como o relacionamento EmpresaCidade. Já sabemos que a direção da seta indica que objeto terá acesso ao outro objeto na associação. No relacionamento apresentado, a partir de uma Empresa poderemos saber qual a Cidade em que ela está situada. No entanto, a partir da Cidade, não temos como saber quais empresas estão situadas lá. Se realmente necessitamos deta infromação, precisamos criar um relacionamento no sentido contrário, de Cidade para Empresa.

Ao fazermos isso, estaremos criando um relacionamento bi-direcional entre as duas classes. Na prática, estamos criando dois relacionamentos uni-direcionais em sentidos contrários. Dependendo da ferramenta de desenvolvimento ou modelagem utilizada, isto será representado como um único relacionamento com setas em ambos os lados (bi-direcional) ou duas setas individuais (dois relacionamentos uni-direcionais). O BlueJ adota a segunda opção, que inclusive é mais didática, pois mostra que são de fato relacionamentos individuais, que precisam ser programados separadamente.

Ao criar um relacionamento em um sentido, o relacionamento no sentido contrário não é criado automaticamente, pois podemos simplesmente não precisar dele. Assim, criá-lo é uma escolha, de acordo com as necessidades do sistema. Vamos então criar o relacionamento CidadeEmpresa como 1..n. Para isto, segue-se a lógica utilizada para os outros relacionamentos: dentro da classe de origem (Cidade neste caso), devemos incluir um atributo da classe de destino (Empresa).

No entanto, com relacionamentos 1..n é preciso algumas modificações. Como a cidade pode ter várias empresas, precisaríamos de uma estrutura como um vetor, por exemplo, para permitir que uma cidade guardasse a relação de empresas lá situadas. A declaração de vetores em Java é muito simples, como Empresa []empresas;. Observe que basta utilizar colcheques vazios para declarar um vetor. Porém, em Java, depois que o tamanho de um vetor é definido, não se pode mais aumentar nem diminuir a quantidade de elementos de tal vetor. Se usarmos um vetor para representar as empresas de uma cidade, antes de começar a utilizar tal vetor precisaremos instanciar o mesmo, indicando a quantidade de elementos que o vetor comportará.

Poderíamos fazer isso no construtor da classe Cidade como this.empresas = new Empresa[10];. Com esta linha estamos instanciando (new) um vetor que pode armazenar até 10 empresas, onde:

  • os colchetes indicam que a variável é um vetor,

  • o valor entre eles representa a capacidade do vetor;

  • e Empresa indica o tipo de elementos que podem ser armazenados nas posições do vetor.

O grande problema do uso de vetores para representar relacionamentos 1..n é que não sabemos quantas empresas cada cidade terá. O tamanho escolhido para o vetor pode ser exagerado ou insuficiente. Cada cidade terá uma quantidade diferente de empresas e estas podem aumentar ou diminuir ao longo do tempo. Se o tamanho do vetor for exagerado, vamos desperdiçar memória, criando um vetor com muitas posições que dificilmente serão usadas. Se for insuficiente, chegará um momento em que novas empresas não poderão ser adicionadas à uma cidade.

A única forma de aumentar a quantidade de elementos em um vetor é, criar um novo vetor de maior capacidade e copiar os elementos do vetor antigo para o novo.

Por esses e outros motivos que estão fora do escopo deste curso, o uso direto de vetores em Java é desaconselhável em várias situações. Vetores são excelentes estruturas que permitem armazenar um conjunto de dados sequencialmente na memória. Se você precisa imprimir todos os elementos de um vetor, percorrê-lo utilizando um laço de repetição é incrivelmente rápido. No entanto, devido à problemas como os apresentados no parágrafo anterior, vetores devem ser usados em Java quando você for obrigado a utilizá-los (pois um determinado método exige um vetor) ou:

  • o problema a ser resolvido é muito simples e dispensa qualquer estrutura mais complexa;

  • o vetor será utilizado apenas internamente em um determinado método, sem ser retornando por tal método;

  • você sabe quantos elementos precisa armazenar e o total de elementos não muda;

  • ou há questões de desempenho que você já avaliou e descobriu que a melhor alternativa é o uso de vetores. Nem sempre o uso de vetores será a alternativa mais eficiente e há cursos inteiros sobre estruturas de dados e complexidade de algoritmos que discutem tais aspectos.

Existe uma expressão em computação que diz "Measure, don’t guess", ou seja, "meça, não advinhe", significando basicamente que supor que uma solução vai melhorar o desempenho de um sistema sem ter feito as devidas medições é uma aposta, um "chute". Assim, simplesmente supor que o uso de vetores vai melhorar o desempenho, sem antes ter utilizado ferramentas de medição, é apenas um palpite. Para mais detalhes, veja este artigo (apenas em inglês).

No lugar de vetores, o mais recomendável é o uso da Java Collections Framework (JCF), uma biblioteca de classes para trabalhar com coleções de elementos. Esta é uma biblioteca moderna, eficiente e provavelmente a mais conhecida e utilizada de toda a plataforma Java.

Todo programa Java tende a ter uma coisa em comum: eles usarão coleções.

— Oleg Shelajev
ZeroTurnaround

Como a JCF disponibiliza uma série de classes, isto significa que a grande maioria dos problemas relacionados ao uso de vetores é resolvida por meio de métodos disponibilizados por tais classes. Como indicado anteriormente, a única forma de aumentar o tamanho de um vetor é criar outro e copiar os dados do anterior para o novo vetor. Utilizando vetores diretamente, seria preciso implementar métodos para realizar tais operações. Com a JCF (que chamamos também simplesmente de Collections), todos esses métodos já estão prontos e não precisamos perder tempo reinventando a roda.

Como usar Collections para criar relacionamentos 1..n

No caso do relacionamento 1..n entre CidadeEmpresa, como já sabemos, devemos incluir um atributo Empresa dentro da classe Cidade, que permite armazenar o conjunto de empresas de uma cidade. Vamos fazer isso usando uma das classes da JCF. Mas primeiro, vamos recapitular a sintaxe para a declaração de um vetor, que segue a estrutura ClasseOuTipoPrimitivoDosItemsDoVetor []nomeDaVariavel; como por exemplo Empresa empresas[] que cria um vetor de empresas.

Se você fez lógica de programação ou introdução à programação, sabe que vetores são tipos compostos homogêneos, ou seja, podemos armazenar vários dados, porém todos de um mesmo tipo. As classes da JCF também podem ser classificadas da mesma forma. A declaração de um vetor é divida em 3 partes:

  1. [] (colchetes) para indicar que queremos criar um vetor (sem isto estaremos apenas criando uma variável comum, que armazena um único valor);

  2. ClasseOuTipoPrimitivoDosItemsDoVetor para indicar o tipo primitivo ou classe dos elementos que podem ser armazenados;

  3. nomeDaVariavel o nome da variável que representará o vetor.

Ao declarar uma variável utilizando alguma classe da JCF, precisamos seguir os mesmos passos, mas com uma sintaxe diferente: ClasseDeColecao<ClasseDosItemsDaColecao> nomeDaVariavel. Uma vez que tais classes não são vetores, é necessário uma sintaxe diferente para que o compilador entenda que não queremos criar um vetor. No entanto, também usamos 3 partes para declarar tal variável:

  1. ClasseDeColecao para indicar que queremos criar uma coleção, utilizando alguma das classes do pacote java.util. A classe mais básica para isso é a ArrayList, que representa uma lista de objetos.

  2. <ClasseDosItemsDaColecao> para indicar qual a classe dos elementos que podem ser armazenados (perceba o uso de <> para isto, diferente dos [] usados para vetores).

  3. nomeDaVariavel o nome da variável que representará a coleção.

Um exemplo que cria uma coleção de empresas pode ser ArrayList<Empresa> empresas. Neste caso estamos utilizando um tipo específico de coleção é que uma lista (List), mais especificamente, um determinado tipo de lista que é o ArrayList. Podemos então dizer que a variável empresas é uma lista de empresas.

Diversas linguagens de programação possuem esse conceito de coleções. Apesar de cada linguagem implementar coleções de uma forma diferente, podendo mudar termos e incluir outros, os fundamentos apresentados aqui tornarão mais fácil a utilização de coleções em outras linguagens. Parece ser muito complicado, mas logo você se acostuma, assim como vetores já é um conceito familiar.

Então finalmente, para declararmos nossa lista de empresas, vamos incluir o atributo empresas dentro da classe Cidade, como abaixo:

public class Cidade
{
    private String nome;
    private Estado estado;

    /**
     * Define o relacionamento Cidade -> Empresa como 1..n.
     */
    private ArrayList<Empresa> empresas;

    //Getters e setters omitidos por simplificação
}

Ao tentar compilar o código da classe, será gerado o erro "cannot find symbol - class ArrayList", indicando que a classe ArrayList não foi encontrada. Esta é a primeira vez que vemos tal erro e ele ocorre pois a classe indicada está em um pacote específico. Um diretório no disco é uma forma de representação de pacotes em Java, contendo um conjunto de classes. ArrayList é uma classe da linguagem, disponível no pacote java.util. Desta forma, precisamos importar tal classe para podermos utilizá-la em nosso código, incluindo um comando import nome.do.pacote.NomeDaClassePraImportar; na primeira linha do arquivo java onde a classe importada será usada. Se você já programou em outras linguagens, este conceito de import é o mesmo em linguagens como Python e versões mais recentes do JavaScript. Em outras linguagens temos:

  • include em C e PHP;

  • require em PHP.

Desta forma, a linha import java.util.ArrayList; deve ser incluída como primeira linha do arquivo da classe Cidade. Você pode estar se perguntando porque outras classes como String não precisaram de um import. Isto se deve ao fato de que String é uma classe do pacote chamado java.lang e o compilador Java importa automaticamente qualquer classe desse pacote, nos dispensando deste trabalho. Veremos mais sobre pacotes mais adiante. O código da classe então fica como abaixo.

import java.util.ArrayList;

public class Cidade
{
    private String nome;
    private Estado estado;
    private ArrayList<Empresa> empresas;

    //Getters e setters omitidos por simplificação
}

Voltando ao nosso código, vemos que empresas nada mais é que um atributo da classe Cidade. Assim, o próximo passo seria adicionar um getter e um setter para ele. No entanto, há um porém quando usamos uma coleção. Se incluírmos um setter, ao chamar tal método, precisaremos informar uma lista completa de empresas situadas naquela cidade. Mas não é assim a forma tradicional de se preencher uma lista. Se resolvermos fazer uma lista de compras, vamos incluíndo os elementos em tal lista um a um. Assim também é a forma mais prática de ser feito em programação. No entanto, é muito comum programadores iniciantes simplesmente criarem getter e setter para listas de forma automática, como fazem para qualquer atributo. O getter será útil para sabermos quais empresas há na cidade, mas o setter acabará não sendo muito prático, pois como falado, a lista é mais facilmente preenchida adicionando-se um elemento por vez.

Desta forma, criaremos o getter e, no lugar do setter, vamos criar um método chamado addEmpresa que adicionará uma empresa à lista de empresas da cidade. Assim, o código da classe Cidade ficará como abaixo:

import java.util.ArrayList;

public class Cidade
{
    private String nome;
    private Estado estado;
    private ArrayList<Empresa> empresas;
    //Getters e setters para nome e estado omitidos por simplificação

    public ArrayList<Empresa> getEmpresas(){
        return empresas;
    }

    public void addEmpresa(Empresa empresa){
        empresas.add(empresa);
    }
}

O vídeo a seguir demonstra o processo de instanciação de uma Empresa e uma Cidade. No entanto, como podem ver, ao tentar adicionar uma Empresa criada à lista de empresas da Cidade, ocorre o erro NullPointerException.

O erro ocorre pois estamos tentando utilizar a lista de empresas antes de termos instanciado a mesma. Observe que em nenhum momento, dentro do código da classe Cidade, utilizamos o operador new para criar uma lista vazia e assim podermos inserir empresas nela. Como é óbvio, se vamos fazer uma lista de compras, primeiramente precisamos conseguir, por exemplo, uma folha de papel (preferencialmente em branco) para podermos começar a adicionar os elementos na nossa lista. Este é o passo que nos falta no código acima. Como visto no Capítulo 5, podemos utilizar um construtor para definir valores iniciais para atributos da nossa classe. Como empresas é um atributo, podemos então instanciá-lo em um construtor padrão na classe Cidade, e assim incluir nosso construtor depois do último atributo da classe (preferencialmente), como abaixo:

    public Cidade(){
        this.empresas = new ArrayList<Empresa>();
    }

Se seguirmos os mesmos passos do vídeo acima, agora conseguiremos adicionar empresas à lista de empresas da cidade, uma empresa por vez. Um detalhe que precisamos ter em mente é a forma como uma ArrayList deve ser intanciada. Como já sabemos, a forma mais básica de instanciar qualquer objeto é new NomeDaClasse(). Apesar de a linha no construtor acima parece diferente, ela segue a mesma lógica: utilizamos new seguido da classe do objeto que queremos instanciar. Neste caso, o tipo da variável empresas que armazenará uma referência para tal objeto não é apenas ArrayList, mas sim ArrayList<Empresa>. O tipo de uma variável é o que vem imediatamente antes dela.

Se olharmos a declaração de empresas no código anterior ao mostrado acima, veremos que o que vem antes do nome do atributo é ArrayList<Empresa>. Observe que não há espaço entre ArrayList e <Empresa> (apesar de ser perfeitamente válido incluir espaço), indicando que eles são uma coisa só, representando um único tipo: uma lista de empresas.

Assim, para instanciar tal lista, precisamos usar new ArrayList<Empresa>(); e não new ArrayList();. Esta última forma mostrada funciona, mas não é a maneira correta de instanciar objetos como coleções desde o Java 5, que foi lançado em 2004. Se você usar esta segunda forma, é como se tivesse parado no tempo há mais de uma década, o que seria uma vergonha 😒.

Avançando alguns anos para o Java 7 de 2011, deixamos de precisar indicar o tipo de elementos da coleção no momento de instanciá-la. Só precisamos fazer isso ao declarar as variáveis. Assim, podemos apenas deixar o <> vazio dentro do construtor. Se utilizarmos da forma acima, ferramentas de desenvolvimento vão sugerir que aquela não é a forma mais adequada e atual. Nossa classe então fica como abaixo:

import java.util.ArrayList;

public class Cidade
{
    private String nome;
    private Estado estado;
    private ArrayList<Empresa> empresas;
    //Getters e setters para nome e estado omitidos por simplificação

    public Cidade(){
        /*
        Observe que removemos o tipo dos elementos
        da lista de dentro do <>.
        Isto só pode ser feito no momento de instanciar a lista.
        */
        this.empresas = new ArrayList<>();
    }

    public ArrayList<Empresa> getEmpresas(){
        return empresas;
    }

    public void addEmpresa(Empresa empresa){
        empresas.add(empresa);
    }
}

results matching ""

    No results matching ""