3. Classes e interfaces
3.1. O objeto por meio de exemplos
3.1.1. Noções gerais
Abordaremos agora, por meio de exemplos, a programação orientada a objetos. Um objeto é uma entidade que contém dados que definem seu estado (chamados de atributos ou propriedades) e funções (chamadas de métodos). Um objeto é criado de acordo com um modelo chamado de classe:
public class C1{
type1 p1; // propriedade p1
type2 p2; // propriedade p2
…
type3 m3(…){ // método m3
…
}
type4 m4(…){ // método m4
…
}
…
}
A partir da classe C1 anterior, é possível criar vários objetos O1, O2,… Todos terão as propriedades p1, p2, … e os métodos m3, m4, … Eles terão valores diferentes para suas propriedades pi, possuindo, assim, cada um um estado próprio.
Se O1 é um objeto do tipo C1, O1.p1 designa a propriedade p1 de O1 e O1.m1 o método m1 de O1.
Consideremos um primeiro modelo de objeto: a classe personne.
3.1.2. Definição da classe “pessoa”
A definição da classe personne será a seguinte:
import java.io.*;
public class personne{
// atributos
private String prenom;
private String nom;
private int age;
// método
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// método
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Temos aqui a definição de uma classe, ou seja, um tipo de dado. Ao criarmos variáveis desse tipo, elas serão chamadas de objetos. Uma classe é, portanto, um modelo a partir do qual os objetos são construídos.
Os membros ou campos de uma classe podem ser dados ou métodos (funções). Esses campos podem ter um dos três atributos a seguir:
privé: Um campo privado (private) só é acessível pelos métodos internos da classe
public: Um campo público é acessível por qualquer função, definida ou não dentro da classe
protégé: Um campo protegido (protected) só é acessível pelos métodos internos da classe ou de um objeto derivado (veja mais adiante o conceito de herança).
Em geral, os dados de uma classe são declarados privados, enquanto seus métodos são declarados públicos. Isso significa que o usuário de um objeto (o programador)
a: não terá acesso direto aos dados privados do objeto
b: poderá chamar os métodos públicos do objeto e, em especial, aqueles que darão acesso aos seus dados privados.
A sintaxe para declarar um objeto é a seguinte:
public class nomClasse{
private donnée ou méthode privée
public donnée ou méthode publique
protected donnée ou méthode protégée
}
Remarques
- A ordem de declaração dos atributos private, protected e public é arbitrária.
3.1.3. O método initialize
Voltemos à nossa classe **pessoa,** declarada como:
import java.io.*;
public class personne{
// atributos
private String prenom;
private String nom;
private int age;
// método
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// método
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Qual é a função do método initialise? Como nome, sobrenome e idade são dados privados da classe Pessoa, as instruções
são inválidas. Precisamos inicializar um objeto do tipo personne por meio de um método público. Essa é a função do método initialise. Escreveremos:
A sintaxe p1.initialise é válida, pois initialise é de acesso público.
3.1.4. O operador new
A sequência de instruções
está incorreta. A instrução
declara p1 como uma referência a um objeto do tipo personne. Esse objeto ainda não existe e, portanto, p1 não está inicializado. É como se escrevêssemos:
onde se indica explicitamente, com a palavra-chave **null, que a variável p1 ainda não faz referência a nenhum objeto.
Quando, em seguida, escrevemos
, estamos chamando o método initialise do objeto referenciado por p1. No entanto, esse objeto ainda não existe e o compilador sinalizará o erro. Para que p1 refira um objeto, é preciso escrever:
Isso tem como efeito a criação de um objeto do tipo personne ainda não inicializado: os atributos nom e prenom, que são referências a objetos do tipo String, terão o valor null, e age terá o valor 0. Portanto, há uma inicialização padrão. Agora que p1 faz referência a um objeto, a instrução de inicialização desse objeto
é válida.
3.1.5. A palavra-chave this
Vejamos o código do método initialise:
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
A instrução this.prenom=P significa que o atributo prenom do objeto atual (this) recebe o valor P. A palavra-chave this designa o objeto atual: aquele no qual se encontra o método executado. Como sabemos disso? Vejamos como ocorre a inicialização do objeto referenciado por p1 no programa chamador:
É o método initialise do objeto p1 que é chamado. Quando, nesse método, fazemos referência ao objeto this, na verdade estamos fazendo referência ao objeto p1. O método initialise também poderia ter sido escrito da seguinte forma:
public void initialise(String P, String N, int age){
prenom=P;
nom=N;
this.age=age;
}
Quando um método de um objeto faz referência a um atributo A desse objeto, a notação this.A é implícita. Deve-se utilizá-la explicitamente quando houver conflito de identificadores. Esse é o caso da instrução:
this.age=age;
onde age designa um atributo do objeto atual, bem como o parâmetro age recebido pelo método. É necessário, então, eliminar a ambiguidade, designando o atributo age como this.age.
3.1.6. Um programa de teste
Aqui está um programa de teste:
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
A classe personne está definida no arquivo-fonte personne.java e é compilada:
E:\data\serge\JAVA\BASES\OBJETS\2>javac personne.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
Fazemos o mesmo para o programa de teste:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
10/06/2002 09:25 418 test1.class
É surpreendente que o programa test1.java não importe a classe personne com a instrução:
Quando o compilador encontra no código-fonte uma referência a uma classe não definida nesse mesmo arquivo-fonte, ele procura a classe em diversos locais:
- nos pacotes importados pelas instruções import
- no diretório a partir do qual o compilador foi iniciado
No nosso exemplo, o compilador foi iniciado a partir do diretório que contém o arquivo personne.class, o que explica por que ele encontrou a definição da classe personne. Incluir, nesse caso, uma instrução import provoca um erro de compilação:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
^
1 error
Para evitar esse erro, mas para lembrar que a classe Person deve ser importada, escreveremos daqui em diante no início do programa:
Agora podemos executar o arquivo test1.class:
É possível agrupar várias classes em um mesmo arquivo-fonte. Vamos, então, agrupar as classes personne e test1 no arquivo-fonte test2.java. A classe test1 é renomeada como test2 para refletir a alteração no nome do arquivo-fonte:
// pacotes importados
import java.io.*;
class personne{
// atributos
private String prenom; // nome da minha pessoa
private String nom; // sobrenome
private int age; // idade dela
// método
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}//inicializa
// método
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}//identifica
}//classe
public class test2{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Observe-se que a classe personne não possui mais o atributo public. De fato, em um arquivo-fonte Java, apenas uma classe pode ter o atributo public. É aquela que possui a função main. Além disso, o arquivo-fonte deve ter o nome dessa última. Vamos compilar o arquivo test2.java:
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>javac test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
10/06/2002 09:41 832 personne.class
10/06/2002 09:41 418 test2.class
Observe que foi gerado um arquivo .class para cada uma das classes presentes no arquivo-fonte. Vamos agora executar o arquivo test2.class:
A partir de agora, utilizaremos indistintamente os dois métodos:
- classes agrupadas em um único arquivo-fonte
- uma classe por arquivo-fonte
3.1.7. Outro método é inicializado
Consideremos novamente a classe personne e adicionemos a ela o seguinte método:
public void initialise(personne P){
prenom=P.prenom;
nom=P.nom;
this.age=P.age;
}
Agora temos dois métodos com o nome initialise: isso é permitido, desde que aceitem parâmetros diferentes. É o que acontece neste caso. O parâmetro agora é uma referência P a uma pessoa. Os atributos da pessoa P são, então, atribuídos ao objeto atual (this). Observe-se que o método initialise tem acesso direto aos atributos do objeto P, embora estes sejam do tipo private. Isso sempre é verdade: os métodos de um objeto O1 de uma classe C sempre têm acesso aos atributos privados dos outros objetos da mesma classe C.
Aqui está um teste da nova classe personne:
// import pessoa;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne();
p2.initialise(p1);
System.out.print("p2=");
p2.identifie();
}
}
e seus resultados:
3.1.8. Construtores da classe Pessoa
Um construtor é um método que leva o nome da classe e é chamado durante a criação do objeto. Geralmente, ele é usado para inicializá-lo. É um método que pode aceitar argumentos, mas não retorna nenhum resultado. Seu protótipo ou sua definição não são precedidos por nenhum tipo (nem mesmo void).
Se uma classe tiver um construtor que aceita n argumentos argi, a declaração e a inicialização de um objeto dessa classe poderão ser feitas da seguinte forma:
classe objet =new classe(arg1,arg2, ... argn);
ou
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
Quando uma classe possui um ou mais construtores, um desses construtores deve ser obrigatoriamente utilizado para criar um objeto dessa classe. Se uma classe C não tiver nenhum construtor, ela possui um construtor padrão, que é o construtor sem parâmetros: public C(). Os atributos do objeto são, então, inicializados com valores padrão. Foi isso que aconteceu quando, nos programas anteriores, escrevemos:
Vamos criar dois construtores para nossa classe personne:
public class personne{
// atributos
private String prenom;
private String nom;
private int age;
// construtores
public personne(String P, String N, int age){
initialise(P,N,age);
}
public personne(personne P){
initialise(P);
}
// método
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public void initialise(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
// método
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Nossos dois construtores limitam-se a chamar os métodos initialise correspondentes. Vale lembrar que, quando em um construtor aparece a notação initialise(P), por exemplo, o compilador a traduz como this.initialise(P). No construtor, o método initialise é, portanto, chamado para trabalhar no objeto referenciado por this, ou seja, o objeto atual, aquele que está sendo construído.
Aqui está um programa de teste:
// import pessoa;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne(p1);
System.out.print("p2=");
p2.identifie();
}
}
e os resultados obtidos:
3.1.9. As referências dos objetos
Sempre utilizamos a mesma classe personne. O programa de teste fica da seguinte forma:
// import pessoa;
import java.io.*;
public class test1{
public static void main(String arg[]){
// p1
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1="); p1.identifie();
// p2 faz referência ao mesmo objeto que p1
personne p2=p1;
System.out.print("p2="); p2.identifie();
// p3 aponta para um objeto que será uma cópia do objeto ao qual p1 aponta
personne p3=new personne(p1);
System.out.print("p3="); p3.identifie();
// altera-se o estado do objeto referenciado por p1
p1.initialise("Micheline","Benoît",67);
System.out.print("p1="); p1.identifie();
// como p2 = p1, o objeto referenciado por p2 deve ter mudado de estado
System.out.print("p2="); p2.identifie();
// como p3 não aponta para o mesmo objeto que p1, o objeto ao qual p3 aponta não deve ter mudado
System.out.print("p3="); p3.identifie();
}
}
Os resultados obtidos são os seguintes:
p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30
Ao declarar a variável p1 por meio de
p1 faz referência ao objeto personne("Jean","Dupont",30), mas não é o próprio objeto. Em C, diríamos que se trata de um ponteiro, c.a.d, que aponta para o endereço do objeto criado. Se, em seguida, escrevermos:
Não é o objeto personne("Jean","Dupont",30) que é modificado, e sim a referência p1 que muda de valor. O objeto pessoa("Jean", "Dupont", 30) será “perdido” se não for referenciado por nenhuma outra variável.
Quando se escreve:
inicializa-se o ponteiro p2: ele “aponta” para o mesmo objeto (designa o mesmo objeto) que o ponteiro p1. Assim, se modificarmos o objeto “apontado” (ou referenciado) por p1, modificamos aquele referenciado por p2.
Quando se escreve:
ocorre a criação de um novo objeto, que é uma cópia do objeto referenciado por p1. Esse novo objeto será referenciado por p3. Se modificarmos o objeto “apontado” (ou referenciado) por p1, não alteraremos em nada aquele referenciado por p3. É isso que mostram os resultados obtidos.
3.1.10. Os objetos temporários
Em uma expressão, é possível chamar explicitamente o construtor de um objeto: ele é criado, mas não temos acesso a ele (para modificá-lo, por exemplo). Esse objeto temporário é criado para fins de avaliação da expressão e, em seguida, descartado. O espaço de memória que ele ocupava será automaticamente recuperado posteriormente por um programa chamado “garimpo de lixo”, cuja função é recuperar o espaço de memória ocupado por objetos que não são mais referenciados pelos dados do programa.
Consideremos o seguinte exemplo:
// import pessoa;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
e vamos modificar os construtores da classe personne para que exibam uma mensagem:
// fabricantes
public personne(String P, String N, int age){
System.out.println("Constructeur personne(String, String, int)");
initialise(P,N,age);
}
public personne(personne P){
System.out.println("Constructeur personne(personne)");
initialise(P);
}
Obtemos os seguintes resultados:
mostrando a construção sucessiva dos dois objetos temporários.
3.1.11. Métodos de leitura e gravação de atributos privados
Adicionamos à classe personne os métodos necessários para ler ou modificar o estado dos atributos dos objetos:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
// acessórios
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modificadores
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Estamos testando a nova classe com o seguinte programa:
// importar pessoa;
public class test1{
public static void main(String[] arg){
personne P=new personne("Jean","Michelin",34);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
P.setAge(56);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
}
}
e obtemos os seguintes resultados:
3.1.12. Métodos e atributos de classe
Suponhamos que queiramos contar o número de objetos personne criados em um aplicativo. Podemos gerenciar um contador manualmente, mas corremos o risco de esquecer os objetos temporários que são criados aqui e ali. Parece mais seguro incluir nos construtores da classe personne uma instrução que incremente um contador. O problema é passar uma referência desse contador para que o construtor possa incrementá-lo: é preciso passar um novo parâmetro a eles. Também é possível incluir o contador na definição da classe. Como se trata de um atributo da própria classe e não de um objeto específico dessa classe, ele é declarado de maneira diferente com a palavra-chave static:
Para referenciá-lo, escreve-se personne.nbPersonnes para indicar que se trata de um atributo da própria classe personne. Aqui, criamos um atributo privado ao qual não teremos acesso direto fora da classe. Portanto, criamos um método público para dar acesso ao atributo da classe nbPersonnes. Para definir o valor de nbPersonnes, o método não precisa de um objeto específico: de fato, nbPersonnes não é um atributo de um objeto específico, mas sim um atributo de toda a classe. Portanto, é necessário um método de classe também declarado como static:
que, externamente, será chamado com a sintaxe personne.getNbPersonnes(). Veja um exemplo.
A classe personne fica da seguinte forma:
public class personne{
// atributo de classe
private static long nbPersonnes=0;
// atributos de objetos
…
// construtores
public personne(String P, String N, int age){
initialise(P,N,age);
nbPersonnes++;
}
public personne(personne P){
initialise(P);
nbPersonnes++;
}
// método
…
// método de classe
public static long getNbPersonnes(){
return nbPersonnes;
}
}// classe
Com o seguinte programa:
// import pessoa;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
personne p2=new personne(p1);
new personne(p1);
System.out.println("Nombre de personnes créées : "+personne.getNbPersonnes());
}// main
}//test1
obtêm-se os seguintes resultados:
3.1.13. Passagem de um objeto para uma função
Já mencionamos que o Java passa os parâmetros efetivos de uma função por valor: os valores dos parâmetros efetivos são copiados para os parâmetros formais. Portanto, uma função não pode modificar os parâmetros efetivos.
No caso de um objeto, não se deve se deixar enganar pelo uso incorreto da linguagem, que ocorre sistematicamente quando se fala de “objeto” em vez de “referência de objeto”. Um objeto só é manipulado por meio de uma referência (um ponteiro) a ele. O que é, portanto, passado para uma função não é o próprio objeto, mas uma referência a esse objeto. É, portanto, o valor da referência — e não o valor do próprio objeto — que é copiado no parâmetro formal: não há criação de um novo objeto.
Se uma referência de objeto R1 for passada para uma função, ela será copiada para o parâmetro formal correspondente R2. Portanto, as referências R2 e R1 apontam para o mesmo objeto. Se a função modificar o objeto apontado por R2, ela obviamente modificará aquele referenciado por R1, uma vez que se trata do mesmo objeto.

É o que mostra o exemplo a seguir:
// import pessoa;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("Paramètre effectif avant modification : ");
p1.identifie();
modifie(p1);
System.out.print("Paramètre effectif après modification : ");
p1.identifie();
}// main
private static void modifie(personne P){
System.out.print("Paramètre formel avant modification : ");
P.identifie();
P.initialise("Sylvie","Vartan",52);
System.out.print("Paramètre formel après modification : ");
P.identifie();
}// modifica
}// classe
O método modifie é declarado como static porque é um método de classe: não é necessário prefixá-lo com um objeto para chamá-lo. Os resultados obtidos são os seguintes:
Constructeur personne(String, String, int)
Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52
Percebe-se que há apenas a criação de um objeto: o da pessoa p1 pela função main e que o objeto foi efetivamente modificado pela função modifie.
3.1.14. Encapsular os parâmetros de saída de uma função em um objeto
Devido à passagem de parâmetros por valor, não é possível escrever em Java uma função que tenha parâmetros de saída do tipo int, por exemplo, pois não é possível passar a referência de um tipo int, que não é um objeto. Portanto, é possível criar uma classe que encapsule o tipo int:
public class entieres{
private int valeur;
public entieres(int valeur){
this.valeur=valeur;
}
public void setValue(int valeur){
this.valeur=valeur;
}
public int getValue(){
return valeur;
}
}
A classe anterior possui um construtor que permite inicializar um inteiro e dois métodos que permitem ler e modificar o valor desse inteiro. Testamos essa classe com o seguinte programa:
// import inteiros;
public class test2{
public static void main(String[] arg){
entieres I=new entieres(12);
System.out.println("I="+I.getValue());
change(I);
System.out.println("I="+I.getValue());
}
private static void change(entieres entier){
entier.setValue(15);
}
}
e obtemos os seguintes resultados:
3.1.15. Uma matriz de pessoas
Um objeto é um dado como qualquer outro e, por isso, vários objetos podem ser agrupados em uma tabela:
// import pessoa;
public class test1{
public static void main(String arg[]){
personne[] amis=new personne[3];
System.out.println("----------------");
amis[0]=new personne("Jean","Dupont",30);
amis[1]=new personne("Sylvie","Vartan",52);
amis[2]=new personne("Neil","Armstrong",66);
int i;
for(i=0;i<amis.length;i++)
amis[i].identifie();
}
}
A instrução pessoa[] amigos = new pessoa[3]; cria um array com 3 elementos do tipo personne. Esses três elementos são inicializados aqui com os valores null e c.a.d, pois não referenciam nenhum objeto. Mais uma vez, por convenção, fala-se em “matriz de objetos”, embora se trate apenas de uma matriz de referências a objetos. A criação da matriz de objetos — matriz que é ela própria um objeto (presença de new) — não cria, portanto, por si só, nenhum objeto do tipo de seus elementos: isso deve ser feito posteriormente.
Obtêm-se os seguintes resultados:
----------------
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66
3.2. O legado pelo exemplo
3.2.1. Introdução
Abordamos aqui o conceito de herança. O objetivo da herança é “personalizar” uma classe existente para que ela atenda às nossas necessidades. Suponhamos que queiramos criar uma classe enseignant: um professor é uma pessoa específica. Ele possui atributos que outra pessoa não terá: a disciplina que leciona, por exemplo. Mas também possui os atributos de qualquer pessoa: nome, sobrenome e idade. Um professor, portanto, faz parte integralmente da classe personne, mas possui atributos adicionais. Em vez de criar uma classe enseignant do zero, seria preferível aproveitar o que já foi definido na classe personne e adaptá-la às características específicas dos professores. É o conceito de herança que nos permite fazer isso.
Para expressar que a classe enseignant herda as propriedades da classe personne, escreveremos:
public class enseignant extends personne
personne é chamada de classe pai (ou mãe) e enseignant, de classe derivada (ou filha). Um objeto enseignant possui todas as características de um objeto personne: ele tem os mesmos atributos e os mesmos métodos. Esses atributos e métodos da classe pai não são repetidos na definição da classe filha: basta indicar os atributos e métodos adicionados pela classe filha:
class enseignant extends personne{
// atributos
private int section;
// construtor
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
}
Supomos que a classe personne esteja definida da seguinte forma:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public String identite(){
return "personne("+prenom+","+nom+","+age+")";
}
// acessores
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modificadores
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
O método identifie foi ligeiramente modificado para retornar uma sequência de caracteres que identifica a pessoa e agora se chama identite. Aqui, a classe enseignant acrescenta aos métodos e atributos da classe personne:
- um atributo section, que é o número da seção à qual o professor pertence no corpo docente (basicamente, uma seção por disciplina)
- um novo construtor que permite inicializar todos os atributos de um professor
3.2.2. Criação de um objeto professor
O construtor da classe enseignant é o seguinte:
// construtor
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
A instrução super(P,N,age) é uma chamada ao construtor da classe pai, neste caso, a classe personne. Sabemos que esse construtor inicializa os campos prenome, sobrenome e age do objeto personne contido dentro do objeto étudiant. Isso parece bem complicado e talvez fosse melhor escrever:
// construtor
public enseignant(String P, String N, int age,int section){
this.prenom=P;
this.nom=N
this.age=age
this.section=section;
}
Isso é impossível. A classe personne declarou como privados (private) seus três campos prenom, nom e age. Apenas objetos da mesma classe têm acesso direto a esses campos. Todos os outros objetos, incluindo objetos derivados como neste caso, devem utilizar métodos públicos para ter acesso a eles. A situação teria sido diferente se a classe personne tivesse declarado os três campos como protegidos (protected): nesse caso, ela permitiria que classes derivadas tivessem acesso direto aos três campos. Em nosso exemplo, usar o construtor da classe pai era, portanto, a solução correta e é o método habitual: ao construir um objeto filho, primeiro chama-se o construtor do objeto pai e, em seguida, completam-se as inicializações específicas do objeto filho (section em nosso exemplo).
Vamos tentar um primeiro programa:
// importar pessoa;
// importar professor;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Este programa se limita a criar um objeto enseignant (novo) e a identificá-lo. A classe enseignant não possui o método identité, mas sua classe pai possui um, que, além disso, é público: por herança, ele se torna um método público da classe enseignant.
Os arquivos-fonte das classes são reunidos em um mesmo diretório e, em seguida, compilados:
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
E:\data\serge\JAVA\BASES\OBJETS\4>javac *.java
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
10/06/2002 10:02 316 enseignant.class
10/06/2002 10:02 1 146 personne.class
10/06/2002 10:02 550 test1.class
O arquivo test1.class é executado:
3.2.3. Sobrecarga de um método
No exemplo anterior, obtivemos a identidade da parte personne do professor, mas faltam algumas informações específicas da turma enseignant (a seção). Portanto, precisamos escrever um método que permita identificar o professor:
class enseignant extends personne{
int section;
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
public String identite(){
return "enseignant("+super.identite()+","+section+")";
}
}
O método identite da classe enseignant baseia-se no método identite de sua classe pai (super.identite) para exibir sua parte “personne” e, em seguida, complementa com o campo section, que é específico da classe enseignant.
A classe enseignant dispõe agora de dois métodos identite:
- aquele herdado da classe pai personne
- e o seu próprio
Se E for um objeto enseignant, E.identite designa o método identite da classe enseignant. Diz-se que o método identite da classe pai é “sobrecarregado” pelo método identite da classe filha. De modo geral, se O for um objeto e M um método, para executar o método O.M, o sistema procura um método M na seguinte ordem:
- na classe do objeto O
- na sua classe pai, se houver uma
- na classe-mãe da sua classe-mãe, se ela existir
- etc…
A herança permite, portanto, sobrecarregar na classe filha métodos com o mesmo nome que existem na classe pai. É isso que permite adaptar a classe filha às suas próprias necessidades. Associada ao polimorfismo, que veremos um pouco mais adiante, a sobrecarga de métodos é o principal benefício da herança.
Consideremos o mesmo exemplo de antes:
// importar pessoa;
// importar professor;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Os resultados obtidos desta vez são os seguintes:
3.2.4. O polimorfismo
Consideremos uma hierarquia de classes: C0 C1 C2 … Cn
onde Ci Cj indica que a classe Cj é derivada da classe Ci. Isso implica que a classe Cj possui todas as características da classe Ci, além de outras. Sejam Oi objetos do tipo Ci. É válido escrever:
De fato, por herança, a classe Cj possui todas as características da classe Ci, além de outras. Portanto, um objeto Oj do tipo Cj contém em si um objeto do tipo Ci. A operação
faz com que Oi seja uma referência ao objeto do tipo Ci contido no objeto Oj.
O fato de queuma variável Oi da classe Ci possa, na verdade, referenciar não apenas um objeto da classe Ci, mas qualquer objeto derivado da classe Ci é chamado de polimorfismo: a capacidade de uma variável referenciar diferentes tipos de objetos.
Vamos dar um exemplo e considerar a seguinte função, independente de qualquer classe:
A classe Object é a “classe-pai” de todas as classes Java. Assim, quando escrevemos:
estamos escrevendo implicitamente:
Portanto, todo objeto Java contém em sua estrutura uma parte do tipo Object. Assim, poderemos escrever:
O parâmetro formal do tipo Object da função affiche receberá um valor do tipo enseignant. Como enseignant deriva de *Object*, isso é válido.
3.2.5. Sobrecarga e polimorfismo
Vamos completar nossa função affiche:
O método obj.toString() retorna uma sequência de caracteres que identifica o objeto obj no formato nom_de_la_classe@adresse_de_l'objeto. O que acontece no caso do nosso exemplo anterior:
O sistema deverá executar a instrução System.out.println(e.toString()), em que e é um objeto professor. Ele procura um método toString na hierarquia de classes que conduz à classe enseignant, começando pela última:
- na classe enseignant, ele não encontra o método toString()
- na classe pai personne, ele não encontra o método toString()
- na classe pai Object, ele encontra o método toString() e o executa
É isso que mostra o programa a seguir:
// importar pessoa;
// importar professor;
public class test1{
public static void main(String arg[]){
enseignant e=new enseignant("Lucile","Dumas",56,61);
affiche(e);
personne p=new personne("Jean","Dupont",30);
affiche(p);
}
public static void affiche(Object obj){
System.out.println(obj.toString());
}
}
Os resultados obtidos são os seguintes:
Ou seja, o objeto nom\_de\_la\_classe@adresse\_de\_l. Como isso não é muito explícito, ficamos tentados a definir um método toString para as classes personne e etudiant, que sobrecarregariam o método toString da classe pai Object. Em vez de escrever métodos que seriam semelhantes aos métodos identite já existentes nas classes personne e enseignant, basta renomear esses métodos identite para toString:
public class personne{
...
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
...
}
class enseignant extends personne{
int section;
…
public String toString(){
return "enseignant("+super.toString()+","+section+")";
}
}
Com o mesmo programa de teste de antes, os resultados obtidos são os seguintes:
3.3. Classes internas
Uma classe pode conter a definição de outra classe. Consideremos o exemplo a seguir:
// classes importadas
import java.io.*;
public class test1{
// classe interna
private class article{
// definimos a estrutura
private String code;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
// construtor
public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
// inicialização dos atributos
this.code=code;
this.nom=nom;
this.prix=prix;
this.stockActuel=stockActuel;
this.stockMinimum=stockMinimum;
}//construtor
//toString
public String toString(){
return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
}//toString
}//classe de item
// dados locais
private article art=null;
// fabricante
public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
// definição de atributo
art=new article(code, nom, prix, stockActuel,stockMinimum);
}//teste1
// acessador
public article getArticle(){
return art;
}//getArticle
public static void main(String arg[]){
// criação de uma instância test1
test1 t1=new test1("a100","velo",1000,10,5);
// exibição test1.art
System.out.println("art="+t1.getArticle());
}//main
}// fim da classe
A classe test1 contém a definição de outra classe, a classe article. Diz-se que article é uma classe interna da classe test1. Isso pode ser útil quando a classe interna só tem utilidade na classe que a contém. Ao compilar o código-fonte test1.java acima, obtêm-se dois arquivos .class:
E:\data\serge\JAVA\classes\interne>dir
05/06/2002 17:26 1 362 test1.java
05/06/2002 17:26 941 test1$article.class
05/06/2002 17:26 1 020 test1.class
Um arquivo test1$article.class foi gerado para a classe article, interna à classe test1. Ao executar o programa acima, obtêm-se os seguintes resultados:
3.4. As interfaces
Uma interface é um conjunto de protótipos de métodos ou propriedades que forma um contrato. Uma classe que decide implementar uma interface se compromete a fornecer uma implementação de todos os métodos definidos na interface. É o compilador que verifica essa implementação.
Veja, por exemplo, a definição da interface java.util.Enumeration:
Resumo dos métodos | ||
boolean | hasMoreElements() Verifica se esta enumeração contém mais elementos. | |
Object | nextElement() Retorna o próximo elemento desta enumeração, caso este objeto de enumeração tenha pelo menos mais um elemento a fornecer. | |
Qualquer classe que implemente esta interface será declarada como
Os métodos hasMoreElements() e nextElement() deverão ser definidos na classe C.
Consideremos o código a seguir, que define uma classe élève para registrar o nome de um aluno e sua nota em uma disciplina:
// uma classe aluno
public class élève{
// atributos públicos
public String nom;
public double note;
// construtor
public élève(String NOM, double NOTE){
nom=NOM;
note=NOTE;
}//construtor
}//aluno
Definimos uma classe notes que reúne as notas de todos os alunos em uma disciplina:
// classes importadas
// importação de aluno
// classe de notas
public class notes{
// atributos
protected String matière;
protected élève[] élèves;
// construtor
public notes (String MATIERE, élève[] ELEVES){
// armazenamento de alunos e disciplinas
matière=MATIERE;
élèves=ELEVES;
}//notas
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// concatenamos todas as notas
for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
};
//última nota
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
valeur+=")";
// fim
return valeur;
}//toString
}//classe
Os atributos matière e élèves são declarados como protected para que possam ser acessados por uma classe derivada. Decidimos derivar a classe notes em uma classe notesStats, que teria dois atributos adicionais: a média e o desvio-padrão das notas:
public class notesStats extends notes implements Istats {
// atributos
private double _moyenne;
private double _écartType;
A classe notesStats deriva da classe notes e implementa a seguinte interface Istats:
// uma interface
public interface Istats{
double moyenne();
double écartType();
}//
Isso significa que a classe notesStats deve ter dois métodos chamados moyenne e écartType com a assinatura indicada na interface Istats. A classe notesStats é a seguinte:
// classes importadas
// importar notas;
// import Istats;
// import aluno;
public class notesStats extends notes implements Istats {
// atributos
private double _moyenne;
private double _écartType;
// construtor
public notesStats (String MATIERE, élève[] ELEVES){
// construção da classe pai
super(MATIERE,ELEVES);
// cálculo da média das notas
double somme=0;
for (int i=0;i<élèves.length;i++){
somme+=élèves[i].note;
}
if(élèves.length!=0) _moyenne=somme/élèves.length;
else _moyenne=-1;
// desvio-padrão
double carrés=0;
for (int i=0;i<élèves.length;i++){
carrés+=Math.pow((élèves[i].note-_moyenne),2);
}//para
if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
else _écartType=-1;
}//construtor
// ToString
public String toString(){
return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
}//ToString
// métodos da interface Istats
public double moyenne(){
// calcula a média das notas
return _moyenne;
}//média
public double écartType(){
// retorna o desvio-padrão
return _écartType;
}//écartType
}//classe
A média _moyenne e o desvio-padrão _ecartType são calculados assim que o objeto é criado. Portanto, os métodos moyenne e écartType precisam apenas retornar o valor dos atributos _moyenne e _ecartType. Ambos os métodos retornam -1 se a tabela de alunos estiver vazia.
A seguinte classe de teste:
// classes importadas
// importação de aluno;
// importação de Istats;
// importar notas;
// importar notesStats;
// turma de teste
public class test{
public static void main(String[] args){
// alguns alunos e notas
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
// que são gravadas em um objeto de notas
notes anglais=new notes("anglais",ELEVES);
// e que são exibidas
System.out.println(""+anglais);
// o mesmo com média e desvio-padrão
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//main
}//turma
resulta em:
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écart-type=1.632993161855452
Todas as diferentes classes deste exemplo estão em arquivos-fonte distintos:
E:\data\serge\JAVA\interfaces\notes>dir
06/06/2002 14:06 707 notes.java
06/06/2002 14:06 878 notes.class
06/06/2002 14:07 1 160 notesStats.java
06/06/2002 14:02 101 Istats.java
06/06/2002 14:02 138 Istats.class
06/06/2002 14:05 247 élève.java
06/06/2002 14:05 309 élève.class
06/06/2002 14:07 1 103 notesStats.class
06/06/2002 14:10 597 test.java
06/06/2002 14:10 931 test.class
A classe notesStats poderia muito bem ter implementado os métodos moyenne e écartType por conta própria, sem indicar que implementava a interface Istats. Qual é, então, a utilidade das interfaces? É a seguinte: uma função pode aceitar como parâmetro um dado do tipo de uma interface I. Qualquer objeto de uma classe C que implemente a interface I poderá, então, ser um parâmetro dessa função. Consideremos a seguinte interface:
// uma interface Iexample
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}//interface
A interface Iexemple define dois métodos: ajouter e soustraire. As seguintes classes, classe1 e classe2, implementam essa interface.
// classes importadas
// import Iexemplo;
public class classe1 implements Iexemple{
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
}//classe
// classes importadas
// import Iexemplo;
public class classe2 implements Iexemple{
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
}//classe
Para simplificar o exemplo, as classes não fazem nada além de implementar a interface Iexemple. Agora, consideremos o seguinte exemplo:
// classes importadas
// import classe1;
// importar classe2;
// classe de teste
public class test{
// uma função estática
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calcular
// a função main
public static void main(String[] arg){
// criação de dois objetos classe1 e classe2
classe1 c1=new classe1();
classe2 c2=new classe2();
// chamadas à função estática calcular
calculer(4,3,c1);
calculer(14,13,c2);
}//main
}//classe test
A função estática calculer aceita como parâmetro um elemento do tipo Iexemple. Portanto, ela poderá receber para esse parâmetro tanto um objeto do tipo classe1 quanto do tipo classe2. É isso que é feito na função main, com os seguintes resultados:
Vemos, portanto, que temos aqui uma propriedade semelhante ao polimorfismo observado nas classes. Assim, se um conjunto de classes Ci não ligadas entre si por herança (portanto, não é possível utilizar o polimorfismo de herança) apresentar um conjunto de métodos com a mesma assinatura, pode ser interessante agrupar esses métodos em uma interface I da qual todas as classes envolvidas herdariam. Instâncias dessas classes Ci podem, então, ser utilizadas como parâmetros de funções que aceitam um parâmetro do tipo I, c.a.d. Essas funções utilizam apenas os métodos dos objetos Ci definidos na interface I e não os atributos e métodos específicos das diferentes classes Ci.
No exemplo anterior, cada classe ou interface era objeto de um arquivo-fonte separado:
E:\data\serge\JAVA\interfaces\opérations>dir
06/06/2002 14:33 128 Iexemple.java
06/06/2002 14:34 218 classe1.java
06/06/2002 14:32 220 classe2.java
06/06/2002 14:33 144 Iexemple.class
06/06/2002 14:34 325 classe1.class
06/06/2002 14:34 326 classe2.class
06/06/2002 14:36 583 test.java
06/06/2002 14:36 628 test.class
Por fim, vale ressaltar que a herança de interfaces pode ser múltipla, c.a.d. O que pode ser escrito como
onde ij são interfaces.
3.5. Classes anônimas
No exemplo anterior, as classes classe1 e classe2 poderiam não ter sido definidas explicitamente. Consideremos o seguinte programa, que faz basicamente a mesma coisa que o anterior, mas sem a definição explícita das classes classe1 e classe2:
// classes importadas
// import Iexample;
// classe de teste
public class test2{
// uma classe interna
private static class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definição da classe3
// uma função estática
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calcular
// a função main
public static void main(String[] arg){
// criação de dois objetos que implementam a interface Iexample
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definição de i1
Iexemple i2=new Iexemple(){
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
};//definição de i2
// outro objeto Iexemple
Iexemple i3=new classe3();
// chamadas à função estática `calcular`
calculer(4,3,i1);
calculer(14,13,i2);
calculer(24,23,i3);
}//main
}//classe test
A particularidade está no código:
// criação de dois objetos que implementam a interface Iexample
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definição i1
Cria-se um objeto i1 cuja única função é implementar a interface Iexemple. Esse objeto é do tipo Iexemple. Portanto, é possível criar objetos do tipo interface. Inúmeros métodos de classes Java retornam objetos do tipo interface c.a.d — objetos cuja única função é implementar os métodos de uma interface. Para criar o objeto i1, poderíamos ficar tentados a escrever:
Iexemple i1=new Iexemple()
No entanto, uma interface não pode ser instanciada. Apenas uma classe que implemente essa interface pode ser instanciada. Aqui, define-se essa classe “na hora” no próprio corpo da definição do objeto i1:
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
// definição de “adicionar”
}
public int soustraire(int a, int b){
// definição de subtrair
}
};//definição de i1
O significado dessa instrução é análogo à sequência:
public class test2{
................
// uma classe interna
private static class classe1 implements Iexemple{
public int ajouter(int a, int b){
// definição de somar
}
public int soustraire(int a, int b){
// definição de subtrair
}
};//definição da classe1
.................
public static void main(String[] arg){
...........
Iexemple i1=new classe1();
}//main
}//classe
No exemplo acima, instanciamos uma classe e não uma interface. Uma classe definida “na hora” é chamada de classe anônima. Esse é um método frequentemente utilizado para instanciar objetos cuja única função é implementar uma interface.
A execução do programa anterior produz os seguintes resultados:
O exemplo anterior utilizava classes anônimas para implementar uma interface. Elas também podem ser usadas para derivar classes que não possuem construtores com parâmetros. Consideremos o exemplo a seguir:
// classes importadas
// import Iexample;
class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definição da classe3
public class test4{
// uma função estática
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calcular
// método main
public static void main(String args[]){
// definição de uma classe anônima derivada de classe3
// para redefinir a função subtrair
classe3 i1=new classe3(){
public int ajouter(int a, int b){
return a+b+10000;
}//subtrair
};//i1
// chamadas à função estática calcular
calculer(4,3,i1);
}//main
}//classe
Encontramos aqui uma classe classe3 que implementa a interface Iexemple. Na função main, definimos uma variável i1 cujo tipo é uma classe derivada de classe3. Essa classe derivada é definida “na hora” em uma classe anônima e redefine o método ajouter da classe classe3. A sintaxe é idêntica à da classe anônima que implementa uma interface. Só que, neste caso, o compilador detecta que classe3 não é uma interface, mas uma classe. Para ele, trata-se, portanto, de uma derivação de classe. Todos os métodos que ele encontrar no corpo da classe anônima substituirão os métodos com o mesmo nome da classe base.
A execução do programa anterior produz os seguintes resultados:
3.6. Os pacotes
3.6.1. Criar classes em um pacote
Para escrever uma linha na tela, usamos a instrução
Se analisarmos a definição da classe System, descobrimos que, na verdade, ela se chama java.lang.System:

Vamos verificar isso com um exemplo:
public class test1{
public static void main(String[] args){
java.lang.System.out.println("Coucou");
}//main
}//classe
Vamos compilar e executar este programa:
E:\data\serge\JAVA\classes\paquetages>javac test1.java
E:\data\serge\JAVA\classes\paquetages>dir
06/06/2002 15:40 127 test1.java
06/06/2002 15:40 410 test1.class
E:\data\serge\JAVA\classes\paquetages>java test1
Coucou
Por que, então, podemos escrever
System.out.println("Coucou");
em vez de
java.lang.System.out.println("Coucou");
Isso porque, implicitamente, em todo programa Java, há uma importação sistemática do “pacote” java.lang. Assim, tudo ocorre como se, no início de todo programa, houvesse a instrução:
O que significa essa instrução? Ela dá acesso a todas as classes do pacote java.lang. O compilador encontrará nele o arquivo System.class, que define a classe System. Ainda não sabemos onde o compilador encontrará o pacote java.lang nem como é a estrutura de um pacote. Voltaremos a esse assunto. Para criar uma classe em um pacote, escreve-se:
Para o exemplo, vamos criar em um pacote nossa classe personne, estudada anteriormente. Escolheremos istia.st como nome do pacote. A classe personne passa a ser:
// nome do pacote no qual a classe pessoa será criada
package istia.st;
// classe pessoa
public class personne{
// nome, sobrenome, idade
private String prenom;
private String nom;
private int age;
// construtor 1
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// toString
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
}//classe
Essa classe é compilada e, em seguida, colocada no diretório istia\st do diretório atual. Por que istia\st? Porque o pacote se chama istia.st.
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:04 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Agora, vamos utilizar a classe personne em uma primeira classe de teste:
public class test{
public static void main(String[] args){
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//mão
}//classe de teste
Observe-se que a classe personne agora é precedida pelo nome de seu pacote, istia.st. Onde o compilador encontrará a classe istia.st.personne? O compilador procura as classes de que precisa em uma lista predefinida de diretórios e em uma árvore de diretórios a partir do diretório atual. Neste caso, ele procurará a classe istia.st.personne no arquivo istia\st\personne.class. É por isso que colocamos o arquivo personne.class no diretório istia\st. Vamos compilar e, em seguida, executar o programa de teste:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
E:\data\serge\JAVA\classes\paquetages\personne>java test
p1=personne(Jean,Dupont,20)
Para evitar escrever
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
É possível importar a classe istia.st.personne com uma cláusula import:
import istia.st.personne;
Assim, podemos escrever
personne p1=new personne("Jean","Dupont",20);
e o compilador traduzirá como
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
O programa de teste fica, então, assim:
// espaços de nomes importados
import istia.st.personne;
public class test2{
public static void main(String[] args){
personne p1=new personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//main
}//classe test2
Vamos compilar e executar este novo programa:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>java test2
p1=personne(Jean,Dupont,20)
Colocamos o pacote istia.st no diretório atual. Isso não é obrigatório. Vamos colocá-lo em uma pasta chamada mesClasses, ainda no diretório atual. Vale lembrar que as classes do pacote istia.st estão localizadas em uma pasta chamada istia\st. A estrutura de diretórios do diretório atual é a seguinte:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses
06/06/2002 16:22 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia
06/06/2002 16:22 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia\st
06/06/2002 16:01 1 153 personne.class
Agora vamos compilar novamente o programa test2.java:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
test2.java:2: package istia.st does not exist
import istia.st.personne;
O compilador não consegue mais encontrar o pacote istia.st desde que ele foi movido. Observe que ele o procura por causa da instrução import. Por padrão, ele o procura a partir do diretório atual, em uma pasta chamada istia\st, que não existe mais. Vamos examinar as opções do compilador:
E:\data\serge\JAVA\classes\paquetages\personne>javac
Usage: javac <options> <source files>
where possible options include:
-g Generate all debugging info
-g:none Generate no debugging info
-g:{lines,vars,source} Generate only some debugging info
-O Optimize; may hinder debugging or enlarge class file
-nowarn Generate no warnings
-verbose Output messages about what the compiler is doing
-deprecation Output source locations where deprecated APIs are used
-classpath <path> Specify where to find user class files
-sourcepath <path> Specify where to find input source files
-bootclasspath <path> Override location of bootstrap class files
-extdirs <dirs> Override location of installed extensions
-d <directory> Specify where to place generated class files
-encoding <encoding> Specify character encoding used by source files
-source <release> Provide source compatibility with specified release
-target <release> Generate class files for specific VM version
-help Print a synopsis of standard options
Aqui, a opção -classpath pode ser útil. Ela permite indicar ao compilador onde procurar suas classes e pacotes. Vamos tentar. Vamos compilar informando ao compilador que o pacote istia.st agora está na pasta mesClasses:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath mesClasses test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:47 236 test2.java
06/06/2002 17:03 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
Desta vez, a compilação ocorre sem problemas. Vamos executar o programa test2.class:
E:\data\serge\JAVA\classes\paquetages\personne>java test2
Exception in thread "main" java.lang.NoClassDefFoundError: istia/st/personne
at test2.main(test2.java:6)
Agora é a vez da máquina virtual Java não encontrar a classe istia/st/personne. Ela procura a classe no diretório atual, mas agora ela está no diretório mesClasses. Vamos verificar as opções da máquina virtual Java:
E:\data\serge\JAVA\classes\paquetages\personne>java
Usage: java [-options] class [args...]
(to execute a class)
or java -jar [-options] jarfile [args...]
(to execute a jar file)
where options include:
-client to select the "client" VM
-server to select the "server" VM
-hotspot is a synonym for the "client" VM [deprecated]
The default VM is client.
-cp -classpath <directories and zip/jar files separated by ;>
set search path for application classes and resources
-D<name>=<value>
set a system property
-verbose[:class|gc|jni]
enable verbose output
-version print product version and exit
-showversion print product version and continue
-? -help print this help message
-X print help on non-standard options
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
enable assertions
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable assertions
-esa | -enablesystemassertions
enable system assertions
-dsa | -disablesystemassertions
disable system assertions
Vemos que o JVM também possui uma opção classpath, assim como o compilador. Vamos usá-la para indicar onde está localizado o pacote istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2
Não avançamos muito. Agora é a própria classe test2 que não está sendo encontrada. Pelo seguinte motivo: na ausência da palavra-chave classpath, o diretório atual é sistematicamente vasculhado durante a busca por classes, mas isso não ocorre quando ela está presente. Consequentemente, a classe test2.class, que se encontra no diretório atual, não é encontrada. A solução? Adicionar o diretório atual ao classpath. O diretório atual é representado pelo símbolo .
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)
Por que todas essas complicações? O objetivo dos pacotes é evitar conflitos de nomes entre classes. Consideremos duas empresas, E1 e E2, que distribuem classes empacotadas, respectivamente, nos pacotes com.e1 e com.e2. Suponhamos um cliente C que adquira esses dois conjuntos de classes, nos quais ambas as empresas definiram uma classe personne. O cliente C fará referência à classe personne daempresa E1 como com.e1.personne e a da empresa E2 como com.e2.personne, evitando assim um conflito de nomes.
3.6.2. Pesquisa de pacotes
Quando escrevemos em um programa
para ter acesso a todas as classes do pacote java.util, onde ele é encontrado? Já mencionamos que os pacotes são procurados, por padrão, no diretório atual ou na lista de diretórios declarados na opção classpath do compilador ou na JVM, caso essa opção esteja presente. Eles também são procurados nos diretórios lib do diretório de instalação do JDK. Consideremos este diretório:

Neste exemplo, as árvores de diretórios jdk14\lib e jdk14\jre\lib serão exploradas para localizar arquivos .class, .jar ou .zip, que são arquivos compactados de classes. Vamos, por exemplo, fazer uma busca pelos arquivos .jar localizados no diretório jdk14 anterior:

Existem várias dezenas deles. Um arquivo .jar pode ser aberto com o utilitário winzip. Vamos abrir o arquivo rt.jar acima (rt=RunTime). Encontramos várias centenas de arquivos .class, incluindo aqueles que pertencem ao pacote java.util:

Uma maneira simples de gerenciar os pacotes é colocá-los no diretório <jdk>\jre\lib, onde <jdk> é o diretório de instalação do JDK. Geralmente, um pacote contém várias classes, e é prático agrupá-las em um único arquivo .jar (JAR = arquivo Java ARchive). O executável jar.exe está localizado na pasta <jdk>\bin:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\bin\jar.exe"
07/02/2002 12:52 28 752 jar.exe
É possível obter ajuda sobre o uso do programa jar executando-o sem parâmetros:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar.exe"
Syntaxe : jar {ctxu}[vfm0M] [fichier-jar] [fichier-manifest] [rÚp -C] fichiers ...
Options :
-c crÚer un nouveau fichier d''archives
-t gÚnÚrer la table des matiÞres du fichier d''archives
-x extraire les fichiers nommÚs (ou tous les fichiers) du fichier d''archives
-u mettre Ó jour le fichier d''archives existant
-v gÚnÚrer des informations verbeuses sur la sortie standard
-f spÚcifier le nom du fichier d''archives
-m inclure les informations manifest provenant du fichier manifest spÚcifiÚ
-0 stocker seulement ; ne pas utiliser la compression ZIP
-M ne pas crÚer de fichier manifest pour les entrÚes
-i gÚnÚrer l''index pour les fichiers jar spÚcifiÚs
-C passer au rÚpertoire spÚcifiÚ et inclure le fichier suivant
Si un rÚpertoire est spÚcifiÚ, il est traitÚ rÚcursivement.
Les noms des fichiers manifest et d''archives doivent Ûtre spÚcifiÚs
dans l''ordre des indicateurs ''m'' et ''f''.
Exemple 1 : pour archiver deux fichiers de classe dans le fichier d''archives classes.jar :
jar cvf classes.jar Foo.class Bar.class
Exemple 2 : utilisez le fichier manifest existant ''monmanifest'' pour archiver tous les fichiers du
rÚpertoire foo/ dans ''classes.jar'':
jar cvfm classes.jar monmanifest -C foo/ .
Voltemos à classe personne.class criada anteriormente no pacote istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Vamos criar um arquivo istia.st.jar que armazene todas as classes do pacote istia.st, ou seja, todas as classes da árvore istia\st acima:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar" cvf istia.st.jar istia\st\*
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
06/06/2002 18:08 874 istia.st.jar
Vamos examinar, com o winzip, o conteúdo do arquivo istia.st.jar:

Vamos colocar o arquivo istia.st.jar no diretório <jdk>\jre\lib\perso:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\jre\lib\perso"
06/06/2002 18:08 874 istia.st.jar
Agora vamos compilar o programa test2.java e, em seguida, executá-lo:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath istia.st.jar test2.java
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath istia.st.jar;. test2
p1=personne(Jean,Dupont,20)
Percebemos que bastou indicar o nome do arquivo a ser pesquisado, sem precisar especificar explicitamente onde ele se encontrava. Todos os diretórios da árvore de diretórios <jdk>\jre\lib são pesquisados para localizar o arquivo .jar solicitado.
3.7. O exemplo IMPÔTS
Retomamos o cálculo do imposto já estudado no capítulo anterior e o processamos utilizando uma classe. Recorde-se o problema:
Consideramos o caso simplificado de um contribuinte que tem apenas seu salário para declarar:
- calcula-se o número de cotas do empregado nbParts = nbEnfants/2 + 1 se ele for solteiro, nbEnfants/2 + 2 se for casado, onde nbEnfants é o número de filhos.
- se tiver pelo menos três filhos, recebe mais meia parcela
- calcula-se sua renda tributável R = 0,72 * S, onde S é seu salário anual
- calcula-se seu coeficiente familiar QF = R / nbParts
- calcula-se seu imposto I. Consideremos a tabela a seguir:
12620,0 | 0 | 0 |
13.190 | 0,05 | 631 |
15.640 | 0,1 | 1.290,5 |
24.740 | 0,15 | 2.072,5 |
31.810 | 0,2 | 3.309,5 |
39.970 | 0,25 | 4.900 |
48.360 | 0,3 | 6.898,5 |
55.790 | 0,35 | 9.316,5 |
92.970 | 0,4 | 12.106 |
127.860 | 0,45 | 16.754,5 |
151.250 | 0,50 | 23.147,5 |
172.040 | 0,55 | 30.710 |
195.000 | 0,60 | 39.312 |
0 | 0,65 | 49.062 |
Cada linha possui 3 campos. Para calcular o imposto I, procura-se a primeira linha em que QF ≤ campo1. Por exemplo, se QF = 23000, será encontrada a linha
O imposto I é, então, igual a 0,15*R - 2072,5*nbParts. Se QF for tal que a relação QF <= campo1 nunca for verificada, então serão utilizados os coeficientes da última linha. Aqui:
o que resulta no imposto I = 0,65 * R - 49.062 * nbParts.
A classe impots será definida da seguinte forma:
// criação de uma classe de impostos
public class impots{
// os dados necessários para o cálculo do imposto
// provêm de uma fonte externa
private double[] limites, coeffR, coeffN;
// fabricante
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// verifica-se se as três tabelas têm o mesmo tamanho
boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
// tudo certo
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//criador
// cálculo do imposto
public long calculer(boolean marié, int nbEnfants, int salaire){
// cálculo do número de cotas
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// cálculo do rendimento tributável e do quociente familiar
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// cálculo do imposto
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// retorno do resultado
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//calcular
}//classe
É criado um objeto “impostos” com os dados necessários para o cálculo do imposto de um contribuinte. Essa é a parte estável do objeto. Uma vez criado esse objeto, é possível chamar repetidamente seu método “calcular”, que calcula o imposto do contribuinte com base em seu estado civil (casado ou solteiro), no número de filhos e no salário anual.
Um programa de teste poderia ser o seguinte:
//classes importadas
// importação de impostos;
import java.io.*;
public class test
{
public static void main(String[] arg) throws IOException
{
// programa interativo de cálculo de impostos
// o usuário digita três dados no teclado: casado nbEnfants salário
// o programa exibe então o imposto a pagar
final String syntaxe="syntaxe : marié nbEnfants salaire\n"
+"marié : o pour marié, n pour non marié\n"
+"nbEnfants : nombre d'enfants\n"
+"salaire : salaire annuel en F";
// tabelas de dados necessárias para o cálculo do imposto
double[] limites=new double[] {12620,13190,15640,24740,31810,39970,48360,55790,92970,127860,151250,172040,195000,0};
double[] coeffR=new double[] {0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65};
double[] coeffN=new double[] {0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5,12106,16754.5,23147.5,30710,39312,49062};
// criação de um fluxo de leitura
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
// criação de um objeto de imposto
impots objImpôt=null;
try{
objImpôt=new impots(limites,coeffR,coeffN);
}catch (Exception ex){
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
System.exit(1);
}//try-catch
// loop infinito
while(true){
// solicitação dos parâmetros para o cálculo do imposto
System.out.print("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :");
String paramètres=IN.readLine().trim();
// há algo a ser feito?
if(paramètres==null || paramètres.equals("")) break;
// verificação do número de argumentos na linha inserida
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// verificação da validade dos parâmetros
// casado
String marié=args[0].toLowerCase();
if (! marié.equals("o") && ! marié.equals("n")){
System.err.println(syntaxe+"\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants=0;
try{
nbEnfants=Integer.parseInt(args[1]);
if(nbEnfants<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument nbEnfants incorrect : tapez un entier positif ou nul");
continue;
}//if
// salário
int salaire=0;
try{
salaire=Integer.parseInt(args[2]);
if(salaire<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}//if
// os parâmetros estão corretos — calcula-se o imposto
System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
// próximo contribuinte
}//enquanto
}//main
}//classe
Aqui está um exemplo de execução do programa anterior:
E:\data\serge\MSNET\c#\impostos\3>teste em Java
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument nbEnfants incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d f
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :