Java e Orientação a Objetos > Modificadores de Acesso e Atributos de Classe

Modificadores de Acesso e Atributos de Classe

"A marca do homem imaturo é que ele quer morrer nobremente por uma causa, enquanto a marca do homem maduro é querer viver modestamente por uma."--J. D. Salinger

Ao final deste capítulo, você será capaz de:

Controlando o acesso

Um dos problemas mais simples que temos no nosso sistema de contas é que o método saca permite sacar independentemente de o saldo ser insuficiente. A seguir, você pode lembrar como está a classe Conta:

class Conta {
    String titular;
    int numero;
    double saldo;

    // ..

    void saca(double valor) {
        this.saldo = this.saldo - valor; 
    }
}

A classe a seguir mostra como é possível ultrapassar o limite de saque usando o método saca:

class TestaContaEstouro1 {
    public static void main(String[] args) {
        Conta minhaConta = new Conta();
        minhaConta.saldo = 1000.0;
        minhaConta.saca(50000); // saldo é só 1000!!
    }
}

Podemos incluir um if dentro do nosso método saca() para evitar a situação que resultaria em uma conta em estado inconsistente, com seu saldo abaixo de 0. Fizemos isso no capítulo de orientação a objetos básica.

Apesar de melhorar bastante, ainda temos um problema mais grave: ninguém garante que o usuário da classe utilizará sempre o método para alterar o saldo da conta. O código a seguir faz isso diretamente:

class TestaContaEstouro2 {
    public static void main(String[] args) {
        Conta minhaConta = new Conta();
        minhaConta.saldo = -200; //saldo está abaixo de 0
    }
}

Como evitar isso? Uma ideia simples seria testar se não estamos sacando um valor maior do que o saldo toda vez que formos alterá-lo:

class TestaContaEstouro3 {

    public static void main(String[] args) {
        // a Conta
        Conta minhaConta = new Conta();
        minhaConta.saldo = 100;

        // quero mudar o saldo para -200
        double novoSaldo = -200;

        // testa se o novoSaldo é válido
        if (novoSaldo < 0) { // 
            System.out.println("Não posso mudar para esse saldo");
        } else {
            minhaConta.saldo = novoSaldo;
        }
    }
}

Esse código iria se repetir ao longo de toda nossa aplicação e, pior, alguém pode esquecer de fazer essa comparação em algum momento, deixando a conta na situação inconsistente. A melhor forma de resolver isso seria forçar quem usa a classe Conta a invocar o método saca e não permitir o acesso direto ao atributo. É o mesmo caso da validação de CPF.

Para fazer isso no Java, basta declarar que os atributos não podem ser acessados de fora da classe por meio da palavra-chave private:

class Conta {
    private double saldo;
    // ...
}

O private é um modificador de acesso (também chamado de modificador de visibilidade).

Marcando um atributo como privado, fechamos o seu acesso em relação a todas as outras classes e fazemos com que o seguinte código não compile:

class TestaAcessoDireto {
   public static void main(String[] args) {
    Conta minhaConta = new Conta();
    //Não compila! Você não pode acessar o atributo privado de outra classe.
    minhaConta.saldo = 1000;
   }
}
    TesteAcessoDireto.java:5 saldo has private access in Conta
                                    minhaConta.saldo = 1000;
                                              ^
    1 error

Na orientação a objetos, é prática quase que obrigatória proteger seus atributos com private (discutiremos outros modificadores de acesso em outros capítulos).

Cada classe é responsável por controlar seus atributos, portanto ela deve julgar se aquele novo valor é válido ou não. Essa validação não deve ser controlada por quem está usando a classe, e sim por ela mesma, centralizando essa responsabilidade e facilitando futuras mudanças no sistema. Muitas outras vezes, nem mesmo queremos que outras classes saibam da existência de determinado atributo, escondendo-o por completo, já que ele diz respeito ao funcionamento interno do objeto.

Repare: quem invoca o método saca não faz a menor ideia de que existe uma verificação para o valor do saque. Para quem for usar essa classe, basta saber o que o método faz e não como exatamente ele o faz (o que um método faz é sempre mais importante do que como ele faz: mudar a implementação é fácil, já mudar a assinatura de um método gerará problemas).

A palavra-chave private também pode ser usada a fim de modificar o acesso a um método. Tal funcionalidade é utilizada em diversos cenários, os mais comuns são: quando existe um método que serve apenas para auxiliar a própria classe e quando há código repetido dentro de dois métodos da classe. Sempre devemos expôr o mínimo possível de funcionalidades com o intuito de criar um baixo acoplamento entre as nossas classes.

Da mesma maneira que temos o private, temos o modificador public, que permite a todos acessarem um determinado atributo ou método :

class Conta {
    //...
    public void saca(double valor) {
        //posso sacar até saldo
        if (valor > this.saldo){ 
            System.out.println("Não posso sacar um valor maior do que o saldo!");
        } else {
            this.saldo = this.saldo - valor;
        }
    }
}

E quando não há modificador de acesso?

Até agora, tínhamos declarado variáveis e métodos sem nenhum modificador como private e public. Quando isso acontece, o seu método ou atributo fica em um estado de visibilidade intermediário entre o private e o public, que veremos mais para frente, no capítulo de pacotes.

É muito comum e faz todo sentido que seus atributos sejam private, e quase todos seus métodos sejam public (não é uma regra!). Desta forma, toda conversa de um objeto com outro é feita por troca de mensagens, isto é, acessando seus métodos. Algo muito mais educado que mexer diretamente em um atributo que não é seu.

Melhor ainda! O dia em que precisarmos mudar como é realizado um saque na nossa classe Conta, adivinhe o local onde precisaríamos modificar? Apenas no método saca, o que faz pleno sentido. Por exemplo, imagine cobrar CPMF de cada saque: basta você modificar ali, e nenhum outro código, fora a classe Conta, precisará ser recompilado. Além do mais: as classes as quais usam esse método nem precisam ficar sabendo de tal modificação. Você precisa apenas recompilar aquela classe e substituir aquele arquivo .class. Ganhamos muito em esconder o funcionamento do nosso método na hora de fazer manutenção e modificações.

Encapsulamento

O que começamos a ver nesse capítulo é a ideia de encapsular, isto é, ocultar todos os membros de uma classe (como vimos acima), além de esconder como funcionam as rotinas (no caso, métodos) do nosso sistema.

Encapsular é fundamental para seu sistema ser suscetível a mudanças: não precisaremos mudar uma regra de negócio em vários lugares, mas, sim, em apenas um único lugar, já que essa regra está encapsulada (veja o caso do método saca).

 {w=90%}

O conjunto de métodos públicos de uma classe é também chamado de interface da classe, pois essa é a única maneira pela qual você se comunica com objetos dessa classe.

Programando voltado à interface, e não à implementação

É sempre bom programar pensando na interface da sua classe, em como seus usuários estarão utilizando-a, e não somente em como ela funcionará.

A implementação em si, o conteúdo dos métodos, não tem tanta importância para o usuário dessa classe, pois ele só precisa saber o que cada método pretende fazer, e não como ele o faz, porque isso pode mudar com o tempo.

Essa frase vem do livro Design Patterns, de Eric Gamma et al., que é cultuado no meio da orientação a objetos.

Sempre que acessamos um objeto, utilizamos sua interface. Existem diversas analogias fáceis no mundo real:

Já temos conhecimentos suficientes para resolver aquele problema da validação de CPF:

class Cliente {
    private String nome;
    private String endereco;
    private String cpf;
    private int idade;

    public void mudaCPF(String cpf) {
        validaCPF(cpf);
        this.cpf = cpf;
    }

    private void validaCPF(String cpf) {
        // série de regras aqui falha caso não seja válida.
    }

    // ..
}

Se alguém tentar criar um Cliente e não usar o mudaCPF para alterar um CPF diretamente, receberá um erro de compilação, já que o atributo CPF é privado. E quando você não precisar verificar o CPF de quem tem mais de 60 anos? Seu método fica o seguinte:

public void mudaCPF(String cpf) {
    if (this.idade <= 60) {
        validaCPF(cpf);
    }
    this.cpf = cpf;
}

O controle sobre o CPF está centralizado: ninguém consegue acessá-lo sem passar por aí. A classe Cliente é a única responsável pelos seus próprios atributos!

Getters e setters

O modificador private faz com que ninguém consiga modificar e tampouco ler o atributo em questão. Com isso, temos um problema: como fazer para mostrar o saldo de uma Conta, uma vez que nem mesmo podemos acessá-lo para leitura?

Precisamos, então, arranjar uma maneira de fazer esse acesso. Sempre que precisamos arrumar uma forma de fazer alguma coisa com um objeto, utilizamos os métodos! Assim, criemos um método, digamos pegaSaldo, para realizar essa simples tarefa:

class Conta {

    private double saldo;

    // outros atributos omitidos

    public double pegaSaldo() {
        return this.saldo;
    }

    // deposita() e saca() omitidos
}

Para acessarmos o saldo de uma conta, podemos fazer:

class TestaAcessoComPegaSaldo {
   public static void main(String[] args) {
    Conta minhaConta = new Conta();
    minhaConta.deposita(1000);
    System.out.println("Saldo: " + minhaConta.pegaSaldo());
   }
}

A fim de permitir o acesso aos atributos (já que eles são private) de uma maneira controlada, a prática mais comum é criar dois métodos, um que retorna o valor, e outro o qual muda o valor.

A convenção para esses métodos é de colocar a palavra get ou set antes do nome do atributo. Por exemplo, a nossa conta com saldo, limite e titular fica assim caso desejarmos dar o acesso da leitura e escrita a todos os atributos:

class Conta {

    private String titular;
    private double saldo;

    public double getSaldo() {
        return this.saldo;
    }

    public void setSaldo(double saldo) {
        this.saldo = saldo;
    }

    public String getTitular() {
        return this.titular;
    }

    public void setTitular(String titular) {
        this.titular = titular;
    }
}

É uma má prática criar uma classe e, logo em seguida, fazer getters e setters para todos seus atributos. Você só deve criar um getter ou setter se tiver a real necessidade. Repare que, nesse exemplo, setSaldo não deveria ter sido criado, pois queremos que todos usem deposita() e saca().

Outro detalhe importante: um método getX não, necessariamente, retorna o valor de um atributo que chama X do objeto em questão. Isso é interessante para o encapsulamento. Imagine a situação: queremos que o banco sempre mostre, como saldo, o valor do limite somado ao saldo (uma prática comum dos bancos que costumam iludir seus clientes). Poderíamos sempre chamar c.getLimite() + c.getSaldo(), mas isso geraria uma situação de replace all quando precisássemos mudar como o saldo é mostrado. Podemos encapsular isso em um método e, por que não, dentro do próprio getSaldo? Repare:

class Conta {

    private String titular;
    private double saldo;
    private double limite; // adicionando um limite a conta

    public double getSaldo() {
        return this.saldo + this.limite;
    }

    // deposita() saca() e transfere() omitidos

    public String getTitular() {
        return this.titular;
    }

    public void setTitular(String titular) {
        this.titular = titular;
    }
}

O código acima não possibilita a chamada do método getLimite(), posto que ele não existe. E nem deve existir enquanto não houver essa necessidade. O método getSaldo() não devolve simplesmente o saldo, e sim o que queremos que seja mostrado como se fosse o saldo. Utilizar getters e setters não só ajuda você a proteger seus atributos como também possibilita ter de mudar algo em um só lugar; chamamos isso de encapsulamento, pois esconde a maneira pela qual os objetos guardam seus dados. É uma prática muito importante.

Nossa classe está totalmente pronta? Isto é, existe a chance de ela ficar com saldo menor que 0? Pode parecer que não, mas e se depositarmos um valor negativo na conta? Ficaríamos com menos dinheiro que o permitido embora não esperássemos por isso. A fim de nos proteger disso, basta mudarmos o método deposita() para que ele verifique se o valor é necessariamente positivo.

Depois disso, precisaríamos mudar mais algum outro código? A resposta é não, graças ao encapsulamento dos nossos dados.

Cuidado com os getters e setters!

Como já dito, não devemos criar getters e setters sem um motivo explícito. No blog da Caelum, há um artigo que ilustra bem esses casos:

http://blog.caelum.com.br/2006/09/14/nao-aprender-oo-getters-e-setters/

Construtores

Quando usamos a palavra-chave new, estamos construindo um objeto. Sempre quando o new é chamado, ele executa o construtor da classe. O construtor da classe é um bloco declarado com o mesmo nome que a classe:

class Conta {
    String titular;
    int numero;
    double saldo;

    // construtor
    Conta() {
        System.out.println("Construindo uma conta.");
    }

    // ..
}

Então, quando fizermos:

Conta c = new Conta();

A mensagem "construindo uma conta" aparecerá. É como uma rotina de inicialização que é chamada sempre que um novo objeto é criado. Um construtor pode parecer, mas não é um método.

O construtor default

Até agora, as nossas classes não tinham nenhum construtor. Então, como é que era possível dar new se todo new chama um construtor obrigatoriamente?

Quando você não declara nenhum construtor na sua classe, o Java cria um para você. Esse construtor é o construtor default. Ele não recebe nenhum argumento e o seu corpo é vazio.

A partir do momento que você declara um construtor, o construtor default não é mais fornecido.

O interessante é que um construtor pode receber um argumento, inicializando, assim, algum tipo de informação:

class Conta {
    String titular;
    int numero;
    double saldo;

    // construtor
    Conta(String titular) {
        this.titular = titular;
    }

    // ..
}

Esse construtor recebe o titular da conta. Desta maneira, quando criarmos uma conta, ela já terá um determinado titular.

String carlos = "Carlos";
Conta c = new Conta(carlos);
System.out.println(c.titular);

A necessidade de um construtor

Tudo estava funcionando até agora. Para que utilizamos um construtor?

A ideia é bem simples. Se toda conta precisa de um titular, como obrigar todos os objetos que forem criados a ter um valor desse tipo? É só criar um único construtor que receba essa String!

O construtor se resume a isso! Dar possibilidades ou obrigar o usuário de uma classe a passar argumentos para o objeto durante o seu processo de criação.

Por exemplo, não podemos abrir um arquivo para leitura sem dizer qual é o nome do arquivo que desejamos ler. Portanto, nada mais natural que passar uma String representando o nome de um arquivo na hora de criar um objeto do tipo de leitura de arquivo, e que isso seja obrigatório.

Você pode ter mais de um construtor na sua classe, e, no momento do new, o construtor apropriado será escolhido.

Construtor: um método especial?

Um construtor não é um método. Algumas pessoas o chamam de um método especial, mas, definitivamente, não o é, uma vez que não tem retorno e só é chamado durante a construção do objeto.

Chamando outro construtor

Um construtor só pode rodar durante a construção do objeto, isto é, você nunca conseguirá chamar o construtor em um objeto já construído. Porém, durante a construção de um objeto, você pode fazer com que um construtor chame outro para não ter de ficar copiando e colando:

class Conta {
  String titular;
  int numero;
  double saldo;

  // construtor
  Conta (String titular) {
      //   faz mais uma série de inicializações e configurações
      this.titular = titular;
  }

  Conta (int numero, String titular) {
      this(titular); // chama o construtor que foi declarado acima
      this.numero = numero;
  }

  //..
}

Existe um outro motivo, o outro lado dos construtores: facilidade. Às vezes, criamos um construtor que recebe diversos argumentos para não obrigar o usuário de uma classe a chamar diversos métodos do tipo 'set'.

No nosso exemplo do CPF, podemos forçar que a classe Cliente receba no mínimo o CPF. Dessa maneira, um Cliente já será construído e terá um CPF válido.

Java Bean

Quando criamos uma classe com todos os atributos privados, seus getters, setters e um construtor vazio (padrão), na verdade, estamos criando um Java Bean (mas não confunda com EJB, que é Enterprise Java Beans).

Atributos de classe

Nosso banco também quer controlar a quantidade de contas existentes no sistema. Como poderíamos fazer isso? A ideia mais simples é:

Conta c = new Conta();
totalDeContas = totalDeContas + 1;

Aqui, voltamos em um problema parecido com o da validação de CPF. Estamos espalhando um código por toda aplicação, e quem garante que conseguiremos lembrar de incrementar a variável totalDeContas toda vez?

Tentamos, então, passar para a seguinte proposta:

class Conta {
    private int totalDeContas;
    //...

    Conta() {
        this.totalDeContas = this.totalDeContas + 1;
    }
}

Quando criarmos duas contas, qual será o valor do totalDeContas de cada uma delas? Será um, pois cada uma tem essa variável. O atributo é de cada objeto.

Seria interessante, então, que essa variável fosse única, compartilhada por todos os objetos dessa classe. À vista disso, quando mudasse por meio de um objeto, o outro enxergaria o mesmo valor. Para fazer isso em Java, declaramos a variável como static.

private static int totalDeContas;

Quando declaramos um atributo como static, ele passa a não ser mais um atributo de cada objeto, e sim um atributo da classe. A informação fica guardada pela classe e não é mais individual para cada objeto.

Para acessarmos um atributo estático, não usamos a palavra-chave this, mas, sim, o nome da classe:

class Conta {
    private static int totalDeContas;
    //...

    Conta() {
        Conta.totalDeContas = Conta.totalDeContas + 1;
    }
}

Já que o atributo é privado, como podemos acessar essa informação a partir de outra classe? Precisamos de um getter para ele!

class Conta {
    private static int totalDeContas;
    //...

    Conta() {
        Conta.totalDeContas = Conta.totalDeContas + 1;
    }

    public int getTotalDeContas() {
        return Conta.totalDeContas;
    }
}

Como fazemos, então, para saber quantas contas foram criadas?

Conta c = new Conta();
int total = c.getTotalDeContas();

Precisamos criar uma conta antes de chamar o método. Isso não é legal, pois gostaríamos de saber quantas contas existem sem precisar ter acesso a um objeto-conta. A ideia aqui é a mesma, transformar esse método que todo objeto-conta tem em um método de toda a classe. Usamos a palavra static de novo, mudando o método anterior.

public static int getTotalDeContas() {
    return Conta.totalDeContas;
}

Para acessar esse novo método:

int total = Conta.getTotalDeContas();

Repare que estamos não chamando um método com uma referência a uma Conta, e sim usando o nome da classe.

Métodos e atributos estáticos

Métodos e atributos estáticos só podem acessar outros métodos e atributos estáticos da mesma classe, o que faz todo sentido, dado que dentro de um método estático, não temos acesso à referência this, pois um método estático é chamado por meio da classe, e não de um objeto.

O static realmente apresenta um "cheiro" procedural, porém, em muitas vezes, é necessário.

Um pouco mais...

Exercícios: encapsulamento, construtores e static

  1. O que é necessário fazer para garantirmos que os atributos da classe Conta não sejam acessados de forma direta em outra classe a qual não seja a própria classe Conta?

  2. Após deixar os atributos da classe Conta com acesso restrito (privado), tente criar uma Conta na classe TestaConta dentro do main e modificar ou ler os atributos da conta criada. O que acontece?

    Crie apenas os getters e setters necessários na sua classe Conta. Pense sempre se é preciso criar cada um deles.

    Não copie e cole! Aproveite para praticar a sintaxe. Logo, passaremos a usar o Eclipse e aí, sim, teremos procedimentos mais simples destinados a esse tipo de tarefa.

    Repare que o método calculaRendimento parece também um getter. Aliás, seria comum alguém nomeá-lo de getRendimento. Getters não precisam apenas retornar atributos, eles podem trabalhar com esses dados.

  3. Altere suas classes que acessam e modificam atributos de uma Conta para utilizar os getters e setters recém-criados.

  4. Faça com que sua classe Conta possa receber, opcionalmente, o nome do titular da Conta durante a criação do objeto.

  5. (Opcional) Adicione um atributo, na classe Conta de tipo int, que se chama identificador. Este deve ter um valor único para cada instância do tipo Conta. A primeira Conta instanciada tem identificador 1, a segunda, 2, e assim por diante. Você deve utilizar os recursos aprendidos aqui na resolução desse problema.

    Crie um getter para o identificador. Devemos ter um setter?

  6. (Opcional) Como garantir que datas como 31/2/2021 não sejam aceitas pela sua classe Data?

  7. (Opcional) Imagine que tenhamos a classe PessoaFisica a qual tem um CPF como atributo. Como garantir que alguma pessoa física tenha CPF inválido e tampouco seja criada uma PessoaFisica sem CPF inicial? (Suponha que já exista um algoritmo de validação de CPF: este deve passar por um método valida(String x)....)

Desafios

  1. Por que esse código não compila?

        class Teste {
            int x = 37;
            public static void main(String [] args) {
                System.out.println(x);
            }
        }
  2. Imagine que haja uma classe FabricaDeCarro, e quero garantir que só exista um objeto desse tipo em toda a memória. Não há uma palavra-chave especial para isso em Java. Então, teríamos de fazer nossa classe de tal maneira que ela respeitasse essa nossa necessidade. Como faríamos isso? (Pesquise: singleton design pattern.)