Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação

Algoritmos e Estruturas de Dados I

Definição e uso de classes


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.

 

Campos de instância e campos de classe(estáticos) - Estrutura

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.

Construtores

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.

Palavra chave this

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

Comparando métodos de instância e métodos de classe


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*/

Aspectos do construtor default

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.

  1. Acrescente à classe acima o seguinte método:
    public void incrementa(){...}
    Este método deve incrementar o valor em uma unidade. Se o valor atual for 3999 passar para zero.

  2. Escreva uma classe Lampada (lâmpada). Pense em quais são os possíveis estados e quais são os possíveis comportamentos para uma lâmpada.

  3. Escreva um programa que demonstre que a inicialização dos campos de instância de uma classe X acontece antes da execução de um construtor de X.