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 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.
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(){}}
class B extends A{
B(){System.out.println("criada instancia de B!");}
}
class construtor{
public static void main(String[] args){
B v=new B();
}
}
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.
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);
}
}
class Triangulo{...}
class Isosceles extends
Triangulo{...}
class Equilatero extends
Isosceles{...}