Para poder formar um modelo
mental inicial (é apenas um auxílio mental!) podemos imaginar que as classes
são como conjuntos e as instâncias/objetos são como elementos destes conjuntos.
Este modelo mental só será útil se a pessoa não confundir os conceitos da
Matemática com os conceitos da Computação. Estamos falando aqui do conceito de
classe da Computação, mais especificamente o conceito de classe da linguagem
Java, o conceito de classe da Matemática, p.ex. nos axiomas de von Neumann-Bernays-Gödel, são bastante
distintos. Para perceber um aspecto bem distinto de (i)classe Java e (ii)
conjunto, considere o problema da categoria dos elementos: só podem ser objetos
de X objetos do tipo X (a classe define um tipo) mas na matemática um conjunto
pode envolver elementos de qualquer tipo, você pode definir de forma extensiva
um conjunto Y constituído de 3 elementos: um livro, uma idéia e um barulho.
Conforme veremos abaixo as classes Java têm uma relação muito forte com a
questão da tipagem.
As classes servem para definir estrutura e comportamento. Na
linguagem Java as classes constituem um dos mecanismos de definição de tipos.
Os oito tipos primitivos estão disponíveis na linguagem, o programador através
da definição de classes define novos tipos. Outro mecanismo de definição de
tipo em Java é a interface, isto será visto mais à frente. É utilizando as
classes ou suas instâncias que podemos compor programas dentro do
paradigma denominado Programação Orientada a Objetos. A estrutura
corresponde, tipicamente, a variáveis ou campos e o comportamento
corresponde a métodos. O papel dos construtores relacionado à
inicialização, já discutido no item String, será discutido de forma ainda mais
abrangente mais a frente. As classes podem ter centenas ou até mesmo milhares
de linha de código fonte. Nossos exemplos aqui são artificiais e curtos para
simplificar os conceitos. Também para efeito de simplificação, discutimos
apenas algumas opções de definição e uso de classes.
A linguagem Java possui
várias classes pré-definidas tais como a classe String. O programador além de
poder utilizar as classes pré-definidas pode também definir classes. Em Java a
capacidade de definir classes corresponde à capacidade de definir tipos.
O método main que é o ponto de origem do fluxo de controle deve ser
declarado dentro de uma classe. Até este ponto da disciplina esta classe tem
sido referida como sendo uma classe que tem o mesmo nome do arquivo onde fica o
código fonte do programa. Agora veremos que um arquivo fonte em java pode
conter não só a classe que contém o método main, mas também outras classes. É
possível organizar o código fonte de um programa em linguagem Java em vários
arquivos, mas isto não será discutido aqui.
O programa abaixo tem como
objetivo ilustrar a definição de uma classe. Uma classe é definida
utilizando-se a palavra chave class seguida do nome da classe e da
definição dos membros dentro de um bloco definido por abre e fecha chaves. No
exemplo abaixo a classe C não define nenhum membro e a classe programa define
apenas o método main. A utilidade desta classe C é apenas exemplificar qual a
menor definição possível para uma classe: class C{} :
class programa{
public static void main(String[] args){
C v1=new C();
C v2;
v2=new C();
System.out.println("Representacao do objeto
referido por v1:"+v1);
System.out.println("Representacao do objeto
referido por v2:"+v2);
}
}
class C{}
O programa acima define
duas classes. A classe programa cumpre a burocracia de conter o método main que
é a origem do fluxo de controle. A classe C corresponde à menor definição
possível, não contém nenhum membro. A definição de uma classe define um tipo. A
definição da classe C define o tipo C. No programa acima vemos dois objetos do
tipo C serem instanciados. Vamos referir à classe programa como sendo uma
classe cliente da classe C, pois o método main define variáveis do tipo
C. Um primeiro objeto é instanciado na iniciação da variável v1. O
operador new cria um objeto e retorna a referência (ou seja, normalmente
iremos criar uma população de objetos utilizando o operador new). A
expressão que segue o operador new corresponde a chamada de um construtor.
No programa acima a classe C não define construtores (isto será visto mais a
frente), o compilador Java provê um construtor default para a classe C.
O segundo objeto é instanciado no comando de atribuição da variável v2. A
tentativa de impressão de uma referência para qualquer objeto corresponde a um
mecanismo de controle de fluxo que será explicado mais a frente na disciplina.
O programa acima imprime duas instâncias de String, correspondentes a representações
(do tipo String) de dois objetos da Classe C.
Toda definição de classe só
pode ser feita estendendo uma outra
classe. Esta extensão estabelece uma relação de herança entre a classe a
ser definida e uma outra classe. O assunto herança será visto mais à
frente na disciplina, mas aqui enfatizamos que existe apenas uma classe que não
tem relação de herança com outras classes. Esta classe única é
considerada a "mãe" de todas as classes, é uma classe pré-definida na
linguagem Java e é denominada Object. Uma definição de classe que não
explicita a relação de herança corresponde à definição de uma classe que é
"filha" direta da (tem relação de herança direta com a ) classe
Object. Ou seja a definição:
class C{}
é uma forma abreviada da definição, ou, é entendido pelo compilador como sendo:
class C extends Object{}
Falando de outra maneira, toda definição de classe corresponde a estender a definição
de alguma outra classe e a única classe que não estende outras classes é a
classe pré-definida Object. Podemos construir uma hierarquia
de classes ligadas pelo mecanismo de extensão. A "raiz" desta
hierarquia é a classe Object. As outras classes ficam
ligadas direta ou indiretamente à classe Object.
Na linguagem Java não temos
mecanismos para descrever separadamente as classes(conjuntos) e os
objetos(elementos). A sintaxe da linguagem é voltada para o conceito de classe
e dentro da definição da classe, de maneira geral, o ítem precedido pela
palavra reservada static é exclusivo da classe e o item que não é modificado pela cláusula static define um aspecto das instâncias.
O programa abaixo mostra
uma classe com dois campos.
class programa{
public static void
main(String[] args){
D v1, v2;
v1=new D();
v2=new D();
v1.i=10; v1.d=1.0E2;
v2.i=200;
v2.d=3.0;
System.out.println("i de v1:"+v1.i+" d de v1:"+v1.d+"
i de v2:"+v2.i);
}
}
class D{
int i; double d;
}
O programa acima define
duas classes. A classe programa contém um membro, o método estático main. A
classe D define duas variáveis ou campos: um campo do tipo inteiro e um
campo do tipo double. No método main são instanciados dois objetos do tipo D.
As referências para os dois objetos são armazenadas nas variáveis v1 e v2. No
caso da definição acima os campos i e d são variáveis de instância. Cada
instância da classe D tem seus campos i e d distintos, ou seja, existirão
tantos i e d quantas forem as instâncias da classe D. O acesso aos membros,
conforme já discutido no item Arranjo e também no item String, é feito através
do operador de seleção - um ponto: '.' aplicado sobre uma referência ou ao nome
de uma classe. Um campo ou variável pode ser definida como sendo uma variável
de classe (e *não* ser uma variável de instância!) prefixando a definição com a
palavra chave static. Assim na definição abaixo existirão apenas dois campos
não importando quantas instâncias da classe E forem criadas:
class E{
static int i;
static double d;
}
Métodos de
instância e métodos de classe (estáticos) - Comportamento
As
classes abaixo tentam modelar a estrutura e comportamento de um dado com seis
faces numeradas de 1 a 6 e que pode ser lançado.
A
classe Cdado abaixo utiliza o método Math.random() para gerar um valor
pseudo-aleatório, do tipo double, entre 0 e 1. Este valor entre 0 e 1 é
convertido para um valor inteiro entre 1 e 6. A classe abaixo permite às
classes clientes acessar um campo do tipo inteiro para armazenar o valor
da face corrente. O método estático ou método de classe gerarFace() foi
modificado pela palavra chave private. Um método ou campo private só pode ser
utilizado internamente à definição da classe, ou seja as classes clientes não
têm acesso aos método e campos private. Um método pode ser modificado pela
palavra chave public. Um método public pode ser acessado pelas classes
clientes.
class programa{
public static void main(String[] args){
Cdado dado=new Cdado();
dado.rolar();
System.out.println(dado.face);
}
}
class Cdado{
public int face=gerarFace();
public void rolar(){
face=gerarFace();
}
private static int gerarFace(){
return (int)(Math.random()*6)+1;
}
}
A utilização da classe
Cdado certamente não é obrigatória na construção de um programa que lida com
dados, o programa abaixo mostra a modelagem de um dado utilizando variáveis do
tipo inteiro e um método:
class programa{
public static void main(String[] args){
int faceDado1=gerarFace();
int faceDado2=gerarFace();
System.out.println(faceDado1);
System.out.println(faceDado2);
}
static int gerarFace(){
return (int)(Math.random()*6)+1;
}
}
É importante notar que a
classe é um mecanismo que suporta abstrações mais expressivas do que o método.
O método dá suporte a abstrações de comportamento ou abstrações funcionais. A
classe permite não só as abstrações de comportamento, mas também abstrações de
estado. Compare os dois programas acima. A estrutura do programa que usa classe
dá suporte ao modelo de um dado, enquanto que o programa que tem apenas o
método main não separa a modelagem, a definição, o uso do conceito de dados.
Por ser um mecanismo muito
poderoso e expressivo a questão de como projetar classes não é um tema simples.
Observe a classe Cdado no programa abaixo.
class programa{
public static void main(String[] args){
Cdado dado=new Cdado();
dado.rolar();
System.out.println(dado.mostrarFace());
}
}
class Cdado{
private int face=gerarFace();
public void rolar(){
face=gerarFace();
}
public int mostrarFace(){
return face;
}
private static int gerarFace(){
return (int)(Math.random()*6)+1;
}
}
No programa acima a classe
Cdado não permite mais o acesso ao campo face. Uma
classe cliente deve obter o valor da face através de um método. Uma vantagem
desta implementação é que uma classe cliente não pode fazer atribuições ao
campo face.
Conforme já visto no caso
particular da classe String, os construtores têm similaridades com métodos de
instância, mas sua função não é genérica como os métodos. Os construtores devem
ser utilizados para inicializar (iniciar) as instâncias. Os construtores só
podem ser invocados utilizando o operador new no momento da construção de uma
instância de uma classe. O identificador ou nome de um construtor corresponde
ao nome de sua classe. A distinção dos construtores de uma mesma classe ocorre
em função de seus parâmetros. No programa abaixo adicionamos um construtor à
classe Cdado. O construtor adicionado permite ajustar qual é o valor inicial da
face (não é verificado se o valor está entre 1 e 6!):
class programa{
public static void main(String[] args){
Cdado dado=new Cdado(4);
System.out.println(dado.mostrarFace());
}
}
class Cdado{
Cdado(int f){face=f;}
private int face=geraFace();
public void rolar(){
face=geraFace();
}
public int mostrarFace(){
return face;
}
private static int geraFace(){
return (int)(Math.random()*6)+1;
}
}
O construtor tem um parâmetro do tipo inteiro. Este construtor é invocado pelo operador
new na hora em que é criado o objeto do tipo Cdado cujo referência é armazenada
na variável dado.
Quando definimos um
construtor o “construtor default” não é mais definido:
class programa{
public static void
main(String[] args){
Cdado
dado=new Cdado(); //erro! Cdado() não
encontrado!
System.out.println(dado.mostrarFace());
}
}
class Cdado{
Cdado(int
f){face=f;}
private int
face=geraFace();
public void rolar(){
face=geraFace();
}
public int
mostrarFace(){
return
face;
}
private static int
geraFace(){
return (int)(Math.random()*6)+1;
}
}
Abaixo vemos uma classe com
vários possíveis construtores:
class C{
C(){…}
C(int a){…}
C(double a){…}
C(int[] a){…}
C(int a, char[] b){…}
C(ClasseA x, ClasseB y, int z,
ClasseC[] d){…}
…
}
A linguagem Java não permite ambiguidades na chamada dos construtores:
class prog{
public static void main(String[]
args){
C x= new C(65,65);
}
}
class C{
C(int a, long b){System.out.println("C(int,
double)");}
C(long a, int
b){System.out.println("C(double, int)");}
}
O programa abaixo
implementa o conceito de peça do jogo “dominó”. Observe a classe PecaDomino. Esta classe só define “estrutura”
e não define nenhum comportamento “interessante” para uma peça de dominó. A
melhor opção para representar um conjunto de peças de dominó seria alguma
classe que implementa a interface java.util.Collection, mas não é este o nosso foco neste
momento, e sim o conceito de classe e construtor. A estrutura definida para uma
peça de dominó consiste em dois campos do tipo int. Foram definidos três
construtores (três estruturas de parâmetros): (i) dois ints, (ii) um arranjo de ints e (iii) uma peça de dominó que é
reproduzida. O objetivo deste programa é apenas ilustrar o papel dos
construtores na inicialização das instâncias de classe.
class prog{
public static void
main(String[] arg){
PecaDomino[]
mao=new PecaDomino[28];
int k=0;
for(int i=0;
i<=6; i++)
for(int j=i;
j<=6; j++)
mao[k++]=new
PecaDomino(i,j);
imprimeMao(mao); //
imprime as 28 pecas
PecaDomino[]
outramao=new PecaDomino[28];
for(int i=0;
i<28; i++)
outramao[i]=new PecaDomino(mao[i]);
imprimeMao(outramao); // imprime a nova mao
}
static void
imprimeMao(PecaDomino[] a){
for(int i=0;
i<a.length; i++)
System.out.println(a[i].toArray()[0]+
" "+a[i].toArray()[1]);
}
}
class PecaDomino{
int ladoA;
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;
}
}
O programa abaixo é uma
versão do programa acima que ilustra que os construtores podem ficar ocultos
dentro do código da classe. No programa abaixo ocultamos apenas um dos três
construtores, o construtor que recebe dois ints. No lugar deste construtor
ficou disponível o método (estático!) criaPeca (cria peça). Este é um outo
estilo de lidar com a construção e inicialização de instâncias. Como exercício
reescreva o programa ocultando os outros construtores.
class prog{
public static void main(String[] arg){
PecaDomino[]
mao=new PecaDomino[28];
int k=0;
for(int i=0;
i<=6; i++)
for(int j=i;
j<=6; j++)
mao[k++]=PecaDomino.criaPeca(i,j);
imprimeMao(mao); // imprime as 28 pecas
PecaDomino[]
outramao=new PecaDomino[28];
for(int i=0; i<28; i++)
outramao[i]=new PecaDomino(mao[i]);
imprimeMao(outramao); // imprime a nova mao
}
static void
imprimeMao(PecaDomino[] a){
for(int i=0;
i<a.length; i++)
System.out.println(a[i].toArray()[0]+
" "+a[i].toArray()[1]);
}
}
class PecaDomino{
int ladoA;
int ladoB;
static PecaDomino
criaPeca(int x, int y){
return new
PecaDomino(x,y);
}
private
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;
}
}
O compilador Java disponibiliza
para o programador a palavra chave this. A palavra chave this só pode ser usada dentro de
construtores e métodos de instância. A palavra chave this funciona como se fosse uma variável
contendo a referência da instância que está sendo criada ou através da qual foi
feita a chamada do método. Existem várias situações onde o uso da palavra
chave this é útil. Mais à frente na disciplina veremos inclusive que a palavra
reservada this pode ser elencada no contexto da relação de extensão entre classes. No
programa abaixo a palavra chave this é usada para desambiguar o nome do
parâmetro e o nome do campo no construtor:
class programa{
public static void main(String[] args){
Cdado dado=new Cdado(4);
System.out.println(dado.mostrarFace());
dado.rolar();
System.out.println(dado.mostrarFace());
}
}
class Cdado{
Cdado(int face){this.face=face;}
private int face=geraFace();
public void rolar(){
face=geraFace();
}
public int mostrarFace(){
return face;
}
private static int geraFace(){
return (int)(Math.random()*6)+1;
}
}
Observe que no construtor Cdado() this.face designa o campo face e face designa o parâmetro face
A classe abaixo lida com
instâncias de CPF (conforme visto anteriormente). Observe o uso de métodos
estáticos e não estáticos no trecho de programa abaixo.
class CPF{
private int cpf;
CPF(int i){cpf=i;}
public int digitosVerificadores(){
return digsVer(cpf);
}
public static int digitosVerificadores(int n){
return digsVer(n);
}
public String digitosVerificadoresStr(){
int dv=digsVer(cpf);
return String.valueOf(dv/10)+dv%10;
}
public static String digitosVerificadoresStr(int n){
int dv=digsVer(n);
return String.valueOf(dv/10)+dv%10;
}
private static int digsVer(int n){
int[]
d=new int[9];
int
aux=n;
int i;
for(i=0; i<9; i++){ d[i]=aux%10; aux=aux/10;}
int
somaprod=0;
for(i=0; i<9; i++)somaprod+=d[i]*(i+2);
int
dvDez=11-somaprod%11;
if(dvDez>9) dvDez=0;
somaprod=dvDez*2;
for(i=0; i<9; i++)somaprod+=d[i]*(i+3);
int
dvUni=11-somaprod%11;
if(dvUni>9) dvUni=0;
return
dvDez*10+dvUni;
}
}
Exemplos de utilização da
classe CPF:
CPF n1=new CPF(123456789);
System.out.println(n1.digitosVerificadores()); /* metodo de instância */
System.out.println(CPF.digitosVerificadoresStr(987654321));
/*metodo estatico*/
Um programador poderia
querer utilizar a classe CPF definida acima usando o construtor default:
CPF v=new CPF();
No caso acima o
programador deseja criar uma instância de CPF sem especificar a
iniciação/inicialização do CPF. O compilador Java gera de forma automática um
construtor default apenas quando não existe nenhuma definição de construtor. O
construtor default deixa de ser gerado automaticamente para a classe CPF quando
é definido o construtor parametrizado com um valor do tipo int. Se não forem
definidos construtores parametrizados para uma classe o acesso ao construtor
default pode ser impedido através do uso do modificador "private":
private CPF(){}
Observe o programa abaixo:
class prog{
public static void main(String[]
arg){
X v1=X.fabricaObjeto();
X v2=X.fabricaObjeto();
v1.souEu();
v2.souEu();
}
}
class X{
private X(){}
public static X fabricaObjeto(){
return new X();
}
static int i=0;
int eu=++i;
public void souEu(){
System.out.println("Eu sou o
objeto numero:"+eu);
}
}
No programa acima o programador do método main,
cliente da classe X não pode utilizar o comando
X v3=new X();
pois
X é uma classe cujo construtor default teve o acesso impedido através do
modificador “private”.
Carga e inicialização de classes, inicialização
de instâncias
Quando a máquina virtual
java (MVJ) executa um programa java, ela recebe a indicação de uma classe que
contém o método main. A MVJ lê o arquivo contendo os bytecodes desta classe e
carrega este código na memória. Qualquer outra classe referenciada por esta
classe poderá ser carregada na memória a qualquer momento. O fluxo de controle
deverá percorrer por vários itens destas classes. Nesta disciplina não iremos
listar todas as ocorrências relativas ao fluxo de controle, mas observe a
definição da classe abaixo:
class C{
public int i=m1();
public static int e=m2();
private int m1(){
Sytem.out.println(“iniciou campo de instancia”);
return 1;
}
private static int
m2(){
Sytem.out.println(“iniciou campo de classe”);
return 1;
}
}
Um programa contendo
esta classe terá que, em algum momento qualquer MAS ANTES DA CRIAÇÃO DE UMA
PRIMEIRA INSTÂNCIA, fazer com que o fluxo de controle percorra as
inicializações de campos estáticos desta classe
(por exemplo na hora da carga), especificamente a linha public
static int e=m2();.
Depois que uma classe é carregada e posteriormente ao fluxo percorrer as
inicializações dos campos de classe, para cada criação de instância, o fluxo de
controle deverá percorrer as inicializações das variáveis ou campos de
instância, especificamente a linha public int i=m1(); Os valores de iniciais dos campos
de instância estão disponíveis para o construtores, pois a execução do
construtor só ocorre depois que o fluxo de controle percorre as inicializações
dos campos de instância.
As classes da linguagem
Java podem conter campos (estáticos e não estáticos), métodos (estáticos e não
estáticos), construtores e ainda os chamados “blocos inicializadores
estáticos”. Estes blocos podem aparecer várias vezes no corpo da classe e, da
mesma forma que as expressões de inicialização de campos são executados na
ordem em que aparecem. Os blocos iniciadores estáticos da mesma forma que as
expressões de iniciação dos campos estáticos são executados apenas uma vez, no
momento em que a classe é “carregada”. O Programa abaixo exemplifica a sintaxe:
class prog{
public static void main(String[]
x){
C v1,v2;
v1=new C();
v2=new C();
}
}
class C{
static{
System.out.println("oi!"); /* executado somente 1 vez */
}
}
Exercícios:
1.
Teste
a freqüência dos valores da face superior de um dado em um número
suficientemente grande de lançamentos. A expressão:(int)Math.round(Math.random()*6+0.5) gera valores de faces uniformemente
distribuídos?
E a expressão(int)(Math.random()*6)+1 ?
2.
Verifique
que acrescentando um construtor X(parametro) na definição de uma classe o construtor
default X() não é mais
definido automaticamente, ou seja, uma classe cliente não consegue utilizar o
construtor X().
3.
Complete
a codificação da classe (*) abaixo:
class ArabicoParaRomano{
static String[]
u={"","I","II","III","IV","V","VI","VII","VIII","IX"};
static String[]
d={"","X","XX","XXX","XL","L","LX","LXX","LXXX","XC"};
static String[]
c={"","C","CC","CCC","CD","D","DC","DCC","DCCC","CM"};
static String[]
m={"","M","MM","MMM"};
private int valor;
ArabicoParaRomano(int v){valor=v>0&v<4000?v:0;}
public void ajustaValor(int v){valor=v>0&v<4000?v:0;}
public String obtemValor(){
return _____________________________________;}
}
class programa{
public static void main(String[] args){
ArabicoParaRomano n=new ArabicoParaRomano(1234);
System.out.println(n.obtemValor()); /* imprime
MCCXXXIV */
n.ajustaValor(3210);
System.out.println(n.obtemValor()); /* imprime MMMCCX
*/
}
}
(*) ArabicoParaRomano é, talvez, um nome mais
apropriado para um método. Uma organização mais interessante para o problema de
representação em algarismos romanos de um número (Number) talvez fosse o uso da
relação "extends" que será vista mais à frente na disciplina. Esta
classe pode ser definida de forma a não ter instâncias ou de forma a permitir
instâncias.