Objetos são instâncias de classes. É através deles que (praticamente) todo o processamento ocorre em sistemas implementados com linguagens de programação orientadas a objetos. O uso racional de objetos, obedecendo aos princípios associados à sua definição conforme estabelecido no paradigma de desenvolvimento orientado a objetos, é chave para o desenvolvimento de sistemas complexos e eficientes.

Um objeto é um elemento que representa, no domínio da solução, alguma entidade (abstrata ou concreta) do domínio de interesse do problema sob análise. Objetos similares são agrupados em classes.

No paradigma de orientação a objetos, tudo pode ser potencialmente representado como um objeto. Sob o ponto de vista da programação orientada a objetos, um objeto não é muito diferente de uma variável normal.

Um programa orientado a objetos é composto por um conjunto de objetos que interagem através de “trocas de mensagens”. Na prática, essa troca de mensagem traduz-se na aplicação de métodos a objetos.

As técnicas de programação orientada a objetos recomendam que a estrutura de um objeto e a implementação de seus métodos devem ser tão privativos como possível. Normalmente, os atributos de um objeto não devem ser visíveis externamente. Da mesma forma, de um método deve ser suficiente conhecer apenas sua especificação, sem necessidade de saber detalhes de como a funcionalidade que ele executa é implementada.

Encapsulação é o princípio de projeto pelo qual cada componente de um programa deve agregar toda a informação relevante para sua manipulação como uma unidade (uma cápsula). Aliado ao conceito de ocultamento de informação, é um poderoso mecanismo da programação orientada a objetos.

Ocultamento da informação é o princípio pelo qual cada componente deve manter oculta sob sua guarda uma decisão de projeto única. Para a utilização desse componente, apenas o mínimo necessário para sua operação deve ser revelado (tornado público).

Na orientação a objetos, o uso da encapsulação e ocultamento da informação recomenda que a representação do estado de um objeto deve ser mantida oculta. Cada objeto deve ser manipulado exclusivamente através dos métodos públicos do objeto, dos quais apenas a assinatura deve ser revelada.

O conjunto de assinaturas dos métodos públicos da classe constitui sua interface operacional.

Dessa forma, detalhes internos sobre a operação do objeto não são conhecidos, permitindo que o usuário do objeto trabalhe em um nível mais alto de abstração, sem preocupação com os detalhes internos da classe. Essa facilidade permite simplificar a construção de programas com funcionalidades complexas, tais como interfaces gráficas ou aplicações distribuídas. Exemplo:


 public class Consulta {
  public static void main(String[]args){
  Pessoa pess = new Pessoa();
  pess.nome = "Joao";
  pess.apelido = "Joca";
  pess.altura = 170.0f;
  pess.idade = 34.0f;
  pess.peso = 65.0f;
  }
 }
 public class Pessoa {
  public String nome, apelido;
  public Float idade, peso, altura;
 }

Observe como seria o acesso a estes atributos em um pseudocódigo. Veja que a palavra “pess” é utilizada para referenciar um objeto da classe Pessoa criada.

Princípios da Orientação a Objetos

Em Orientação a Objetos temos três conceitos básicos:

  • Herança: A capacidade de uma Classe herdar métodos e propriedades de uma classe ancestral. Ela pode acrescentar ou modificar o comportamento de sua ancestral.
  • Encapsulamento: A capacidade que uma Classe tem de ocultar a sua própria implementação, apresentando ao “cliente” que a utiliza apenas uma interface simplificada.
  • Polimorfismo: A habilidade de métodos com mesmo nome apresentarem comportamento diferente em classes diferentes (porém derivadas de um mesmo ancestral).

Para uma linguagem de programação ser considerada “Orientada a Objetos” é necessário implementar mecanismos que permitam utilizar estes três conceitos básicos.

Herança

A herança é a principal característica de distinção entre um sistema de programação orientado a objeto e outros sistemas de programação. As classes são inseridas em uma hierarquia de especializações de tal forma que uma classe mais especializada herda todas as propriedades da classe mais geral a qual é subordinada na hierarquia. A classe mais geral é denominada superclasse e a classe mais especializada subclasse.

O principal benefício da herança é a reutilização de código. A herança permite ao programador criar uma nova classe programando somente as diferenças existentes na subclasse em relação à superclasse. Isto se adéqua bem a forma como compreendemos o mundo real, no qual conseguimos identificar naturalmente estas relações.

A fim de exemplificarmos este conceito, vamos considerar que queiramos modelar os seres vivos pluricelulares existentes no planeta. Podemos então começar com a classe SerVivo.

Ela modela as características que todo ser vivo deve possuir, como a capacidade de reproduzir-se ou a necessidade de alimentar-se. Sendo assim, a classe SerVivo define atributos e métodos tais como:

  • Atributos: Alimentos, Idade
  • Métodos: Nascer, Alimentar, Respirar, Crescer, Reproduzir, Morrer

Os seres vivos por sua vez classificam-se em Animais e Vegetais, os quais possuem características próprias que os distingue:

rcpoop2iljfig02.jpg

Analisando o problema em questão (o de modelar os seres vivos), nós naturalmente identificamos classes que são especializações de classes mais genéricas e o conceito de herança da orientação a objeto nos permite implementar tal situação.

Os animais e vegetais antes de tudo são seres vivos e cada subclasse herda automaticamente os atributos e métodos (respeitando as regras dos modificadores de acesso) da superclasse, neste caso, a classe SerVivo. Além disso, as subclasses podem prover atributos e métodos adicionais para representar suas próprias características. Por exemplo, a classe Animal poderia definir os seguintes métodos e atributos:

  • Atributos: Forma de Locomoção, Habitat, Tempo Médio de Vida
  • Métodos: Locomover

A herança na programação é obtida especificando-se qual superclasse a subclasse estende. Em Java isto é feito utilizando-se a palavra chave extends:


public class SerVivo {
 //Definição da classe SerVivo
 }
 public class Animal extends SerVivo {
 //Atributos e métodos adicionais que distinguem um Animal de um SerVivo
 //qualquer
 }

Em Java, todas as classes, tanto as existentes nas APIs como as definidas pelos programadores, automaticamente derivam de uma superclasse padrão, a classe Object.

Se uma classe não especifica explicitamente uma superclasse, como o caso da classe SerVivo, então podemos considerar que esta deriva diretamente de Object, como se ela tivesse sido definida como:


public class SerVivo extends Object { ... }

Além disso, Java permite apenas herança simples, isto é, uma classe pode estender apenas de uma única outra classe.

Resumindo, a classe SerVivo define os atributos e métodos que são comuns a qualquer tipo de ser vivo. A subclasse Animal herda estes métodos e atributos, já que Animal é um SerVivo , e tem que especificar apenas seus atributos e métodos específicos.

rcpoop2iljfig03.jpg

Encapsulamento

Diferente da abordagem estruturada, onde dados e procedimentos são definidos de forma separada no código, na programação orientada a objeto os dados e procedimentos que manipulam estes dados são definidos numa unidade única, o objeto. Isso possibilita uma melhor modularidade do código, porém, a ideia principal é poder utilizar os objetos sem ter que se conhecer sua implementação interna, que deve ficar escondida do usuário do objeto que irá interagir com este apenas através de sua interface.

“À propriedade de se implementar dados e procedimentos correlacionados em uma mesma entidade e de se proteger sua estrutura interna escondendo-a de observadores externos dá-se o nome de encapsulamento.“

O objetivo do encapsulamento é separar o usuário do objeto do programador do objeto e seus principais benefícios são:

  • Possibilidade de alterar a implementação de um método ou a estrutura de dados escondidos de um objeto sem afetar as aplicações que dele se utilizam;
  • Criação de programas mais modulares e organizados, o que possibilita um melhor reaproveitamento do código e melhor manutenibilidade da aplicação.

Via de regra, as variáveis de instância declaradas em uma definição de classe, bem como os métodos que executam operações internas sobre estas variáveis, se houverem, devem ser escondidos na definição da classe. Isso é feito geralmente através de construções nas linguagens de programação conhecidas como modificadores de acesso, como por exemplo, o public, protected e private do Java. Quando definimos uma classe, é recomendado (para alguns é uma regra sagrada) que declaremos públicos apenas os métodos da sua interface. É na interface (ou protocolo) da classe que definimos quais mensagens podemos enviar às instâncias de uma classe, ou seja, quais são as operações que podemos solicitar que os objetos realizem. Por exemplo, na classe Ponto abaixo, deveríamos ter feito seus atributos protegidos e apenas o método público:


public class Ponto
 {
 private double x;
 private double y;
 public void girar (int grau, Ponto p) {
 //Código para girar o ponto em torno de outro ponto p a quantidade definida em //grau
 }
 //Os métodos públicos consistuem a interface da classe.
 public void setX (double k) { x = k; }
 public void setY (double k) { y = k; }
 public double getX () { return x; }
 public double getY () { return y; }
 }

É comum em programação orientada a objeto definir métodos gets e sets que provêm acesso aos dados protegidos da classe.

Criando Objetos e Acessando Dados Encapsulados

Criamos objetos em Java de forma muito similar a criação de variáveis de tipos primitivos. Se uma aplicação quisesse usar a classe Círculo, poderia declarar uma variável deste tipo da seguinte forma:


Círculo círculo; à Como Java é case-sensitive pode-se declarar desta forma.
 //Esta sentença cria uma variável círculo do tipo Círculo.

Entretanto, isso não é suficiente para acessarmos os métodos e atributos públicos da classe. A sentença acima somente declara uma variável, mas não cria um objeto da classe especificada. Em Java, objetos são criados usando o operador new da seguinte forma:


Círculo círculo;
 círculo = new Círculo();

O operador new cria uma instância da classe e retorna a referência do novo objeto.

Como vimos, todos os objetos em Java são tratados através da referência ao endereço de memória onde o objeto está armazenado. O operador new realiza três tarefas:

  1. Aloca memória para o novo objeto;
  2. Chama um método especial de inicialização da classe chamado construtor;
  3. Retorna a referência para o novo objeto;

É importante compreender o que ocorre na declaração da variável e na inicialização da variável. Quando fazemos

é reservado uma porção da memória principal do Java (stack) para armazenar o endereço na memória auxiliar (heap) onde o objeto será armazenado. Como apenas com a declaração da variável o objeto ainda não existe, o conteúdo inicial dela será o valor nulo (null), indicando que ela ainda não se refere a nenhum objeto.

rcpoop2iljfig04.jpg

Apenas após a inicialização é que uma variável de um tipo não primitivo estará valendo algo e através dela será possível acessar os dados e operações do objeto em questão.

rcpoop2iljfig05.jpg

Uma vez um objeto tendo sido criado, seus métodos e atributos públicos podem ser acessados utilizando o identificador do objeto (variável que armazena sua referência) através do operador ponto:


.
.
A aplicação que criou o objeto Círculo acima pode solicitar ao objeto que ele se desenhe fazendo:
círculo.criaCírculo();

Polimorfismo

O termo Polimorfismo origina-se do grego e quer dizer "o que possui várias formas".

Em programação está relacionado à possibilidade de se usar o mesmo nome para métodos diferentes e à capacidade que o programa tem em discernir, dentre os métodos homônimos, aquele que deve ser executado. De maneira geral o polimorfismo permite a criação de programas mais claros, pois elimina a necessidade de darmos nomes diferentes para métodos que conceitualmente fazem a mesma coisa, e também programas mais flexíveis, pois facilita em muito a extensão dos mesmos.

O polimorfismo pode ser de duas formas, estático ou dinâmico:

  • Polimorfismo Estático: Ocorre quando na definição de uma classe criamos métodos com o mesmo nome, porém com argumentos diferentes. Dizemos neste caso que o método está sobrecarregado (overloading). A decisão de qual método chamar é tomada em tempo de compilação, baseada nos argumentos que foram passados. Como exemplo de polimorfismo estático, podemos relembrar dos vários construtores que criamos para a classe Círculo:
    
     Círculo() {}
     Círculo(Ponto centro, Ponto extremidade, Cor c) {
     raio = new Linha(centro, extremidade, c);
     cor = c;
     }
     Círculo (double x1, double y1, double x2, double y2, Cor c)
     {
     raio = new Linha(new Ponto(x1, y1), new Ponto(x2, y2), c);
     cor = c ;
     }
    

    Todos estes métodos construtores possuem o mesmo nome, mas devem ser diferidos entre si pelos parâmetros que recebem.

    Quando num programa fazemos Círculo circulo = new Círculo(linha); o compilador consegue decidir em tempo de compilação qual método chamar, neste caso, o método Círculo que recebe uma linha como parâmetro.

  • Polimorfismo Dinâmico: Esta associado com o conceito de herança e ocorre quando uma subclasse redefine um método existente na superclasse. Dizemos neste caso que o método foi sobrescrito (overriding) na subclasse. A decisão de qual método executar é tomada somente em tempo de execução, como veremos mais adiante.

    O polimorfismo dinâmico ocorre quando uma subclasse redefine um método de sua superclasse a fim de prover ao método um comportamento mais adequado às suas características. Vamos rever a classe animal conforme a definimos abaixo:

    
    class Animal extends SerVivo{
     String formaLocomoção;
     String habitat;
     int tempoMédioVida;
     public void locomover() { ... }
     }
    

    Como nem todo ser vivo nasce, cresce, alimenta-se, respira, se reproduz e morre da mesma maneira, é razoável que queiramos redefinir todos estes métodos na classe animal:

    
     class Animal extends SerVivo{
     String formaLocomoção;
     String habitat;
     int tempoMédioVida;
     public void locomover() { ... }
     public void nascer() { ... }
     public void crescer() { ... }
     public void alimentar() { ... }
     public void respirar() { ... }
     public Animal reproduzir() { ... }
     public morrer() { ... }
     }
    

    Os métodos na subclasse devem ser definidos com a mesma “assinatura” do método na superclasse, isto é, com o mesmo nome, tipo de retorno e argumentos.

    Bem, mas a vantagem do polimorfismo dinâmico não é apenas a de permitir maior flexibilidade na modelagem das classes de objetos. Para entendermos o que há de mais fantástico nele, temos que nos atentar para o seguinte:

    rcpoop2iljfig06.jpg

    Tendo isto em mente, poderíamos definir um método da seguinte forma:

    
     void analisaSerVivo (SerVivo ser) {
     //Este método faz a análise clínica de qualquer SerVivo e para isso precisa pedir
     //ao animal que respire
     ser.respirar();
     . . .
     }
     e num determinado momento chamá-lo desta maneira
     . . .
     Animal animal = new Animal();
     analisaSerVivo(animal);
     . . .
    

    Neste caso fica a questão, se o método tem um argumento declarado como SerVivo e recebe como parâmetro um objeto Animal, quando tiver que executar o método respirar() qual método será efetivamente chamado, o método respirar definido emSerVivo ou o método respirar definido em Animal?

    O método executado será o mais apropriado, isto é, aquele pertencente ao objeto que foi passado à variável, neste caso, o método respirar existente em Animal.

    Isto é, o polimorfismo dinâmico, que recebe este nome porque o compilador não consegue nestes casos decidir em tempo de compilação qual método chamar, já que uma variável de um tipo base pode receber qualquer tipo derivado. Esta decisão é feita apenas em tempo de execução, quando aí o Java poderá saber qual objeto a variável está de fato referenciando.

    Se o polimorfismo dinâmico não existisse, seria necessário um método analisaSerVivo para cada ser vivo existente, e sempre que um ser vivo novo fosse acrescentado o código teria que ser modificado. Da forma como aqui está, o método continuará funcionando para qualquer ser vivo existente no projeto.