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

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.

 

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

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.

Construtores

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;

  }

}

 

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 é ú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

 

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 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.

  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.
  4. Considere que X é uma classe. Escreva um programa que mostre que a declaração de uma variável do tipo X não inicia a execução da inicialização dos campos de classe (campos estáticos).
  5. Quando a classe não expõe sua estrutura/estado os desenhistas e programadores ficam mais livres para as decisões de implementação. Observe a classe abaixo, ela é implementada de forma diferente à apresentada anteriormente. Cite uma desvantagem da classe abaixo relacionada à “generalidade” de valores de peças de um “dominó generalizado”.
    class PecaDomino{
      int cod;
      PecaDomino(int a, int b){
        cod=a*10+b;
      }
      PecaDomino(int[] a){
        cod=a[0]*10+a[1];
      }
      PecaDomino(PecaDomino p){
        cod=p.cod;
      }
      int[] toArray(){
        int[] d=new int[2];
        d[0]=cod/10;
        d[1]=cod%10;
        return d;
      }
      public String toString(){
        return (cod/10)+":"+(cod%10);
      }
    }

  6. Implemente a classe PecaDomino de forma que os objetos representando as peças sejam imutáveis. Os objetos (peças) devem ser criados através de um bloco estático de inicialização (static{...}). As implementações podem ter diversas escolhas. O trecho abaixo é uma tentativa. Esta tentativa utiliza um arranjo e simplifica a conversão entre índice e valores de peça (p.ex. índice 0 corresponde á peça 0, índice  48 corresponde à peça 6:6 . Esta tentativa pode ser “aperfeiçoada” instanciando apenas 28 objetos ao invés de 49, neste caso o tratamento da conversão entre índices e valores de peça (p.ex. índice 0 corresponde a peça 0:0, índice 27 corresponde à peça 6:6) fica mais elaborado. Uma solução simples seria o uso do comando “switch”! Outra solução simples é mostrada abaixo, utilizando dois arranjos de valores (mas só instancia 28 objetos correspondendo às peças!).
    ...
      PecaDomino p=PecaDomino.peca(3,6);
    ...
    class PecaDomino{
      static PecaDomino[] a=new PecaDomino[49];
      static{
        for(int i=0; i<49; i++)
          a[i]=new PecaDomino();
      }
      private PecaDomino(){}
      static PecaDomino peca(int i, int j){
        int x=i<j?i:j;
        int y=i<j?j:i;
        return a[x*7+y];
      }...

    class PecaDomino{
      static PecaDomino[] a=new PecaDomino[28];
      static int[] valor1=
        new int[]{0,0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2,2,3,3,3,3,4,4,4,5,5,6};
      static int[] valor2=
        new int[]{0,1,2,3,4,5,6,1,2,3,4,5,6,2,3,4,5,6,3,4,5,6,4,5,6,5,6,6};
      static{
        for(int i=0; i<28; i++)
          a[i]=new PecaDomino();
      }
      private PecaDomino(){}
      static PecaDomino peca(int i, int j){
        int x=i<j?i:j;
        int y=i<j?j:i;
        int k;
        for(k=0; i<28; k++)if(valor1[k]==x&valor2[k]==y) break;
        return a[k];
      }
      public String toString(){
        int i;
        for(i=0; i<28; i++) if(a[i]==this) break;
        return valor1[i]+":"+valor2[i];
      }
    }