Classes estendem classes

Conforme enfatizado no material anterior (Classes) na linguagem Java só é possível definir uma classe estendendo uma outra classe já definida. Se o programador não especificar uma classe então o compilador irá considerar que a classe estende a classe Object. A classe Object é a única classe cuja definição não estende nenhuma outra classe! Ao definirmos uma classe SUB estendendo  uma outra classe SUPER estamos impondo várias características à classe SUB. Em particular, como será visto, o tipo das instâncias de SUB é compatível com o tipo das instâncias de SUPER (mas nem toda instância de SUPER é compatível com instâncias de SUB) e a definição de SUB herda a definição de SUPER, o material abaixo discute o significado desta herança de definições. Esta possibilidade de definir uma classe a partir de uma outra classe torna a atividade de programação menos complexa! Podemos desdobrar diferentes graus de abstração à medida que criamos hierarquias de classes (a hierarquia é definida pela relação “extends”!).  Veremos também que é fundamental a boa disciplina de só relacionarmos uma classe A com uma classe B através de “extends” se e somente se todo A “é um” B.


Uma classe dá um nome para um conjunto de campos e métodos (funções e procedimentos). Estes campos e métodos devem modelar um certo conceito. O interesse nas classes ocorre em pelo menos duas situações.
Primeiramente pelo fato de que os métodos (funções e procedimentos) de Java só podem existir contidos em classes (é o caso da classe que contém o método
main). Em segundo lugar pelo fato de que uma classe define um tipo. Podemos assim definir variáveis que irão conter referências para instâncias da respectiva classe.

Já dissemos que uma importante analogia é colocar as classes em correspondência com conjuntos e os objetos ou instâncias serem colocados em correspondência com elementos do respectivo “conjunto”. As classes podem ser relacionadas através da relação “extends” e esta relação pode ser colocada em correspondência com a relação de inclusão de conjuntos.

Falando de outro modo: uma classe pode se relacionar com uma outra classe estendendo a modelagem de um conceito. Considere o exemplo abaixo:

class A {...}
class A_estendida extends A{...}
...
A var1= new A();
A_estendida var2 = new A_estendida();

Na analogia de classes/objetos com conjuntos/elementos a relação “extends” define que A_estendida é um subconjunto de A, ou ainda, toda instância de A_estendidada é uma instância de A (analogamente aos conjuntos: nem toda instância de A é uma instância de A_estendida).

A figura abaixo ilustra uma possível confusão que o termo “estender” (extend) pode causar:

Observe que ao defirmos class A_estendida extends A não estamos “esticando” a classe A, estamos incluindo a definição de A_estendida dentro da definição de A.

Para entender de onde pode vir a confusão explicitada acima lembre-se que ao defirmos uma classe temos dois domínios: (i) domínio intencional corresponde às definições estruturais e comportamentais (p.ex. atributos e métodos)  e o (ii) domínio “extensional” corresponde às instâncias. A definição de uma classe a partir de outra opera utilizando o termo “extends” no domínio intencional, ou seja, estendemos a estrutura e comportamento da classe.

Os exemplos acima são sintéticos. Se preferir exemplos com significado pense em A como sendo Veículo e A_estendida como sendo Caminhão, ou ainda pense em A como sendo Construção e A_estendida como sendo Casa, ou ainda  pense em A como sendo Telefone e A_estendida como sendo "Telefone sem fio". A idéia é que A_estendida  estende a modelagem de A. Esta relação de extensão pode ser descrita de várias formas:

Todos os termos acima podem ser utilizados nos relacionamentos transitivos. Se a classe C estende B e B estende A então dizemos que C é herdeira de A ou que C é uma subclasse de A. Podemos distinguir dizendo, por exemplo, que B é herdeira direta de A. Podemos até mesmo usar relações familiares: a classe B é filha de A, a classe C é neta de A.

As instâncias de uma classe A herdeira de uma classe B apresentam todo o comportamento de instâncias de B e potencialmente apresentam comportamentos adicionais. Por isso podemos dizer que toda instância de A "é uma" instância de B. Em qualquer lugar onde pode aparecer uma instância de B podemos colocar uma instância de A, mas o contrário certamente não é verdadeiro.

 

A importância do conceito de superclasse/subclasse/herança:
podemos desenvolver programas tendo como abstração certas superclasses, posteriormente  podemos utilizar tanto as superclasses quanto as subclasses correspondentes a estas superclasses;  esta característica torna o código mais adaptável e reutilizável.

class Aluno{..}
class Aluno_de_graduacao extends Aluno{...}

Ao elaborarmos um programa relacionado à abstração “Aluno” podemos escrever trechos do programa de forma abrangente utilizando a classe Aluno sempre que for adequado, em outros trechos do programa temos a possibilidade de utilizar não só a classe Aluno mas também suas subclasses (p.ex. Aluno_de_graduacao).

Uma classe A herdeira de B pode reimplementar o comportamento herdado. Um método não estático pode ser sobrecarregado (overloaded), ou seja diferentes assinaturas  distinguindo o método.  Um método de instância pode trocar a implementação (redefinir/override) de um método de instância da superclasse por sua própria implementação. Um método de classe pode trocar a implementação (redefinir/ocultar/esconder/hide) de um método de classe da superclasse. Um campo somente pode ser escondido. Quando um método é chamado, através de uma referência para um objeto, a classe efetiva do objeto é considerada. Quando um campo é acessado o tipo da referência é usado.
class superclasse{
  public String str = "str da superclasse";
  public void escreve(){ System.out.println("superclasse"+str);}
}
class subclasse extends superclasse{
  public String str="str da subclasse";
  public void escreve(){System.out.println("subclasse"+str);}
}
class prog{
  public static void main(String[] args){
    subclasse subc= new subclasse();
    superclasse superc=subc;
    subc.escreve();
    superc.escreve(); 
    System.out.println(subc.str);
    System.out.println(superc.str);
     
  }
}
 

Na invocação de escreve() a classe do objeto é considerada, portanto subc.escreve() e superc.escreve() são idênticos; na seleção de str o tipo de superc é diferente do tipo de subc.

Em Java uma classe herda de somente uma classe, ou seja Java não permite herança multipla. A boa prática, a boa disciplina,  recomenda que uma classe B estenda uma classe A somente quando podemos dizer que toda instância de B "é uma" instância de A, veja o exemplo abaixo:

class Aluno{..}
class Aluno_de_graduação extends Aluno{...}

certamente podemos dizer que todo aluno de graduação "é um" aluno.

Não devemos usar a relação de extensão simplesmente para poder ter acesso à estrutura e comportamento (campos e métodos) de uma classe. Considere a definição de uma classe Formulario a partir de uma classe Celula:
 

class Celula{...}
class Formulario extends Celula{...}  /* contra exemplo!!! */

Exceto por casos muito particulares e intrincados *não* podemos dizer que todo formulário é uma celula! Portanto num caso como este a relação entre as classes correspondentes a "formulário" e "célula"  deve ser uma Relação de Composição. Uma classe A tem uma relação de composição com uma classe B quando a classe A possui campos do tipo da classe B. Certamente podemos imaginar que a classe Formulario, de maneira disciplinada, deve ter em sua composição acesso a objetos do tipo definido pela classe Celula:

class Celula{...}
class Formulario{... <declaração de campos do tipo Celula> ...}

A relação de composição entre a classe A e B pode ser interpretada como sendo a relação "faz parte de" ou ainda a relação "tem". Em termos do exemplo acima: Celula "faz parte de" Formulario, ou ainda Formulario "tem" Celula.
Abaixo demonstramos alguma relações de extensão que julgamos adequadas no domínio de formulários e células:
class Celula{...}
class CelulaNumerica extends Celula{...}
class CelulaTexto extends Celula{...}
class CelulaInt extends CelulaNumerica{...}

A expressão "hierarquia de classes" usualmente denota um conjunto de classes e seus relacionamentos de extensão. Em java, por default toda classe que não utiliza a cláusula extends tem, implicitamente, a classe java.lang.Object como superclasse direta (java.lang é um nome de pacote).

Um exemplo de hierarquia de classes definida no pacote java.io:
class Reader{...}
class BufferedReader extends Reader{...}

ou seja a classe java.io.Reader estende a classe java.lang.Object, e a classe java.io.BufferedReader estende a classe java.io.Reader.

No exemplo abaixo Poupança "é uma" Conta. O identificador this corresponde a uma referência ao objeto através do qual está sendo feito o acesso. Podemos colocar um this qualificado com qualquer dos membros não estáticos de uma classe. O comando numero=contador++; pode ser substituido por this.numero=contador++;, mas use o this somente quando necessário! Abaixo ele é usado para desambiguar o identificador saldo. O identificador super corresponde a uma referência ao objeto corrente como uma instância de sua superclasse, mas no caso abaixo o identificador super na forma super() corresponde, convencionalmente, a uma chamada de construtor da superclasse imediata.  Vale lembrar que este é um exemplo didático e está longe de uma aplicação real onde o estado dos objetos (instâncias de Conta) devem ser mantidos mesmo quando os programas não estão em execução. Mais à frente nesta disciplina veremos que uma forma de manter o estado de um objeto é através do uso de arquivos em disco.

class Conta extends Object{
  static private int contador=0;
  private int numero;
  private double saldo;
  Conta(double depositoInicial){
   saldo=depositoInicial;
   numero=contador++;
  }
  int obterNumero(){ return numero;}
  double obterSaldo(){ return saldo;}
  void debitar(double quantia){saldo=saldo-quantia;}
  void creditar(double quantia){saldo=saldo+quantia;}
}

class Poupanca extends Conta{
  Poupanca(double depositoInicial){
    super(depositoInicial);
  }
  void atualizacaoMensal(){
    creditar(obterSaldo()*(0.06/12.));
  }
}
class lugarOndeFicaMain{
  static void escreveSaldo(Conta c){
    System.out.println("Saldo da conta:"+ c.obterNumero()+" e' "+c.obterSaldo());
  }
  public static void main(String[] args){
    Conta v1;
    Poupanca v2;
    v2=new Poupanca(100.);
    escreveSaldo(v2);
    v2.atualizacaoMensal();
    escreveSaldo(v2);
    v1=new Conta(50.);
    escreveSaldo(v1);
  }
}

No programa acima as instâncias de Conta  e de Poupanca são criadas com numeros de conta sequenciais, 0, 1, 2, 3, etc, observe que o construtor Conta(double depositoInicial) garante isso através do comando numero=contador++; que mantém atualizada a variável de classe contador. O construtor da classe Poupanca invoca o construtor da classe Conta utilizando a palavra reservada super.

A figura abaixo corresponde a uma abstração da relação entre classes e entre suas instâncias. No plano superior temos o domínio da classe Object. Neste domínio são mostrados três objetos. Todo objeto instanciado no ambiente de execução Java resulta na instanciação de um objeto da classe Object. No plano médio temos o domínio da classe Conta. Neste domínio são mostrados dois objetos. Um dos objetos é um elemento próprio do tipo Conta. No plano inferior temos o domínio da  classe Poupanca com apenas um objeto. A instanciação de um objeto do tipo Poupanca implica na instanciação direta de um objeto do tipo Poupanca e na instanciação indireta de um objeto do tipo Conta e um objeto do tipo Object. A configuração desta figura pode ser obtida com, por exemplo, três expressões: (i) new Object(); correspondendo ao objeto à direita (ii) new Conta(10.); correspondendo aos dois objetos ao centro e (iii) new Poupanca(50.); correspondendo aos três objetos à esquerda.

Considere uma classe B que seja subclasse da classe A. Alguns textos da linguagem Java consideram que, ao ser criado um objeto da classe B, na verdade são criados dois objetos, um objeto da classe A e um objeto da classe B. Se, em qualquer dos construtores de da classe B, não for invocado um dos construtores da classe A o compilador irá considerar que, no início deste construtor, existe uma invocação para o construtor sem argumentos (default) da classe A.

Em outras palavras considere o seguinte trecho de programa:
class prog{
  public static void main(String[] args){
    B v=new B();
  }
}
class A{}
class B extends A{}

Este trecho corresponde ao seguinte trecho onde código gerado pelo compilador é explicitado:
class prog{
  public static void main(String[] args){
    B v=new B();
  }
}
class A{
A(){super();}
}
class B extends A{
B(){super();}
}

 

O programa abaixo também demonstra o relacionamento das instâncias  das classes relacionadas por “extends”:

class A{
  public static int contA=0;
  public int id;
  A(){
    id=++contA;
    System.out.println("Eu sou uma instancia de A com id:"+id);
  }
}
class B extends A{
  public static int contB=0;
  public int id;
  B(){
    id=++contB;
    System.out.println("Eu sou uma instancia de B com id:"+id);
  }  
}
class C extends B{
  public static int contC=0;
  public int id;
  C(){
    id=++contC;
    System.out.println("Eu sou uma instancia de C com id:"+id);
  }
}
class Prog{
  public static void main(String[] arg){
    A x1=new A();
    B x2=new B();
    C x3=new C();
  }
}

 A palavra reservada super corresponde a uma referência para a instância da classe mãe da classe onde super aparece. Em uma classe C que estende S a expressão super.identificador corresponde a ((S)this).identificador. Em uma classe C que estende B que estende A, para acessar membros de A não podemos usar a expressão super.super.identificador, podemos usar a expressão ((A)this).identificador.

A figura abaixo mostra os objetos e classes envolvidos com as referências das variáveis x1, x2 e x3 do programa acima. A classe Object e suas instâncias não são exibidas para simplificar o desenho. Observe que este desenho tem a intenção apenas de ilustrar uma possibilidade de organização dos dados, além disso existem vários outros conceitos tais como “heap”, área de métodos e “constant pool” que são ignorados. Na figura as variáveis de classe são representadas em blocos de memória que correspondem às classes. As variáveis de instância são representadas em blocos de memória que correspondem aos objetos. Observe que o fato de cada objeto saber sua classe é representado através de setas azuis. Na classe C os métodos de instância podem fazer referência a qualquer das “instâncias ancestrais”, como exercício reescreva a classe C incluindo um método que imprime o valor da variável id de todas as “instâncias ancestrais”:

·        ((A)this).id

·        ((B)this).id e

·        this.id



Nos programas acima as variáveis e as referências atribuídas a elas tinham sempre o mesmo tipo (a menos da questão classe/interface), este é um padrão muito comum:
C V = new C();
O trecho acima corresponde a uma atribuição de uma referência para um objeto, construído pelo operador
new, do tipo C a  uma variável V também do tipo C. Conforme explicado anteriormente a definição de uma classe sempre estende a definição de alguma outra classe, exceto pelo caso da classe Object. Existem regras de compatibilidade entre referências para instâncias de classes ligadas pela hierarquia de extensão. Quando falamos de tipos primitivos discutimos a compatibilidade entre valores dos diferentes tipos. Agora discutiremos de forma breve a compatibilidade entre referências para diferentes classes.

Considere a seguinte trecho de programa:
Super V = new Sub();
O trecho acima é correto (a palavra "super" com "s" minúsculo é uma palavra reservada em Java), como regra geral, quando a classe Sub corresponde a uma definição que, diretamente, estende a classe Super, ou que, indiretamente, estende a classe Super. Uma classe A estende indiretamente uma classe B se existir uma seqüência de classes c1, c2, ...cn, tal que c1 estende a classe B, c2 estende a classe c1, ..., cn estende a classe cn-1 e a classe A estende a classe cn. Esta consideração é válida não só para iniciações (inicializações), mas também para o comando de atribuição e atribuição entre parâmetros reais e formais. A regra geral acima é fácil de ser aceita se lembrarmos que a relação de extensão em Java deve, por disciplina, ser encarada como uma relação de especialização ou generalização. Considere a definição:
class ClasseA extends ClasseB{...}
O trecho acima pode ser lido como sendo: instâncias de ClasseA são especializações das instâncias de ClasseB. Ou ainda, todas instâncias da ClasseA são instâncias da ClasseB. A idéia é baseada no princípio da substituição: qualquer instância de ClasseA pode ser usada em qualquer lugar onde podem ser usadas instâncias de ClasseB. Observe que o contrário não é verdadeiro, a ClasseA especializa a ClasseB, e instâncias de ClasseB não podem, em geral, serem usadas nos lugares que podem ser usadas instâncias de ClasseA.

No caso dos tipos primitivos podemos atribuir um valor do tipo double para uma variável do tipo int utilizando elencamento:
double d=1.0;
int i=(int) d;
De forma similar podemos atribuir uma referência para uma instância para uma variável de um tipo mais geral somente através de elencamento. É feita uma verificação em tempo de execução e a instância tem que ser mesmo do tipo alegado! Considere o trecho abaixo:
class ClasseB{...}
class ClasseA  extends ClasseB{...}
...
    ClasseB v1= new ClasseA();
    ClasseA v2=(ClasseA) v1;
...
É responsabilidade do programador garantir que as referências são adequadas;

 

Nesta disciplina não iremos estudar as diversas convenções usuais sobre herança da linguagem Java, mas deve ser notado que existe todo um conjunto de diretrizes sobre o uso de diversos conceitos. Para ilustrarmos isso observe a classe PecaDomino (peça de dominó) abaixo:

class prog{
  public static void main(String[] arg){
    PecaDomino p1=new PecaDomino(1,3);
    PecaDomino p2=new PecaDomino(3,1);
    System.out.println(p1+" "+p2);
    System.out.println(p1.equals(p2));
    System.out.println(p1.hashCode());
    System.out.println(p2.hashCode());
  }
}


class PecaDomino extends Object{
  private int ladoA;
  private int ladoB;
  PecaDomino(int a, int b){
    ladoA=a;
    ladoB=b;
  }
  PecaDomino(int[] a){
    ladoA=a[0];
    ladoB=a[1];
  }
  PecaDomino(PecaDomino p){
    ladoA=p.toArray()[0];
    ladoB=p.toArray()[1];
  }
  int[] toArray(){
    int[] d=new int[2];
    d[0]=ladoA;
    d[1]=ladoB;
    return d;
  }
  public boolean equals(Object obj){
    if((obj == null) ||
      (obj.getClass() != this.getClass())) return false;
    PecaDomino p=(PecaDomino)obj;
    return ladoA==p.ladoA & ladoB==p.ladoB ||
           ladoA==p.ladoB & ladoB==p.ladoA;
  }
  public int hashCode(){
    return ladoA>ladoB?ladoB*7+ladoA:ladoA*7+ladoB;
  }
  public String toString(){
    return ladoA>ladoB?ladoB+":"+ladoA:ladoA+":"+ladoB;
  }

Observe que esta classe redefine os métodos equals(), hashcode() e toString() definidos originalmente na classe Object. O “contrato” do método equals() tem o compromisso de devolver true se as duas instâncias envolvidas são iguais e false se são diferentes. Observe a implementação, a primeira verificação relaciona-se a referência nula, a verificação seguinte utiliza o método getClass(). O método getClass() da classe Object retorna uma referência para um objeto que representa a classe de uma instância! A verificação seguinte consiste comparar os valores. O método hashCode() é útil para a implementação de algumas técnicas de armazenamento e pesquisa que serão vistas com mais detalhe em disciplinas mais à frente. A idéia é ter um valor inteiro que sirva como um identificador mas sem garantia de unicidade. O contrato do método hashcode() consiste em retornar, para uma dada instância, um valor qualquer do tipo int que seja sempre o mesmo durante o tempo de execução do programa (pode variar entre diferentes execuções!). Se o1.equals(o2) então o1.hashCode() deve ser o mesmo de o2.hashCode(). O método toString() retorna um String que representa “textualmente” a instância correspondente. Ao serem definidas as classes existem várias possibilidades de definição e redefinição de métodos similares aos casos aqui discutidos.

 

A classe Object

A classe Object não possui nenhum campo (estrutura/estado). A classe Object possui uns poucos métodos:

protected Object clone()

boolean equals(Object obj)

protected void finalize()

Class getClass()

int hashCode()

void notify()

void notifyAll()

String toString()

void wait()

void wait(long timeout)

void wait(long timeout, int nanos)

Os métodos notify(), notifyAll(), wait(), wait(long timeout), wait(long timeout, int nanos) são diretamente relacionados ao tópico “threads” que não é discutido nesta disciplina introdutória de programação. Os métodos clone(), equals(Object o), finalize(), hashCode(), toString()  deveriam ser planejados para cada classe a ser definida. Nesta disciplina introdutória não iremos obrigar o cumprimento de uma disciplina que existe no ambiente do profissional de programação Java. Mas devemos ressaltar que no ambiente do programador profisional de programas em Java não podemos definir classes da forma como definimos nesta disciplina. Em particular, no ambiente profissional além do cuidado de redefinir métodos da Object (e outras classes) definimos, em geral, somente uma classe por arquivo!


O arcabouço “Collections”  da linguagem Java

O ultimo material desta disciplina (Listas) está relacionado a um arcabouço (framework) da linguagem Java denominado Coleções (Collections). Este arcabouço possui vários elementos relacionados ao problema de tratamento de grupos (p.ex. conjuntos, multiconjuntos, listas) de elementos. Várias das classes deste arcabouço são definidas a partir de outras classes.
 

Exercícios
 

  1. Faça um desenho, conforme as convenções da disciplina, das variáveis e objetos relativos ao seguinte trecho de programa:

    Poupanca[] x=new Poupanca[]{new Poupanca(100.00), new Poupanca(200.00)};

  2. Explique por que o seguinte trecho de programa Java é compilado:


class A{private A(){} A(int i){}}
class B extends A{private B(){super(2);}}
e o seguinte trecho não compila:
class A{private A(){}}
class B extends A{private B(){}}
 

  1. Execute o programa Java abaixo e explique porque ao ser criada uma instância de B também é criada uma instância de A. Explique também como é acionado o comando de impressão no construtor de A sendo que não existe uma chamada explícita.
    class A{
      A(){System.out.println("criada instancia de A!");}
    }


class B extends A{
  B(){System.out.println("criada instancia de B!");}
}

class construtor{
  public static void main(String[] args){
    B v=new B();
  }
}

  1. Reescreva a classe Poupanca (e a classe Conta?) apresentadas acima de forma a existir uma numeração especifica para contas do tipo Conta e contas do tipo Poupança. Considere que no trecho abaixo são mostradas a primeira instanciação de Conta e a primeira instanciação de Poupanca:


Conta c1=new Conta(100.);
Poupanca p1= new Poupanca(120.);
As invocações:
System.out.println(c1.obterNumero());
System.out.println(p1.obterNumero());
imprimem dois valores zero.
Na implementação mostrada seriam impressos os valores 0 e 1.
 
 

  1. Complete o programa abaixo:


class programa{
  public static void main(String[] arg){
    /* declara um arranjo de 3 componentes do tipo Retangulo */
    Retangulo[] ar=new Retangulo[3];
    /*cria 3 retangulos */
    ar[0]=new Retangulo(3,4);
    ar[1]=new Retangulo(5,7);
    ar[2]=new Quadrado(8);
    for(int i=0; i<ar.length; i++)
      System.out.println("Retangulo:"+(i+1)+
                       " Largura:"+ar[i].largura()+
                       " Altura:"+ar[i].altura()+
                       " Area:"+ar[i].area()+
                       " Perimetro:"+ar[i].perimetro());
  }
}

class Retangulo{
  private int lado1, lado2;
  Retangulo(int l1, int l2){ lado1=l1; lado2=l2;}
  public int area(){________________________}
  public int perimetro(){________________________}
  public int altura(){return lado1;}
  public int largura(){return lado2;}
}

class Quadrado extends Retangulo{
  Quadrado(int lado){
    super(lado,lado);
  }
}

  1. Reescreva o programa acima definindo as classes Quadrilatero, Trapezio e Paralelogramo. Não é necessário reescrever os métodos e construtores da classe Retangulo.
  2. Escreva um programa que define e utilize a classe Triangulo e as subclasses Isosceles, Equilatero. Coloque os construtores, campos e métodos que julgar adequados.


class Triangulo{...}
class Isosceles extends Triangulo{...}
class Equilatero extends Isosceles{...}