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, já discutido no item
String, será discutido de forma ainda mais abrangente mais a frente. Por agora
iremos somente repetir que os construtores servem, tipicamente, para definir a
iniciação (ou inicialização!) das instâncias. 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, não discutimos várias 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. Esta classe C não tem nenhuma utilidade a não ser demonstrar qual
é a menor definição possível para uma classe.
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. 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 contrutor 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 a 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
de dois objetos da Classe C.
Toda definição de classe
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.
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.round(Math.random()*6+0.5); /*razoável??*/
}
}
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.round(Math.random()*6+0.5);
}
}
É 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.round(Math.random()*6+0.5);
}
}
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 funcionam como métodos mas não
podem ser invocados da mesmo forma como podem ser os métodos. Os contrutores
somente podem ser invocados utilizando o operador new na hora
da construção de uma instância de uma classe. O identificador de um construtor
corresponde ao identificador de sua classe. A distinção dos contrutores 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.round(Math.random()*6+0.5);
}
}
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.
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 é bastante ú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 contrutor:
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.round(Math.random()*6+0.5);
}
}
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 contrutor. O
contrutor 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.
Exercícios:
1.
Utilize a definição dada da classe Cdado e teste a freqüência
dos valores das faces 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? Senão, dê uma expressão
mais adequada.
2.
Verifique que acrescentando um construtor X(parametro) na definição de
uma classe o contrutor 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.