Programação Orientada a Objetos Utilizando a Linguagem de Programação Java

Classes e Objetos | Estado de um Objeto | Métodos e Comportamento de um Objeto | Herança | Polimorfismo

Veja também: Programação Orientada a Objetos

Classe e Objetos

Java é uma linguagem orientada a objetos que utiliza o conceito de classes para a implementação do código. Neste caso, os objetos são instâncias de uma classe. No caso da linguagem Java, todo o código deve estar contido dentro da definição de uma classe, não sendo permitida a criação de código fora de uma classe.

Objetos, em Java, são instâncias de uma classe criados com o operador new. Este operador reserva espaço na memória para armazenar o objeto do tipo criado, e invoca o construtor escolhido (Algoritmo 1).

Instanciação de um objeto em Java.

Pessoa a = new Pessoa("Ivonei");

Após terminar a criação do objeto e sua inicialização, a referência ao objeto é atribuída à variável ($a$, no exemplo). Por meio desta variável, será possível a utilização do objeto.

Um objeto possui estado e comportamento, e, na linguagem Java, estes são definidos através da implementação de classes.

Definição de uma classe

Uma classe em Java define um novo tipo de dados, e a sintaxe para a sua definição inclui a visibilidade da classe (Algoritmo 2).

Declaração mínima de uma classe em Java

public class Pessoa {
}

Cada arquivo fonte pode possuir apenas uma classe pública, uma vez que o nome do arquivo deve ser exatamente igual ao nome da classe, incluindo a capitalização das letras. Apesar disso, um mesmo arquivo pode conter várias classes que não são públicas. Estas classes não possuem um especificador de visibilidade, e possuem visibiladade de pacote.

Estado de um Objeto

O estado de um objeto é definido pelo conjunto de valores de todas as suas variáveis de instância. Podemos ter objetos diferentes (duas instâncias de uma classe são sempre independentes, logo são objetos diferentes), mesmo que seu estados sejam idênticos (imagine dois produtos, idênticos, recém saídos de uma linha de produção).

Variáveis de Instância

Para melhorar o encapsulamento, e evitar que o estado do objeto seja alterado de formas imprevisíveis, é recomendado que todas as variáveis de instância sejam declaradas como privadas da classe (Algoritmo 3). Dessa forma, nenhum objeto de outra classe poderá alterar o estado de um objeto para uma situação inválida ou instável.

Variáveis de Instância

public class Pessoa {
    private String nome;
    private java.time.LocalDate dataNascimento;
}

Métodos e Comportamento de um Objeto

O comportamento de um objeto é definido a partir de métodos que executam operações e serviços que um objeto oferece.

Uma vez que o estado de um objeto é privado, para consultar ou alterar o estado desse objeto, devem existir métodos de acesso e métodos modificadores, que modificam os valores da instância do objeto. Como a linguagem Java não provê construções para a criação de atributos ou propriedades dos objetos, utilazam-se métodos para criar o conceito de atributos.

Atributos

Os atributos de um objeto estão relacionados as suas variáveis de instância, e são derivados delas. Atributos são sempre públicos, e podem ser de leitura e escrita, apenas leitura, ou apenas escrita. O Algoritmo 4 cria um atributo de leitura e escrita na classe Pessoa, chamado Nome.

Criação de um atributo

public class Pessoa {
    private String nome;
    private java.time.LocalDate dataNascimento;

    /**
     * Retorna o nome de um objeto Pessoa.
     * @return o nome da Pessoa.
     */
    public String getNome() {
        return nome;
    }

    /**
     * Altera o nome da pessoa.
     * @param nome O novo nome da pessoa, por exemplo, seu nome social.
     */
    public void setNome(String nome) {
        if (nome != null && ! nome.isEmpty()) {
            this.nome = nome;
        }
    }
}

Embora sempre derivados a partir de variáveis de instância, não é necessário que exista uma variável de instância para cadaatributo. No exemplo apresentado no Algoritmo 5, são criados três atributos (Idade, Signo, DataNascimento) a partir de uma mesma variável de instância (dataNascimento). Atributos criados dessa forma são, normalmente, atributos apenas de leitura. No exemplo, todos os três atributos são apenas de leitura.

Criação de atributos baseados em uma única variável de instância.

public class Pessoa {
    private String nome;
    private java.time.LocalDate dataNascimento;

    /**
     * Retorna a idade de uma Pessoa no dia de hoje.
     * @return a idade da pessoa.
     */
    public int getIdade() {
        Period idade = Period.between(dataNascimento, LocalDate.now());
        return idade.getYears();
    }

    /**
     * Retorna o signo de uma pessoa.
     * @return o signo da pessoa, de acordo com sua data de nascimento.
     */
    public String getSigno() {
        String[] signos = {"Capricornio", "Aquario", "Peixes", "Aries",
                            "Touro", "Gemeos", "Cancer", "Leao",
                            "Virgem", "Libra", "Escorpiao", "Sagitario"};
        int[] diaDosSignos = { 20, 50, 80, 110, 141, 172,
                               204, 235, 266, 296, 326, 356 };
        int diaNascimento = dataNascimento.getDayOfYear();
        for (int i = 0; i < diaDosSignos.length; i++) {
            if (diaNascimento < diaDosSignos[i])
                return signos[i];
        }
        return signos[0];
    }

    /**
     * Retorna a data de nascimento da pessoa.
     * @return a data de nascimento.
     */
    public String getDataNascimento() {
        return dataNascimento;
    }
}

Note que nem toda variável de instância precisa, obrigatoriamente, permitir que seu valor seja alterado. No caso do exemplo da classe Pessoa, não faz sentido (para o sistema em questão), que o valor da data de nascimento do objeto Pessoa seja alterado, por esse motivo, esse valor só é acessado como um atributo de leitura (Algoritmo 6).

Criando um atributo apenas para "leitura"

public class Pessoa {
    private java.time.LocalDate dataNascimento;

    /**
     * Retorna a data de nascimento da pessoa.
     * @return a data de nascimento.
     */
    public String getDataNascimento() {
        return dataNascimento;
    }
}

Porém, como não é possível alterar o valor da data de nascimento, é necessário que este valor seja inicializado na criação do objeto, o que pode ser implementado a partir de valores padrão para o objeto, ou atravéz de um construtor que aceite como parâmetro o valor apropriado (Algortimo 7).

Note que na implementação do Algoritmo 6, tomou-se o cuidado de garantir, já no construtor, que o estado do objeto, após a sua criação seja um estado válido, não permitindo, portanto, valores inválidos para a data de nascimento.

Garantindo que o objeto é um objeto válido desde a sua criação.

public class Pessoa {
    private String nome;
    private java.time.LocalDate dataNascimento;

    /**
     * Inicializa um objeto Pessoa, com a data de nascimento no dia
     * em que o objeto foi instanciado.
     */
    public Pessoa() {
        this.dataNascimento = LocalDate.now();
    }

    /**
     * Inicializa um objeto Pessoa, com a data de nascimento
     * especificada, apenas se esta for uma data válida.
     */
    public Pessoa(LocalDate dataNascimento) {
        LocalDate max = LocalDate.now();
        LocalDate min = max.minusYears(125);

        if (dataNascimento.isBefore(min) || dataNascimento.isAfter(max)) {
            throw new IllegalArgumentException("dataNascimento invalida.");
        }

        this.dataNascimento = dataNascimento;
    }
}

Os atributos, na linguagem Java, são criados por métodos de acesso, estes métodos retornam um valor para quem enviou a mensagem, e não alteram o estado do objeto. Para alterar o estado do objeto, são utilizados métodos modificadores, que são métodos que ao serem invocados recebem como parâmetro valores que serão utilizados para alterar o estado do objeto. Os métodos modificadores podem criar atributos de leitura e escrita, ou apenas de escrita.

Comportamentos

Imagine um objeto de ContaBancária que provê funcionalidades para sacar e depositar valores. Um objeto destes poderia oferecer um atributo para a consulta ao valor do saldo, no entanto, não faz sentido que este objeto permita que se ajuste o valor do saldo a um valor específico, uma vez que o comportamento do saldo é relativo ao valor utilizado em uma operação de saque ou depósito.

Para atender a esta especificação o objeto ContaBancaria (Algoritmo 8) apresenta uma interface pública para alterar o estado do objeto sob a forma de métodos de comportamento (que no caso são métodos modificadores, uma vez que modificam o estado do objeto).

Comportamento em uma classe de ContaBancaria

public class ContaBancaria {
    private double saldo;

    public double getSaldo() { ... }

    public void sacar(double valor) { ... }
    public void depositar(double valor) { ... }
}

Alguns comportamentos do objeto são privados, de forma que não impactam a interface pública do objeto caso sejam modificados. Estes métodos privados auxiliam a escrita de um código mais organizado e fácil de manter (Algoritmo 9).

Comportamento em uma classe de ContaBancaria

public class ContaBancaria {
    private double saldo;

    private boolean autorizaSaque(double valor) {
        return (valor > 0) && (valor <= saldo);
    }

    public void sacar(double valor) {
        if (autorizaSaque(valor)) {
            saldo -= valor;
        }
    }

    public void depositar(double valor) {
        if (valor > 0) {
            saldo -= valor;
        }
    }
}

Métodos e variáveis de classe

Além dos métodos e variáveis de instância, Java oferece suporte a métodos e variáveis de classe. Todo método e variável de classe está associado a uma classe, e não possui uma instância de objeto associada a ele. O código e os dados são compartilhados por todas as instâncias da classe.

Um método de classe só pode acessar variáveis de classe e outros métodos de classe. Não é possível acessar métodos e variáveis de instância, sem a obtenção de uma referência a um objeto.

Utiliza-se o modificador static para definir que uma variável ou método pertencerá à classe e não a uma instância. O uso de métodos e variáveis de classe é mostrado no Algoritmo 10.

Variáveis e métodos de classe.

class OnibusDePortoAlegre {
        private static double precoPassagem = 4.05;

        public static double getPrecoPassagem() {
            return precoPassagem;
        }

        public static double setPrecoPassagem(double newValue) {
            if (newValue > precoPassagem)
                precoPassagem = newValue;
        }
}

Em muitas situações, variáveis de classe são utilizadas para definir valores e flags utilizadas pela classe, e nesse caso, são normalmente públicas (ao contrário de privadas). No entanto, para garantir o encapsulamento, são declaradas como constantes (final), de forma que não podem mais ser alteradas (Algoritmo 11).

Variáveis de classe utilizadas como flags.

class GameLocation {
    public static final int NORTH = 1;
    public static final int SOUTH = 2;
    public static final int EAST  = 3;
    public static final int WEST  = 4;

    public GameLocation getNextLocation(int direction) {
        switch (direction) {
            case NORTH: return coldNorth;
            case SOUTH: return southSwamp;
            case EAST: return brightEastern;
            case WEST: return goldenWest;
            default: return invalidLocation;
        }
    }
}

Herança

Em Java, a relação de herança é definida na criação da classe utilizando a palavra reservada extends (classe Book apresentada no Algoritmo 12). Quando utilizada desta forma, Java implementa a herança de implementação, ou seja, a classe que "estende" uma outra classe, herda toda a sua implementação (métodos e estado). Esta forma de herança cria uma forte relação É-UM entre as classes.

Java oferece suporte apenas a herança de implementação simples, não sendo possível utilizar uma lista de classes em conjunto com o extends.

Exemplo de herança de implementação.

public class Product {
    private double price;

    public Product(double price) {
        this.price = price;
    }

    public void setPrice(double price) {
        if (price > 0)
            this.price == price;
    }

    public double getPrice() {
        return price;
    }
}

public class Book extends Product {
    private String author;
    private String title;

    public Book(String title, String author, double price) {
        super(price);
        this.author = author;
        this.title = title;
    }

    public getAuthor() {
        return author;
    }

    public getTitle() {
        return title;
    }
}

Note que a inicialização do objeto (construtor) deve connsiderar a inicialização da superclasse. Quando a superclasse possuir um construtor padrão, este será invocado automaticamente, porém, este comportamento pode ser alterado explicitamente ao invocar o construtor desejado utilizando o super. Caso a superclasse não possua um construtor padrão, é necessário que se invoque explicitamente o construtor desejado, como exemplificado no Algoritmo 12.

Com relação à herança, Java oferece suporte a um terceiro nível de ocultação de informação, além dos tradicionais public e private, mediante o uso do modificador protected. Todo elemento protegido permite o acesso de objetos da propría classe e de classes derivadas dela, direta ou indiretamente.

Classes abstratas

Java também oferece o conceito de classes abstratas. Existem dois tipos de classes abstratas em Java, as classes que são abstratas por opção do desenvolvedor, e as classes que são obrigatoriamente abstratas. Toda classe que possuir, ao menos um, método abstrato, deve ser obrigatoriamente declarada como abstrata (abstract) (Algoritmo 13).

Exemplo de classe obrigatoriamente abstrata.

abstract public class Produto {
    private double price;

    public Product(double price) {
        this.price = price;
    }

    public void setPrice(double price) {
        if (price > 0)
            this.price == price;
    }

    public double getPrice() {
        return price;
    }

    abstract public String getDescription();

}

Ao herdar de uma classe abstrata para criar uma classe concreta, é necessário que todos os métodos abstratos sejam sobrescritos (Algoritmo 14).

Implementação de uma classe concreta que extende uma classe abstrata.

public class Book extends Product {
    private String author;
    private String title;

    public Book(String title, String author, double price) {
        super(price);
        this.author = author;
        this.title = title;
    }

    public getAuthor() {
        return author;
    }

    public getTitle() {
        return title;
    }

    @Override
    public String getDescription() {
        return String.format("%s (%s): %.02f",title,author, getPrice());
    }
}

Note que o uso da anotação @Override é opcional, mas permite ao compilador Java avisar se houve ou não algum equívoco na declaração do método que será sobrescrito.

Polimorfismo

Diversos exemplos de polimorfismo já foram apresentados anteriormente, logo, os exemplos a seguir mostrarão exemplos de implementação dos tipos específicos de polimorfismo, utilizando a linguagem Java.

Polimorfismo ad hoc

No polimorfismo ad hoc, uma mesma classe sobrecarrega uma mensagem com diferentes implementações, e a seleção que qual método será executado para a mensagem dependerá do tipo e da quantidade de parâmetros.

Para compreender a sobrecarga de funções (polimorfismo ad hoc), é importante entender que o método é selecionado de acordo com a sua assinatura, e esta é definida pelo identificador da mensagem e pela lista de tipos dos parâmetros do método. O tipo de retorno, os nomes dos parâmetros, e exceções que o método possa lançar, não influenciam na sua assinatura.

A implementação apresentada no Algoritmo 15, utiliza a sobrecarga, variando o número de parâmetros em cada uma das funções. Esse tipo de sobrecarga é muito comum, uma vez que Java não possui parâmetros com valor default.

Sobrecarga de função em Java.

class ShoppingCart {
    public void add(Product product) {
        ProductItem found = findOrAddProduct(product)
        found.add(1);
    }

    public void add(Product product, int amount) {
        ProductItem found = findOrAddProduct(product)
        found.add(amount);
    }
}

Polimorfismo por Subtipo

Em Java, o polimorfismo por subtipo (ou inclusão) é normalmente chamado de polimorfismo, e requer a herança de implementação (Algoritmo 16).

Polimorfismo por subtipo, com herança, em Java.

abstract class Product {
    abstract public String getDescription();
}

class Book extends Product {
    @Override
    public String getDescription() {
        return "I am is a book.";
    }
}

class DVD extends Product {
    private int area;

    @Override
    public String getDescription() {
        return "I am a DVD, and my area is " + area;
    }
}

Polimorfismo Paramétrico

O polimorfismo paramétrico em Java utiliza [Generics] para parametrizar o tipo de dado que executará o método, ou será alvo dele. O Algoritmo 17 mostra uma implementação utilizando este tipo de polimorfismo.

Polimorfismo Paramétrico, utilizando Generics, em Java.

interface DistanceTo<T> {
    public double distanceTo(T target)
}

public <T extends DistanceTo<T>> T closest(List<T> list, T target) {
    double min = Double.max_value;
    T result = null;
    for (T candidate : list) {
        double distance = candidate.distanceTo(target);
        if  (distance < min) {
            min = distance;
            result = candidate;
        }
    }
    return result;
}